From 989beb8350273a8fb8a5a78374b9a79c5b5abfb9 Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Wed, 8 Nov 2023 15:01:29 +0800 Subject: [PATCH] Simplify Crab token system (#1310) * Stash * Simplify Crab token system * Doc * Fix benchmarks * Emit an event instead of a log * Add more tests --- Cargo.lock | 3 + pallet/account-migration/src/mock.rs | 10 +- pallet/staking/Cargo.toml | 4 +- pallet/staking/src/benchmarking.rs | 14 +- pallet/staking/src/lib.rs | 182 ++++++++++++------------ pallet/staking/src/mock.rs | 163 ++++++++++++++++----- pallet/staking/src/tests.rs | 110 ++++++++++++-- precompile/staking/src/mock.rs | 5 +- runtime/crab/src/pallets/staking.rs | 27 ++-- runtime/darwinia/Cargo.toml | 2 + runtime/darwinia/src/pallets/staking.rs | 44 ++++-- runtime/pangolin/src/pallets/staking.rs | 21 ++- runtime/pangoro/Cargo.toml | 2 + runtime/pangoro/src/pallets/staking.rs | 44 ++++-- 14 files changed, 442 insertions(+), 189 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7c0b919c..375a839fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2862,6 +2862,7 @@ dependencies = [ "darwinia-precompile-staking", "darwinia-precompile-state-storage", "darwinia-staking", + "dc-inflation", "dc-primitives", "fp-evm", "fp-rpc", @@ -2952,6 +2953,7 @@ dependencies = [ "pallet-balances", "pallet-session", "pallet-timestamp", + "pallet-treasury", "parity-scale-codec", "pretty_env_logger", "scale-info", @@ -8949,6 +8951,7 @@ dependencies = [ "darwinia-precompile-staking", "darwinia-precompile-state-storage", "darwinia-staking", + "dc-inflation", "dc-primitives", "fp-evm", "fp-rpc", diff --git a/pallet/account-migration/src/mock.rs b/pallet/account-migration/src/mock.rs index b30caa9f3..396393c4e 100644 --- a/pallet/account-migration/src/mock.rs +++ b/pallet/account-migration/src/mock.rs @@ -47,11 +47,6 @@ impl darwinia_staking::Stake for Dummy { Ok(()) } } -impl frame_support::traits::UnixTime for Dummy { - fn now() -> core::time::Duration { - core::time::Duration::new(0, 0) - } -} #[sp_version::runtime_version] pub const VERSION: sp_version::RuntimeVersion = sp_version::RuntimeVersion { @@ -171,12 +166,9 @@ impl darwinia_staking::Config for Runtime { type MaxDeposits = (); type MaxUnstakings = (); type MinStakingDuration = (); - type PayoutFraction = (); - type RewardRemainder = (); + type OnSessionEnd = (); type Ring = Dummy; - type RingCurrency = Balances; type RuntimeEvent = RuntimeEvent; - type UnixTime = Dummy; type WeightInfo = (); } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/pallet/staking/Cargo.toml b/pallet/staking/Cargo.toml index dfdf2b2b8..4d321cadf 100644 --- a/pallet/staking/Cargo.toml +++ b/pallet/staking/Cargo.toml @@ -13,7 +13,6 @@ scale-info = { workspace = true } # darwinia darwinia-staking-traits = { workspace = true } -dc-inflation = { workspace = true } dc-types = { workspace = true } # darwinia optional darwinia-deposit = { workspace = true, optional = true } @@ -36,10 +35,12 @@ pretty_env_logger = { version = "0.4" } darwinia-deposit = { workspace = true, features = ["std"] } # substrate +dc-inflation = { workspace = true, features = ["std"] } pallet-assets = { workspace = true, features = ["std"] } pallet-balances = { workspace = true, features = ["std"] } pallet-session = { workspace = true, features = ["std"] } pallet-timestamp = { workspace = true, features = ["std"] } +pallet-treasury = { workspace = true, features = ["std"] } sp-core = { workspace = true, features = ["std"] } sp-io = { workspace = true, features = ["std"] } substrate-test-utils = { workspace = true } @@ -53,7 +54,6 @@ std = [ # darwinia "darwinia-staking-traits/std", - "dc-inflation/std", # darwinia optional "darwinia-deposit?/std", diff --git a/pallet/staking/src/benchmarking.rs b/pallet/staking/src/benchmarking.rs index b4c6bae7d..342e4f6c2 100644 --- a/pallet/staking/src/benchmarking.rs +++ b/pallet/staking/src/benchmarking.rs @@ -29,6 +29,8 @@ use sp_std::prelude::*; mod benchmarks { // darwinia use super::*; + // substrate + use frame_support::traits::Currency; fn deposit_for(who: &T::AccountId, count: u32) -> Vec> where @@ -53,7 +55,7 @@ mod benchmarks { let a = frame_benchmarking::whitelisted_caller(); // Remove `+ 1` after https://github.com/paritytech/substrate/pull/13655. - T::RingCurrency::make_free_balance_be(&a, 1_024 * UNIT + 1); + ::Ring::make_free_balance_be(&a, 1_024 * UNIT + 1); ::Kton::mint(&a, UNIT).unwrap(); let deposits = deposit_for::(&a, x); @@ -70,7 +72,7 @@ mod benchmarks { let a = frame_benchmarking::whitelisted_caller(); // Remove `+ 1` after https://github.com/paritytech/substrate/pull/13655. - T::RingCurrency::make_free_balance_be(&a, 1_024 * UNIT + 1); + ::Ring::make_free_balance_be(&a, 1_024 * UNIT + 1); ::Kton::mint(&a, UNIT).unwrap(); let deposits = deposit_for::(&a, x); @@ -90,7 +92,7 @@ mod benchmarks { let a = frame_benchmarking::whitelisted_caller(); // Remove `+ 1` after https://github.com/paritytech/substrate/pull/13655. - T::RingCurrency::make_free_balance_be(&a, 1_024 * UNIT + 1); + ::Ring::make_free_balance_be(&a, 1_024 * UNIT + 1); ::Kton::mint(&a, UNIT).unwrap(); let deposits = deposit_for::(&a, x); @@ -112,7 +114,7 @@ mod benchmarks { let a = frame_benchmarking::whitelisted_caller(); // Remove `+ 1` after https://github.com/paritytech/substrate/pull/13655. - T::RingCurrency::make_free_balance_be(&a, 1_024 * UNIT + 1); + ::Ring::make_free_balance_be(&a, 1_024 * UNIT + 1); ::Kton::mint(&a, UNIT).unwrap(); let deposits = deposit_for::(&a, ::MaxUnstakings::get()); @@ -154,7 +156,7 @@ mod benchmarks { let a_cloned = a.clone(); // Remove `+ 1` after https://github.com/paritytech/substrate/pull/13655. - T::RingCurrency::make_free_balance_be(&a, UNIT + 1); + ::Ring::make_free_balance_be(&a, UNIT + 1); >::stake( RawOrigin::Signed(a.clone()).into(), @@ -177,7 +179,7 @@ mod benchmarks { let a = frame_benchmarking::whitelisted_caller::(); // Remove `+ 1` after https://github.com/paritytech/substrate/pull/13655. - T::RingCurrency::make_free_balance_be(&a, UNIT + 1); + ::Ring::make_free_balance_be(&a, UNIT + 1); >::stake( RawOrigin::Signed(a.clone()).into(), diff --git a/pallet/staking/src/lib.rs b/pallet/staking/src/lib.rs index ae283987d..85416bdaf 100644 --- a/pallet/staking/src/lib.rs +++ b/pallet/staking/src/lib.rs @@ -50,15 +50,9 @@ pub use darwinia_staking_traits::*; // crates.io use codec::FullCodec; // darwinia -use dc_inflation::TOTAL_SUPPLY; -use dc_types::{Balance, Moment, UNIT}; +use dc_types::{Balance, Moment}; // substrate -use frame_support::{ - log, - pallet_prelude::*, - traits::{Currency, OnUnbalanced, UnixTime}, - EqNoBound, PalletId, PartialEqNoBound, -}; +use frame_support::{log, pallet_prelude::*, EqNoBound, PalletId, PartialEqNoBound}; use frame_system::pallet_prelude::*; #[cfg(feature = "std")] use frame_system::RawOrigin; @@ -88,19 +82,6 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; - /// Unix time getter. - type UnixTime: UnixTime; - - /// RING [`Currency`] interface. - /// - /// Only use for inflation. - type RingCurrency: Currency; - - /// Tokens have been minted and are unused for stakers reward. - /// - /// Usually, it's treasury. - type RewardRemainder: OnUnbalanced>; - /// RING [`Stake`] interface. type Ring: Stake; @@ -110,16 +91,13 @@ pub mod pallet { /// Deposit [`StakeExt`] interface. type Deposit: StakeExt; + /// On session end handler. + type OnSessionEnd: OnSessionEnd; + /// Minimum time to stake at least. #[pallet::constant] type MinStakingDuration: Get; - /// The percentage of the total payout that is distributed to stakers. - /// - /// Usually, the rest goes to the treasury. - #[pallet::constant] - type PayoutFraction: Get; - /// Maximum deposit count. #[pallet::constant] type MaxDeposits: Get; @@ -154,7 +132,12 @@ pub mod pallet { /// A payout has been made for the staker. Payout { staker: T::AccountId, - ring_amount: Balance, + amount: Balance, + }, + /// Unable to pay the staker's reward. + Unpaied { + staker: T::AccountId, + amount: Balance, }, /// A new collator set has been elected. Elected { @@ -793,59 +776,35 @@ pub mod pallet { // TODO: weight /// Pay the session reward to the stakers. - pub fn payout(session_duration: Moment, elapsed_time: Moment) { - let unminted = TOTAL_SUPPLY - T::RingCurrency::total_issuance(); - - log::info!( - "\ - [pallet::staking] making a payout for: \ - `unminted = {unminted}`, \ - `session_duration = {session_duration}`, \ - `elapsed_time = {elapsed_time}`\ - " - ); - - let Some(inflation) = dc_inflation::in_period( - unminted, - session_duration, - elapsed_time, - ) else { - log::error!("[pallet::staking] failed to calculate the inflation"); - - return; - }; - - // TODO: add some tests in the core inflation, - // and get a more precise value/worst case. - if inflation > 1_000_000 * UNIT { - log::error!("[pallet::staking] it's impossible to mint over 1 million RING within a session according to current reward curve"); - - return; - } - - let payout = T::PayoutFraction::get() * inflation; + pub fn payout(amount: Balance) -> Balance { let (total_points, reward_map) = >::get(); // Due to the `payout * percent` there might be some losses. let mut actual_payout = 0; for (c, p) in reward_map { let Some(commission) = >::get(&c) else { - #[cfg(test)] - panic!("[pallet::staking] collator({c:?}) must be found; qed"); - log::error!("[pallet::staking] collator({c:?}) must be found; qed"); + #[cfg(test)] + panic!("[pallet::staking] collator({c:?}) must be found; qed"); + #[cfg(not(test))] + { + log::error!("[pallet::staking] collator({c:?}) must be found; qed"); - continue; - }; - let c_total_payout = Perbill::from_rational(p, total_points) * payout; + continue; + } + }; + let c_total_payout = Perbill::from_rational(p, total_points) * amount; let mut c_payout = commission * c_total_payout; let n_payout = c_total_payout - c_payout; let Some(c_exposure) = >::get(&c) else { - #[cfg(test)] - panic!("[pallet::staking] exposure({c:?}) must be found; qed"); - log::error!("[pallet::staking] exposure({c:?}) must be found; qed"); + #[cfg(test)] + panic!("[pallet::staking] exposure({c:?}) must be found; qed"); + #[cfg(not(test))] + { + log::error!("[pallet::staking] exposure({c:?}) must be found; qed"); - continue; - }; + continue; + } + }; for n_exposure in c_exposure.nominators { let n_payout = @@ -855,26 +814,31 @@ pub mod pallet { // If the collator nominated themselves. c_payout += n_payout; - } else if T::RingCurrency::deposit_into_existing(&n_exposure.who, n_payout) - .is_ok() - { + } else if T::OnSessionEnd::reward(&n_exposure.who, n_payout).is_ok() { actual_payout += n_payout; Self::deposit_event(Event::Payout { staker: n_exposure.who, - ring_amount: n_payout, + amount: n_payout, + }); + } else { + Self::deposit_event(Event::Unpaied { + staker: n_exposure.who, + amount: n_payout, }); } } - if T::RingCurrency::deposit_into_existing(&c, c_payout).is_ok() { + if T::OnSessionEnd::reward(&c, c_payout).is_ok() { actual_payout += c_payout; - Self::deposit_event(Event::Payout { staker: c, ring_amount: c_payout }); + Self::deposit_event(Event::Payout { staker: c, amount: c_payout }); + } else { + Self::deposit_event(Event::Unpaied { staker: c, amount: c_payout }); } } - T::RewardRemainder::on_unbalanced(T::RingCurrency::issue(inflation - actual_payout)); + actual_payout } /// Prepare the session state. @@ -934,9 +898,55 @@ type Power = u32; type Vote = u32; type DepositId = <::Deposit as Stake>::Item; -type NegativeImbalance = <::RingCurrency as Currency< - ::AccountId, ->>::NegativeImbalance; + +/// On session end handler. +/// +/// Currently it is only used to control the inflation. +pub trait OnSessionEnd +where + T: Config, +{ + /// Generic session termination procedures. + fn on_session_end() { + let inflation = Self::inflate(); + let reward = Self::calculate_reward(inflation); + let actual_payout = >::payout(reward); + + Self::clean(inflation.unwrap_or(reward).saturating_sub(actual_payout)); + } + + /// Inflation settings. + fn inflate() -> Option { + None + } + + /// Calculate the reward. + fn calculate_reward(maybe_inflation: Option) -> Balance; + + /// The reward function. + fn reward(who: &T::AccountId, amount: Balance) -> DispatchResult; + + /// Clean the data; currently, only the unissued reward is present. + fn clean(_unissued: Balance) {} +} +impl OnSessionEnd for () +where + T: Config, +{ + fn inflate() -> Option { + None + } + + fn calculate_reward(_maybe_inflation: Option) -> Balance { + 0 + } + + fn reward(_who: &T::AccountId, _amount: Balance) -> DispatchResult { + Ok(()) + } + + fn clean(_unissued: Balance) {} +} /// A convertor from collators id. Since this pallet does not have stash/controller, this is /// just identity. @@ -1044,17 +1054,7 @@ where T: Config, { fn end_session(_: u32) { - let now = T::UnixTime::now().as_millis(); - let session_duration = now - >::get(); - let elapsed_time = >::mutate(|t| { - *t += session_duration; - - *t - }); - - >::put(now); - - Self::payout(session_duration, elapsed_time); + T::OnSessionEnd::on_session_end(); } fn start_session(_: u32) {} diff --git a/pallet/staking/src/mock.rs b/pallet/staking/src/mock.rs index 11acfb7f2..3b1ad4542 100644 --- a/pallet/staking/src/mock.rs +++ b/pallet/staking/src/mock.rs @@ -21,15 +21,16 @@ pub use crate as darwinia_staking; // darwinia use dc_types::{AssetId, Balance, Moment, UNIT}; // substrate -use frame_support::traits::{GenesisBuild, OnInitialize}; +use frame_support::traits::{Currency, GenesisBuild, OnInitialize, OnUnbalanced}; use sp_io::TestExternalities; use sp_runtime::RuntimeAppPublic; type BlockNumber = u64; +type AccountId = u32; impl frame_system::Config for Runtime { type AccountData = pallet_balances::AccountData; - type AccountId = u32; + type AccountId = AccountId; type BaseCallFilter = frame_support::traits::Everything; type BlockHashCount = (); type BlockLength = (); @@ -96,11 +97,11 @@ impl pallet_assets::Config for Runtime { type BenchmarkHelper = BenchmarkHelper; type CallbackHandle = (); type CreateOrigin = frame_support::traits::AsEnsureOriginWithArg< - frame_system::EnsureSignedBy, u32>, + frame_system::EnsureSignedBy, AccountId>, >; type Currency = Balances; type Extra = (); - type ForceOrigin = frame_system::EnsureRoot; + type ForceOrigin = frame_system::EnsureRoot; type Freezer = (); type MetadataDepositBase = (); type MetadataDepositPerByte = (); @@ -110,22 +111,9 @@ impl pallet_assets::Config for Runtime { type WeightInfo = (); } -frame_support::parameter_types! { - pub static Time: core::time::Duration = core::time::Duration::new(0, 0); -} -impl Time { - pub fn run(milli_secs: Moment) { - Time::mutate(|t| *t += core::time::Duration::from_millis(milli_secs as _)); - } -} -impl frame_support::traits::UnixTime for Time { - fn now() -> core::time::Duration { - Time::get() - } -} pub enum KtonMinting {} impl darwinia_deposit::SimpleAsset for KtonMinting { - type AccountId = u32; + type AccountId = AccountId; fn mint(beneficiary: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { Assets::mint(RuntimeOrigin::signed(0), 0.into(), *beneficiary, amount) @@ -150,7 +138,7 @@ impl darwinia_deposit::Config for Runtime { pub enum RingStaking {} impl darwinia_staking::Stake for RingStaking { - type AccountId = u32; + type AccountId = AccountId; type Item = Balance; fn stake(who: &Self::AccountId, item: Self::Item) -> sp_runtime::DispatchResult { @@ -173,7 +161,7 @@ impl darwinia_staking::Stake for RingStaking { } frame_support::parameter_types! { - pub static SessionHandlerCollators: Vec = Vec::new(); + pub static SessionHandlerCollators: Vec = Vec::new(); pub static SessionChangeBlock: BlockNumber = 0; } sp_runtime::impl_opaque_keys! { @@ -183,18 +171,18 @@ sp_runtime::impl_opaque_keys! { } type Period = frame_support::traits::ConstU64<3>; pub struct SessionHandler; -impl pallet_session::SessionHandler for SessionHandler { +impl pallet_session::SessionHandler for SessionHandler { const KEY_TYPE_IDS: &'static [sp_runtime::KeyTypeId] = &[sp_runtime::testing::UintAuthorityId::ID]; - fn on_genesis_session(keys: &[(u32, K)]) + fn on_genesis_session(keys: &[(AccountId, K)]) where K: sp_runtime::traits::OpaqueKeys, { SessionHandlerCollators::set(keys.iter().map(|(a, _)| *a).collect::>()) } - fn on_new_session(_: bool, keys: &[(u32, K)], _: &[(u32, K)]) + fn on_new_session(_: bool, keys: &[(AccountId, K)], _: &[(AccountId, K)]) where K: sp_runtime::traits::OpaqueKeys, { @@ -204,7 +192,7 @@ impl pallet_session::SessionHandler for SessionHandler { fn on_before_session_ending() {} - fn on_disabled(_: u32) {} + fn on_disabled(_: AccountId) {} } impl sp_runtime::BoundToRuntimeAppPublic for SessionHandler { type Public = sp_runtime::testing::UintAuthorityId; @@ -221,9 +209,35 @@ impl pallet_session::Config for Runtime { type WeightInfo = (); } +frame_support::parameter_types! { + pub const TreasuryPalletId: frame_support::PalletId = frame_support::PalletId(*b"da/trsry"); +} +impl pallet_treasury::Config for Runtime { + type ApproveOrigin = frame_system::EnsureRoot; + type Burn = (); + type BurnDestination = (); + type Currency = Balances; + type MaxApprovals = (); + type OnSlash = (); + type PalletId = TreasuryPalletId; + type ProposalBond = (); + type ProposalBondMaximum = (); + type ProposalBondMinimum = (); + type RejectOrigin = frame_system::EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type SpendFunds = (); + type SpendOrigin = frame_support::traits::NeverEnsureOrigin; + type SpendPeriod = frame_support::traits::ConstU64<999>; + type WeightInfo = (); +} + +frame_support::parameter_types! { + pub PayoutFraction: sp_runtime::Perbill = sp_runtime::Perbill::from_percent(40); + pub static OnSessionEnd: u8 = 0; +} pub enum KtonStaking {} impl darwinia_staking::Stake for KtonStaking { - type AccountId = u32; + type AccountId = AccountId; type Item = Balance; fn stake(who: &Self::AccountId, item: Self::Item) -> sp_runtime::DispatchResult { @@ -244,8 +258,86 @@ impl darwinia_staking::Stake for KtonStaking { ) } } -frame_support::parameter_types! { - pub const PayoutFraction: sp_runtime::Perbill = sp_runtime::Perbill::from_percent(40); +pub enum StatedOnSessionEnd {} +impl darwinia_staking::OnSessionEnd for StatedOnSessionEnd { + fn inflate() -> Option { + if ON_SESSION_END.with(|v| *v.borrow()) == 0 { + OnDarwiniaSessionEnd::inflate() + } else { + OnCrabSessionEnd::inflate() + } + } + + fn calculate_reward(maybe_inflation: Option) -> Balance { + if ON_SESSION_END.with(|v| *v.borrow()) == 0 { + OnDarwiniaSessionEnd::calculate_reward(maybe_inflation) + } else { + OnCrabSessionEnd::calculate_reward(maybe_inflation) + } + } + + fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { + if ON_SESSION_END.with(|v| *v.borrow()) == 0 { + OnDarwiniaSessionEnd::reward(who, amount) + } else { + OnCrabSessionEnd::reward(who, amount) + } + } + + fn clean(unissued: Balance) { + if ON_SESSION_END.with(|v| *v.borrow()) == 0 { + OnDarwiniaSessionEnd::clean(unissued) + } else { + OnCrabSessionEnd::clean(unissued) + } + } +} +pub enum OnDarwiniaSessionEnd {} +impl darwinia_staking::OnSessionEnd for OnDarwiniaSessionEnd { + fn inflate() -> Option { + let now = Timestamp::now(); + let session_duration = now - >::get(); + let elapsed_time = >::mutate(|t| { + *t = t.saturating_add(session_duration); + + *t + }); + + >::put(now); + + let unminted = dc_inflation::TOTAL_SUPPLY.saturating_sub(Balances::total_issuance()); + + dc_inflation::in_period(unminted, session_duration, elapsed_time) + } + + fn calculate_reward(maybe_inflation: Option) -> Balance { + maybe_inflation.map(|i| PayoutFraction::get() * i).unwrap_or_default() + } + + fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { + let _ = Balances::deposit_into_existing(who, amount); + + Ok(()) + } + + fn clean(unissued: Balance) { + Treasury::on_unbalanced(Balances::issue(unissued)); + } +} +pub enum OnCrabSessionEnd {} +impl darwinia_staking::OnSessionEnd for OnCrabSessionEnd { + fn calculate_reward(_maybe_inflation: Option) -> Balance { + 10_000 * UNIT + } + + fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { + >::transfer( + &Treasury::account_id(), + who, + amount, + frame_support::traits::ExistenceRequirement::KeepAlive, + ) + } } impl darwinia_staking::Config for Runtime { type Deposit = Deposit; @@ -253,12 +345,9 @@ impl darwinia_staking::Config for Runtime { type MaxDeposits = ::MaxDeposits; type MaxUnstakings = frame_support::traits::ConstU32<16>; type MinStakingDuration = frame_support::traits::ConstU64<3>; - type PayoutFraction = PayoutFraction; - type RewardRemainder = (); + type OnSessionEnd = StatedOnSessionEnd; type Ring = RingStaking; - type RingCurrency = Balances; type RuntimeEvent = RuntimeEvent; - type UnixTime = Time; type WeightInfo = (); } #[cfg(not(feature = "runtime-benchmarks"))] @@ -276,6 +365,7 @@ frame_support::construct_runtime! { Assets: pallet_assets, Deposit: darwinia_deposit, Session: pallet_session, + Treasury: pallet_treasury, Staking: darwinia_staking, } } @@ -314,6 +404,12 @@ pub struct ExtBuilder { genesis_collator: bool, } impl ExtBuilder { + pub fn on_session_end_type(self, r#type: u8) -> Self { + ON_SESSION_END.with(|v| *v.borrow_mut() = r#type); + + self + } + pub fn collator_count(mut self, collator_count: u32) -> Self { self.collator_count = collator_count; @@ -332,7 +428,10 @@ impl ExtBuilder { frame_system::GenesisConfig::default().build_storage::().unwrap(); pallet_balances::GenesisConfig:: { - balances: (1..=10).map(|i| (i, 1_000 * UNIT)).collect(), + balances: (1..=10) + .map(|i| (i, 1_000 * UNIT)) + .chain([(Treasury::account_id(), 1_000_000 * UNIT)]) + .collect(), } .assimilate_storage(&mut storage) .unwrap(); diff --git a/pallet/staking/src/tests.rs b/pallet/staking/src/tests.rs index a63b271a3..e525b6a24 100644 --- a/pallet/staking/src/tests.rs +++ b/pallet/staking/src/tests.rs @@ -556,24 +556,27 @@ fn payout_should_work() { (1..=10).for_each(|i| assert_eq!(Balances::free_balance(i), 1_000 * UNIT)); let session_duration = Duration::new(6 * 60 * 60, 0).as_millis(); - Staking::payout(session_duration, Staking::elapsed_time()); + + Timestamp::set_timestamp(session_duration); + new_session(); + let rewards = [ - 1_366_118_850_452_628_471_390, - 2_550_088_490_535_282_845_143, - 3_551_909_019_701_415_672_898, - 4_371_580_329_754_413_739_136, - 5_009_102_470_967_450_861_167, - 4_098_356_559_554_598_536_559, - 2_914_386_924_389_972_036_239, - 1_912_566_395_223_839_208_483, - 1_092_895_081_892_155_893_291, - 455_372_941_225_566_312_752, + 1365981985468705603914, + 2549833009234996736972, + 3551553170742362179958, + 4371142361805028424107, + 5008600632691132655310, + 4097945964602008744224, + 2914094945753252770656, + 1912374784245887327670, + 1092785589904864310528, + 455327319565152874824, ]; (1..=10) .zip(rewards.iter()) - .for_each(|(i, r)| assert_eq!(Balances::free_balance(i), 1_000 * UNIT + r)); + .for_each(|(i, r)| assert_eq!(Balances::free_balance(i) - 1_000 * UNIT, *r)); assert_eq_error_rate!( - ::PayoutFraction::get() + PayoutFraction::get() * dc_inflation::in_period( dc_inflation::TOTAL_SUPPLY - Balances::total_issuance(), session_duration, @@ -581,8 +584,85 @@ fn payout_should_work() { ) .unwrap(), rewards.iter().sum::(), - // Error rate 0.1 RING. - UNIT / 10 + // Error rate 1 RING. + UNIT + ); + }); + + ExtBuilder::default().on_session_end_type(1).collator_count(5).build().execute_with(|| { + (1..=5).for_each(|i| { + assert_ok!(Staking::stake( + RuntimeOrigin::signed(i), + 0, + i as Balance * UNIT, + Vec::new() + )); + assert_ok!(Staking::collect(RuntimeOrigin::signed(i), Perbill::from_percent(i * 10))); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(i), i)); + }); + (6..=10).for_each(|i| { + assert_ok!(Staking::stake( + RuntimeOrigin::signed(i), + 0, + (11 - i as Balance) * UNIT, + Vec::new() + )); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(i), i - 5)); + }); + new_session(); + new_session(); + Staking::reward_by_ids(&[(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]); + (1..=10).for_each(|i| assert_eq!(Balances::free_balance(i), 1_000 * UNIT)); + + let total_issuance = Balances::total_issuance(); + + new_session(); + + let rewards = [ + 499999998800000000000, + 933333320000000000000, + 1300000000000000000000, + 1599999999200000000000, + 1833333336000000000000, + 1499999999400000000000, + 1066666680000000000000, + 700000000000000000000, + 399999999600000000000, + 166666663000000000000, + ]; + (1..=10) + .zip(rewards.iter()) + .for_each(|(i, r)| assert_eq!(Balances::free_balance(i) - 1_000 * UNIT, *r)); + + assert_eq!(Balances::total_issuance(), total_issuance); + assert_eq!( + Balances::free_balance(&Treasury::account_id()), + 1_000_000 * UNIT - rewards.iter().sum::() + ); + + dbg!(Balances::free_balance(&Treasury::account_id())); + assert_ok!(Balances::transfer_all( + RuntimeOrigin::signed(Treasury::account_id()), + Default::default(), + false + )); + dbg!(Balances::free_balance(&Treasury::account_id())); + Staking::reward_by_ids(&[(1, 20)]); + System::reset_events(); + new_session(); + + assert_eq!( + System::events() + .into_iter() + .filter_map(|e| match e.event { + RuntimeEvent::Staking(e @ Event::Unpaied { .. }) => Some(e), + _ => None, + }) + .collect::>(), + vec![ + Event::Unpaied { staker: 6, amount: 7499999997000000000000 }, + Event::Unpaied { staker: 1, amount: 2499999994000000000000 } + ] ); }); } diff --git a/precompile/staking/src/mock.rs b/precompile/staking/src/mock.rs index 61f5862db..eb1845cdf 100644 --- a/precompile/staking/src/mock.rs +++ b/precompile/staking/src/mock.rs @@ -236,12 +236,9 @@ impl darwinia_staking::Config for TestRuntime { type MaxDeposits = ::MaxDeposits; type MaxUnstakings = frame_support::traits::ConstU32<16>; type MinStakingDuration = frame_support::traits::ConstU64<3>; - type PayoutFraction = PayoutFraction; - type RewardRemainder = (); + type OnSessionEnd = (); type Ring = RingStaking; - type RingCurrency = Balances; type RuntimeEvent = RuntimeEvent; - type UnixTime = Timestamp; type WeightInfo = (); } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/runtime/crab/src/pallets/staking.rs b/runtime/crab/src/pallets/staking.rs index e7161784a..7fafe2ffc 100644 --- a/runtime/crab/src/pallets/staking.rs +++ b/runtime/crab/src/pallets/staking.rs @@ -18,6 +18,8 @@ // darwinia use crate::*; +// substrate +use frame_support::traits::Currency; fast_runtime_or_not!(DURATION, BlockNumber, 5 * MINUTES, 14 * DAYS); @@ -29,7 +31,7 @@ impl darwinia_staking::Stake for RingStaking { type Item = Balance; fn stake(who: &Self::AccountId, item: Self::Item) -> sp_runtime::DispatchResult { - >::transfer( + >::transfer( who, &darwinia_staking::account_id(), item, @@ -38,7 +40,7 @@ impl darwinia_staking::Stake for RingStaking { } fn unstake(who: &Self::AccountId, item: Self::Item) -> sp_runtime::DispatchResult { - >::transfer( + >::transfer( &darwinia_staking::account_id(), who, item, @@ -70,8 +72,20 @@ impl darwinia_staking::Stake for KtonStaking { } } -frame_support::parameter_types! { - pub const PayoutFraction: sp_runtime::Perbill = sp_runtime::Perbill::from_percent(40); +pub enum OnCrabSessionEnd {} +impl darwinia_staking::OnSessionEnd for OnCrabSessionEnd { + fn calculate_reward(_maybe_inflation: Option) -> Balance { + 20_000 * UNIT + } + + fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { + >::transfer( + &Treasury::account_id(), + who, + amount, + frame_support::traits::ExistenceRequirement::KeepAlive, + ) + } } impl darwinia_staking::Config for Runtime { @@ -80,12 +94,9 @@ impl darwinia_staking::Config for Runtime { type MaxDeposits = ::MaxDeposits; type MaxUnstakings = ConstU32<16>; type MinStakingDuration = MinStakingDuration; - type PayoutFraction = PayoutFraction; - type RewardRemainder = Treasury; + type OnSessionEnd = OnCrabSessionEnd; type Ring = RingStaking; - type RingCurrency = Balances; type RuntimeEvent = RuntimeEvent; - type UnixTime = Timestamp; type WeightInfo = weights::darwinia_staking::WeightInfo; } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/runtime/darwinia/Cargo.toml b/runtime/darwinia/Cargo.toml index 5428945d7..b706d573d 100644 --- a/runtime/darwinia/Cargo.toml +++ b/runtime/darwinia/Cargo.toml @@ -44,6 +44,7 @@ darwinia-precompile-deposit = { workspace = true } darwinia-precompile-staking = { workspace = true } darwinia-precompile-state-storage = { workspace = true } darwinia-staking = { workspace = true } +dc-inflation = { workspace = true } dc-primitives = { workspace = true } # darwinia-messages-substrate @@ -169,6 +170,7 @@ std = [ "darwinia-precompile-staking/std", "darwinia-precompile-state-storage/std", "darwinia-staking/std", + "dc-inflation/std", "dc-primitives/std", # darwinia-messages-substrate diff --git a/runtime/darwinia/src/pallets/staking.rs b/runtime/darwinia/src/pallets/staking.rs index 1ea8cc964..9e2398355 100644 --- a/runtime/darwinia/src/pallets/staking.rs +++ b/runtime/darwinia/src/pallets/staking.rs @@ -18,6 +18,8 @@ // darwinia use crate::*; +// substrate +use frame_support::traits::{Currency, OnUnbalanced}; fast_runtime_or_not!(DURATION, BlockNumber, 5 * MINUTES, 14 * DAYS); @@ -29,7 +31,7 @@ impl darwinia_staking::Stake for RingStaking { type Item = Balance; fn stake(who: &Self::AccountId, item: Self::Item) -> sp_runtime::DispatchResult { - >::transfer( + >::transfer( who, &darwinia_staking::account_id(), item, @@ -38,7 +40,7 @@ impl darwinia_staking::Stake for RingStaking { } fn unstake(who: &Self::AccountId, item: Self::Item) -> sp_runtime::DispatchResult { - >::transfer( + >::transfer( &darwinia_staking::account_id(), who, item, @@ -70,8 +72,37 @@ impl darwinia_staking::Stake for KtonStaking { } } -frame_support::parameter_types! { - pub const PayoutFraction: sp_runtime::Perbill = sp_runtime::Perbill::from_percent(40); +pub enum OnDarwiniaSessionEnd {} +impl darwinia_staking::OnSessionEnd for OnDarwiniaSessionEnd { + fn inflate() -> Option { + let now = Timestamp::now() as Moment; + let session_duration = now - >::get(); + let elapsed_time = >::mutate(|t| { + *t = t.saturating_add(session_duration); + + *t + }); + + >::put(now); + + let unminted = dc_inflation::TOTAL_SUPPLY.saturating_sub(Balances::total_issuance()); + + dc_inflation::in_period(unminted, session_duration, elapsed_time) + } + + fn calculate_reward(maybe_inflation: Option) -> Balance { + maybe_inflation.map(|i| sp_runtime::Perbill::from_percent(40) * i).unwrap_or_default() + } + + fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { + let _ = Balances::deposit_into_existing(who, amount)?; + + Ok(()) + } + + fn clean(unissued: Balance) { + Treasury::on_unbalanced(Balances::issue(unissued)); + } } impl darwinia_staking::Config for Runtime { @@ -80,12 +111,9 @@ impl darwinia_staking::Config for Runtime { type MaxDeposits = ::MaxDeposits; type MaxUnstakings = ConstU32<16>; type MinStakingDuration = MinStakingDuration; - type PayoutFraction = PayoutFraction; - type RewardRemainder = Treasury; + type OnSessionEnd = OnDarwiniaSessionEnd; type Ring = RingStaking; - type RingCurrency = Balances; type RuntimeEvent = RuntimeEvent; - type UnixTime = Timestamp; type WeightInfo = weights::darwinia_staking::WeightInfo; } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/runtime/pangolin/src/pallets/staking.rs b/runtime/pangolin/src/pallets/staking.rs index f7b268b83..fe78c2833 100644 --- a/runtime/pangolin/src/pallets/staking.rs +++ b/runtime/pangolin/src/pallets/staking.rs @@ -66,8 +66,20 @@ impl darwinia_staking::Stake for KtonStaking { } } -frame_support::parameter_types! { - pub const PayoutFraction: sp_runtime::Perbill = sp_runtime::Perbill::from_percent(40); +pub enum OnPangolinSessionEnd {} +impl darwinia_staking::OnSessionEnd for OnPangolinSessionEnd { + fn calculate_reward(_maybe_inflation: Option) -> Balance { + 20_000 * UNIT + } + + fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { + >::transfer( + &Treasury::account_id(), + who, + amount, + frame_support::traits::ExistenceRequirement::KeepAlive, + ) + } } impl darwinia_staking::Config for Runtime { @@ -76,12 +88,9 @@ impl darwinia_staking::Config for Runtime { type MaxDeposits = ::MaxDeposits; type MaxUnstakings = ConstU32<16>; type MinStakingDuration = ConstU32<{ 2 * MINUTES }>; - type PayoutFraction = PayoutFraction; - type RewardRemainder = Treasury; + type OnSessionEnd = OnPangolinSessionEnd; type Ring = RingStaking; - type RingCurrency = Balances; type RuntimeEvent = RuntimeEvent; - type UnixTime = Timestamp; type WeightInfo = weights::darwinia_staking::WeightInfo; } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/runtime/pangoro/Cargo.toml b/runtime/pangoro/Cargo.toml index 30707d4d8..3bedf1599 100644 --- a/runtime/pangoro/Cargo.toml +++ b/runtime/pangoro/Cargo.toml @@ -44,6 +44,7 @@ darwinia-precompile-deposit = { workspace = true } darwinia-precompile-staking = { workspace = true } darwinia-precompile-state-storage = { workspace = true } darwinia-staking = { workspace = true } +dc-inflation = { workspace = true } dc-primitives = { workspace = true } # darwinia-messages-substrate @@ -169,6 +170,7 @@ std = [ "darwinia-precompile-staking/std", "darwinia-precompile-state-storage/std", "darwinia-staking/std", + "dc-inflation/std", "dc-primitives/std", # darwinia-messages-substrate diff --git a/runtime/pangoro/src/pallets/staking.rs b/runtime/pangoro/src/pallets/staking.rs index 5b0dd749c..8deddfaa9 100644 --- a/runtime/pangoro/src/pallets/staking.rs +++ b/runtime/pangoro/src/pallets/staking.rs @@ -18,6 +18,8 @@ // darwinia use crate::*; +// substrate +use frame_support::traits::{Currency, OnUnbalanced}; pub enum RingStaking {} impl darwinia_staking::Stake for RingStaking { @@ -25,7 +27,7 @@ impl darwinia_staking::Stake for RingStaking { type Item = Balance; fn stake(who: &Self::AccountId, item: Self::Item) -> sp_runtime::DispatchResult { - >::transfer( + >::transfer( who, &darwinia_staking::account_id(), item, @@ -34,7 +36,7 @@ impl darwinia_staking::Stake for RingStaking { } fn unstake(who: &Self::AccountId, item: Self::Item) -> sp_runtime::DispatchResult { - >::transfer( + >::transfer( &darwinia_staking::account_id(), who, item, @@ -66,8 +68,37 @@ impl darwinia_staking::Stake for KtonStaking { } } -frame_support::parameter_types! { - pub const PayoutFraction: sp_runtime::Perbill = sp_runtime::Perbill::from_percent(40); +pub enum OnPangoroSessionEnd {} +impl darwinia_staking::OnSessionEnd for OnPangoroSessionEnd { + fn inflate() -> Option { + let now = Timestamp::now() as Moment; + let session_duration = now - >::get() as Moment; + let elapsed_time = >::mutate(|t| { + *t = t.saturating_add(session_duration); + + *t + }); + + >::put(now); + + let unminted = dc_inflation::TOTAL_SUPPLY.saturating_sub(Balances::total_issuance()); + + dc_inflation::in_period(unminted, session_duration, elapsed_time) + } + + fn calculate_reward(maybe_inflation: Option) -> Balance { + maybe_inflation.map(|i| sp_runtime::Perbill::from_percent(40) * i).unwrap_or_default() + } + + fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { + let _ = Balances::deposit_into_existing(who, amount)?; + + Ok(()) + } + + fn clean(unissued: Balance) { + Treasury::on_unbalanced(Balances::issue(unissued)); + } } impl darwinia_staking::Config for Runtime { @@ -76,12 +107,9 @@ impl darwinia_staking::Config for Runtime { type MaxDeposits = ::MaxDeposits; type MaxUnstakings = ConstU32<16>; type MinStakingDuration = ConstU32<{ 10 * MINUTES }>; - type PayoutFraction = PayoutFraction; - type RewardRemainder = Treasury; + type OnSessionEnd = OnPangoroSessionEnd; type Ring = RingStaking; - type RingCurrency = Balances; type RuntimeEvent = RuntimeEvent; - type UnixTime = Timestamp; type WeightInfo = weights::darwinia_staking::WeightInfo; } #[cfg(not(feature = "runtime-benchmarks"))]