1use 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 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 pub fn validate_new(&self, env: &Env, denom: &str) -> Result<(), NymPoolContractError> {
113 self.basic().validate(env, denom)?;
115
116 match self {
118 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 pub fn set_initial_state(&mut self, env: &Env) {
128 match self {
129 Allowance::Basic(_) => {}
131 Allowance::ClassicPeriodic(allowance) => allowance.set_initial_state(env),
132 Allowance::CumulativePeriodic(allowance) => allowance.set_initial_state(env),
133 Allowance::Delayed(_) => {}
135 }
136 }
137
138 pub fn try_update_state(&mut self, env: &Env) {
139 match self {
140 Allowance::Basic(_) => {}
142 Allowance::ClassicPeriodic(allowance) => allowance.try_update_state(env),
143 Allowance::CumulativePeriodic(allowance) => allowance.try_update_state(env),
144 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 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 #[cw_serde]
203 pub struct BasicAllowance {
204 pub spend_limit: Option<Coin>,
208
209 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 if let Some(expiration) = self.expiration_unix_timestamp {
224 ensure_unix_timestamp_not_in_the_past(expiration, env)?;
225 }
226
227 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 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 #[cw_serde]
285 pub struct ClassicPeriodicAllowance {
286 pub basic: BasicAllowance,
288
289 pub period_duration_secs: u64,
292
293 pub period_spend_limit: Coin,
296
297 #[serde(default)]
300 pub period_can_spend: Option<Coin>,
301
302 #[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 if self.period_duration_secs == 0 {
314 return Err(NymPoolContractError::ZeroAllowancePeriod);
315 }
316
317 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 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 fn determine_period_can_spend(&self) -> Coin {
353 let Some(ref basic_limit) = self.basic.spend_limit else {
354 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 pub fn try_update_state(&mut self, env: &Env) {
378 if env.block.time.seconds() < self.period_reset_unix_timestamp {
379 return;
381 }
382 self.period_can_spend = Some(self.determine_period_can_spend());
383
384 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 if let Some(ref mut spend_limit) = self.basic.spend_limit {
417 spend_limit.amount -= amount.amount;
418 }
419
420 #[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 pub basic: BasicAllowance,
434
435 pub period_duration_secs: u64,
438
439 pub period_grant: Coin,
441
442 pub accumulation_limit: Option<Coin>,
444
445 #[serde(default)]
448 pub spendable: Option<Coin>,
449
450 #[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 if self.period_duration_secs == 0 {
461 return Err(NymPoolContractError::ZeroAllowancePeriod);
462 }
463
464 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 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 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 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 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 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 fn determine_spendable(&self, env: &Env) -> Coin {
533 #[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 pub fn try_update_state(&mut self, env: &Env) {
565 let missed_periods = self.missed_periods(env);
566
567 if missed_periods == 0 {
568 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 if let Some(ref mut spend_limit) = self.basic.spend_limit {
597 spend_limit.amount -= amount.amount;
598 }
599
600 #[allow(clippy::unwrap_used)]
603 let spendable = self.spendable.as_mut().unwrap();
604 spendable.amount -= amount.amount;
605
606 Ok(())
607 }
608 }
609
610 #[cw_serde]
614 pub struct DelayedAllowance {
615 pub basic: BasicAllowance,
617
618 pub available_at_unix_timestamp: u64,
620 }
621
622 impl DelayedAllowance {
623 pub(super) fn validate_new_inner(&self, env: &Env) -> Result<(), NymPoolContractError> {
624 ensure_unix_timestamp_not_in_the_past(self.available_at_unix_timestamp, env)?;
626
627 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 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 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 env.block.time =
887 Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap() + 1);
888 assert!(allowance.validate(&env, TEST_DENOM).is_err());
889
890 env.block.time =
892 Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap());
893 assert!(allowance.validate(&env, TEST_DENOM).is_ok());
894
895 env.block.time =
897 Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap() - 1);
898 assert!(allowance.validate(&env, TEST_DENOM).is_ok());
899
900 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 assert!(allowance.validate(&env, "baddenom").is_err());
913
914 assert!(allowance.validate(&env, TEST_DENOM).is_ok());
916
917 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 allowance.spend_limit = Some(coin(0, TEST_DENOM));
930 assert!(allowance.validate(&env, TEST_DENOM).is_err());
931
932 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 assert!(allowance.validate_new_inner("baddenom").is_err());
963
964 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 allowance.period_spend_limit = coin(0, TEST_DENOM);
974 assert!(allowance.validate_new_inner(TEST_DENOM).is_err());
975
976 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 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 allowance.period_spend_limit = coin(999, TEST_DENOM);
997 assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
998
999 allowance.period_spend_limit = coin(1000, TEST_DENOM);
1001 assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1002
1003 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 assert!(allowance.validate_new_inner("baddenom").is_err());
1034
1035 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 allowance.period_grant = coin(0, TEST_DENOM);
1045 assert!(allowance.validate_new_inner(TEST_DENOM).is_err());
1046
1047 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 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 allowance.period_grant = coin(999, TEST_DENOM);
1069 assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1070
1071 allowance.period_grant = coin(1000, TEST_DENOM);
1073 assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1074
1075 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 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 allowance.accumulation_limit = Some(coin(999, TEST_DENOM));
1097 assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1098
1099 allowance.accumulation_limit = Some(coin(1000, TEST_DENOM));
1101 assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1102
1103 allowance.basic.spend_limit = None;
1105 assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1106
1107 allowance.accumulation_limit = None;
1109 assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1110
1111 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 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 allowance.accumulation_limit = Some(coin(501, TEST_DENOM));
1133 assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1134
1135 allowance.accumulation_limit = Some(coin(500, TEST_DENOM));
1137 assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
1138
1139 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 assert!(allowance.validate_new_inner(TEST_DENOM).is_err());
1151
1152 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 env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp + 1);
1171 assert!(allowance.validate_new_inner(&env).is_err());
1172
1173 env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp);
1175 assert!(allowance.validate_new_inner(&env).is_ok());
1176
1177 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 allowance.available_at_unix_timestamp = 1001;
1191 assert!(allowance.validate_new_inner(&env).is_err());
1192
1193 allowance.available_at_unix_timestamp = 1000;
1195 assert!(allowance.validate_new_inner(&env).is_ok());
1196
1197 allowance.available_at_unix_timestamp = 999;
1199 assert!(allowance.validate_new_inner(&env).is_ok());
1200
1201 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 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 expected.period_can_spend = Some(expected.period_spend_limit.clone());
1236
1237 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 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 let env = mock_env();
1275 delayed.set_initial_state(&env);
1276 assert_eq!(delayed, og);
1277 }
1278 }
1279}