nym_pool_contract_common/
types.rs

1// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use cosmwasm_schema::cw_serde;
5use cosmwasm_std::{Addr, Coin};
6
7pub type GranterAddress = Addr;
8pub type GranteeAddress = Addr;
9
10pub use grants::*;
11pub use query_responses::*;
12
13#[cw_serde]
14pub struct TransferRecipient {
15    pub recipient: String,
16    pub amount: Coin,
17}
18
19pub mod grants {
20    use crate::utils::ensure_unix_timestamp_not_in_the_past;
21    use crate::{GranteeAddress, GranterAddress, NymPoolContractError};
22    use cosmwasm_schema::cw_serde;
23    use cosmwasm_std::{Addr, Coin, Env, Timestamp, Uint128};
24    use std::cmp::min;
25
26    #[cw_serde]
27    pub struct GranterInformation {
28        // realistically this is always going to be the contract admin,
29        // but let's keep this metadata regardless just in case it ever changes,
30        // such as we create a granter controlled by validator multisig or governance
31        pub created_by: Addr,
32        pub created_at_height: u64,
33    }
34
35    #[cw_serde]
36    pub struct Grant {
37        pub granter: GranterAddress,
38        pub grantee: GranteeAddress,
39        pub granted_at_height: u64,
40        pub allowance: Allowance,
41    }
42
43    #[cw_serde]
44    pub enum Allowance {
45        Basic(BasicAllowance),
46        ClassicPeriodic(ClassicPeriodicAllowance),
47        CumulativePeriodic(CumulativePeriodicAllowance),
48        Delayed(DelayedAllowance),
49    }
50
51    impl From<BasicAllowance> for Allowance {
52        fn from(value: BasicAllowance) -> Self {
53            Allowance::Basic(value)
54        }
55    }
56
57    impl From<ClassicPeriodicAllowance> for Allowance {
58        fn from(value: ClassicPeriodicAllowance) -> Self {
59            Allowance::ClassicPeriodic(value)
60        }
61    }
62
63    impl From<CumulativePeriodicAllowance> for Allowance {
64        fn from(value: CumulativePeriodicAllowance) -> Self {
65            Allowance::CumulativePeriodic(value)
66        }
67    }
68
69    impl From<DelayedAllowance> for Allowance {
70        fn from(value: DelayedAllowance) -> Self {
71            Allowance::Delayed(value)
72        }
73    }
74
75    impl Allowance {
76        pub fn expired(&self, env: &Env) -> bool {
77            self.basic().expired(env)
78        }
79
80        pub fn basic(&self) -> &BasicAllowance {
81            match self {
82                Allowance::Basic(allowance) => allowance,
83                Allowance::ClassicPeriodic(allowance) => &allowance.basic,
84                Allowance::CumulativePeriodic(allowance) => &allowance.basic,
85                Allowance::Delayed(allowance) => &allowance.basic,
86            }
87        }
88
89        pub fn basic_mut(&mut self) -> &mut BasicAllowance {
90            match self {
91                Allowance::Basic(allowance) => allowance,
92                Allowance::ClassicPeriodic(allowance) => &mut allowance.basic,
93                Allowance::CumulativePeriodic(allowance) => &mut allowance.basic,
94                Allowance::Delayed(allowance) => &mut allowance.basic,
95            }
96        }
97
98        pub fn expiration(&self) -> Option<Timestamp> {
99            let expiration_unix = match self {
100                Allowance::Basic(allowance) => allowance.expiration_unix_timestamp,
101                Allowance::ClassicPeriodic(allowance) => allowance.basic.expiration_unix_timestamp,
102                Allowance::CumulativePeriodic(allowance) => {
103                    allowance.basic.expiration_unix_timestamp
104                }
105                Allowance::Delayed(allowance) => allowance.basic.expiration_unix_timestamp,
106            };
107
108            expiration_unix.map(Timestamp::from_seconds)
109        }
110
111        /// Perform validation of a new grant that's to be created
112        pub fn validate_new(&self, env: &Env, denom: &str) -> Result<(), NymPoolContractError> {
113            // 1. perform validation on the inner, basic, allowance
114            self.basic().validate(env, denom)?;
115
116            // 2. perform additional validation specific to each variant
117            match self {
118                // we already validated basic allowance
119                Allowance::Basic(_) => Ok(()),
120                Allowance::ClassicPeriodic(allowance) => allowance.validate_new_inner(denom),
121                Allowance::CumulativePeriodic(allowance) => allowance.validate_new_inner(denom),
122                Allowance::Delayed(allowance) => allowance.validate_new_inner(env),
123            }
124        }
125
126        /// Updates initial state of this allowance settings things such as period reset timestamps.
127        pub fn set_initial_state(&mut self, env: &Env) {
128            match self {
129                // nothing to do for the basic allowance
130                Allowance::Basic(_) => {}
131                Allowance::ClassicPeriodic(allowance) => allowance.set_initial_state(env),
132                Allowance::CumulativePeriodic(allowance) => allowance.set_initial_state(env),
133                // nothing to do for the delayed allowance
134                Allowance::Delayed(_) => {}
135            }
136        }
137
138        pub fn try_update_state(&mut self, env: &Env) {
139            match self {
140                // nothing to do for the basic allowance
141                Allowance::Basic(_) => {}
142                Allowance::ClassicPeriodic(allowance) => allowance.try_update_state(env),
143                Allowance::CumulativePeriodic(allowance) => allowance.try_update_state(env),
144                // nothing to do for the delayed allowance
145                Allowance::Delayed(_) => {}
146            }
147        }
148
149        pub fn within_spendable_limits(&self, amount: &Coin) -> bool {
150            match self {
151                Allowance::Basic(allowance) => allowance.within_spendable_limits(amount),
152                Allowance::ClassicPeriodic(allowance) => allowance.within_spendable_limits(amount),
153                Allowance::CumulativePeriodic(allowance) => {
154                    allowance.within_spendable_limits(amount)
155                }
156                Allowance::Delayed(allowance) => allowance.within_spendable_limits(amount),
157            }
158        }
159
160        // check whether given the current allowance state, the provided amount could be spent
161        // note: it's responsibility of the caller to call `try_update_state` before the call.
162        pub fn ensure_can_spend(
163            &self,
164            env: &Env,
165            amount: &Coin,
166        ) -> Result<(), NymPoolContractError> {
167            match self {
168                Allowance::Basic(allowance) => allowance.ensure_can_spend(env, amount),
169                Allowance::ClassicPeriodic(allowance) => allowance.ensure_can_spend(env, amount),
170                Allowance::CumulativePeriodic(allowance) => allowance.ensure_can_spend(env, amount),
171                Allowance::Delayed(allowance) => allowance.ensure_can_spend(env, amount),
172            }
173        }
174
175        pub fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
176            self.try_update_state(env);
177
178            match self {
179                Allowance::Basic(allowance) => allowance.try_spend(env, amount),
180                Allowance::ClassicPeriodic(allowance) => allowance.try_spend(env, amount),
181                Allowance::CumulativePeriodic(allowance) => allowance.try_spend(env, amount),
182                Allowance::Delayed(allowance) => allowance.try_spend(env, amount),
183            }
184        }
185
186        pub fn increase_spend_limit(&mut self, amount: Uint128) {
187            if let Some(ref mut limit) = self.basic_mut().spend_limit {
188                limit.amount += amount
189            }
190        }
191
192        pub fn is_used_up(&self) -> bool {
193            let Some(ref limit) = self.basic().spend_limit else {
194                return false;
195            };
196            limit.amount.is_zero()
197        }
198    }
199
200    /// BasicAllowance is an allowance with a one-time grant of coins
201    /// that optionally expires. The grantee can use up to SpendLimit to cover fees.
202    #[cw_serde]
203    pub struct BasicAllowance {
204        /// spend_limit specifies the maximum amount of coins that can be spent
205        /// by this allowance and will be updated as coins are spent. If it is
206        /// empty, there is no spend limit and any amount of coins can be spent.
207        pub spend_limit: Option<Coin>,
208
209        /// expiration specifies an optional time when this allowance expires
210        pub expiration_unix_timestamp: Option<u64>,
211    }
212
213    impl BasicAllowance {
214        pub fn unlimited() -> BasicAllowance {
215            BasicAllowance {
216                spend_limit: None,
217                expiration_unix_timestamp: None,
218            }
219        }
220
221        pub fn validate(&self, env: &Env, denom: &str) -> Result<(), NymPoolContractError> {
222            // expiration shouldn't be in the past.
223            if let Some(expiration) = self.expiration_unix_timestamp {
224                ensure_unix_timestamp_not_in_the_past(expiration, env)?;
225            }
226
227            // if spend limit is set, it must use the same denomination as the underlying pool
228            if let Some(ref spend_limit) = self.spend_limit {
229                if spend_limit.denom != denom {
230                    return Err(NymPoolContractError::InvalidDenom {
231                        expected: denom.to_string(),
232                        got: spend_limit.denom.to_string(),
233                    });
234                }
235
236                if spend_limit.amount.is_zero() {
237                    return Err(NymPoolContractError::ZeroAmount);
238                }
239            }
240
241            Ok(())
242        }
243
244        pub fn expired(&self, env: &Env) -> bool {
245            let Some(expiration) = self.expiration_unix_timestamp else {
246                return false;
247            };
248            let current_unix_timestamp = env.block.time.seconds();
249
250            expiration < current_unix_timestamp
251        }
252
253        fn within_spendable_limits(&self, amount: &Coin) -> bool {
254            let Some(ref spend_limit) = self.spend_limit else {
255                // if there's no spend limit then whatever the amount is, it's spendable
256                return true;
257            };
258
259            spend_limit.amount >= amount.amount
260        }
261
262        fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
263            if self.expired(env) {
264                return Err(NymPoolContractError::GrantExpired);
265            }
266            if !self.within_spendable_limits(amount) {
267                return Err(NymPoolContractError::SpendingAboveAllowance);
268            }
269            Ok(())
270        }
271
272        fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
273            self.ensure_can_spend(env, amount)?;
274
275            if let Some(ref mut spend_limit) = self.spend_limit {
276                spend_limit.amount -= amount.amount;
277            }
278            Ok(())
279        }
280    }
281
282    /// ClassicPeriodicAllowance extends BasicAllowance to allow for both a maximum cap,
283    /// as well as a limit per time period.
284    #[cw_serde]
285    pub struct ClassicPeriodicAllowance {
286        /// basic specifies a struct of `BasicAllowance`
287        pub basic: BasicAllowance,
288
289        /// period_duration_secs specifies the time duration in which period_spend_limit coins can
290        /// be spent before that allowance is reset
291        pub period_duration_secs: u64,
292
293        /// period_spend_limit specifies the maximum number of coins that can be spent
294        /// in the period
295        pub period_spend_limit: Coin,
296
297        /// period_can_spend is the number of coins left to be spent before the period_reset time
298        // set by the contract during initialisation of the grant
299        #[serde(default)]
300        pub period_can_spend: Option<Coin>,
301
302        /// period_reset is the time at which this period resets and a new one begins,
303        /// it is calculated from the start time of the first transaction after the
304        /// last period ended
305        // set by the contract during initialisation of the grant
306        #[serde(default)]
307        pub period_reset_unix_timestamp: u64,
308    }
309
310    impl ClassicPeriodicAllowance {
311        pub(super) fn validate_new_inner(&self, denom: &str) -> Result<(), NymPoolContractError> {
312            // period duration shouldn't be zero
313            if self.period_duration_secs == 0 {
314                return Err(NymPoolContractError::ZeroAllowancePeriod);
315            }
316
317            // the denom for period spend limit must match the expected value
318            if self.period_spend_limit.denom != denom {
319                return Err(NymPoolContractError::InvalidDenom {
320                    expected: denom.to_string(),
321                    got: self.period_spend_limit.denom.to_string(),
322                });
323            }
324
325            if self.period_spend_limit.amount.is_zero() {
326                return Err(NymPoolContractError::ZeroAmount);
327            }
328
329            // if the basic spend limit is set, the period spend limit cannot be larger than it
330            if let Some(ref basic_limit) = self.basic.spend_limit {
331                if basic_limit.amount < self.period_spend_limit.amount {
332                    return Err(NymPoolContractError::PeriodicGrantOverSpendLimit {
333                        periodic: self.period_spend_limit.clone(),
334                        total_limit: basic_limit.clone(),
335                    });
336                }
337            }
338
339            Ok(())
340        }
341
342        /// The value that can be spent in the period is the lesser of the basic spend limit
343        /// and the period spend limit
344        ///
345        /// ```go
346        ///    if _, isNeg := a.Basic.SpendLimit.SafeSub(a.PeriodSpendLimit...); isNeg && !a.Basic.SpendLimit.Empty() {
347        ///        a.PeriodCanSpend = a.Basic.SpendLimit
348        ///    } else {
349        ///        a.PeriodCanSpend = a.PeriodSpendLimit
350        ///    }
351        /// ```
352        fn determine_period_can_spend(&self) -> Coin {
353            let Some(ref basic_limit) = self.basic.spend_limit else {
354                // if there's no spend limit, there's nothing to compare against
355                return self.period_spend_limit.clone();
356            };
357
358            if basic_limit.amount < self.period_spend_limit.amount {
359                basic_limit.clone()
360            } else {
361                self.period_spend_limit.clone()
362            }
363        }
364
365        pub(super) fn set_initial_state(&mut self, env: &Env) {
366            self.try_update_state(env);
367        }
368
369        /// try_update_state will check if the period_reset_unix_timestamp has been hit. If not, it is a no-op.
370        /// If we hit the reset period, it will top up the period_can_spend amount to
371        /// min(period_spend_limit, basic.spend_limit) so it is never more than the maximum allowed.
372        /// It will also update the period_reset_unix_timestamp.
373        ///
374        /// If we are within one period, it will update from the
375        /// last period_reset (eg. if you always do one tx per day, it will always reset the same time)
376        /// If we are more than one period out (eg. no activity in a week), reset is one period from the execution of this method
377        pub fn try_update_state(&mut self, env: &Env) {
378            if env.block.time.seconds() < self.period_reset_unix_timestamp {
379                // we haven't yet reached the reset time
380                return;
381            }
382            self.period_can_spend = Some(self.determine_period_can_spend());
383
384            // If we are within the period, step from expiration (eg. if you always do one tx per day,
385            // it will always reset the same time)
386            // If we are more then one period out (eg. no activity in a week),
387            // reset is one period from this time
388            self.period_reset_unix_timestamp += self.period_duration_secs;
389            if env.block.time.seconds() > self.period_duration_secs {
390                self.period_reset_unix_timestamp =
391                    env.block.time.seconds() + self.period_duration_secs;
392            }
393        }
394
395        fn within_spendable_limits(&self, amount: &Coin) -> bool {
396            let Some(ref available) = self.period_can_spend else {
397                return false;
398            };
399            available.amount >= amount.amount
400        }
401
402        fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
403            if self.basic.expired(env) {
404                return Err(NymPoolContractError::GrantExpired);
405            }
406            if !self.within_spendable_limits(amount) {
407                return Err(NymPoolContractError::SpendingAboveAllowance);
408            }
409            Ok(())
410        }
411
412        fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
413            self.ensure_can_spend(env, amount)?;
414
415            // deduct from both the current period and the max amount
416            if let Some(ref mut spend_limit) = self.basic.spend_limit {
417                spend_limit.amount -= amount.amount;
418            }
419
420            // SAFETY: initial `period_can_spend` value is always unconditionally set by the contract during
421            // grant creation
422            #[allow(clippy::unwrap_used)]
423            let period_can_spend = self.period_can_spend.as_mut().unwrap();
424            period_can_spend.amount -= amount.amount;
425
426            Ok(())
427        }
428    }
429
430    #[cw_serde]
431    pub struct CumulativePeriodicAllowance {
432        /// basic specifies a struct of `BasicAllowance`
433        pub basic: BasicAllowance,
434
435        /// period_duration_secs specifies the time duration in which spendable coins can
436        /// be spent before that allowance is incremented
437        pub period_duration_secs: u64,
438
439        /// period_grant specifies the maximum number of coins that is granted per period
440        pub period_grant: Coin,
441
442        /// accumulation_limit is the maximum value the grants and accumulate to
443        pub accumulation_limit: Option<Coin>,
444
445        /// spendable is the number of coins left to be spent before additional grant is applied
446        // set by the contract during initialisation of the grant
447        #[serde(default)]
448        pub spendable: Option<Coin>,
449
450        /// last_grant_applied is the time at which last transaction associated with this allowance
451        /// has been sent and `spendable` value has been adjusted
452        // set by the contract during initialisation of the grant
453        #[serde(default)]
454        pub last_grant_applied_unix_timestamp: u64,
455    }
456
457    impl CumulativePeriodicAllowance {
458        pub(super) fn validate_new_inner(&self, denom: &str) -> Result<(), NymPoolContractError> {
459            // period duration shouldn't be zero
460            if self.period_duration_secs == 0 {
461                return Err(NymPoolContractError::ZeroAllowancePeriod);
462            }
463
464            // the denom for period grant must match the expected value
465            if self.period_grant.denom != denom {
466                return Err(NymPoolContractError::InvalidDenom {
467                    expected: denom.to_string(),
468                    got: self.period_grant.denom.to_string(),
469                });
470            }
471
472            if self.period_grant.amount.is_zero() {
473                return Err(NymPoolContractError::ZeroAmount);
474            }
475
476            // the period grant must not be larger than the total spend limit, if set
477            if let Some(ref basic_limit) = self.basic.spend_limit {
478                if basic_limit.amount < self.period_grant.amount {
479                    return Err(NymPoolContractError::PeriodicGrantOverSpendLimit {
480                        periodic: self.period_grant.clone(),
481                        total_limit: basic_limit.clone(),
482                    });
483                }
484            }
485
486            if let Some(ref accumulation_limit) = self.accumulation_limit {
487                // if set, the accumulation limit must not be smaller than the period grant
488                if accumulation_limit.amount < self.period_grant.amount {
489                    return Err(NymPoolContractError::AccumulationBelowGrantAmount {
490                        accumulation: accumulation_limit.clone(),
491                        periodic_grant: self.period_grant.clone(),
492                    });
493                }
494
495                // if set, the denom for accumulation limit must match the expected value
496                if accumulation_limit.denom != denom {
497                    return Err(NymPoolContractError::InvalidDenom {
498                        expected: denom.to_string(),
499                        got: accumulation_limit.denom.to_string(),
500                    });
501                }
502
503                // if set, the accumulation limit must not be larger than the total spend limit
504                if let Some(ref basic_limit) = self.basic.spend_limit {
505                    if basic_limit.amount < accumulation_limit.amount {
506                        return Err(NymPoolContractError::AccumulationOverSpendLimit {
507                            accumulation: accumulation_limit.clone(),
508                            total_limit: basic_limit.clone(),
509                        });
510                    }
511                }
512            }
513
514            Ok(())
515        }
516
517        pub(super) fn set_initial_state(&mut self, env: &Env) {
518            self.last_grant_applied_unix_timestamp = env.block.time.seconds();
519
520            // initially we can spend equivalent of a single grant
521            self.spendable = Some(self.period_grant.clone())
522        }
523
524        #[inline]
525        fn missed_periods(&self, env: &Env) -> u64 {
526            (env.block.time.seconds() - self.last_grant_applied_unix_timestamp)
527                % self.period_duration_secs
528        }
529
530        /// The value that can be spent is the last of the basic spend limit, the accumulation limit
531        /// and number of missed periods multiplied by the period grant
532        fn determine_spendable(&self, env: &Env) -> Coin {
533            // SAFETY: initial `spendable` value is always unconditionally set by the contract during
534            // grant creation
535            #[allow(clippy::unwrap_used)]
536            let spendable = self.spendable.as_ref().unwrap();
537
538            let missed_periods = self.missed_periods(env);
539            let mut max_spendable = spendable.clone();
540            max_spendable.amount += Uint128::new(missed_periods as u128) * self.period_grant.amount;
541
542            match (&self.basic.spend_limit, &self.accumulation_limit) {
543                (Some(spend_limit), Some(accumulation_limit)) => {
544                    let limit = min(spend_limit.amount, accumulation_limit.amount);
545                    let amount = min(limit, max_spendable.amount);
546                    Coin::new(amount, max_spendable.denom)
547                }
548                (None, Some(accumulation_limit)) => {
549                    let amount = min(accumulation_limit.amount, max_spendable.amount);
550                    Coin::new(amount, max_spendable.denom)
551                }
552                (Some(spend_limit), None) => {
553                    let amount = min(spend_limit.amount, max_spendable.amount);
554                    Coin::new(amount, max_spendable.denom)
555                }
556                (None, None) => max_spendable,
557            }
558        }
559
560        /// try_update_state will check if we've rolled over into the next grant period. If not, it is a no-op.
561        /// If we hit the next period, it will top up the spendable amount to
562        /// min(accumulation_limit, basic.spend_limit, spendable + period_grant * num_missed_periods) so it is never more than the maximum allowed.
563        /// It will also update the last_grant_applied_unix_timestamp.
564        pub fn try_update_state(&mut self, env: &Env) {
565            let missed_periods = self.missed_periods(env);
566
567            if missed_periods == 0 {
568                // we haven't yet reached the next grant time
569                return;
570            }
571
572            self.spendable = Some(self.determine_spendable(env))
573        }
574
575        fn within_spendable_limits(&self, amount: &Coin) -> bool {
576            let Some(ref available) = self.spendable else {
577                return false;
578            };
579            available.amount >= amount.amount
580        }
581
582        fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
583            if self.basic.expired(env) {
584                return Err(NymPoolContractError::GrantExpired);
585            }
586            if !self.within_spendable_limits(amount) {
587                return Err(NymPoolContractError::SpendingAboveAllowance);
588            }
589            Ok(())
590        }
591
592        fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
593            self.ensure_can_spend(env, amount)?;
594
595            // deduct from both the current period and the max amount
596            if let Some(ref mut spend_limit) = self.basic.spend_limit {
597                spend_limit.amount -= amount.amount;
598            }
599
600            // SAFETY: initial `spendable` value is always unconditionally set by the contract during
601            // grant creation
602            #[allow(clippy::unwrap_used)]
603            let spendable = self.spendable.as_mut().unwrap();
604            spendable.amount -= amount.amount;
605
606            Ok(())
607        }
608    }
609
610    /// Create a grant to allow somebody to withdraw from the pool only after the specified time.
611    /// For example, we could create a grant for mixnet rewarding/testing/etc
612    /// However, if the required work has not been completed, the grant could be revoked before it's withdrawn
613    #[cw_serde]
614    pub struct DelayedAllowance {
615        /// basic specifies a struct of `BasicAllowance`
616        pub basic: BasicAllowance,
617
618        /// available_at specifies when this allowance is going to become usable
619        pub available_at_unix_timestamp: u64,
620    }
621
622    impl DelayedAllowance {
623        pub(super) fn validate_new_inner(&self, env: &Env) -> Result<(), NymPoolContractError> {
624            // available at must be set in the future
625            ensure_unix_timestamp_not_in_the_past(self.available_at_unix_timestamp, env)?;
626
627            // and it must become available before the underlying allowance expires
628            if let Some(expiration) = self.basic.expiration_unix_timestamp {
629                if expiration < self.available_at_unix_timestamp {
630                    return Err(NymPoolContractError::UnattainableDelayedAllowance {
631                        expiration_timestamp: expiration,
632                        available_timestamp: self.available_at_unix_timestamp,
633                    });
634                }
635            }
636
637            Ok(())
638        }
639
640        fn within_spendable_limits(&self, amount: &Coin) -> bool {
641            self.basic.within_spendable_limits(amount)
642        }
643
644        fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
645            if self.basic.expired(env) {
646                return Err(NymPoolContractError::GrantExpired);
647            }
648            if !self.within_spendable_limits(amount) {
649                return Err(NymPoolContractError::SpendingAboveAllowance);
650            }
651            if self.available_at_unix_timestamp < env.block.time.seconds() {
652                return Err(NymPoolContractError::GrantNotYetAvailable {
653                    available_at_timestamp: self.available_at_unix_timestamp,
654                });
655            }
656
657            Ok(())
658        }
659
660        fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
661            self.ensure_can_spend(env, amount)?;
662
663            if let Some(ref mut spend_limit) = self.basic.spend_limit {
664                spend_limit.amount -= amount.amount;
665            }
666
667            Ok(())
668        }
669    }
670}
671
672pub mod query_responses {
673    use crate::{Grant, GranteeAddress, GranterAddress, GranterInformation};
674    use cosmwasm_schema::cw_serde;
675    use cosmwasm_std::Coin;
676
677    #[cw_serde]
678    pub struct AvailableTokensResponse {
679        pub available: Coin,
680    }
681
682    #[cw_serde]
683    pub struct TotalLockedTokensResponse {
684        pub locked: Coin,
685    }
686
687    #[cw_serde]
688    pub struct LockedTokensResponse {
689        pub grantee: GranteeAddress,
690
691        pub locked: Option<Coin>,
692    }
693
694    #[cw_serde]
695    pub struct GrantInformation {
696        pub grant: Grant,
697        pub expired: bool,
698    }
699
700    #[cw_serde]
701    pub struct GrantResponse {
702        pub grantee: GranteeAddress,
703        pub grant: Option<GrantInformation>,
704    }
705
706    #[cw_serde]
707    pub struct GranterResponse {
708        pub granter: GranterAddress,
709        pub information: Option<GranterInformation>,
710    }
711
712    #[cw_serde]
713    pub struct GrantsPagedResponse {
714        pub grants: Vec<GrantInformation>,
715        pub start_next_after: Option<String>,
716    }
717
718    #[cw_serde]
719    pub struct GranterDetails {
720        pub granter: GranterAddress,
721        pub information: GranterInformation,
722    }
723
724    impl From<(GranterAddress, GranterInformation)> for GranterDetails {
725        fn from((granter, information): (GranterAddress, GranterInformation)) -> Self {
726            GranterDetails {
727                granter,
728                information,
729            }
730        }
731    }
732
733    #[cw_serde]
734    pub struct GrantersPagedResponse {
735        pub granters: Vec<GranterDetails>,
736        pub start_next_after: Option<String>,
737    }
738
739    #[cw_serde]
740    pub struct LockedTokens {
741        pub grantee: GranteeAddress,
742        pub locked: Coin,
743    }
744
745    #[cw_serde]
746    pub struct LockedTokensPagedResponse {
747        pub locked: Vec<LockedTokens>,
748        pub start_next_after: Option<String>,
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use super::*;
755    use cosmwasm_std::{Uint128, coin};
756
757    const TEST_DENOM: &str = "unym";
758
759    fn mock_basic_allowance() -> BasicAllowance {
760        BasicAllowance {
761            spend_limit: Some(coin(100000, TEST_DENOM)),
762            expiration_unix_timestamp: Some(1643652000),
763        }
764    }
765
766    fn mock_classic_periodic_allowance() -> ClassicPeriodicAllowance {
767        ClassicPeriodicAllowance {
768            basic: mock_basic_allowance(),
769            period_duration_secs: 10,
770            period_spend_limit: coin(1000, TEST_DENOM),
771            period_can_spend: None,
772            period_reset_unix_timestamp: 0,
773        }
774    }
775
776    fn mock_cumulative_periodic_allowance() -> CumulativePeriodicAllowance {
777        CumulativePeriodicAllowance {
778            basic: mock_basic_allowance(),
779            period_duration_secs: 10,
780            period_grant: coin(1000, TEST_DENOM),
781            accumulation_limit: Some(coin(10000, TEST_DENOM)),
782            spendable: None,
783            last_grant_applied_unix_timestamp: 0,
784        }
785    }
786
787    fn mock_delayed_allowance() -> DelayedAllowance {
788        DelayedAllowance {
789            basic: mock_basic_allowance(),
790            available_at_unix_timestamp: 1643650000,
791        }
792    }
793
794    #[test]
795    fn increasing_spend_limit() {
796        // no-op if there's no limit
797        let mut basic = mock_basic_allowance();
798        basic.spend_limit = None;
799        let mut basic = Allowance::Basic(basic);
800
801        let mut classic = mock_classic_periodic_allowance();
802        classic.basic.spend_limit = None;
803        let mut classic = Allowance::ClassicPeriodic(classic);
804
805        let mut cumulative = mock_cumulative_periodic_allowance();
806        cumulative.basic.spend_limit = None;
807        let mut cumulative = Allowance::CumulativePeriodic(cumulative);
808
809        let mut delayed = mock_delayed_allowance();
810        delayed.basic.spend_limit = None;
811        let mut delayed = Allowance::Delayed(delayed);
812
813        let basic_og = basic.clone();
814        let classic_og = classic.clone();
815        let cumulative_og = cumulative.clone();
816        let delayed_og = delayed.clone();
817
818        basic.increase_spend_limit(Uint128::new(100));
819        classic.increase_spend_limit(Uint128::new(100));
820        cumulative.increase_spend_limit(Uint128::new(100));
821        delayed.increase_spend_limit(Uint128::new(100));
822
823        assert_eq!(basic, basic_og);
824        assert_eq!(classic, classic_og);
825        assert_eq!(cumulative, cumulative_og);
826        assert_eq!(delayed, delayed_og);
827
828        // adds to spend limit otherwise
829        let limit = coin(1000, TEST_DENOM);
830        let mut basic = mock_basic_allowance();
831        basic.spend_limit = Some(limit.clone());
832        let mut basic = Allowance::Basic(basic);
833
834        let mut classic = mock_classic_periodic_allowance();
835        classic.basic.spend_limit = Some(limit.clone());
836        let mut classic = Allowance::ClassicPeriodic(classic);
837
838        let mut cumulative = mock_cumulative_periodic_allowance();
839        cumulative.basic.spend_limit = Some(limit.clone());
840        let mut cumulative = Allowance::CumulativePeriodic(cumulative);
841
842        let mut delayed = mock_delayed_allowance();
843        delayed.basic.spend_limit = Some(limit.clone());
844        let mut delayed = Allowance::Delayed(delayed);
845
846        basic.increase_spend_limit(Uint128::new(100));
847        classic.increase_spend_limit(Uint128::new(100));
848        cumulative.increase_spend_limit(Uint128::new(100));
849        delayed.increase_spend_limit(Uint128::new(100));
850
851        assert_eq!(
852            basic.basic().spend_limit.as_ref().unwrap().amount,
853            limit.amount + Uint128::new(100)
854        );
855        assert_eq!(
856            classic.basic().spend_limit.as_ref().unwrap().amount,
857            limit.amount + Uint128::new(100)
858        );
859        assert_eq!(
860            cumulative.basic().spend_limit.as_ref().unwrap().amount,
861            limit.amount + Uint128::new(100)
862        );
863        assert_eq!(
864            delayed.basic().spend_limit.as_ref().unwrap().amount,
865            limit.amount + Uint128::new(100)
866        );
867    }
868
869    #[cfg(test)]
870    mod validating_new_allowances {
871        use super::*;
872
873        #[cfg(test)]
874        mod basic_allowance {
875            use super::*;
876            use cosmwasm_std::Timestamp;
877            use cosmwasm_std::testing::mock_env;
878
879            #[test]
880            fn doesnt_allow_expirations_in_the_past() {
881                let mut allowance = mock_basic_allowance();
882
883                let mut env = mock_env();
884
885                // allowance expiration is in the past
886                env.block.time =
887                    Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap() + 1);
888                assert!(allowance.validate(&env, TEST_DENOM).is_err());
889
890                // allowance expiration is equal to the current block time
891                env.block.time =
892                    Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap());
893                assert!(allowance.validate(&env, TEST_DENOM).is_ok());
894
895                // allowance expiration is in the future
896                env.block.time =
897                    Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap() - 1);
898                assert!(allowance.validate(&env, TEST_DENOM).is_ok());
899
900                // no explicit expiration
901                allowance.expiration_unix_timestamp = None;
902                assert!(allowance.validate(&env, TEST_DENOM).is_ok());
903            }
904
905            #[test]
906            fn spend_limit_must_match_expected_denom() {
907                let mut allowance = mock_basic_allowance();
908
909                let env = mock_env();
910
911                // mismatched denom
912                assert!(allowance.validate(&env, "baddenom").is_err());
913
914                // matched denom
915                assert!(allowance.validate(&env, TEST_DENOM).is_ok());
916
917                // no spend limit
918                allowance.spend_limit = None;
919                assert!(allowance.validate(&env, TEST_DENOM).is_ok());
920            }
921
922            #[test]
923            fn spend_limit_must_be_non_zero() {
924                let mut allowance = mock_basic_allowance();
925
926                let env = mock_env();
927
928                // zero amount
929                allowance.spend_limit = Some(coin(0, TEST_DENOM));
930                assert!(allowance.validate(&env, TEST_DENOM).is_err());
931
932                // non-zero amount
933                allowance.spend_limit = Some(coin(69, TEST_DENOM));
934                assert!(allowance.validate(&env, TEST_DENOM).is_ok());
935            }
936        }
937
938        #[cfg(test)]
939        mod classic_periodic_allowance {
940            use super::*;
941            use crate::NymPoolContractError;
942
943            #[test]
944            fn period_duration_must_be_nonzero() {
945                let mut allowance = mock_classic_periodic_allowance();
946
947                allowance.period_duration_secs = 0;
948                assert_eq!(
949                    allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
950                    NymPoolContractError::ZeroAllowancePeriod
951                );
952
953                allowance.period_duration_secs = 1;
954                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
955            }
956
957            #[test]
958            fn spend_limit_must_match_expected_denom() {
959                let allowance = mock_classic_periodic_allowance();
960
961                // mismatched denom
962                assert!(allowance.validate_new_inner("baddenom").is_err());
963
964                // matched denom
965                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
966            }
967
968            #[test]
969            fn spend_limit_must_be_non_zero() {
970                let mut allowance = mock_classic_periodic_allowance();
971
972                // zero amount
973                allowance.period_spend_limit = coin(0, TEST_DENOM);
974                assert!(allowance.validate_new_inner(TEST_DENOM).is_err());
975
976                // non-zero amount
977                allowance.period_spend_limit = coin(69, TEST_DENOM);
978                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
979            }
980
981            #[test]
982            fn period_spend_limit_must_be_smaller_than_total_limit() {
983                let mut allowance = mock_classic_periodic_allowance();
984
985                let total_limit = coin(1000, TEST_DENOM);
986                allowance.basic.spend_limit = Some(total_limit);
987
988                // above total spend limit
989                allowance.period_spend_limit = coin(1001, TEST_DENOM);
990                assert!(matches!(
991                    allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
992                    NymPoolContractError::PeriodicGrantOverSpendLimit { .. }
993                ));
994
995                // below total spend limit
996                allowance.period_spend_limit = coin(999, TEST_DENOM);
997                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
998
999                // equal to total spend limit
1000                allowance.period_spend_limit = coin(1000, TEST_DENOM);
1001                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1002
1003                // no total spend limit
1004                allowance.basic.spend_limit = None;
1005                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1006            }
1007        }
1008
1009        #[cfg(test)]
1010        mod cumulative_periodic_allowance {
1011            use super::*;
1012            use crate::NymPoolContractError;
1013
1014            #[test]
1015            fn period_duration_must_be_nonzero() {
1016                let mut allowance = mock_cumulative_periodic_allowance();
1017
1018                allowance.period_duration_secs = 0;
1019                assert_eq!(
1020                    allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
1021                    NymPoolContractError::ZeroAllowancePeriod
1022                );
1023
1024                allowance.period_duration_secs = 1;
1025                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1026            }
1027
1028            #[test]
1029            fn grant_must_match_expected_denom() {
1030                let allowance = mock_cumulative_periodic_allowance();
1031
1032                // mismatched denom
1033                assert!(allowance.validate_new_inner("baddenom").is_err());
1034
1035                // matched denom
1036                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1037            }
1038
1039            #[test]
1040            fn grant_must_be_non_zero() {
1041                let mut allowance = mock_cumulative_periodic_allowance();
1042
1043                // zero amount
1044                allowance.period_grant = coin(0, TEST_DENOM);
1045                assert!(allowance.validate_new_inner(TEST_DENOM).is_err());
1046
1047                // non-zero amount
1048                allowance.period_grant = coin(69, TEST_DENOM);
1049                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1050            }
1051
1052            #[test]
1053            fn grant_amount_must_be_smaller_than_total_limit() {
1054                let mut allowance = mock_cumulative_periodic_allowance();
1055
1056                let total_limit = coin(1000, TEST_DENOM);
1057                allowance.basic.spend_limit = Some(total_limit);
1058                allowance.accumulation_limit = None;
1059
1060                // above total spend limit
1061                allowance.period_grant = coin(1001, TEST_DENOM);
1062                assert!(matches!(
1063                    allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
1064                    NymPoolContractError::PeriodicGrantOverSpendLimit { .. }
1065                ));
1066
1067                // below total spend limit
1068                allowance.period_grant = coin(999, TEST_DENOM);
1069                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1070
1071                // equal to total spend limit
1072                allowance.period_grant = coin(1000, TEST_DENOM);
1073                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1074
1075                // no total spend limit
1076                allowance.basic.spend_limit = None;
1077                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1078            }
1079
1080            #[test]
1081            fn accumulation_limit_must_be_smaller_than_total_limit() {
1082                let mut allowance = mock_cumulative_periodic_allowance();
1083
1084                let total_limit = coin(1000, TEST_DENOM);
1085                allowance.basic.spend_limit = Some(total_limit.clone());
1086                allowance.period_grant = coin(500, TEST_DENOM);
1087
1088                // above total spend limit
1089                allowance.accumulation_limit = Some(coin(1001, TEST_DENOM));
1090                assert!(matches!(
1091                    allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
1092                    NymPoolContractError::AccumulationOverSpendLimit { .. }
1093                ));
1094
1095                // below total spend limit
1096                allowance.accumulation_limit = Some(coin(999, TEST_DENOM));
1097                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1098
1099                // equal to total spend limit
1100                allowance.accumulation_limit = Some(coin(1000, TEST_DENOM));
1101                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1102
1103                // no total spend limit
1104                allowance.basic.spend_limit = None;
1105                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1106
1107                // no accumulation limit
1108                allowance.accumulation_limit = None;
1109                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1110
1111                // no accumulation limit but with spend limit
1112                allowance.basic.spend_limit = Some(total_limit);
1113                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1114            }
1115
1116            #[test]
1117            fn accumulation_limit_must_not_be_smaller_than_grant_amount() {
1118                let mut allowance = mock_cumulative_periodic_allowance();
1119
1120                let total_limit = coin(1000, TEST_DENOM);
1121                allowance.basic.spend_limit = Some(total_limit);
1122                allowance.period_grant = coin(500, TEST_DENOM);
1123
1124                // below grant amount
1125                allowance.accumulation_limit = Some(coin(499, TEST_DENOM));
1126                assert!(matches!(
1127                    allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
1128                    NymPoolContractError::AccumulationBelowGrantAmount { .. }
1129                ));
1130
1131                // above grant amount
1132                allowance.accumulation_limit = Some(coin(501, TEST_DENOM));
1133                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1134
1135                // equal to grant amount
1136                allowance.accumulation_limit = Some(coin(500, TEST_DENOM));
1137                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1138
1139                // no accumulation limit
1140                allowance.accumulation_limit = None;
1141                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1142            }
1143
1144            #[test]
1145            fn accumulation_limit_must_match_expected_denom() {
1146                let mut allowance = mock_cumulative_periodic_allowance();
1147                allowance.accumulation_limit = Some(coin(1000, "baddenom"));
1148
1149                // mismatched denom
1150                assert!(allowance.validate_new_inner(TEST_DENOM).is_err());
1151
1152                // matched denom
1153                allowance.accumulation_limit = Some(coin(1000, TEST_DENOM));
1154                assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1155            }
1156        }
1157
1158        #[cfg(test)]
1159        mod delayed_allowance {
1160            use super::*;
1161            use cosmwasm_std::Timestamp;
1162            use cosmwasm_std::testing::mock_env;
1163
1164            #[test]
1165            fn doesnt_allow_availability_in_the_past() {
1166                let allowance = mock_delayed_allowance();
1167                let mut env = mock_env();
1168
1169                // availability is in the past
1170                env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp + 1);
1171                assert!(allowance.validate_new_inner(&env).is_err());
1172
1173                // availability is equal to the current block time
1174                env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp);
1175                assert!(allowance.validate_new_inner(&env).is_ok());
1176
1177                // availability is in the future
1178                env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp - 1);
1179                assert!(allowance.validate_new_inner(&env).is_ok());
1180            }
1181
1182            #[test]
1183            fn must_have_available_before_allowance_expiration() {
1184                let mut allowance = mock_delayed_allowance();
1185                let mut env = mock_env();
1186                env.block.time = Timestamp::from_seconds(100);
1187                allowance.basic.expiration_unix_timestamp = Some(1000);
1188
1189                // after expiration
1190                allowance.available_at_unix_timestamp = 1001;
1191                assert!(allowance.validate_new_inner(&env).is_err());
1192
1193                // equal to expiration
1194                allowance.available_at_unix_timestamp = 1000;
1195                assert!(allowance.validate_new_inner(&env).is_ok());
1196
1197                // before expiration
1198                allowance.available_at_unix_timestamp = 999;
1199                assert!(allowance.validate_new_inner(&env).is_ok());
1200
1201                // with no explicit expiration
1202                allowance.basic.expiration_unix_timestamp = None;
1203                assert!(allowance.validate_new_inner(&env).is_ok());
1204            }
1205        }
1206    }
1207
1208    #[cfg(test)]
1209    mod setting_initial_state {
1210        use super::*;
1211        use cosmwasm_std::testing::mock_env;
1212
1213        #[test]
1214        fn basic_allowance() {
1215            let mut basic = Allowance::Basic(mock_basic_allowance());
1216
1217            let og = basic.clone();
1218
1219            // this is a no-op
1220            let env = mock_env();
1221            basic.set_initial_state(&env);
1222            assert_eq!(basic, og);
1223        }
1224
1225        #[test]
1226        fn classic_periodic_allowance() {
1227            let mut inner = mock_classic_periodic_allowance();
1228            let mut cumulative = Allowance::ClassicPeriodic(inner.clone());
1229
1230            let env = mock_env();
1231
1232            let mut expected = inner.clone();
1233
1234            // sets the spendable amount to min(basic_limit, period_limit)
1235            expected.period_can_spend = Some(expected.period_spend_limit.clone());
1236
1237            // set period reset to current block time + period duration
1238            expected.period_reset_unix_timestamp =
1239                env.block.time.seconds() + expected.period_duration_secs;
1240
1241            inner.set_initial_state(&env);
1242            assert_eq!(inner, expected);
1243
1244            cumulative.set_initial_state(&env);
1245            assert_eq!(cumulative, Allowance::ClassicPeriodic(inner));
1246        }
1247
1248        #[test]
1249        fn cumulative_periodic_allowance() {
1250            let mut inner = mock_cumulative_periodic_allowance();
1251            let mut cumulative = Allowance::CumulativePeriodic(inner.clone());
1252
1253            let env = mock_env();
1254
1255            // sets the last applied grant to current time and spendable to a single grant value
1256            let mut expected = inner.clone();
1257            expected.last_grant_applied_unix_timestamp = env.block.time.seconds();
1258            expected.spendable = Some(expected.period_grant.clone());
1259
1260            inner.set_initial_state(&env);
1261            assert_eq!(inner, expected);
1262
1263            cumulative.set_initial_state(&env);
1264            assert_eq!(cumulative, Allowance::CumulativePeriodic(inner));
1265        }
1266
1267        #[test]
1268        fn delayed_allowance() {
1269            let mut delayed = Allowance::Delayed(mock_delayed_allowance());
1270
1271            let og = delayed.clone();
1272
1273            // this is a no-op
1274            let env = mock_env();
1275            delayed.set_initial_state(&env);
1276            assert_eq!(delayed, og);
1277        }
1278    }
1279}