use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin};
pub type GranterAddress = Addr;
pub type GranteeAddress = Addr;
pub use grants::*;
pub use query_responses::*;
#[cw_serde]
pub struct TransferRecipient {
pub recipient: String,
pub amount: Coin,
}
pub mod grants {
use crate::utils::ensure_unix_timestamp_not_in_the_past;
use crate::{GranteeAddress, GranterAddress, NymPoolContractError};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin, Env, Timestamp, Uint128};
use std::cmp::min;
#[cw_serde]
pub struct GranterInformation {
pub created_by: Addr,
pub created_at_height: u64,
}
#[cw_serde]
pub struct Grant {
pub granter: GranterAddress,
pub grantee: GranteeAddress,
pub granted_at_height: u64,
pub allowance: Allowance,
}
#[cw_serde]
pub enum Allowance {
Basic(BasicAllowance),
ClassicPeriodic(ClassicPeriodicAllowance),
CumulativePeriodic(CumulativePeriodicAllowance),
Delayed(DelayedAllowance),
}
impl From<BasicAllowance> for Allowance {
fn from(value: BasicAllowance) -> Self {
Allowance::Basic(value)
}
}
impl From<ClassicPeriodicAllowance> for Allowance {
fn from(value: ClassicPeriodicAllowance) -> Self {
Allowance::ClassicPeriodic(value)
}
}
impl From<CumulativePeriodicAllowance> for Allowance {
fn from(value: CumulativePeriodicAllowance) -> Self {
Allowance::CumulativePeriodic(value)
}
}
impl From<DelayedAllowance> for Allowance {
fn from(value: DelayedAllowance) -> Self {
Allowance::Delayed(value)
}
}
impl Allowance {
pub fn expired(&self, env: &Env) -> bool {
self.basic().expired(env)
}
pub fn basic(&self) -> &BasicAllowance {
match self {
Allowance::Basic(allowance) => allowance,
Allowance::ClassicPeriodic(allowance) => &allowance.basic,
Allowance::CumulativePeriodic(allowance) => &allowance.basic,
Allowance::Delayed(allowance) => &allowance.basic,
}
}
pub fn basic_mut(&mut self) -> &mut BasicAllowance {
match self {
Allowance::Basic(allowance) => allowance,
Allowance::ClassicPeriodic(allowance) => &mut allowance.basic,
Allowance::CumulativePeriodic(allowance) => &mut allowance.basic,
Allowance::Delayed(allowance) => &mut allowance.basic,
}
}
pub fn expiration(&self) -> Option<Timestamp> {
let expiration_unix = match self {
Allowance::Basic(allowance) => allowance.expiration_unix_timestamp,
Allowance::ClassicPeriodic(allowance) => allowance.basic.expiration_unix_timestamp,
Allowance::CumulativePeriodic(allowance) => {
allowance.basic.expiration_unix_timestamp
}
Allowance::Delayed(allowance) => allowance.basic.expiration_unix_timestamp,
};
expiration_unix.map(Timestamp::from_seconds)
}
pub fn validate_new(&self, env: &Env, denom: &str) -> Result<(), NymPoolContractError> {
self.basic().validate(env, denom)?;
match self {
Allowance::Basic(_) => Ok(()),
Allowance::ClassicPeriodic(allowance) => allowance.validate_new_inner(denom),
Allowance::CumulativePeriodic(allowance) => allowance.validate_new_inner(denom),
Allowance::Delayed(allowance) => allowance.validate_new_inner(env),
}
}
pub fn set_initial_state(&mut self, env: &Env) {
match self {
Allowance::Basic(_) => {}
Allowance::ClassicPeriodic(allowance) => allowance.set_initial_state(env),
Allowance::CumulativePeriodic(allowance) => allowance.set_initial_state(env),
Allowance::Delayed(_) => {}
}
}
pub fn try_update_state(&mut self, env: &Env) {
match self {
Allowance::Basic(_) => {}
Allowance::ClassicPeriodic(allowance) => allowance.try_update_state(env),
Allowance::CumulativePeriodic(allowance) => allowance.try_update_state(env),
Allowance::Delayed(_) => {}
}
}
pub fn within_spendable_limits(&self, amount: &Coin) -> bool {
match self {
Allowance::Basic(allowance) => allowance.within_spendable_limits(amount),
Allowance::ClassicPeriodic(allowance) => allowance.within_spendable_limits(amount),
Allowance::CumulativePeriodic(allowance) => {
allowance.within_spendable_limits(amount)
}
Allowance::Delayed(allowance) => allowance.within_spendable_limits(amount),
}
}
pub fn ensure_can_spend(
&self,
env: &Env,
amount: &Coin,
) -> Result<(), NymPoolContractError> {
match self {
Allowance::Basic(allowance) => allowance.ensure_can_spend(env, amount),
Allowance::ClassicPeriodic(allowance) => allowance.ensure_can_spend(env, amount),
Allowance::CumulativePeriodic(allowance) => allowance.ensure_can_spend(env, amount),
Allowance::Delayed(allowance) => allowance.ensure_can_spend(env, amount),
}
}
pub fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
self.try_update_state(env);
match self {
Allowance::Basic(allowance) => allowance.try_spend(env, amount),
Allowance::ClassicPeriodic(allowance) => allowance.try_spend(env, amount),
Allowance::CumulativePeriodic(allowance) => allowance.try_spend(env, amount),
Allowance::Delayed(allowance) => allowance.try_spend(env, amount),
}
}
pub fn increase_spend_limit(&mut self, amount: Uint128) {
if let Some(ref mut limit) = self.basic_mut().spend_limit {
limit.amount += amount
}
}
pub fn is_used_up(&self) -> bool {
let Some(ref limit) = self.basic().spend_limit else {
return false;
};
limit.amount.is_zero()
}
}
#[cw_serde]
pub struct BasicAllowance {
pub spend_limit: Option<Coin>,
pub expiration_unix_timestamp: Option<u64>,
}
impl BasicAllowance {
pub fn unlimited() -> BasicAllowance {
BasicAllowance {
spend_limit: None,
expiration_unix_timestamp: None,
}
}
pub fn validate(&self, env: &Env, denom: &str) -> Result<(), NymPoolContractError> {
if let Some(expiration) = self.expiration_unix_timestamp {
ensure_unix_timestamp_not_in_the_past(expiration, env)?;
}
if let Some(ref spend_limit) = self.spend_limit {
if spend_limit.denom != denom {
return Err(NymPoolContractError::InvalidDenom {
expected: denom.to_string(),
got: spend_limit.denom.to_string(),
});
}
if spend_limit.amount.is_zero() {
return Err(NymPoolContractError::ZeroAmount);
}
}
Ok(())
}
pub fn expired(&self, env: &Env) -> bool {
let Some(expiration) = self.expiration_unix_timestamp else {
return false;
};
let current_unix_timestamp = env.block.time.seconds();
expiration < current_unix_timestamp
}
fn within_spendable_limits(&self, amount: &Coin) -> bool {
let Some(ref spend_limit) = self.spend_limit else {
return true;
};
spend_limit.amount >= amount.amount
}
fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
if self.expired(env) {
return Err(NymPoolContractError::GrantExpired);
}
if !self.within_spendable_limits(amount) {
return Err(NymPoolContractError::SpendingAboveAllowance);
}
Ok(())
}
fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
self.ensure_can_spend(env, amount)?;
if let Some(ref mut spend_limit) = self.spend_limit {
spend_limit.amount -= amount.amount;
}
Ok(())
}
}
#[cw_serde]
pub struct ClassicPeriodicAllowance {
pub basic: BasicAllowance,
pub period_duration_secs: u64,
pub period_spend_limit: Coin,
#[serde(default)]
pub period_can_spend: Option<Coin>,
#[serde(default)]
pub period_reset_unix_timestamp: u64,
}
impl ClassicPeriodicAllowance {
pub(super) fn validate_new_inner(&self, denom: &str) -> Result<(), NymPoolContractError> {
if self.period_duration_secs == 0 {
return Err(NymPoolContractError::ZeroAllowancePeriod);
}
if self.period_spend_limit.denom != denom {
return Err(NymPoolContractError::InvalidDenom {
expected: denom.to_string(),
got: self.period_spend_limit.denom.to_string(),
});
}
if self.period_spend_limit.amount.is_zero() {
return Err(NymPoolContractError::ZeroAmount);
}
if let Some(ref basic_limit) = self.basic.spend_limit {
if basic_limit.amount < self.period_spend_limit.amount {
return Err(NymPoolContractError::PeriodicGrantOverSpendLimit {
periodic: self.period_spend_limit.clone(),
total_limit: basic_limit.clone(),
});
}
}
Ok(())
}
fn determine_period_can_spend(&self) -> Coin {
let Some(ref basic_limit) = self.basic.spend_limit else {
return self.period_spend_limit.clone();
};
if basic_limit.amount < self.period_spend_limit.amount {
basic_limit.clone()
} else {
self.period_spend_limit.clone()
}
}
pub(super) fn set_initial_state(&mut self, env: &Env) {
self.try_update_state(env);
}
pub fn try_update_state(&mut self, env: &Env) {
if env.block.time.seconds() < self.period_reset_unix_timestamp {
return;
}
self.period_can_spend = Some(self.determine_period_can_spend());
self.period_reset_unix_timestamp += self.period_duration_secs;
if env.block.time.seconds() > self.period_duration_secs {
self.period_reset_unix_timestamp =
env.block.time.seconds() + self.period_duration_secs;
}
}
fn within_spendable_limits(&self, amount: &Coin) -> bool {
let Some(ref available) = self.period_can_spend else {
return false;
};
available.amount >= amount.amount
}
fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
if self.basic.expired(env) {
return Err(NymPoolContractError::GrantExpired);
}
if !self.within_spendable_limits(amount) {
return Err(NymPoolContractError::SpendingAboveAllowance);
}
Ok(())
}
fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
self.ensure_can_spend(env, amount)?;
if let Some(ref mut spend_limit) = self.basic.spend_limit {
spend_limit.amount -= amount.amount;
}
#[allow(clippy::unwrap_used)]
let period_can_spend = self.period_can_spend.as_mut().unwrap();
period_can_spend.amount -= amount.amount;
Ok(())
}
}
#[cw_serde]
pub struct CumulativePeriodicAllowance {
pub basic: BasicAllowance,
pub period_duration_secs: u64,
pub period_grant: Coin,
pub accumulation_limit: Option<Coin>,
#[serde(default)]
pub spendable: Option<Coin>,
#[serde(default)]
pub last_grant_applied_unix_timestamp: u64,
}
impl CumulativePeriodicAllowance {
pub(super) fn validate_new_inner(&self, denom: &str) -> Result<(), NymPoolContractError> {
if self.period_duration_secs == 0 {
return Err(NymPoolContractError::ZeroAllowancePeriod);
}
if self.period_grant.denom != denom {
return Err(NymPoolContractError::InvalidDenom {
expected: denom.to_string(),
got: self.period_grant.denom.to_string(),
});
}
if self.period_grant.amount.is_zero() {
return Err(NymPoolContractError::ZeroAmount);
}
if let Some(ref basic_limit) = self.basic.spend_limit {
if basic_limit.amount < self.period_grant.amount {
return Err(NymPoolContractError::PeriodicGrantOverSpendLimit {
periodic: self.period_grant.clone(),
total_limit: basic_limit.clone(),
});
}
}
if let Some(ref accumulation_limit) = self.accumulation_limit {
if accumulation_limit.amount < self.period_grant.amount {
return Err(NymPoolContractError::AccumulationBelowGrantAmount {
accumulation: accumulation_limit.clone(),
periodic_grant: self.period_grant.clone(),
});
}
if accumulation_limit.denom != denom {
return Err(NymPoolContractError::InvalidDenom {
expected: denom.to_string(),
got: accumulation_limit.denom.to_string(),
});
}
if let Some(ref basic_limit) = self.basic.spend_limit {
if basic_limit.amount < accumulation_limit.amount {
return Err(NymPoolContractError::AccumulationOverSpendLimit {
accumulation: accumulation_limit.clone(),
total_limit: basic_limit.clone(),
});
}
}
}
Ok(())
}
pub(super) fn set_initial_state(&mut self, env: &Env) {
self.last_grant_applied_unix_timestamp = env.block.time.seconds();
self.spendable = Some(self.period_grant.clone())
}
#[inline]
fn missed_periods(&self, env: &Env) -> u64 {
(env.block.time.seconds() - self.last_grant_applied_unix_timestamp)
% self.period_duration_secs
}
fn determine_spendable(&self, env: &Env) -> Coin {
#[allow(clippy::unwrap_used)]
let spendable = self.spendable.as_ref().unwrap();
let missed_periods = self.missed_periods(env);
let mut max_spendable = spendable.clone();
max_spendable.amount += Uint128::new(missed_periods as u128) * self.period_grant.amount;
match (&self.basic.spend_limit, &self.accumulation_limit) {
(Some(spend_limit), Some(accumulation_limit)) => {
let limit = min(spend_limit.amount, accumulation_limit.amount);
let amount = min(limit, max_spendable.amount);
Coin::new(amount, max_spendable.denom)
}
(None, Some(accumulation_limit)) => {
let amount = min(accumulation_limit.amount, max_spendable.amount);
Coin::new(amount, max_spendable.denom)
}
(Some(spend_limit), None) => {
let amount = min(spend_limit.amount, max_spendable.amount);
Coin::new(amount, max_spendable.denom)
}
(None, None) => max_spendable,
}
}
pub fn try_update_state(&mut self, env: &Env) {
let missed_periods = self.missed_periods(env);
if missed_periods == 0 {
return;
}
self.spendable = Some(self.determine_spendable(env))
}
fn within_spendable_limits(&self, amount: &Coin) -> bool {
let Some(ref available) = self.spendable else {
return false;
};
available.amount >= amount.amount
}
fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
if self.basic.expired(env) {
return Err(NymPoolContractError::GrantExpired);
}
if !self.within_spendable_limits(amount) {
return Err(NymPoolContractError::SpendingAboveAllowance);
}
Ok(())
}
fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
self.ensure_can_spend(env, amount)?;
if let Some(ref mut spend_limit) = self.basic.spend_limit {
spend_limit.amount -= amount.amount;
}
#[allow(clippy::unwrap_used)]
let spendable = self.spendable.as_mut().unwrap();
spendable.amount -= amount.amount;
Ok(())
}
}
#[cw_serde]
pub struct DelayedAllowance {
pub basic: BasicAllowance,
pub available_at_unix_timestamp: u64,
}
impl DelayedAllowance {
pub(super) fn validate_new_inner(&self, env: &Env) -> Result<(), NymPoolContractError> {
ensure_unix_timestamp_not_in_the_past(self.available_at_unix_timestamp, env)?;
if let Some(expiration) = self.basic.expiration_unix_timestamp {
if expiration < self.available_at_unix_timestamp {
return Err(NymPoolContractError::UnattainableDelayedAllowance {
expiration_timestamp: expiration,
available_timestamp: self.available_at_unix_timestamp,
});
}
}
Ok(())
}
fn within_spendable_limits(&self, amount: &Coin) -> bool {
self.basic.within_spendable_limits(amount)
}
fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
if self.basic.expired(env) {
return Err(NymPoolContractError::GrantExpired);
}
if !self.within_spendable_limits(amount) {
return Err(NymPoolContractError::SpendingAboveAllowance);
}
if self.available_at_unix_timestamp < env.block.time.seconds() {
return Err(NymPoolContractError::GrantNotYetAvailable {
available_at_timestamp: self.available_at_unix_timestamp,
});
}
Ok(())
}
fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> {
self.ensure_can_spend(env, amount)?;
if let Some(ref mut spend_limit) = self.basic.spend_limit {
spend_limit.amount -= amount.amount;
}
Ok(())
}
}
}
pub mod query_responses {
use crate::{Grant, GranteeAddress, GranterAddress, GranterInformation};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
#[cw_serde]
pub struct AvailableTokensResponse {
pub available: Coin,
}
#[cw_serde]
pub struct TotalLockedTokensResponse {
pub locked: Coin,
}
#[cw_serde]
pub struct LockedTokensResponse {
pub grantee: GranteeAddress,
pub locked: Option<Coin>,
}
#[cw_serde]
pub struct GrantInformation {
pub grant: Grant,
pub expired: bool,
}
#[cw_serde]
pub struct GrantResponse {
pub grantee: GranteeAddress,
pub grant: Option<GrantInformation>,
}
#[cw_serde]
pub struct GranterResponse {
pub granter: GranterAddress,
pub information: Option<GranterInformation>,
}
#[cw_serde]
pub struct GrantsPagedResponse {
pub grants: Vec<GrantInformation>,
pub start_next_after: Option<String>,
}
#[cw_serde]
pub struct GranterDetails {
pub granter: GranterAddress,
pub information: GranterInformation,
}
impl From<(GranterAddress, GranterInformation)> for GranterDetails {
fn from((granter, information): (GranterAddress, GranterInformation)) -> Self {
GranterDetails {
granter,
information,
}
}
}
#[cw_serde]
pub struct GrantersPagedResponse {
pub granters: Vec<GranterDetails>,
pub start_next_after: Option<String>,
}
#[cw_serde]
pub struct LockedTokens {
pub grantee: GranteeAddress,
pub locked: Coin,
}
#[cw_serde]
pub struct LockedTokensPagedResponse {
pub locked: Vec<LockedTokens>,
pub start_next_after: Option<String>,
}
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::{Uint128, coin};
const TEST_DENOM: &str = "unym";
fn mock_basic_allowance() -> BasicAllowance {
BasicAllowance {
spend_limit: Some(coin(100000, TEST_DENOM)),
expiration_unix_timestamp: Some(1643652000),
}
}
fn mock_classic_periodic_allowance() -> ClassicPeriodicAllowance {
ClassicPeriodicAllowance {
basic: mock_basic_allowance(),
period_duration_secs: 10,
period_spend_limit: coin(1000, TEST_DENOM),
period_can_spend: None,
period_reset_unix_timestamp: 0,
}
}
fn mock_cumulative_periodic_allowance() -> CumulativePeriodicAllowance {
CumulativePeriodicAllowance {
basic: mock_basic_allowance(),
period_duration_secs: 10,
period_grant: coin(1000, TEST_DENOM),
accumulation_limit: Some(coin(10000, TEST_DENOM)),
spendable: None,
last_grant_applied_unix_timestamp: 0,
}
}
fn mock_delayed_allowance() -> DelayedAllowance {
DelayedAllowance {
basic: mock_basic_allowance(),
available_at_unix_timestamp: 1643650000,
}
}
#[test]
fn increasing_spend_limit() {
let mut basic = mock_basic_allowance();
basic.spend_limit = None;
let mut basic = Allowance::Basic(basic);
let mut classic = mock_classic_periodic_allowance();
classic.basic.spend_limit = None;
let mut classic = Allowance::ClassicPeriodic(classic);
let mut cumulative = mock_cumulative_periodic_allowance();
cumulative.basic.spend_limit = None;
let mut cumulative = Allowance::CumulativePeriodic(cumulative);
let mut delayed = mock_delayed_allowance();
delayed.basic.spend_limit = None;
let mut delayed = Allowance::Delayed(delayed);
let basic_og = basic.clone();
let classic_og = classic.clone();
let cumulative_og = cumulative.clone();
let delayed_og = delayed.clone();
basic.increase_spend_limit(Uint128::new(100));
classic.increase_spend_limit(Uint128::new(100));
cumulative.increase_spend_limit(Uint128::new(100));
delayed.increase_spend_limit(Uint128::new(100));
assert_eq!(basic, basic_og);
assert_eq!(classic, classic_og);
assert_eq!(cumulative, cumulative_og);
assert_eq!(delayed, delayed_og);
let limit = coin(1000, TEST_DENOM);
let mut basic = mock_basic_allowance();
basic.spend_limit = Some(limit.clone());
let mut basic = Allowance::Basic(basic);
let mut classic = mock_classic_periodic_allowance();
classic.basic.spend_limit = Some(limit.clone());
let mut classic = Allowance::ClassicPeriodic(classic);
let mut cumulative = mock_cumulative_periodic_allowance();
cumulative.basic.spend_limit = Some(limit.clone());
let mut cumulative = Allowance::CumulativePeriodic(cumulative);
let mut delayed = mock_delayed_allowance();
delayed.basic.spend_limit = Some(limit.clone());
let mut delayed = Allowance::Delayed(delayed);
basic.increase_spend_limit(Uint128::new(100));
classic.increase_spend_limit(Uint128::new(100));
cumulative.increase_spend_limit(Uint128::new(100));
delayed.increase_spend_limit(Uint128::new(100));
assert_eq!(
basic.basic().spend_limit.as_ref().unwrap().amount,
limit.amount + Uint128::new(100)
);
assert_eq!(
classic.basic().spend_limit.as_ref().unwrap().amount,
limit.amount + Uint128::new(100)
);
assert_eq!(
cumulative.basic().spend_limit.as_ref().unwrap().amount,
limit.amount + Uint128::new(100)
);
assert_eq!(
delayed.basic().spend_limit.as_ref().unwrap().amount,
limit.amount + Uint128::new(100)
);
}
#[cfg(test)]
mod validating_new_allowances {
use super::*;
#[cfg(test)]
mod basic_allowance {
use super::*;
use cosmwasm_std::Timestamp;
use cosmwasm_std::testing::mock_env;
#[test]
fn doesnt_allow_expirations_in_the_past() {
let mut allowance = mock_basic_allowance();
let mut env = mock_env();
env.block.time =
Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap() + 1);
assert!(allowance.validate(&env, TEST_DENOM).is_err());
env.block.time =
Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap());
assert!(allowance.validate(&env, TEST_DENOM).is_ok());
env.block.time =
Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap() - 1);
assert!(allowance.validate(&env, TEST_DENOM).is_ok());
allowance.expiration_unix_timestamp = None;
assert!(allowance.validate(&env, TEST_DENOM).is_ok());
}
#[test]
fn spend_limit_must_match_expected_denom() {
let mut allowance = mock_basic_allowance();
let env = mock_env();
assert!(allowance.validate(&env, "baddenom").is_err());
assert!(allowance.validate(&env, TEST_DENOM).is_ok());
allowance.spend_limit = None;
assert!(allowance.validate(&env, TEST_DENOM).is_ok());
}
#[test]
fn spend_limit_must_be_non_zero() {
let mut allowance = mock_basic_allowance();
let env = mock_env();
allowance.spend_limit = Some(coin(0, TEST_DENOM));
assert!(allowance.validate(&env, TEST_DENOM).is_err());
allowance.spend_limit = Some(coin(69, TEST_DENOM));
assert!(allowance.validate(&env, TEST_DENOM).is_ok());
}
}
#[cfg(test)]
mod classic_periodic_allowance {
use super::*;
use crate::NymPoolContractError;
#[test]
fn period_duration_must_be_nonzero() {
let mut allowance = mock_classic_periodic_allowance();
allowance.period_duration_secs = 0;
assert_eq!(
allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
NymPoolContractError::ZeroAllowancePeriod
);
allowance.period_duration_secs = 1;
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
#[test]
fn spend_limit_must_match_expected_denom() {
let allowance = mock_classic_periodic_allowance();
assert!(allowance.validate_new_inner("baddenom").is_err());
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
#[test]
fn spend_limit_must_be_non_zero() {
let mut allowance = mock_classic_periodic_allowance();
allowance.period_spend_limit = coin(0, TEST_DENOM);
assert!(allowance.validate_new_inner(TEST_DENOM).is_err());
allowance.period_spend_limit = coin(69, TEST_DENOM);
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
#[test]
fn period_spend_limit_must_be_smaller_than_total_limit() {
let mut allowance = mock_classic_periodic_allowance();
let total_limit = coin(1000, TEST_DENOM);
allowance.basic.spend_limit = Some(total_limit);
allowance.period_spend_limit = coin(1001, TEST_DENOM);
assert!(matches!(
allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
NymPoolContractError::PeriodicGrantOverSpendLimit { .. }
));
allowance.period_spend_limit = coin(999, TEST_DENOM);
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
allowance.period_spend_limit = coin(1000, TEST_DENOM);
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
allowance.basic.spend_limit = None;
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
}
#[cfg(test)]
mod cumulative_periodic_allowance {
use super::*;
use crate::NymPoolContractError;
#[test]
fn period_duration_must_be_nonzero() {
let mut allowance = mock_cumulative_periodic_allowance();
allowance.period_duration_secs = 0;
assert_eq!(
allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
NymPoolContractError::ZeroAllowancePeriod
);
allowance.period_duration_secs = 1;
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
#[test]
fn grant_must_match_expected_denom() {
let allowance = mock_cumulative_periodic_allowance();
assert!(allowance.validate_new_inner("baddenom").is_err());
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
#[test]
fn grant_must_be_non_zero() {
let mut allowance = mock_cumulative_periodic_allowance();
allowance.period_grant = coin(0, TEST_DENOM);
assert!(allowance.validate_new_inner(TEST_DENOM).is_err());
allowance.period_grant = coin(69, TEST_DENOM);
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
#[test]
fn grant_amount_must_be_smaller_than_total_limit() {
let mut allowance = mock_cumulative_periodic_allowance();
let total_limit = coin(1000, TEST_DENOM);
allowance.basic.spend_limit = Some(total_limit);
allowance.accumulation_limit = None;
allowance.period_grant = coin(1001, TEST_DENOM);
assert!(matches!(
allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
NymPoolContractError::PeriodicGrantOverSpendLimit { .. }
));
allowance.period_grant = coin(999, TEST_DENOM);
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
allowance.period_grant = coin(1000, TEST_DENOM);
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
allowance.basic.spend_limit = None;
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
#[test]
fn accumulation_limit_must_be_smaller_than_total_limit() {
let mut allowance = mock_cumulative_periodic_allowance();
let total_limit = coin(1000, TEST_DENOM);
allowance.basic.spend_limit = Some(total_limit.clone());
allowance.period_grant = coin(500, TEST_DENOM);
allowance.accumulation_limit = Some(coin(1001, TEST_DENOM));
assert!(matches!(
allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
NymPoolContractError::AccumulationOverSpendLimit { .. }
));
allowance.accumulation_limit = Some(coin(999, TEST_DENOM));
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
allowance.accumulation_limit = Some(coin(1000, TEST_DENOM));
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
allowance.basic.spend_limit = None;
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
allowance.accumulation_limit = None;
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
allowance.basic.spend_limit = Some(total_limit);
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
#[test]
fn accumulation_limit_must_not_be_smaller_than_grant_amount() {
let mut allowance = mock_cumulative_periodic_allowance();
let total_limit = coin(1000, TEST_DENOM);
allowance.basic.spend_limit = Some(total_limit);
allowance.period_grant = coin(500, TEST_DENOM);
allowance.accumulation_limit = Some(coin(499, TEST_DENOM));
assert!(matches!(
allowance.validate_new_inner(TEST_DENOM).unwrap_err(),
NymPoolContractError::AccumulationBelowGrantAmount { .. }
));
allowance.accumulation_limit = Some(coin(501, TEST_DENOM));
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
allowance.accumulation_limit = Some(coin(500, TEST_DENOM));
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
allowance.accumulation_limit = None;
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
#[test]
fn accumulation_limit_must_match_expected_denom() {
let mut allowance = mock_cumulative_periodic_allowance();
allowance.accumulation_limit = Some(coin(1000, "baddenom"));
assert!(allowance.validate_new_inner(TEST_DENOM).is_err());
allowance.accumulation_limit = Some(coin(1000, TEST_DENOM));
assert!(allowance.validate_new_inner(TEST_DENOM).is_ok());
}
}
#[cfg(test)]
mod delayed_allowance {
use super::*;
use cosmwasm_std::Timestamp;
use cosmwasm_std::testing::mock_env;
#[test]
fn doesnt_allow_availability_in_the_past() {
let allowance = mock_delayed_allowance();
let mut env = mock_env();
env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp + 1);
assert!(allowance.validate_new_inner(&env).is_err());
env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp);
assert!(allowance.validate_new_inner(&env).is_ok());
env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp - 1);
assert!(allowance.validate_new_inner(&env).is_ok());
}
#[test]
fn must_have_available_before_allowance_expiration() {
let mut allowance = mock_delayed_allowance();
let mut env = mock_env();
env.block.time = Timestamp::from_seconds(100);
allowance.basic.expiration_unix_timestamp = Some(1000);
allowance.available_at_unix_timestamp = 1001;
assert!(allowance.validate_new_inner(&env).is_err());
allowance.available_at_unix_timestamp = 1000;
assert!(allowance.validate_new_inner(&env).is_ok());
allowance.available_at_unix_timestamp = 999;
assert!(allowance.validate_new_inner(&env).is_ok());
allowance.basic.expiration_unix_timestamp = None;
assert!(allowance.validate_new_inner(&env).is_ok());
}
}
}
#[cfg(test)]
mod setting_initial_state {
use super::*;
use cosmwasm_std::testing::mock_env;
#[test]
fn basic_allowance() {
let mut basic = Allowance::Basic(mock_basic_allowance());
let og = basic.clone();
let env = mock_env();
basic.set_initial_state(&env);
assert_eq!(basic, og);
}
#[test]
fn classic_periodic_allowance() {
let mut inner = mock_classic_periodic_allowance();
let mut cumulative = Allowance::ClassicPeriodic(inner.clone());
let env = mock_env();
let mut expected = inner.clone();
expected.period_can_spend = Some(expected.period_spend_limit.clone());
expected.period_reset_unix_timestamp =
env.block.time.seconds() + expected.period_duration_secs;
inner.set_initial_state(&env);
assert_eq!(inner, expected);
cumulative.set_initial_state(&env);
assert_eq!(cumulative, Allowance::ClassicPeriodic(inner));
}
#[test]
fn cumulative_periodic_allowance() {
let mut inner = mock_cumulative_periodic_allowance();
let mut cumulative = Allowance::CumulativePeriodic(inner.clone());
let env = mock_env();
let mut expected = inner.clone();
expected.last_grant_applied_unix_timestamp = env.block.time.seconds();
expected.spendable = Some(expected.period_grant.clone());
inner.set_initial_state(&env);
assert_eq!(inner, expected);
cumulative.set_initial_state(&env);
assert_eq!(cumulative, Allowance::CumulativePeriodic(inner));
}
#[test]
fn delayed_allowance() {
let mut delayed = Allowance::Delayed(mock_delayed_allowance());
let og = delayed.clone();
let env = mock_env();
delayed.set_initial_state(&env);
assert_eq!(delayed, og);
}
}
}