diff --git a/Cargo.lock b/Cargo.lock index d17379fc..5a0c9d6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8111,6 +8111,9 @@ dependencies = [ [[package]] name = "serai-emissions-primitives" version = "0.1.0" +dependencies = [ + "serai-primitives", +] [[package]] name = "serai-env" @@ -8197,7 +8200,6 @@ dependencies = [ "serai-dex-pallet", "serai-emissions-pallet", "serai-genesis-liquidity-pallet", - "serai-genesis-liquidity-primitives", "serai-in-instructions-primitives", "serai-primitives", "serai-validator-sets-pallet", diff --git a/deny.toml b/deny.toml index 2bf6cefa..874824fe 100644 --- a/deny.toml +++ b/deny.toml @@ -56,8 +56,6 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-genesis-liquidity-pallet" }, { allow = ["AGPL-3.0"], name = "serai-emissions-pallet" }, - { allow = ["AGPL-3.0"], name = "serai-genesis-liquidity-pallet" }, - { allow = ["AGPL-3.0"], name = "serai-in-instructions-pallet" }, { allow = ["AGPL-3.0"], name = "serai-validator-sets-pallet" }, diff --git a/substrate/abi/src/emissions.rs b/substrate/abi/src/emissions.rs index cb576611..ff948f5f 100644 --- a/substrate/abi/src/emissions.rs +++ b/substrate/abi/src/emissions.rs @@ -1,16 +1 @@ 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))] -pub enum Call { - // This call is just a place holder so that abi works as expected. - empty_call, -} - -#[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))] -pub enum Event { - empty_event, -} diff --git a/substrate/abi/src/lib.rs b/substrate/abi/src/lib.rs index 452202cf..22247e10 100644 --- a/substrate/abi/src/lib.rs +++ b/substrate/abi/src/lib.rs @@ -35,7 +35,7 @@ pub enum Call { Dex(dex::Call), ValidatorSets(validator_sets::Call), GenesisLiquidity(genesis_liquidity::Call), - Emissions(emissions::Call), + Emissions, InInstructions(in_instructions::Call), Signals(signals::Call), Babe(babe::Call), @@ -58,7 +58,7 @@ pub enum Event { Dex(dex::Event), ValidatorSets(validator_sets::Event), GenesisLiquidity(genesis_liquidity::Event), - Emissions(emissions::Event), + Emissions, InInstructions(in_instructions::Event), Signals(signals::Event), Babe, diff --git a/substrate/client/src/serai/dex.rs b/substrate/client/src/serai/dex.rs index a30db6da..ea76e625 100644 --- a/substrate/client/src/serai/dex.rs +++ b/substrate/client/src/serai/dex.rs @@ -61,7 +61,7 @@ impl<'a> SeraiDex<'a> { } /// Returns the reserves of `coin:SRI` pool. - pub async fn get_reserves(&self, coin: Coin) -> Result, SeraiError> { + pub async fn get_reserves(&self, coin: Coin) -> Result, SeraiError> { self.0.runtime_api("DexApi_get_reserves", (coin, Coin::Serai)).await } diff --git a/substrate/client/src/serai/genesis_liquidity.rs b/substrate/client/src/serai/genesis_liquidity.rs index 1a6de221..00a369c8 100644 --- a/substrate/client/src/serai/genesis_liquidity.rs +++ b/substrate/client/src/serai/genesis_liquidity.rs @@ -63,7 +63,8 @@ impl<'a> SeraiGenesisLiquidity<'a> { Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero())) } - pub async fn genesis_complete(&self) -> Result, SeraiError> { - self.0.storage(PALLET, "GenesisComplete", ()).await + pub async fn genesis_complete(&self) -> Result { + let result: Option<()> = self.0.storage(PALLET, "GenesisComplete", ()).await?; + Ok(result.is_some()) } } diff --git a/substrate/client/src/serai/mod.rs b/substrate/client/src/serai/mod.rs index 258b1c07..c688bf36 100644 --- a/substrate/client/src/serai/mod.rs +++ b/substrate/client/src/serai/mod.rs @@ -383,7 +383,7 @@ impl<'a> TemporalSerai<'a> { let bytes = Serai::hex_decode(result.clone())?; R::decode(&mut bytes.as_slice()).map_err(|_| { SeraiError::InvalidRuntime(format!( - "different type than what is expected returned, raw value: {}", + "different type than what is expected to be returned, raw value: {}", hex::encode(result) )) }) diff --git a/substrate/client/tests/common/genesis_liquidity.rs b/substrate/client/tests/common/genesis_liquidity.rs index abaa9829..0c0cd269 100644 --- a/substrate/client/tests/common/genesis_liquidity.rs +++ b/substrate/client/tests/common/genesis_liquidity.rs @@ -1,4 +1,4 @@ -use std::{time::Duration, collections::HashMap}; +use std::collections::HashMap; use rand_core::{RngCore, OsRng}; use zeroize::Zeroizing; @@ -7,39 +7,30 @@ use ciphersuite::{Ciphersuite, Ristretto}; use frost::dkg::musig::musig; use schnorrkel::Schnorrkel; -use serai_client::{ - genesis_liquidity::{ - primitives::{GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES}, - SeraiGenesisLiquidity, - }, - validator_sets::primitives::{musig_context, Session, ValidatorSet}, -}; +use sp_core::{sr25519::Signature, Pair as PairTrait}; use serai_abi::{ genesis_liquidity::primitives::{oraclize_values_message, Values}, - primitives::COINS, -}; - -use sp_core::{sr25519::Signature, Pair as PairTrait}; - -use serai_client::{ - primitives::{ - Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, insecure_pair_from_name, GENESIS_SRI, - }, + validator_sets::primitives::{musig_context, Session, ValidatorSet}, in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch}, - Serai, + primitives::{ + Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, insecure_pair_from_name, + }, }; +use serai_client::{Serai, SeraiGenesisLiquidity}; + use crate::common::{in_instructions::provide_batch, tx::publish_tx}; #[allow(dead_code)] -pub async fn test_genesis_liquidity(serai: Serai) -> HashMap { - // all coins except the native - let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::>(); - +pub async fn set_up_genesis( + serai: &Serai, + coins: &[Coin], + values: &HashMap, +) -> (HashMap>, HashMap) { // make accounts with amounts let mut accounts = HashMap::new(); - for coin in coins.clone() { + for coin in coins { // make 5 accounts per coin let mut values = vec![]; for _ in 0 .. 5 { @@ -47,18 +38,18 @@ pub async fn test_genesis_liquidity(serai: Serai) -> HashMap { OsRng.fill_bytes(&mut address.0); values.push((address, Amount(OsRng.next_u64() % 10u64.pow(coin.decimals())))); } - accounts.insert(coin, values); + accounts.insert(*coin, values); } // send a batch per coin let mut batch_ids: HashMap = HashMap::new(); - for coin in coins.clone() { + for coin in coins { // set up instructions - let instructions = accounts[&coin] + let instructions = accounts[coin] .iter() .map(|(addr, amount)| InInstructionWithBalance { instruction: InInstruction::GenesisLiquidity(*addr), - balance: Balance { coin, amount: *amount }, + balance: Balance { coin: *coin, amount: *amount }, }) .collect::>(); @@ -76,103 +67,17 @@ pub async fn test_genesis_liquidity(serai: Serai) -> HashMap { let batch = Batch { network: coin.network(), id: batch_ids[&coin.network()], block, instructions }; - provide_batch(&serai, batch).await; + provide_batch(serai, batch).await; } // set values relative to each other. We can do that without checking for genesis period blocks // since we are running in test(fast-epoch) mode. // TODO: Random values here - let values = Values { monero: 184100, ether: 4785000, dai: 1500 }; - set_values(&serai, &values).await; - let values_map = HashMap::from([ - (Coin::Monero, values.monero), - (Coin::Ether, values.ether), - (Coin::Dai, values.dai), - ]); + let values = + Values { monero: values[&Coin::Monero], ether: values[&Coin::Ether], dai: values[&Coin::Dai] }; + set_values(serai, &values).await; - // wait until genesis is complete - while serai - .as_of_latest_finalized_block() - .await - .unwrap() - .genesis_liquidity() - .genesis_complete() - .await - .unwrap() - .is_none() - { - tokio::time::sleep(Duration::from_secs(1)).await; - } - - // check total SRI supply is +100M - // there are 6 endowed accounts in dev-net. Take this into consideration when checking - // for the total sri minted at this time. - let serai = serai.as_of_latest_finalized_block().await.unwrap(); - let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap(); - 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 mut pool_amounts = HashMap::new(); - let mut total_value = 0u128; - for coin in coins.clone() { - let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); - let value = if coin != Coin::Bitcoin { - (total_coin * u128::from(values_map[&coin])) / 10u128.pow(coin.decimals()) - } else { - total_coin - }; - - total_value += value; - pool_amounts.insert(coin, (total_coin, value)); - } - - // check distributed SRI per pool - let mut total_sri_distributed = 0u128; - for coin in coins.clone() { - let sri = if coin == *COINS.last().unwrap() { - u128::from(GENESIS_SRI).checked_sub(total_sri_distributed).unwrap() - } else { - (pool_amounts[&coin].1 * u128::from(GENESIS_SRI)) / total_value - }; - total_sri_distributed += sri; - - let reserves = serai.dex().get_reserves(coin).await.unwrap().unwrap(); - assert_eq!(u128::from(reserves.0), pool_amounts[&coin].0); // coin side - assert_eq!(u128::from(reserves.1), sri); // SRI side - } - - // check each liquidity provider got liquidity tokens proportional to their value - for coin in coins { - let liq_supply = serai.genesis_liquidity().supply(coin).await.unwrap(); - for (acc, amount) in &accounts[&coin] { - let acc_liq_shares = serai.genesis_liquidity().liquidity(acc, coin).await.unwrap().shares; - - // since we can't test the ratios directly(due to integer division giving 0) - // we test whether they give the same result when multiplied by another constant. - // Following test ensures the account in fact has the right amount of shares. - let mut shares_ratio = (INITIAL_GENESIS_LP_SHARES * acc_liq_shares) / liq_supply.shares; - let amounts_ratio = - (INITIAL_GENESIS_LP_SHARES * amount.0) / u64::try_from(pool_amounts[&coin].0).unwrap(); - - // we can tolerate 1 unit diff between them due to integer division. - if shares_ratio.abs_diff(amounts_ratio) == 1 { - shares_ratio = amounts_ratio; - } - - assert_eq!(shares_ratio, amounts_ratio); - } - } - // TODO: test remove the liq before/after genesis ended. - - batch_ids + (accounts, batch_ids) } #[allow(dead_code)] diff --git a/substrate/client/tests/emissions.rs b/substrate/client/tests/emissions.rs index e42469b9..5484dc99 100644 --- a/substrate/client/tests/emissions.rs +++ b/substrate/client/tests/emissions.rs @@ -6,7 +6,7 @@ use serai_client::TemporalSerai; use serai_abi::{ emissions::primitives::{INITIAL_REWARD_PER_BLOCK, SECURE_BY}, in_instructions::primitives::Batch, - primitives::{NETWORKS, BlockHash}, + primitives::{NETWORKS, Coin, BlockHash, COINS, FAST_EPOCH_DURATION, TARGET_BLOCK_TIME}, validator_sets::primitives::Session, }; @@ -16,7 +16,7 @@ use serai_client::{ }; mod common; -use common::{genesis_liquidity::test_genesis_liquidity, in_instructions::provide_batch}; +use common::{genesis_liquidity::set_up_genesis, in_instructions::provide_batch}; serai_test_fast_epoch!( emissions: (|serai: Serai| async move { @@ -45,8 +45,23 @@ async fn send_batches(serai: &Serai, ids: &mut HashMap) { } async fn test_emissions(serai: Serai) { - // provide some genesis liquidity - let mut batch_ids = test_genesis_liquidity(serai.clone()).await; + // set up the genesis + let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::>(); + let values = HashMap::from([(Coin::Monero, 184100), (Coin::Ether, 4785000), (Coin::Dai, 1500)]); + let (_, mut batch_ids) = set_up_genesis(&serai, &coins, &values).await; + + // wait until genesis is complete + while !serai + .as_of_latest_finalized_block() + .await + .unwrap() + .genesis_liquidity() + .genesis_complete() + .await + .unwrap() + { + tokio::time::sleep(Duration::from_secs(1)).await; + } for _ in 0 .. 3 { // get current stakes @@ -156,23 +171,26 @@ async fn wait_for_session_change(serai: &Serai) -> u32 { .0; let next_session = current_session + 1; - // Epoch time is 2 mins with the fast epoch feature, so lets wait double that. - tokio::time::timeout(tokio::time::Duration::from_secs(60 * 4), async { - while serai - .as_of_latest_finalized_block() - .await - .unwrap() - .validator_sets() - .session(NetworkId::Serai) - .await - .unwrap() - .unwrap() - .0 < - next_session - { - tokio::time::sleep(Duration::from_secs(6)).await; - } - }) + // lets wait double the epoch time. + tokio::time::timeout( + tokio::time::Duration::from_secs(FAST_EPOCH_DURATION * TARGET_BLOCK_TIME * 2), + async { + while serai + .as_of_latest_finalized_block() + .await + .unwrap() + .validator_sets() + .session(NetworkId::Serai) + .await + .unwrap() + .unwrap() + .0 < + next_session + { + tokio::time::sleep(Duration::from_secs(6)).await; + } + }, + ) .await .unwrap(); diff --git a/substrate/client/tests/genesis_liquidity.rs b/substrate/client/tests/genesis_liquidity.rs index c61d6e70..867763e1 100644 --- a/substrate/client/tests/genesis_liquidity.rs +++ b/substrate/client/tests/genesis_liquidity.rs @@ -1,10 +1,107 @@ +use std::{time::Duration, collections::HashMap}; + use serai_client::Serai; +use serai_abi::primitives::{Coin, COINS, Amount, GENESIS_SRI}; + +use serai_client::genesis_liquidity::primitives::{ + GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES, +}; + mod common; -use common::genesis_liquidity::test_genesis_liquidity; +use common::genesis_liquidity::set_up_genesis; serai_test_fast_epoch!( genesis_liquidity: (|serai: Serai| async move { test_genesis_liquidity(serai).await; }) ); + +pub async fn test_genesis_liquidity(serai: Serai) { + // set up the genesis + let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::>(); + let values = HashMap::from([(Coin::Monero, 184100), (Coin::Ether, 4785000), (Coin::Dai, 1500)]); + let (accounts, _) = set_up_genesis(&serai, &coins, &values).await; + + // wait until genesis is complete + while !serai + .as_of_latest_finalized_block() + .await + .unwrap() + .genesis_liquidity() + .genesis_complete() + .await + .unwrap() + { + tokio::time::sleep(Duration::from_secs(1)).await; + } + + // check total SRI supply is +100M + // there are 6 endowed accounts in dev-net. Take this into consideration when checking + // for the total sri minted at this time. + let serai = serai.as_of_latest_finalized_block().await.unwrap(); + let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap(); + 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 mut pool_amounts = HashMap::new(); + let mut total_value = 0u128; + for coin in coins.clone() { + let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); + let value = if coin != Coin::Bitcoin { + (total_coin * u128::from(values[&coin])) / 10u128.pow(coin.decimals()) + } else { + total_coin + }; + + total_value += value; + pool_amounts.insert(coin, (total_coin, value)); + } + + // check distributed SRI per pool + let mut total_sri_distributed = 0u128; + for coin in coins.clone() { + let sri = if coin == *COINS.last().unwrap() { + u128::from(GENESIS_SRI).checked_sub(total_sri_distributed).unwrap() + } else { + (pool_amounts[&coin].1 * u128::from(GENESIS_SRI)) / total_value + }; + total_sri_distributed += sri; + + let reserves = serai.dex().get_reserves(coin).await.unwrap().unwrap(); + assert_eq!(u128::from(reserves.0 .0), pool_amounts[&coin].0); // coin side + assert_eq!(u128::from(reserves.1 .0), sri); // SRI side + } + + // check each liquidity provider got liquidity tokens proportional to their value + for coin in coins { + let liq_supply = serai.genesis_liquidity().supply(coin).await.unwrap(); + for (acc, amount) in &accounts[&coin] { + let acc_liq_shares = serai.genesis_liquidity().liquidity(acc, coin).await.unwrap().shares; + + // since we can't test the ratios directly(due to integer division giving 0) + // we test whether they give the same result when multiplied by another constant. + // Following test ensures the account in fact has the right amount of shares. + let mut shares_ratio = (INITIAL_GENESIS_LP_SHARES * acc_liq_shares) / liq_supply.shares; + let amounts_ratio = + (INITIAL_GENESIS_LP_SHARES * amount.0) / u64::try_from(pool_amounts[&coin].0).unwrap(); + + // we can tolerate 1 unit diff between them due to integer division. + if shares_ratio.abs_diff(amounts_ratio) == 1 { + shares_ratio = amounts_ratio; + } + + assert_eq!(shares_ratio, amounts_ratio); + } + } + + // TODO: test remove the liq before/after genesis ended. +} diff --git a/substrate/client/tests/validator_sets.rs b/substrate/client/tests/validator_sets.rs index c80c65a9..3c8ce676 100644 --- a/substrate/client/tests/validator_sets.rs +++ b/substrate/client/tests/validator_sets.rs @@ -6,7 +6,9 @@ use sp_core::{ }; use serai_client::{ - primitives::{NETWORKS, NetworkId, BlockHash, insecure_pair_from_name}, + primitives::{ + NETWORKS, NetworkId, BlockHash, insecure_pair_from_name, FAST_EPOCH_DURATION, TARGET_BLOCK_TIME, + }, validator_sets::{ primitives::{Session, ValidatorSet, KeyPair}, ValidatorSetsEvent, @@ -326,22 +328,25 @@ async fn verify_session_and_active_validators( session: u32, participants: &[Public], ) { - // wait until the active session. This wait should be max 2 mins since the epoch time. - let block = tokio::time::timeout(core::time::Duration::from_secs(5 * 60), async move { - loop { - let mut block = serai.latest_finalized_block_hash().await.unwrap(); - if session_for_block(serai, block, network).await < session { - // Sleep a block - tokio::time::sleep(core::time::Duration::from_secs(6)).await; - continue; + // wait until the active session. + let block = tokio::time::timeout( + core::time::Duration::from_secs(FAST_EPOCH_DURATION * TARGET_BLOCK_TIME * 2), + async move { + loop { + let mut block = serai.latest_finalized_block_hash().await.unwrap(); + if session_for_block(serai, block, network).await < session { + // Sleep a block + tokio::time::sleep(core::time::Duration::from_secs(6)).await; + continue; + } + while session_for_block(serai, block, network).await > session { + block = serai.block(block).await.unwrap().unwrap().header.parent_hash.0; + } + assert_eq!(session_for_block(serai, block, network).await, session); + break block; } - while session_for_block(serai, block, network).await > session { - block = serai.block(block).await.unwrap().unwrap().header.parent_hash.0; - } - assert_eq!(session_for_block(serai, block, network).await, session); - break block; - } - }) + }, + ) .await .unwrap(); let serai_for_block = serai.as_of(block); @@ -358,7 +363,7 @@ async fn verify_session_and_active_validators( // make sure finalization continues as usual after the changes let current_finalized_block = serai.latest_finalized_block().await.unwrap().header.number; - tokio::time::timeout(core::time::Duration::from_secs(60), async move { + tokio::time::timeout(core::time::Duration::from_secs(TARGET_BLOCK_TIME * 10), async move { let mut finalized_block = serai.latest_finalized_block().await.unwrap().header.number; while finalized_block <= current_finalized_block + 2 { tokio::time::sleep(core::time::Duration::from_secs(6)).await; diff --git a/substrate/dex/pallet/src/lib.rs b/substrate/dex/pallet/src/lib.rs index 84e8bdd3..04ab02e6 100644 --- a/substrate/dex/pallet/src/lib.rs +++ b/substrate/dex/pallet/src/lib.rs @@ -194,10 +194,10 @@ pub mod pallet { #[pallet::getter(fn security_oracle_value)] pub type SecurityOracleValue = StorageMap<_, Identity, Coin, Amount, OptionQuery>; - /// Current length of the `SpotPrices` map. + /// Total swap volume of a given pool in terms of SRI. #[pallet::storage] #[pallet::getter(fn swap_volume)] - pub type SwapVolume = StorageMap<_, Identity, Coin, u64, OptionQuery>; + pub type SwapVolume = StorageMap<_, Identity, PoolId, u64, OptionQuery>; impl Pallet { fn restore_median( @@ -866,23 +866,20 @@ pub mod pallet { &to, Balance { coin: *coin2, amount: Amount(*amount_out) }, )?; + + // update the volume + let swap_volume = if *coin1 == Coin::Serai { + amounts.get(i as usize).ok_or(Error::::CorrespondenceError)? + } else { + amount_out + }; + let existing = SwapVolume::::get(pool_id).unwrap_or(0); + let new_volume = existing.saturating_add(*swap_volume); + SwapVolume::::set(pool_id, Some(new_volume)); } i += 1; } - // update the volume, SRI amount is always at index 1 if the path len is 2 or 3. - // path len 1 is not allowed and 3 is already the maximum. - let swap_volume = amounts.get(1).ok_or(Error::::CorrespondenceError)?; - let existing = SwapVolume::::get(coin1).unwrap_or(0); - let new_volume = existing.saturating_add(*swap_volume); - SwapVolume::::set(coin1, Some(new_volume)); - - // if we did 2 pools, update the volume for second coin as well. - if u32::try_from(path.len()).unwrap() == T::MaxSwapPathLength::get() { - let existing = SwapVolume::::get(path.last().unwrap()).unwrap_or(0); - let new_volume = existing.saturating_add(*swap_volume); - SwapVolume::::set(path.last().unwrap(), Some(new_volume)); - } Self::deposit_event(Event::SwapExecuted { who: sender, send_to, diff --git a/substrate/emissions/pallet/Cargo.toml b/substrate/emissions/pallet/Cargo.toml index a03b9290..7b398658 100644 --- a/substrate/emissions/pallet/Cargo.toml +++ b/substrate/emissions/pallet/Cargo.toml @@ -18,7 +18,6 @@ ignored = ["scale", "scale-info"] [lints] workspace = true - [dependencies] scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2", default-features = false, features = ["derive"] } @@ -58,4 +57,4 @@ std = [ "emissions-primitives/std", ] try-runtime = [] # TODO -default = ["std"] \ No newline at end of file +default = ["std"] diff --git a/substrate/emissions/pallet/src/lib.rs b/substrate/emissions/pallet/src/lib.rs index e97a6a53..3034931d 100644 --- a/substrate/emissions/pallet/src/lib.rs +++ b/substrate/emissions/pallet/src/lib.rs @@ -4,7 +4,7 @@ #[frame_support::pallet] pub mod pallet { use super::*; - use frame_system::pallet_prelude::*; + use frame_system::{pallet_prelude::*, RawOrigin}; use frame_support::{pallet_prelude::*, sp_runtime::SaturatedConversion}; use sp_std::{vec, vec::Vec, ops::Mul, collections::btree_map::BTreeMap}; @@ -18,7 +18,8 @@ pub mod pallet { use serai_primitives::*; use validator_sets_primitives::{MAX_KEY_SHARES_PER_SET, Session}; - use emissions_primitives::*; + pub use emissions_primitives as primitives; + use primitives::*; #[pallet::config] pub trait Config: @@ -79,7 +80,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn last_swap_volume)] - pub(crate) type LastSwapVolume = StorageMap<_, Identity, NetworkId, u64, OptionQuery>; + pub(crate) type LastSwapVolume = StorageMap<_, Identity, Coin, u64, OptionQuery>; #[pallet::storage] pub(crate) type GenesisCompleteBlock = StorageValue<_, u64, OptionQuery>; @@ -194,72 +195,80 @@ pub mod pallet { block_count * REWARD_PER_BLOCK }; - // get swap volumes - let mut volume_per_network: 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); - volume_per_network.insert( - c.network(), - (*volume_per_network.get(&c.network()).unwrap_or(&0)).saturating_add(current_volume), - ); - } - - // map current volumes to epoch volumes - let mut total_volume = 0u64; - for (n, vol) in &mut volume_per_network { - let last_volume = Self::last_swap_volume(n).unwrap_or(0); - let vol_this_epoch = vol.saturating_sub(last_volume); - - // update the current volume - LastSwapVolume::::set(n, Some(*vol)); - - total_volume = total_volume.saturating_add(vol_this_epoch); - *vol = vol_this_epoch; - } - // map epoch ec-security-distance/volume to rewards - let rewards_per_network = if pre_ec_security { - 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::>() + let (rewards_per_network, volume_per_network, volume_per_coin) = if pre_ec_security { + ( + 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::>(), + None, + None, + ) } else { - volume_per_network - .into_iter() - .map(|(n, vol)| { - // 20% of the reward goes to the Serai network and rest is distributed among others - // based on swap-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(vol)) / u128::from(total_volume), - ) - .unwrap() + // get swap volumes + 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 = Self::last_swap_volume(c).unwrap_or(0); + let vol_this_epoch = current_volume.saturating_sub(last_volume); + + // update the current volume + LastSwapVolume::::set(c, Some(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_network + .iter() + .map(|(n, vol)| { + // 20% of the reward goes to the Serai network and rest is distributed among others + // based on swap-volume. + let reward = if *n == NetworkId::Serai { + reward_this_epoch / 5 } else { - 0 - } - }; - (n, reward) - }) - .collect::>() + 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(*vol)) / u128::from(total_volume), + ) + .unwrap() + } else { + 0 + } + }; + (*n, reward) + }) + .collect::>(), + Some(volume_per_network), + Some(volume_per_coin), + ) }; // distribute the rewards within the network for (n, reward) in rewards_per_network { - let (validators_reward, pool_reward) = if n == NetworkId::Serai { + let (validators_reward, network_pool_reward) = if n == NetworkId::Serai { (reward, 0) } else { // calculate pool vs validator share @@ -271,28 +280,35 @@ pub mod pallet { let total = DESIRED_DISTRIBUTION.saturating_add(distribution); let validators_reward = DESIRED_DISTRIBUTION.saturating_mul(reward) / total; - let pool_reward = reward.saturating_sub(validators_reward); - (validators_reward, pool_reward) + let network_pool_reward = reward.saturating_sub(validators_reward); + (validators_reward, network_pool_reward) }; // distribute validators rewards - if Self::distribute_to_validators(n, validators_reward).is_err() { - // TODO: log the failure - continue; - } + Self::distribute_to_validators(n, validators_reward); // send the rest to the pool - let coin_count = u64::try_from(n.coins().len()).unwrap(); - for c in n.coins() { - // assumes reward is equally distributed between network coins. - if Coins::::mint( - Dex::::get_pool_account(*c), - Balance { coin: Coin::Serai, amount: Amount(pool_reward / coin_count) }, - ) - .is_err() - { - // TODO: log the failure - continue; + if network_pool_reward != 0 { + // these should be available to unwrap if we have a network_pool_reward. Because that + // means we had an unused capacity hence in a post-ec era. + let vpn = volume_per_network.as_ref().unwrap(); + let vpc = volume_per_coin.as_ref().unwrap(); + for c in n.coins() { + let pool_reward = u64::try_from( + u128::from(network_pool_reward).saturating_mul(u128::from(vpc[c])) / + u128::from(vpn[&n]), + ) + .unwrap(); + + if Coins::::mint( + Dex::::get_pool_account(*c), + Balance { coin: Coin::Serai, amount: Amount(pool_reward) }, + ) + .is_err() + { + // TODO: log the failure + continue; + } } } } @@ -334,15 +350,15 @@ pub mod pallet { false } - 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) + // Distribute the reward among network's set based on + // -> (key shares * stake per share) + ((stake % stake per share) / 2) + fn distribute_to_validators(n: NetworkId, reward: u64) { let stake_per_share = ValidatorSets::::allocation_per_key_share(n).unwrap().0; let mut scores = vec![]; let mut total_score = 0u64; for (p, amount) in Self::participants(n).unwrap() { let remainder = amount % stake_per_share; - let score = (amount - remainder) + (remainder / 2); + let score = amount - (remainder / 2); total_score = total_score.saturating_add(score); scores.push((p, score)); @@ -355,11 +371,16 @@ pub mod pallet { ) .unwrap(); - Coins::::mint(p, Balance { coin: Coin::Serai, amount: Amount(p_reward) })?; - ValidatorSets::::deposit_stake(n, p, Amount(p_reward))?; - } + if Coins::::mint(p, Balance { coin: Coin::Serai, amount: Amount(p_reward) }).is_err() { + // TODO: log the failure + continue; + } - Ok(()) + if ValidatorSets::::deposit_stake(n, p, Amount(p_reward)).is_err() { + // TODO: log the failure + continue; + } + } } pub fn swap_to_staked_sri( @@ -372,9 +393,36 @@ pub mod pallet { Err(Error::::NetworkHasEconomicSecurity)?; } - // calculate how much SRI the balance makes - let value = - Dex::::security_oracle_value(balance.coin).ok_or(Error::::NoValueForCoin)?; + // swap half of the liquidity for SRI to form PoL. + let half = balance.amount.0 / 2; + let path = BoundedVec::try_from(vec![balance.coin, Coin::Serai]).unwrap(); + let origin = RawOrigin::Signed(POL_ACCOUNT.into()); + Dex::::swap_exact_tokens_for_tokens( + origin.clone().into(), + path, + half, + 1, // minimum out, so we accept whatever we get. + POL_ACCOUNT.into(), + )?; + + // get how much we got for our swap + let sri_amount = Coins::::balance(POL_ACCOUNT.into(), Coin::Serai).0; + + // add liquidity + Dex::::add_liquidity( + origin.clone().into(), + balance.coin, + half, + sri_amount, + 1, + 1, + POL_ACCOUNT.into(), + )?; + + // use last block spot price to calculate how much SRI the balance makes. + let last_block = >::block_number() - 1u32.into(); + let value = Dex::::spot_price_for_block(last_block, balance.coin) + .ok_or(Error::::NoValueForCoin)?; // TODO: may panic? It might be best for this math ops to return the result as is instead of // doing an unwrap so that it can be properly dealt with. let sri_amount = balance.amount.mul(value); @@ -383,6 +431,7 @@ pub mod pallet { Coins::::mint(to, Balance { coin: Coin::Serai, amount: sri_amount })?; // TODO: deposit_stake lets staking less than per key share. Should we allow that here? ValidatorSets::::deposit_stake(network, to, sri_amount)?; + Ok(()) } diff --git a/substrate/emissions/primitives/Cargo.toml b/substrate/emissions/primitives/Cargo.toml index c7f44ca2..db3d38e0 100644 --- a/substrate/emissions/primitives/Cargo.toml +++ b/substrate/emissions/primitives/Cargo.toml @@ -16,7 +16,8 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] +serai-primitives = { path = "../../primitives", default-features = false } [features] -std = [] +std = ["serai-primitives/std"] default = ["std"] diff --git a/substrate/emissions/primitives/src/lib.rs b/substrate/emissions/primitives/src/lib.rs index ce9ee205..efc9aa5e 100644 --- a/substrate/emissions/primitives/src/lib.rs +++ b/substrate/emissions/primitives/src/lib.rs @@ -2,6 +2,11 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(not(feature = "std"), no_std)] +use serai_primitives::{SeraiAddress, system_address}; + +// Protocol owned liquidity account. +pub const POL_ACCOUNT: SeraiAddress = system_address(b"PoL-account"); + /// Amount of blocks in 30 days for 6s per block. const BLOCKS_PER_MONTH: u64 = 10 * 60 * 24 * 30; diff --git a/substrate/genesis-liquidity/pallet/src/lib.rs b/substrate/genesis-liquidity/pallet/src/lib.rs index c719090d..0b463af5 100644 --- a/substrate/genesis-liquidity/pallet/src/lib.rs +++ b/substrate/genesis-liquidity/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::*; diff --git a/substrate/in-instructions/pallet/Cargo.toml b/substrate/in-instructions/pallet/Cargo.toml index 9053f3bc..a12e38b3 100644 --- a/substrate/in-instructions/pallet/Cargo.toml +++ b/substrate/in-instructions/pallet/Cargo.toml @@ -33,7 +33,6 @@ frame-support = { git = "https://github.com/serai-dex/substrate", default-featur serai-primitives = { path = "../../primitives", default-features = false } in-instructions-primitives = { package = "serai-in-instructions-primitives", path = "../primitives", default-features = false } -genesis-liquidity-primitives = { package = "serai-genesis-liquidity-primitives", path = "../../genesis-liquidity/primitives", default-features = false } coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false } @@ -57,7 +56,6 @@ std = [ "serai-primitives/std", "in-instructions-primitives/std", - "genesis-liquidity-primitives/std", "coins-pallet/std", "dex-pallet/std", diff --git a/substrate/in-instructions/pallet/src/lib.rs b/substrate/in-instructions/pallet/src/lib.rs index 7c13db76..d3652104 100644 --- a/substrate/in-instructions/pallet/src/lib.rs +++ b/substrate/in-instructions/pallet/src/lib.rs @@ -19,7 +19,6 @@ pub mod pallet { use sp_core::sr25519::Public; use serai_primitives::{Coin, Amount, Balance}; - use genesis_liquidity_primitives::GENESIS_LIQUIDITY_ACCOUNT; use frame_support::pallet_prelude::*; use frame_system::{pallet_prelude::*, RawOrigin}; @@ -34,8 +33,10 @@ pub mod pallet { Config as ValidatorSetsConfig, Pallet as ValidatorSets, }; - use genesis_liquidity_pallet::{Pallet as GenesisLiq, Config as GenesisLiqConfig}; - use emissions_pallet::{Pallet as Emissions, Config as EmissionsConfig}; + use genesis_liquidity_pallet::{ + Pallet as GenesisLiq, Config as GenesisLiqConfig, primitives::GENESIS_LIQUIDITY_ACCOUNT, + }; + use emissions_pallet::{Pallet as Emissions, Config as EmissionsConfig, primitives::POL_ACCOUNT}; use super::*; @@ -216,6 +217,7 @@ pub mod pallet { GenesisLiq::::add_coin_liquidity(address.into(), instruction.balance)?; } InInstruction::SwapToStakedSRI(address, network) => { + Coins::::mint(POL_ACCOUNT.into(), instruction.balance)?; Emissions::::swap_to_staked_sri(address.into(), network, instruction.balance)?; } } diff --git a/substrate/primitives/src/constants.rs b/substrate/primitives/src/constants.rs index c5c53d75..851763f8 100644 --- a/substrate/primitives/src/constants.rs +++ b/substrate/primitives/src/constants.rs @@ -27,3 +27,6 @@ pub const ARBITRAGE_TIME: u16 = (2 * HOURS) as u16; /// /// We additionally +1 so there is a true median. pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = (2 * ARBITRAGE_TIME) + 1; + +/// Amount of blocks per epoch in the fast-epoch feature that is used in tests. +pub const FAST_EPOCH_DURATION: u64 = 2 * MINUTES; diff --git a/substrate/runtime/src/abi.rs b/substrate/runtime/src/abi.rs index 41e60599..6f393eaa 100644 --- a/substrate/runtime/src/abi.rs +++ b/substrate/runtime/src/abi.rs @@ -138,7 +138,7 @@ impl From for RuntimeCall { }) } }, - Call::Emissions(_) => todo!(), // TODO + Call::Emissions => todo!(), // TODO Call::InInstructions(ii) => match ii { serai_abi::in_instructions::Call::execute_batch { batch } => { RuntimeCall::InInstructions(in_instructions::Call::execute_batch { batch }) diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index 78689f80..5046c1f2 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -52,7 +52,7 @@ use sp_runtime::{ #[allow(unused_imports)] use primitives::{ NetworkId, PublicKey, AccountLookup, SubstrateAmount, Coin, NETWORKS, MEDIAN_PRICE_WINDOW_LENGTH, - HOURS, DAYS, MINUTES, TARGET_BLOCK_TIME, BLOCK_SIZE, + HOURS, DAYS, MINUTES, TARGET_BLOCK_TIME, BLOCK_SIZE, FAST_EPOCH_DURATION, }; use support::{ @@ -283,7 +283,7 @@ pub type ReportLongevity = ::EpochDuration; impl babe::Config for Runtime { #[cfg(feature = "fast-epoch")] - type EpochDuration = ConstU64<{ 2 * MINUTES }>; + type EpochDuration = ConstU64<{ FAST_EPOCH_DURATION }>; #[cfg(not(feature = "fast-epoch"))] type EpochDuration = ConstU64<{ 4 * 7 * DAYS }>; diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 0da87a18..c9b94019 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -832,7 +832,6 @@ pub mod pallet { total_required } - // TODO: make the increase_allocation public instead? pub fn deposit_stake( network: NetworkId, account: T::AccountId,