From 30df83786e6ab88fe7a57176b5a1f563413dc450 Mon Sep 17 00:00:00 2001 From: akildemir Date: Fri, 10 May 2024 12:22:33 +0300 Subject: [PATCH] add initial reward era test --- Cargo.lock | 3 +- substrate/abi/Cargo.toml | 1 + substrate/abi/src/emissions.rs | 2 + substrate/client/src/serai/dex.rs | 6 + .../client/tests/common/genesis_liquidity.rs | 227 ++++++++++++++++++ substrate/client/tests/common/mod.rs | 1 + substrate/client/tests/emissions.rs | 141 +++++++++++ substrate/client/tests/genesis_liquidity.rs | 226 +---------------- substrate/emissions/pallet/Cargo.toml | 6 - substrate/emissions/pallet/src/lib.rs | 165 +++++++------ substrate/node/src/chain_spec.rs | 20 +- 11 files changed, 496 insertions(+), 302 deletions(-) create mode 100644 substrate/client/tests/common/genesis_liquidity.rs create mode 100644 substrate/client/tests/emissions.rs diff --git a/Cargo.lock b/Cargo.lock index 5c25235a..48254e73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7652,6 +7652,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serai-coins-primitives", + "serai-emissions-primitives", "serai-genesis-liquidity-primitives", "serai-in-instructions-primitives", "serai-primitives", @@ -7817,7 +7818,6 @@ version = "0.1.0" dependencies = [ "frame-support", "frame-system", - "pallet-babe", "parity-scale-codec", "scale-info", "serai-coins-pallet", @@ -7828,7 +7828,6 @@ dependencies = [ "serai-validator-sets-pallet", "serai-validator-sets-primitives", "sp-runtime", - "sp-session", "sp-std", ] diff --git a/substrate/abi/Cargo.toml b/substrate/abi/Cargo.toml index ac294930..e442e86c 100644 --- a/substrate/abi/Cargo.toml +++ b/substrate/abi/Cargo.toml @@ -32,6 +32,7 @@ serai-primitives = { path = "../primitives", version = "0.1" } serai-coins-primitives = { path = "../coins/primitives", version = "0.1" } serai-validator-sets-primitives = { path = "../validator-sets/primitives", version = "0.1" } serai-genesis-liquidity-primitives = { path = "../genesis-liquidity/primitives", version = "0.1" } +serai-emissions-primitives = { path = "../emissions/primitives", version = "0.1" } serai-in-instructions-primitives = { path = "../in-instructions/primitives", version = "0.1" } serai-signals-primitives = { path = "../signals/primitives", version = "0.1" } diff --git a/substrate/abi/src/emissions.rs b/substrate/abi/src/emissions.rs index 04634c6a..cb576611 100644 --- a/substrate/abi/src/emissions.rs +++ b/substrate/abi/src/emissions.rs @@ -1,3 +1,5 @@ +pub use serai_emissions_primitives as primitives; + #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/substrate/client/src/serai/dex.rs b/substrate/client/src/serai/dex.rs index 18341125..39699ba8 100644 --- a/substrate/client/src/serai/dex.rs +++ b/substrate/client/src/serai/dex.rs @@ -7,6 +7,8 @@ use crate::{SeraiError, hex_decode, TemporalSerai}; pub type DexEvent = serai_abi::dex::Event; +const PALLET: &str = "Dex"; + #[derive(Clone, Copy)] pub struct SeraiDex<'a>(pub(crate) &'a TemporalSerai<'a>); impl<'a> SeraiDex<'a> { @@ -76,4 +78,8 @@ impl<'a> SeraiDex<'a> { .map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?; Ok(resut.map(|amounts| (Amount(amounts.0), Amount(amounts.1)))) } + + pub async fn oracle_value(&self, coin: Coin) -> Result, SeraiError> { + self.0.storage(PALLET, "SecurityOracleValue", coin).await + } } diff --git a/substrate/client/tests/common/genesis_liquidity.rs b/substrate/client/tests/common/genesis_liquidity.rs new file mode 100644 index 00000000..33699377 --- /dev/null +++ b/substrate/client/tests/common/genesis_liquidity.rs @@ -0,0 +1,227 @@ +use std::{time::Duration, collections::HashMap}; + +use rand_core::{RngCore, OsRng}; +use zeroize::Zeroizing; + +use ciphersuite::{Ciphersuite, Ristretto}; +use frost::dkg::musig::musig; +use schnorrkel::Schnorrkel; + +use serai_client::{ + genesis_liquidity::{ + primitives::{GENESIS_LIQUIDITY_ACCOUNT, GENESIS_SRI}, + SeraiGenesisLiquidity, + }, + validator_sets::primitives::{musig_context, Session, ValidatorSet}, +}; + +use serai_abi::{ + genesis_liquidity::primitives::{set_initial_price_message, Prices}, + primitives::COINS, +}; + +use sp_core::{sr25519::Signature, Pair as PairTrait}; + +use serai_client::{ + primitives::{ + Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, insecure_pair_from_name, + }, + in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch}, + Serai, +}; + +use crate::common::{in_instructions::provide_batch, tx::publish_tx}; + +#[allow(dead_code)] +pub async fn test_genesis_liquidity(serai: Serai) { + // amounts + let amounts = vec![ + Amount(5_53246991), + Amount(3_14562819), + Amount(9_33648912), + Amount(150_873639000000), + Amount(248_665228000000), + Amount(451_765529000000), + ]; + + // addresses + let mut btc_addresses = vec![]; + let mut xmr_addresses = vec![]; + let addr_count = amounts.len(); + for (i, amount) in amounts.into_iter().enumerate() { + let mut address = SeraiAddress::new([0; 32]); + OsRng.fill_bytes(&mut address.0); + if i < addr_count / 2 { + btc_addresses.push((address, amount)); + } else { + xmr_addresses.push((address, amount)); + } + } + btc_addresses.sort_by(|a1, a2| a1.0.cmp(&a2.0)); + xmr_addresses.sort_by(|a1, a2| a1.0.cmp(&a2.0)); + + // btc batch + let mut block_hash = BlockHash([0; 32]); + OsRng.fill_bytes(&mut block_hash.0); + let btc_ins = btc_addresses + .iter() + .map(|(addr, amount)| InInstructionWithBalance { + instruction: InInstruction::GenesisLiquidity(*addr), + balance: Balance { coin: Coin::Bitcoin, amount: *amount }, + }) + .collect::>(); + let batch = + Batch { network: NetworkId::Bitcoin, id: 0, block: block_hash, instructions: btc_ins }; + provide_batch(&serai, batch).await; + + // xmr batch + let mut block_hash = BlockHash([0; 32]); + OsRng.fill_bytes(&mut block_hash.0); + let xmr_ins = xmr_addresses + .iter() + .map(|(addr, amount)| InInstructionWithBalance { + instruction: InInstruction::GenesisLiquidity(*addr), + balance: Balance { coin: Coin::Monero, amount: *amount }, + }) + .collect::>(); + let batch = Batch { network: NetworkId::Monero, id: 0, block: block_hash, instructions: xmr_ins }; + provide_batch(&serai, batch).await; + + // set prices + let prices = Prices { bitcoin: 10u64.pow(8), monero: 184100, ethereum: 4785000, dai: 1500 }; + set_prices(&serai, &prices).await; + + // wait until genesis ends.. + tokio::time::timeout(tokio::time::Duration::from_secs(300), async { + while serai.latest_finalized_block().await.unwrap().number() < 25 { + tokio::time::sleep(Duration::from_secs(6)).await; + } + }) + .await + .unwrap(); + + // check total SRI supply is +100M + let last_block = serai.latest_finalized_block().await.unwrap().hash(); + let serai = serai.as_of(last_block); + // Check balance instead of supply + let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap(); + // there are 6 endowed accounts in dev-net. Take this into consideration when checking + // for the total sri minted at this time. + let endowed_amount: u64 = 1 << 60; + let total_sri = (6 * endowed_amount) + GENESIS_SRI; + assert_eq!(sri, Amount(total_sri)); + + // check genesis account has no coins, all transferred to pools. + for coin in COINS { + let amount = serai.coins().coin_balance(coin, GENESIS_LIQUIDITY_ACCOUNT).await.unwrap(); + assert_eq!(amount.0, 0); + } + + // check pools has proper liquidity + let pool_btc = btc_addresses.iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); + let pool_xmr = xmr_addresses.iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); + + let pool_btc_value = (pool_btc * u128::from(prices.bitcoin)) / 10u128.pow(8); + let pool_xmr_value = (pool_xmr * u128::from(prices.monero)) / 10u128.pow(12); + let total_value = pool_btc_value + pool_xmr_value; + + // calculated distributed SRI. We know that xmr is at the end of COINS array + // so it will be approximated to roof instead of floor after integer division. + let btc_sri = (pool_btc_value * u128::from(GENESIS_SRI)) / total_value; + let xmr_sri = ((pool_xmr_value * u128::from(GENESIS_SRI)) / total_value) + 1; + + let btc_reserves = serai.dex().get_reserves(Coin::Bitcoin, Coin::Serai).await.unwrap().unwrap(); + assert_eq!(u128::from(btc_reserves.0 .0), pool_btc); + assert_eq!(u128::from(btc_reserves.1 .0), btc_sri); + + let xmr_reserves = serai.dex().get_reserves(Coin::Monero, Coin::Serai).await.unwrap().unwrap(); + assert_eq!(u128::from(xmr_reserves.0 .0), pool_xmr); + assert_eq!(u128::from(xmr_reserves.1 .0), xmr_sri); + + // check each btc liq provider got liq tokens proportional to their value + let btc_liq_token_supply = u128::from( + serai + .liquidity_tokens() + .token_balance(Coin::Bitcoin, GENESIS_LIQUIDITY_ACCOUNT) + .await + .unwrap() + .0, + ); + let mut total_tokens_this_coin: u128 = 0; + for (i, (addr, amount)) in btc_addresses.iter().enumerate() { + let addr_value = (u128::from(amount.0) * u128::from(prices.bitcoin)) / 10u128.pow(8); + let addr_liq_tokens = if i == btc_addresses.len() - 1 { + btc_liq_token_supply - total_tokens_this_coin + } else { + (addr_value * btc_liq_token_supply) / pool_btc_value + }; + + let addr_actual_token_amount = + serai.genesis_liquidity().liquidity_tokens(addr, Coin::Bitcoin).await.unwrap(); + + assert_eq!(addr_liq_tokens, addr_actual_token_amount.0.into()); + total_tokens_this_coin += addr_liq_tokens; + } + + // check each xmr liq provider got liq tokens proportional to their value + let xmr_liq_token_supply = u128::from( + serai + .liquidity_tokens() + .token_balance(Coin::Monero, GENESIS_LIQUIDITY_ACCOUNT) + .await + .unwrap() + .0, + ); + total_tokens_this_coin = 0; + for (i, (addr, amount)) in xmr_addresses.iter().enumerate() { + let addr_value = (u128::from(amount.0) * u128::from(prices.monero)) / 10u128.pow(12); + let addr_liq_tokens = if i == xmr_addresses.len() - 1 { + xmr_liq_token_supply - total_tokens_this_coin + } else { + (addr_value * xmr_liq_token_supply) / pool_xmr_value + }; + + let addr_actual_token_amount = + serai.genesis_liquidity().liquidity_tokens(addr, Coin::Monero).await.unwrap(); + + assert_eq!(addr_liq_tokens, addr_actual_token_amount.0.into()); + total_tokens_this_coin += addr_liq_tokens; + } + + // TODO: remove the liq before/after genesis ended. +} + +#[allow(dead_code)] +async fn set_prices(serai: &Serai, prices: &Prices) { + // prepare a Musig tx to set the initial prices + let pair = insecure_pair_from_name("Alice"); + let public = pair.public(); + let set = ValidatorSet { session: Session(0), network: NetworkId::Serai }; + + let public_key = ::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap(); + let secret_key = ::read_F::<&[u8]>( + &mut pair.as_ref().secret.to_bytes()[.. 32].as_ref(), + ) + .unwrap(); + + assert_eq!(Ristretto::generator() * secret_key, public_key); + let threshold_keys = + musig::(&musig_context(set), &Zeroizing::new(secret_key), &[public_key]).unwrap(); + + let sig = frost::tests::sign_without_caching( + &mut OsRng, + frost::tests::algorithm_machines( + &mut OsRng, + &Schnorrkel::new(b"substrate"), + &HashMap::from([(threshold_keys.params().i(), threshold_keys.into())]), + ), + &set_initial_price_message(&set, prices), + ); + + // set initial prices + let _ = publish_tx( + serai, + &SeraiGenesisLiquidity::set_initial_price(*prices, Signature(sig.to_bytes())), + ) + .await; +} diff --git a/substrate/client/tests/common/mod.rs b/substrate/client/tests/common/mod.rs index e9d88594..7dda7d0a 100644 --- a/substrate/client/tests/common/mod.rs +++ b/substrate/client/tests/common/mod.rs @@ -2,6 +2,7 @@ pub mod tx; pub mod validator_sets; pub mod in_instructions; pub mod dex; +pub mod genesis_liquidity; #[macro_export] macro_rules! serai_test { diff --git a/substrate/client/tests/emissions.rs b/substrate/client/tests/emissions.rs new file mode 100644 index 00000000..8c9fabb2 --- /dev/null +++ b/substrate/client/tests/emissions.rs @@ -0,0 +1,141 @@ +use std::{time::Duration, collections::HashMap}; + +use serai_client::TemporalSerai; + +use serai_abi::{ + emissions::primitives::INITIAL_REWARD_PER_BLOCK, + primitives::{Coin, COINS, NETWORKS}, +}; + +use serai_client::{ + primitives::{Amount, NetworkId, Balance}, + Serai, +}; + +mod common; +use common::genesis_liquidity::test_genesis_liquidity; + +serai_test_fast_epoch!( + emissions: (|serai: Serai| async move { + test_emissions(serai).await; + }) +); + +async fn test_emissions(serai: Serai) { + // provide some genesis liquidity + test_genesis_liquidity(serai.clone()).await; + + let mut current_stake = HashMap::new(); + for n in NETWORKS { + let stake = serai + .as_of_latest_finalized_block() + .await + .unwrap() + .validator_sets() + .total_allocated_stake(n) + .await + .unwrap() + .unwrap_or(Amount(0)) + .0; + current_stake.insert(n, stake); + } + + // wait until we have at least 1 session, epoch time is half an hour with the fast epoch + // feature, so lets wait double that. + tokio::time::timeout(tokio::time::Duration::from_secs(60 * 3), async { + while serai + .as_of_latest_finalized_block() + .await + .unwrap() + .validator_sets() + .session(NetworkId::Serai) + .await + .unwrap() + .unwrap() + .0 < + 1 + { + tokio::time::sleep(Duration::from_secs(6)).await; + } + }) + .await + .unwrap(); + + let last_block = serai.latest_finalized_block().await.unwrap(); + let serai_latest = serai.as_of(last_block.hash()); + + // we should be in the initial period, so calculate how much each network supposedly get.. + // we can check the supply to see how much coin hence liability we have. + let mut distances: HashMap = HashMap::new(); + let mut total_distance = 0; + for coin in COINS { + if coin == Coin::Serai { + continue; + } + + let amount = serai_latest.coins().coin_supply(coin).await.unwrap(); + let required = required_stake(&serai_latest, Balance { coin, amount }).await; + let mut current = *current_stake.get(&coin.network()).unwrap(); + if current > required { + current = required; + } + + let distance = required - current; + total_distance += distance; + + distances.insert( + coin.network(), + distances.get(&coin.network()).unwrap_or(&0).saturating_add(distance), + ); + } + + // add serai network portion(20%) + let new_total_distance = total_distance.saturating_mul(10) / 8; + distances.insert(NetworkId::Serai, new_total_distance - total_distance); + total_distance = new_total_distance; + + // since we should be in the first block after the first epoch, block number should also + // give us the block count. + let block_count = last_block.number(); + let reward_this_epoch = block_count * INITIAL_REWARD_PER_BLOCK; + + let reward_per_network = distances + .into_iter() + .map(|(n, distance)| { + 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 { + let stake = + serai_latest.validator_sets().total_allocated_stake(n).await.unwrap().unwrap_or(Amount(0)).0; + + // the reward should have been automatically staked for the network + assert_eq!(stake, *current_stake.get(&n).unwrap() + reward); + } + + // TODO: check stake per address +} + +/// Returns the required stake in terms SRI for a given `Balance`. +async fn required_stake(serai: &TemporalSerai<'_>, balance: Balance) -> u64 { + // This is inclusive to an increase in accuracy + let sri_per_coin = serai.dex().oracle_value(balance.coin).await.unwrap().unwrap_or(Amount(0)); + + // See dex-pallet for the reasoning on these + let coin_decimals = balance.coin.decimals().max(5); + let accuracy_increase = u128::from(u64::pow(10, coin_decimals)); + + let total_coin_value = + u64::try_from(u128::from(balance.amount.0) * u128::from(sri_per_coin.0) / accuracy_increase) + .unwrap_or(u64::MAX); + + // required stake formula (COIN_VALUE * 1.5) + margin(20%) + let required_stake = total_coin_value.saturating_mul(3).saturating_div(2); + required_stake.saturating_add(total_coin_value.saturating_div(5)) +} diff --git a/substrate/client/tests/genesis_liquidity.rs b/substrate/client/tests/genesis_liquidity.rs index 9efc8f09..c61d6e70 100644 --- a/substrate/client/tests/genesis_liquidity.rs +++ b/substrate/client/tests/genesis_liquidity.rs @@ -1,232 +1,10 @@ -use std::{time::Duration, collections::HashMap}; - -use rand_core::{RngCore, OsRng}; -use zeroize::Zeroizing; - -use ciphersuite::{Ciphersuite, Ristretto}; -use frost::dkg::musig::musig; -use schnorrkel::Schnorrkel; - -use serai_client::{ - genesis_liquidity::{ - primitives::{GENESIS_LIQUIDITY_ACCOUNT, GENESIS_SRI}, - SeraiGenesisLiquidity, - }, - validator_sets::primitives::{musig_context, Session, ValidatorSet}, -}; - -use serai_abi::{ - genesis_liquidity::primitives::{set_initial_price_message, Prices}, - primitives::COINS, -}; - -use sp_core::{sr25519::Signature, Pair as PairTrait}; - -use serai_client::{ - primitives::{ - Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, insecure_pair_from_name, - }, - in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch}, - Serai, -}; +use serai_client::Serai; mod common; -use common::{in_instructions::provide_batch, tx::publish_tx}; +use common::genesis_liquidity::test_genesis_liquidity; serai_test_fast_epoch!( genesis_liquidity: (|serai: Serai| async move { test_genesis_liquidity(serai).await; }) ); - -async fn test_genesis_liquidity(serai: Serai) { - // amounts - let amounts = vec![ - Amount(5_53246991), - Amount(3_14562819), - Amount(9_33648912), - Amount(150_873639000000), - Amount(248_665228000000), - Amount(451_765529000000), - ]; - - // addresses - let mut btc_addresses = vec![]; - let mut xmr_addresses = vec![]; - let addr_count = amounts.len(); - for (i, amount) in amounts.into_iter().enumerate() { - let mut address = SeraiAddress::new([0; 32]); - OsRng.fill_bytes(&mut address.0); - if i < addr_count / 2 { - btc_addresses.push((address, amount)); - } else { - xmr_addresses.push((address, amount)); - } - } - btc_addresses.sort_by(|a1, a2| a1.0.cmp(&a2.0)); - xmr_addresses.sort_by(|a1, a2| a1.0.cmp(&a2.0)); - - // btc batch - let mut block_hash = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block_hash.0); - let btc_ins = btc_addresses - .iter() - .map(|(addr, amount)| InInstructionWithBalance { - instruction: InInstruction::GenesisLiquidity(*addr), - balance: Balance { coin: Coin::Bitcoin, amount: *amount }, - }) - .collect::>(); - let batch = - Batch { network: NetworkId::Bitcoin, id: 0, block: block_hash, instructions: btc_ins }; - provide_batch(&serai, batch).await; - - // xmr batch - let mut block_hash = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block_hash.0); - let xmr_ins = xmr_addresses - .iter() - .map(|(addr, amount)| InInstructionWithBalance { - instruction: InInstruction::GenesisLiquidity(*addr), - balance: Balance { coin: Coin::Monero, amount: *amount }, - }) - .collect::>(); - let batch = Batch { network: NetworkId::Monero, id: 0, block: block_hash, instructions: xmr_ins }; - provide_batch(&serai, batch).await; - - // set prices - let prices = Prices { bitcoin: 10u64.pow(8), monero: 184100, ethereum: 4785000, dai: 1500 }; - set_prices(&serai, &prices).await; - - // wait until genesis ends.. - tokio::time::timeout(tokio::time::Duration::from_secs(300), async { - while serai.latest_finalized_block().await.unwrap().number() < 25 { - tokio::time::sleep(Duration::from_secs(6)).await; - } - }) - .await - .unwrap(); - - // check total SRI supply is +100M - let last_block = serai.latest_finalized_block().await.unwrap().hash(); - let serai = serai.as_of(last_block); - // Check balance instead of supply - let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap(); - // there are 6 endowed accounts in dev-net. Take this into consideration when checking - // for the total sri minted at this time. - let endowed_amount: u64 = 1 << 60; - let total_sri = (6 * endowed_amount) + GENESIS_SRI; - assert_eq!(sri, Amount(total_sri)); - - // check genesis account has no coins, all transferred to pools. - for coin in COINS { - let amount = serai.coins().coin_balance(coin, GENESIS_LIQUIDITY_ACCOUNT).await.unwrap(); - assert_eq!(amount.0, 0); - } - - // check pools has proper liquidity - let pool_btc = btc_addresses.iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); - let pool_xmr = xmr_addresses.iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); - - let pool_btc_value = (pool_btc * u128::from(prices.bitcoin)) / 10u128.pow(8); - let pool_xmr_value = (pool_xmr * u128::from(prices.monero)) / 10u128.pow(12); - let total_value = pool_btc_value + pool_xmr_value; - - // calculated distributed SRI. We know that xmr is at the end of COINS array - // so it will be approximated to roof instead of floor after integer division. - let btc_sri = (pool_btc_value * u128::from(GENESIS_SRI)) / total_value; - let xmr_sri = ((pool_xmr_value * u128::from(GENESIS_SRI)) / total_value) + 1; - - let btc_reserves = serai.dex().get_reserves(Coin::Bitcoin, Coin::Serai).await.unwrap().unwrap(); - assert_eq!(u128::from(btc_reserves.0 .0), pool_btc); - assert_eq!(u128::from(btc_reserves.1 .0), btc_sri); - - let xmr_reserves = serai.dex().get_reserves(Coin::Monero, Coin::Serai).await.unwrap().unwrap(); - assert_eq!(u128::from(xmr_reserves.0 .0), pool_xmr); - assert_eq!(u128::from(xmr_reserves.1 .0), xmr_sri); - - // check each btc liq provider got liq tokens proportional to their value - let btc_liq_token_supply = u128::from( - serai - .liquidity_tokens() - .token_balance(Coin::Bitcoin, GENESIS_LIQUIDITY_ACCOUNT) - .await - .unwrap() - .0, - ); - let mut total_tokens_this_coin: u128 = 0; - for (i, (addr, amount)) in btc_addresses.iter().enumerate() { - let addr_value = (u128::from(amount.0) * u128::from(prices.bitcoin)) / 10u128.pow(8); - let addr_liq_tokens = if i == btc_addresses.len() - 1 { - btc_liq_token_supply - total_tokens_this_coin - } else { - (addr_value * btc_liq_token_supply) / pool_btc_value - }; - - let addr_actual_token_amount = - serai.genesis_liquidity().liquidity_tokens(addr, Coin::Bitcoin).await.unwrap(); - - assert_eq!(addr_liq_tokens, addr_actual_token_amount.0.into()); - total_tokens_this_coin += addr_liq_tokens; - } - - // check each xmr liq provider got liq tokens proportional to their value - let xmr_liq_token_supply = u128::from( - serai - .liquidity_tokens() - .token_balance(Coin::Monero, GENESIS_LIQUIDITY_ACCOUNT) - .await - .unwrap() - .0, - ); - total_tokens_this_coin = 0; - for (i, (addr, amount)) in xmr_addresses.iter().enumerate() { - let addr_value = (u128::from(amount.0) * u128::from(prices.monero)) / 10u128.pow(12); - let addr_liq_tokens = if i == xmr_addresses.len() - 1 { - xmr_liq_token_supply - total_tokens_this_coin - } else { - (addr_value * xmr_liq_token_supply) / pool_xmr_value - }; - - let addr_actual_token_amount = - serai.genesis_liquidity().liquidity_tokens(addr, Coin::Monero).await.unwrap(); - - assert_eq!(addr_liq_tokens, addr_actual_token_amount.0.into()); - total_tokens_this_coin += addr_liq_tokens; - } - - // TODO: remove the liq before/after genesis ended. -} - -async fn set_prices(serai: &Serai, prices: &Prices) { - // prepare a Musig tx to set the initial prices - let pair = insecure_pair_from_name("Alice"); - let public = pair.public(); - let set = ValidatorSet { session: Session(0), network: NetworkId::Serai }; - - let public_key = ::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap(); - let secret_key = ::read_F::<&[u8]>( - &mut pair.as_ref().secret.to_bytes()[.. 32].as_ref(), - ) - .unwrap(); - - assert_eq!(Ristretto::generator() * secret_key, public_key); - let threshold_keys = - musig::(&musig_context(set), &Zeroizing::new(secret_key), &[public_key]).unwrap(); - - let sig = frost::tests::sign_without_caching( - &mut OsRng, - frost::tests::algorithm_machines( - &mut OsRng, - &Schnorrkel::new(b"substrate"), - &HashMap::from([(threshold_keys.params().i(), threshold_keys.into())]), - ), - &set_initial_price_message(&set, prices), - ); - - // set initial prices - let _ = publish_tx( - serai, - &SeraiGenesisLiquidity::set_initial_price(*prices, Signature(sig.to_bytes())), - ) - .await; -} diff --git a/substrate/emissions/pallet/Cargo.toml b/substrate/emissions/pallet/Cargo.toml index dc5299bb..49e3277d 100644 --- a/substrate/emissions/pallet/Cargo.toml +++ b/substrate/emissions/pallet/Cargo.toml @@ -27,11 +27,8 @@ frame-system = { git = "https://github.com/serai-dex/substrate", default-feature frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } -sp-session = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } -pallet-babe = { git = "https://github.com/serai-dex/substrate", default-features = false } - coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false } dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false } @@ -50,11 +47,8 @@ std = [ "frame-support/std", "sp-std/std", - "sp-session/std", "sp-runtime/std", - "pallet-babe/std", - "coins-pallet/std", "validator-sets-pallet/std", "dex-pallet/std", diff --git a/substrate/emissions/pallet/src/lib.rs b/substrate/emissions/pallet/src/lib.rs index c5f47f7f..50ddab3c 100644 --- a/substrate/emissions/pallet/src/lib.rs +++ b/substrate/emissions/pallet/src/lib.rs @@ -1,6 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] -#[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding)] +#[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding, clippy::empty_docs)] #[frame_support::pallet] pub mod pallet { use super::*; @@ -8,27 +8,21 @@ pub mod pallet { use frame_support::{pallet_prelude::*, sp_runtime::SaturatedConversion}; use sp_std::{vec, vec::Vec, collections::btree_map::BTreeMap}; - use sp_session::ShouldEndSession; use sp_runtime; use coins_pallet::{Config as CoinsConfig, Pallet as Coins, AllowMint}; use dex_pallet::{Config as DexConfig, Pallet as Dex}; use validator_sets_pallet::{Pallet as ValidatorSets, Config as ValidatorSetsConfig}; - use pallet_babe::{Pallet as Babe, Config as BabeConfig}; use serai_primitives::{NetworkId, NETWORKS, *}; - use validator_sets_primitives::MAX_KEY_SHARES_PER_SET; + use validator_sets_primitives::{MAX_KEY_SHARES_PER_SET, Session}; use genesis_liquidity_primitives::GENESIS_PERIOD_BLOCKS; use emissions_primitives::*; #[pallet::config] pub trait Config: - frame_system::Config - + ValidatorSetsConfig - + BabeConfig - + CoinsConfig - + DexConfig + frame_system::Config + ValidatorSetsConfig + CoinsConfig + DexConfig { type RuntimeEvent: From> + IsType<::RuntimeEvent>; } @@ -37,7 +31,7 @@ pub mod pallet { #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] pub struct GenesisConfig { /// Networks to spawn Serai with. - pub networks: Vec, + pub networks: Vec<(NetworkId, Amount)>, /// List of participants to place in the initial validator sets. pub participants: Vec, } @@ -50,10 +44,7 @@ pub mod pallet { #[pallet::error] pub enum Error { - GenesisPeriodEnded, - AmountOverflowed, - NotEnoughLiquidity, - CanOnlyRemoveFullAmount, + MintFailed, } #[pallet::event] @@ -73,13 +64,17 @@ pub mod pallet { >; #[pallet::storage] - #[pallet::getter(fn epoch_begin_block)] - pub(crate) type EpochBeginBlock = StorageMap<_, Identity, u64, u64, ValueQuery>; + #[pallet::getter(fn session_begin_block)] + pub(crate) type SessionBeginBlock = StorageMap<_, Identity, u32, u64, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn session)] + pub type CurrentSession = StorageMap<_, Identity, NetworkId, u32, ValueQuery>; #[pallet::storage] #[pallet::getter(fn economic_security_reached)] pub(crate) type EconomicSecurityReached = - StorageMap<_, Identity, NetworkId, BlockNumberFor, ValueQuery>; + StorageMap<_, Identity, NetworkId, bool, ValueQuery>; #[pallet::storage] #[pallet::getter(fn last_swap_volume)] @@ -88,57 +83,60 @@ pub mod pallet { #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { - for id in self.networks.clone() { + for (id, stake) in self.networks.clone() { let mut participants = vec![]; for p in self.participants.clone() { - participants.push((p, 0u64)); + participants.push((p, stake.0)); } Participants::::set(id, Some(participants.try_into().unwrap())); + CurrentSession::::set(id, 0); + EconomicSecurityReached::::set(id, false); } - EpochBeginBlock::::set(0, 0); + SessionBeginBlock::::set(0, 0); } } #[pallet::hooks] impl Hooks> for Pallet { - /// Since we are on `on_finalize`, session should have already rotated. - /// We can distribute the rewards for the last set. fn on_finalize(n: BlockNumberFor) { + // wait 1 extra block to actually see genesis changes + let genesis_ended = n >= (GENESIS_PERIOD_BLOCKS + 1).into(); + // we accept we reached economic security once we can mint smallest amount of a network's coin for coin in COINS { - let existing = EconomicSecurityReached::::get(coin.network()); - if existing == 0u32.into() && - ::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) }) + let check = !Self::economic_security_reached(coin.network()) && genesis_ended; + if check && ::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) }) { - EconomicSecurityReached::::set(coin.network(), n); + EconomicSecurityReached::::set(coin.network(), true); } } - // emissions start only after genesis period and happens once per epoch + // check wif we got a new session + let mut session_changed = false; + let session = ValidatorSets::::session(NetworkId::Serai).unwrap_or(Session(0)).0; + if session > Self::session(NetworkId::Serai) { + session_changed = true; + CurrentSession::::set(NetworkId::Serai, session); + } + + // emissions start only after genesis period and happens once per session. // so we don't do anything before that time. - if !(n >= GENESIS_PERIOD_BLOCKS.into() && T::ShouldEndSession::should_end_session(n)) { + if !(genesis_ended && session_changed) { return; } - // figure out the amount of blocks in the last epoch - // TODO: we use epoch index here but should we use SessionIndex since this is how we decide - // whether time to distribute the rewards or not? Because apparently epochs != Sessions - // since we can skip some epochs if the chain is offline more than epoch duration?? - let epoch = Babe::::current_epoch().epoch_index - 1; - let block_count = n.saturated_into::() - Self::epoch_begin_block(epoch); + // figure out the amount of blocks in the last session. Session is at least 1 + // if we come here. + let current_block = n.saturated_into::(); + let block_count = current_block - Self::session_begin_block(session - 1); // get total reward for this epoch let pre_ec_security = Self::pre_ec_security(); let mut distances = BTreeMap::new(); let mut total_distance: u64 = 0; - let reward_this_epoch = if Self::initial_period(n) { - // rewards are fixed for initial period - block_count * INITIAL_REWARD_PER_BLOCK - } else if pre_ec_security { + let reward_this_epoch = if pre_ec_security { // calculate distance to economic security per network - let mut total_required: u64 = 0; - let mut total_current: u64 = 0; for n in NETWORKS { if n == NetworkId::Serai { continue; @@ -150,11 +148,10 @@ pub mod pallet { current = required; } - distances.insert(n, required - current); - total_required = total_required.saturating_add(required); - total_current = total_current.saturating_add(current); + let distance = required - current; + distances.insert(n, distance); + total_distance = total_distance.saturating_add(distance); } - total_distance = total_required.saturating_sub(total_current); // add serai network portion(20%) let new_total_distance = @@ -162,10 +159,15 @@ pub mod pallet { distances.insert(NetworkId::Serai, new_total_distance - total_distance); total_distance = new_total_distance; - // rewards for pre-economic security is - // (STAKE_REQUIRED - CURRENT_STAKE) / blocks_until(SECURE_BY). - let block_reward = total_distance / Self::blocks_until(SECURE_BY); - block_count * block_reward + if Self::initial_period(n) { + // rewards are fixed for initial period + block_count * INITIAL_REWARD_PER_BLOCK + } else { + // rewards for pre-economic security is + // (STAKE_REQUIRED - CURRENT_STAKE) / blocks_until(SECURE_BY). + let block_reward = total_distance / Self::blocks_until(SECURE_BY); + block_count * block_reward + } } else { // post ec security block_count * REWARD_PER_BLOCK @@ -201,7 +203,11 @@ pub mod pallet { .map(|(n, distance)| { let reward = if pre_ec_security { // calculate how much each network gets based on distance to ec-security - reward_this_epoch.saturating_mul(distance) / total_distance + u64::try_from( + u128::from(reward_this_epoch).saturating_mul(u128::from(distance)) / + u128::from(total_distance), + ) + .unwrap() } else { // 20% of the reward goes to the Serai network and rest is distributed among others // based on swap-volume. @@ -209,7 +215,12 @@ pub mod pallet { reward_this_epoch / 5 } else { let reward = reward_this_epoch - (reward_this_epoch / 5); - reward.saturating_mul(*volume_per_network.get(&n).unwrap_or(&0)) / total_volume + u64::try_from( + u128::from(reward) + .saturating_mul(u128::from(*volume_per_network.get(&n).unwrap_or(&0))) / + u128::from(total_volume), + ) + .unwrap() } }; (n, reward) @@ -218,35 +229,46 @@ pub mod pallet { // distribute the rewards within the network for (n, reward) in rewards_per_network { - // 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 (validators_reward, pool_reward) = 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 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 pool_reward = total - validators_reward; + let validators_reward = DESIRED_DISTRIBUTION.saturating_mul(reward) / total; + let pool_reward = total - validators_reward; + (validators_reward, pool_reward) + }; // distribute validators rewards - Self::distribute_to_validators(n, validators_reward); + if Self::distribute_to_validators(n, validators_reward).is_err() { + // TODO: log the failure + continue; + } // send the rest to the pool let coin_count = u64::try_from(n.coins().len()).unwrap(); for c in n.coins() { - // TODO: we just print a warning here instead of unwrap? // assumes reward is equally distributed between network coins. - Coins::::mint( + if Coins::::mint( Dex::::get_pool_account(*c), Balance { coin: Coin::Serai, amount: Amount(pool_reward / coin_count) }, ) - .unwrap(); + .is_err() + { + // TODO: log the failure + continue; + } } } // set the begin block and participants - EpochBeginBlock::::set(epoch, n.saturated_into::()); + SessionBeginBlock::::set(session, current_block); for n in NETWORKS { // TODO: `participants_for_latest_decided_set` returns keys with key shares but we // store keys with actual stake amounts. Pr https://github.com/serai-dex/serai/pull/518 @@ -273,14 +295,14 @@ pub mod pallet { continue; } - if Self::economic_security_reached(n) == 0u32.into() { + if !Self::economic_security_reached(n) { return true; } } false } - fn distribute_to_validators(n: NetworkId, reward: u64) { + fn distribute_to_validators(n: NetworkId, reward: u64) -> DispatchResult { // distribute among network's set based on // -> (key shares * stake per share) + ((stake % stake per share) / 2) let stake_per_share = ValidatorSets::::allocation_per_key_share(n).unwrap().0; @@ -296,10 +318,17 @@ pub mod pallet { // stake the rewards for (p, score) in scores { - let p_reward = reward.saturating_mul(score) / total_score; - // TODO: print a warning here? - let _ = ValidatorSets::::deposit_stake(n, p, Amount(p_reward)); + let p_reward = 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) }) + .map_err(|_| Error::::MintFailed)?; + ValidatorSets::::deposit_stake(n, p, Amount(p_reward))?; } + + Ok(()) } } } diff --git a/substrate/node/src/chain_spec.rs b/substrate/node/src/chain_spec.rs index 03b574ae..dd3b75ce 100644 --- a/substrate/node/src/chain_spec.rs +++ b/substrate/node/src/chain_spec.rs @@ -58,7 +58,15 @@ fn devnet_genesis( }, genesis_liquidity: GenesisLiquidityConfig { participants: validators.clone() }, emissions: EmissionsConfig { - networks: serai_runtime::primitives::NETWORKS.to_vec(), + networks: serai_runtime::primitives::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(), participants: validators.clone(), }, signals: SignalsConfig::default(), @@ -109,7 +117,15 @@ fn testnet_genesis(wasm_binary: &[u8], validators: Vec<&'static str>) -> Runtime }, genesis_liquidity: GenesisLiquidityConfig { participants: validators.clone() }, emissions: EmissionsConfig { - networks: serai_runtime::primitives::NETWORKS.to_vec(), + networks: serai_runtime::primitives::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(), participants: validators.clone(), }, signals: SignalsConfig::default(),