Skip to main content

builder_unlock/
state.rs

1use astroport::common::OwnershipProposal;
2use cosmwasm_schema::cw_serde;
3use cosmwasm_std::{ensure, Addr, StdResult, Storage, Uint128};
4use cw_storage_plus::{Item, Map, SnapshotItem, SnapshotMap, Strategy};
5
6use astroport_governance::builder_unlock::{
7    AllocationParams, AllocationStatus, Config, CreateAllocationParams, Schedule,
8    SimulateWithdrawResponse, State,
9};
10
11use crate::error::ContractError;
12
13/// Stores the contract configuration
14pub const CONFIG: Item<Config> = Item::new("config");
15/// Stores global unlock state such as the total amount of ASTRO tokens still to be distributed
16pub const STATE: SnapshotItem<State> = SnapshotItem::new(
17    "state",
18    "state__checkpoint",
19    "state__changelog",
20    Strategy::EveryBlock,
21);
22/// Allocation parameters for each unlock recipient
23pub const PARAMS: Map<&Addr, AllocationParams> = Map::new("params");
24/// The status of each unlock schedule
25pub const STATUS: SnapshotMap<&Addr, AllocationStatus> = SnapshotMap::new(
26    "status",
27    "status__checkpoint",
28    "status__changelog",
29    Strategy::EveryBlock,
30);
31/// Contains a proposal to change contract ownership
32pub const OWNERSHIP_PROPOSAL: Item<OwnershipProposal> = Item::new("ownership_proposal");
33
34#[cw_serde]
35pub struct Allocation {
36    /// The allocation parameters
37    pub params: AllocationParams,
38    /// The allocation status
39    pub status: AllocationStatus,
40    /// Allocation owner
41    pub user: Addr,
42    /// Current block timestamp
43    pub block_ts: u64,
44}
45
46impl Allocation {
47    pub fn must_load(
48        storage: &dyn Storage,
49        block_ts: u64,
50        user: &Addr,
51    ) -> Result<Self, ContractError> {
52        let params = PARAMS
53            .load(storage, user)
54            .map_err(|_| ContractError::NoAllocation {
55                address: user.to_string(),
56            })?;
57        let status = STATUS.may_load(storage, user)?.unwrap_or_default();
58
59        Ok(Self {
60            params,
61            status,
62            user: user.clone(),
63            block_ts,
64        })
65    }
66
67    pub fn save(self, storage: &mut dyn Storage) -> StdResult<()> {
68        PARAMS.save(storage, &self.user, &self.params)?;
69        STATUS.save(storage, &self.user, &self.status, self.block_ts)
70    }
71
72    pub fn new_allocation(
73        storage: &mut dyn Storage,
74        block_ts: u64,
75        user: &Addr,
76        params: CreateAllocationParams,
77    ) -> Result<Self, ContractError> {
78        ensure!(
79            !PARAMS.has(storage, user),
80            ContractError::AllocationExists {
81                user: user.to_string()
82            }
83        );
84
85        params.validate(user.as_str())?;
86
87        Ok(Self {
88            params: AllocationParams {
89                unlock_schedule: params.unlock_schedule,
90                proposed_receiver: None,
91            },
92            status: AllocationStatus {
93                amount: params.amount,
94                astro_withdrawn: Default::default(),
95                unlocked_amount_checkpoint: Default::default(),
96            },
97            user: user.clone(),
98            block_ts,
99        })
100    }
101
102    pub fn withdraw_and_update(&mut self) -> Result<Uint128, ContractError> {
103        ensure!(
104            self.params.proposed_receiver.is_none(),
105            ContractError::WithdrawErrorWhenProposedReceiver {}
106        );
107
108        let SimulateWithdrawResponse { astro_to_withdraw } =
109            self.compute_withdraw_amount(self.block_ts);
110
111        ensure!(
112            !astro_to_withdraw.is_zero(),
113            ContractError::NoUnlockedAstro {}
114        );
115
116        self.status.astro_withdrawn += astro_to_withdraw;
117
118        Ok(astro_to_withdraw)
119    }
120
121    pub fn propose_new_receiver(
122        &mut self,
123        storage: &dyn Storage,
124        new_receiver: &Addr,
125    ) -> Result<(), ContractError> {
126        match &self.params.proposed_receiver {
127            Some(proposed_receiver) => Err(ContractError::ProposedReceiverAlreadySet {
128                proposed_receiver: proposed_receiver.clone(),
129            }),
130            None => {
131                ensure!(
132                    !PARAMS.has(storage, new_receiver),
133                    ContractError::ProposedReceiverAlreadyHasAllocation {}
134                );
135
136                self.params.proposed_receiver = Some(new_receiver.clone());
137
138                Ok(())
139            }
140        }
141    }
142
143    pub fn drop_proposed_receiver(&mut self) -> Result<Addr, ContractError> {
144        match self.params.proposed_receiver.clone() {
145            Some(proposed_receiver) => {
146                self.params.proposed_receiver = None;
147                Ok(proposed_receiver)
148            }
149            None => Err(ContractError::ProposedReceiverNotSet {}),
150        }
151    }
152
153    /// Produces new allocation object for new receiver. Old allocation is removed from state.
154    pub fn claim_allocation(
155        self,
156        storage: &mut dyn Storage,
157        new_receiver: &Addr,
158    ) -> Result<Self, ContractError> {
159        PARAMS.remove(storage, &self.user);
160        STATUS.remove(storage, &self.user, self.block_ts)?;
161
162        Ok(Self {
163            user: new_receiver.clone(),
164            params: AllocationParams {
165                proposed_receiver: None,
166                ..self.params
167            },
168            ..self
169        })
170    }
171
172    /// Computes number of tokens that are now unlocked for a given allocation
173    pub fn compute_unlocked_amount(&self, timestamp: u64) -> Uint128 {
174        let (schedule, unlock_checkpoint, total_amount) = (
175            &self.params.unlock_schedule,
176            self.status.unlocked_amount_checkpoint,
177            self.status.amount,
178        );
179
180        // Tokens haven't begun unlocking
181        if timestamp < schedule.start_time + schedule.cliff {
182            unlock_checkpoint
183        } else if (timestamp < schedule.start_time + schedule.duration) && schedule.duration != 0 {
184            // If percent_at_cliff is set, then this amount should be unlocked at cliff.
185            // The rest of tokens are vested linearly between cliff and end_time
186            let unlocked_amount = if let Some(percent_at_cliff) = schedule.percent_at_cliff {
187                let amount_at_cliff = total_amount * percent_at_cliff;
188
189                amount_at_cliff
190                    + total_amount.saturating_sub(amount_at_cliff).multiply_ratio(
191                        timestamp - schedule.start_time - schedule.cliff,
192                        schedule.duration - schedule.cliff,
193                    )
194            } else {
195                // Tokens unlock linearly between start time and end time
196                total_amount.multiply_ratio(timestamp - schedule.start_time, schedule.duration)
197            };
198
199            if unlocked_amount > unlock_checkpoint {
200                unlocked_amount
201            } else {
202                unlock_checkpoint
203            }
204        }
205        // After end time, all tokens are fully unlocked
206        else {
207            total_amount
208        }
209    }
210
211    /// Computes number of tokens that are withdrawable for a given allocation
212    pub fn compute_withdraw_amount(&self, timestamp: u64) -> SimulateWithdrawResponse {
213        let astro_unlocked = self.compute_unlocked_amount(timestamp);
214
215        // Withdrawal amount is unlocked amount minus the amount already withdrawn
216        SimulateWithdrawResponse {
217            astro_to_withdraw: astro_unlocked - self.status.astro_withdrawn,
218        }
219    }
220
221    pub fn decrease_allocation(&mut self, amount: Uint128) -> Result<(), ContractError> {
222        let unlocked_amount = self.compute_unlocked_amount(self.block_ts);
223        let locked_amount = self.status.amount - unlocked_amount;
224
225        ensure!(
226            locked_amount >= amount,
227            ContractError::InsufficientLockedAmount { locked_amount }
228        );
229
230        self.status.amount = self.status.amount.checked_sub(amount)?;
231        self.status.unlocked_amount_checkpoint = unlocked_amount;
232
233        Ok(())
234    }
235
236    pub fn increase_allocation(&mut self, amount: Uint128) -> Result<(), ContractError> {
237        self.status.amount += amount;
238        Ok(())
239    }
240
241    pub fn update_unlock_schedule(&mut self, new_schedule: &Schedule) -> StdResult<()> {
242        let unlocked_amount_checkpoint = self.compute_unlocked_amount(self.block_ts);
243
244        if unlocked_amount_checkpoint > self.status.unlocked_amount_checkpoint {
245            self.status.unlocked_amount_checkpoint = unlocked_amount_checkpoint;
246        }
247
248        self.params
249            .update_schedule(new_schedule.clone(), self.user.as_str())
250    }
251}