diff --git a/gsdk/src/metadata/generated.rs b/gsdk/src/metadata/generated.rs index 92b84875eee..5df6de3ebcc 100644 --- a/gsdk/src/metadata/generated.rs +++ b/gsdk/src/metadata/generated.rs @@ -3280,8 +3280,8 @@ pub mod runtime_types { #[doc = "\n\t\t\tThe [event](https://docs.substrate.io/main-docs/build/events-errors/) emitted\n\t\t\tby this pallet.\n\t\t\t"] pub enum Event { #[codec(index = 0)] - #[doc = "Transferred to the pool from an external account."] - Refilled { amount: ::core::primitive::u128 }, + #[doc = "Deposited to the pool."] + Deposited { amount: ::core::primitive::u128 }, #[codec(index = 1)] #[doc = "Transferred from the pool to an external account."] Withdrawn { amount: ::core::primitive::u128 }, diff --git a/pallets/staking-rewards/src/lib.rs b/pallets/staking-rewards/src/lib.rs index 66fd46e05fb..71b4ba92c59 100644 --- a/pallets/staking-rewards/src/lib.rs +++ b/pallets/staking-rewards/src/lib.rs @@ -231,8 +231,8 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// Transferred to the pool from an external account. - Refilled { amount: BalanceOf }, + /// Deposited to the pool. + Deposited { amount: BalanceOf }, /// Transferred from the pool to an external account. Withdrawn { amount: BalanceOf }, /// Burned from the pool. @@ -271,7 +271,7 @@ pub mod pallet { log::error!("Failed to replenish the staking rewards pool: {:?}", e); Error::::FailureToRefillPool })?; - Self::deposit_event(Event::Refilled { amount: value }); + Self::deposit_event(Event::Deposited { amount: value }); Ok(()) } @@ -295,7 +295,7 @@ pub mod pallet { log::error!("Failed to replenish the staking rewards pool: {:?}", e); Error::::FailureToRefillPool })?; - Self::deposit_event(Event::Refilled { amount: value }); + Self::deposit_event(Event::Deposited { amount: value }); Ok(()) } @@ -403,6 +403,8 @@ impl EraPayout> for Pallet { } } +/// Balance out excessive total supply whenever new tokens are minted through +/// burning the equivalent amount from the inflation offset pool impl OnUnbalanced> for Pallet { fn on_nonzero_unbalanced(minted: PositiveImbalanceOf) { let amount = minted.peek(); @@ -420,22 +422,38 @@ impl OnUnbalanced> for Pallet { Self::deposit_event(Event::Burned { amount }); } else { log::warn!( - "Staking rewards pool has insufficient balance to burn minted rewards. The currency total supply may grow." + "Staking rewards pool has insufficient balance to burn minted rewards. \ + The currency total supply may grow." ); }; } } +/// Funnel the funds-to-burn into the inflation offset pool to maintain the total supply +pub struct OffsetPool(sp_std::marker::PhantomData); +impl OnUnbalanced> for OffsetPool { + fn on_nonzero_unbalanced(amount: NegativeImbalanceOf) { + let numeric_amount = amount.peek(); + + // Should resolve into existing but resolving with creation is a safer bet anyway + T::Currency::resolve_creating(&Pallet::::account_id(), amount); + + Pallet::deposit_event(Event::::Deposited { + amount: numeric_amount, + }); + } +} + /// A type to be plugged into the Staking pallet as the `RewardRemainder` associated type. /// -/// Implements the `OnUnbalanced` trait in a way that would try to burn -/// the amount equivalent to that provided in the input `NegativeImbalance` from the rewards -/// pool in order to keep the token total supply intact. It is assumed that the subsequent -/// `OnUnbalanced` handler (e.g. Treasury) would `resolve` the imbalance and not drop it - -/// otherwise the the total supply will decrease. -pub struct RewardsStash(sp_std::marker::PhantomData<(T, U)>); - -impl OnUnbalanced> for RewardsStash +/// A wrapper around the final `RewardRemainder` destination that burns from the inflation offset +/// pool the equivalent of the provided `NegativeImbalance` value in order to balance out what has +/// been minted as a part of the staking rewards for the era but not yet attributed to any account. +/// It is assumed that the subsequent `OnUnbalanced` handler (e.g. Treasury) would `resolve` the +/// imbalance and not drop it - otherwise the the total supply will decrease. +pub struct RewardProxy(sp_std::marker::PhantomData<(T, U)>); + +impl OnUnbalanced> for RewardProxy where U: OnUnbalanced>, { diff --git a/runtime/vara/src/integration_tests.rs b/runtime/vara/src/integration_tests.rs index fbc17eef11f..2a3763a494e 100644 --- a/runtime/vara/src/integration_tests.rs +++ b/runtime/vara/src/integration_tests.rs @@ -30,8 +30,8 @@ use sp_core::{ed25519, sr25519, Pair}; use sp_keyring::AccountKeyring; use sp_runtime::{Digest, DigestItem}; -const ENDOWMENT: u128 = 100 * UNITS; -const STASH: u128 = 10 * UNITS; +const ENDOWMENT: u128 = 100_000 * UNITS; +const STASH: u128 = 1_000 * UNITS; pub(crate) fn initialize_block(new_blk: BlockNumberFor) { // All blocks are to be authored by validator at index 0 @@ -57,6 +57,7 @@ pub(crate) fn on_initialize(new_block_number: BlockNumberFor) { Babe::on_initialize(new_block_number); Balances::on_initialize(new_block_number); Authorship::on_initialize(new_block_number); + Treasury::on_initialize(new_block_number); GearProgram::on_initialize(new_block_number); GearMessenger::on_initialize(new_block_number); Gear::on_initialize(new_block_number); @@ -72,6 +73,7 @@ pub(crate) fn on_finalize(current_blk: BlockNumberFor) { Gear::on_finalize(current_blk); GearMessenger::on_finalize(current_blk); GearProgram::on_finalize(current_blk); + Treasury::on_finalize(current_blk); Authorship::on_finalize(current_blk); Balances::on_finalize(current_blk); Grandpa::on_finalize(current_blk); @@ -176,10 +178,11 @@ impl ExtBuilder { .assimilate_storage(&mut storage) .unwrap(); + #[cfg(feature = "dev")] SudoConfig { key: self.root } .assimilate_storage(&mut storage) .unwrap(); - + GenesisBuild::::assimilate_storage(&TreasuryConfig {}, &mut storage).unwrap(); GenesisBuild::::assimilate_storage( &VestingConfig { vesting: self.vested_accounts, @@ -244,6 +247,23 @@ pub(crate) fn get_last_program_id() -> [u8; 32] { } } +pub(crate) fn get_treasury_events() -> (Balance, Balance, Balance) { + System::events() + .into_iter() + .fold((0, 0, 0), |r, e| match e.event { + RuntimeEvent::Treasury(pallet_treasury::Event::Spending { budget_remaining }) => { + (budget_remaining, r.1, r.2) + } + RuntimeEvent::Treasury(pallet_treasury::Event::Burnt { burnt_funds }) => { + (r.0, burnt_funds, r.2) + } + RuntimeEvent::Treasury(pallet_treasury::Event::Rollover { rollover_balance }) => { + (r.0, r.1, rollover_balance) + } + _ => r, + }) +} + #[test] fn tokens_locking_works() { init_logger(); @@ -289,18 +309,18 @@ fn tokens_locking_works() { .endowment(ENDOWMENT) .endowed_accounts(vec![charlie.into(), dave.into(), eve.into(), ferdie.into()]) .vested_accounts(vec![ - (dave.into(), 10, 100, 10 * UNITS), // 1 TOKEN unlocked per block - (eve.into(), 10, 100, 10 * UNITS), - (ferdie.into(), 10, 100, 10 * UNITS), + (dave.into(), 10, 100, 10_000 * UNITS), // 1 TOKEN unlocked per block + (eve.into(), 10, 100, 10_000 * UNITS), + (ferdie.into(), 10, 100, 10_000 * UNITS), ]) .root(alice.into()) .build() .execute_with(|| { let acc_data = System::account(dave.to_account_id()).data; - // Free balance of vested accounts is still 100 TOKENS - assert_eq!(acc_data.free, 100 * UNITS); + // Free balance of vested accounts is still 100_000 TOKENS + assert_eq!(acc_data.free, 100_000 * UNITS); // Locked balance is 90 TOKENS - assert_eq!(acc_data.misc_frozen, 90 * UNITS); + assert_eq!(acc_data.misc_frozen, 90_000 * UNITS); // Locked funds can't be reserved to pay for gas and/or value // Transaction should be invalidated when attempting to `reserve` currency: @@ -313,7 +333,7 @@ fn tokens_locking_works() { b"salt".to_vec(), vec![], 10_000_000_000, - 10 * UNITS, + 10_000 * UNITS, ), pallet_gear_bank::Error::::InsufficientBalance ); @@ -321,7 +341,7 @@ fn tokens_locking_works() { // TODO: delete lines below (issue #3081). core::mem::drop(Balances::deposit_creating( &alice.to_account_id(), - 10 * UNITS, + 10_000 * UNITS, )); // Locked funds can't be transferred to a program as a message `value` @@ -345,10 +365,250 @@ fn tokens_locking_works() { program_id.into(), vec![], 10_000_000_000, - 11 * UNITS, + 11_000 * UNITS, false, ), pallet_gear_bank::Error::::InsufficientBalance ); }); } + +#[test] +fn treasury_surplus_is_not_burned() { + init_logger(); + + let alice = AccountKeyring::Alice; + let bob = AccountKeyring::Bob; + let charlie = AccountKeyring::Charlie; + let dave = AccountKeyring::Dave; + + let treasury_id = Treasury::account_id(); + + ExtBuilder::default() + .initial_authorities(vec![ + ( + alice.into(), + charlie.into(), + alice.public(), + ed25519::Pair::from_string("//Alice", None) + .unwrap() + .public(), + alice.public(), + alice.public(), + ), + ( + bob.into(), + dave.into(), + bob.public(), + ed25519::Pair::from_string("//Bob", None).unwrap().public(), + bob.public(), + bob.public(), + ), + ]) + .stash(STASH) + .endowment(ENDOWMENT) + .endowed_accounts(vec![charlie.into(), dave.into()]) + .root(alice.into()) + .build() + .execute_with(|| { + // Treasury pot is empty in the beginning + assert_eq!(Treasury::pot(), 0); + + let initial_total_issuance = Balances::total_issuance(); + + // Top up treasury balance + assert_ok!(Balances::transfer( + RuntimeOrigin::signed(charlie.to_account_id()), + sp_runtime::MultiAddress::Id(treasury_id.clone()), + 1_000 * UNITS, + )); + assert_eq!(Treasury::pot(), 1_000 * UNITS); + + System::reset_events(); + + // Run chain for a day so that `Treasury::spend_funds()` is triggered + run_to_block(DAYS); + + // Check that the `Treasury::spend_funds()` has, indeed, taken place + let (budget_remaining, burnt_funds, rollover_balance) = get_treasury_events(); + // Treasury remaining budget value upon entry in `spend_funds()` function + assert_eq!(budget_remaining, 1_000 * UNITS); + // Actually burnt funds + assert_eq!(burnt_funds, 0); + // Remaining balance being rolled over to the next period + assert_eq!(rollover_balance, 1_000 * UNITS); + + // Treasury had a surplus, but none of it was burned + assert_eq!(Treasury::pot(), 1_000 * UNITS); + + // The total issuance persisted + assert_eq!(Balances::total_issuance(), initial_total_issuance); + + // Run chain until another `Treasury::spend_funds()` invocation + run_to_block(2 * DAYS); + + // Treasury still has a surplus, but nothing is burned + assert_eq!(Treasury::pot(), 1_000 * UNITS); + + assert_eq!(Balances::total_issuance(), initial_total_issuance); + }); +} + +#[test] +fn dust_ends_up_in_offset_pool() { + init_logger(); + + let alice = AccountKeyring::Alice; + let bob = AccountKeyring::Bob; + let charlie = AccountKeyring::Charlie; + let dave = AccountKeyring::Dave; + let ferdie = AccountKeyring::Ferdie; + + let offset_pool_id = StakingRewards::account_id(); + + ExtBuilder::default() + .initial_authorities(vec![ + ( + alice.into(), + charlie.into(), + alice.public(), + ed25519::Pair::from_string("//Alice", None) + .unwrap() + .public(), + alice.public(), + alice.public(), + ), + ( + bob.into(), + dave.into(), + bob.public(), + ed25519::Pair::from_string("//Bob", None).unwrap().public(), + bob.public(), + bob.public(), + ), + ]) + .stash(STASH) + .endowment(ENDOWMENT) + .endowed_accounts(vec![charlie.into(), dave.into(), offset_pool_id.clone()]) + .root(alice.into()) + .build() + .execute_with(|| { + let initial_pool_balance = Balances::free_balance(&offset_pool_id); + assert_eq!(initial_pool_balance, ENDOWMENT); + + let initial_total_issuance = Balances::total_issuance(); + + // Sending ED to `ferdie` to create the account in storage + assert_ok!(Balances::transfer( + RuntimeOrigin::signed(charlie.to_account_id()), + sp_runtime::MultiAddress::Id(ferdie.to_account_id()), + EXISTENTIAL_DEPOSIT, + )); + // `ferdie`'s balance is now ED + assert_eq!( + Balances::free_balance(ferdie.to_account_id()), + EXISTENTIAL_DEPOSIT + ); + + // Sending ED / 2 out of `ferdie` creates dust + assert_ok!(Balances::transfer( + RuntimeOrigin::signed(ferdie.to_account_id()), + sp_runtime::MultiAddress::Id(dave.to_account_id()), + EXISTENTIAL_DEPOSIT / 2, + )); + // `ferdie`'s balance is now 0 + assert_eq!(Balances::free_balance(ferdie.to_account_id()), 0); + // Dust has been accumulated in the offset pool account + assert_eq!( + Balances::free_balance(&offset_pool_id), + initial_pool_balance + EXISTENTIAL_DEPOSIT / 2 + ); + // The `total_issuance` has persisted + assert_eq!(Balances::total_issuance(), initial_total_issuance); + }); +} + +#[test] +fn slashed_proposals_back_to_treasury() { + init_logger(); + + let alice = AccountKeyring::Alice; + let bob = AccountKeyring::Bob; + let charlie = AccountKeyring::Charlie; + let dave = AccountKeyring::Dave; + let ferdie = AccountKeyring::Ferdie; + + let treasury_id = Treasury::account_id(); + + ExtBuilder::default() + .initial_authorities(vec![ + ( + alice.into(), + charlie.into(), + alice.public(), + ed25519::Pair::from_string("//Alice", None) + .unwrap() + .public(), + alice.public(), + alice.public(), + ), + ( + bob.into(), + dave.into(), + bob.public(), + ed25519::Pair::from_string("//Bob", None).unwrap().public(), + bob.public(), + bob.public(), + ), + ]) + .stash(STASH) + .endowment(ENDOWMENT) + .endowed_accounts(vec![charlie.into(), dave.into()]) + .root(alice.into()) + .build() + .execute_with(|| { + // Treasury pot is empty in the beginning + assert_eq!(Treasury::pot(), 0); + + let initial_total_issuance = Balances::total_issuance(); + + // Top up treasury balance + assert_ok!(Balances::transfer( + RuntimeOrigin::signed(charlie.to_account_id()), + sp_runtime::MultiAddress::Id(treasury_id.clone()), + 1_000 * UNITS, + )); + assert_eq!(Treasury::pot(), 1_000 * UNITS); + + assert_ok!(Treasury::propose_spend( + RuntimeOrigin::signed(dave.to_account_id()), + 1_000 * UNITS, + sp_runtime::MultiAddress::Id(ferdie.to_account_id()), + )); + let proposal_bond = + ::ProposalBond::get() * UNITS * 1_000; + let dave_acc_data = System::account(dave.to_account_id()).data; + // Proposer's free balance has decreased by the `proposal_bond` + assert_eq!(dave_acc_data.free, ENDOWMENT - proposal_bond); + // The reserved balance is 5% of the proposed amount + assert_eq!(dave_acc_data.reserved, proposal_bond); + + assert_ok!(Treasury::reject_proposal(RuntimeOrigin::root(), 0)); + + // Run chain for a day so that `Treasury::spend_funds()` is triggered + run_to_block(DAYS); + + // The `proposal_bond` has been slashed + let dave_acc_data = System::account(dave.to_account_id()).data; + assert_eq!(dave_acc_data.free, ENDOWMENT - proposal_bond); + // Nothing is reserved now + assert_eq!(dave_acc_data.reserved, 0); + + // Treasury funds haven't been spent, no burning has taken place, + // the slashed deposit has landed in the `Treasury`, as well + assert_eq!(Treasury::pot(), 1_000 * UNITS + proposal_bond); + + // The total issuance has, therefore, persisted + assert_eq!(Balances::total_issuance(), initial_total_issuance); + }); +} diff --git a/runtime/vara/src/lib.rs b/runtime/vara/src/lib.rs index 76635c44a70..bde63b1de42 100644 --- a/runtime/vara/src/lib.rs +++ b/runtime/vara/src/lib.rs @@ -332,11 +332,9 @@ impl pallet_balances::Config for Runtime { type MaxLocks = ConstU32<50>; type MaxReserves = (); type ReserveIdentifier = [u8; 8]; - /// The type for recording an account's balance. type Balance = Balance; - /// The ubiquitous event type. type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); + type DustRemoval = pallet_gear_staking_rewards::OffsetPool; type ExistentialDeposit = ConstU128; type AccountStore = System; type WeightInfo = weights::pallet_balances::SubstrateWeight; @@ -603,7 +601,7 @@ impl pallet_staking::Config for Runtime { type ElectionProvider = ElectionProviderMultiPhase; type GenesisElectionProvider = onchain::OnChainExecution; // Burning the reward remainder for now. - // TODO: set remainder back to `RewardsStash` to stop burning `Treasury` part. + // TODO: set remainder back to `RewardProxy` to stop burning `Treasury` part. type RewardRemainder = (); type RuntimeEvent = RuntimeEvent; type Slash = Treasury; @@ -683,7 +681,7 @@ parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); pub const ProposalBondMinimum: Balance = ECONOMIC_UNITS; pub const SpendPeriod: BlockNumber = DAYS; - pub const Burn: Permill = Permill::from_percent(50); + pub const Burn: Permill = Permill::zero(); pub const TipCountdown: BlockNumber = DAYS; pub const TipFindersFee: Percent = Percent::from_percent(20); pub const TipReportDepositBase: Balance = ECONOMIC_UNITS; @@ -699,7 +697,7 @@ impl pallet_treasury::Config for Runtime { type ApproveOrigin = EitherOfDiverse, Treasurer>; type RejectOrigin = EitherOfDiverse, Treasurer>; type RuntimeEvent = RuntimeEvent; - type OnSlash = (); + type OnSlash = Treasury; type ProposalBond = ProposalBond; type ProposalBondMinimum = ProposalBondMinimum; type ProposalBondMaximum = ();