From cccc1fc7e6c5cebcfe9b447654411bd6df672193 Mon Sep 17 00:00:00 2001 From: akildemir <34187742+akildemir@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:12:04 +0300 Subject: [PATCH] Implement block emissions (#551) * add genesis liquidity implementation * add missing deposit event * fix CI issues * minor fixes * make math safer * fix fmt * implement block emissions * make remove liquidity an authorized call * implement setting initial values for coins * add genesis liquidity test & misc fixes * updato develop latest * fix rotation test * fix licencing * add fast-epoch feature * only create the pool when adding liquidity first time * add initial reward era test * test whole pre ec security emissions * fix clippy * add swap-to-staked-sri feature * rebase changes * fix tests * Remove accidentally commited ETH ABI files * fix some pr comments * Finish up fixing pr comments * exclude SRI from is_allowed check * Misc changes --------- Co-authored-by: akildemir Co-authored-by: Luke Parker --- Cargo.lock | 30 +- deny.toml | 1 + substrate/abi/Cargo.toml | 2 + substrate/abi/src/emissions.rs | 1 + substrate/abi/src/lib.rs | 12 +- substrate/client/src/serai/dex.rs | 23 +- .../client/src/serai/genesis_liquidity.rs | 5 + substrate/client/src/serai/mod.rs | 33 +- substrate/client/src/serai/validator_sets.rs | 10 +- .../client/tests/common/genesis_liquidity.rs | 115 +++++ .../client/tests/common/in_instructions.rs | 12 +- substrate/client/tests/common/mod.rs | 1 + substrate/client/tests/dex.rs | 32 +- substrate/client/tests/emissions.rs | 257 ++++++++++ substrate/client/tests/genesis_liquidity.rs | 153 +----- substrate/client/tests/validator_sets.rs | 41 +- substrate/coins/pallet/src/lib.rs | 4 +- substrate/dex/pallet/src/lib.rs | 58 +-- substrate/dex/pallet/src/tests.rs | 8 - substrate/emissions/pallet/Cargo.toml | 61 +++ substrate/emissions/pallet/LICENSE | 15 + substrate/emissions/pallet/src/lib.rs | 460 ++++++++++++++++++ substrate/emissions/primitives/Cargo.toml | 23 + substrate/emissions/primitives/LICENSE | 21 + substrate/emissions/primitives/src/lib.rs | 26 + substrate/genesis-liquidity/pallet/src/lib.rs | 5 +- substrate/in-instructions/pallet/Cargo.toml | 4 +- substrate/in-instructions/pallet/src/lib.rs | 17 +- .../in-instructions/primitives/src/lib.rs | 1 + substrate/node/src/chain_spec.rs | 36 +- substrate/primitives/src/constants.rs | 19 +- substrate/primitives/src/networks.rs | 4 +- substrate/runtime/Cargo.toml | 7 +- substrate/runtime/src/abi.rs | 22 +- substrate/runtime/src/lib.rs | 12 +- substrate/validator-sets/pallet/src/lib.rs | 52 +- 36 files changed, 1280 insertions(+), 303 deletions(-) create mode 100644 substrate/abi/src/emissions.rs create mode 100644 substrate/client/tests/common/genesis_liquidity.rs create mode 100644 substrate/client/tests/emissions.rs create mode 100644 substrate/emissions/pallet/Cargo.toml create mode 100644 substrate/emissions/pallet/LICENSE create mode 100644 substrate/emissions/pallet/src/lib.rs create mode 100644 substrate/emissions/primitives/Cargo.toml create mode 100644 substrate/emissions/primitives/LICENSE create mode 100644 substrate/emissions/primitives/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e02396ca4..e6ff320e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7930,6 +7930,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serai-coins-primitives", + "serai-emissions-primitives", "serai-genesis-liquidity-primitives", "serai-in-instructions-primitives", "serai-primitives", @@ -8089,6 +8090,32 @@ dependencies = [ "chrono", ] +[[package]] +name = "serai-emissions-pallet" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "serai-coins-pallet", + "serai-dex-pallet", + "serai-emissions-primitives", + "serai-genesis-liquidity-pallet", + "serai-primitives", + "serai-validator-sets-pallet", + "serai-validator-sets-primitives", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "serai-emissions-primitives" +version = "0.1.0" +dependencies = [ + "serai-primitives", +] + [[package]] name = "serai-env" version = "0.1.0" @@ -8172,8 +8199,8 @@ dependencies = [ "scale-info", "serai-coins-pallet", "serai-dex-pallet", + "serai-emissions-pallet", "serai-genesis-liquidity-pallet", - "serai-genesis-liquidity-primitives", "serai-in-instructions-primitives", "serai-primitives", "serai-validator-sets-pallet", @@ -8442,6 +8469,7 @@ dependencies = [ "serai-abi", "serai-coins-pallet", "serai-dex-pallet", + "serai-emissions-pallet", "serai-genesis-liquidity-pallet", "serai-in-instructions-pallet", "serai-primitives", diff --git a/deny.toml b/deny.toml index b8da5705c..b10772ef6 100644 --- a/deny.toml +++ b/deny.toml @@ -54,6 +54,7 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-dex-pallet" }, { allow = ["AGPL-3.0"], name = "serai-genesis-liquidity-pallet" }, + { allow = ["AGPL-3.0"], name = "serai-emissions-pallet" }, { allow = ["AGPL-3.0"], name = "serai-in-instructions-pallet" }, diff --git a/substrate/abi/Cargo.toml b/substrate/abi/Cargo.toml index c2947aaab..072f7460e 100644 --- a/substrate/abi/Cargo.toml +++ b/substrate/abi/Cargo.toml @@ -34,6 +34,7 @@ serai-primitives = { path = "../primitives", version = "0.1", default-features = serai-coins-primitives = { path = "../coins/primitives", version = "0.1", default-features = false } serai-validator-sets-primitives = { path = "../validator-sets/primitives", version = "0.1", default-features = false } serai-genesis-liquidity-primitives = { path = "../genesis-liquidity/primitives", version = "0.1", default-features = false } +serai-emissions-primitives = { path = "../emissions/primitives", version = "0.1", default-features = false } serai-in-instructions-primitives = { path = "../in-instructions/primitives", version = "0.1", default-features = false } serai-signals-primitives = { path = "../signals/primitives", version = "0.1", default-features = false } @@ -57,6 +58,7 @@ std = [ "serai-coins-primitives/std", "serai-validator-sets-primitives/std", "serai-genesis-liquidity-primitives/std", + "serai-emissions-primitives/std", "serai-in-instructions-primitives/std", "serai-signals-primitives/std", ] diff --git a/substrate/abi/src/emissions.rs b/substrate/abi/src/emissions.rs new file mode 100644 index 000000000..ff948f5f6 --- /dev/null +++ b/substrate/abi/src/emissions.rs @@ -0,0 +1 @@ +pub use serai_emissions_primitives as primitives; diff --git a/substrate/abi/src/lib.rs b/substrate/abi/src/lib.rs index ac8b8824c..c45a89356 100644 --- a/substrate/abi/src/lib.rs +++ b/substrate/abi/src/lib.rs @@ -16,10 +16,13 @@ pub mod liquidity_tokens; pub mod dex; pub mod validator_sets; -pub mod in_instructions; -pub mod signals; pub mod genesis_liquidity; +pub mod emissions; + +pub mod in_instructions; + +pub mod signals; pub mod babe; pub mod grandpa; @@ -32,8 +35,8 @@ pub enum Call { Coins(coins::Call), LiquidityTokens(liquidity_tokens::Call), Dex(dex::Call), - GenesisLiquidity(genesis_liquidity::Call), ValidatorSets(validator_sets::Call), + GenesisLiquidity(genesis_liquidity::Call), InInstructions(in_instructions::Call), Signals(signals::Call), Babe(babe::Call), @@ -54,8 +57,9 @@ pub enum Event { Coins(coins::Event), LiquidityTokens(liquidity_tokens::Event), Dex(dex::Event), - GenesisLiquidity(genesis_liquidity::Event), ValidatorSets(validator_sets::Event), + GenesisLiquidity(genesis_liquidity::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 d9edc56b6..ea76e6258 100644 --- a/substrate/client/src/serai/dex.rs +++ b/substrate/client/src/serai/dex.rs @@ -1,12 +1,12 @@ use sp_core::bounded_vec::BoundedVec; use serai_abi::primitives::{SeraiAddress, Amount, Coin}; -use scale::{decode_from_bytes, Encode}; - -use crate::{Serai, SeraiError, TemporalSerai}; +use crate::{SeraiError, 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> { @@ -62,17 +62,10 @@ impl<'a> SeraiDex<'a> { /// Returns the reserves of `coin:SRI` pool. pub async fn get_reserves(&self, coin: Coin) -> Result, SeraiError> { - let reserves = self - .0 - .serai - .call( - "state_call", - ["DexApi_get_reserves".to_string(), hex::encode((coin, Coin::Serai).encode())], - ) - .await?; - let bytes = Serai::hex_decode(reserves)?; - let result = decode_from_bytes::>(bytes.into()) - .map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?; - Ok(result.map(|amounts| (Amount(amounts.0), Amount(amounts.1)))) + self.0.runtime_api("DexApi_get_reserves", (coin, Coin::Serai)).await + } + + pub async fn oracle_value(&self, coin: Coin) -> Result, SeraiError> { + self.0.storage(PALLET, "SecurityOracleValue", coin).await } } diff --git a/substrate/client/src/serai/genesis_liquidity.rs b/substrate/client/src/serai/genesis_liquidity.rs index 04e80d745..00a369c8c 100644 --- a/substrate/client/src/serai/genesis_liquidity.rs +++ b/substrate/client/src/serai/genesis_liquidity.rs @@ -62,4 +62,9 @@ impl<'a> SeraiGenesisLiquidity<'a> { pub async fn supply(&self, coin: Coin) -> Result { Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero())) } + + 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 dfd14779e..c688bf365 100644 --- a/substrate/client/src/serai/mod.rs +++ b/substrate/client/src/serai/mod.rs @@ -198,17 +198,6 @@ impl Serai { Ok(()) } - // TODO: move this into substrate/client/src/validator_sets.rs - async fn active_network_validators(&self, network: NetworkId) -> Result, SeraiError> { - let validators: String = self - .call("state_call", ["SeraiRuntimeApi_validators".to_string(), hex::encode(network.encode())]) - .await?; - let bytes = Self::hex_decode(validators)?; - let r = Vec::::decode(&mut bytes.as_slice()) - .map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?; - Ok(r) - } - pub async fn latest_finalized_block_hash(&self) -> Result<[u8; 32], SeraiError> { let hash: String = self.call("chain_getFinalizedHead", ()).await?; Self::hex_decode(hash)?.try_into().map_err(|_| { @@ -378,6 +367,28 @@ impl<'a> TemporalSerai<'a> { })?)) } + async fn runtime_api( + &self, + method: &'static str, + params: P, + ) -> Result { + let result: String = self + .serai + .call( + "state_call", + [method.to_string(), hex::encode(params.encode()), hex::encode(self.block)], + ) + .await?; + + let bytes = Serai::hex_decode(result.clone())?; + R::decode(&mut bytes.as_slice()).map_err(|_| { + SeraiError::InvalidRuntime(format!( + "different type than what is expected to be returned, raw value: {}", + hex::encode(result) + )) + }) + } + pub fn coins(&'a self) -> SeraiCoins<'a> { SeraiCoins(self) } diff --git a/substrate/client/src/serai/validator_sets.rs b/substrate/client/src/serai/validator_sets.rs index 959f8ee60..ec67bae0f 100644 --- a/substrate/client/src/serai/validator_sets.rs +++ b/substrate/client/src/serai/validator_sets.rs @@ -163,7 +163,7 @@ impl<'a> SeraiValidatorSets<'a> { &self, network: NetworkId, ) -> Result, SeraiError> { - self.0.serai.active_network_validators(network).await + self.0.runtime_api("SeraiRuntimeApi_validators", network).await } // TODO: Store these separately since we almost never need both at once? @@ -178,6 +178,14 @@ impl<'a> SeraiValidatorSets<'a> { self.0.storage(PALLET, "PendingSlashReport", network).await } + pub async fn session_begin_block( + &self, + network: NetworkId, + session: Session, + ) -> Result, SeraiError> { + self.0.storage(PALLET, "SessionBeginBlock", (network, session)).await + } + pub fn set_keys( network: NetworkId, removed_participants: sp_runtime::BoundedVec< diff --git a/substrate/client/tests/common/genesis_liquidity.rs b/substrate/client/tests/common/genesis_liquidity.rs new file mode 100644 index 000000000..0c0cd2696 --- /dev/null +++ b/substrate/client/tests/common/genesis_liquidity.rs @@ -0,0 +1,115 @@ +use std::collections::HashMap; + +use rand_core::{RngCore, OsRng}; +use zeroize::Zeroizing; + +use ciphersuite::{Ciphersuite, Ristretto}; +use frost::dkg::musig::musig; +use schnorrkel::Schnorrkel; + +use sp_core::{sr25519::Signature, Pair as PairTrait}; + +use serai_abi::{ + genesis_liquidity::primitives::{oraclize_values_message, Values}, + validator_sets::primitives::{musig_context, Session, ValidatorSet}, + in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch}, + 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 set_up_genesis( + serai: &Serai, + coins: &[Coin], + values: &HashMap, +) -> (HashMap>, HashMap) { + // make accounts with amounts + let mut accounts = HashMap::new(); + for coin in coins { + // make 5 accounts per coin + let mut values = vec![]; + for _ in 0 .. 5 { + let mut address = SeraiAddress::new([0; 32]); + OsRng.fill_bytes(&mut address.0); + values.push((address, Amount(OsRng.next_u64() % 10u64.pow(coin.decimals())))); + } + accounts.insert(*coin, values); + } + + // send a batch per coin + let mut batch_ids: HashMap = HashMap::new(); + for coin in coins { + // set up instructions + let instructions = accounts[coin] + .iter() + .map(|(addr, amount)| InInstructionWithBalance { + instruction: InInstruction::GenesisLiquidity(*addr), + balance: Balance { coin: *coin, amount: *amount }, + }) + .collect::>(); + + // set up bloch hash + let mut block = BlockHash([0; 32]); + OsRng.fill_bytes(&mut block.0); + + // set up batch id + batch_ids + .entry(coin.network()) + .and_modify(|v| { + *v += 1; + }) + .or_insert(0); + + let batch = + Batch { network: coin.network(), id: batch_ids[&coin.network()], block, instructions }; + 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: values[&Coin::Monero], ether: values[&Coin::Ether], dai: values[&Coin::Dai] }; + set_values(serai, &values).await; + + (accounts, batch_ids) +} + +#[allow(dead_code)] +async fn set_values(serai: &Serai, values: &Values) { + // prepare a Musig tx to oraclize the relative values + let pair = insecure_pair_from_name("Alice"); + let public = pair.public(); + // we publish the tx in set 1 + let set = ValidatorSet { session: Session(1), 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())]), + ), + &oraclize_values_message(&set, values), + ); + + // oraclize values + let _ = + publish_tx(serai, &SeraiGenesisLiquidity::oraclize_values(*values, Signature(sig.to_bytes()))) + .await; +} diff --git a/substrate/client/tests/common/in_instructions.rs b/substrate/client/tests/common/in_instructions.rs index e335244a2..103940abf 100644 --- a/substrate/client/tests/common/in_instructions.rs +++ b/substrate/client/tests/common/in_instructions.rs @@ -10,7 +10,7 @@ use sp_core::Pair; use serai_client::{ primitives::{insecure_pair_from_name, BlockHash, NetworkId, Balance, SeraiAddress}, - validator_sets::primitives::{Session, ValidatorSet, KeyPair}, + validator_sets::primitives::{ValidatorSet, KeyPair}, in_instructions::{ primitives::{Batch, SignedBatch, batch_message, InInstruction, InInstructionWithBalance}, InInstructionsEvent, @@ -22,12 +22,12 @@ use crate::common::{tx::publish_tx, validator_sets::set_keys}; #[allow(dead_code)] pub async fn provide_batch(serai: &Serai, batch: Batch) -> [u8; 32] { - // TODO: Get the latest session - let set = ValidatorSet { session: Session(0), network: batch.network }; + let serai_latest = serai.as_of_latest_finalized_block().await.unwrap(); + let session = serai_latest.validator_sets().session(batch.network).await.unwrap().unwrap(); + let set = ValidatorSet { session, network: batch.network }; + let pair = insecure_pair_from_name(&format!("ValidatorSet {set:?}")); - let keys = if let Some(keys) = - serai.as_of_latest_finalized_block().await.unwrap().validator_sets().keys(set).await.unwrap() - { + let keys = if let Some(keys) = serai_latest.validator_sets().keys(set).await.unwrap() { keys } else { let keys = KeyPair(pair.public(), vec![].try_into().unwrap()); diff --git a/substrate/client/tests/common/mod.rs b/substrate/client/tests/common/mod.rs index e9d88594c..7dda7d0a2 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/dex.rs b/substrate/client/tests/dex.rs index da0270ff7..d02d52602 100644 --- a/substrate/client/tests/dex.rs +++ b/substrate/client/tests/dex.rs @@ -1,14 +1,13 @@ use rand_core::{RngCore, OsRng}; -use scale::Encode; -use sp_core::{Pair as PairTrait, bounded_vec::BoundedVec, hashing::blake2_256}; +use sp_core::{Pair as PairTrait, bounded_vec::BoundedVec}; use serai_abi::in_instructions::primitives::DexCall; use serai_client::{ primitives::{ Amount, NetworkId, Coin, Balance, BlockHash, insecure_pair_from_name, ExternalAddress, - SeraiAddress, PublicKey, + SeraiAddress, }, in_instructions::primitives::{ InInstruction, InInstructionWithBalance, Batch, IN_INSTRUCTION_EXECUTOR, OutAddress, @@ -28,33 +27,6 @@ use common::{ // TODO: Modularize common code // TODO: Check Transfer events serai_test!( - create_pool: (|serai: Serai| async move { - let block = serai.finalized_block_by_number(0).await.unwrap().unwrap().hash(); - let events = serai.as_of(block).dex().events().await.unwrap(); - - assert_eq!( - events, - vec![ - DexEvent::PoolCreated { - pool_id: Coin::Bitcoin, - pool_account: PublicKey::from_raw(blake2_256(&Coin::Bitcoin.encode())).into(), - }, - DexEvent::PoolCreated { - pool_id: Coin::Ether, - pool_account: PublicKey::from_raw(blake2_256(&Coin::Ether.encode())).into(), - }, - DexEvent::PoolCreated { - pool_id: Coin::Dai, - pool_account: PublicKey::from_raw(blake2_256(&Coin::Dai.encode())).into(), - }, - DexEvent::PoolCreated { - pool_id: Coin::Monero, - pool_account: PublicKey::from_raw(blake2_256(&Coin::Monero.encode())).into(), - }, - ] - ); - }) - add_liquidity: (|serai: Serai| async move { let coin = Coin::Monero; let pair = insecure_pair_from_name("Ferdie"); diff --git a/substrate/client/tests/emissions.rs b/substrate/client/tests/emissions.rs new file mode 100644 index 000000000..62ff043ea --- /dev/null +++ b/substrate/client/tests/emissions.rs @@ -0,0 +1,257 @@ +use std::{time::Duration, collections::HashMap}; +use rand_core::{RngCore, OsRng}; + +use serai_client::TemporalSerai; + +use serai_abi::{ + emissions::primitives::{INITIAL_REWARD_PER_BLOCK, SECURE_BY}, + in_instructions::primitives::Batch, + primitives::{ + BlockHash, Coin, COINS, FAST_EPOCH_DURATION, FAST_EPOCH_INITIAL_PERIOD, NETWORKS, + TARGET_BLOCK_TIME, + }, + validator_sets::primitives::Session, +}; + +use serai_client::{ + primitives::{Amount, NetworkId, Balance}, + Serai, +}; + +mod common; +use common::{genesis_liquidity::set_up_genesis, in_instructions::provide_batch}; + +serai_test_fast_epoch!( + emissions: (|serai: Serai| async move { + test_emissions(serai).await; + }) +); + +async fn send_batches(serai: &Serai, ids: &mut HashMap) { + for network in NETWORKS { + if network != NetworkId::Serai { + // set up batch id + ids + .entry(network) + .and_modify(|v| { + *v += 1; + }) + .or_insert(0); + + // set up block hash + let mut block = BlockHash([0; 32]); + OsRng.fill_bytes(&mut block.0); + + provide_batch(serai, Batch { network, id: ids[&network], block, instructions: vec![] }).await; + } + } +} + +async fn test_emissions(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 (_, 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; + } + let genesis_complete_block = serai.latest_finalized_block().await.unwrap().number(); + + for _ in 0 .. 3 { + // get current stakes + let mut current_stake = HashMap::new(); + for n in NETWORKS { + // TODO: investigate why serai network TAS isn't visible at session 0. + 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 for a session change + let current_session = wait_for_session_change(&serai).await; + + // get last block + let last_block = serai.latest_finalized_block().await.unwrap(); + let serai_latest = serai.as_of(last_block.hash()); + let change_block_number = last_block.number(); + + // get distances to ec security & block count of the previous session + let (distances, total_distance) = get_distances(&serai_latest, ¤t_stake).await; + let block_count = get_session_blocks(&serai_latest, current_session - 1).await; + + // calculate how much reward in this session + let reward_this_epoch = + if change_block_number < (genesis_complete_block + FAST_EPOCH_INITIAL_PERIOD) { + block_count * INITIAL_REWARD_PER_BLOCK + } else { + let blocks_until = SECURE_BY - change_block_number; + let block_reward = total_distance / blocks_until; + block_count * block_reward + }; + + 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::>(); + + // retire the prev-set so that TotalAllocatedStake updated. + send_batches(&serai, &mut batch_ids).await; + + for (n, reward) in reward_per_network { + let stake = serai + .as_of_latest_finalized_block() + .await + .unwrap() + .validator_sets() + .total_allocated_stake(n) + .await + .unwrap() + .unwrap_or(Amount(0)) + .0; + + // all reward should automatically staked for the network since we are in initial period. + assert_eq!(stake, *current_stake.get(&n).unwrap() + reward); + } + + // TODO: check stake per address? + // TODO: check post ec security era + } +} + +/// 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(10u64.pow(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)) +} + +async fn wait_for_session_change(serai: &Serai) -> u32 { + let current_session = serai + .as_of_latest_finalized_block() + .await + .unwrap() + .validator_sets() + .session(NetworkId::Serai) + .await + .unwrap() + .unwrap() + .0; + let next_session = current_session + 1; + + // 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(); + + next_session +} + +async fn get_distances( + serai: &TemporalSerai<'_>, + current_stake: &HashMap, +) -> (HashMap, u64) { + // 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 n in NETWORKS { + if n == NetworkId::Serai { + continue; + } + + let mut required = 0; + for c in n.coins() { + let amount = serai.coins().coin_supply(*c).await.unwrap(); + required += required_stake(serai, Balance { coin: *c, amount }).await; + } + + let mut current = *current_stake.get(&n).unwrap(); + if current > required { + current = required; + } + + let distance = required - current; + total_distance += distance; + + distances.insert(n, 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; + + (distances, total_distance) +} + +async fn get_session_blocks(serai: &TemporalSerai<'_>, session: u32) -> u64 { + let begin_block = serai + .validator_sets() + .session_begin_block(NetworkId::Serai, Session(session)) + .await + .unwrap() + .unwrap(); + + let next_begin_block = serai + .validator_sets() + .session_begin_block(NetworkId::Serai, Session(session + 1)) + .await + .unwrap() + .unwrap(); + + next_begin_block.saturating_sub(begin_block) +} diff --git a/substrate/client/tests/genesis_liquidity.rs b/substrate/client/tests/genesis_liquidity.rs index 8edc87d3d..867763e13 100644 --- a/substrate/client/tests/genesis_liquidity.rs +++ b/substrate/client/tests/genesis_liquidity.rs @@ -1,37 +1,15 @@ 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, INITIAL_GENESIS_LP_SHARES}, - SeraiGenesisLiquidity, - }, - validator_sets::primitives::{musig_context, Session, ValidatorSet}, -}; - -use serai_abi::{ - genesis_liquidity::primitives::{oraclize_values_message, Values}, - primitives::COINS, -}; +use serai_client::Serai; -use sp_core::{sr25519::Signature, Pair as PairTrait}; +use serai_abi::primitives::{Coin, COINS, Amount, GENESIS_SRI}; -use serai_client::{ - primitives::{ - Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, insecure_pair_from_name, GENESIS_SRI, - }, - in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch}, - Serai, +use serai_client::genesis_liquidity::primitives::{ + GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES, }; mod common; -use common::{in_instructions::provide_batch, tx::publish_tx}; +use common::genesis_liquidity::set_up_genesis; serai_test_fast_epoch!( genesis_liquidity: (|serai: Serai| async move { @@ -39,79 +17,25 @@ serai_test_fast_epoch!( }) ); -async fn test_genesis_liquidity(serai: Serai) { - // all coins except the native +pub async fn test_genesis_liquidity(serai: Serai) { + // set up the genesis let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::>(); - - // make accounts with amounts - let mut accounts = HashMap::new(); - for coin in coins.clone() { - // make 5 accounts per coin - let mut values = vec![]; - for _ in 0 .. 5 { - let mut address = SeraiAddress::new([0; 32]); - OsRng.fill_bytes(&mut address.0); - values.push((address, Amount(OsRng.next_u64() % 10u64.pow(coin.decimals())))); - } - accounts.insert(coin, values); + 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; } - // send a batch per coin - let mut batch_ids: HashMap = HashMap::new(); - for coin in coins.clone() { - // set up instructions - let instructions = accounts[&coin] - .iter() - .map(|(addr, amount)| InInstructionWithBalance { - instruction: InInstruction::GenesisLiquidity(*addr), - balance: Balance { coin, amount: *amount }, - }) - .collect::>(); - - // set up bloch hash - let mut block = BlockHash([0; 32]); - OsRng.fill_bytes(&mut block.0); - - // set up batch id - batch_ids - .entry(coin.network()) - .and_modify(|v| { - *v += 1; - }) - .or_insert(0); - - let batch = - Batch { network: coin.network(), id: batch_ids[&coin.network()], block, instructions }; - provide_batch(&serai, batch).await; - } - - // wait until genesis ends - let genesis_blocks = 10; // TODO - let block_time = 6; // TODO - tokio::time::timeout( - tokio::time::Duration::from_secs(3 * (genesis_blocks * block_time)), - async { - while serai.latest_finalized_block().await.unwrap().number() < 10 { - tokio::time::sleep(Duration::from_secs(6)).await; - } - }, - ) - .await - .unwrap(); - - // set values relative to each other - // 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), - ]); - - // wait a little bit.. - tokio::time::sleep(Duration::from_secs(12)).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. @@ -133,7 +57,7 @@ async fn test_genesis_liquidity(serai: Serai) { 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()) + (total_coin * u128::from(values[&coin])) / 10u128.pow(coin.decimals()) } else { total_coin }; @@ -181,36 +105,3 @@ async fn test_genesis_liquidity(serai: Serai) { // TODO: test remove the liq before/after genesis ended. } - -async fn set_values(serai: &Serai, values: &Values) { - // prepare a Musig tx to oraclize the relative values - let pair = insecure_pair_from_name("Alice"); - let public = pair.public(); - // we publish the tx in set 4 - let set = ValidatorSet { session: Session(4), 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())]), - ), - &oraclize_values_message(&set, values), - ); - - // oraclize values - let _ = - publish_tx(serai, &SeraiGenesisLiquidity::oraclize_values(*values, Signature(sig.to_bytes()))) - .await; -} diff --git a/substrate/client/tests/validator_sets.rs b/substrate/client/tests/validator_sets.rs index 8aa8174f4..c2c6c509d 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 30 secs since the epoch time. - let block = tokio::time::timeout(core::time::Duration::from_secs(2 * 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; - } - while session_for_block(serai, block, network).await > session { - block = serai.block(block).await.unwrap().unwrap().header.parent_hash.0; + // 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(TARGET_BLOCK_TIME)).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; } - assert_eq!(session_for_block(serai, block, network).await, session); - break block; - } - }) + }, + ) .await .unwrap(); let serai_for_block = serai.as_of(block); @@ -358,10 +363,10 @@ 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; + tokio::time::sleep(core::time::Duration::from_secs(TARGET_BLOCK_TIME)).await; finalized_block = serai.latest_finalized_block().await.unwrap().header.number; } }) diff --git a/substrate/coins/pallet/src/lib.rs b/substrate/coins/pallet/src/lib.rs index 510e0edfb..f6b055f70 100644 --- a/substrate/coins/pallet/src/lib.rs +++ b/substrate/coins/pallet/src/lib.rs @@ -159,7 +159,9 @@ pub mod pallet { /// /// Errors if any amount overflows. pub fn mint(to: Public, balance: Balance) -> Result<(), Error> { - if !T::AllowMint::is_allowed(&balance) { + // If the coin isn't Serai, which we're always allowed to mint, and the mint isn't explicitly + // allowed, error + if (balance.coin != Coin::Serai) && (!T::AllowMint::is_allowed(&balance)) { Err(Error::::MintNotAllowed)?; } diff --git a/substrate/dex/pallet/src/lib.rs b/substrate/dex/pallet/src/lib.rs index f296a2622..04ab02e6f 100644 --- a/substrate/dex/pallet/src/lib.rs +++ b/substrate/dex/pallet/src/lib.rs @@ -194,6 +194,11 @@ pub mod pallet { #[pallet::getter(fn security_oracle_value)] pub type SecurityOracleValue = StorageMap<_, Identity, Coin, Amount, OptionQuery>; + /// Total swap volume of a given pool in terms of SRI. + #[pallet::storage] + #[pallet::getter(fn swap_volume)] + pub type SwapVolume = StorageMap<_, Identity, PoolId, u64, OptionQuery>; + impl Pallet { fn restore_median( coin: Coin, @@ -373,31 +378,6 @@ pub mod pallet { }, } - #[pallet::genesis_config] - #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] - pub struct GenesisConfig { - /// Pools to create at launch. - pub pools: Vec, - /// field just to have T. - pub _ignore: PhantomData, - } - - impl Default for GenesisConfig { - fn default() -> Self { - GenesisConfig { pools: Default::default(), _ignore: Default::default() } - } - } - - #[pallet::genesis_build] - impl BuildGenesisConfig for GenesisConfig { - fn build(&self) { - // create the pools - for coin in &self.pools { - Pallet::::create_pool(*coin).unwrap(); - } - } - } - #[pallet::error] pub enum Error { /// Provided coins are equal. @@ -510,19 +490,15 @@ pub mod pallet { /// /// Once a pool is created, someone may [`Pallet::add_liquidity`] to it. pub(crate) fn create_pool(coin: Coin) -> DispatchResult { - ensure!(coin != Coin::Serai, Error::::EqualCoins); - - // prepare pool_id - let pool_id = Self::get_pool_id(coin, Coin::Serai).unwrap(); + // get pool_id + let pool_id = Self::get_pool_id(coin, Coin::Serai)?; ensure!(!Pools::::contains_key(pool_id), Error::::PoolExists); let pool_account = Self::get_pool_account(pool_id); frame_system::Pallet::::inc_providers(&pool_account); Pools::::insert(pool_id, ()); - Self::deposit_event(Event::PoolCreated { pool_id, pool_account }); - Ok(()) } @@ -561,11 +537,14 @@ pub mod pallet { ) -> DispatchResult { let sender = ensure_signed(origin)?; ensure!((sri_desired > 0) && (coin_desired > 0), Error::::WrongDesiredAmount); - ensure!(coin != Coin::Serai, Error::::EqualCoins); - let pool_id = Self::get_pool_id(coin, Coin::Serai).unwrap(); + let pool_id = Self::get_pool_id(coin, Coin::Serai)?; - Pools::::get(pool_id).as_ref().ok_or(Error::::PoolNotFound)?; + // create the pool if it doesn't exist. We can just attempt to do that because our checks + // far enough to allow that. + if Pools::::get(pool_id).is_none() { + Self::create_pool(coin)?; + } let pool_account = Self::get_pool_account(pool_id); let sri_reserve = Self::get_balance(&pool_account, Coin::Serai); @@ -887,9 +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; } + Self::deposit_event(Event::SwapExecuted { who: sender, send_to, diff --git a/substrate/dex/pallet/src/tests.rs b/substrate/dex/pallet/src/tests.rs index b00141997..ee714471f 100644 --- a/substrate/dex/pallet/src/tests.rs +++ b/substrate/dex/pallet/src/tests.rs @@ -1155,16 +1155,8 @@ fn can_not_swap_same_coin() { new_test_ext().execute_with(|| { let user = system_address(b"user1").into(); let coin1 = Coin::Dai; - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(1000) })); - let liquidity1 = 1000; - let liquidity2 = 20; - assert_noop!( - Dex::add_liquidity(RuntimeOrigin::signed(user), coin1, liquidity2, liquidity1, 1, 1, user,), - Error::::PoolNotFound - ); - let exchange_amount = 10; assert_noop!( Dex::swap_exact_tokens_for_tokens( diff --git a/substrate/emissions/pallet/Cargo.toml b/substrate/emissions/pallet/Cargo.toml new file mode 100644 index 000000000..878175730 --- /dev/null +++ b/substrate/emissions/pallet/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "serai-emissions-pallet" +version = "0.1.0" +description = "Emissions pallet for Serai" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/substrate/emissions/pallet" +authors = ["Akil Demir "] +edition = "2021" +rust-version = "1.77" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.cargo-machete] +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"] } + +frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false } +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-runtime = { 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 } +genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../../genesis-liquidity/pallet", default-features = false } + +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 } + +[features] +std = [ + "scale/std", + "scale-info/std", + + "frame-system/std", + "frame-support/std", + + "sp-std/std", + "sp-runtime/std", + + "coins-pallet/std", + "validator-sets-pallet/std", + "dex-pallet/std", + "genesis-liquidity-pallet/std", + + "serai-primitives/std", + "emissions-primitives/std", +] +fast-epoch = [] +try-runtime = [] # TODO +default = ["std"] diff --git a/substrate/emissions/pallet/LICENSE b/substrate/emissions/pallet/LICENSE new file mode 100644 index 000000000..e091b1498 --- /dev/null +++ b/substrate/emissions/pallet/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2024 Luke Parker + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/substrate/emissions/pallet/src/lib.rs b/substrate/emissions/pallet/src/lib.rs new file mode 100644 index 000000000..66310d5cd --- /dev/null +++ b/substrate/emissions/pallet/src/lib.rs @@ -0,0 +1,460 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding, clippy::empty_docs)] +#[frame_support::pallet] +pub mod pallet { + use super::*; + 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}; + 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 genesis_liquidity_pallet::{Pallet as GenesisLiquidity, Config as GenesisLiquidityConfig}; + + use serai_primitives::*; + use validator_sets_primitives::{MAX_KEY_SHARES_PER_SET, Session}; + pub use emissions_primitives as primitives; + use primitives::*; + + #[pallet::config] + pub trait Config: + frame_system::Config + + ValidatorSetsConfig + + CoinsConfig + + DexConfig + + GenesisLiquidityConfig + { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::genesis_config] + #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] + pub struct GenesisConfig { + /// Networks to spawn Serai with. + pub networks: Vec<(NetworkId, Amount)>, + /// List of participants to place in the initial validator sets. + pub participants: Vec, + } + + impl Default for GenesisConfig { + fn default() -> Self { + GenesisConfig { networks: Default::default(), participants: Default::default() } + } + } + + #[pallet::error] + pub enum Error { + NetworkHasEconomicSecurity, + NoValueForCoin, + InsufficientAllocation, + } + + #[pallet::event] + pub enum Event {} + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + // TODO: Remove this. This should be the sole domain of validator-sets + #[pallet::storage] + #[pallet::getter(fn participants)] + pub(crate) type Participants = StorageMap< + _, + Identity, + NetworkId, + BoundedVec<(PublicKey, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET }>>, + OptionQuery, + >; + + // TODO: Remove this too + #[pallet::storage] + #[pallet::getter(fn session)] + pub type CurrentSession = StorageMap<_, Identity, NetworkId, u32, ValueQuery>; + + // TODO: Find a better place for this + #[pallet::storage] + #[pallet::getter(fn economic_security_reached)] + pub(crate) type EconomicSecurityReached = + StorageMap<_, Identity, NetworkId, bool, ValueQuery>; + + // TODO: Find a better place for this + #[pallet::storage] + pub(crate) type GenesisCompleteBlock = StorageValue<_, u64, OptionQuery>; + + #[pallet::storage] + pub(crate) type LastSwapVolume = StorageMap<_, Identity, Coin, u64, OptionQuery>; + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + for (id, stake) in self.networks.clone() { + let mut participants = vec![]; + for p in self.participants.clone() { + participants.push((p, stake.0)); + } + Participants::::set(id, Some(participants.try_into().unwrap())); + CurrentSession::::set(id, 0); + EconomicSecurityReached::::set(id, false); + } + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(n: BlockNumberFor) -> Weight { + if GenesisCompleteBlock::::get().is_none() && + GenesisLiquidity::::genesis_complete().is_some() + { + GenesisCompleteBlock::::set(Some(n.saturated_into::())); + } + + // we wait 1 extra block after genesis ended to see the changes. We only need this extra + // block in dev&test networks where we start the chain with accounts that already has some + // staked SRI. So when we check for ec-security pre-genesis we look like we are economically + // secure. The reason for this although we only check for it once the genesis is complete(so + // if the genesis complete we shouldn't be economically secure because the funds are not + // enough) is because ValidatorSets pallet runs before the genesis pallet in runtime. + // So ValidatorSets pallet sees the old state until next block. + // TODO: revisit this when mainnet genesis validator stake code is done. + let gcb = GenesisCompleteBlock::::get(); + let genesis_ended = gcb.is_some() && (n.saturated_into::() > gcb.unwrap()); + + // we accept we reached economic security once we can mint smallest amount of a network's coin + for coin in COINS { + let check = genesis_ended && !Self::economic_security_reached(coin.network()); + if check && ::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) }) + { + EconomicSecurityReached::::set(coin.network(), true); + } + } + + // check if we got a new session + let mut session_changed = false; + let session = ValidatorSets::::session(NetworkId::Serai).unwrap_or(Session(0)); + if session.0 > Self::session(NetworkId::Serai) { + session_changed = true; + CurrentSession::::set(NetworkId::Serai, session.0); + } + + // update participants per session before the genesis + // after the genesis, we update them after reward distribution. + if (!genesis_ended) && session_changed { + Self::update_participants(); + } + + // We only want to distribute emissions if the genesis period is over AND the session has + // ended + if !(genesis_ended && session_changed) { + return Weight::zero(); // TODO + } + + // figure out the amount of blocks in the last session + // Since the session has changed, we're now at least at session 1 + let block_count = ValidatorSets::::session_begin_block(NetworkId::Serai, session) - + ValidatorSets::::session_begin_block(NetworkId::Serai, Session(session.0 - 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 pre_ec_security { + // 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; + + 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 + }; + + // map epoch ec-security-distance/volume to rewards + 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 { + // 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 = LastSwapVolume::::get(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 { + 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, network_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 validators_reward = DESIRED_DISTRIBUTION.saturating_mul(reward) / total; + let network_pool_reward = reward.saturating_sub(validators_reward); + (validators_reward, network_pool_reward) + }; + + // distribute validators rewards + Self::distribute_to_validators(n, validators_reward); + + // send the rest to the pool + 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; + } + } + } + } + + // 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. + Self::update_participants(); + Weight::zero() // TODO + } + } + + impl Pallet { + fn blocks_until(block: u64) -> u64 { + let current = >::block_number().saturated_into::(); + block.saturating_sub(current) + } + + 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 = GenesisCompleteBlock::::get(); + genesis_complete_block.is_some() && + (n.saturated_into::() < (genesis_complete_block.unwrap() + initial_period_duration)) + } + + /// Returns true if any of the external networks haven't reached economic security yet. + fn pre_ec_security() -> bool { + for n in NETWORKS { + if n == NetworkId::Serai { + continue; + } + + if !Self::economic_security_reached(n) { + return true; + } + } + false + } + + // 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 / 2); + + total_score = total_score.saturating_add(score); + scores.push((p, score)); + } + + // stake the rewards + for (p, score) in scores { + let p_reward = u64::try_from( + u128::from(reward).saturating_mul(u128::from(score)) / u128::from(total_score), + ) + .unwrap(); + + Coins::::mint(p, Balance { coin: Coin::Serai, amount: Amount(p_reward) }).unwrap(); + if ValidatorSets::::distribute_block_rewards(n, p, Amount(p_reward)).is_err() { + // TODO: log the failure + continue; + } + } + } + + pub fn swap_to_staked_sri( + to: PublicKey, + network: NetworkId, + balance: Balance, + ) -> DispatchResult { + // check the network didn't reach the economic security yet + if Self::economic_security_reached(network) { + Err(Error::::NetworkHasEconomicSecurity)?; + } + + // 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); + + // Mint + Coins::::mint(to, Balance { coin: Coin::Serai, amount: sri_amount })?; + + // Stake the SRI for the network. + ValidatorSets::::allocate( + frame_system::RawOrigin::Signed(to).into(), + network, + sri_amount, + )?; + Ok(()) + } + + fn update_participants() { + for n in NETWORKS { + let participants = ValidatorSets::::participants_for_latest_decided_set(n) + .unwrap() + .into_iter() + .map(|(key, _)| (key, ValidatorSets::::allocation((n, key)).unwrap_or(Amount(0)).0)) + .collect::>(); + + Participants::::set(n, Some(participants.try_into().unwrap())); + } + } + } +} + +pub use pallet::*; diff --git a/substrate/emissions/primitives/Cargo.toml b/substrate/emissions/primitives/Cargo.toml new file mode 100644 index 000000000..db3d38e0b --- /dev/null +++ b/substrate/emissions/primitives/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "serai-emissions-primitives" +version = "0.1.0" +description = "Serai emissions primitives" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/substrate/emissions/primitives" +authors = ["Akil Demir "] +edition = "2021" +rust-version = "1.77" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +serai-primitives = { path = "../../primitives", default-features = false } + +[features] +std = ["serai-primitives/std"] +default = ["std"] diff --git a/substrate/emissions/primitives/LICENSE b/substrate/emissions/primitives/LICENSE new file mode 100644 index 000000000..659881f1a --- /dev/null +++ b/substrate/emissions/primitives/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/substrate/emissions/primitives/src/lib.rs b/substrate/emissions/primitives/src/lib.rs new file mode 100644 index 000000000..38aa8ca2e --- /dev/null +++ b/substrate/emissions/primitives/src/lib.rs @@ -0,0 +1,26 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +use serai_primitives::{DAYS, YEARS, SeraiAddress, system_address}; + +// Protocol owned liquidity account. +pub const POL_ACCOUNT: SeraiAddress = system_address(b"Serai-protocol_owned_liquidity"); + +/// INITIAL_REWARD = 100,000 SRI / BLOCKS_PER_DAY for 60 days +pub const INITIAL_REWARD_PER_BLOCK: u64 = (100_000 * 10u64.pow(8)) / DAYS; + +/// REWARD = 20M SRI / BLOCKS_PER_YEAR +pub const REWARD_PER_BLOCK: u64 = (20_000_000 * 10u64.pow(8)) / YEARS; + +/// 20% of all stake desired to be for Serai network +pub const SERAI_VALIDATORS_DESIRED_PERCENTAGE: u64 = 20; + +/// Desired unused capacity ratio for a network assuming capacity is 10,000. +pub const DESIRED_DISTRIBUTION: u64 = 1_000; + +/// Percentage scale for the validator vs. pool reward distribution. +pub const ACCURACY_MULTIPLIER: u64 = 10_000; + +/// The block to target for economic security +pub const SECURE_BY: u64 = YEARS; diff --git a/substrate/genesis-liquidity/pallet/src/lib.rs b/substrate/genesis-liquidity/pallet/src/lib.rs index ad371ae41..0b463af58 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::*; @@ -15,7 +15,7 @@ pub mod pallet { use coins_pallet::{Config as CoinsConfig, Pallet as Coins, AllowMint}; use validator_sets_pallet::{Config as VsConfig, Pallet as ValidatorSets}; - use serai_primitives::{Coin, COINS, *}; + use serai_primitives::*; use validator_sets_primitives::{ValidatorSet, musig_key}; pub use genesis_liquidity_primitives as primitives; use primitives::*; @@ -72,6 +72,7 @@ pub mod pallet { pub(crate) type Oracle = StorageMap<_, Identity, Coin, u64, OptionQuery>; #[pallet::storage] + #[pallet::getter(fn genesis_complete)] pub(crate) type GenesisComplete = StorageValue<_, (), OptionQuery>; #[pallet::hooks] diff --git a/substrate/in-instructions/pallet/Cargo.toml b/substrate/in-instructions/pallet/Cargo.toml index 4eafd199e..a12e38b3b 100644 --- a/substrate/in-instructions/pallet/Cargo.toml +++ b/substrate/in-instructions/pallet/Cargo.toml @@ -33,12 +33,12 @@ 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 } validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false } genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../../genesis-liquidity/pallet", default-features = false } +emissions-pallet = { package = "serai-emissions-pallet", path = "../../emissions/pallet", default-features = false } [features] std = [ @@ -56,12 +56,12 @@ std = [ "serai-primitives/std", "in-instructions-primitives/std", - "genesis-liquidity-primitives/std", "coins-pallet/std", "dex-pallet/std", "validator-sets-pallet/std", "genesis-liquidity-pallet/std", + "emissions-pallet/std", ] default = ["std"] diff --git a/substrate/in-instructions/pallet/src/lib.rs b/substrate/in-instructions/pallet/src/lib.rs index 2667b13df..d3652104c 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,13 +33,21 @@ pub mod pallet { Config as ValidatorSetsConfig, Pallet as ValidatorSets, }; - use genesis_liquidity_pallet::{Pallet as GenesisLiq, Config as GenesisLiqConfig}; + 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::*; #[pallet::config] pub trait Config: - frame_system::Config + CoinsConfig + DexConfig + ValidatorSetsConfig + GenesisLiqConfig + frame_system::Config + + CoinsConfig + + DexConfig + + ValidatorSetsConfig + + GenesisLiqConfig + + EmissionsConfig { type RuntimeEvent: From> + IsType<::RuntimeEvent>; } @@ -209,6 +216,10 @@ pub mod pallet { Coins::::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance)?; 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)?; + } } Ok(()) } diff --git a/substrate/in-instructions/primitives/src/lib.rs b/substrate/in-instructions/primitives/src/lib.rs index 87d9ce373..1455e4236 100644 --- a/substrate/in-instructions/primitives/src/lib.rs +++ b/substrate/in-instructions/primitives/src/lib.rs @@ -79,6 +79,7 @@ pub enum InInstruction { Transfer(SeraiAddress), Dex(DexCall), GenesisLiquidity(SeraiAddress), + SwapToStakedSRI(SeraiAddress, NetworkId), } #[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] diff --git a/substrate/node/src/chain_spec.rs b/substrate/node/src/chain_spec.rs index e66ee4a6d..e67674cc5 100644 --- a/substrate/node/src/chain_spec.rs +++ b/substrate/node/src/chain_spec.rs @@ -7,7 +7,7 @@ use sc_service::ChainType; use serai_runtime::{ primitives::*, WASM_BINARY, BABE_GENESIS_EPOCH_CONFIG, RuntimeGenesisConfig, SystemConfig, - CoinsConfig, DexConfig, ValidatorSetsConfig, SignalsConfig, BabeConfig, GrandpaConfig, + CoinsConfig, ValidatorSetsConfig, SignalsConfig, BabeConfig, GrandpaConfig, EmissionsConfig, }; pub type ChainSpec = sc_service::GenericChainSpec; @@ -46,11 +46,6 @@ fn devnet_genesis( _ignore: Default::default(), }, - dex: DexConfig { - pools: vec![Coin::Bitcoin, Coin::Ether, Coin::Dai, Coin::Monero], - _ignore: Default::default(), - }, - validator_sets: ValidatorSetsConfig { networks: serai_runtime::primitives::NETWORKS .iter() @@ -63,6 +58,18 @@ fn devnet_genesis( .collect(), participants: validators.clone(), }, + emissions: EmissionsConfig { + 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(), babe: BabeConfig { authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(), @@ -97,11 +104,6 @@ fn testnet_genesis(wasm_binary: &[u8], validators: Vec<&'static str>) -> Runtime _ignore: Default::default(), }, - dex: DexConfig { - pools: vec![Coin::Bitcoin, Coin::Ether, Coin::Dai, Coin::Monero], - _ignore: Default::default(), - }, - validator_sets: ValidatorSetsConfig { networks: serai_runtime::primitives::NETWORKS .iter() @@ -114,6 +116,18 @@ fn testnet_genesis(wasm_binary: &[u8], validators: Vec<&'static str>) -> Runtime .collect(), participants: validators.clone(), }, + emissions: EmissionsConfig { + 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(), babe: BabeConfig { authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(), diff --git a/substrate/primitives/src/constants.rs b/substrate/primitives/src/constants.rs index c5c53d75d..b3db73178 100644 --- a/substrate/primitives/src/constants.rs +++ b/substrate/primitives/src/constants.rs @@ -7,13 +7,16 @@ pub const TARGET_BLOCK_TIME: u64 = 6; /// Measured in blocks. pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME; -pub const HOURS: BlockNumber = MINUTES * 60; -pub const DAYS: BlockNumber = HOURS * 24; -pub const WEEKS: BlockNumber = DAYS * 7; -pub const MONTHS: BlockNumber = WEEKS * 4; +pub const HOURS: BlockNumber = 60 * MINUTES; +pub const DAYS: BlockNumber = 24 * HOURS; +pub const WEEKS: BlockNumber = 7 * DAYS; +// Defines a month as 30 days, which is slightly inaccurate +pub const MONTHS: BlockNumber = 30 * DAYS; +// Defines a year as 12 inaccurate months, which is 360 days literally (~1.5% off) +pub const YEARS: BlockNumber = 12 * MONTHS; /// 6 months of blocks -pub const GENESIS_SRI_TRICKLE_FEED: u64 = MONTHS * 6; +pub const GENESIS_SRI_TRICKLE_FEED: u64 = 6 * MONTHS; // 100 Million SRI pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8); @@ -27,3 +30,9 @@ 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; + +/// Amount of blocks for the initial period era of the emissions under the fast-epoch feature. +pub const FAST_EPOCH_INITIAL_PERIOD: u64 = 2 * FAST_EPOCH_DURATION; diff --git a/substrate/primitives/src/networks.rs b/substrate/primitives/src/networks.rs index fd713ca1b..1213378c4 100644 --- a/substrate/primitives/src/networks.rs +++ b/substrate/primitives/src/networks.rs @@ -15,7 +15,9 @@ use sp_core::{ConstU32, bounded::BoundedVec}; use crate::{borsh_serialize_bounded_vec, borsh_deserialize_bounded_vec}; /// The type used to identify networks. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[derive( + Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, PartialOrd, Ord, MaxEncodedLen, TypeInfo, +)] #[cfg_attr(feature = "std", derive(Zeroize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index a8e60174f..8419d9bfb 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -61,6 +61,7 @@ dex-pallet = { package = "serai-dex-pallet", path = "../dex/pallet", default-fea validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../validator-sets/pallet", default-features = false } genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../genesis-liquidity/pallet", default-features = false } +emissions-pallet = { package = "serai-emissions-pallet", path = "../emissions/pallet", default-features = false } in-instructions-pallet = { package = "serai-in-instructions-pallet", path = "../in-instructions/pallet", default-features = false } @@ -117,6 +118,7 @@ std = [ "validator-sets-pallet/std", "genesis-liquidity-pallet/std", + "emissions-pallet/std", "in-instructions-pallet/std", @@ -129,7 +131,10 @@ std = [ "pallet-transaction-payment-rpc-runtime-api/std", ] -fast-epoch = ["genesis-liquidity-pallet/fast-epoch"] +fast-epoch = [ + "genesis-liquidity-pallet/fast-epoch", + "emissions-pallet/fast-epoch", +] runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", diff --git a/substrate/runtime/src/abi.rs b/substrate/runtime/src/abi.rs index b479036d2..48b4a6c74 100644 --- a/substrate/runtime/src/abi.rs +++ b/substrate/runtime/src/abi.rs @@ -89,17 +89,6 @@ impl From for RuntimeCall { send_to: send_to.into(), }), }, - Call::GenesisLiquidity(gl) => match gl { - serai_abi::genesis_liquidity::Call::remove_coin_liquidity { balance } => { - RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::remove_coin_liquidity { balance }) - } - serai_abi::genesis_liquidity::Call::oraclize_values { values, signature } => { - RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::oraclize_values { - values, - signature, - }) - } - }, Call::ValidatorSets(vs) => match vs { serai_abi::validator_sets::Call::set_keys { network, @@ -138,6 +127,17 @@ impl From for RuntimeCall { RuntimeCall::ValidatorSets(validator_sets::Call::claim_deallocation { network, session }) } }, + Call::GenesisLiquidity(gl) => match gl { + serai_abi::genesis_liquidity::Call::remove_coin_liquidity { balance } => { + RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::remove_coin_liquidity { balance }) + } + serai_abi::genesis_liquidity::Call::oraclize_values { values, signature } => { + RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::oraclize_values { + values, + signature, + }) + } + }, 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 5301f0432..5046c1f25 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -32,6 +32,7 @@ pub use pallet_babe as babe; pub use pallet_grandpa as grandpa; pub use genesis_liquidity_pallet as genesis_liquidity; +pub use emissions_pallet as emissions; // Actually used by the runtime use sp_core::OpaqueMetadata; @@ -51,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::{ @@ -252,6 +253,10 @@ impl genesis_liquidity::Config for Runtime { type RuntimeEvent = RuntimeEvent; } +impl emissions::Config for Runtime { + type RuntimeEvent = RuntimeEvent; +} + // for publishing equivocation evidences. impl frame_system::offchain::SendTransactionTypes for Runtime where @@ -278,7 +283,7 @@ pub type ReportLongevity = ::EpochDuration; impl babe::Config for Runtime { #[cfg(feature = "fast-epoch")] - type EpochDuration = ConstU64<{ MINUTES / 2 }>; // 30 seconds + type EpochDuration = ConstU64<{ FAST_EPOCH_DURATION }>; #[cfg(not(feature = "fast-epoch"))] type EpochDuration = ConstU64<{ 4 * 7 * DAYS }>; @@ -326,9 +331,10 @@ construct_runtime!( Coins: coins, LiquidityTokens: coins::::{Pallet, Call, Storage, Event}, Dex: dex, - GenesisLiquidity: genesis_liquidity, ValidatorSets: validator_sets, + GenesisLiquidity: genesis_liquidity, + Emissions: emissions, InInstructions: in_instructions, diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index b89d65964..a404ae732 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -15,6 +15,7 @@ use sp_staking::offence::{ReportOffence, Offence, OffenceError}; use frame_system::{pallet_prelude::*, RawOrigin}; use frame_support::{ pallet_prelude::*, + sp_runtime::SaturatedConversion, traits::{DisabledValidators, KeyOwnerProofSystem, FindAuthor}, BoundedVec, WeakBoundedVec, StoragePrefixedMap, }; @@ -262,12 +263,20 @@ pub mod pallet { _t: PhantomData, prefix: Vec, last: Vec, + allocation_per_key_share: Amount, } impl SortedAllocationsIter { fn new(network: NetworkId) -> Self { let mut prefix = SortedAllocations::::final_prefix().to_vec(); prefix.extend(&network.encode()); - Self { _t: PhantomData, prefix: prefix.clone(), last: prefix } + Self { + _t: PhantomData, + prefix: prefix.clone(), + last: prefix, + allocation_per_key_share: Pallet::::allocation_per_key_share(network).expect( + "SortedAllocationsIter iterating over a network without a set allocation per key share", + ), + } } } impl Iterator for SortedAllocationsIter { @@ -275,10 +284,17 @@ pub mod pallet { fn next(&mut self) -> Option { let next = sp_io::storage::next_key(&self.last)?; if !next.starts_with(&self.prefix) { - return None; + None?; } let key = Pallet::::recover_key_from_sorted_allocation_key(&next); let amount = Pallet::::recover_amount_from_sorted_allocation_key(&next); + + // We may have validators present, with less than the minimum allocation, due to block + // rewards + if amount.0 < self.allocation_per_key_share.0 { + None?; + } + self.last = next; Some((key, amount)) } @@ -309,6 +325,12 @@ pub mod pallet { #[pallet::storage] pub type SeraiDisabledIndices = StorageMap<_, Identity, u32, Public, OptionQuery>; + /// Mapping from session to its starting block number. + #[pallet::storage] + #[pallet::getter(fn session_begin_block)] + pub type SessionBeginBlock = + StorageDoubleMap<_, Identity, NetworkId, Identity, Session, u64, ValueQuery>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -391,6 +413,11 @@ pub mod pallet { Pallet::::deposit_event(Event::NewSet { set }); Participants::::set(network, Some(participants.try_into().unwrap())); + SessionBeginBlock::::set( + network, + session, + >::block_number().saturated_into::(), + ); } } @@ -490,11 +517,13 @@ pub mod pallet { network: NetworkId, account: T::AccountId, amount: Amount, + block_reward: bool, ) -> DispatchResult { let old_allocation = Self::allocation((network, account)).unwrap_or(Amount(0)).0; let new_allocation = old_allocation + amount.0; let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0; - if new_allocation < allocation_per_key_share { + // If this is a block reward, we always allow it to be allocated + if (new_allocation < allocation_per_key_share) && (!block_reward) { Err(Error::::InsufficientAllocation)?; } @@ -819,6 +848,21 @@ pub mod pallet { total_required } + pub fn distribute_block_rewards( + network: NetworkId, + account: T::AccountId, + amount: Amount, + ) -> DispatchResult { + // TODO: Should this call be part of the `increase_allocation` since we have to have it + // before each call to it? + Coins::::transfer_internal( + account, + Self::account(), + Balance { coin: Coin::Serai, amount }, + )?; + Self::increase_allocation(network, account, amount, true) + } + fn can_slash_serai_validator(validator: Public) -> bool { // Checks if they're active or actively deallocating (letting us still slash them) // We could check if they're upcoming/still allocating, yet that'd mean the equivocation is @@ -966,7 +1010,7 @@ pub mod pallet { Self::account(), Balance { coin: Coin::Serai, amount }, )?; - Self::increase_allocation(network, validator, amount) + Self::increase_allocation(network, validator, amount, false) } #[pallet::call_index(3)]