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: Curve,
24 start_time: Timestamp,
25
26 pub status: Status,
27 pub recipient: Addr,
28 pub denom: CheckedDenom,
29
30 pub claimed: Uint128,
32 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: Uint128,
50 },
51}
52
53#[cw_serde]
54pub enum Schedule {
55 SaturatingLinear,
57 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 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 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 owner_withdrawable + (vesting.total() - vesting.claimed) - staked
151 }
152 }
153 }
154
155 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 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 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 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 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 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 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 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 pub fn query_stake(&self, storage: &dyn Storage, q: StakeTrackerQuery) -> StdResult<Binary> {
398 self.staking.query(storage, q)
399 }
400
401 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 pub fn total(&self) -> Uint128 {
434 Uint128::new(self.vested.range().1)
435 }
436
437 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 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 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 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()?; 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}