diff --git a/Cargo.lock b/Cargo.lock index e6ff320e..1a302f6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8096,8 +8096,13 @@ version = "0.1.0" dependencies = [ "frame-support", "frame-system", + "pallet-babe", + "pallet-grandpa", + "pallet-timestamp", "parity-scale-codec", + "rand_core", "scale-info", + "serai-abi", "serai-coins-pallet", "serai-dex-pallet", "serai-emissions-primitives", @@ -8105,6 +8110,8 @@ dependencies = [ "serai-primitives", "serai-validator-sets-pallet", "serai-validator-sets-primitives", + "sp-core", + "sp-io", "sp-runtime", "sp-std", ] diff --git a/substrate/emissions/pallet/Cargo.toml b/substrate/emissions/pallet/Cargo.toml index d7f9bc59..d16ef120 100644 --- a/substrate/emissions/pallet/Cargo.toml +++ b/substrate/emissions/pallet/Cargo.toml @@ -37,6 +37,18 @@ serai-primitives = { path = "../../primitives", default-features = false } validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../../validator-sets/primitives", default-features = false } emissions-primitives = { package = "serai-emissions-primitives", path = "../primitives", default-features = false } +[dev-dependencies] +pallet-babe = { git = "https://github.com/serai-dex/substrate", default-features = false } +pallet-grandpa = { git = "https://github.com/serai-dex/substrate", default-features = false } +pallet-timestamp = { git = "https://github.com/serai-dex/substrate", default-features = false } + +sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false } + +serai-abi = { path = "../../abi", default-features = false, features = ["serde"] } + +rand_core = "0.6" + [features] std = [ "scale/std", @@ -47,6 +59,11 @@ std = [ "sp-std/std", "sp-runtime/std", + "sp-core/std", + "sp-io/std", + + "serai-abi/std", + "serai-abi/serde", "coins-pallet/std", "validator-sets-pallet/std", @@ -55,7 +72,19 @@ std = [ "serai-primitives/std", "emissions-primitives/std", + + "pallet-babe/std", + "pallet-grandpa/std", + "pallet-timestamp/std", ] + +try-runtime = [ + "frame-system/try-runtime", + "frame-support/try-runtime", + + "sp-runtime/try-runtime", +] + fast-epoch = [] -try-runtime = [] # TODO + default = ["std"] diff --git a/substrate/emissions/pallet/src/lib.rs b/substrate/emissions/pallet/src/lib.rs index e280ea89..9d7516db 100644 --- a/substrate/emissions/pallet/src/lib.rs +++ b/substrate/emissions/pallet/src/lib.rs @@ -1,5 +1,11 @@ #![cfg_attr(not(feature = "std"), no_std)] +#[cfg(test)] +mod tests; + +#[cfg(test)] +mod mock; + #[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding, clippy::empty_docs)] #[frame_support::pallet] pub mod pallet { @@ -295,22 +301,18 @@ pub mod pallet { ) .unwrap(); - if Coins::::mint( + Coins::::mint( Dex::::get_pool_account(*c), Balance { coin: Coin::Serai, amount: Amount(pool_reward) }, ) - .is_err() - { - // TODO: log the failure - continue; - } + .unwrap(); } } } // TODO: we have the past session participants here in the emissions pallet so that we can // distribute rewards to them in the next session. Ideally we should be able to fetch this - // information from valiadtor sets pallet. + // information from validator sets pallet. Self::update_participants(); Weight::zero() // TODO } @@ -323,15 +325,9 @@ pub mod pallet { } fn initial_period(n: BlockNumberFor) -> bool { - #[cfg(feature = "fast-epoch")] - let initial_period_duration = FAST_EPOCH_INITIAL_PERIOD; - - #[cfg(not(feature = "fast-epoch"))] - let initial_period_duration = 2 * MONTHS; - let genesis_complete_block = GenesisLiquidity::::genesis_complete_block(); genesis_complete_block.is_some() && - (n.saturated_into::() < (genesis_complete_block.unwrap() + initial_period_duration)) + (n.saturated_into::() < (genesis_complete_block.unwrap() + (2 * MONTHS))) } /// Returns true if any of the external networks haven't reached economic security yet. diff --git a/substrate/emissions/pallet/src/mock.rs b/substrate/emissions/pallet/src/mock.rs new file mode 100644 index 00000000..6bc6eead --- /dev/null +++ b/substrate/emissions/pallet/src/mock.rs @@ -0,0 +1,191 @@ +//! Test environment for Dex pallet. + +use super::*; + +use frame_support::{ + construct_runtime, + traits::{ConstU16, ConstU32, ConstU64}, +}; + +use sp_core::{H256, Pair, sr25519::Public}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +use serai_primitives::*; +use validator_sets::{primitives::MAX_KEY_SHARES_PER_SET, MembershipProof}; + +use crate as emissions; +pub use coins_pallet as coins; +pub use validator_sets_pallet as validator_sets; +pub use genesis_liquidity_pallet as genesis_liquidity; +pub use dex_pallet as dex; +pub use pallet_babe as babe; +pub use pallet_grandpa as grandpa; +pub use pallet_timestamp as timestamp; + +type Block = frame_system::mocking::MockBlock; +// Maximum number of authorities per session. +pub type MaxAuthorities = ConstU32<{ MAX_KEY_SHARES_PER_SET }>; + +pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = 10; + +construct_runtime!( + pub enum Test + { + System: frame_system, + Timestamp: timestamp, + Coins: coins, + LiquidityTokens: coins::::{Pallet, Call, Storage, Event}, + Emissions: emissions, + ValidatorSets: validator_sets, + GenesisLiquidity: genesis_liquidity, + Dex: dex, + Babe: babe, + Grandpa: grandpa, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = Public; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = Babe; + type MinimumPeriod = ConstU64<{ (TARGET_BLOCK_TIME * 1000) / 2 }>; + type WeightInfo = (); +} + +impl babe::Config for Test { + type EpochDuration = ConstU64<{ FAST_EPOCH_DURATION }>; + + type ExpectedBlockTime = ConstU64<{ TARGET_BLOCK_TIME * 1000 }>; + type EpochChangeTrigger = babe::ExternalTrigger; + type DisabledValidators = ValidatorSets; + + type WeightInfo = (); + type MaxAuthorities = MaxAuthorities; + + type KeyOwnerProof = MembershipProof; + type EquivocationReportSystem = (); +} + +impl grandpa::Config for Test { + type RuntimeEvent = RuntimeEvent; + + type WeightInfo = (); + type MaxAuthorities = MaxAuthorities; + + type MaxSetIdSessionEntries = ConstU64<0>; + type KeyOwnerProof = MembershipProof; + type EquivocationReportSystem = (); +} + +impl coins::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AllowMint = ValidatorSets; +} + +impl coins::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AllowMint = (); +} + +impl dex::Config for Test { + type RuntimeEvent = RuntimeEvent; + + type LPFee = ConstU32<3>; // 0.3% + type MintMinLiquidity = ConstU64<10000>; + + type MaxSwapPathLength = ConstU32<3>; // coin1 -> SRI -> coin2 + + type MedianPriceWindowLength = ConstU16<{ MEDIAN_PRICE_WINDOW_LENGTH }>; + + type WeightInfo = dex::weights::SubstrateWeight; +} + +impl validator_sets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = Babe; +} + +impl genesis_liquidity::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + let accounts: Vec = vec![ + insecure_pair_from_name("Alice").public(), + insecure_pair_from_name("Bob").public(), + insecure_pair_from_name("Charlie").public(), + insecure_pair_from_name("Dave").public(), + insecure_pair_from_name("Eve").public(), + insecure_pair_from_name("Ferdie").public(), + ]; + let validators = vec![insecure_pair_from_name("Alice").public()]; + + let networks = NETWORKS + .iter() + .map(|network| match network { + NetworkId::Serai => (NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))), + NetworkId::Bitcoin => (NetworkId::Bitcoin, Amount(1_000_000 * 10_u64.pow(8))), + NetworkId::Ethereum => (NetworkId::Ethereum, Amount(1_000_000 * 10_u64.pow(8))), + NetworkId::Monero => (NetworkId::Monero, Amount(100_000 * 10_u64.pow(8))), + }) + .collect::>(); + + coins::GenesisConfig:: { + accounts: accounts + .into_iter() + .map(|a| (a, Balance { coin: Coin::Serai, amount: Amount(1 << 60) })) + .collect(), + _ignore: Default::default(), + } + .assimilate_storage(&mut t) + .unwrap(); + + validator_sets::GenesisConfig:: { + networks: networks.clone(), + participants: validators.clone(), + } + .assimilate_storage(&mut t) + .unwrap(); + + crate::GenesisConfig:: { networks, participants: validators.clone() } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(0)); + ext +} diff --git a/substrate/emissions/pallet/src/tests.rs b/substrate/emissions/pallet/src/tests.rs new file mode 100644 index 00000000..a2c98393 --- /dev/null +++ b/substrate/emissions/pallet/src/tests.rs @@ -0,0 +1,224 @@ +use crate::{mock::*, primitives::*}; + +use rand_core::{RngCore, OsRng}; + +use sp_core::{sr25519::Signature, Pair}; +use sp_std::{vec, collections::btree_map::BTreeMap}; +use sp_runtime::BoundedVec; + +use frame_system::RawOrigin; +use frame_support::traits::{Hooks, Get}; + +use genesis_liquidity_pallet::{ + Pallet as GenesisLiquidity, + primitives::{Values, GENESIS_LIQUIDITY_ACCOUNT}, +}; +use validator_sets_pallet::{Pallet as ValidatorSets, primitives::Session}; +use coins_pallet::Pallet as Coins; +use dex_pallet::Pallet as Dex; + +use serai_primitives::*; +use validator_sets_primitives::{KeyPair, ValidatorSet}; + +fn set_up_genesis() -> u64 { + // add some genesis liquidity + for coin in COINS { + if coin == Coin::Serai { + continue; + } + + 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())) }; + + Coins::::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), balance).unwrap(); + GenesisLiquidity::::add_coin_liquidity(address.into(), balance).unwrap(); + } + + // make genesis liquidity event happen + let block_number = MONTHS; + let values = Values { monero: 184100, ether: 4785000, dai: 1500 }; + GenesisLiquidity::::oraclize_values(RawOrigin::None.into(), values, Signature([0u8; 64])) + .unwrap(); + as Hooks>::on_initialize(block_number); + System::set_block_number(block_number); + + // populate the coin values + as Hooks>::on_finalize(block_number); + + block_number +} + +// TODO: make this fn belong to the pallet itself use it there as well? +// The problem with that would be if there is a problem with this function +// tests can't catch it since it would the same fn? +fn distances() -> (BTreeMap, u64) { + let mut distances = BTreeMap::new(); + let mut total_distance: u64 = 0; + + // calculate distance to economic security per network + for n in NETWORKS { + if n == NetworkId::Serai { + continue; + } + + let required = ValidatorSets::::required_stake_for_network(n); + let mut current = ValidatorSets::::total_allocated_stake(n).unwrap_or(Amount(0)).0; + if current > required { + current = required; + } + + let distance = required - current; + distances.insert(n, distance); + total_distance = total_distance.saturating_add(distance); + } + + // add serai network portion (20%) + let new_total_distance = + total_distance.saturating_mul(100) / (100 - SERAI_VALIDATORS_DESIRED_PERCENTAGE); + distances.insert(NetworkId::Serai, new_total_distance - total_distance); + total_distance = new_total_distance; + + (distances, total_distance) +} + +fn set_keys_for_session() { + for n in NETWORKS { + if n == NetworkId::Serai { + continue; + } + + ValidatorSets::::set_keys( + RawOrigin::None.into(), + n, + BoundedVec::new(), + KeyPair(insecure_pair_from_name("Alice").public(), vec![].try_into().unwrap()), + Signature([0u8; 64]), + ) + .unwrap(); + } +} + +#[test] +fn check_pre_ec_security_initial_period_emissions() { + new_test_ext().execute_with(|| { + // set up genesis liquidity + let mut block_number = set_up_genesis(); + + 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. + set_keys_for_session(); + + // get current stakes + let mut current_stake = BTreeMap::new(); + for n in NETWORKS { + current_stake.insert(n, ValidatorSets::::total_allocated_stake(n).unwrap().0); + } + + // trigger rewards distribution for the past session + ValidatorSets::::new_session(); + >::on_initialize(block_number + 1); + + // calculate the total reward for this epoch + let (distances, total_distance) = distances(); + 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 * INITIAL_REWARD_PER_BLOCK; + + let reward_per_network = distances + .into_iter() + .map(|(n, distance)| { + // calculate how much each network gets based on distance to ec-security + let reward = u64::try_from( + u128::from(reward_this_epoch).saturating_mul(u128::from(distance)) / + u128::from(total_distance), + ) + .unwrap(); + (n, reward) + }) + .collect::>(); + + for (n, reward) in reward_per_network { + 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() + reward + ); + } + + block_number += <::EpochDuration as Get>::get(); + System::set_block_number(block_number); + } + }); +} + +#[test] +fn check_pre_ec_security_emissions() { + new_test_ext().execute_with(|| { + // set up genesis liquidity + let mut block_number = set_up_genesis(); + + // move the block number out of initial period which is 2 more months + block_number += 2 * MONTHS; + 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. + set_keys_for_session(); + + // get current stakes + let mut current_stake = BTreeMap::new(); + for n in NETWORKS { + current_stake.insert(n, ValidatorSets::::total_allocated_stake(n).unwrap().0); + } + + // trigger rewards distribution for the past session + ValidatorSets::::new_session(); + >::on_initialize(block_number + 1); + + // calculate the total reward for this epoch + let (distances, total_distance) = distances(); + 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 * (total_distance / (SECURE_BY - block_number)); + + let reward_per_network = distances + .into_iter() + .map(|(n, distance)| { + // calculate how much each network gets based on distance to ec-security + let reward = u64::try_from( + u128::from(reward_this_epoch).saturating_mul(u128::from(distance)) / + u128::from(total_distance), + ) + .unwrap(); + (n, reward) + }) + .collect::>(); + + for (n, reward) in reward_per_network { + 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() + reward + ); + } + + block_number += <::EpochDuration as Get>::get(); + System::set_block_number(block_number); + } + }); +} diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index a404ae73..1a41c7cf 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -390,6 +390,7 @@ pub mod pallet { let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0; let mut participants = vec![]; + let mut total_allocated_stake = 0; { let mut iter = SortedAllocationsIter::::new(network); let mut key_shares = 0; @@ -400,6 +401,7 @@ pub mod pallet { (amount.0 / allocation_per_key_share).min(u64::from(MAX_KEY_SHARES_PER_SET)); participants.push((key, these_key_shares)); + total_allocated_stake += amount.0; key_shares += these_key_shares; } amortize_excess_key_shares(&mut participants); @@ -413,6 +415,10 @@ pub mod pallet { Pallet::::deposit_event(Event::NewSet { set }); Participants::::set(network, Some(participants.try_into().unwrap())); + if network == NetworkId::Serai { + TotalAllocatedStake::::set(network, Some(Amount(total_allocated_stake))); + } + SessionBeginBlock::::set( network, session, @@ -705,7 +711,7 @@ pub mod pallet { (!Keys::::contains_key(ValidatorSet { network, session: Session(session.0 - 1) })) } - fn new_session() { + pub fn new_session() { for network in serai_primitives::NETWORKS { // If this network hasn't started sessions yet, don't start one now let Some(current_session) = Self::session(network) else { continue };