diff --git a/Cargo.lock b/Cargo.lock index c8b693d2..6d641a93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7487,6 +7487,30 @@ dependencies = [ "chrono", ] +[[package]] +name = "serai-emissions-pallet" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "pallet-babe", + "parity-scale-codec", + "scale-info", + "serai-coins-pallet", + "serai-dex-pallet", + "serai-emissions-primitives", + "serai-primitives", + "serai-validator-sets-pallet", + "serai-validator-sets-primitives", + "sp-runtime", + "sp-session", + "sp-std", +] + +[[package]] +name = "serai-emissions-primitives" +version = "0.1.0" + [[package]] name = "serai-env" version = "0.1.0" @@ -7809,6 +7833,7 @@ dependencies = [ "scale-info", "serai-coins-pallet", "serai-dex-pallet", + "serai-emissions-pallet", "serai-genesis-liquidity-pallet", "serai-in-instructions-pallet", "serai-primitives", diff --git a/substrate/abi/src/emissions.rs b/substrate/abi/src/emissions.rs new file mode 100644 index 00000000..04634c6a --- /dev/null +++ b/substrate/abi/src/emissions.rs @@ -0,0 +1,14 @@ +#[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 e59f3085..593e5440 100644 --- a/substrate/abi/src/lib.rs +++ b/substrate/abi/src/lib.rs @@ -12,6 +12,7 @@ pub mod in_instructions; pub mod signals; pub mod genesis_liquidity; +pub mod emissions; pub mod babe; pub mod grandpa; @@ -26,8 +27,9 @@ pub enum Call { Coins(coins::Call), LiquidityTokens(coins::Call), Dex(dex::Call), - GenesisLiquidity(genesis_liquidity::Call), ValidatorSets(validator_sets::Call), + GenesisLiquidity(genesis_liquidity::Call), + Emissions(emissions::Call), InInstructions(in_instructions::Call), Signals(signals::Call), Babe(babe::Call), @@ -48,8 +50,9 @@ pub enum Event { Coins(coins::Event), LiquidityTokens(coins::Event), Dex(dex::Event), - GenesisLiquidity(genesis_liquidity::Event), ValidatorSets(validator_sets::Event), + GenesisLiquidity(genesis_liquidity::Event), + Emissions(emissions::Event), InInstructions(in_instructions::Event), Signals(signals::Event), Babe, diff --git a/substrate/dex/pallet/src/lib.rs b/substrate/dex/pallet/src/lib.rs index f296a262..b4cdf28d 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>; + /// Current length of the `SpotPrices` map. + #[pallet::storage] + #[pallet::getter(fn swap_volume)] + pub type SwapVolume = StorageMap<_, Identity, Coin, u64, OptionQuery>; + impl Pallet { fn restore_median( coin: Coin, @@ -890,6 +895,20 @@ pub mod pallet { } 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 new file mode 100644 index 00000000..757eaeca --- /dev/null +++ b/substrate/emissions/pallet/Cargo.toml @@ -0,0 +1,65 @@ +[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-session = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } + +pallet-babe = { git = "https://github.com/serai-dex/substrate", default-features = false } + +coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } +validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false } +dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false } + +serai-primitives = { path = "../../primitives", default-features = false } +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-session/std", + "sp-runtime/std", + + "pallet-babe/std", + + "coins-pallet/std", + "validator-sets-pallet/std", + "dex-pallet/std", + + "serai-primitives/std", + "emissions-primitives/std", +] + +default = ["std"] \ No newline at end of file diff --git a/substrate/emissions/pallet/LICENSE b/substrate/emissions/pallet/LICENSE new file mode 100644 index 00000000..e091b149 --- /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 00000000..87e2803e --- /dev/null +++ b/substrate/emissions/pallet/src/lib.rs @@ -0,0 +1,306 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding)] +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_system::pallet_prelude::*; + use frame_support::{pallet_prelude::*, sp_runtime::SaturatedConversion}; + + use sp_std::{vec, vec::Vec, collections::btree_map::BTreeMap}; + use sp_session::ShouldEndSession; + use sp_runtime; + + use coins_pallet::{Config as CoinsConfig, Pallet as Coins, AllowMint}; + use dex_pallet::{Config as DexConfig, Pallet as Dex}; + + use validator_sets_pallet::{Pallet as ValidatorSets, Config as ValidatorSetsConfig}; + use pallet_babe::{Pallet as Babe, Config as BabeConfig}; + + use serai_primitives::{NetworkId, NETWORKS, *}; + use serai_validator_sets_primitives::MAX_KEY_SHARES_PER_SET; + use emissions_primitives::*; + + #[pallet::config] + pub trait Config: + frame_system::Config + + ValidatorSetsConfig + + BabeConfig + + CoinsConfig + + DexConfig + { + 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, + /// 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 { + GenesisPeriodEnded, + AmountOverflowed, + NotEnoughLiquidity, + CanOnlyRemoveFullAmount, + } + + #[pallet::event] + pub enum Event {} + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::storage] + #[pallet::getter(fn participants)] + pub(crate) type Participants = StorageMap< + _, + Identity, + NetworkId, + BoundedVec<(PublicKey, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET }>>, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn epoch_begin_block)] + pub(crate) type EpochBeginBlock = StorageMap<_, Identity, u64, u64, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn economic_security_reached)] + pub(crate) type EconomicSecurityReached = + StorageMap<_, Identity, NetworkId, BlockNumberFor, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn last_swap_volume)] + pub(crate) type LastSwapVolume = StorageMap<_, Identity, NetworkId, u64, OptionQuery>; + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + for id in self.networks.clone() { + let mut participants = vec![]; + for p in self.participants.clone() { + participants.push((p, 0u64)); + } + Participants::::set(id, Some(participants.try_into().unwrap())); + } + + EpochBeginBlock::::set(0, 0); + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Since we are on `on_finalize`, session should have already rotated. + /// We can distribute the rewards for the last set. + fn on_finalize(n: BlockNumberFor) { + // we accept we reached economic security once we can mint smallest amount of a network's coin + for coin in COINS { + let existing = EconomicSecurityReached::::get(coin.network()); + if existing == 0u32.into() && + ::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) }) + { + EconomicSecurityReached::::set(coin.network(), n); + } + } + + // emissions start only after genesis period and happens once per epoch + // so we don't do anything before that time. + if !(n >= BLOCKS_PER_MONTH.into() && T::ShouldEndSession::should_end_session(n)) { + return; + } + + // figure out the amount of blocks in the last epoch + // TODO: we use epoch index here but should we use SessionIndex since this is how we decide + // whether time to distribute the rewards or not? Because apparently epochs != Sessions + // since we can skip some epochs if the chain is offline more than epoch duration?? + let epoch = Babe::::current_epoch().epoch_index - 1; + let block_count = n.saturated_into::() - Self::epoch_begin_block(epoch); + + // get total reward for this epoch + let pre_ec_security = Self::pre_ec_security(); + let mut distances = BTreeMap::new(); + let mut total_distance: u64 = 0; + let reward_this_epoch = if Self::initial_period(n) { + // rewards are fixed for initial period + block_count * INITIAL_REWARD_PER_BLOCK + } else if pre_ec_security { + // calculate distance to economic security per network + let mut total_required: u64 = 0; + let mut total_current: u64 = 0; + for n in NETWORKS { + if n == NetworkId::Serai { + continue; + } + + 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; + } + + distances.insert(n, required - current); + total_required = total_required.saturating_add(required); + total_current = total_current.saturating_add(current); + } + total_distance = total_required.saturating_sub(total_current); + + // add serai network portion(20%) + let new_total_distance = + total_distance.saturating_mul(10) / (10 - SERAI_VALIDATORS_DESIRED_PERCENTAGE); + distances.insert(NetworkId::Serai, new_total_distance - total_distance); + total_distance = new_total_distance; + + // rewards for pre-economic security is + // (STAKE_REQUIRED - CURRENT_STAKE) / blocks_until(SECURE_BY). + let block_reward = total_distance / Self::blocks_until(SECURE_BY); + block_count * block_reward + } else { + // post ec security + 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 = distances + .into_iter() + .map(|(n, distance)| { + let reward = if pre_ec_security { + // calculate how much each network gets based on distance to ec-security + reward_this_epoch.saturating_mul(distance) / total_distance + } else { + // 20% of the reward goes to the Serai network and rest is distributed among others + // based on swap-volume. + if n == NetworkId::Serai { + reward_this_epoch / 5 + } else { + let reward = reward_this_epoch - (reward_this_epoch / 5); + reward.saturating_mul(*volume_per_network.get(&n).unwrap_or(&0)) / total_volume + } + }; + (n, reward) + }) + .collect::>(); + + // distribute the rewards within the network + for (n, reward) in rewards_per_network { + // calculate pool vs validator share + let capacity = ValidatorSets::::total_allocated_stake(n).unwrap_or(Amount(0)).0; + let required = ValidatorSets::::required_stake_for_network(n); + let unused_capacity = capacity.saturating_sub(required); + + let distribution = unused_capacity.saturating_mul(ACCURACY_MULTIPLIER) / capacity; + let total = DESIRED_DISTRIBUTION.saturating_add(distribution); + + let validators_reward = DESIRED_DISTRIBUTION.saturating_mul(reward) / total; + let pool_reward = total - validators_reward; + + // distribute validators rewards + 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() { + // TODO: we just print a warning here instead of unwrap? + // assumes reward is equally distributed between network coins. + Coins::::mint( + Dex::::get_pool_account(*c), + Balance { coin: Coin::Serai, amount: Amount(pool_reward / coin_count) }, + ) + .unwrap(); + } + } + + // set the begin block and participants + EpochBeginBlock::::set(epoch, n.saturated_into::()); + for n in NETWORKS { + // TODO: `participants_for_latest_decided_set` returns keys with key shares but we + // store keys with actual stake amounts. Pr https://github.com/serai-dex/serai/pull/518 + // supposed to change that and so this pr relies and that pr. + Participants::::set(n, ValidatorSets::::participants_for_latest_decided_set(n)); + } + } + } + + impl Pallet { + fn blocks_until(block: u64) -> u64 { + let current = >::block_number().saturated_into::(); + block.saturating_sub(current) + } + + fn initial_period(n: BlockNumberFor) -> bool { + n >= BLOCKS_PER_MONTH.into() && n < (3 * BLOCKS_PER_MONTH).into() + } + + /// 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) == 0u32.into() { + return true; + } + } + false + } + + fn distribute_to_validators(n: NetworkId, reward: u64) { + // distribute among network's set based on + // -> (key shares * stake per share) + ((stake % stake per share) / 2) + let stake_per_share = ValidatorSets::::allocation_per_key_share(n).unwrap().0; + 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); + + total_score = total_score.saturating_add(score); + scores.push((p, score)); + } + + // stake the rewards + for (p, score) in scores { + let p_reward = reward.saturating_mul(score) / total_score; + // TODO: print a warning here? + let _ = ValidatorSets::::deposit_stake(n, p, Amount(p_reward)); + } + } + } +} + +pub use pallet::*; diff --git a/substrate/emissions/primitives/Cargo.toml b/substrate/emissions/primitives/Cargo.toml new file mode 100644 index 00000000..c7f44ca2 --- /dev/null +++ b/substrate/emissions/primitives/Cargo.toml @@ -0,0 +1,22 @@ +[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] + +[features] +std = [] +default = ["std"] diff --git a/substrate/emissions/primitives/LICENSE b/substrate/emissions/primitives/LICENSE new file mode 100644 index 00000000..659881f1 --- /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 00000000..f42509ac --- /dev/null +++ b/substrate/emissions/primitives/src/lib.rs @@ -0,0 +1,24 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +/// Amount of blocks in 30 days for 6s per block. +pub const BLOCKS_PER_MONTH: u32 = 10 * 60 * 24 * 30; + +/// INITIAL_REWARD = 100,000 SRI / BLOCKS_PER_DAY for 60 days +pub const INITIAL_REWARD_PER_BLOCK: u64 = 100_000 * 10u64.pow(8) / ((BLOCKS_PER_MONTH as u64) / 30); + +/// REWARD = 20M SRI / BLOCKS_PER_YEAR +pub const REWARD_PER_BLOCK: u64 = 20_000_000 * 10u64.pow(8) / ((BLOCKS_PER_MONTH as u64) * 12); + +/// 20% of all stake desired to be for Serai network(2/10) +pub const SERAI_VALIDATORS_DESIRED_PERCENTAGE: u64 = 2; + +/// 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 = (BLOCKS_PER_MONTH as u64) * 12; diff --git a/substrate/node/src/chain_spec.rs b/substrate/node/src/chain_spec.rs index b630c00b..9c549604 100644 --- a/substrate/node/src/chain_spec.rs +++ b/substrate/node/src/chain_spec.rs @@ -7,6 +7,7 @@ use sc_service::ChainType; use serai_runtime::{ primitives::*, WASM_BINARY, BABE_GENESIS_EPOCH_CONFIG, RuntimeGenesisConfig, SystemConfig, CoinsConfig, DexConfig, ValidatorSetsConfig, SignalsConfig, BabeConfig, GrandpaConfig, + EmissionsConfig, }; pub type ChainSpec = sc_service::GenericChainSpec; @@ -59,6 +60,10 @@ fn testnet_genesis( .collect(), participants: validators.clone(), }, + emissions: EmissionsConfig { + networks: serai_runtime::primitives::NETWORKS.to_vec(), + participants: validators.clone(), + }, signals: SignalsConfig::default(), babe: BabeConfig { authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(), diff --git a/substrate/primitives/src/networks.rs b/substrate/primitives/src/networks.rs index fd713ca1..1213378c 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 8efe0c85..d159725a 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -60,6 +60,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 } @@ -114,6 +115,7 @@ std = [ "validator-sets-pallet/std", "genesis-liquidity-pallet/std", + "emissions-pallet/std", "in-instructions-pallet/std", diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index d79f2f03..af3bc917 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -33,6 +33,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; @@ -294,6 +295,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 @@ -368,9 +373,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 d1385c2d..f712cfbf 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -489,11 +489,12 @@ 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 new_allocation < allocation_per_key_share && !block_reward { Err(Error::::InsufficientAllocation)?; } @@ -796,6 +797,15 @@ pub mod pallet { total_required } + pub fn deposit_stake( + network: NetworkId, + account: T::AccountId, + amount: Amount, + ) -> DispatchResult { + // TODO: make the increase_allocation public instead? + 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 @@ -936,7 +946,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)]