diff --git a/standalone/runtime/tests/mock/mod.rs b/standalone/runtime/tests/mock/mod.rs index 2d9047ac58..76214ad3b2 100644 --- a/standalone/runtime/tests/mock/mod.rs +++ b/standalone/runtime/tests/mock/mod.rs @@ -410,6 +410,9 @@ pub fn iter_all_currencies() -> impl Iterator { impl UserData { pub fn get(id: [u8; 32]) -> Self { let account_id = account_of(id); + Self::from_account(account_id) + } + pub fn from_account(account_id: AccountId) -> Self { let mut hash_map = BTreeMap::new(); for currency_id in iter_all_currencies() { diff --git a/standalone/runtime/tests/mock/redeem_testing_utils.rs b/standalone/runtime/tests/mock/redeem_testing_utils.rs index 7549204382..cd2ef290b3 100644 --- a/standalone/runtime/tests/mock/redeem_testing_utils.rs +++ b/standalone/runtime/tests/mock/redeem_testing_utils.rs @@ -8,6 +8,16 @@ pub const VAULT: [u8; 32] = BOB; pub const VAULT2: [u8; 32] = CAROL; pub const USER_BTC_ADDRESS: BtcAddress = BtcAddress::P2PKH(H160([2u8; 20])); +pub trait RedeemRequestTestExt { + fn amount_without_fee_as_collateral(&self, currency_id: CurrencyId) -> Amount; +} +impl RedeemRequestTestExt for RedeemRequest { + fn amount_without_fee_as_collateral(&self, currency_id: CurrencyId) -> Amount { + let amount_without_fee = self.amount_btc() + self.transfer_fee_btc(); + amount_without_fee.convert_to(currency_id).unwrap() + } +} + pub struct ExecuteRedeemBuilder { redeem_id: H256, redeem: RedeemRequest, @@ -80,11 +90,20 @@ pub fn setup_cancelable_redeem(user: [u8; 32], vault: &VaultId, issued_tokens: A // expire request without transferring btc mine_blocks((RedeemPallet::redeem_period() + 99) / 100 + 1); - SecurityPallet::set_active_block_number(RedeemPallet::redeem_period() + 1 + 1); + SecurityPallet::set_active_block_number( + SecurityPallet::active_block_number() + RedeemPallet::redeem_period() + 1 + 1, + ); redeem_id } +pub fn expire_bans() { + mine_blocks((RedeemPallet::redeem_period() + 99) / 100 + 1); + SecurityPallet::set_active_block_number( + SecurityPallet::active_block_number() + VaultRegistryPallet::punishment_delay() + 1 + 1, + ); +} + pub fn set_redeem_state( vault_to_be_redeemed: Amount, user_to_redeem: Amount, diff --git a/standalone/runtime/tests/mock/reward_testing_utils.rs b/standalone/runtime/tests/mock/reward_testing_utils.rs index 96ee610f52..0dbb689eee 100644 --- a/standalone/runtime/tests/mock/reward_testing_utils.rs +++ b/standalone/runtime/tests/mock/reward_testing_utils.rs @@ -1,6 +1,6 @@ use crate::*; use primitives::TruncateFixedPointToInt; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; pub type StakeId = (VaultId, AccountId); @@ -10,7 +10,7 @@ pub struct IdealRewardPool { secure_threshold: BTreeMap, accept_new_issues: BTreeMap, commission: BTreeMap, - collateral: BTreeMap, + collateral: BTreeMap, rewards: BTreeMap, } @@ -36,19 +36,22 @@ impl IdealRewardPool { pub fn deposit_nominator_collateral(&mut self, account: &StakeId, amount: u128) -> &mut Self { log::debug!("deposit_nominator_collateral {amount}"); let current_collateral = self.collateral.get(account).map(|x| *x).unwrap_or_default(); - self.collateral.insert(account.clone(), current_collateral + amount); + self.collateral + .insert(account.clone(), current_collateral + FixedU128::from(amount)); self } pub fn withdraw_nominator_collateral(&mut self, account: &StakeId, amount: u128) -> &mut Self { log::debug!("withdraw_nominator_collateral {amount}"); let current_collateral = self.collateral.get(account).map(|x| *x).unwrap_or_default(); - self.collateral.insert(account.clone(), current_collateral - amount); + self.collateral + .insert(account.clone(), current_collateral - FixedU128::from(amount)); self } pub fn slash_collateral(&mut self, account: &VaultId, amount: u128) -> &mut Self { log::error!("slash_collateral {amount}"); + let amount = FixedU128::from(amount); let nominators: Vec<_> = { self.collateral .iter() @@ -56,7 +59,10 @@ impl IdealRewardPool { .map(|(key, value)| (key.clone(), value.clone())) .collect() }; - let total_stake: u128 = nominators.iter().map(|(_key, value)| *value).sum(); + let total_stake: FixedU128 = nominators + .iter() + .map(|(_key, value)| *value) + .fold(Zero::zero(), |x: FixedU128, y: FixedU128| x + y); for (key, stake) in nominators { let new_stake = stake - (stake * amount) / total_stake; self.collateral.insert(key, new_stake); @@ -77,26 +83,91 @@ impl IdealRewardPool { return self; } - for (stake_id, _) in self.collateral.iter() { - let stake = self.stake(stake_id); - let reward = (stake * reward) / total_stake; - - let (vault_id, nominator_id) = stake_id; - - let commission = self.commission.get(vault_id).cloned().unwrap_or_default(); - - let (vault_commission, vault_reward) = self.rewards.get(&vault_id.account_id).cloned().unwrap_or_default(); - self.rewards.insert( - vault_id.account_id.clone(), - (vault_commission + reward * commission, vault_reward), - ); - - let (nominator_commission, nominator_reward) = self.rewards.get(&nominator_id).cloned().unwrap_or_default(); - self.rewards.insert( - nominator_id.clone(), - (nominator_commission, nominator_reward + (reward - reward * commission)), - ); + let vault_stakes: HashMap<_, _> = self + .collateral + .iter() + .map(|((vault, _nominator), collateral)| (vault, collateral)) + .filter(|(vault, _)| *self.accept_new_issues.get(vault).unwrap_or(&true)) + .into_group_map() + .into_iter() + .map(|(vault, nominator_collaterals)| { + let vault_collateral = nominator_collaterals + .into_iter() + .cloned() + .fold(Zero::zero(), |x: FixedU128, y: FixedU128| x + y); + let threshold = self.secure_threshold[vault]; + let reward_stake = (vault_collateral / threshold).truncate_to_inner().unwrap(); + (vault, reward_stake) + }) + .collect(); + + let capacity_stakes: Vec<_> = vault_stakes + .iter() + .map(|(vault, stake)| (vault.collateral_currency(), stake)) + .into_group_map() + .into_iter() + .map(|(currency, vault_stakes)| { + let currency_capacity: u128 = vault_stakes.into_iter().sum(); + let exchange_rate = self.exchange_rate[¤cy]; + let capacity_stake = (FixedU128::from(currency_capacity) / exchange_rate) + .truncate_to_inner() + .unwrap(); + (currency, capacity_stake) + }) + .collect(); + + log::error!("Capacity_stakes: {capacity_stakes:?}"); + + let total_capacity: u128 = capacity_stakes.iter().map(|(_, capacity)| capacity).sum(); + for (currency, capacity_stake) in capacity_stakes { + let currency_reward = (reward * FixedU128::from(capacity_stake)) / FixedU128::from(total_capacity); + let currency_reward = currency_reward.trunc(); + // reward for this currency = reward * (capacity_stake / total_capacity) + let vaults: Vec<_> = vault_stakes + .iter() + .filter(|(vault, _)| vault.collateral_currency() == currency) + .collect(); + let total_vault_stake: u128 = vaults.iter().map(|(_, stake)| **stake).sum(); + for vault_stake in vaults.iter() { + let nominators: Vec<_> = self + .nominations() + .iter() + .cloned() + .filter(|((vault, _nominator), _stake)| &vault == vault_stake.0) + .map(|((_vault, nominator), stake)| (nominator, stake)) + .collect(); + let total_nomination = nominators + .iter() + .map(|(_, nomination)| *nomination) + .fold(Zero::zero(), |x: FixedU128, y: FixedU128| x + y); + let vault_reward = + (currency_reward * FixedU128::from(*vault_stake.1)) / FixedU128::from(total_vault_stake); + let vault_reward = vault_reward.trunc(); + log::error!("vault_reward: {}", vault_reward.truncate_to_inner().unwrap()); + + let commission = self.commission.get(vault_stake.0).cloned().unwrap_or_default(); + + let vault = vault_stake.0.clone(); + let (vault_commission, old_vault_reward) = + self.rewards.get(&vault.account_id).cloned().unwrap_or_default(); + self.rewards.insert( + vault.account_id.clone(), + (vault_commission + vault_reward * commission, old_vault_reward), + ); + + for (nominator_id, nomination) in nominators { + let nominator_reward = (vault_reward - vault_reward * commission) * nomination / total_nomination; + + let (nominator_commission, old_nominator_reward) = + self.rewards.get(&nominator_id).cloned().unwrap_or_default(); + self.rewards.insert( + nominator_id.clone(), + (nominator_commission, old_nominator_reward + nominator_reward), + ); + } + } } + self } @@ -120,10 +191,12 @@ impl IdealRewardPool { .iter() .filter(|((vault, nominator), _stake)| nominator == account && vault.collateral_currency() == currency_id) .map(|(_key, value)| *value) - .sum() + .fold(Zero::zero(), |x: FixedU128, y: FixedU128| x + y) + .truncate_to_inner() + .unwrap() } - pub fn nominations(&self) -> Vec<(StakeId, u128)> { + pub fn nominations(&self) -> Vec<(StakeId, FixedU128)> { self.collateral .iter() .map(|(key, value)| (key.clone(), value.clone())) diff --git a/standalone/runtime/tests/test_fee_pool.rs b/standalone/runtime/tests/test_fee_pool.rs index 81545e37e1..0aa9e1fe33 100644 --- a/standalone/runtime/tests/test_fee_pool.rs +++ b/standalone/runtime/tests/test_fee_pool.rs @@ -1,6 +1,7 @@ mod mock; +use crate::redeem_testing_utils::{expire_bans, setup_cancelable_redeem, RedeemRequestTestExt}; use currency::Amount; -use frame_support::storage::migration::put_storage_value; +use frame_support::migration::put_storage_value; use interbtc_runtime_standalone::{Timestamp, UnsignedFixedPoint}; use mock::{ assert_eq, @@ -9,6 +10,7 @@ use mock::{ reward_testing_utils::IdealRewardPool, *, }; +use primitives::TruncateFixedPointToInt; use rand::Rng; use sp_consensus_aura::{Slot, SlotDuration}; use sp_timestamp::Timestamp as SlotTimestamp; @@ -47,6 +49,37 @@ macro_rules! assert_approx_eq { }}; } +fn test_with_2(execute: impl Fn(VaultId) -> R) { + let test_with = |currency_id, wrapped_id| { + ExtBuilder::build().execute_with(|| { + SecurityPallet::set_active_block_number(1); + for currency_id in iter_collateral_currencies() { + assert_ok!(OraclePallet::_set_exchange_rate( + currency_id, + FixedU128::from_float(DEFAULT_EXCHANGE_RATE) + )); + } + if wrapped_id != Token(IBTC) { + assert_ok!(OraclePallet::_set_exchange_rate(wrapped_id, FixedU128::one())); + } + activate_lending_and_mint(Token(DOT), LendToken(1)); + UserData::force_to(USER, default_user_state()); + let vault_id = PrimitiveVaultId::new(account_of(VAULT), currency_id, wrapped_id); + CoreVaultData::force_to(&vault_id, default_vault_state(&vault_id)); + LiquidationVaultData::force_to(default_liquidation_vault_state(&vault_id.currencies)); + + enable_nomination(); + assert_nomination_opt_in(&vault_id); + + let commission = UnsignedFixedPoint::from_float(COMMISSION); + set_commission(&vault_id, commission); + + execute(vault_id) + }); + }; + test_with(Token(DOT), Token(KBTC)); +} + fn test_with(execute: impl Fn(VaultId) -> R) { let test_with = |currency_id, wrapped_id| { ExtBuilder::build().execute_with(|| { @@ -414,6 +447,7 @@ enum Action { DistributeRewards, SetExchangeRate, SetAcceptIssues, + FailRedeem, } impl Action { @@ -427,11 +461,145 @@ impl Action { 5 => Self::DistributeRewards, 6 => Self::SetExchangeRate, 7 => Self::SetAcceptIssues, + /* note the range - this will never be produced, since slashing is known to break nomination */ + 8 => Self::FailRedeem, _ => unreachable!(), } } } +#[derive(Clone, Debug)] +enum ConcreteAction { + DepositNominationCollateral { + nominator_id: AccountId, + vault_id: VaultId, + amount: Amount, + }, + WithdrawNominationCollateral { + nominator_id: AccountId, + vault_id: VaultId, + amount: Amount, + }, + SetSecureThreshold { + vault_id: VaultId, + threshold: FixedU128, + }, + SetExchangeRate { + currency_id: CurrencyId, + exchange_rate: FixedU128, + }, + DistributeRewards { + amount: Amount, + }, + FailRedeem { + vault_id: VaultId, + amount: Amount, + }, + AcceptNewIssues { + vault_id: VaultId, + accept: bool, + }, +} + +impl ConcreteAction { + fn execute(&self, reference_pool: &mut IdealRewardPool) { + match self { + ConcreteAction::DepositNominationCollateral { + nominator_id, + vault_id, + amount, + } => { + reference_pool.deposit_nominator_collateral(&(vault_id.clone(), nominator_id.clone()), amount.amount()); + assert_nominate_collateral(vault_id, nominator_id.clone(), amount.clone()); + } + ConcreteAction::WithdrawNominationCollateral { + nominator_id, + vault_id, + amount, + } => { + reference_pool + .withdraw_nominator_collateral(&(vault_id.clone(), nominator_id.clone()), amount.amount()); + assert_withdraw_nominator_collateral(nominator_id.clone(), vault_id, amount.clone()); + } + ConcreteAction::SetSecureThreshold { vault_id, threshold } => { + assert_ok!( + RuntimeCall::VaultRegistry(VaultRegistryCall::set_custom_secure_threshold { + currency_pair: vault_id.currencies.clone(), + custom_threshold: Some(*threshold), + }) + .dispatch(origin_of(vault_id.account_id.clone())) + ); + + reference_pool.set_secure_threshold(vault_id, threshold.clone()); + } + ConcreteAction::SetExchangeRate { + currency_id, + exchange_rate, + } => { + if currency_id.is_lend_token() { + let underlying_id = LoansPallet::underlying_id(*currency_id).unwrap(); + let lend_token_rate = exchange_rate.mul(LoansPallet::exchange_rate(underlying_id)); + // Only need to set the exchange rate of the underlying currency in the oracle pallet + OraclePallet::_set_exchange_rate(underlying_id, *exchange_rate).unwrap(); + // The reference pool must store both exchange rates explicitly + reference_pool.set_exchange_rate(*currency_id, lend_token_rate); + reference_pool.set_exchange_rate(underlying_id, *exchange_rate); + } else { + OraclePallet::_set_exchange_rate(*currency_id, *exchange_rate).unwrap(); + reference_pool.set_exchange_rate(*currency_id, *exchange_rate); + if let Ok(lend_token_id) = LoansPallet::lend_token_id(*currency_id) { + let lend_token_rate = exchange_rate.div(LoansPallet::exchange_rate(*currency_id)); + reference_pool.set_exchange_rate(lend_token_id, lend_token_rate); + } + } + } + ConcreteAction::DistributeRewards { amount } => { + distribute_rewards(*amount); + reference_pool.distribute_reward(amount.amount()); + } + ConcreteAction::FailRedeem { vault_id, amount } => { + expire_bans(); + + let free_balance_before = CurrencySource::FreeBalance(vault_id.account_id.clone()) + .current_balance(DEFAULT_GRIEFING_CURRENCY) + .unwrap(); + let redeem_id = setup_cancelable_redeem(USER, &vault_id, *amount); + let free_balance_after = CurrencySource::FreeBalance(vault_id.account_id.clone()) + .current_balance(DEFAULT_GRIEFING_CURRENCY) + .unwrap(); + + VaultRegistryPallet::transfer_funds( + CurrencySource::FreeBalance(vault_id.account_id.clone()), + CurrencySource::AvailableReplaceCollateral(vault_id.clone()), + &(free_balance_after - free_balance_before), + ) + .unwrap(); + + let redeem = RedeemPallet::get_open_redeem_request_from_id(&redeem_id).unwrap(); + let amount_without_fee_collateral = + redeem.amount_without_fee_as_collateral(vault_id.collateral_currency()); + let punishment_fee = FeePallet::get_punishment_fee(&amount_without_fee_collateral).unwrap(); + + assert_ok!(RuntimeCall::Redeem(RedeemCall::cancel_redeem { + redeem_id: redeem_id, + reimburse: false + }) + .dispatch(origin_of(account_of(USER)))); + + reference_pool.slash_collateral(&vault_id, punishment_fee.amount()); + } + ConcreteAction::AcceptNewIssues { vault_id, accept } => { + assert_ok!(RuntimeCall::VaultRegistry(VaultRegistryCall::accept_new_issues { + currency_pair: vault_id.currencies.clone(), + accept_new_issues: *accept, + }) + .dispatch(origin_of(vault_id.account_id.clone()))); + reference_pool.accept_new_issues(&vault_id, *accept); + } + } + } +} + #[test] #[cfg_attr(feature = "skip-slow-tests", ignore)] fn test_fee_pool_matches_ideal_implementation() { @@ -441,102 +609,113 @@ fn test_fee_pool_matches_ideal_implementation() { } } -fn do_random_nomination_sequence() { - test_with(|vault_id| { - let mut rng = rand::thread_rng(); - - let max_collateral = 1000; - - let token1 = Token(DOT); - let token2 = Token(KSM); - - // set up some potential nominators - let nominators: Vec<_> = (100..107).map(|id| account_of([id; 32])).collect(); - for nominator in nominators.iter() { - for currency_id in [vault_id.collateral_currency(), token1, token2] { - if currency_id.is_lend_token() { - // Hardcoding the lend_token balance would break the internal exchange rate calculation - // in the Loans pallet, which is using the total amount of issued lend_tokens - mint_lend_tokens(nominator.clone(), currency_id); - } else { - assert_ok!(RuntimeCall::Tokens(TokensCall::set_balance { - who: nominator.clone(), - currency_id, - new_free: max_collateral, - new_reserved: 0, - }) - .dispatch(root())); - } +const MAX_COLLATERAL: u128 = 1000; +fn setup_nomination(vault_id: VaultId) -> (IdealRewardPool, Vec, Vec) { + let token1 = Token(DOT); + let token2 = Token(KSM); + + // set up some potential nominators + let nominators: Vec<_> = (100..107).map(|id| account_of([id; 32])).collect(); + for nominator in nominators.iter() { + for currency_id in [vault_id.collateral_currency(), token1, token2] { + if currency_id.is_lend_token() { + // Hardcoding the lend_token balance would break the internal exchange rate calculation + // in the Loans pallet, which is using the total amount of issued lend_tokens + mint_lend_tokens(nominator.clone(), currency_id); + } else { + assert_ok!(RuntimeCall::Tokens(TokensCall::set_balance { + who: nominator.clone(), + currency_id, + new_free: MAX_COLLATERAL, + new_reserved: 0, + }) + .dispatch(root())); } } + } - // setup some vaults - let vault_id = &vault_id; - let vaults: Vec<_> = (107..110) - .map(|id| { - let collateral_currency = match rng.gen_bool(0.5) { - false => token1, - true => token2, - }; - let vault_id = PrimitiveVaultId { - account_id: account_of([id; 32]), - currencies: VaultCurrencyPair { - collateral: collateral_currency, - wrapped: vault_id.wrapped_currency(), - }, - }; - CoreVaultData::force_to(&vault_id, default_vault_state(&vault_id)); - assert_nomination_opt_in(&vault_id); - set_commission(&vault_id, FixedU128::from_float(COMMISSION)); - vault_id + // setup some vaults + let vault_id = &vault_id; + let vaults: Vec<_> = (107..110) + .map(|id| { + let collateral_currency = if id % 2 == 0 { token1 } else { token2 }; + let vault_id = PrimitiveVaultId { + account_id: account_of([id; 32]), + currencies: VaultCurrencyPair { + collateral: collateral_currency, + wrapped: vault_id.wrapped_currency(), + }, + }; + CoreVaultData::force_to(&vault_id, default_vault_state(&vault_id)); + assert_nomination_opt_in(&vault_id); + set_commission(&vault_id, FixedU128::from_float(COMMISSION)); + vault_id + }) + .chain(vec![vault_id.clone()]) + .collect(); + + // setup the reference pool - vaults have initial stake + let mut reference_pool = IdealRewardPool::default(); + for vault_id in vaults.iter() { + let collateral = default_vault_state(&vault_id).backing_collateral.amount(); + reference_pool.deposit_nominator_collateral(&(vault_id.clone(), vault_id.account_id.clone()), collateral); + reference_pool.set_commission(&vault_id, FixedU128::from_float(COMMISSION)); + + assert_ok!( + RuntimeCall::VaultRegistry(VaultRegistryCall::set_custom_secure_threshold { + currency_pair: vault_id.currencies.clone(), + custom_threshold: Some(FixedU128::from_float(3.0)), }) - .chain(vec![vault_id.clone()]) - .collect(); + .dispatch(origin_of(vault_id.account_id.clone())) + ); + let threshold = VaultRegistryPallet::get_vault_secure_threshold(&vault_id).unwrap(); + reference_pool.set_secure_threshold(&vault_id, threshold); + + if vault_id.collateral_currency().is_lend_token() { + let underlying_id = LoansPallet::underlying_id(vault_id.collateral_currency()).unwrap(); + let lend_token_rate = FixedU128::one().mul(LoansPallet::exchange_rate(underlying_id)); + OraclePallet::_set_exchange_rate(underlying_id, FixedU128::one()).unwrap(); + reference_pool.set_exchange_rate(vault_id.collateral_currency(), lend_token_rate); + reference_pool.set_exchange_rate(underlying_id, FixedU128::one()); + } else { + OraclePallet::_set_exchange_rate(vault_id.collateral_currency(), FixedU128::one()).unwrap(); + reference_pool.set_exchange_rate(vault_id.collateral_currency(), FixedU128::one()); + } + } - // setup the reference pool - vaults have initial stake - let mut reference_pool = IdealRewardPool::default(); - for vault_id in vaults.iter() { - let collateral = default_vault_state(&vault_id).backing_collateral.amount(); - reference_pool.deposit_nominator_collateral(&(vault_id.clone(), vault_id.account_id.clone()), collateral); - reference_pool.set_commission(&vault_id, FixedU128::from_float(COMMISSION)); + (reference_pool, nominators, vaults) +} +fn do_random_nomination_sequence() { + test_with_2(|vault_id| { + let mut rng = rand::thread_rng(); - assert_ok!( - RuntimeCall::VaultRegistry(VaultRegistryCall::set_custom_secure_threshold { - currency_pair: vault_id.currencies.clone(), - custom_threshold: Some(FixedU128::from_float(3.0)), - }) - .dispatch(origin_of(vault_id.account_id.clone())) - ); - let threshold = VaultRegistryPallet::get_vault_secure_threshold(&vault_id).unwrap(); - reference_pool.set_secure_threshold(&vault_id, threshold); - - if vault_id.collateral_currency().is_lend_token() { - let underlying_id = LoansPallet::underlying_id(vault_id.collateral_currency()).unwrap(); - let lend_token_rate = FixedU128::one().mul(LoansPallet::exchange_rate(underlying_id)); - OraclePallet::_set_exchange_rate(underlying_id, FixedU128::one()).unwrap(); - reference_pool.set_exchange_rate(vault_id.collateral_currency(), lend_token_rate); - reference_pool.set_exchange_rate(underlying_id, FixedU128::one()); - } else { - OraclePallet::_set_exchange_rate(vault_id.collateral_currency(), FixedU128::one()).unwrap(); - reference_pool.set_exchange_rate(vault_id.collateral_currency(), FixedU128::one()); - } - } + let (mut reference_pool, nominators, vaults) = setup_nomination(vault_id.clone()); let mut actual_rewards = BTreeMap::new(); + let mut actions = Vec::new(); for _ in 0..50 { // 50 random actions. - match Action::random(&mut rng) { + let action = match Action::random(&mut rng) { Action::DepositNominationCollateral => { - let nominator = &nominators[rng.gen_range(0..nominators.len())]; - let vault = &vaults[rng.gen_range(0..vaults.len())]; - let current_stake = reference_pool.get_nominator_collateral(nominator, vault.collateral_currency()); - let amount = rng.gen_range(0..max_collateral - current_stake); - reference_pool.deposit_nominator_collateral(&(vault.clone(), nominator.clone()), amount); - assert_nominate_collateral( - vault, - nominator.clone(), - Amount::new(amount, vault.collateral_currency()), + log::error!("DepositNominationCollateral"); + + let nominator_id = nominators[rng.gen_range(0..nominators.len())].clone(); + let vault_id = vaults[rng.gen_range(0..vaults.len())].clone(); + let free = + UserData::from_account(nominator_id.clone()).balances[&vault_id.collateral_currency()].free; + if free.is_zero() { + continue; + } + + let amount = Amount::new( + rng.gen_range(0..MAX_COLLATERAL.min(free.amount())), + vault_id.collateral_currency(), ); + ConcreteAction::DepositNominationCollateral { + nominator_id, + vault_id, + amount, + } } Action::WithdrawNominationCollateral => { let nominations = reference_pool.nominations(); @@ -550,34 +729,32 @@ fn do_random_nomination_sequence() { .unwrap() .amount() .saturating_sub(100), // acount for rounding errors - amount, + amount.truncate_to_inner().unwrap(), ); if max_amount == 0 { continue; } - let amount = rng.gen_range(0..max_amount); + let amount = Amount::new(rng.gen_range(0..max_amount), vault_id.collateral_currency()); - reference_pool.withdraw_nominator_collateral(&(vault_id.clone(), nominator_id.clone()), amount); - assert_withdraw_nominator_collateral( + ConcreteAction::WithdrawNominationCollateral { nominator_id, - &vault_id, - Amount::new(amount, vault_id.collateral_currency()), - ); + vault_id, + amount, + } } Action::DepositVaultCollateral => { - let vault_id = &vaults[rng.gen_range(0..vaults.len())]; + let vault_id = vaults[rng.gen_range(0..vaults.len())].clone(); let max_amount = CoreVaultData::vault(vault_id.clone()).free_balance[&vault_id.collateral_currency()].amount(); - let amount = rng.gen_range(0..max_amount); - assert_ok!(RuntimeCall::Nomination(NominationCall::deposit_collateral { - vault_id: vault_id.clone(), + let amount = Amount::new(rng.gen_range(0..max_amount), vault_id.collateral_currency()); + + ConcreteAction::DepositNominationCollateral { + nominator_id: vault_id.account_id.clone(), + vault_id, amount, - }) - .dispatch(origin_of(vault_id.account_id.clone()))); - reference_pool - .deposit_nominator_collateral(&(vault_id.clone(), vault_id.account_id.clone()), amount); + } } Action::WithdrawVaultCollateral => { let vaults: Vec<_> = reference_pool @@ -597,67 +774,59 @@ fn do_random_nomination_sequence() { if max_amount.is_zero() { continue; } - let amount = rng.gen_range(0..max_amount); - assert_ok!(RuntimeCall::Nomination(NominationCall::withdraw_collateral { - vault_id: vault_id.clone(), - index: None, + let amount = Amount::new(rng.gen_range(0..max_amount), vault_id.collateral_currency()); + + ConcreteAction::WithdrawNominationCollateral { + nominator_id: vault_id.account_id.clone(), + vault_id, amount, - }) - .dispatch(origin_of(vault_id.account_id.clone()))); - reference_pool - .withdraw_nominator_collateral(&(vault_id.clone(), vault_id.account_id.clone()), amount); + } } Action::SetSecureThreshold => { let vault_id = vaults[rng.gen_range(0..vaults.len())].clone(); let threshold = FixedU128::from_float(rng.gen_range(2.0..5.0)); - assert_ok!( - RuntimeCall::VaultRegistry(VaultRegistryCall::set_custom_secure_threshold { - currency_pair: vault_id.currencies.clone(), - custom_threshold: Some(threshold), - }) - .dispatch(origin_of(vault_id.account_id.clone())) - ); - - reference_pool.set_secure_threshold(&vault_id, threshold); + ConcreteAction::SetSecureThreshold { vault_id, threshold } } Action::SetExchangeRate => { let vault_id = vaults[rng.gen_range(0..vaults.len())].clone(); let currency_id = vault_id.collateral_currency(); let exchange_rate = FixedU128::from_float(rng.gen_range(0.5..5.0)); - if currency_id.is_lend_token() { - let underlying_id = LoansPallet::underlying_id(vault_id.collateral_currency()).unwrap(); - let lend_token_rate = exchange_rate.mul(LoansPallet::exchange_rate(underlying_id)); - // Only need to set the exchange rate of the underlying currency in the oracle pallet - OraclePallet::_set_exchange_rate(underlying_id, exchange_rate.clone()).unwrap(); - // The reference pool must store both exchange rates explicitly - reference_pool.set_exchange_rate(currency_id, lend_token_rate); - reference_pool.set_exchange_rate(underlying_id, exchange_rate.clone()); - } else { - OraclePallet::_set_exchange_rate(currency_id, exchange_rate.clone()).unwrap(); - reference_pool.set_exchange_rate(currency_id, exchange_rate.clone()); - if let Ok(lend_token_id) = LoansPallet::lend_token_id(currency_id.clone()) { - let lend_token_rate = exchange_rate.div(LoansPallet::exchange_rate(currency_id.clone())); - reference_pool.set_exchange_rate(lend_token_id, lend_token_rate); - } + + ConcreteAction::SetExchangeRate { + currency_id, + exchange_rate, } } Action::DistributeRewards => { - let amount = rng.gen_range(0..10_000_000_000); - distribute_rewards(Amount::new(amount, REWARD_CURRENCY)); - reference_pool.distribute_reward(amount); + let amount = Amount::new(rng.gen_range(0..10_000_000_000), REWARD_CURRENCY); + ConcreteAction::DistributeRewards { amount } + } + Action::FailRedeem => { + let vault_id = vaults[rng.gen_range(0..vaults.len())].clone(); + let vault = CoreVaultData::vault(vault_id.clone()); + let redeemable = vault.issued - vault.to_be_redeemed; + let user_btc = UserData::get(USER).balances[&vault_id.wrapped_currency()].free; + + let max_amount = user_btc.min(&redeemable).unwrap(); + let min_amount = redeem::Pallet::::get_dust_value(vault_id.wrapped_currency()) + + redeem::Pallet::::get_current_inclusion_fee(vault_id.wrapped_currency()).unwrap(); + if max_amount <= min_amount { + continue; + } + + let amount = max_amount.with_amount(|x| rng.gen_range(min_amount.amount()..x)); + + ConcreteAction::FailRedeem { vault_id, amount } } Action::SetAcceptIssues => { let vault_id = vaults[rng.gen_range(0..vaults.len())].clone(); - let accept_new_issues = rng.gen_bool(0.5); - assert_ok!(RuntimeCall::VaultRegistry(VaultRegistryCall::accept_new_issues { - currency_pair: vault_id.currencies.clone(), - accept_new_issues, - }) - .dispatch(origin_of(vault_id.account_id.clone()))); - reference_pool.accept_new_issues(&vault_id, accept_new_issues); + let accept = rng.gen_bool(0.5); + ConcreteAction::AcceptNewIssues { vault_id, accept } } }; + actions.push(action.clone()); + action.execute(&mut reference_pool); } for ((vault_id, nominator_id), _) in reference_pool.nominations() { withdraw_vault_rewards(&vault_id); @@ -679,7 +848,14 @@ fn do_random_nomination_sequence() { }) .sum(); - assert!(abs_difference(total_reference_pool, total_actually_received) < 1000); + if abs_difference(total_reference_pool, total_actually_received) >= 1000 { + log::error!("Failed assertion for actions {actions:?}"); + // use the following to format the debug output above to an array to be used in reproduction + #[cfg_attr(rustfmt, rustfmt_skip)] // don't fmt this comment, it breaks the cmd + // cat $file | sed 's/CurrencyId:://g' | sed 's/TokenSymbol:://g' | sed 's/[^ ]*\(..\) [(]5[^)]*[)]/account_of([0x\1; 32])/g' | sed 's/FixedU128/FixedU128::from_float/g' | sed 's/Amount { amount: \([0-9]\+\), currency_id: \([^}]\+\)}/Amount::new(\1, \2)/g' + + assert_eq!(total_reference_pool, total_actually_received); + } // check the rewards of all stakeholders for (nominator_id, expected_reward) in reference_pool.rewards() { @@ -690,13 +866,156 @@ fn do_random_nomination_sequence() { currency::get_free_balance::(REWARD_CURRENCY, &nominator_id).amount() }; // ensure the difference is small, but allow some rounding errors.. - if abs_difference(actual_reward, expected_reward) > expected_reward / 10_000 + 10 { + if abs_difference(actual_reward, expected_reward) > expected_reward / 500 + 10 { + log::error!("Failed assertion for actions {actions:?}"); assert_eq!(actual_reward, expected_reward); } } }) } +#[test] +#[ignore] // this function is used to debug failing test cases of test_fee_pool_matches_ideal_implementation +fn reproduce_failing_test() { + for i in 1..50 { + _reproduce_failing_test(i); + } +} + +fn _reproduce_failing_test(num_actions: usize) { + let _ = env_logger::try_init(); + test_with_2(|vault_id| { + let (mut reference_pool, _, vaults) = setup_nomination(vault_id.clone()); + + use ConcreteAction::*; + + #[cfg_attr(rustfmt, rustfmt_skip)] + let actions = vec![ + DepositNominationCollateral { nominator_id: account_of([0x6c; 32]), vault_id: VaultId { account_id: account_of([0x6c; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(50000, Token(DOT)), }, + DistributeRewards { amount: Amount::new(10000000000, Token(INTR)), }, + SetSecureThreshold { vault_id: VaultId { account_id: account_of([0x6c; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, threshold: FixedU128::from_float(3.419287478062694912), }, + WithdrawNominationCollateral { nominator_id: account_of([0x6c; 32]), vault_id: VaultId { account_id: account_of([0x6c; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(53948, Token(DOT)), }, + WithdrawNominationCollateral { nominator_id: account_of([0x6c; 32]), vault_id: VaultId { account_id: account_of([0x6c; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(573157, Token(DOT)), }, + SetExchangeRate { currency_id: Token(DOT), exchange_rate: FixedU128::from_float(3.092839019606802944), }, + DepositNominationCollateral { nominator_id: account_of([0x6c; 32]), vault_id: VaultId { account_id: account_of([0x6c; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(367450, Token(DOT)), }, + SetExchangeRate { currency_id: Token(DOT), exchange_rate: FixedU128::from_float(1.373757773995398144), }, + SetExchangeRate { currency_id: Token(DOT), exchange_rate: FixedU128::from_float(2.691586595358666240), }, + DistributeRewards { amount: Amount::new(8982684181, Token(INTR)), }, + SetSecureThreshold { vault_id: VaultId { account_id: account_of([0x6b; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, threshold: FixedU128::from_float(2.203683781128871680), }, + WithdrawNominationCollateral { nominator_id: account_of([0x6d; 32]), vault_id: VaultId { account_id: account_of([0x6d; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, amount: Amount::new(650763, Token(KSM)), }, + DistributeRewards { amount: Amount::new(4447058456, Token(INTR)), }, + WithdrawNominationCollateral { nominator_id: account_of([0x01; 32]), vault_id: VaultId { account_id: account_of([0x01; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(31049, Token(DOT)), }, + WithdrawNominationCollateral { nominator_id: account_of([0x01; 32]), vault_id: VaultId { account_id: account_of([0x01; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(10377, Token(DOT)), }, + SetExchangeRate { currency_id: Token(KSM), exchange_rate: FixedU128::from_float(1.670225524936316928), }, + FailRedeem { vault_id: VaultId { account_id: account_of([0x6d; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, amount: Amount::new(55567, Token(KBTC)), }, + SetSecureThreshold { vault_id: VaultId { account_id: account_of([0x6d; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, threshold: FixedU128::from_float(3.365260653951399936), }, + SetSecureThreshold { vault_id: VaultId { account_id: account_of([0x6b; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, threshold: FixedU128::from_float(2.649903409252506624), }, + SetExchangeRate { currency_id: Token(DOT), exchange_rate: FixedU128::from_float(4.728498710955768832), }, + DepositNominationCollateral { nominator_id: account_of([0x64; 32]), vault_id: VaultId { account_id: account_of([0x6d; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, amount: Amount::new(827, Token(KSM)), }, + FailRedeem { vault_id: VaultId { account_id: account_of([0x6d; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, amount: Amount::new(55118, Token(KBTC)), }, + DistributeRewards { amount: Amount::new(8032902951, Token(INTR)), }, + SetExchangeRate { currency_id: Token(DOT), exchange_rate: FixedU128::from_float(4.115433122284937216), }, + DepositNominationCollateral { nominator_id: account_of([0x6c; 32]), vault_id: VaultId { account_id: account_of([0x6c; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(121195, Token(DOT)), }, + SetSecureThreshold { vault_id: VaultId { account_id: account_of([0x6b; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, threshold: FixedU128::from_float(3.677915815946293760), }, + SetExchangeRate { currency_id: Token(KSM), exchange_rate: FixedU128::from_float(1.880357238054170112), }, + SetSecureThreshold { vault_id: VaultId { account_id: account_of([0x6d; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, threshold: FixedU128::from_float(2.035206833200252416), }, + SetExchangeRate { currency_id: Token(DOT), exchange_rate: FixedU128::from_float(1.260768734589768192), }, + DepositNominationCollateral { nominator_id: account_of([0x67; 32]), vault_id: VaultId { account_id: account_of([0x6c; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(511, Token(DOT)), }, + WithdrawNominationCollateral { nominator_id: account_of([0x6c; 32]), vault_id: VaultId { account_id: account_of([0x6c; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(262678, Token(DOT)), }, + SetSecureThreshold { vault_id: VaultId { account_id: account_of([0x6d; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, threshold: FixedU128::from_float(4.710951067647164416), }, + WithdrawNominationCollateral { nominator_id: account_of([0x6b; 32]), vault_id: VaultId { account_id: account_of([0x6b; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, amount: Amount::new(82259, Token(KSM)), }, + DepositNominationCollateral { nominator_id: account_of([0x69; 32]), vault_id: VaultId { account_id: account_of([0x01; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(382, Token(DOT)), }, + SetSecureThreshold { vault_id: VaultId { account_id: account_of([0x01; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, threshold: FixedU128::from_float(3.275451943011447296), }, + DepositNominationCollateral { nominator_id: account_of([0x6d; 32]), vault_id: VaultId { account_id: account_of([0x6d; 32]), currencies: VaultCurrencyPair { collateral: Token(KSM), wrapped: Token(KBTC), }, }, amount: Amount::new(549361, Token(KSM)), }, + SetExchangeRate { currency_id: Token(KSM), exchange_rate: FixedU128::from_float(2.314548580022945792), }, + WithdrawNominationCollateral { nominator_id: account_of([0x6c; 32]), vault_id: VaultId { account_id: account_of([0x6c; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, amount: Amount::new(24121, Token(DOT)), }, + SetExchangeRate { currency_id: Token(DOT), exchange_rate: FixedU128::from_float(4.374931332402049024), }, + SetSecureThreshold { vault_id: VaultId { account_id: account_of([0x6c; 32]), currencies: VaultCurrencyPair { collateral: Token(DOT), wrapped: Token(KBTC), }, }, threshold: FixedU128::from_float(4.625228520227672064), }, + ]; + + for action in actions.iter().take(num_actions - 1) { + action.execute(&mut reference_pool); + } + // separated last action for easier breakpointing + for action in actions.iter().skip(num_actions - 1).take(1) { + log::error!("Last action: {action:?}"); + action.execute(&mut reference_pool); + } + + let total_distributed: u128 = actions + .iter() + .take(num_actions) + .filter_map(|x| match x { + ConcreteAction::DistributeRewards { amount } => Some(amount.amount()), + _ => None, + }) + .sum(); + + for ((vault_id, nominator_id), _) in reference_pool.nominations() { + withdraw_vault_rewards(&vault_id); + withdraw_nominator_rewards(&vault_id, &nominator_id); + } + + let total_reference_pool: u128 = reference_pool.rewards().iter().map(|(_, value)| *value).sum(); + let total_actually_received: u128 = reference_pool + .rewards() + .iter() + .map(|(nominator, _)| { + let initial = if vaults.iter().any(|x| &x.account_id == nominator) { + 200_000 + } else { + 0 + }; + let actual_reward = currency::get_free_balance::(REWARD_CURRENCY, &nominator).amount() - initial; + let reference_nominated = reference_pool.get_total_reward_for(&nominator); + let reference_reward = reference_pool + .rewards() + .iter() + .find_map(|x| if &x.0 == nominator { Some(x.1) } else { None }) + .unwrap(); + + let diff = actual_reward as i128 - reference_reward as i128; + + let abs_diff = abs_difference(actual_reward, reference_reward); + + log::error!("actual_reward {nominator:?} {actual_reward} {reference_reward}, diff: {diff}, {reference_nominated}"); + log::error!("Num_Actions: {num_actions}"); + assert!(abs_diff < 1000); + + actual_reward + }) + .sum(); + + log::error!("Total distributed: {total_distributed}"); + log::error!("Total reference_pool: {total_reference_pool}"); + log::error!("Total actually_received: {total_actually_received}"); + log::error!( + "Difference: {}", + abs_difference(total_distributed, total_actually_received) + ); + log::error!("num_actions: {num_actions}"); + + // check the rewards of all stakeholders + for (nominator_id, expected_reward) in reference_pool.rewards() { + // vaults had some initial free balance in the reward currency - compensate for that.. + let actual_reward = if vaults.iter().any(|x| x.account_id == nominator_id) { + currency::get_free_balance::(REWARD_CURRENCY, &nominator_id).amount() - 200_000 + } else { + currency::get_free_balance::(REWARD_CURRENCY, &nominator_id).amount() + }; + + // ensure the difference is small, but allow some rounding errors.. + if abs_difference(actual_reward, expected_reward) > expected_reward / 500 + 10 { + assert_eq!(actual_reward, expected_reward); + } + } + + if abs_difference(total_reference_pool, total_actually_received) >= 1000 { + assert_eq!(total_reference_pool, total_actually_received); + } + }); +} + #[test] fn accrued_lend_token_interest_increases_reward_share() { ExtBuilder::build().execute_with(|| { diff --git a/standalone/runtime/tests/test_redeem.rs b/standalone/runtime/tests/test_redeem.rs index fc6c8f0cbf..623dd3b4c9 100644 --- a/standalone/runtime/tests/test_redeem.rs +++ b/standalone/runtime/tests/test_redeem.rs @@ -58,16 +58,6 @@ fn consume_to_be_replaced(vault: &mut CoreVaultData, amount_btc: Amount vault.to_be_replaced -= to_be_replaced_decrease; } -pub trait RedeemRequestTestExt { - fn amount_without_fee_as_collateral(&self, currency_id: CurrencyId) -> Amount; -} -impl RedeemRequestTestExt for RedeemRequest { - fn amount_without_fee_as_collateral(&self, currency_id: CurrencyId) -> Amount { - let amount_without_fee = self.amount_btc() + self.transfer_fee_btc(); - amount_without_fee.convert_to(currency_id).unwrap() - } -} - mod spec_based_tests { use primitives::VaultCurrencyPair;