From b767bab1e0de32da4ab7b9adf35ac18c96c84ea8 Mon Sep 17 00:00:00 2001 From: akildemir Date: Mon, 2 Sep 2024 15:43:17 +0300 Subject: [PATCH] add post economic security era test --- substrate/emissions/pallet/src/lib.rs | 24 ++- substrate/emissions/pallet/src/mock.rs | 2 +- substrate/emissions/pallet/src/tests.rs | 242 +++++++++++++++++++++++- 3 files changed, 256 insertions(+), 12 deletions(-) diff --git a/substrate/emissions/pallet/src/lib.rs b/substrate/emissions/pallet/src/lib.rs index cbfe906a..b5f97969 100644 --- a/substrate/emissions/pallet/src/lib.rs +++ b/substrate/emissions/pallet/src/lib.rs @@ -337,17 +337,21 @@ pub mod pallet { } // stake the rewards - for (p, score) in scores { - let p_reward = u64::try_from( - u128::from(reward).saturating_mul(u128::from(score)) / u128::from(total_score), - ) - .unwrap(); + let mut total_reward_distributed = 0u64; + for (i, (p, score)) in scores.iter().enumerate() { + let p_reward = if i == (scores.len() - 1) { + reward.saturating_sub(total_reward_distributed) + } else { + u64::try_from( + u128::from(reward).saturating_mul(u128::from(*score)) / u128::from(total_score), + ) + .unwrap() + }; - Coins::::mint(p, Balance { coin: Coin::Serai, amount: Amount(p_reward) }).unwrap(); - if ValidatorSets::::distribute_block_rewards(n, p, Amount(p_reward)).is_err() { - // TODO: log the failure - continue; - } + Coins::::mint(*p, Balance { coin: Coin::Serai, amount: Amount(p_reward) }).unwrap(); + ValidatorSets::::distribute_block_rewards(n, *p, Amount(p_reward)).unwrap(); + + total_reward_distributed = total_reward_distributed.saturating_add(p_reward); } } diff --git a/substrate/emissions/pallet/src/mock.rs b/substrate/emissions/pallet/src/mock.rs index aca52fbe..49e44100 100644 --- a/substrate/emissions/pallet/src/mock.rs +++ b/substrate/emissions/pallet/src/mock.rs @@ -158,7 +158,7 @@ pub(crate) fn new_test_ext() -> sp_io::TestExternalities { insecure_pair_from_name("Eve").public(), insecure_pair_from_name("Ferdie").public(), ]; - let validators = vec![insecure_pair_from_name("Alice").public()]; + let validators = accounts.clone(); let networks = NETWORKS .iter() diff --git a/substrate/emissions/pallet/src/tests.rs b/substrate/emissions/pallet/src/tests.rs index a2c98393..594e2429 100644 --- a/substrate/emissions/pallet/src/tests.rs +++ b/substrate/emissions/pallet/src/tests.rs @@ -16,6 +16,7 @@ use genesis_liquidity_pallet::{ use validator_sets_pallet::{Pallet as ValidatorSets, primitives::Session}; use coins_pallet::Pallet as Coins; use dex_pallet::Pallet as Dex; +use economic_security::Pallet as EconomicSecurity; use serai_primitives::*; use validator_sets_primitives::{KeyPair, ValidatorSet}; @@ -29,7 +30,8 @@ fn set_up_genesis() -> u64 { let mut address = SeraiAddress::new([0; 32]); OsRng.fill_bytes(&mut address.0); - let balance = Balance { coin, amount: Amount(OsRng.next_u64() % 10u64.pow(coin.decimals())) }; + let balance = + Balance { coin, amount: Amount(OsRng.next_u64() % (10_000 * 10u64.pow(coin.decimals()))) }; Coins::::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), balance).unwrap(); GenesisLiquidity::::add_coin_liquidity(address.into(), balance).unwrap(); @@ -99,6 +101,90 @@ fn set_keys_for_session() { } } +fn make_fake_swap_volume() { + let acc = insecure_pair_from_name("random").public(); + for _ in 0 .. 10 { + let path_len = (OsRng.next_u32() % 2) + 2; + + let coins = &COINS[1 ..]; + let path = if path_len == 2 { + let coin = coins[(OsRng.next_u32() as usize) % coins.len()]; + let in_or_out = (OsRng.next_u32() % 2) == 0; + if in_or_out { + vec![coin, Coin::Serai] + } else { + vec![Coin::Serai, coin] + } + } else { + let in_coin = coins[(OsRng.next_u32() as usize) % coins.len()]; + let coins_without_in_coin = coins.iter().filter(|&c| *c != in_coin).collect::>(); + let out_coin = + coins_without_in_coin[(OsRng.next_u32() as usize) % coins_without_in_coin.len()]; + vec![in_coin, Coin::Serai, *out_coin] + }; + + let one_in_coin = 10u64.pow(path[0].decimals()); + Coins::::mint(acc, Balance { coin: path[0], amount: Amount(2 * one_in_coin) }).unwrap(); + let amount_in = OsRng.next_u64() % (one_in_coin); + + Dex::::swap_exact_tokens_for_tokens( + RawOrigin::Signed(acc).into(), + path.try_into().unwrap(), + amount_in, + 1, + acc, + ) + .unwrap(); + } +} + +fn get_session_swap_volumes( + last_swap_volume: &mut BTreeMap, +) -> (BTreeMap, BTreeMap, u64) { + let mut volume_per_coin: BTreeMap = BTreeMap::new(); + for c in COINS { + // this should return 0 for SRI and so it shouldn't affect the total volume. + let current_volume = Dex::::swap_volume(c).unwrap_or(0); + let last_volume = last_swap_volume.get(&c).unwrap_or(&0); + let vol_this_epoch = current_volume.saturating_sub(*last_volume); + + // update the current volume + last_swap_volume.insert(c, current_volume); + volume_per_coin.insert(c, vol_this_epoch); + } + + // aggregate per network + let mut total_volume = 0u64; + let mut volume_per_network: BTreeMap = BTreeMap::new(); + for (c, vol) in &volume_per_coin { + volume_per_network.insert( + c.network(), + (*volume_per_network.get(&c.network()).unwrap_or(&0)).saturating_add(*vol), + ); + total_volume = total_volume.saturating_add(*vol); + } + + (volume_per_coin, volume_per_network, total_volume) +} + +fn get_pool_vs_validator_rewards(n: NetworkId, reward: u64) -> (u64, u64) { + if n == NetworkId::Serai { + (reward, 0) + } else { + // calculate pool vs validator share + let capacity = ValidatorSets::::total_allocated_stake(n).unwrap_or(Amount(0)).0; + let required = ValidatorSets::::required_stake_for_network(n); + let unused_capacity = capacity.saturating_sub(required); + + let distribution = unused_capacity.saturating_mul(ACCURACY_MULTIPLIER) / capacity; + let total = DESIRED_DISTRIBUTION.saturating_add(distribution); + + let validators_reward = DESIRED_DISTRIBUTION.saturating_mul(reward) / total; + let network_pool_reward = reward.saturating_sub(validators_reward); + (validators_reward, network_pool_reward) + } +} + #[test] fn check_pre_ec_security_initial_period_emissions() { new_test_ext().execute_with(|| { @@ -169,6 +255,17 @@ fn check_pre_ec_security_emissions() { block_number += 2 * MONTHS; System::set_block_number(block_number); + // make a fresh session + set_keys_for_session(); + ValidatorSets::::new_session(); + for network in NETWORKS { + ValidatorSets::::retire_set(ValidatorSet { session: Session(0), network }); + } + + // move the block for the next session + block_number += <::EpochDuration as Get>::get(); + System::set_block_number(block_number); + for _ in 0 .. 5 { // set session keys. we need this here before reading the current stakes for session 0. // We need it for other sessions to be able to retire the set. @@ -222,3 +319,146 @@ fn check_pre_ec_security_emissions() { } }); } + +#[test] +fn check_post_ec_security_emissions() { + new_test_ext().execute_with(|| { + // set up genesis liquidity + let mut block_number = set_up_genesis(); + + // make all networks reach economic security + set_keys_for_session(); + let (distances, _) = distances(); + for (network, distance) in distances { + if network == NetworkId::Serai { + continue; + } + + let participants = + ValidatorSets::::participants_for_latest_decided_set(network).unwrap(); + let al_per_key_share = ValidatorSets::::allocation_per_key_share(network).unwrap().0; + + // we want some unused capacity so we stake more SRI than necessary + let mut key_shares = (distance / al_per_key_share) + 10; + + 'outer: while key_shares > 0 { + for (account, _) in &participants { + ValidatorSets::::distribute_block_rewards( + network, + *account, + Amount(al_per_key_share), + ) + .unwrap(); + + if key_shares > 0 { + key_shares -= 1; + } else { + break 'outer; + } + } + } + } + + // update TAS + ValidatorSets::::new_session(); + for network in NETWORKS { + ValidatorSets::::retire_set(ValidatorSet { session: Session(0), network }); + } + + // make sure we reached economic security + as Hooks>::on_initialize(block_number); + for n in NETWORKS.iter().filter(|&n| *n != NetworkId::Serai).collect::>() { + EconomicSecurity::::economic_security_block(*n).unwrap(); + } + + // move the block number for the next session + block_number += <::EpochDuration as Get>::get(); + System::set_block_number(block_number); + + let mut last_swap_volume = BTreeMap::new(); + for _ in 0 .. 5 { + set_keys_for_session(); + + // make some fake swap volume + make_fake_swap_volume(); + let (vpc, vpn, total_volume) = get_session_swap_volumes(&mut last_swap_volume); + + // get current stakes & each pool SRI amounts + let mut current_stake = BTreeMap::new(); + let mut current_pool_coins = BTreeMap::new(); + for n in NETWORKS { + current_stake.insert(n, ValidatorSets::::total_allocated_stake(n).unwrap().0); + + for c in n.coins() { + let acc = Dex::::get_pool_account(*c); + current_pool_coins.insert(c, Coins::::balance(acc, Coin::Serai).0); + } + } + + // trigger rewards distribution for the past session + ValidatorSets::::new_session(); + >::on_initialize(block_number + 1); + + // calculate the total reward for this epoch + let session = ValidatorSets::::session(NetworkId::Serai).unwrap_or(Session(0)); + let block_count = ValidatorSets::::session_begin_block(NetworkId::Serai, session) - + ValidatorSets::::session_begin_block(NetworkId::Serai, Session(session.0 - 1)); + let reward_this_epoch = block_count * REWARD_PER_BLOCK; + + let reward_per_network = vpn + .iter() + .map(|(n, volume)| { + let reward = if *n == NetworkId::Serai { + reward_this_epoch / 5 + } else { + let reward = reward_this_epoch - (reward_this_epoch / 5); + // TODO: It is highly unlikely but what to do in case of 0 total volume? + if total_volume != 0 { + u64::try_from( + u128::from(reward).saturating_mul(u128::from(*volume)) / u128::from(total_volume), + ) + .unwrap() + } else { + 0 + } + }; + (*n, reward) + }) + .collect::>(); + + for (n, reward) in reward_per_network { + let (validator_rewards, network_pool_rewards) = get_pool_vs_validator_rewards(n, reward); + ValidatorSets::::retire_set(ValidatorSet { + session: Session(session.0 - 1), + network: n, + }); + + // all validator rewards should automatically be staked + assert_eq!( + ValidatorSets::::total_allocated_stake(n).unwrap().0, + *current_stake.get(&n).unwrap() + validator_rewards + ); + + // all pool rewards should be available in the pool account + if network_pool_rewards != 0 { + for c in n.coins() { + let pool_reward = u64::try_from( + u128::from(network_pool_rewards).saturating_mul(u128::from(vpc[c])) / + u128::from(vpn[&n]), + ) + .unwrap(); + + let acc = Dex::::get_pool_account(*c); + assert_eq!( + Coins::::balance(acc, Coin::Serai).0, + current_pool_coins[&c] + pool_reward + ) + } + } + } + + block_number += <::EpochDuration as Get>::get(); + System::set_block_number(block_number); + } + }); +}