Files
nym/contracts/ecash/src/multitest.rs
T
Jędrzej Stuczyński 6581ebf235 feat: multiple deposit prices (#6608)
* added reduced pricing handling logic

* admin methods for setting the whitelist of reduced price accounts

* updated client traits

* query to get all whitelisted accounts

* query for getting detailed deposit statistics

* fixes

* set initial whitelisted accounts in the migration

* stop transferring tokens to the holding account after redemption

* stop gateways from creating redemption multisig proposals

* make sure credential-proxy uses reduced deposits when available

* cargo fmt

* update deposit handler to allow EITHER default price or reduced price

this will allow non-breaking upgrades of NS and credential proxy

* removed use of unstable rust features

* rebuilt contract schema

* correct license timestamp
2026-03-26 16:02:19 +00:00

274 lines
8.3 KiB
Rust

// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::testing::MockApi;
use cosmwasm_std::{coin, Coin};
use cw_multi_test::IntoBech32;
use cw_utils::PaymentError;
use nym_ecash_contract_common::EcashContractError;
use sylvia::{cw_multi_test::App as MtApp, multitest::App};
use crate::contract::sv::mt::{CodeId, NymEcashContractProxy};
const DENOM: &str = "unym";
const DEPOSIT_AMOUNT: u128 = 75_000_000;
#[test]
fn invalid_deposit() {
let owner = "owner".into_bech32();
let denom = "unym";
let mtapp = MtApp::new(|router, _, storage| {
router
.bank
.init_balance(
storage,
&owner,
vec![
Coin::new(10000000u32, denom),
Coin::new(10000000u32, "some_denom"),
],
)
.unwrap()
});
let app = App::new(mtapp);
let code_id = CodeId::store_code(&app);
let contract = code_id
.instantiate(
MockApi::default().addr_make("holding_acount").to_string(),
MockApi::default().addr_make("multisig_addr").to_string(),
MockApi::default().addr_make("group_addr").to_string(),
coin(75000000, denom),
)
.call(&owner)
.unwrap();
let verification_key = "Verification key";
assert_eq!(
contract
.deposit_ticket_book_funds(verification_key.to_string(),)
.call(&owner)
.unwrap_err(),
EcashContractError::InvalidDeposit(PaymentError::NoFunds {})
);
let coin = Coin::new(1000000u32, denom.to_string());
let second_coin = Coin::new(1000000u32, "some_denom");
assert_eq!(
contract
.deposit_ticket_book_funds(verification_key.to_string(),)
.with_funds(&[coin, second_coin.clone()])
.call(&owner)
.unwrap_err(),
EcashContractError::InvalidDeposit(PaymentError::MultipleDenoms {})
);
assert_eq!(
contract
.deposit_ticket_book_funds(verification_key.to_string(),)
.with_funds(&[second_coin])
.call(&owner)
.unwrap_err(),
EcashContractError::InvalidDeposit(PaymentError::MissingDenom(denom.to_string()))
);
}
#[test]
fn wrong_deposit_amount() {
let owner = "owner".into_bech32();
let mtapp = MtApp::new(|router, _, storage| {
router
.bank
.init_balance(storage, &owner, vec![Coin::new(1_000_000_000u128, DENOM)])
.unwrap()
});
let app = App::new(mtapp);
let code_id = CodeId::store_code(&app);
let contract = code_id
.instantiate(
MockApi::default().addr_make("holding_account").to_string(),
MockApi::default().addr_make("multisig_addr").to_string(),
MockApi::default().addr_make("group_addr").to_string(),
coin(DEPOSIT_AMOUNT, DENOM),
)
.call(&owner)
.unwrap();
let vk = "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK";
// too little
assert_eq!(
contract
.deposit_ticket_book_funds(vk.to_string())
.with_funds(&[coin(1_000_000u128, DENOM)])
.call(&owner)
.unwrap_err(),
EcashContractError::WrongAmount {
received: coin(1_000_000u128, DENOM),
amount: coin(DEPOSIT_AMOUNT, DENOM),
}
);
// too much
assert_eq!(
contract
.deposit_ticket_book_funds(vk.to_string())
.with_funds(&[coin(100_000_000u128, DENOM)])
.call(&owner)
.unwrap_err(),
EcashContractError::WrongAmount {
received: coin(100_000_000u128, DENOM),
amount: coin(DEPOSIT_AMOUNT, DENOM),
}
);
}
#[test]
fn correct_default_deposit_succeeds() {
let owner = "owner".into_bech32();
let mtapp = MtApp::new(|router, _, storage| {
router
.bank
.init_balance(storage, &owner, vec![Coin::new(1_000_000_000u128, DENOM)])
.unwrap()
});
let app = App::new(mtapp);
let code_id = CodeId::store_code(&app);
let contract = code_id
.instantiate(
MockApi::default().addr_make("holding_account").to_string(),
MockApi::default().addr_make("multisig_addr").to_string(),
MockApi::default().addr_make("group_addr").to_string(),
coin(DEPOSIT_AMOUNT, DENOM),
)
.call(&owner)
.unwrap();
let vk = "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK";
contract
.deposit_ticket_book_funds(vk.to_string())
.with_funds(&[coin(DEPOSIT_AMOUNT, DENOM)])
.call(&owner)
.unwrap();
}
#[test]
fn reduced_price_deposit_end_to_end() {
let owner = "owner".into_bech32();
let whitelisted = "whitelisted".into_bech32();
let non_whitelisted = "non_whitelisted".into_bech32();
let reduced_amount: u128 = 10_000_000;
let mtapp = MtApp::new(|router, _, storage| {
router
.bank
.init_balance(
storage,
&whitelisted,
vec![Coin::new(1_000_000_000u128, DENOM)],
)
.unwrap();
router
.bank
.init_balance(
storage,
&non_whitelisted,
vec![Coin::new(1_000_000_000u128, DENOM)],
)
.unwrap();
});
let app = App::new(mtapp);
let code_id = CodeId::store_code(&app);
let contract = code_id
.instantiate(
MockApi::default().addr_make("holding_account").to_string(),
MockApi::default().addr_make("multisig_addr").to_string(),
MockApi::default().addr_make("group_addr").to_string(),
coin(DEPOSIT_AMOUNT, DENOM),
)
.call(&owner)
.unwrap();
let vk = "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK";
// whitelist an address with a reduced price
contract
.set_reduced_deposit_price(whitelisted.to_string(), coin(reduced_amount, DENOM))
.call(&owner)
.unwrap();
// whitelisted address can deposit at the reduced price
contract
.deposit_ticket_book_funds(vk.to_string())
.with_funds(&[coin(reduced_amount, DENOM)])
.call(&whitelisted)
.unwrap();
// whitelisted address can also deposit at the default price —
// treated as a normal (non-reduced) deposit for statistics purposes
contract
.deposit_ticket_book_funds(vk.to_string())
.with_funds(&[coin(DEPOSIT_AMOUNT, DENOM)])
.call(&whitelisted)
.unwrap();
// whitelisted address is rejected when sending an amount that is
// neither the reduced nor the default price
assert_eq!(
contract
.deposit_ticket_book_funds(vk.to_string())
.with_funds(&[coin(50_000_000, DENOM)])
.call(&whitelisted)
.unwrap_err(),
EcashContractError::WrongAmount {
received: coin(50_000_000, DENOM),
amount: coin(reduced_amount, DENOM),
}
);
// non-whitelisted address is rejected at the reduced amount
assert_eq!(
contract
.deposit_ticket_book_funds(vk.to_string())
.with_funds(&[coin(reduced_amount, DENOM)])
.call(&non_whitelisted)
.unwrap_err(),
EcashContractError::WrongAmount {
received: coin(reduced_amount, DENOM),
amount: coin(DEPOSIT_AMOUNT, DENOM),
}
);
// non-whitelisted address succeeds at the default amount
contract
.deposit_ticket_book_funds(vk.to_string())
.with_funds(&[coin(DEPOSIT_AMOUNT, DENOM)])
.call(&non_whitelisted)
.unwrap();
let stats = contract.get_deposits_statistics().unwrap();
assert_eq!(stats.total_deposits_made, 3);
assert_eq!(
stats.total_deposited,
coin(reduced_amount + DEPOSIT_AMOUNT * 2, DENOM)
);
// whitelisted depositing at default price + non-whitelisted = 2 default deposits
assert_eq!(stats.total_deposits_made_with_default_price, 2);
assert_eq!(
stats.total_deposited_with_default_price,
coin(DEPOSIT_AMOUNT * 2, DENOM)
);
assert_eq!(stats.total_deposits_made_with_custom_price, 1);
assert_eq!(
stats.total_deposited_with_custom_price,
coin(reduced_amount, DENOM)
);
}