cw_vesting/
vesting.rs

1use std::cmp::min;
2
3use cosmwasm_schema::cw_serde;
4#[cfg(feature = "staking")]
5use cosmwasm_std::DistributionMsg;
6use cosmwasm_std::{Addr, Binary, CosmosMsg, StdResult, Storage, Timestamp, Uint128, Uint64};
7use cw_denom::CheckedDenom;
8use cw_storage_plus::Item;
9use wynd_utils::{Curve, PiecewiseLinear, SaturatingLinear};
10
11use cw_stake_tracker::{StakeTracker, StakeTrackerQuery};
12
13use crate::error::ContractError;
14
15pub struct Payment<'a> {
16    vesting: Item<'a, Vest>,
17    staking: StakeTracker<'a>,
18}
19
20#[cw_serde]
21pub struct Vest {
22    /// vested(t), where t is seconds since start_time.
23    vested: Curve,
24    start_time: Timestamp,
25
26    pub status: Status,
27    pub recipient: Addr,
28    pub denom: CheckedDenom,
29
30    /// The number of tokens that have been claimed by the vest receiver.
31    pub claimed: Uint128,
32    /// The number of tokens that have been slashed while staked by
33    /// the vest receiver. Slashed tokens count against the number of
34    /// tokens the receiver is entitled to.
35    pub slashed: Uint128,
36
37    pub title: String,
38    pub description: Option<String>,
39}
40
41#[cw_serde]
42pub enum Status {
43    Unfunded,
44    Funded,
45    Canceled {
46        /// owner_withdrawable(t). This is monotonically decreasing and
47        /// will be zero once the owner has completed withdrawing
48        /// their funds.
49        owner_withdrawable: Uint128,
50    },
51}
52
53#[cw_serde]
54pub enum Schedule {
55    /// Vests linearally from `0` to `total`.
56    SaturatingLinear,
57    /// Vests by linearally interpolating between the provided
58    /// (seconds, amount) points. The first amount must be zero and
59    /// the last amount the total vesting amount. `seconds` are
60    /// seconds since the vest start time.
61    ///
62    /// There is a problem in the underlying Curve library that
63    /// doesn't allow zero start values, so the first value of
64    /// `seconds` must be > 1. To start at a particular time (if you
65    /// need that level of percision), subtract one from the true
66    /// start time, and make the first `seconds` value `1`.
67    ///
68    /// <https://github.com/cosmorama/wynddao/pull/4>
69    PiecewiseLinear(Vec<(u64, Uint128)>),
70}
71
72pub struct VestInit {
73    pub total: Uint128,
74    pub schedule: Schedule,
75    pub start_time: Timestamp,
76    pub duration_seconds: u64,
77    pub denom: CheckedDenom,
78    pub recipient: Addr,
79    pub title: String,
80    pub description: Option<String>,
81}
82
83impl<'a> Payment<'a> {
84    pub const fn new(
85        vesting_prefix: &'a str,
86        staked_prefix: &'a str,
87        validator_prefix: &'a str,
88        cardinality_prefix: &'a str,
89    ) -> Self {
90        Self {
91            vesting: Item::new(vesting_prefix),
92            staking: StakeTracker::new(staked_prefix, validator_prefix, cardinality_prefix),
93        }
94    }
95
96    /// Validates its arguments and initializes the payment. Returns
97    /// the underlying vest.
98    pub fn initialize(
99        &self,
100        storage: &mut dyn Storage,
101        init: VestInit,
102    ) -> Result<Vest, ContractError> {
103        let v = Vest::new(init)?;
104        self.vesting.save(storage, &v)?;
105        Ok(v)
106    }
107
108    pub fn get_vest(&self, storage: &dyn Storage) -> StdResult<Vest> {
109        self.vesting.load(storage)
110    }
111
112    /// calculates the number of liquid tokens avaliable.
113    fn liquid(&self, vesting: &Vest, staked: Uint128) -> Uint128 {
114        match vesting.status {
115            Status::Unfunded => Uint128::zero(),
116            Status::Funded => vesting.total() - vesting.claimed - staked - vesting.slashed,
117            Status::Canceled { owner_withdrawable } => {
118                // On cancelation, all liquid funds are settled and
119                // vesting.total() is set to the amount that has
120                // vested so far. Then, the remaining staked tokens
121                // are divided up between the owner and the vestee so
122                // that the vestee will receive all of their vested
123                // tokens. The following is then made true:
124                //
125                // staked = vesting_owed + owner_withdrawable
126                // staked = (vesting.total - vesting.claimed) + owner_withdrawable
127                //
128                // staked - currently_staked = claimable, as those tokens
129                // have unbonded and become avaliable and you can't
130                // delegate in the cancelled state, so:
131                //
132                // claimable = (vesting.total - vesting.claimed) + owner_withdrawable - currently_staked
133                //
134                // Note that this is slightly simplified, in practice we
135                // maintain:
136                //
137                // owner_withdrawable := owner.total - owner.claimed
138                //
139                // Where owner.total is the initial amount they were
140                // entitled to.
141                //
142                // ## Slashing
143                //
144                // If a slash occurs while the contract is in a
145                // canceled state, the slash amount is deducted from
146                // `owner_withdrawable`. We don't count slashes that
147                // occured during the Funded state as those are
148                // considered when computing `owner_withdrawable`
149                // initially.
150                owner_withdrawable + (vesting.total() - vesting.claimed) - staked
151            }
152        }
153    }
154
155    /// Gets the current number tokens that may be distributed to the
156    /// vestee.
157    pub fn distributable(
158        &self,
159        storage: &dyn Storage,
160        vesting: &Vest,
161        t: Timestamp,
162    ) -> StdResult<Uint128> {
163        let staked = self.staking.total_staked(storage, t)?;
164
165        let liquid = self.liquid(vesting, staked);
166        let claimable = (vesting.vested(t) - vesting.claimed).saturating_sub(vesting.slashed);
167        Ok(min(liquid, claimable))
168    }
169
170    /// Distributes vested tokens. If a specific amount is
171    /// requested, that amount will be distributed, otherwise all
172    /// tokens currently avaliable for distribution will be
173    /// transfered.
174    pub fn distribute(
175        &self,
176        storage: &mut dyn Storage,
177        t: Timestamp,
178        request: Option<Uint128>,
179    ) -> Result<CosmosMsg, ContractError> {
180        let vesting = self.vesting.load(storage)?;
181
182        let distributable = self.distributable(storage, &vesting, t)?;
183        let request = request.unwrap_or(distributable);
184
185        let mut vesting = vesting;
186        vesting.claimed += request;
187        self.vesting.save(storage, &vesting)?;
188
189        if request > distributable || request.is_zero() {
190            Err(ContractError::InvalidWithdrawal {
191                request,
192                claimable: distributable,
193            })
194        } else {
195            Ok(vesting
196                .denom
197                .get_transfer_to_message(&vesting.recipient, request)?)
198        }
199    }
200
201    /// Cancels the vesting payment. The current amount vested becomes
202    /// the total amount that will ever vest, and all staked tokens
203    /// are unbonded. note that canceling does not impact already
204    /// vested tokens.
205    ///
206    /// Upon canceling, the contract will use any liquid tokens in the
207    /// contract to settle pending payments to the vestee, and then
208    /// return the rest to the owner. If there are not enough liquid
209    /// tokens to settle the vestee immediately, the vestee may
210    /// distribute tokens as normal until they have received the
211    /// amount of tokens they are entitled to. The owner may withdraw
212    /// the remaining tokens via the `withdraw_canceled` method.
213    pub fn cancel(
214        &self,
215        storage: &mut dyn Storage,
216        t: Timestamp,
217        owner: &Addr,
218    ) -> Result<Vec<CosmosMsg>, ContractError> {
219        let mut vesting = self.vesting.load(storage)?;
220        if matches!(vesting.status, Status::Canceled { .. }) {
221            Err(ContractError::Cancelled {})
222        } else {
223            let staked = self.staking.total_staked(storage, t)?;
224
225            // Use liquid tokens to settle vestee as much as possible
226            // and return any remaining liquid funds to the owner.
227            let liquid = self.liquid(&vesting, staked);
228            let claimable = (vesting.vested(t) - vesting.claimed).saturating_sub(vesting.slashed);
229            let to_vestee = min(claimable, liquid);
230            let to_owner = liquid - to_vestee;
231
232            vesting.claimed += to_vestee;
233
234            // After cancelation liquid funds are settled, and
235            // the owners entitlement to the staked tokens is all
236            // staked tokens that are not needed to settle the
237            // vestee.
238            let owner_outstanding =
239                staked - (vesting.vested(t) - vesting.claimed).saturating_sub(vesting.slashed);
240
241            vesting.cancel(t, owner_outstanding);
242            self.vesting.save(storage, &vesting)?;
243
244            // As the vest is cancelled, the veste is no longer
245            // entitled to staking rewards that may accure before the
246            // owner has a chance to undelegate from validators. Set
247            // the owner to the reward receiver.
248            let mut msgs = vec![
249                #[cfg(feature = "staking")]
250                DistributionMsg::SetWithdrawAddress {
251                    address: owner.to_string(),
252                }
253                .into(),
254            ];
255
256            if !to_owner.is_zero() {
257                msgs.push(vesting.denom.get_transfer_to_message(owner, to_owner)?);
258            }
259            if !to_vestee.is_zero() {
260                msgs.push(
261                    vesting
262                        .denom
263                        .get_transfer_to_message(&vesting.recipient, to_vestee)?,
264                );
265            }
266
267            Ok(msgs)
268        }
269    }
270
271    pub fn withdraw_canceled_payment(
272        &self,
273        storage: &mut dyn Storage,
274        t: Timestamp,
275        request: Option<Uint128>,
276        owner: &Addr,
277    ) -> Result<CosmosMsg, ContractError> {
278        let vesting = self.vesting.load(storage)?;
279        let staked = self.staking.total_staked(storage, t)?;
280        if let Status::Canceled { owner_withdrawable } = vesting.status {
281            let liquid = self.liquid(&vesting, staked);
282            let claimable = min(liquid, owner_withdrawable);
283            let request = request.unwrap_or(claimable);
284            if request > claimable || request.is_zero() {
285                Err(ContractError::InvalidWithdrawal { request, claimable })
286            } else {
287                let mut vesting = vesting;
288                vesting.status = Status::Canceled {
289                    owner_withdrawable: owner_withdrawable - request,
290                };
291                self.vesting.save(storage, &vesting)?;
292
293                Ok(vesting.denom.get_transfer_to_message(owner, request)?)
294            }
295        } else {
296            Err(ContractError::NotCancelled)
297        }
298    }
299
300    pub fn on_undelegate(
301        &self,
302        storage: &mut dyn Storage,
303        t: Timestamp,
304        validator: String,
305        amount: Uint128,
306        unbonding_duration_seconds: u64,
307    ) -> Result<(), ContractError> {
308        self.staking
309            .on_undelegate(storage, t, validator, amount, unbonding_duration_seconds)?;
310        Ok(())
311    }
312
313    pub fn on_redelegate(
314        &self,
315        storage: &mut dyn Storage,
316        t: Timestamp,
317        src: String,
318        dst: String,
319        amount: Uint128,
320    ) -> StdResult<()> {
321        self.staking.on_redelegate(storage, t, src, dst, amount)?;
322        Ok(())
323    }
324
325    pub fn on_delegate(
326        &self,
327        storage: &mut dyn Storage,
328        t: Timestamp,
329        validator: String,
330        amount: Uint128,
331    ) -> Result<(), ContractError> {
332        self.staking.on_delegate(storage, t, validator, amount)?;
333        Ok(())
334    }
335
336    pub fn set_funded(&self, storage: &mut dyn Storage) -> Result<(), ContractError> {
337        let mut v = self.vesting.load(storage)?;
338        debug_assert!(v.status == Status::Unfunded);
339        v.status = Status::Funded;
340        self.vesting.save(storage, &v)?;
341        Ok(())
342    }
343
344    /// Registers a slash of bonded tokens at time `t`.
345    ///
346    /// Invariants:
347    ///   1. The slash did indeed occur.
348    ///
349    /// Checking that these invariants are true is the responsibility
350    /// of the caller.
351    pub fn register_slash(
352        &self,
353        storage: &mut dyn Storage,
354        validator: String,
355        t: Timestamp,
356        amount: Uint128,
357        during_unbonding: bool,
358    ) -> Result<(), ContractError> {
359        if amount.is_zero() {
360            Err(ContractError::NoSlash)
361        } else {
362            let mut vest = self.vesting.load(storage)?;
363            match vest.status {
364                Status::Unfunded => return Err(ContractError::UnfundedSlash),
365                Status::Funded => vest.slashed += amount,
366                Status::Canceled { owner_withdrawable } => {
367                    // if the owner withdraws, then registeres the
368                    // slash as having happened (i.e. they are being
369                    // mean), then the slash will be forced to spill
370                    // over into the receivers claimable amount.
371                    if amount > owner_withdrawable {
372                        vest.status = Status::Canceled {
373                            owner_withdrawable: Uint128::zero(),
374                        };
375                        vest.slashed += amount - owner_withdrawable;
376                    } else {
377                        vest.status = Status::Canceled {
378                            owner_withdrawable: owner_withdrawable - amount,
379                        }
380                    }
381                }
382            };
383            self.vesting.save(storage, &vest)?;
384            if during_unbonding {
385                self.staking
386                    .on_unbonding_slash(storage, t, validator, amount)?;
387            } else {
388                self.staking
389                    .on_bonded_slash(storage, t, validator, amount)?;
390            }
391            Ok(())
392        }
393    }
394
395    /// Passes a query through to the vest's stake tracker which has
396    /// information about bonded and unbonding token balances.
397    pub fn query_stake(&self, storage: &dyn Storage, q: StakeTrackerQuery) -> StdResult<Binary> {
398        self.staking.query(storage, q)
399    }
400
401    /// Returns the duration of the vesting agreement (not the
402    /// remaining time) in seconds, or `None` if the vest has been cancelled.
403    pub fn duration(&self, storage: &dyn Storage) -> StdResult<Option<Uint64>> {
404        self.vesting.load(storage).map(|v| v.duration())
405    }
406}
407
408impl Vest {
409    pub fn new(init: VestInit) -> Result<Self, ContractError> {
410        if init.total.is_zero() {
411            Err(ContractError::ZeroVest)
412        } else if init.duration_seconds == 0 {
413            Err(ContractError::Instavest)
414        } else {
415            Ok(Self {
416                claimed: Uint128::zero(),
417                slashed: Uint128::zero(),
418                vested: init
419                    .schedule
420                    .into_curve(init.total, init.duration_seconds)?,
421                start_time: init.start_time,
422                denom: init.denom,
423                recipient: init.recipient,
424                status: Status::Unfunded,
425                title: init.title,
426                description: init.description,
427            })
428        }
429    }
430
431    /// Gets the total number of tokens that will vest as part of this
432    /// payment.
433    pub fn total(&self) -> Uint128 {
434        Uint128::new(self.vested.range().1)
435    }
436
437    /// Gets the number of tokens that have vested at `time`.
438    pub fn vested(&self, t: Timestamp) -> Uint128 {
439        let elapsed = t.seconds().saturating_sub(self.start_time.seconds());
440        self.vested.value(elapsed)
441    }
442
443    /// Cancels the current vest. No additional tokens will vest after `t`.
444    pub fn cancel(&mut self, t: Timestamp, owner_withdrawable: Uint128) {
445        debug_assert!(!matches!(self.status, Status::Canceled { .. }));
446
447        self.status = Status::Canceled { owner_withdrawable };
448        self.vested = Curve::Constant { y: self.vested(t) };
449    }
450
451    /// Gets the duration of the vest. For constant curves, `None` is
452    /// returned.
453    pub fn duration(&self) -> Option<Uint64> {
454        let (start, end) = match &self.vested {
455            Curve::Constant { .. } => return None,
456            Curve::SaturatingLinear(SaturatingLinear { min_x, max_x, .. }) => (*min_x, *max_x),
457            Curve::PiecewiseLinear(PiecewiseLinear { steps }) => {
458                (steps[0].0, steps[steps.len() - 1].0)
459            }
460        };
461        Some(Uint64::new(end - start))
462    }
463}
464
465impl Schedule {
466    /// The vesting schedule tracks vested(t), so for a curve to be
467    /// valid:
468    ///
469    /// 1. it must start at 0,
470    /// 2. it must end at total,
471    /// 3. it must never decrease.
472    ///
473    /// Piecewise curves must have at least two steps. One step would
474    /// be a constant vest (why would you want this?).
475    ///
476    /// A schedule is valid if `total` is zero: nothing will ever be
477    /// paid out. Consumers should consider validating that `total` is
478    /// non-zero.
479    pub fn into_curve(self, total: Uint128, duration_seconds: u64) -> Result<Curve, ContractError> {
480        let c = match self {
481            Schedule::SaturatingLinear => {
482                Curve::saturating_linear((0, 0), (duration_seconds, total.u128()))
483            }
484            Schedule::PiecewiseLinear(steps) => {
485                if steps.len() < 2 {
486                    return Err(ContractError::ConstantVest);
487                }
488                Curve::PiecewiseLinear(wynd_utils::PiecewiseLinear { steps })
489            }
490        };
491        c.validate_monotonic_increasing()?; // => max >= curve(t) \forall t
492        let range = c.range();
493        if range != (0, total.u128()) {
494            return Err(ContractError::VestRange {
495                min: Uint128::new(range.0),
496                max: Uint128::new(range.1),
497            });
498        }
499        Ok(c)
500    }
501}