From 6dbbb9056cc21802ea5049a0c5035a4a9f4dbd06 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Mon, 31 Jul 2023 10:44:04 +0200 Subject: [PATCH 01/14] dApp Staking v3 Part1 --- pallets/dapp-staking-v3/Cargo.toml | 42 ++ pallets/dapp-staking-v3/src/lib.rs | 460 ++++++++++++++++++ pallets/dapp-staking-v3/src/test/mock.rs | 176 +++++++ pallets/dapp-staking-v3/src/test/mod.rs | 22 + .../dapp-staking-v3/src/test/testing_utils.rs | 213 ++++++++ pallets/dapp-staking-v3/src/test/tests.rs | 364 ++++++++++++++ .../dapp-staking-v3/src/test/tests_types.rs | 105 ++++ pallets/dapp-staking-v3/src/types.rs | 299 ++++++++++++ 8 files changed, 1681 insertions(+) create mode 100644 pallets/dapp-staking-v3/Cargo.toml create mode 100644 pallets/dapp-staking-v3/src/lib.rs create mode 100644 pallets/dapp-staking-v3/src/test/mock.rs create mode 100644 pallets/dapp-staking-v3/src/test/mod.rs create mode 100644 pallets/dapp-staking-v3/src/test/testing_utils.rs create mode 100644 pallets/dapp-staking-v3/src/test/tests.rs create mode 100644 pallets/dapp-staking-v3/src/test/tests_types.rs create mode 100644 pallets/dapp-staking-v3/src/types.rs diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml new file mode 100644 index 0000000000..27f927fe6e --- /dev/null +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "pallet-dapp-staking-v3" +version = "0.0.1-alpha" +description = "Pallet for dApp staking v3 protocol" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +frame-support = { workspace = true } +frame-system = { workspace = true } +num-traits = { workspace = true } +parity-scale-codec = { workspace = true } + +scale-info = { workspace = true } +serde = { workspace = true, optional = true } +sp-arithmetic = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +[dev-dependencies] +pallet-balances = { workspace = true } + +[features] +default = ["std"] +std = [ + "serde", + "parity-scale-codec/std", + "scale-info/std", + "num-traits/std", + "sp-core/std", + "sp-runtime/std", + "sp-arithmetic/std", + "sp-io/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", +] diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs new file mode 100644 index 0000000000..d2057651ed --- /dev/null +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -0,0 +1,460 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! # dApp Staking v3 Pallet +//! +//! - [`Config`] +//! +//! ## Overview +//! +//! Pallet that implements dapps staking protocol. +//! +//! <> +//! +//! ## Interface +//! +//! ### Dispatchable Function +//! +//! <> +//! +//! ### Other +//! +//! <> +//! + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + pallet_prelude::*, + traits::{Currency, LockIdentifier, LockableCurrency, StorageVersion, WithdrawReasons}, + weights::Weight, +}; +use frame_system::pallet_prelude::*; +use sp_runtime::traits::{BadOrigin, Saturating, Zero}; + +use crate::types::*; +pub use pallet::*; + +#[cfg(test)] +mod test; + +mod types; + +const STAKING_ID: LockIdentifier = *b"dapstake"; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); + + #[pallet::pallet] + #[pallet::generate_store(pub(crate) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Currency used for staking. + type Currency: LockableCurrency; + + /// Describes smart contract in the context required by dApp staking. + type SmartContract: Parameter + Member + MaxEncodedLen; + + /// Privileged origin for managing dApp staking pallet. + type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; + + /// Maximum number of contracts that can be integrated into dApp staking at once. + /// TODO: maybe this can be reworded or improved later on - but we want a ceiling! + #[pallet::constant] + type MaxNumberOfContracts: Get; + + /// Maximum number of locked chunks that can exist per account at a time. + #[pallet::constant] + type MaxLockedChunks: Get; + + /// Maximum number of unlocking chunks that can exist per account at a time. + #[pallet::constant] + type MaxUnlockingChunks: Get; + + /// Minimum amount an account has to lock in dApp staking in order to participate. + #[pallet::constant] + type MinimumLockedAmount: Get>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// A smart contract has been registered for dApp staking + DAppRegistered { + owner: T::AccountId, + smart_contract: T::SmartContract, + dapp_id: DAppId, + }, + /// dApp reward destination has been updated. + DAppRewardDestinationUpdated { + smart_contract: T::SmartContract, + beneficiary: Option, + }, + /// dApp owner has been changed. + DAppOwnerChanged { + smart_contract: T::SmartContract, + new_owner: T::AccountId, + }, + /// dApp has been unregistered + DAppUnregistered { + smart_contract: T::SmartContract, + era: EraNumber, + }, + /// Account has locked some amount into dApp staking. + Locked { + account: T::AccountId, + amount: BalanceOf, + }, + } + + #[pallet::error] + pub enum Error { + /// Pallet is disabled/in maintenance mode. + Disabled, + /// Smart contract already exists within dApp staking protocol. + ContractAlreadyExists, + /// Maximum number of smart contracts has been reached. + ExceededMaxNumberOfContracts, + /// Not possible to assign a new dApp Id. + /// This should never happen since current type can support up to 65536 - 1 unique dApps. + NewDAppIdUnavailable, + /// Specified smart contract does not exist in dApp staking. + ContractNotFound, + /// Call origin is not dApp owner. + OriginNotOwner, + /// dApp is part of dApp staking but isn't active anymore. + NotOperatedDApp, + /// Performing locking or staking with 0 amount. + ZeroAmount, + /// Total locked amount for staker is below minimum threshold. + LockedAmountBelowThreshold, + /// Cannot add additional locked balance chunks due to size limit. + TooManyLockedBalanceChunks, + } + + /// General information about dApp staking protocol state. + #[pallet::storage] + pub type ActiveProtocolState = + StorageValue<_, ProtocolState>, ValueQuery>; + + /// Counter for unique dApp identifiers. + #[pallet::storage] + pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; + + /// Map of all dApps integrated into dApp staking protocol. + #[pallet::storage] + pub type IntegratedDApps = CountedStorageMap< + _, + Blake2_128Concat, + T::SmartContract, + DAppInfo, + OptionQuery, + >; + + /// General locked/staked information for each account. + #[pallet::storage] + pub type Ledger = + StorageMap<_, Blake2_128Concat, T::AccountId, AccountLedgerFor, ValueQuery>; + + /// General information about the current era. + #[pallet::storage] + pub type CurrentEraInfo = StorageValue<_, EraInfo>, ValueQuery>; + + #[pallet::call] + impl Pallet { + /// Used to enable or disable maintenance mode. + /// Can only be called by manager origin. + #[pallet::call_index(0)] + #[pallet::weight(Weight::zero())] + pub fn maintenance_mode(origin: OriginFor, enabled: bool) -> DispatchResult { + T::ManagerOrigin::ensure_origin(origin)?; + ActiveProtocolState::::mutate(|state| state.maintenance = enabled); + Ok(()) + } + + /// Used to register a new contract for dApp staking. + /// + /// If successful, smart contract will be assigned a simple, unique numerical identifier. + #[pallet::call_index(1)] + #[pallet::weight(Weight::zero())] + pub fn register( + origin: OriginFor, + owner: T::AccountId, + smart_contract: T::SmartContract, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + T::ManagerOrigin::ensure_origin(origin)?; + + ensure!( + !IntegratedDApps::::contains_key(&smart_contract), + Error::::ContractAlreadyExists, + ); + + ensure!( + IntegratedDApps::::count() < T::MaxNumberOfContracts::get().into(), + Error::::ExceededMaxNumberOfContracts + ); + + let dapp_id = NextDAppId::::get(); + // MAX value must never be assigned as a dApp Id since it serves as a sentinel value. + ensure!(dapp_id < DAppId::MAX, Error::::NewDAppIdUnavailable); + + IntegratedDApps::::insert( + &smart_contract, + DAppInfo { + owner: owner.clone(), + id: dapp_id, + state: DAppState::Registered, + reward_destination: None, + }, + ); + + NextDAppId::::put(dapp_id.saturating_add(1)); + + Self::deposit_event(Event::::DAppRegistered { + owner, + smart_contract, + dapp_id, + }); + + Ok(()) + } + + /// Used to modify the reward destination account for a dApp. + /// + /// Caller has to be dApp owner. + /// If set to `None`, rewards will be deposited to the dApp owner. + #[pallet::call_index(2)] + #[pallet::weight(Weight::zero())] + pub fn set_dapp_reward_destination( + origin: OriginFor, + smart_contract: T::SmartContract, + beneficiary: Option, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let dev_account = ensure_signed(origin)?; + + IntegratedDApps::::try_mutate( + &smart_contract, + |maybe_dapp_info| -> DispatchResult { + let dapp_info = maybe_dapp_info + .as_mut() + .ok_or(Error::::ContractNotFound)?; + + ensure!(dapp_info.owner == dev_account, Error::::OriginNotOwner); + + dapp_info.reward_destination = beneficiary.clone(); + + Ok(()) + }, + )?; + + Self::deposit_event(Event::::DAppRewardDestinationUpdated { + smart_contract, + beneficiary, + }); + + Ok(()) + } + + /// Used to change dApp owner. + /// + /// Can be called by dApp owner or dApp staking manager origin. + /// This is useful in two cases: + /// 1. when the dApp owner account is compromised, manager can change the owner to a new account + /// 2. if project wants to transfer ownership to a new account (DAO, multisig, etc.). + #[pallet::call_index(3)] + #[pallet::weight(Weight::zero())] + pub fn set_dapp_owner( + origin: OriginFor, + smart_contract: T::SmartContract, + new_owner: T::AccountId, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let origin = Self::ensure_signed_or_manager(origin)?; + + IntegratedDApps::::try_mutate( + &smart_contract, + |maybe_dapp_info| -> DispatchResult { + let dapp_info = maybe_dapp_info + .as_mut() + .ok_or(Error::::ContractNotFound)?; + + // If manager origin, `None`, no need to check if caller is the owner. + if let Some(caller) = origin { + ensure!(dapp_info.owner == caller, Error::::OriginNotOwner); + } + + dapp_info.owner = new_owner.clone(); + + Ok(()) + }, + )?; + + Self::deposit_event(Event::::DAppOwnerChanged { + smart_contract, + new_owner, + }); + + Ok(()) + } + + /// Unregister dApp from dApp staking protocol, making it ineligible for future rewards. + /// This doesn't remove the dApp completely from the system just yet, but it can no longer be used for staking. + /// + /// Can be called by dApp owner or dApp staking manager origin. + #[pallet::call_index(4)] + #[pallet::weight(Weight::zero())] + pub fn unregister( + origin: OriginFor, + smart_contract: T::SmartContract, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + T::ManagerOrigin::ensure_origin(origin)?; + + let current_era = ActiveProtocolState::::get().era; + + IntegratedDApps::::try_mutate( + &smart_contract, + |maybe_dapp_info| -> DispatchResult { + let dapp_info = maybe_dapp_info + .as_mut() + .ok_or(Error::::ContractNotFound)?; + + ensure!( + dapp_info.state == DAppState::Registered, + Error::::NotOperatedDApp + ); + + dapp_info.state = DAppState::Unregistered(current_era); + + Ok(()) + }, + )?; + + // TODO: might require some modification later on, like additional checks to ensure contract can be unregistered. + + Self::deposit_event(Event::::DAppUnregistered { + smart_contract, + era: current_era, + }); + + Ok(()) + } + + /// Locks additional funds into dApp staking. + /// + /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked. + /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount. + /// + /// It is possible for call to fail due to caller account already having too many locked balance chunks in storage. To solve this, + /// caller should claim pending rewards, before retrying to lock additional funds. + #[pallet::call_index(5)] + #[pallet::weight(Weight::zero())] + pub fn lock( + origin: OriginFor, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let state = ActiveProtocolState::::get(); + let mut ledger = Ledger::::get(&account); + + // Calculate & check amount available for locking + let available_balance = + T::Currency::free_balance(&account).saturating_sub(ledger.locked_amount()); + let amount_to_lock = available_balance.min(amount); + ensure!(!amount_to_lock.is_zero(), Error::::ZeroAmount); + + // Only lock for the next era onwards. + let lock_era = state.era.saturating_add(1); + ledger + .add_lock_amount(amount_to_lock, lock_era) + .map_err(|_| Error::::TooManyLockedBalanceChunks)?; + ensure!( + ledger.locked_amount() >= T::MinimumLockedAmount::get(), + Error::::LockedAmountBelowThreshold + ); + + Self::update_ledger(&account, ledger); + CurrentEraInfo::::mutate(|era_info| { + era_info.total_locked.saturating_accrue(amount_to_lock); + }); + + Self::deposit_event(Event::::Locked { + account, + amount: amount_to_lock, + }); + + Ok(()) + } + } + + impl Pallet { + /// `Err` if pallet disabled for maintenance, `Ok` otherwise. + pub(crate) fn ensure_pallet_enabled() -> Result<(), Error> { + if ActiveProtocolState::::get().maintenance { + Err(Error::::Disabled) + } else { + Ok(()) + } + } + + /// Ensure that the origin is either the `ManagerOrigin` or a signed origin. + /// + /// In case of manager, `Ok(None)` is returned, and if signed origin `Ok(Some(AccountId))` is returned. + pub(crate) fn ensure_signed_or_manager( + origin: T::RuntimeOrigin, + ) -> Result, BadOrigin> { + if T::ManagerOrigin::ensure_origin(origin.clone()).is_ok() { + return Ok(None); + } + let who = ensure_signed(origin)?; + Ok(Some(who)) + } + + /// Update the account ledger, and dApp staking balance lock. + /// + /// In case account ledger is empty, entries from the DB are removed and lock is released. + pub(crate) fn update_ledger(account: &T::AccountId, ledger: AccountLedgerFor) { + if ledger.is_empty() { + Ledger::::remove(&account); + T::Currency::remove_lock(STAKING_ID, account); + } else { + T::Currency::set_lock( + STAKING_ID, + account, + ledger.locked_amount(), + WithdrawReasons::all(), + ); + Ledger::::insert(account, ledger); + } + } + } +} diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs new file mode 100644 index 0000000000..d4f11189f9 --- /dev/null +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -0,0 +1,176 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::{self as pallet_dapp_staking, *}; + +use frame_support::{ + construct_runtime, parameter_types, + traits::{ConstU128, ConstU16, ConstU32}, + weights::Weight, +}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; + +use sp_io::TestExternalities; + +pub(crate) type AccountId = u64; +pub(crate) type BlockNumber = u64; +pub(crate) type Balance = u128; + +pub(crate) const EXISTENTIAL_DEPOSIT: Balance = 2; +pub(crate) const MINIMUM_LOCK_AMOUNT: Balance = 10; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub struct Test + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + DappStaking: pallet_dapp_staking, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_ref_time(1024)); +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type MaxLocks = ConstU32<4>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128; + type AccountStore = System; + type WeightInfo = (); +} + +impl pallet_dapp_staking::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type SmartContract = MockSmartContract; + type ManagerOrigin = frame_system::EnsureRoot; + type MaxNumberOfContracts = ConstU16<10>; + type MaxLockedChunks = ConstU32<5>; + type MaxUnlockingChunks = ConstU32<5>; + type MinimumLockedAmount = ConstU128; +} + +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] +pub enum MockSmartContract { + Wasm(AccountId), + Other(AccountId), +} + +impl Default for MockSmartContract { + fn default() -> Self { + MockSmartContract::Wasm(1) + } +} + +pub struct ExtBuilder; +impl ExtBuilder { + pub fn build() -> TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let balances = vec![1000; 9] + .into_iter() + .enumerate() + .map(|(idx, amount)| (idx as u64 + 1, amount)) + .collect(); + + pallet_balances::GenesisConfig:: { balances: balances } + .assimilate_storage(&mut storage) + .ok(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| { + System::set_block_number(1); + DappStaking::on_initialize(System::block_number()); + }); + + ext + } +} + +/// Run to the specified block number. +/// Function assumes first block has been initialized. +pub(crate) fn _run_to_block(n: u64) { + while System::block_number() < n { + DappStaking::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + // This is performed outside of dapps staking but we expect it before on_initialize + DappStaking::on_initialize(System::block_number()); + } +} + +/// Run for the specified number of blocks. +/// Function assumes first block has been initialized. +pub(crate) fn _run_for_blocks(n: u64) { + _run_to_block(System::block_number() + n); +} + +/// Advance blocks until the specified era has been reached. +/// +/// Function has no effect if era is already passed. +pub(crate) fn advance_to_era(era: EraNumber) { + // TODO: Properly implement this later when additional logic has been implemented + ActiveProtocolState::::mutate(|state| state.era = era); +} diff --git a/pallets/dapp-staking-v3/src/test/mod.rs b/pallets/dapp-staking-v3/src/test/mod.rs new file mode 100644 index 0000000000..94a090243c --- /dev/null +++ b/pallets/dapp-staking-v3/src/test/mod.rs @@ -0,0 +1,22 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +mod mock; +mod testing_utils; +mod tests; +mod tests_types; diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs new file mode 100644 index 0000000000..f383e9afee --- /dev/null +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -0,0 +1,213 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::test::mock::*; +use crate::*; + +use frame_support::assert_ok; +use std::collections::HashMap; + +/// Helper struct used to store the entire pallet state snapshot. +/// Used when comparison of before/after states is required. +pub(crate) struct MemorySnapshot { + active_protocol_state: ProtocolState>, + next_dapp_id: DAppId, + current_era_info: EraInfo>, + integrated_dapps: HashMap< + ::SmartContract, + DAppInfo<::AccountId>, + >, + ledger: HashMap<::AccountId, AccountLedgerFor>, +} + +impl MemorySnapshot { + /// Generate a new memory snapshot, capturing entire dApp staking pallet state. + pub fn new() -> Self { + Self { + active_protocol_state: ActiveProtocolState::::get(), + next_dapp_id: NextDAppId::::get(), + current_era_info: CurrentEraInfo::::get(), + integrated_dapps: IntegratedDApps::::iter().collect(), + ledger: Ledger::::iter().collect(), + } + } + + /// Returns locked balance in dApp staking for the specified account. + /// In case no balance is locked, returns zero. + pub fn locked_balance(&self, account: &AccountId) -> Balance { + self.ledger + .get(&account) + .map_or(Balance::zero(), |ledger| ledger.locked_amount()) + } +} + +/// Register contract for staking and assert success. +pub(crate) fn assert_register(owner: AccountId, smart_contract: &MockSmartContract) { + // Init check to ensure smart contract hasn't already been integrated + assert!(!IntegratedDApps::::contains_key(smart_contract)); + let pre_snapshot = MemorySnapshot::new(); + + // Register smart contract + assert_ok!(DappStaking::register( + RuntimeOrigin::root(), + owner, + smart_contract.clone() + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::DAppRegistered { + owner, + smart_contract: smart_contract.clone(), + dapp_id: pre_snapshot.next_dapp_id, + })); + + // Verify post-state + let dapp_info = IntegratedDApps::::get(smart_contract).unwrap(); + assert_eq!(dapp_info.state, DAppState::Registered); + assert_eq!(dapp_info.owner, owner); + assert_eq!(dapp_info.id, pre_snapshot.next_dapp_id); + assert!(dapp_info.reward_destination.is_none()); + + assert_eq!(pre_snapshot.next_dapp_id + 1, NextDAppId::::get()); + assert_eq!( + pre_snapshot.integrated_dapps.len() + 1, + IntegratedDApps::::count() as usize + ); +} + +/// Update dApp reward destination and assert success +pub(crate) fn assert_set_dapp_reward_destination( + owner: AccountId, + smart_contract: &MockSmartContract, + beneficiary: Option, +) { + // Change reward destination + assert_ok!(DappStaking::set_dapp_reward_destination( + RuntimeOrigin::signed(owner), + smart_contract.clone(), + beneficiary, + )); + System::assert_last_event(RuntimeEvent::DappStaking( + Event::DAppRewardDestinationUpdated { + smart_contract: smart_contract.clone(), + beneficiary: beneficiary, + }, + )); + + // Sanity check & reward destination update + assert_eq!( + IntegratedDApps::::get(&smart_contract) + .unwrap() + .reward_destination, + beneficiary + ); +} + +/// Update dApp owner and assert success. +/// if `caller` is `None`, `Root` origin is used, otherwise standard `Signed` origin is used. +pub(crate) fn assert_set_dapp_owner( + caller: Option, + smart_contract: &MockSmartContract, + new_owner: AccountId, +) { + let origin = caller.map_or(RuntimeOrigin::root(), |owner| RuntimeOrigin::signed(owner)); + + // Change dApp owner + assert_ok!(DappStaking::set_dapp_owner( + origin, + smart_contract.clone(), + new_owner, + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::DAppOwnerChanged { + smart_contract: smart_contract.clone(), + new_owner, + })); + + // Verify post-state + assert_eq!( + IntegratedDApps::::get(&smart_contract).unwrap().owner, + new_owner + ); +} + +/// Update dApp status to unregistered and assert success. +pub(crate) fn assert_unregister(smart_contract: &MockSmartContract) { + let pre_snapshot = MemorySnapshot::new(); + + // Unregister dApp + assert_ok!(DappStaking::unregister( + RuntimeOrigin::root(), + smart_contract.clone(), + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::DAppUnregistered { + smart_contract: smart_contract.clone(), + era: pre_snapshot.active_protocol_state.era, + })); + + // Verify post-state + assert_eq!( + IntegratedDApps::::get(&smart_contract).unwrap().state, + DAppState::Unregistered(pre_snapshot.active_protocol_state.era), + ); +} + +/// Lock funds into dApp staking and assert success. +pub(crate) fn assert_lock(account: AccountId, amount: Balance) { + let pre_snapshot = MemorySnapshot::new(); + + let free_balance = Balances::free_balance(&account); + let locked_balance = pre_snapshot.locked_balance(&account); + let available_balance = free_balance + .checked_sub(locked_balance) + .expect("Locked amount cannot be greater than available free balance"); + let expected_lock_amount = available_balance.min(amount); + assert!(!expected_lock_amount.is_zero()); + + // Lock funds + assert_ok!(DappStaking::lock(RuntimeOrigin::signed(account), amount,)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Locked { + account, + amount: expected_lock_amount, + })); + + // Verify post-state + let post_snapshot = MemorySnapshot::new(); + + assert_eq!( + post_snapshot.locked_balance(&account), + locked_balance + expected_lock_amount, + "Locked balance should be increased by the amount locked." + ); + assert_eq!( + post_snapshot + .ledger + .get(&account) + .expect("Ledger entry has to exist after succcessful lock call") + .era(), + post_snapshot.active_protocol_state.era + 1 + ); + + assert_eq!( + post_snapshot.current_era_info.total_locked, + pre_snapshot.current_era_info.total_locked + expected_lock_amount, + "Total locked balance should be increased by the amount locked." + ); + assert_eq!( + post_snapshot.current_era_info.active_era_locked, + pre_snapshot.current_era_info.active_era_locked, + "Active era locked amount should remain exactly the same." + ); +} diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs new file mode 100644 index 0000000000..381a385139 --- /dev/null +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -0,0 +1,364 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::test::mock::*; +use crate::test::testing_utils::*; +use crate::{ + pallet as pallet_dapp_staking, ActiveProtocolState, DAppId, Error, IntegratedDApps, NextDAppId, +}; + +use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; +use sp_runtime::traits::Zero; + +#[test] +fn maintenace_mode_works() { + ExtBuilder::build().execute_with(|| { + // Check that maintenance mode is disabled by default + assert!(!ActiveProtocolState::::get().maintenance); + + // Enable maintenance mode & check post-state + assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), true)); + assert!(ActiveProtocolState::::get().maintenance); + + // Call still works, even in maintenance mode + assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), true)); + assert!(ActiveProtocolState::::get().maintenance); + + // Incorrect origin doesn't work + assert_noop!( + DappStaking::maintenance_mode(RuntimeOrigin::signed(1), false), + BadOrigin + ); + }) +} + +#[test] +fn maintenace_mode_call_filtering_works() { + ExtBuilder::build().execute_with(|| { + // Enable maintenance mode & check post-state + assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), true)); + assert!(ActiveProtocolState::::get().maintenance); + + assert_noop!( + DappStaking::register(RuntimeOrigin::root(), 1, MockSmartContract::Wasm(1)), + Error::::Disabled + ); + assert_noop!( + DappStaking::set_dapp_reward_destination( + RuntimeOrigin::signed(1), + MockSmartContract::Wasm(1), + Some(2) + ), + Error::::Disabled + ); + assert_noop!( + DappStaking::set_dapp_owner(RuntimeOrigin::signed(1), MockSmartContract::Wasm(1), 2), + Error::::Disabled + ); + assert_noop!( + DappStaking::unregister(RuntimeOrigin::root(), MockSmartContract::Wasm(1)), + Error::::Disabled + ); + assert_noop!( + DappStaking::lock(RuntimeOrigin::signed(1), 100), + Error::::Disabled + ); + }) +} + +#[test] +fn register_is_ok() { + ExtBuilder::build().execute_with(|| { + // Basic test + assert_register(5, &MockSmartContract::Wasm(1)); + + // Register two contracts using the same owner + assert_register(7, &MockSmartContract::Wasm(2)); + assert_register(7, &MockSmartContract::Wasm(3)); + }) +} + +#[test] +fn register_with_incorrect_origin_fails() { + ExtBuilder::build().execute_with(|| { + assert_noop!( + DappStaking::register(RuntimeOrigin::signed(1), 3, MockSmartContract::Wasm(2)), + BadOrigin + ); + }) +} + +#[test] +fn register_already_registered_contract_fails() { + ExtBuilder::build().execute_with(|| { + let smart_contract = MockSmartContract::Wasm(1); + assert_register(2, &smart_contract); + assert_noop!( + DappStaking::register(RuntimeOrigin::root(), 2, smart_contract), + Error::::ContractAlreadyExists + ); + }) +} + +#[test] +fn register_past_max_number_of_contracts_fails() { + ExtBuilder::build().execute_with(|| { + let limit = ::MaxNumberOfContracts::get(); + for id in 1..=limit { + assert_register(1, &MockSmartContract::Wasm(id.into())); + } + + assert_noop!( + DappStaking::register( + RuntimeOrigin::root(), + 2, + MockSmartContract::Wasm((limit + 1).into()) + ), + Error::::ExceededMaxNumberOfContracts + ); + }) +} + +#[test] +fn register_past_sentinel_value_of_id_fails() { + ExtBuilder::build().execute_with(|| { + // hacky approach, but good enough for test + NextDAppId::::put(DAppId::MAX - 1); + + // First register should pass since sentinel value hasn't been reached yet + assert_register(1, &MockSmartContract::Wasm(3)); + + // Second one should fail since we've reached the sentine value and cannot add more contracts + assert_eq!(NextDAppId::::get(), DAppId::MAX); + assert_noop!( + DappStaking::register(RuntimeOrigin::root(), 1, MockSmartContract::Wasm(5)), + Error::::NewDAppIdUnavailable + ); + }) +} + +#[test] +fn set_dapp_reward_destination_for_contract_is_ok() { + ExtBuilder::build().execute_with(|| { + // Prepare & register smart contract + let owner = 1; + let smart_contract = MockSmartContract::Wasm(3); + assert_register(owner, &smart_contract); + + // Update beneficiary + assert!(IntegratedDApps::::get(&smart_contract) + .unwrap() + .reward_destination + .is_none()); + assert_set_dapp_reward_destination(owner, &smart_contract, Some(3)); + assert_set_dapp_reward_destination(owner, &smart_contract, Some(5)); + assert_set_dapp_reward_destination(owner, &smart_contract, None); + }) +} + +#[test] +fn set_dapp_reward_destination_fails() { + ExtBuilder::build().execute_with(|| { + let owner = 1; + let smart_contract = MockSmartContract::Wasm(3); + + // Contract doesn't exist yet + assert_noop!( + DappStaking::set_dapp_reward_destination( + RuntimeOrigin::signed(owner), + smart_contract, + Some(5) + ), + Error::::ContractNotFound + ); + + // Non-owner cannnot change reward destination + assert_register(owner, &smart_contract); + assert_noop!( + DappStaking::set_dapp_reward_destination( + RuntimeOrigin::signed(owner + 1), + smart_contract, + Some(5) + ), + Error::::OriginNotOwner + ); + }) +} + +#[test] +fn set_dapp_owner_is_ok() { + ExtBuilder::build().execute_with(|| { + // Prepare & register smart contract + let owner = 1; + let smart_contract = MockSmartContract::Wasm(3); + assert_register(owner, &smart_contract); + + // Update owner + let new_owner = 7; + assert_set_dapp_owner(Some(owner), &smart_contract, new_owner); + assert_set_dapp_owner(Some(new_owner), &smart_contract, 1337); + + // Ensure manager can bypass owner + assert_set_dapp_owner(None, &smart_contract, owner); + }) +} + +#[test] +fn set_dapp_owner_fails() { + ExtBuilder::build().execute_with(|| { + let owner = 1; + let smart_contract = MockSmartContract::Wasm(3); + + // Contract doesn't exist yet + assert_noop!( + DappStaking::set_dapp_owner(RuntimeOrigin::signed(owner), smart_contract, 5), + Error::::ContractNotFound + ); + + // Ensure non-owner cannot steal ownership + assert_register(owner, &smart_contract); + assert_noop!( + DappStaking::set_dapp_owner( + RuntimeOrigin::signed(owner + 1), + smart_contract, + owner + 1 + ), + Error::::OriginNotOwner + ); + }) +} + +#[test] +fn unregister_is_ok() { + ExtBuilder::build().execute_with(|| { + // Prepare dApp + let owner = 1; + let smart_contract = MockSmartContract::Wasm(3); + assert_register(owner, &smart_contract); + + assert_unregister(&smart_contract); + }) +} + +#[test] +fn unregister_fails() { + ExtBuilder::build().execute_with(|| { + let owner = 1; + let smart_contract = MockSmartContract::Wasm(3); + + // Cannot unregister contract which doesn't exist + assert_noop!( + DappStaking::unregister(RuntimeOrigin::root(), smart_contract), + Error::::ContractNotFound + ); + + // Cannot unregister with incorrect origin + assert_register(owner, &smart_contract); + assert_noop!( + DappStaking::unregister(RuntimeOrigin::signed(owner), smart_contract), + BadOrigin + ); + + // Cannot unregister same contract twice + assert_unregister(&smart_contract); + assert_noop!( + DappStaking::unregister(RuntimeOrigin::root(), smart_contract), + Error::::NotOperatedDApp + ); + }) +} + +#[test] +fn lock_is_ok() { + ExtBuilder::build().execute_with(|| { + // Lock some amount + let locker = 2; + let free_balance = Balances::free_balance(&locker); + assert!(free_balance > 500, "Sanity check"); + assert_lock(locker, 100); + assert_lock(locker, 200); + + // Attempt to lock more than is available + assert_lock(locker, free_balance - 200); + + // Ensure minimum lock amount works + let locker = 3; + assert_lock( + locker, + ::MinimumLockedAmount::get(), + ); + }) +} + +#[test] +fn lock_with_incorrect_amount_fails() { + ExtBuilder::build().execute_with(|| { + // Cannot lock "nothing" + assert_noop!( + DappStaking::lock(RuntimeOrigin::signed(1), Balance::zero()), + Error::::ZeroAmount, + ); + + // Attempting to lock something after everything has been locked is same + // as attempting to lock with "nothing" + let locker = 1; + assert_lock(locker, Balances::free_balance(&locker)); + assert_noop!( + DappStaking::lock(RuntimeOrigin::signed(locker), 1), + Error::::ZeroAmount, + ); + + // Locking just below the minimum amount should fail + let locker = 2; + let minimum_locked_amount: Balance = + ::MinimumLockedAmount::get(); + assert_noop!( + DappStaking::lock(RuntimeOrigin::signed(locker), minimum_locked_amount - 1), + Error::::LockedAmountBelowThreshold, + ); + }) +} + +#[test] +fn lock_with_too_many_chunks_fails() { + ExtBuilder::build().execute_with(|| { + let max_locked_chunks = ::MaxLockedChunks::get(); + let minimum_locked_amount: Balance = + ::MinimumLockedAmount::get(); + + // Fill up the locked chunks to the limit + let locker = 1; + assert_lock(locker, minimum_locked_amount); + for current_era in 1..max_locked_chunks { + advance_to_era(current_era + 1); + assert_lock(locker, 1); + } + + // Ensure we can still lock in the current era since number of chunks should not increase + for _ in 0..10 { + assert_lock(locker, 1); + } + + // Advance to the next era and ensure it's not possible to add additional chunks + advance_to_era(ActiveProtocolState::::get().era + 1); + assert_noop!( + DappStaking::lock(RuntimeOrigin::signed(locker), 1), + Error::::TooManyLockedBalanceChunks, + ); + }) +} diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs new file mode 100644 index 0000000000..1f2419d714 --- /dev/null +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -0,0 +1,105 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::test::mock::*; +use crate::*; + +// Helper to generate custom `Get` types for testing the `AccountLedger` struct. +macro_rules! get_u32_type { + ($struct_name:ident, $value:expr) => { + struct $struct_name; + impl Get for $struct_name { + fn get() -> u32 { + $value + } + } + }; +} + +#[test] +fn protocol_state_default() { + let protoc_state = ProtocolState::::default(); + + assert_eq!(protoc_state.era, 0); + assert_eq!( + protoc_state.next_era_start, 1, + "Era should start immediately on the first block" + ); +} + +#[test] +fn account_ledger_default() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + let acc_ledger = AccountLedger::::default(); + + assert!(acc_ledger.is_empty()); + assert!(acc_ledger.locked_amount().is_zero()); + assert!(acc_ledger.era().is_zero()); + assert!(acc_ledger.latest_locked_chunk().is_none()); +} + +#[test] +fn account_ledger_add_lock_amount_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = + AccountLedger::::default(); + + // First step, sanity checks + let first_era = 1; + assert!(acc_ledger.locked_amount().is_zero()); + assert!(acc_ledger.add_lock_amount(0, first_era).is_ok()); + assert!(acc_ledger.locked_amount().is_zero()); + + // Adding lock value works as expected + let init_amount = 20; + assert!(acc_ledger.add_lock_amount(init_amount, first_era).is_ok()); + assert_eq!(acc_ledger.locked_amount(), init_amount); + assert_eq!(acc_ledger.era(), first_era); + assert!(!acc_ledger.is_empty()); + assert_eq!( + acc_ledger.latest_locked_chunk(), + Some(&LockedChunk:: { + amount: init_amount, + era: first_era, + }) + ); + + // Add to the same era + let addition = 7; + assert!(acc_ledger.add_lock_amount(addition, first_era).is_ok()); + assert_eq!(acc_ledger.locked_amount(), init_amount + addition); + assert_eq!(acc_ledger.era(), first_era); + + // Add up to storage limit + for i in 2..=LockedDummy::get() { + assert!(acc_ledger.add_lock_amount(addition, first_era + i).is_ok()); + assert_eq!( + acc_ledger.locked_amount(), + init_amount + addition * i as u128 + ); + assert_eq!(acc_ledger.era(), first_era + i); + } + + // Any further additions should fail due to exhausting bounded storage capacity + assert!(acc_ledger + .add_lock_amount(addition, acc_ledger.era() + 1) + .is_err()); + assert!(!acc_ledger.is_empty()); +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs new file mode 100644 index 0000000000..958cd496d5 --- /dev/null +++ b/pallets/dapp-staking-v3/src/types.rs @@ -0,0 +1,299 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use frame_support::{pallet_prelude::*, traits::Currency, BoundedVec}; +use frame_system::pallet_prelude::*; +use parity_scale_codec::{Decode, Encode}; +use sp_runtime::traits::{AtLeast32BitUnsigned, Zero}; + +use crate::pallet::Config; + +// TODO: instead of using `pub` visiblity for fields, either use `pub(crate)` or add dedicated methods for accessing them. + +/// The balance type used by the currency system. +pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +/// Convenience type for `AccountLedger` usage. +pub type AccountLedgerFor = AccountLedger< + BalanceOf, + BlockNumberFor, + ::MaxLockedChunks, + ::MaxUnlockingChunks, +>; + +/// Era number type +pub type EraNumber = u32; +/// Period number type +pub type PeriodNumber = u32; +/// Dapp Id type +pub type DAppId = u16; + +/// Distinct period types in dApp staking protocol. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum PeriodType { + /// Period during which the focus is on voting. + /// Inner value is the era in which the voting period ends. + Voting(#[codec(compact)] EraNumber), + /// Period during which dApps and stakers earn rewards. + /// Inner value is the era in which the Build&Eearn period ends. + BuildAndEarn(#[codec(compact)] EraNumber), +} + +/// Force types to speed up the next era, and even period. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum ForcingTypes { + /// Force the next era to start. + NewEra, + /// Force the current period phase to end, and new one to start + NewEraAndPeriodPhase, +} + +/// General information & state of the dApp staking protocol. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub struct ProtocolState { + /// Ongoing era number. + #[codec(compact)] + pub era: EraNumber, + /// Block number at which the next era should start. + /// TODO: instead of abusing on-initialize and wasting block-space, + /// I believe we should utilize `pallet-scheduler` to schedule the next era. Make an item for this. + #[codec(compact)] + pub next_era_start: BlockNumber, + /// Ongoing period number. + #[codec(compact)] + pub period: PeriodNumber, + /// Ongoing period type and when is it expected to end. + pub period_type: PeriodType, + /// `true` if pallet is in maintenance mode (disabled), `false` otherwise. + /// TODO: provide some configurable barrier to handle this on the runtime level instead? Make an item for this? + pub maintenance: bool, +} + +impl Default for ProtocolState +where + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, +{ + fn default() -> Self { + Self { + era: 0, + next_era_start: BlockNumber::from(1_u32), + period: 0, + period_type: PeriodType::Voting(0), + maintenance: false, + } + } +} + +/// dApp state in which some dApp is in. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum DAppState { + /// dApp is registered and active. + Registered, + /// dApp has been unregistered in the contained era + Unregistered(#[codec(compact)] EraNumber), +} + +/// General information about dApp. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub struct DAppInfo { + /// Owner of the dApp, default reward beneficiary. + pub owner: AccountId, + /// dApp's unique identifier in dApp staking. + #[codec(compact)] + pub id: DAppId, + /// Current state of the dApp. + pub state: DAppState, + // If `None`, rewards goes to the developer account, otherwise to the account Id in `Some`. + pub reward_destination: Option, +} + +/// How much was locked in a specific era +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub struct LockedChunk { + #[codec(compact)] + pub amount: Balance, + #[codec(compact)] + pub era: EraNumber, +} + +impl Default for LockedChunk +where + Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, +{ + fn default() -> Self { + Self { + amount: Balance::zero(), + era: EraNumber::zero(), + } + } +} + +// TODO: would users get better UX if we kept using eras? Using blocks is more precise though. +/// How much was unlocked in some block. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub struct UnlockingChunk< + Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, +> { + #[codec(compact)] + pub amount: Balance, + #[codec(compact)] + pub unlock_block: BlockNumber, +} + +impl Default for UnlockingChunk +where + Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, +{ + fn default() -> Self { + Self { + amount: Balance::zero(), + unlock_block: BlockNumber::zero(), + } + } +} + +/// General info about user's stakes +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(LockedLen, UnlockingLen))] +pub struct AccountLedger< + Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, + LockedLen: Get, + UnlockingLen: Get, +> { + /// How much was staked in each era + pub locked: BoundedVec, LockedLen>, + /// How much started unlocking on a certain block + pub unlocking: BoundedVec, UnlockingLen>, + //TODO, make this a compact struct!!! + /// How much user had staked in some period + // #[codec(compact)] + pub staked: (Balance, PeriodNumber), +} + +impl Default + for AccountLedger +where + Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, + LockedLen: Get, + UnlockingLen: Get, +{ + fn default() -> Self { + Self { + locked: BoundedVec::, LockedLen>::default(), + unlocking: BoundedVec::, UnlockingLen>::default(), + staked: (Balance::zero(), 0), + } + } +} + +impl + AccountLedger +where + Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, + LockedLen: Get, + UnlockingLen: Get, +{ + /// Empty if no locked/unlocking/staked info exists. + pub fn is_empty(&self) -> bool { + self.locked.is_empty() && self.unlocking.is_empty() && self.staked.0.is_zero() + } + + /// Returns latest locked chunk if it exists, `None` otherwise + pub fn latest_locked_chunk(&self) -> Option<&LockedChunk> { + self.locked.last() + } + + /// Returns locked amount. + /// If `zero`, means that associated account hasn't locked any funds. + pub fn locked_amount(&self) -> Balance { + self.latest_locked_chunk() + .map_or(Balance::zero(), |locked| locked.amount) + } + + /// Returns latest era in which locked amount was updated or zero in case no lock amount exists + pub fn era(&self) -> EraNumber { + self.latest_locked_chunk() + .map_or(EraNumber::zero(), |locked| locked.era) + } + + /// Adds the specified amount to the total locked amount, if possible. + /// + /// If entry for the specified era already exists, it's updated. + /// + /// If entry for the specified era doesn't exist, it's created and insertion is attempted. + /// In case vector has no more capacity, error is returned, and whole operation is a noop. + pub fn add_lock_amount(&mut self, amount: Balance, era: EraNumber) -> Result<(), ()> { + if amount.is_zero() { + return Ok(()); + } + + let mut locked_chunk = if let Some(&locked_chunk) = self.locked.last() { + locked_chunk + } else { + LockedChunk::default() + }; + + locked_chunk.amount.saturating_accrue(amount); + + if locked_chunk.era == era && !self.locked.is_empty() { + if let Some(last) = self.locked.last_mut() { + *last = locked_chunk; + } + } else { + locked_chunk.era = era; + self.locked.try_push(locked_chunk).map_err(|_| ())?; + } + + Ok(()) + } +} + +/// Rewards pool for lock participants & dApps +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct RewardInfo { + /// Rewards pool for accounts which have locked funds in dApp staking + #[codec(compact)] + pub participants: Balance, + /// Reward pool for dApps + #[codec(compact)] + pub dapps: Balance, +} + +/// Info about current era, including the rewards, how much is locked, unlocking, etc. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct EraInfo { + /// Info about era rewards + pub rewards: RewardInfo, + /// How much balance is considered to be locked in the current era. + /// This value influences the reward distribution. + #[codec(compact)] + pub active_era_locked: Balance, + /// How much balance is locked in dApp staking, in total. + /// For rewards, this amount isn't relevant for the current era, but only from the next one. + #[codec(compact)] + pub total_locked: Balance, + /// How much balance is undergoing unlocking process (still counts into locked amount) + #[codec(compact)] + pub unlocking: Balance, +} From 6a36db164906c72931958909fa894d2f15a47a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Thu, 21 Sep 2023 09:55:45 +0100 Subject: [PATCH 02/14] Feat/dapp staking v3 phase2 (#991) * Phase2 progress * Adapt for 0.9.43 * Abstract sparse vector type * Further modifications * claim unlocked functionality WIP * claim unlocked tested * Relock unlocking * Check era when adding chunk * Custom error for some types * Additional type tests - WIP * More type tests * Review comments * Additional changes --- Cargo.lock | 19 + pallets/dapp-staking-v3/Cargo.toml | 3 + pallets/dapp-staking-v3/src/lib.rs | 179 +++- pallets/dapp-staking-v3/src/test/mock.rs | 29 +- .../dapp-staking-v3/src/test/testing_utils.rs | 198 +++- pallets/dapp-staking-v3/src/test/tests.rs | 400 +++++++- .../dapp-staking-v3/src/test/tests_types.rs | 915 +++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 476 +++++++-- 8 files changed, 2111 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 28f586a9b3..536ec19859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7105,6 +7105,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-dapp-staking-v3" +version = "0.0.1-alpha" +dependencies = [ + "astar-primitives", + "frame-support", + "frame-system", + "num-traits", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-dapps-staking" version = "3.10.0" diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml index 27f927fe6e..acc457ac8f 100644 --- a/pallets/dapp-staking-v3/Cargo.toml +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -21,6 +21,8 @@ sp-io = { workspace = true } sp-runtime = { workspace = true } sp-std = { workspace = true } +astar-primitives = { workspace = true } + [dev-dependencies] pallet-balances = { workspace = true } @@ -39,4 +41,5 @@ std = [ "frame-support/std", "frame-system/std", "pallet-balances/std", + "astar-primitives/std", ] diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index d2057651ed..fc5a24d8a3 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -47,6 +47,8 @@ use frame_support::{ use frame_system::pallet_prelude::*; use sp_runtime::traits::{BadOrigin, Saturating, Zero}; +use astar_primitives::Balance; + use crate::types::*; pub use pallet::*; @@ -65,7 +67,6 @@ pub mod pallet { const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); #[pallet::pallet] - #[pallet::generate_store(pub(crate) trait Store)] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); @@ -75,7 +76,12 @@ pub mod pallet { type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// Currency used for staking. - type Currency: LockableCurrency; + /// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :) + type Currency: LockableCurrency< + Self::AccountId, + Moment = Self::BlockNumber, + Balance = Balance, + >; /// Describes smart contract in the context required by dApp staking. type SmartContract: Parameter + Member + MaxEncodedLen; @@ -89,6 +95,7 @@ pub mod pallet { type MaxNumberOfContracts: Get; /// Maximum number of locked chunks that can exist per account at a time. + // TODO: should this just be hardcoded to 2? Nothing else makes sense really - current era and next era are required. #[pallet::constant] type MaxLockedChunks: Get; @@ -98,7 +105,15 @@ pub mod pallet { /// Minimum amount an account has to lock in dApp staking in order to participate. #[pallet::constant] - type MinimumLockedAmount: Get>; + type MinimumLockedAmount: Get; + + /// Amount of blocks that need to pass before unlocking chunks can be claimed by the owner. + #[pallet::constant] + type UnlockingPeriod: Get>; + + /// Maximum number of staking chunks that can exist per account at a time. + #[pallet::constant] + type MaxStakingChunks: Get; } #[pallet::event] @@ -128,7 +143,22 @@ pub mod pallet { /// Account has locked some amount into dApp staking. Locked { account: T::AccountId, - amount: BalanceOf, + amount: Balance, + }, + /// Account has started the unlocking process for some amount. + Unlocking { + account: T::AccountId, + amount: Balance, + }, + /// Account has claimed unlocked amount, removing the lock from it. + ClaimedUnlocked { + account: T::AccountId, + amount: Balance, + }, + /// Account has relocked all of the unlocking chunks. + Relock { + account: T::AccountId, + amount: Balance, }, } @@ -155,6 +185,14 @@ pub mod pallet { LockedAmountBelowThreshold, /// Cannot add additional locked balance chunks due to size limit. TooManyLockedBalanceChunks, + /// Cannot add additional unlocking chunks due to size limit + TooManyUnlockingChunks, + /// Remaining stake prevents entire balance of starting the unlocking process. + RemainingStakePreventsFullUnlock, + /// There are no eligible unlocked chunks to claim. This can happen either if no eligible chunks exist, or if user has no chunks at all. + NoUnlockedChunksToClaim, + /// There are no unlocking chunks available to relock. + NoUnlockingChunks, } /// General information about dApp staking protocol state. @@ -183,7 +221,7 @@ pub mod pallet { /// General information about the current era. #[pallet::storage] - pub type CurrentEraInfo = StorageValue<_, EraInfo>, ValueQuery>; + pub type CurrentEraInfo = StorageValue<_, EraInfo, ValueQuery>; #[pallet::call] impl Pallet { @@ -376,10 +414,7 @@ pub mod pallet { /// caller should claim pending rewards, before retrying to lock additional funds. #[pallet::call_index(5)] #[pallet::weight(Weight::zero())] - pub fn lock( - origin: OriginFor, - #[pallet::compact] amount: BalanceOf, - ) -> DispatchResult { + pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -388,7 +423,7 @@ pub mod pallet { // Calculate & check amount available for locking let available_balance = - T::Currency::free_balance(&account).saturating_sub(ledger.locked_amount()); + T::Currency::free_balance(&account).saturating_sub(ledger.active_locked_amount()); let amount_to_lock = available_balance.min(amount); ensure!(!amount_to_lock.is_zero(), Error::::ZeroAmount); @@ -398,13 +433,13 @@ pub mod pallet { .add_lock_amount(amount_to_lock, lock_era) .map_err(|_| Error::::TooManyLockedBalanceChunks)?; ensure!( - ledger.locked_amount() >= T::MinimumLockedAmount::get(), + ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), Error::::LockedAmountBelowThreshold ); Self::update_ledger(&account, ledger); CurrentEraInfo::::mutate(|era_info| { - era_info.total_locked.saturating_accrue(amount_to_lock); + era_info.add_locked(amount_to_lock); }); Self::deposit_event(Event::::Locked { @@ -414,6 +449,124 @@ pub mod pallet { Ok(()) } + + /// Attempts to start the unlocking process for the specified amount. + /// + /// Only the amount that isn't actively used for staking can be unlocked. + /// If the amount is greater than the available amount for unlocking, everything is unlocked. + /// If the remaining locked amount would take the account below the minimum locked amount, everything is unlocked. + #[pallet::call_index(6)] + #[pallet::weight(Weight::zero())] + pub fn unlock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let state = ActiveProtocolState::::get(); + let mut ledger = Ledger::::get(&account); + + let available_for_unlocking = ledger.unlockable_amount(state.period); + let amount_to_unlock = available_for_unlocking.min(amount); + + // Ensure we unlock everything if remaining amount is below threshold. + let remaining_amount = ledger + .active_locked_amount() + .saturating_sub(amount_to_unlock); + let amount_to_unlock = if remaining_amount < T::MinimumLockedAmount::get() { + ensure!( + ledger.active_stake(state.period).is_zero(), + Error::::RemainingStakePreventsFullUnlock + ); + ledger.active_locked_amount() + } else { + amount_to_unlock + }; + + // Sanity check + ensure!(!amount_to_unlock.is_zero(), Error::::ZeroAmount); + + // Update ledger with new lock and unlocking amounts + ledger + .subtract_lock_amount(amount_to_unlock, state.era) + .map_err(|_| Error::::TooManyLockedBalanceChunks)?; + + let current_block = frame_system::Pallet::::block_number(); + let unlock_block = current_block.saturating_add(T::UnlockingPeriod::get()); + ledger + .add_unlocking_chunk(amount_to_unlock, unlock_block) + .map_err(|_| Error::::TooManyUnlockingChunks)?; + + // Update storage + Self::update_ledger(&account, ledger); + CurrentEraInfo::::mutate(|era_info| { + era_info.unlocking_started(amount_to_unlock); + }); + + Self::deposit_event(Event::::Unlocking { + account, + amount: amount_to_unlock, + }); + + Ok(()) + } + + /// Claims all of fully unlocked chunks, removing the lock from them. + #[pallet::call_index(7)] + #[pallet::weight(Weight::zero())] + pub fn claim_unlocked(origin: OriginFor) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let mut ledger = Ledger::::get(&account); + + let current_block = frame_system::Pallet::::block_number(); + let amount = ledger.claim_unlocked(current_block); + ensure!(amount > Zero::zero(), Error::::NoUnlockedChunksToClaim); + + Self::update_ledger(&account, ledger); + CurrentEraInfo::::mutate(|era_info| { + era_info.unlocking_removed(amount); + }); + + // TODO: We should ensure user doesn't unlock everything if they still have storage leftovers (e.g. unclaimed rewards?) + + Self::deposit_event(Event::::ClaimedUnlocked { account, amount }); + + Ok(()) + } + + #[pallet::call_index(8)] + #[pallet::weight(Weight::zero())] + pub fn relock_unlocking(origin: OriginFor) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let state = ActiveProtocolState::::get(); + let mut ledger = Ledger::::get(&account); + + ensure!(!ledger.unlocking.is_empty(), Error::::NoUnlockingChunks); + + // Only lock for the next era onwards. + let lock_era = state.era.saturating_add(1); + let amount = ledger.consume_unlocking_chunks(); + + ledger + .add_lock_amount(amount, lock_era) + .map_err(|_| Error::::TooManyLockedBalanceChunks)?; + ensure!( + ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), + Error::::LockedAmountBelowThreshold + ); + + Self::update_ledger(&account, ledger); + CurrentEraInfo::::mutate(|era_info| { + era_info.add_locked(amount); + era_info.unlocking_removed(amount); + }); + + Self::deposit_event(Event::::Relock { account, amount }); + + Ok(()) + } } impl Pallet { @@ -450,7 +603,7 @@ pub mod pallet { T::Currency::set_lock( STAKING_ID, account, - ledger.locked_amount(), + ledger.active_locked_amount(), WithdrawReasons::all(), ); Ledger::::insert(account, ledger); diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index d4f11189f9..b8ef08163a 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -20,7 +20,7 @@ use crate::{self as pallet_dapp_staking, *}; use frame_support::{ construct_runtime, parameter_types, - traits::{ConstU128, ConstU16, ConstU32}, + traits::{ConstU128, ConstU16, ConstU32, ConstU64}, weights::Weight, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -58,7 +58,7 @@ construct_runtime!( parameter_types! { pub const BlockHashCount: u64 = 250; pub BlockWeights: frame_system::limits::BlockWeights = - frame_system::limits::BlockWeights::simple_max(Weight::from_ref_time(1024)); + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); } impl frame_system::Config for Test { @@ -97,6 +97,10 @@ impl pallet_balances::Config for Test { type DustRemoval = (); type ExistentialDeposit = ConstU128; type AccountStore = System; + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; type WeightInfo = (); } @@ -108,7 +112,9 @@ impl pallet_dapp_staking::Config for Test { type MaxNumberOfContracts = ConstU16<10>; type MaxLockedChunks = ConstU32<5>; type MaxUnlockingChunks = ConstU32<5>; + type MaxStakingChunks = ConstU32<8>; type MinimumLockedAmount = ConstU128; + type UnlockingPeriod = ConstU64<20>; } #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] @@ -144,6 +150,15 @@ impl ExtBuilder { ext.execute_with(|| { System::set_block_number(1); DappStaking::on_initialize(System::block_number()); + + // TODO: remove this after proper on_init handling is implemented + pallet_dapp_staking::ActiveProtocolState::::put(ProtocolState { + era: 1, + next_era_start: BlockNumber::from(101_u32), + period: 1, + period_type: PeriodType::Voting(16), + maintenance: false, + }); }); ext @@ -163,7 +178,7 @@ pub(crate) fn _run_to_block(n: u64) { /// Run for the specified number of blocks. /// Function assumes first block has been initialized. -pub(crate) fn _run_for_blocks(n: u64) { +pub(crate) fn run_for_blocks(n: u64) { _run_to_block(System::block_number() + n); } @@ -174,3 +189,11 @@ pub(crate) fn advance_to_era(era: EraNumber) { // TODO: Properly implement this later when additional logic has been implemented ActiveProtocolState::::mutate(|state| state.era = era); } + +/// Advance blocks until the specified period has been reached. +/// +/// Function has no effect if period is already passed. +pub(crate) fn advance_to_period(period: PeriodNumber) { + // TODO: Properly implement this later when additional logic has been implemented + ActiveProtocolState::::mutate(|state| state.period = period); +} diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index f383e9afee..ef15c21f0f 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -17,19 +17,25 @@ // along with Astar. If not, see . use crate::test::mock::*; -use crate::*; +use crate::types::*; +use crate::{ + pallet as pallet_dapp_staking, ActiveProtocolState, BlockNumberFor, CurrentEraInfo, DAppId, + Event, IntegratedDApps, Ledger, NextDAppId, +}; -use frame_support::assert_ok; +use frame_support::{assert_ok, traits::Get}; +use sp_runtime::traits::Zero; use std::collections::HashMap; /// Helper struct used to store the entire pallet state snapshot. /// Used when comparison of before/after states is required. +#[derive(Debug)] pub(crate) struct MemorySnapshot { active_protocol_state: ProtocolState>, next_dapp_id: DAppId, - current_era_info: EraInfo>, + current_era_info: EraInfo, integrated_dapps: HashMap< - ::SmartContract, + ::SmartContract, DAppInfo<::AccountId>, >, ledger: HashMap<::AccountId, AccountLedgerFor>, @@ -52,7 +58,7 @@ impl MemorySnapshot { pub fn locked_balance(&self, account: &AccountId) -> Balance { self.ledger .get(&account) - .map_or(Balance::zero(), |ledger| ledger.locked_amount()) + .map_or(Balance::zero(), |ledger| ledger.active_locked_amount()) } } @@ -196,7 +202,7 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) { .ledger .get(&account) .expect("Ledger entry has to exist after succcessful lock call") - .era(), + .lock_era(), post_snapshot.active_protocol_state.era + 1 ); @@ -211,3 +217,183 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) { "Active era locked amount should remain exactly the same." ); } + +/// Start the unlocking process for locked funds and assert success. +pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { + let pre_snapshot = MemorySnapshot::new(); + + assert!( + pre_snapshot.ledger.contains_key(&account), + "Cannot unlock for non-existing ledger." + ); + + // Calculate expected unlock amount + let pre_ledger = &pre_snapshot.ledger[&account]; + let expected_unlock_amount = { + // Cannot unlock more than is available + let possible_unlock_amount = pre_ledger + .unlockable_amount(pre_snapshot.active_protocol_state.period) + .min(amount); + + // When unlocking would take account below the minimum lock threshold, unlock everything + let locked_amount = pre_ledger.active_locked_amount(); + let min_locked_amount = ::MinimumLockedAmount::get(); + if locked_amount.saturating_sub(possible_unlock_amount) < min_locked_amount { + locked_amount + } else { + possible_unlock_amount + } + }; + + // Unlock funds + assert_ok!(DappStaking::unlock(RuntimeOrigin::signed(account), amount,)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Unlocking { + account, + amount: expected_unlock_amount, + })); + + // Verify post-state + let post_snapshot = MemorySnapshot::new(); + + // Verify ledger is as expected + let period_number = pre_snapshot.active_protocol_state.period; + let post_ledger = &post_snapshot.ledger[&account]; + assert_eq!( + pre_ledger.active_locked_amount(), + post_ledger.active_locked_amount() + expected_unlock_amount, + "Active locked amount should be decreased by the amount unlocked." + ); + assert_eq!( + pre_ledger.unlocking_amount() + expected_unlock_amount, + post_ledger.unlocking_amount(), + "Total unlocking amount should be increased by the amount unlocked." + ); + assert_eq!( + pre_ledger.total_locked_amount(), + post_ledger.total_locked_amount(), + "Total locked amount should remain exactly the same since the unlocking chunks are still locked." + ); + assert_eq!( + pre_ledger.unlockable_amount(period_number), + post_ledger.unlockable_amount(period_number) + expected_unlock_amount, + "Unlockable amount should be decreased by the amount unlocked." + ); + + // In case ledger is empty, it should have been removed from the storage + if post_ledger.is_empty() { + assert!(!Ledger::::contains_key(&account)); + } + + // Verify era info post-state + let pre_era_info = &pre_snapshot.current_era_info; + let post_era_info = &post_snapshot.current_era_info; + assert_eq!( + pre_era_info.unlocking + expected_unlock_amount, + post_era_info.unlocking + ); + assert_eq!( + pre_era_info + .total_locked + .saturating_sub(expected_unlock_amount), + post_era_info.total_locked + ); + assert_eq!( + pre_era_info + .active_era_locked + .saturating_sub(expected_unlock_amount), + post_era_info.active_era_locked + ); +} + +/// Claims the unlocked funds back into free balance of the user and assert success. +pub(crate) fn assert_claim_unlocked(account: AccountId) { + let pre_snapshot = MemorySnapshot::new(); + + assert!( + pre_snapshot.ledger.contains_key(&account), + "Cannot claim unlocked for non-existing ledger." + ); + + let current_block = System::block_number(); + let mut consumed_chunks = 0; + let mut amount = 0; + for unlock_chunk in pre_snapshot.ledger[&account].clone().unlocking.into_inner() { + if unlock_chunk.unlock_block <= current_block { + amount += unlock_chunk.amount; + consumed_chunks += 1; + } + } + + // Claim unlocked chunks + assert_ok!(DappStaking::claim_unlocked(RuntimeOrigin::signed(account))); + System::assert_last_event(RuntimeEvent::DappStaking(Event::ClaimedUnlocked { + account, + amount, + })); + + // Verify post-state + let post_snapshot = MemorySnapshot::new(); + + let post_ledger = if let Some(ledger) = post_snapshot.ledger.get(&account) { + ledger.clone() + } else { + Default::default() + }; + + assert_eq!( + post_ledger.unlocking.len(), + pre_snapshot.ledger[&account].unlocking.len() - consumed_chunks + ); + assert_eq!( + post_ledger.unlocking_amount(), + pre_snapshot.ledger[&account].unlocking_amount() - amount + ); + assert_eq!( + post_snapshot.current_era_info.unlocking, + pre_snapshot.current_era_info.unlocking - amount + ); +} + +/// Claims the unlocked funds back into free balance of the user and assert success. +pub(crate) fn assert_relock_unlocking(account: AccountId) { + let pre_snapshot = MemorySnapshot::new(); + + assert!( + pre_snapshot.ledger.contains_key(&account), + "Cannot relock unlocking non-existing ledger." + ); + + let amount = pre_snapshot.ledger[&account].unlocking_amount(); + + // Relock unlocking chunks + assert_ok!(DappStaking::relock_unlocking(RuntimeOrigin::signed( + account + ))); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Relock { account, amount })); + + // Verify post-state + let post_snapshot = MemorySnapshot::new(); + + // Account ledger + let post_ledger = &post_snapshot.ledger[&account]; + assert!(post_ledger.unlocking.is_empty()); + assert!(post_ledger.unlocking_amount().is_zero()); + assert_eq!( + post_ledger.active_locked_amount(), + pre_snapshot.ledger[&account].active_locked_amount() + amount + ); + assert_eq!( + post_ledger.lock_era(), + post_snapshot.active_protocol_state.era + 1 + ); + + // Current era info + assert_eq!( + post_snapshot.current_era_info.unlocking, + pre_snapshot.current_era_info.unlocking - amount + ); + assert_eq!( + post_snapshot.current_era_info.total_locked, + pre_snapshot.current_era_info.total_locked + amount + ); +} diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 381a385139..4a6dcdc901 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -19,10 +19,11 @@ use crate::test::mock::*; use crate::test::testing_utils::*; use crate::{ - pallet as pallet_dapp_staking, ActiveProtocolState, DAppId, Error, IntegratedDApps, NextDAppId, + pallet as pallet_dapp_staking, ActiveProtocolState, DAppId, Error, IntegratedDApps, Ledger, + NextDAppId, SparseBoundedAmountEraVec, StakeChunk, }; -use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; +use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get, BoundedVec}; use sp_runtime::traits::Zero; #[test] @@ -78,6 +79,10 @@ fn maintenace_mode_call_filtering_works() { DappStaking::lock(RuntimeOrigin::signed(1), 100), Error::::Disabled ); + assert_noop!( + DappStaking::unlock(RuntimeOrigin::signed(1), 100), + Error::::Disabled + ); }) } @@ -362,3 +367,394 @@ fn lock_with_too_many_chunks_fails() { ); }) } + +#[test] +fn unlock_basic_example_is_ok() { + ExtBuilder::build().execute_with(|| { + // Lock some amount + let account = 2; + let lock_amount = 101; + assert_lock(account, lock_amount); + + // Unlock some amount in the same era that it was locked + let first_unlock_amount = 7; + assert_unlock(account, first_unlock_amount); + + // Advance era and unlock additional amount + advance_to_era(ActiveProtocolState::::get().era + 1); + assert_unlock(account, first_unlock_amount); + + // Lock a bit more, and unlock again + assert_lock(account, lock_amount); + assert_unlock(account, first_unlock_amount); + }) +} + +#[test] +fn unlock_with_remaining_amount_below_threshold_is_ok() { + ExtBuilder::build().execute_with(|| { + // Lock some amount in a few eras + let account = 2; + let lock_amount = 101; + assert_lock(account, lock_amount); + advance_to_era(ActiveProtocolState::::get().era + 1); + assert_lock(account, lock_amount); + advance_to_era(ActiveProtocolState::::get().era + 3); + + // Unlock such amount that remaining amount is below threshold, resulting in full unlock + let minimum_locked_amount: Balance = + ::MinimumLockedAmount::get(); + let ledger = Ledger::::get(&account); + assert_unlock( + account, + ledger.active_locked_amount() - minimum_locked_amount + 1, + ); + }) +} + +#[test] +fn unlock_with_amount_higher_than_avaiable_is_ok() { + ExtBuilder::build().execute_with(|| { + // Lock some amount in a few eras + let account = 2; + let lock_amount = 101; + assert_lock(account, lock_amount); + advance_to_era(ActiveProtocolState::::get().era + 1); + assert_lock(account, lock_amount); + + // TODO: Hacky, maybe improve later when staking is implemented? + let stake_amount = 91; + Ledger::::mutate(&account, |ledger| { + ledger.staked = SparseBoundedAmountEraVec( + BoundedVec::try_from(vec![StakeChunk { + amount: stake_amount, + era: ActiveProtocolState::::get().era, + }]) + .expect("Only one chunk so creation should succeed."), + ); + ledger.staked_period = Some(ActiveProtocolState::::get().period); + }); + + // Try to unlock more than is available, due to active staked amount + assert_unlock(account, lock_amount - stake_amount + 1); + + // Ensure there is no effect of staked amount once we move to the following period + assert_lock(account, lock_amount - stake_amount); // restore previous state + advance_to_period(ActiveProtocolState::::get().period + 1); + assert_unlock(account, lock_amount - stake_amount + 1); + }) +} + +#[test] +fn unlock_advanced_examples_are_ok() { + ExtBuilder::build().execute_with(|| { + // Lock some amount + let account = 2; + let lock_amount = 101; + assert_lock(account, lock_amount); + + // Unlock some amount in the same era that it was locked + let unlock_amount = 7; + assert_unlock(account, unlock_amount); + + // Advance era and unlock additional amount + advance_to_era(ActiveProtocolState::::get().era + 1); + assert_unlock(account, unlock_amount * 2); + + // Advance few more eras, and unlock everything + advance_to_era(ActiveProtocolState::::get().era + 7); + assert_unlock(account, lock_amount); + assert!(Ledger::::get(&account) + .active_locked_amount() + .is_zero()); + + // Advance one more era and ensure we can still lock & unlock + advance_to_era(ActiveProtocolState::::get().era + 1); + assert_lock(account, lock_amount); + assert_unlock(account, unlock_amount); + }) +} + +#[test] +fn unlock_everything_with_active_stake_fails() { + ExtBuilder::build().execute_with(|| { + let account = 2; + let lock_amount = 101; + assert_lock(account, lock_amount); + advance_to_era(ActiveProtocolState::::get().era + 1); + + // We stake so the amount is just below the minimum locked amount, causing full unlock impossible. + let minimum_locked_amount: Balance = + ::MinimumLockedAmount::get(); + let stake_amount = minimum_locked_amount - 1; + // TODO: Hacky, maybe improve later when staking is implemented? + Ledger::::mutate(&account, |ledger| { + ledger.staked = SparseBoundedAmountEraVec( + BoundedVec::try_from(vec![StakeChunk { + amount: stake_amount, + era: ActiveProtocolState::::get().era, + }]) + .expect("Only one chunk so creation should succeed."), + ); + ledger.staked_period = Some(ActiveProtocolState::::get().period); + }); + + // Try to unlock more than is available, due to active staked amount + assert_noop!( + DappStaking::unlock(RuntimeOrigin::signed(account), lock_amount), + Error::::RemainingStakePreventsFullUnlock, + ); + }) +} + +#[test] +fn unlock_with_zero_amount_fails() { + ExtBuilder::build().execute_with(|| { + let account = 2; + let lock_amount = 101; + assert_lock(account, lock_amount); + advance_to_era(ActiveProtocolState::::get().era + 1); + + // Unlock with zero fails + assert_noop!( + DappStaking::unlock(RuntimeOrigin::signed(account), 0), + Error::::ZeroAmount, + ); + + // Stake everything, so available unlock amount is always zero + // TODO: Hacky, maybe improve later when staking is implemented? + Ledger::::mutate(&account, |ledger| { + ledger.staked = SparseBoundedAmountEraVec( + BoundedVec::try_from(vec![StakeChunk { + amount: lock_amount, + era: ActiveProtocolState::::get().era, + }]) + .expect("Only one chunk so creation should succeed."), + ); + ledger.staked_period = Some(ActiveProtocolState::::get().period); + }); + + // Try to unlock anything, expect zero amount error + assert_noop!( + DappStaking::unlock(RuntimeOrigin::signed(account), lock_amount), + Error::::ZeroAmount, + ); + }) +} + +#[test] +fn unlock_with_exceeding_locked_storage_limits_fails() { + ExtBuilder::build().execute_with(|| { + let account = 2; + let lock_amount = 103; + assert_lock(account, lock_amount); + + let unlock_amount = 3; + for _ in 0..::MaxLockedChunks::get() { + advance_to_era(ActiveProtocolState::::get().era + 1); + assert_unlock(account, unlock_amount); + } + + // We can still unlock in the current era, theoretically + for _ in 0..5 { + assert_unlock(account, unlock_amount); + } + + // Following unlock should fail due to exceeding storage limits + advance_to_era(ActiveProtocolState::::get().era + 1); + assert_noop!( + DappStaking::unlock(RuntimeOrigin::signed(account), unlock_amount), + Error::::TooManyLockedBalanceChunks, + ); + }) +} + +#[test] +fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { + ExtBuilder::build().execute_with(|| { + // Lock some amount in a few eras + let account = 2; + let lock_amount = 103; + assert_lock(account, lock_amount); + + let unlock_amount = 3; + for _ in 0..::MaxUnlockingChunks::get() { + run_for_blocks(1); + assert_unlock(account, unlock_amount); + } + + // We can still unlock in the current erblocka, theoretically + for _ in 0..5 { + assert_unlock(account, unlock_amount); + } + + // Following unlock should fail due to exceeding storage limits + run_for_blocks(1); + assert_noop!( + DappStaking::unlock(RuntimeOrigin::signed(account), unlock_amount), + Error::::TooManyUnlockingChunks, + ); + }) +} + +#[test] +fn claim_unlocked_is_ok() { + ExtBuilder::build().execute_with(|| { + let unlocking_blocks: BlockNumber = + ::UnlockingPeriod::get(); + + // Lock some amount in a few eras + let account = 2; + let lock_amount = 103; + assert_lock(account, lock_amount); + + // Basic example + let unlock_amount = 3; + assert_unlock(account, unlock_amount); + run_for_blocks(unlocking_blocks); + assert_claim_unlocked(account); + + // Advanced example + let max_unlocking_chunks: u32 = + ::MaxUnlockingChunks::get(); + for _ in 0..max_unlocking_chunks { + run_for_blocks(1); + assert_unlock(account, unlock_amount); + } + + // Leave two blocks remaining after the claim + run_for_blocks(unlocking_blocks - 2); + assert_claim_unlocked(account); + + // Claim last two blocks together + run_for_blocks(2); + assert_claim_unlocked(account); + assert!(Ledger::::get(&account).unlocking.is_empty()); + }) +} + +#[test] +fn claim_unlocked_no_eligible_chunks_fails() { + ExtBuilder::build().execute_with(|| { + // Sanity check + let account = 2; + assert_noop!( + DappStaking::claim_unlocked(RuntimeOrigin::signed(account)), + Error::::NoUnlockedChunksToClaim, + ); + + // Cannot claim if unlock period hasn't passed yet + let lock_amount = 103; + assert_lock(account, lock_amount); + let unlocking_blocks: BlockNumber = + ::UnlockingPeriod::get(); + run_for_blocks(unlocking_blocks - 1); + assert_noop!( + DappStaking::claim_unlocked(RuntimeOrigin::signed(account)), + Error::::NoUnlockedChunksToClaim, + ); + }) +} + +#[test] +fn relock_unlocking_is_ok() { + ExtBuilder::build().execute_with(|| { + // Lock some amount + let account = 2; + let lock_amount = 91; + assert_lock(account, lock_amount); + + // Prepare some unlock chunks + let unlock_amount = 5; + assert_unlock(account, unlock_amount); + run_for_blocks(2); + assert_unlock(account, unlock_amount); + + assert_relock_unlocking(account); + + let max_unlocking_chunks: u32 = + ::MaxUnlockingChunks::get(); + for _ in 0..max_unlocking_chunks { + run_for_blocks(1); + assert_unlock(account, unlock_amount); + } + + assert_relock_unlocking(account); + }) +} + +#[test] +fn relock_unlocking_no_chunks_fails() { + ExtBuilder::build().execute_with(|| { + assert_noop!( + DappStaking::relock_unlocking(RuntimeOrigin::signed(1)), + Error::::NoUnlockingChunks, + ); + }) +} + +#[test] +fn relock_unlocking_too_many_chunks_fails() { + ExtBuilder::build().execute_with(|| { + let max_locked_chunks = ::MaxLockedChunks::get(); + + // Fill up the locked chunks to the limit + let account = 3; + for current_era in 1..=max_locked_chunks { + assert_lock(account, 11); + advance_to_era(current_era + 1); + } + assert_unlock(account, 7); + + assert_noop!( + DappStaking::relock_unlocking(RuntimeOrigin::signed(account)), + Error::::TooManyLockedBalanceChunks, + ); + }) +} + +#[test] +fn relock_unlocking_insufficient_lock_amount_fails() { + ExtBuilder::build().execute_with(|| { + let minimum_locked_amount: Balance = + ::MinimumLockedAmount::get(); + + // lock amount should be above the threshold + let account = 2; + assert_lock(account, minimum_locked_amount + 1); + + // Create two unlocking chunks + assert_unlock(account, 1); + run_for_blocks(1); + assert_unlock(account, minimum_locked_amount); + + // This scenario can only be achieved if minimum staking amount increases on live network. + // Otherwise we always have a guarantee that the latest unlocking chunk at least covers the + // minimum staking amount. + // To test this, we will do a "dirty trick", and swap the two unlocking chunks that were just created. + // This shoudl ensure that the latest unlocking chunk is below the minimum staking amount. + Ledger::::mutate(&account, |ledger| { + ledger.unlocking = ledger + .unlocking + .clone() + .try_mutate(|inner| { + let temp_block = inner[0].unlock_block; + inner[0].unlock_block = inner[1].unlock_block; + inner[1].unlock_block = temp_block; + inner.swap(0, 1); + }) + .expect("No size manipulation, only element swap."); + }); + + // Make sure only one chunk is left + let unlocking_blocks: BlockNumber = + ::UnlockingPeriod::get(); + run_for_blocks(unlocking_blocks - 1); + assert_claim_unlocked(account); + + assert_noop!( + DappStaking::relock_unlocking(RuntimeOrigin::signed(account)), + Error::::LockedAmountBelowThreshold, + ); + }) +} diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 1f2419d714..87623d8e82 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -16,12 +16,15 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use crate::test::mock::*; +use frame_support::assert_ok; + +use crate::test::mock::{Balance, *}; use crate::*; // Helper to generate custom `Get` types for testing the `AccountLedger` struct. macro_rules! get_u32_type { ($struct_name:ident, $value:expr) => { + #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] struct $struct_name; impl Get for $struct_name { fn get() -> u32 { @@ -31,6 +34,274 @@ macro_rules! get_u32_type { }; } +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, Default)] +struct DummyEraAmount { + amount: Balance, + era: u32, +} +impl AmountEraPair for DummyEraAmount { + fn get_amount(&self) -> Balance { + self.amount + } + fn get_era(&self) -> u32 { + self.era + } + fn set_era(&mut self, era: u32) { + self.era = era; + } + fn saturating_accrue(&mut self, increase: Balance) { + self.amount.saturating_accrue(increase); + } + fn saturating_reduce(&mut self, reduction: Balance) { + self.amount.saturating_reduce(reduction); + } +} +impl DummyEraAmount { + pub fn new(amount: Balance, era: u32) -> Self { + Self { amount, era } + } +} + +#[test] +fn sparse_bounded_amount_era_vec_add_amount_works() { + get_u32_type!(MaxLen, 5); + + // Sanity check + let mut vec = SparseBoundedAmountEraVec::::new(); + assert!(vec.0.is_empty()); + assert_ok!(vec.add_amount(0, 0)); + assert!(vec.0.is_empty()); + + // 1st scenario - add to empty vector, should create one entry + let init_amount = 19; + let first_era = 3; + assert_ok!(vec.add_amount(init_amount, first_era)); + assert_eq!(vec.0.len(), 1); + assert_eq!(vec.0[0], DummyEraAmount::new(init_amount, first_era)); + + // 2nd scenario - add to the same era, should update the entry + assert_ok!(vec.add_amount(init_amount, first_era)); + assert_eq!(vec.0.len(), 1); + assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); + + // 3rd scenario - add to the next era, should create a new entry + let second_era = first_era + 1; + assert_ok!(vec.add_amount(init_amount, second_era)); + assert_eq!(vec.0.len(), 2); + assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); + assert_eq!(vec.0[1], DummyEraAmount::new(init_amount * 3, second_era)); + + // 4th scenario - add to the previous era, should fail and be a noop + assert_eq!( + vec.add_amount(init_amount, first_era), + Err(SparseBoundedError::OldEra) + ); + assert_eq!(vec.0.len(), 2); + assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); + assert_eq!(vec.0[1], DummyEraAmount::new(init_amount * 3, second_era)); + + // 5th scenario - exceed capacity, should fail + for i in vec.0.len()..MaxLen::get() as usize { + assert_ok!(vec.add_amount(init_amount, second_era + i as u32)); + } + assert_eq!( + vec.add_amount(init_amount, 100), + Err(SparseBoundedError::NoCapacity) + ); +} + +// Test two scenarios: +// +// 1. [amount, era] -> subtract(x, era) -> [amount - x, era] +// 2. [amount, era] -> subtract (amount * 2, era) -> [] +#[test] +fn sparse_bounded_amount_era_vec_subtract_amount_basic_scenario_works() { + get_u32_type!(MaxLen, 5); + + // Sanity check + let mut vec = SparseBoundedAmountEraVec::::new(); + assert_ok!(vec.subtract_amount(0, 0)); + assert!(vec.0.is_empty()); + + // 1st scenario - only one entry exists, and it's the same era as the unlock + let init_amount = 19; + let first_era = 1; + let sub_amount = 3; + assert_ok!(vec.add_amount(init_amount, first_era)); + assert_ok!(vec.subtract_amount(sub_amount, first_era)); + assert_eq!(vec.0.len(), 1); + assert_eq!( + vec.0[0], + DummyEraAmount::new(init_amount - sub_amount, first_era), + "Only single entry and it should be updated." + ); + + // 2nd scenario - subtract everything (and more - underflow!) from the current era, causing full removal. Should cleanup the vector. + assert_ok!(vec.subtract_amount(init_amount * 2, first_era)); + assert!(vec.0.is_empty(), "Full removal should cleanup the vector."); +} + +#[test] +fn sparse_bounded_amount_era_vec_subtract_amount_advanced_consecutive_works() { + get_u32_type!(MaxLen, 5); + let mut vec = SparseBoundedAmountEraVec::::new(); + + // 1st scenario - two entries, consecutive eras, subtract from the second era. + // Only the second entry should be updated. + let (first_era, second_era) = (1, 2); + let (first_amount, second_amount) = (19, 23); + assert_ok!(vec.add_amount(first_amount, first_era)); + assert_ok!(vec.add_amount(second_amount, second_era)); + + let sub_amount = 3; + assert_ok!(vec.subtract_amount(sub_amount, second_era)); + assert_eq!(vec.0.len(), 2); + assert_eq!( + vec.0[0], + DummyEraAmount::new(first_amount, first_era), + "First entry should remain unchanged." + ); + assert_eq!( + vec.0[1], + DummyEraAmount::new(first_amount + second_amount - sub_amount, second_era), + "Second entry should have it's amount reduced by the subtracted amount." + ); + + // 2nd scenario - two entries, consecutive eras, subtract from the first era. + // Both the first and second entry should be updated. + assert_ok!(vec.subtract_amount(sub_amount, first_era)); + assert_eq!(vec.0.len(), 2); + assert_eq!( + vec.0[0], + DummyEraAmount::new(first_amount - sub_amount, first_era), + "First entry is updated since it was specified." + ); + assert_eq!( + vec.0[1], + DummyEraAmount::new(first_amount + second_amount - sub_amount * 2, second_era), + "Second entry is updated because it comes AFTER the first one - same applies to all future entries." + ); + + // 3rd scenario - three entries, consecutive eras, subtract from the second era. + // Only second and third entry should be updated. First one should remain unchanged. + let third_era = 3; + let third_amount = 29; + assert_ok!(vec.add_amount(third_amount, third_era)); + assert_ok!(vec.subtract_amount(sub_amount, second_era)); + assert_eq!(vec.0.len(), 3); + assert_eq!( + vec.0[0], + DummyEraAmount::new(first_amount - sub_amount, first_era), + "First entry should remain unchanged, compared to previous scenario." + ); + assert_eq!( + vec.0[1], + DummyEraAmount::new(first_amount + second_amount - sub_amount * 3, second_era), + "Second entry should be reduced by the subtracted amount, compared to previous scenario." + ); + assert_eq!( + vec.0[2], + DummyEraAmount::new( + first_amount + second_amount + third_amount - sub_amount * 3, + third_era + ), + "Same as for the second entry." + ); +} + +#[test] +fn sparse_bounded_amount_era_vec_subtract_amount_advanced_non_consecutive_works() { + get_u32_type!(MaxLen, 5); + let mut vec = SparseBoundedAmountEraVec::::new(); + + // 1st scenario - two entries, non-consecutive eras, subtract from the mid era. + // Only the second entry should be updated but a new entry should be created. + let (first_era, second_era) = (1, 5); + let (first_amount, second_amount) = (19, 23); + assert_ok!(vec.add_amount(first_amount, first_era)); + assert_ok!(vec.add_amount(second_amount, second_era)); + + let sub_amount = 3; + let mid_era = second_era - 1; + assert_ok!(vec.subtract_amount(sub_amount, mid_era)); + assert_eq!(vec.0.len(), 3); + assert_eq!( + vec.0[0], + DummyEraAmount::new(first_amount, first_era), + "No impact on the first entry expected." + ); + assert_eq!( + vec.0[1], + DummyEraAmount::new(first_amount - sub_amount, mid_era), + "Newly created entry should be equal to the first amount, minus what was subtracted." + ); + assert_eq!( + vec.0[2], + DummyEraAmount::new(vec.0[1].amount + second_amount, second_era), + "Previous 'second' entry should be total added minus the subtracted amount." + ); + + // 2nd scenario - fully unlock the mid-entry to create a zero entry. + assert_ok!(vec.subtract_amount(vec.0[1].amount, mid_era)); + assert_eq!(vec.0.len(), 3); + assert_eq!( + vec.0[0], + DummyEraAmount::new(first_amount, first_era), + "No impact on the first entry expected." + ); + assert_eq!( + vec.0[1], + DummyEraAmount::new(0, mid_era), + "Zero entry should be kept since it's in between two non-zero entries." + ); + assert_eq!( + vec.0[2], + DummyEraAmount::new(second_amount, second_era), + "Only the second staked amount should remain since everything else was unstaked." + ); + + // 3rd scenario - create an additional non-zero chunk as prep for the next scenario. + let pre_mid_era = mid_era - 1; + assert!(pre_mid_era > first_era, "Sanity check."); + assert_ok!(vec.subtract_amount(sub_amount, pre_mid_era)); + assert_eq!(vec.0.len(), 4); + assert_eq!( + vec.0[1], + DummyEraAmount::new(first_amount - sub_amount, pre_mid_era), + "Newly created entry, derives it's initial value from the first entry." + ); + assert_eq!( + vec.0[2], + DummyEraAmount::new(0, mid_era), + "Zero entry should be kept at this point since it's still between two non-zero entries." + ); + assert_eq!( + vec.0[3], + DummyEraAmount::new(second_amount - sub_amount, second_era), + "Last entry should be further reduced by the newly subtracted amount." + ); + + // 4th scenario - create an additional zero entry, but ensure it's cleaned up correctly. + let final_sub_amount = vec.0[1].amount; + assert_ok!(vec.subtract_amount(final_sub_amount, pre_mid_era)); + assert_eq!(vec.0.len(), 3); + assert_eq!( + vec.0[0], + DummyEraAmount::new(first_amount, first_era), + "First entry should still remain unchanged." + ); + assert_eq!( + vec.0[1], + DummyEraAmount::new(0, pre_mid_era), + "The older zero entry should consume the newer ones, hence the pre_mid_era usage" + ); + assert_eq!( + vec.0[2], + DummyEraAmount::new(second_amount - sub_amount - final_sub_amount, second_era), + "Last entry should be further reduced by the newly subtracted amount." + ); +} + #[test] fn protocol_state_default() { let protoc_state = ProtocolState::::default(); @@ -46,11 +317,13 @@ fn protocol_state_default() { fn account_ledger_default() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let acc_ledger = AccountLedger::::default(); + get_u32_type!(StakingDummy, 8); + let acc_ledger = + AccountLedger::::default(); assert!(acc_ledger.is_empty()); - assert!(acc_ledger.locked_amount().is_zero()); - assert!(acc_ledger.era().is_zero()); + assert!(acc_ledger.active_locked_amount().is_zero()); + assert!(acc_ledger.lock_era().is_zero()); assert!(acc_ledger.latest_locked_chunk().is_none()); } @@ -58,24 +331,28 @@ fn account_ledger_default() { fn account_ledger_add_lock_amount_works() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); let mut acc_ledger = - AccountLedger::::default(); + AccountLedger::::default(); // First step, sanity checks let first_era = 1; - assert!(acc_ledger.locked_amount().is_zero()); + assert!(acc_ledger.active_locked_amount().is_zero()); + assert!(acc_ledger.total_locked_amount().is_zero()); assert!(acc_ledger.add_lock_amount(0, first_era).is_ok()); - assert!(acc_ledger.locked_amount().is_zero()); + assert!(acc_ledger.active_locked_amount().is_zero()); // Adding lock value works as expected let init_amount = 20; assert!(acc_ledger.add_lock_amount(init_amount, first_era).is_ok()); - assert_eq!(acc_ledger.locked_amount(), init_amount); - assert_eq!(acc_ledger.era(), first_era); + assert_eq!(acc_ledger.active_locked_amount(), init_amount); + assert_eq!(acc_ledger.total_locked_amount(), init_amount); + assert_eq!(acc_ledger.lock_era(), first_era); assert!(!acc_ledger.is_empty()); + assert_eq!(acc_ledger.locked.0.len(), 1); assert_eq!( acc_ledger.latest_locked_chunk(), - Some(&LockedChunk:: { + Some(&LockedChunk { amount: init_amount, era: first_era, }) @@ -84,22 +361,626 @@ fn account_ledger_add_lock_amount_works() { // Add to the same era let addition = 7; assert!(acc_ledger.add_lock_amount(addition, first_era).is_ok()); - assert_eq!(acc_ledger.locked_amount(), init_amount + addition); - assert_eq!(acc_ledger.era(), first_era); + assert_eq!(acc_ledger.active_locked_amount(), init_amount + addition); + assert_eq!(acc_ledger.total_locked_amount(), init_amount + addition); + assert_eq!(acc_ledger.lock_era(), first_era); + assert_eq!(acc_ledger.locked.0.len(), 1); + + // Adding to previous era should fail + assert_eq!( + acc_ledger.add_lock_amount(addition, first_era - 1), + Err(SparseBoundedError::OldEra) + ); // Add up to storage limit for i in 2..=LockedDummy::get() { assert!(acc_ledger.add_lock_amount(addition, first_era + i).is_ok()); assert_eq!( - acc_ledger.locked_amount(), + acc_ledger.active_locked_amount(), init_amount + addition * i as u128 ); - assert_eq!(acc_ledger.era(), first_era + i); + assert_eq!(acc_ledger.lock_era(), first_era + i); + assert_eq!(acc_ledger.locked.0.len(), i as usize); } // Any further additions should fail due to exhausting bounded storage capacity + let acc_ledger_clone = acc_ledger.clone(); + assert_eq!( + acc_ledger.add_lock_amount(addition, acc_ledger.lock_era() + 1), + Err(SparseBoundedError::NoCapacity) + ); + assert_eq!(acc_ledger, acc_ledger_clone); +} + +#[test] +fn account_ledger_subtract_lock_amount_basic_usage_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Sanity check scenario + // Cannot reduce if there is nothing locked, should be a noop + assert!(acc_ledger.subtract_lock_amount(0, 1).is_ok()); + assert!(acc_ledger.subtract_lock_amount(10, 1).is_ok()); + assert!(acc_ledger.locked.0.len().is_zero()); + assert!(acc_ledger.is_empty()); + + // First basic scenario + // Add some lock amount, then reduce it for the same era + let first_era = 1; + let first_lock_amount = 19; + let unlock_amount = 7; assert!(acc_ledger - .add_lock_amount(addition, acc_ledger.era() + 1) - .is_err()); - assert!(!acc_ledger.is_empty()); + .add_lock_amount(first_lock_amount, first_era) + .is_ok()); + assert!(acc_ledger + .subtract_lock_amount(unlock_amount, first_era) + .is_ok()); + assert_eq!(acc_ledger.locked.0.len(), 1); + assert_eq!( + acc_ledger.total_locked_amount(), + first_lock_amount - unlock_amount + ); + assert_eq!( + acc_ledger.active_locked_amount(), + first_lock_amount - unlock_amount + ); + assert_eq!(acc_ledger.unlocking_amount(), 0); + + // Second basic scenario + // Reduce the lock from the era which isn't latest in the vector + let first_lock_amount = first_lock_amount - unlock_amount; + let second_lock_amount = 31; + let second_era = 2; + assert!(acc_ledger + .add_lock_amount(second_lock_amount - first_lock_amount, second_era) + .is_ok()); + assert_eq!(acc_ledger.active_locked_amount(), second_lock_amount); + assert_eq!(acc_ledger.locked.0.len(), 2); + + // Subtract from the first era and verify state is as expected + assert!(acc_ledger + .subtract_lock_amount(unlock_amount, first_era) + .is_ok()); + assert_eq!(acc_ledger.locked.0.len(), 2); + assert_eq!( + acc_ledger.active_locked_amount(), + second_lock_amount - unlock_amount + ); + assert_eq!( + acc_ledger.locked.0[0].amount, + first_lock_amount - unlock_amount + ); + assert_eq!( + acc_ledger.locked.0[1].amount, + second_lock_amount - unlock_amount + ); + + // Third basic scenario + // Reduce the the latest era, don't expect the first one to change + assert!(acc_ledger + .subtract_lock_amount(unlock_amount, second_era) + .is_ok()); + assert_eq!(acc_ledger.locked.0.len(), 2); + assert_eq!( + acc_ledger.active_locked_amount(), + second_lock_amount - unlock_amount * 2 + ); + assert_eq!( + acc_ledger.locked.0[0].amount, + first_lock_amount - unlock_amount + ); + assert_eq!( + acc_ledger.locked.0[1].amount, + second_lock_amount - unlock_amount * 2 + ); +} + +#[test] +fn account_ledger_subtract_lock_amount_overflow_fails() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + let first_lock_amount = 17 * 19; + let era = 1; + let unlock_amount = 5; + assert!(acc_ledger.add_lock_amount(first_lock_amount, era).is_ok()); + for idx in 1..=LockedDummy::get() { + assert!(acc_ledger.subtract_lock_amount(unlock_amount, idx).is_ok()); + assert_eq!(acc_ledger.locked.0.len(), idx as usize); + assert_eq!( + acc_ledger.active_locked_amount(), + first_lock_amount - unlock_amount * idx as u128 + ); + } + + // Updating existing lock should still work + let locked_snapshot = acc_ledger.locked.0.clone(); + for i in 1..10 { + assert!(acc_ledger + .subtract_lock_amount(unlock_amount, LockedDummy::get()) + .is_ok()); + assert_eq!(acc_ledger.locked.0.len(), LockedDummy::get() as usize); + + let last_idx = LockedDummy::get() as usize - 1; + assert_eq!( + &acc_ledger.locked.0[0..last_idx], + &locked_snapshot[0..last_idx] + ); + assert_eq!( + acc_ledger.locked.0[last_idx].amount as u128 + unlock_amount * i, + locked_snapshot[last_idx].amount + ); + } + + // Attempt to add additional chunks should fail, and is a noop. + let acc_ledger_clone = acc_ledger.clone(); + assert_eq!( + acc_ledger.subtract_lock_amount(unlock_amount, LockedDummy::get() + 1), + Err(SparseBoundedError::NoCapacity) + ); + assert_eq!(acc_ledger, acc_ledger_clone); +} + +#[test] +fn account_ledger_subtract_lock_amount_advanced_example_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Prepare an example where we have two non-consecutive entries, and we unlock in the era right before the second entry. + // This covers a scenario where user has called `lock` in the current era, + // creating an entry for the next era, and then decides to immediately unlock a portion of the locked amount. + let first_lock_amount = 17; + let second_lock_amount = 23; + let first_era = 1; + let second_era = 5; + let unlock_era = second_era - 1; + let unlock_amount = 5; + assert!(acc_ledger + .add_lock_amount(first_lock_amount, first_era) + .is_ok()); + assert!(acc_ledger + .add_lock_amount(second_lock_amount, second_era) + .is_ok()); + assert_eq!(acc_ledger.locked.0.len(), 2); + + assert!(acc_ledger + .subtract_lock_amount(unlock_amount, unlock_era) + .is_ok()); + assert_eq!( + acc_ledger.active_locked_amount(), + first_lock_amount + second_lock_amount - unlock_amount + ); + + // Check entries in more detail + assert_eq!(acc_ledger.locked.0.len(), 3); + assert_eq!(acc_ledger.locked.0[0].amount, first_lock_amount,); + assert_eq!( + acc_ledger.locked.0[2].amount, + first_lock_amount + second_lock_amount - unlock_amount + ); + // Verify the new entry is as expected + assert_eq!( + acc_ledger.locked.0[1].amount, + first_lock_amount - unlock_amount + ); + assert_eq!(acc_ledger.locked.0[1].era, unlock_era); +} + +#[test] +fn account_ledger_subtract_lock_amount_with_only_one_locked_chunk() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Scenario: user locks for era 2 while era 1 is active, immediately followed by unlock call. + // Locked amount should be updated for the next era, but active locked amount should be unchanged (zero). + let lock_amount = 17; + let unlock_amount = 5; + let lock_era = 2; + let unlock_era = 1; + assert!(acc_ledger.add_lock_amount(lock_amount, lock_era).is_ok()); + assert!(acc_ledger + .subtract_lock_amount(unlock_amount, unlock_era) + .is_ok()); + + assert_eq!(acc_ledger.locked.0.len(), 1); + assert_eq!( + acc_ledger.locked.0[0], + LockedChunk { + amount: lock_amount - unlock_amount, + era: lock_era, + } + ); +} + +#[test] +fn account_ledger_subtract_lock_amount_correct_zero_cleanup() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Ensure that zero entries are cleaned up correctly when required. + // There are a couple of distinct scenarios: + // 1. There is only one entry, and it's zero. The vector should be cleared & empty. + // 2. There are multiple entries, and the last one is zero. It's valid since it marks when someone fully unlocked. + // 3. Zero entry can exist in between two non-zero entries (not covered in this UT). + + // 1st scenario (A) - only one zero entry, unlock is in the same era + let lock_amount = 17; + let lock_era = 2; + assert!(acc_ledger.add_lock_amount(lock_amount, lock_era).is_ok()); + assert!(acc_ledger + .subtract_lock_amount(lock_amount, lock_era) + .is_ok()); + assert!(acc_ledger.locked.0.is_empty()); + + // 1st scenario (B) - only one zero entry, unlock is in the previous era + assert!(acc_ledger.add_lock_amount(lock_amount, lock_era).is_ok()); + assert!(acc_ledger + .subtract_lock_amount(lock_amount, lock_era - 1) + .is_ok()); + assert!(acc_ledger.locked.0.is_empty()); + + // 2nd scenario - last entry is zero + let first_lock_era = 3; + let second_lock_era = 11; + let unlock_era = second_lock_era + 2; + assert!(acc_ledger + .add_lock_amount(lock_amount, first_lock_era) + .is_ok()); + assert!(acc_ledger + .add_lock_amount(lock_amount, second_lock_era) + .is_ok()); + // Following should add new entry, to mark when the user fully unlocked + assert!(acc_ledger + .subtract_lock_amount(acc_ledger.active_locked_amount(), unlock_era) + .is_ok()); + assert_eq!(acc_ledger.locked.0.len(), 3); + assert!(acc_ledger.active_locked_amount().is_zero()); +} + +#[test] +fn account_ledger_subtract_lock_amount_zero_entry_between_two_non_zero() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + let (first_lock_amount, second_lock_amount, third_lock_amount) = (17, 23, 29); + let (first_lock_era, second_lock_era, third_lock_era) = (1, 3, 7); + + // Prepare scenario with 3 locked chunks + assert!(acc_ledger + .add_lock_amount(first_lock_amount, first_lock_era) + .is_ok()); + assert!(acc_ledger + .add_lock_amount(second_lock_amount, second_lock_era) + .is_ok()); + assert!(acc_ledger + .add_lock_amount(third_lock_amount, third_lock_era) + .is_ok()); + assert_eq!(acc_ledger.locked.0.len(), 3); + + // Unlock everything for the era right before the latest chunk era + // This should result in scenario like: + // [17, 17 + 23, 0, 29] + assert!(acc_ledger + .subtract_lock_amount(first_lock_amount + second_lock_amount, third_lock_era - 1) + .is_ok()); + assert_eq!(acc_ledger.locked.0.len(), 4); + assert_eq!(acc_ledger.active_locked_amount(), third_lock_amount); + assert_eq!( + acc_ledger.locked.0[0], + LockedChunk { + amount: first_lock_amount, + era: first_lock_era + } + ); + assert_eq!( + acc_ledger.locked.0[1], + LockedChunk { + amount: first_lock_amount + second_lock_amount, + era: second_lock_era + } + ); + assert_eq!( + acc_ledger.locked.0[2], + LockedChunk { + amount: 0, + era: third_lock_era - 1 + } + ); + assert_eq!( + acc_ledger.locked.0[3], + LockedChunk { + amount: third_lock_amount, + era: third_lock_era + } + ); +} + +#[test] +fn account_ledger_subtract_lock_amount_consecutive_zeroes_merged() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Prepare scenario with 3 locked chunks, where the middle one is zero + let lock_amount = 61; + let last_era = 11; + assert!(acc_ledger.add_lock_amount(lock_amount, 2).is_ok()); + assert!(acc_ledger.subtract_lock_amount(lock_amount, 5).is_ok()); + assert!(acc_ledger.add_lock_amount(lock_amount, last_era).is_ok()); + let second_chunk = acc_ledger.locked.0[1]; + + // Unlock everything in the era right before the latest chunk era, but that chunk should not persist + // [61, 0, 61] --> [61, 0, 0, 61] shouldn't happen since the 2nd zero is redundant. + assert!(acc_ledger + .subtract_lock_amount(lock_amount, last_era - 1) + .is_ok()); + assert_eq!(acc_ledger.locked.0.len(), 2); + assert_eq!(acc_ledger.locked.0[1], second_chunk); +} + +#[test] +fn account_ledger_add_unlocking_chunk_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Sanity check scenario + // Cannot reduce if there is nothing locked, should be a noop + assert!(acc_ledger.add_unlocking_chunk(0, 0).is_ok()); + assert!(acc_ledger.unlocking.len().is_zero()); + assert!(acc_ledger.is_empty()); + + // Basic scenario + let unlock_amount = 17; + let block_number = 29; + assert!(acc_ledger + .add_unlocking_chunk(unlock_amount, block_number) + .is_ok()); + assert_eq!( + acc_ledger.unlocking, + vec![UnlockingChunk { + amount: unlock_amount, + unlock_block: block_number + }] + ); + assert_eq!(acc_ledger.unlocking_amount(), unlock_amount); + + // Unlock additional amount in the same block + assert!(acc_ledger + .add_unlocking_chunk(unlock_amount, block_number) + .is_ok()); + assert_eq!( + acc_ledger.unlocking, + vec![UnlockingChunk { + amount: unlock_amount * 2, + unlock_block: block_number + }] + ); + assert_eq!(acc_ledger.unlocking_amount(), unlock_amount * 2); + + // Add unlocking chunks up to vector capacity + let mut total_unlocking = acc_ledger.unlocking_amount(); + for i in 2..=UnlockingDummy::get() { + let new_unlock_amount = unlock_amount + i as u128; + assert!(acc_ledger + .add_unlocking_chunk(new_unlock_amount, block_number + i as u64) + .is_ok()); + total_unlocking += new_unlock_amount; + assert_eq!(acc_ledger.unlocking_amount(), total_unlocking); + assert_eq!( + acc_ledger.unlocking[i as usize - 1].amount, + new_unlock_amount + ); + } + + // Any further addition should fail, resulting in a noop + let acc_ledger_snapshot = acc_ledger.clone(); + assert_eq!( + acc_ledger.add_unlocking_chunk(1, block_number + UnlockingDummy::get() as u64 + 1), + Err(SparseBoundedError::NoCapacity) + ); + assert_eq!(acc_ledger, acc_ledger_snapshot); +} + +#[test] +fn active_stake_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Sanity check + assert!(acc_ledger.active_stake(0).is_zero()); + assert!(acc_ledger.active_stake(1).is_zero()); + + // Period matches + let amount = 29; + let period = 5; + acc_ledger.staked = SparseBoundedAmountEraVec( + BoundedVec::try_from(vec![StakeChunk { amount, era: 1 }]) + .expect("Only one chunk so creation should succeed."), + ); + acc_ledger.staked_period = Some(period); + assert_eq!(acc_ledger.active_stake(period), amount); + + // Period doesn't match + assert!(acc_ledger.active_stake(period - 1).is_zero()); + assert!(acc_ledger.active_stake(period + 1).is_zero()); +} + +#[test] +fn unlockable_amount_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Sanity check scenario + assert!(acc_ledger.unlockable_amount(0).is_zero()); + + // Nothing is staked + let lock_amount = 29; + let lock_era = 3; + assert!(acc_ledger.add_lock_amount(lock_amount, lock_era).is_ok()); + assert_eq!(acc_ledger.unlockable_amount(0), lock_amount); + + // Some amount is staked, period matches + let stake_period = 5; + let stake_amount = 17; + acc_ledger.staked = SparseBoundedAmountEraVec( + BoundedVec::try_from(vec![StakeChunk { + amount: stake_amount, + era: lock_era, + }]) + .expect("Only one chunk so creation should succeed."), + ); + acc_ledger.staked_period = Some(stake_period); + assert_eq!( + acc_ledger.unlockable_amount(stake_period), + lock_amount - stake_amount + ); + + // Period doesn't match + assert_eq!(acc_ledger.unlockable_amount(stake_period - 1), lock_amount); + assert_eq!(acc_ledger.unlockable_amount(stake_period + 2), lock_amount); + + // Absurd example, for the sake of completeness - staked without any lock + acc_ledger.locked.0 = Default::default(); + assert!(acc_ledger.unlockable_amount(stake_period).is_zero()); + assert!(acc_ledger.unlockable_amount(stake_period - 2).is_zero()); + assert!(acc_ledger.unlockable_amount(stake_period + 1).is_zero()); +} + +#[test] +fn claim_unlocked_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Sanity check scenario + assert!(acc_ledger.claim_unlocked(0).is_zero()); + + // Add a chunk, assert it can be claimed correctly + let amount = 19; + let block_number = 1; + assert_ok!(acc_ledger.add_unlocking_chunk(amount, block_number)); + assert!(acc_ledger.claim_unlocked(0).is_zero()); + assert_eq!(acc_ledger.claim_unlocked(block_number), amount); + assert!(acc_ledger.unlocking.is_empty()); + + // Add multiple chunks, assert claim works correctly + let (amount1, amount2, amount3) = (7, 13, 19); + let (block1, block2, block3) = (1, 3, 5); + + // Prepare unlocking chunks + assert_ok!(acc_ledger.add_unlocking_chunk(amount1, block1)); + assert_ok!(acc_ledger.add_unlocking_chunk(amount2, block2)); + assert_ok!(acc_ledger.add_unlocking_chunk(amount3, block3)); + + // Only claim 1 chunk + assert_eq!(acc_ledger.claim_unlocked(block1 + 1), amount1); + assert_eq!(acc_ledger.unlocking.len(), 2); + + // Claim remaining two chunks + assert_eq!(acc_ledger.claim_unlocked(block3 + 1), amount2 + amount3); + assert!(acc_ledger.unlocking.is_empty()); +} + +#[test] +fn consume_unlocking_chunks_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Sanity check scenario + assert!(acc_ledger.consume_unlocking_chunks().is_zero()); + + // Add multiple chunks, cal should return correct amount + let (amount1, amount2) = (7, 13); + assert_ok!(acc_ledger.add_unlocking_chunk(amount1, 1)); + assert_ok!(acc_ledger.add_unlocking_chunk(amount2, 2)); + + assert_eq!(acc_ledger.consume_unlocking_chunks(), amount1 + amount2); + assert!(acc_ledger.unlocking.is_empty()); +} + +#[test] +fn era_info_manipulation_works() { + let mut era_info = EraInfo::default(); + + // Sanity check + assert!(era_info.total_locked.is_zero()); + assert!(era_info.active_era_locked.is_zero()); + assert!(era_info.unlocking.is_zero()); + + // Basic add lock + let lock_amount = 7; + era_info.add_locked(lock_amount); + assert_eq!(era_info.total_locked, lock_amount); + era_info.add_locked(lock_amount); + assert_eq!(era_info.total_locked, lock_amount * 2); + + // Basic unlocking started + let unlock_amount = 2; + era_info.total_locked = 17; + era_info.active_era_locked = 13; + let era_info_snapshot = era_info; + + // First unlock & checks + era_info.unlocking_started(unlock_amount); + assert_eq!( + era_info.total_locked, + era_info_snapshot.total_locked - unlock_amount + ); + assert_eq!( + era_info.active_era_locked, + era_info_snapshot.active_era_locked - unlock_amount + ); + assert_eq!(era_info.unlocking, unlock_amount); + + // Second unlock and checks + era_info.unlocking_started(unlock_amount); + assert_eq!( + era_info.total_locked, + era_info_snapshot.total_locked - unlock_amount * 2 + ); + assert_eq!( + era_info.active_era_locked, + era_info_snapshot.active_era_locked - unlock_amount * 2 + ); + assert_eq!(era_info.unlocking, unlock_amount * 2); + + // Claim unlocked chunks + let old_era_info = era_info.clone(); + era_info.unlocking_removed(1); + assert_eq!(era_info.unlocking, old_era_info.unlocking - 1); + assert_eq!(era_info.active_era_locked, old_era_info.active_era_locked); } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 958cd496d5..59321fe311 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -16,25 +16,26 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use frame_support::{pallet_prelude::*, traits::Currency, BoundedVec}; +use frame_support::{pallet_prelude::*, BoundedVec}; use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; -use sp_runtime::traits::{AtLeast32BitUnsigned, Zero}; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, Zero}, + Saturating, +}; + +use astar_primitives::Balance; use crate::pallet::Config; // TODO: instead of using `pub` visiblity for fields, either use `pub(crate)` or add dedicated methods for accessing them. -/// The balance type used by the currency system. -pub type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; - /// Convenience type for `AccountLedger` usage. pub type AccountLedgerFor = AccountLedger< - BalanceOf, BlockNumberFor, ::MaxLockedChunks, ::MaxUnlockingChunks, + ::MaxStakingChunks, >; /// Era number type @@ -44,6 +45,184 @@ pub type PeriodNumber = u32; /// Dapp Id type pub type DAppId = u16; +// TODO: perhaps this trait is not needed and instead of having 2 separate '___Chunk' types, we can have just one? +/// Trait for types that can be used as a pair of amount & era. +pub trait AmountEraPair: MaxEncodedLen + Default + Copy { + /// Balance amount used somehow during the accompanied era. + fn get_amount(&self) -> Balance; + /// Era acting as timestamp for the accompanied amount. + fn get_era(&self) -> EraNumber; + // Sets the era to the specified value. + fn set_era(&mut self, era: EraNumber); + /// Increase the total amount by the specified increase, saturating at the maximum value. + fn saturating_accrue(&mut self, increase: Balance); + /// Reduce the total amount by the specified reduction, saturating at the minumum value. + fn saturating_reduce(&mut self, reduction: Balance); +} + +/// Simple enum representing errors possible when using sparse bounded vector. +#[derive(Debug, PartialEq, Eq)] +pub enum SparseBoundedError { + /// Old era values cannot be added. + OldEra, + /// Bounded storage capacity exceeded. + NoCapacity, +} + +/// Helper struct for easier manipulation of sparse pairs. +/// +/// The struct guarantees the following: +/// ----------------------------------- +/// 1. The vector is always sorted by era, in ascending order. +/// 2. There are no two consecutive zero chunks. +/// 3. There are no two chunks with the same era. +/// 4. The vector is always bounded by the specified maximum length. +/// +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(ML))] +pub struct SparseBoundedAmountEraVec>(pub BoundedVec); + +impl SparseBoundedAmountEraVec +where + P: AmountEraPair, + ML: Get, +{ + /// Create new instance + pub fn new() -> Self { + Self(BoundedVec::::default()) + } + + /// Places the specified pair into the vector, in an appropriate place. + /// + /// There are two possible successful scenarios: + /// 1. If entry for the specified era already exists, it's updated. + /// [(100, 1)] -- add_amount(50, 1) --> [(150, 1)] + /// + /// 2. If entry for the specified era doesn't exist, it's created and insertion is attempted. + /// [(100, 1)] -- add_amount(50, 2) --> [(100, 1), (150, 2)] + /// + /// In case vector has no more capacity, error is returned, and whole operation is a noop. + pub fn add_amount( + &mut self, + amount: Balance, + era: EraNumber, + ) -> Result<(), SparseBoundedError> { + if amount.is_zero() { + return Ok(()); + } + + let mut chunk = if let Some(&chunk) = self.0.last() { + ensure!(chunk.get_era() <= era, SparseBoundedError::OldEra); + chunk + } else { + P::default() + }; + + chunk.saturating_accrue(amount); + + if chunk.get_era() == era && !self.0.is_empty() { + if let Some(last) = self.0.last_mut() { + *last = chunk; + } + } else { + chunk.set_era(era); + self.0 + .try_push(chunk) + .map_err(|_| SparseBoundedError::NoCapacity)?; + } + + Ok(()) + } + + /// Subtracts the specified amount of the total locked amount, if possible. + /// + /// There are multiple success scenarios/rules: + /// 1. If entry for the specified era already exists, it's updated. + /// a. [(100, 1)] -- subtract_amount(50, 1) --> [(50, 1)] + /// b. [(100, 1)] -- subtract_amount(100, 1) --> [] + /// + /// 2. All entries following the specified era will have their amount reduced as well. + /// [(100, 1), (150, 2)] -- subtract_amount(50, 1) --> [(50, 1), (100, 2)] + /// + /// 3. If entry for the specified era doesn't exist, it's created and insertion is attempted. + /// [(100, 1), (200, 3)] -- subtract_amount(100, 2) --> [(100, 1), (0, 2), (100, 3)] + /// + /// 4. No two consecutive zero chunks are allowed. + /// [(100, 1), (0, 2), (100, 3), (200, 4)] -- subtract_amount(100, 3) --> [(100, 1), (0, 2), (100, 4)] + /// + /// In case vector has no more capacity, error is returned, and whole operation is a noop. + pub fn subtract_amount( + &mut self, + amount: Balance, + era: EraNumber, + ) -> Result<(), SparseBoundedError> { + if amount.is_zero() || self.0.is_empty() { + return Ok(()); + } + // TODO: this method can surely be optimized (avoid too many iters) but focus on that later, + // when it's all working fine, and we have good test coverage. + // TODO2: realistically, the only eligible eras are the last two ones (current & previous). Code could be optimized for that. + + // Find the most relevant locked chunk for the specified era + let index = if let Some(index) = self.0.iter().rposition(|&chunk| chunk.get_era() <= era) { + index + } else { + // Covers scenario when there's only 1 chunk for the next era, and remove it if it's zero. + self.0 + .iter_mut() + .for_each(|chunk| chunk.saturating_reduce(amount)); + self.0.retain(|chunk| !chunk.get_amount().is_zero()); + return Ok(()); + }; + + // Update existing or insert a new chunk + let mut inner = self.0.clone().into_inner(); + let relevant_chunk_index = if inner[index].get_era() == era { + inner[index].saturating_reduce(amount); + index + } else { + // Take the most relevant chunk for the desired era, + // and use it as 'base' for the new chunk. + let mut chunk = inner[index]; + chunk.saturating_reduce(amount); + chunk.set_era(era); + + // Insert the new chunk AFTER the previous 'most relevant chunk'. + // The chunk we find is always either for the requested era, or some era before it. + inner.insert(index + 1, chunk); + index + 1 + }; + + // Update all chunks after the relevant one, and remove eligible zero chunks + inner[relevant_chunk_index + 1..] + .iter_mut() + .for_each(|chunk| chunk.saturating_reduce(amount)); + + // Prune all consecutive zero chunks + let mut new_inner = Vec::

::new(); + new_inner.push(inner[0]); + for i in 1..inner.len() { + if inner[i].get_amount().is_zero() && inner[i - 1].get_amount().is_zero() { + continue; + } else { + new_inner.push(inner[i]); + } + } + + inner = new_inner; + + // Cleanup if only one zero chunk exists + if inner.len() == 1 && inner[0].get_amount().is_zero() { + inner.pop(); + } + + // Update `locked` to the new vector + self.0 = BoundedVec::try_from(inner).map_err(|_| SparseBoundedError::NoCapacity)?; + + Ok(()) + } +} + /// Distinct period types in dApp staking protocol. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub enum PeriodType { @@ -125,17 +304,14 @@ pub struct DAppInfo { /// How much was locked in a specific era #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct LockedChunk { +pub struct LockedChunk { #[codec(compact)] pub amount: Balance, #[codec(compact)] pub era: EraNumber, } -impl Default for LockedChunk -where - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, -{ +impl Default for LockedChunk { fn default() -> Self { Self { amount: Balance::zero(), @@ -144,23 +320,36 @@ where } } -// TODO: would users get better UX if we kept using eras? Using blocks is more precise though. +impl AmountEraPair for LockedChunk { + fn get_amount(&self) -> Balance { + self.amount + } + fn get_era(&self) -> EraNumber { + self.era + } + fn set_era(&mut self, era: EraNumber) { + self.era = era; + } + fn saturating_accrue(&mut self, increase: Balance) { + self.amount.saturating_accrue(increase); + } + fn saturating_reduce(&mut self, reduction: Balance) { + self.amount.saturating_reduce(reduction); + } +} + /// How much was unlocked in some block. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct UnlockingChunk< - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, -> { +pub struct UnlockingChunk { #[codec(compact)] pub amount: Balance, #[codec(compact)] pub unlock_block: BlockNumber, } -impl Default for UnlockingChunk +impl Default for UnlockingChunk where - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, { fn default() -> Self { Self { @@ -170,108 +359,241 @@ where } } +/// Information about how much was staked in a specific era. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub struct StakeChunk { + #[codec(compact)] + pub amount: Balance, + #[codec(compact)] + pub era: EraNumber, +} + +impl Default for StakeChunk { + fn default() -> Self { + Self { + amount: Balance::zero(), + era: EraNumber::zero(), + } + } +} + +impl AmountEraPair for StakeChunk { + fn get_amount(&self) -> Balance { + self.amount + } + fn get_era(&self) -> EraNumber { + self.era + } + fn set_era(&mut self, era: EraNumber) { + self.era = era; + } + fn saturating_accrue(&mut self, increase: Balance) { + self.amount.saturating_accrue(increase); + } + fn saturating_reduce(&mut self, reduction: Balance) { + self.amount.saturating_reduce(reduction); + } +} + /// General info about user's stakes #[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] -#[scale_info(skip_type_params(LockedLen, UnlockingLen))] +#[scale_info(skip_type_params(LockedLen, UnlockingLen, StakedLen))] pub struct AccountLedger< - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, LockedLen: Get, UnlockingLen: Get, + StakedLen: Get, > { /// How much was staked in each era - pub locked: BoundedVec, LockedLen>, + pub locked: SparseBoundedAmountEraVec, /// How much started unlocking on a certain block - pub unlocking: BoundedVec, UnlockingLen>, - //TODO, make this a compact struct!!! + pub unlocking: BoundedVec, UnlockingLen>, /// How much user had staked in some period - // #[codec(compact)] - pub staked: (Balance, PeriodNumber), + pub staked: SparseBoundedAmountEraVec, + /// Last period in which account had staked. + pub staked_period: Option, } -impl Default - for AccountLedger +impl Default + for AccountLedger where - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, LockedLen: Get, UnlockingLen: Get, + StakedLen: Get, { fn default() -> Self { Self { - locked: BoundedVec::, LockedLen>::default(), - unlocking: BoundedVec::, UnlockingLen>::default(), - staked: (Balance::zero(), 0), + locked: SparseBoundedAmountEraVec(BoundedVec::::default()), + unlocking: BoundedVec::, UnlockingLen>::default(), + staked: SparseBoundedAmountEraVec(BoundedVec::::default()), + staked_period: None, } } } -impl - AccountLedger +impl + AccountLedger where - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, LockedLen: Get, UnlockingLen: Get, + StakedLen: Get, { /// Empty if no locked/unlocking/staked info exists. pub fn is_empty(&self) -> bool { - self.locked.is_empty() && self.unlocking.is_empty() && self.staked.0.is_zero() + self.locked.0.is_empty() && self.unlocking.is_empty() && self.staked.0.is_empty() } /// Returns latest locked chunk if it exists, `None` otherwise - pub fn latest_locked_chunk(&self) -> Option<&LockedChunk> { - self.locked.last() + pub fn latest_locked_chunk(&self) -> Option<&LockedChunk> { + self.locked.0.last() } - /// Returns locked amount. - /// If `zero`, means that associated account hasn't locked any funds. - pub fn locked_amount(&self) -> Balance { + /// Returns active locked amount. + /// If `zero`, means that associated account hasn't got any active locked funds. + pub fn active_locked_amount(&self) -> Balance { self.latest_locked_chunk() .map_or(Balance::zero(), |locked| locked.amount) } + /// Returns unlocking amount. + /// If `zero`, means that associated account hasn't got any unlocking chunks. + pub fn unlocking_amount(&self) -> Balance { + self.unlocking.iter().fold(Balance::zero(), |sum, chunk| { + sum.saturating_add(chunk.amount) + }) + } + + /// Total locked amount by the user. + /// Includes both active locked amount & unlocking amount. + pub fn total_locked_amount(&self) -> Balance { + self.active_locked_amount() + .saturating_add(self.unlocking_amount()) + } + /// Returns latest era in which locked amount was updated or zero in case no lock amount exists - pub fn era(&self) -> EraNumber { + pub fn lock_era(&self) -> EraNumber { self.latest_locked_chunk() .map_or(EraNumber::zero(), |locked| locked.era) } + /// Active staked balance. + /// + /// In case latest stored information is from the past period, active stake is considered to be zero. + pub fn active_stake(&self, active_period: PeriodNumber) -> Balance { + match self.staked_period { + Some(last_staked_period) if last_staked_period == active_period => self + .staked + .0 + .last() + .map_or(Balance::zero(), |chunk| chunk.amount), + _ => Balance::zero(), + } + } + /// Adds the specified amount to the total locked amount, if possible. + /// Caller must ensure that the era matches the next one, not the current one. /// /// If entry for the specified era already exists, it's updated. /// /// If entry for the specified era doesn't exist, it's created and insertion is attempted. /// In case vector has no more capacity, error is returned, and whole operation is a noop. - pub fn add_lock_amount(&mut self, amount: Balance, era: EraNumber) -> Result<(), ()> { + pub fn add_lock_amount( + &mut self, + amount: Balance, + era: EraNumber, + ) -> Result<(), SparseBoundedError> { + self.locked.add_amount(amount, era) + } + + /// Subtracts the specified amount of the total locked amount, if possible. + /// + /// If entry for the specified era already exists, it's updated. + /// + /// If entry for the specified era doesn't exist, it's created and insertion is attempted. + /// In case vector has no more capacity, error is returned, and whole operation is a noop. + pub fn subtract_lock_amount( + &mut self, + amount: Balance, + era: EraNumber, + ) -> Result<(), SparseBoundedError> { + self.locked.subtract_amount(amount, era) + } + + /// Adds the specified amount to the unlocking chunks. + /// + /// If entry for the specified block already exists, it's updated. + /// + /// If entry for the specified block doesn't exist, it's created and insertion is attempted. + /// In case vector has no more capacity, error is returned, and whole operation is a noop. + pub fn add_unlocking_chunk( + &mut self, + amount: Balance, + unlock_block: BlockNumber, + ) -> Result<(), SparseBoundedError> { if amount.is_zero() { return Ok(()); } - let mut locked_chunk = if let Some(&locked_chunk) = self.locked.last() { - locked_chunk - } else { - LockedChunk::default() - }; - - locked_chunk.amount.saturating_accrue(amount); + let idx = self + .unlocking + .binary_search_by(|chunk| chunk.unlock_block.cmp(&unlock_block)); - if locked_chunk.era == era && !self.locked.is_empty() { - if let Some(last) = self.locked.last_mut() { - *last = locked_chunk; + match idx { + Ok(idx) => { + self.unlocking[idx].amount.saturating_accrue(amount); + } + Err(idx) => { + let new_unlocking_chunk = UnlockingChunk { + amount, + unlock_block, + }; + self.unlocking + .try_insert(idx, new_unlocking_chunk) + .map_err(|_| SparseBoundedError::NoCapacity)?; } - } else { - locked_chunk.era = era; - self.locked.try_push(locked_chunk).map_err(|_| ())?; } Ok(()) } + + /// Amount available for unlocking. + pub fn unlockable_amount(&self, current_period: PeriodNumber) -> Balance { + self.active_locked_amount() + .saturating_sub(self.active_stake(current_period)) + } + + /// Claims all of the fully unlocked chunks, and returns the total claimable amount. + pub fn claim_unlocked(&mut self, current_block_number: BlockNumber) -> Balance { + let mut total = Balance::zero(); + + self.unlocking.retain(|chunk| { + if chunk.unlock_block <= current_block_number { + total.saturating_accrue(chunk.amount); + false + } else { + true + } + }); + + total + } + + /// Consumes all of the unlocking chunks, and returns the total amount being unlocked. + pub fn consume_unlocking_chunks(&mut self) -> Balance { + let amount = self.unlocking.iter().fold(Balance::zero(), |sum, chunk| { + sum.saturating_add(chunk.amount) + }); + self.unlocking = Default::default(); + + amount + } } -/// Rewards pool for lock participants & dApps -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] -pub struct RewardInfo { +/// Rewards pool for stakers & dApps +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct RewardInfo { /// Rewards pool for accounts which have locked funds in dApp staking #[codec(compact)] pub participants: Balance, @@ -281,10 +603,10 @@ pub struct RewardInfo { } /// Info about current era, including the rewards, how much is locked, unlocking, etc. -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] -pub struct EraInfo { +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct EraInfo { /// Info about era rewards - pub rewards: RewardInfo, + pub rewards: RewardInfo, /// How much balance is considered to be locked in the current era. /// This value influences the reward distribution. #[codec(compact)] @@ -293,7 +615,27 @@ pub struct EraInfo { /// For rewards, this amount isn't relevant for the current era, but only from the next one. #[codec(compact)] pub total_locked: Balance, - /// How much balance is undergoing unlocking process (still counts into locked amount) + /// How much balance is undergoing unlocking process. + /// This amount still counts into locked amount. #[codec(compact)] pub unlocking: Balance, } + +impl EraInfo { + /// Update with the new amount that has just been locked. + pub fn add_locked(&mut self, amount: Balance) { + self.total_locked.saturating_accrue(amount); + } + + /// Update with the new amount that has just started undergoing the unlocking period. + pub fn unlocking_started(&mut self, amount: Balance) { + self.active_era_locked.saturating_reduce(amount); + self.total_locked.saturating_reduce(amount); + self.unlocking.saturating_accrue(amount); + } + + /// Update with the new amount that has been removed from unlocking. + pub fn unlocking_removed(&mut self, amount: Balance) { + self.unlocking.saturating_reduce(amount); + } +} From 655c28045e6ce704ca9e8c8696367ab5558e68c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Thu, 12 Oct 2023 08:17:57 +0200 Subject: [PATCH 03/14] dApp staking v3 - part 3 (#1036) * dApp staking v3 - PR3 * SingularStakingInfo * SingularStakingInfo tests * Stake work * Stake & lots of tests * TODOs, renaming, improvements * Refactoring & cleanup * More fixes * Rework series * Series tests * Minor adjustment * stake test utils & first test * Stake test * stake extrinsic tests * Era & Period change logic * Update era/period transition in mock & tests * on_init tests & refactoring * Unstake & some minor improvements * Lots of type tests for unstake * More tests * More types test * Testing utils for unstake, some TODOs * Unstake tests * Minor adjustments * Fixes * Additional scenario * Address review comments * Correct unstake amount * Tests --- pallets/dapp-staking-v3/src/lib.rs | 420 +++- pallets/dapp-staking-v3/src/test/mock.rs | 58 +- .../dapp-staking-v3/src/test/testing_utils.rs | 377 +++- pallets/dapp-staking-v3/src/test/tests.rs | 717 +++++-- .../dapp-staking-v3/src/test/tests_types.rs | 1686 ++++++++++++----- pallets/dapp-staking-v3/src/types.rs | 863 +++++++-- 6 files changed, 3381 insertions(+), 740 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index fc5a24d8a3..ba4e77f907 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -89,15 +89,24 @@ pub mod pallet { /// Privileged origin for managing dApp staking pallet. type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; - /// Maximum number of contracts that can be integrated into dApp staking at once. - /// TODO: maybe this can be reworded or improved later on - but we want a ceiling! + /// Length of a standard era in block numbers. #[pallet::constant] - type MaxNumberOfContracts: Get; + type StandardEraLength: Get; - /// Maximum number of locked chunks that can exist per account at a time. - // TODO: should this just be hardcoded to 2? Nothing else makes sense really - current era and next era are required. + /// Length of the `Voting` period in standard eras. + /// Although `Voting` period only consumes one 'era', we still measure its length in standard eras + /// for the sake of simplicity & consistency. #[pallet::constant] - type MaxLockedChunks: Get; + type StandardErasPerVotingPeriod: Get; + + /// Length of the `Build&Earn` period in standard eras. + /// Each `Build&Earn` period consists of one or more distinct standard eras. + #[pallet::constant] + type StandardErasPerBuildAndEarnPeriod: Get; + + /// Maximum number of contracts that can be integrated into dApp staking at once. + #[pallet::constant] + type MaxNumberOfContracts: Get; /// Maximum number of unlocking chunks that can exist per account at a time. #[pallet::constant] @@ -114,11 +123,22 @@ pub mod pallet { /// Maximum number of staking chunks that can exist per account at a time. #[pallet::constant] type MaxStakingChunks: Get; + + /// Minimum amount staker can stake on a contract. + #[pallet::constant] + type MinimumStakeAmount: Get; } #[pallet::event] #[pallet::generate_deposit(pub(crate) fn deposit_event)] pub enum Event { + /// New era has started. + NewEra { era: EraNumber }, + /// New period has started. + NewPeriod { + period_type: PeriodType, + number: PeriodNumber, + }, /// A smart contract has been registered for dApp staking DAppRegistered { owner: T::AccountId, @@ -160,6 +180,18 @@ pub mod pallet { account: T::AccountId, amount: Balance, }, + /// Account has staked some amount on a smart contract. + Stake { + account: T::AccountId, + smart_contract: T::SmartContract, + amount: Balance, + }, + /// Account has unstaked some amount from a smart contract. + Unstake { + account: T::AccountId, + smart_contract: T::SmartContract, + amount: Balance, + }, } #[pallet::error] @@ -183,9 +215,9 @@ pub mod pallet { ZeroAmount, /// Total locked amount for staker is below minimum threshold. LockedAmountBelowThreshold, - /// Cannot add additional locked balance chunks due to size limit. + /// Cannot add additional locked balance chunks due to capacity limit. TooManyLockedBalanceChunks, - /// Cannot add additional unlocking chunks due to size limit + /// Cannot add additional unlocking chunks due to capacity limit. TooManyUnlockingChunks, /// Remaining stake prevents entire balance of starting the unlocking process. RemainingStakePreventsFullUnlock, @@ -193,6 +225,26 @@ pub mod pallet { NoUnlockedChunksToClaim, /// There are no unlocking chunks available to relock. NoUnlockingChunks, + /// The amount being staked is too large compared to what's available for staking. + UnavailableStakeFunds, + /// There are unclaimed rewards remaining from past periods. They should be claimed before staking again. + UnclaimedRewardsFromPastPeriods, + /// Cannot add additional stake chunks due to capacity limit. + TooManyStakeChunks, + /// An unexpected error occured while trying to stake. + InternalStakeError, + /// Total staked amount on contract is below the minimum required value. + InsufficientStakeAmount, + /// Stake operation is rejected since period ends in the next era. + PeriodEndsInNextEra, + /// Unstaking is rejected since the period in which past stake was active has passed. + UnstakeFromPastPeriod, + /// Unstake amount is greater than the staked amount. + UnstakeAmountTooLarge, + /// Account has no staking information for the contract. + NoStakingInfo, + /// An unexpected error occured while trying to unstake. + InternalUnstakeError, } /// General information about dApp staking protocol state. @@ -219,10 +271,109 @@ pub mod pallet { pub type Ledger = StorageMap<_, Blake2_128Concat, T::AccountId, AccountLedgerFor, ValueQuery>; + /// Information about how much each staker has staked for each smart contract in some period. + #[pallet::storage] + pub type StakerInfo = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + T::SmartContract, + SingularStakingInfo, + OptionQuery, + >; + + /// Information about how much has been staked on a smart contract in some era or period. + #[pallet::storage] + pub type ContractStake = + StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakingInfoSeries, ValueQuery>; + /// General information about the current era. #[pallet::storage] pub type CurrentEraInfo = StorageValue<_, EraInfo, ValueQuery>; + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(now: BlockNumberFor) -> Weight { + let mut protocol_state = ActiveProtocolState::::get(); + + // We should not modify pallet storage while in maintenance mode. + // This is a safety measure, since maintenance mode is expected to be + // enabled in case some misbehavior or corrupted storage is detected. + if protocol_state.maintenance { + return T::DbWeight::get().reads(1); + } + + // Nothing to do if it's not new era + if !protocol_state.is_new_era(now) { + return T::DbWeight::get().reads(1); + } + + let mut era_info = CurrentEraInfo::::get(); + let next_era = protocol_state.era.saturating_add(1); + let maybe_period_event = match protocol_state.period_type() { + PeriodType::Voting => { + // For the sake of consistency + let ending_era = + next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); + let build_and_earn_start_block = + now.saturating_add(T::StandardEraLength::get()); + protocol_state.next_period_type(ending_era, build_and_earn_start_block); + + era_info.migrate_to_next_era(Some(protocol_state.period_type())); + + Some(Event::::NewPeriod { + period_type: protocol_state.period_type(), + number: protocol_state.period_number(), + }) + } + PeriodType::BuildAndEarn => { + // TODO: trigger reward calculation here. This will be implemented later. + + // Switch to `Voting` period if conditions are met. + if protocol_state.period_info.is_next_period(next_era) { + // For the sake of consistency we treat the whole `Voting` period as a single era. + // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. + let ending_era = next_era.saturating_add(1); + let voting_period_length = Self::blocks_per_voting_period(); + let next_era_start_block = now.saturating_add(voting_period_length); + + protocol_state.next_period_type(ending_era, next_era_start_block); + + era_info.migrate_to_next_era(Some(protocol_state.period_type())); + + // TODO: trigger tier configuration calculation based on internal & external params. + + Some(Event::::NewPeriod { + period_type: protocol_state.period_type(), + number: protocol_state.period_number(), + }) + } else { + let next_era_start_block = now.saturating_add(T::StandardEraLength::get()); + protocol_state.next_era_start = next_era_start_block; + + era_info.migrate_to_next_era(None); + + None + } + } + }; + + protocol_state.era = next_era; + ActiveProtocolState::::put(protocol_state); + + CurrentEraInfo::::put(era_info); + + Self::deposit_event(Event::::NewEra { era: next_era }); + if let Some(period_event) = maybe_period_event { + Self::deposit_event(period_event); + } + + // TODO: benchmark later + T::DbWeight::get().reads_writes(2, 2) + } + } + #[pallet::call] impl Pallet { /// Used to enable or disable maintenance mode. @@ -397,6 +548,10 @@ pub mod pallet { // TODO: might require some modification later on, like additional checks to ensure contract can be unregistered. + // TODO2: we should remove staked amount from appropriate entries, since contract has been 'invalidated' + + // TODO3: will need to add a call similar to what we have in DSv2, for stakers to 'unstake_from_unregistered_contract' + Self::deposit_event(Event::::DAppUnregistered { smart_contract, era: current_era, @@ -418,7 +573,6 @@ pub mod pallet { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; - let state = ActiveProtocolState::::get(); let mut ledger = Ledger::::get(&account); // Calculate & check amount available for locking @@ -427,11 +581,8 @@ pub mod pallet { let amount_to_lock = available_balance.min(amount); ensure!(!amount_to_lock.is_zero(), Error::::ZeroAmount); - // Only lock for the next era onwards. - let lock_era = state.era.saturating_add(1); - ledger - .add_lock_amount(amount_to_lock, lock_era) - .map_err(|_| Error::::TooManyLockedBalanceChunks)?; + ledger.add_lock_amount(amount_to_lock); + ensure!( ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), Error::::LockedAmountBelowThreshold @@ -464,7 +615,7 @@ pub mod pallet { let state = ActiveProtocolState::::get(); let mut ledger = Ledger::::get(&account); - let available_for_unlocking = ledger.unlockable_amount(state.period); + let available_for_unlocking = ledger.unlockable_amount(state.period_info.number); let amount_to_unlock = available_for_unlocking.min(amount); // Ensure we unlock everything if remaining amount is below threshold. @@ -473,7 +624,7 @@ pub mod pallet { .saturating_sub(amount_to_unlock); let amount_to_unlock = if remaining_amount < T::MinimumLockedAmount::get() { ensure!( - ledger.active_stake(state.period).is_zero(), + ledger.active_stake(state.period_info.number).is_zero(), Error::::RemainingStakePreventsFullUnlock ); ledger.active_locked_amount() @@ -485,9 +636,7 @@ pub mod pallet { ensure!(!amount_to_unlock.is_zero(), Error::::ZeroAmount); // Update ledger with new lock and unlocking amounts - ledger - .subtract_lock_amount(amount_to_unlock, state.era) - .map_err(|_| Error::::TooManyLockedBalanceChunks)?; + ledger.subtract_lock_amount(amount_to_unlock); let current_block = frame_system::Pallet::::block_number(); let unlock_block = current_block.saturating_add(T::UnlockingPeriod::get()); @@ -529,6 +678,8 @@ pub mod pallet { // TODO: We should ensure user doesn't unlock everything if they still have storage leftovers (e.g. unclaimed rewards?) + // TODO2: to make it more bounded, we could add a limit to how much distinct stake entries a user can have + Self::deposit_event(Event::::ClaimedUnlocked { account, amount }); Ok(()) @@ -540,18 +691,13 @@ pub mod pallet { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; - let state = ActiveProtocolState::::get(); let mut ledger = Ledger::::get(&account); ensure!(!ledger.unlocking.is_empty(), Error::::NoUnlockingChunks); - // Only lock for the next era onwards. - let lock_era = state.era.saturating_add(1); let amount = ledger.consume_unlocking_chunks(); - ledger - .add_lock_amount(amount, lock_era) - .map_err(|_| Error::::TooManyLockedBalanceChunks)?; + ledger.add_lock_amount(amount); ensure!( ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), Error::::LockedAmountBelowThreshold @@ -567,6 +713,219 @@ pub mod pallet { Ok(()) } + + /// Stake the specified amount on a smart contract. + /// The `amount` specified **must** be available for staking and meet the required minimum, otherwise the call will fail. + /// + /// Depending on the period type, appropriate stake amount will be updated. + #[pallet::call_index(9)] + #[pallet::weight(Weight::zero())] + pub fn stake( + origin: OriginFor, + smart_contract: T::SmartContract, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + ensure!(amount > 0, Error::::ZeroAmount); + + ensure!( + Self::is_active(&smart_contract), + Error::::NotOperatedDApp + ); + + let protocol_state = ActiveProtocolState::::get(); + // Staker always stakes from the NEXT era + let stake_era = protocol_state.era.saturating_add(1); + ensure!( + !protocol_state.period_info.is_next_period(stake_era), + Error::::PeriodEndsInNextEra + ); + + let mut ledger = Ledger::::get(&account); + + // 1. + // Increase stake amount for the next era & current period in staker's ledger + ledger + .add_stake_amount(amount, stake_era, protocol_state.period_number()) + .map_err(|err| match err { + AccountLedgerError::InvalidPeriod => { + Error::::UnclaimedRewardsFromPastPeriods + } + AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, + AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, + // Defensive check, should never happen + _ => Error::::InternalStakeError, + })?; + + // 2. + // Update `StakerInfo` storage with the new stake amount on the specified contract. + // + // There are two distinct scenarios: + // 1. Existing entry matches the current period number - just update it. + // 2. Entry doesn't exist or it's for an older period - create a new one. + // + // This is ok since we only use this storage entry to keep track of how much each staker + // has staked on each contract in the current period. We only ever need the latest information. + // This is because `AccountLedger` is the one keeping information about how much was staked when. + let new_staking_info = match StakerInfo::::get(&account, &smart_contract) { + Some(mut staking_info) + if staking_info.period_number() == protocol_state.period_number() => + { + staking_info.stake(amount, protocol_state.period_info.period_type); + staking_info + } + _ => { + ensure!( + amount >= T::MinimumStakeAmount::get(), + Error::::InsufficientStakeAmount + ); + let mut staking_info = SingularStakingInfo::new( + protocol_state.period_info.number, + protocol_state.period_info.period_type, + ); + staking_info.stake(amount, protocol_state.period_info.period_type); + staking_info + } + }; + + // 3. + // Update `ContractStake` storage with the new stake amount on the specified contract. + let mut contract_stake_info = ContractStake::::get(&smart_contract); + ensure!( + contract_stake_info + .stake(amount, protocol_state.period_info, stake_era) + .is_ok(), + Error::::InternalStakeError + ); + + // 4. + // Update total staked amount for the next era. + CurrentEraInfo::::mutate(|era_info| { + era_info.add_stake_amount(amount, protocol_state.period_type()); + }); + + // 5. + // Update remaining storage entries + Self::update_ledger(&account, ledger); + StakerInfo::::insert(&account, &smart_contract, new_staking_info); + ContractStake::::insert(&smart_contract, contract_stake_info); + + Self::deposit_event(Event::::Stake { + account, + smart_contract, + amount, + }); + + Ok(()) + } + + /// Unstake the specified amount from a smart contract. + /// The `amount` specified **must** not exceed what's staked, otherwise the call will fail. + /// + /// Depending on the period type, appropriate stake amount will be updated. + #[pallet::call_index(10)] + #[pallet::weight(Weight::zero())] + pub fn unstake( + origin: OriginFor, + smart_contract: T::SmartContract, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + ensure!(amount > 0, Error::::ZeroAmount); + + ensure!( + Self::is_active(&smart_contract), + Error::::NotOperatedDApp + ); + + let protocol_state = ActiveProtocolState::::get(); + let unstake_era = protocol_state.era; + + let mut ledger = Ledger::::get(&account); + + // 1. + // Update `StakerInfo` storage with the reduced stake amount on the specified contract. + let (new_staking_info, amount) = match StakerInfo::::get(&account, &smart_contract) { + Some(mut staking_info) => { + ensure!( + staking_info.period_number() == protocol_state.period_number(), + Error::::UnstakeFromPastPeriod + ); + ensure!( + staking_info.total_staked_amount() >= amount, + Error::::UnstakeAmountTooLarge + ); + + // If unstaking would take the total staked amount below the minimum required value, + // unstake everything. + let amount = if staking_info.total_staked_amount().saturating_sub(amount) + < T::MinimumStakeAmount::get() + { + staking_info.total_staked_amount() + } else { + amount + }; + + staking_info.unstake(amount, protocol_state.period_type()); + (staking_info, amount) + } + None => { + return Err(Error::::NoStakingInfo.into()); + } + }; + + // 2. + // Reduce stake amount + ledger + .unstake_amount(amount, unstake_era, protocol_state.period_number()) + .map_err(|err| match err { + AccountLedgerError::InvalidPeriod => Error::::UnstakeFromPastPeriod, + AccountLedgerError::UnstakeAmountLargerThanStake => { + Error::::UnstakeAmountTooLarge + } + AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, + _ => Error::::InternalUnstakeError, + })?; + + // 3. + // Update `ContractStake` storage with the reduced stake amount on the specified contract. + let mut contract_stake_info = ContractStake::::get(&smart_contract); + ensure!( + contract_stake_info + .unstake(amount, protocol_state.period_info, unstake_era) + .is_ok(), + Error::::InternalUnstakeError + ); + + // 4. + // Update total staked amount for the next era. + CurrentEraInfo::::mutate(|era_info| { + era_info.unstake_amount(amount, protocol_state.period_type()); + }); + + // 5. + // Update remaining storage entries + Self::update_ledger(&account, ledger); + ContractStake::::insert(&smart_contract, contract_stake_info); + + if new_staking_info.is_empty() { + StakerInfo::::remove(&account, &smart_contract); + } else { + StakerInfo::::insert(&account, &smart_contract, new_staking_info); + } + + Self::deposit_event(Event::::Unstake { + account, + smart_contract, + amount, + }); + + Ok(()) + } } impl Pallet { @@ -609,5 +968,16 @@ pub mod pallet { Ledger::::insert(account, ledger); } } + + /// Returns the number of blocks per voting period. + pub(crate) fn blocks_per_voting_period() -> BlockNumberFor { + T::StandardEraLength::get().saturating_mul(T::StandardErasPerVotingPeriod::get().into()) + } + + /// `true` if smart contract is active, `false` if it has been unregistered. + fn is_active(smart_contract: &T::SmartContract) -> bool { + IntegratedDApps::::get(smart_contract) + .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) + } } } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index b8ef08163a..fb0cc65c27 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -109,14 +109,18 @@ impl pallet_dapp_staking::Config for Test { type Currency = Balances; type SmartContract = MockSmartContract; type ManagerOrigin = frame_system::EnsureRoot; + type StandardEraLength = ConstU64<10>; + type StandardErasPerVotingPeriod = ConstU32<8>; + type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; type MaxNumberOfContracts = ConstU16<10>; - type MaxLockedChunks = ConstU32<5>; type MaxUnlockingChunks = ConstU32<5>; type MaxStakingChunks = ConstU32<8>; type MinimumLockedAmount = ConstU128; type UnlockingPeriod = ConstU64<20>; + type MinimumStakeAmount = ConstU128<3>; } +// TODO: why not just change this to e.g. u32 for test? #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] pub enum MockSmartContract { Wasm(AccountId), @@ -151,12 +155,22 @@ impl ExtBuilder { System::set_block_number(1); DappStaking::on_initialize(System::block_number()); - // TODO: remove this after proper on_init handling is implemented + // TODO: not sure why the mess with type happens here, I can check it later + let era_length: BlockNumber = + <::StandardEraLength as sp_core::Get<_>>::get(); + let voting_period_length_in_eras: EraNumber = + <::StandardErasPerVotingPeriod as sp_core::Get<_>>::get( + ); + + // TODO: handle this via GenesisConfig, and some helper functions to set the state pallet_dapp_staking::ActiveProtocolState::::put(ProtocolState { era: 1, - next_era_start: BlockNumber::from(101_u32), - period: 1, - period_type: PeriodType::Voting(16), + next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + 1, + period_info: PeriodInfo { + number: 1, + period_type: PeriodType::Voting, + ending_era: 2, + }, maintenance: false, }); }); @@ -167,7 +181,7 @@ impl ExtBuilder { /// Run to the specified block number. /// Function assumes first block has been initialized. -pub(crate) fn _run_to_block(n: u64) { +pub(crate) fn run_to_block(n: u64) { while System::block_number() < n { DappStaking::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); @@ -179,21 +193,43 @@ pub(crate) fn _run_to_block(n: u64) { /// Run for the specified number of blocks. /// Function assumes first block has been initialized. pub(crate) fn run_for_blocks(n: u64) { - _run_to_block(System::block_number() + n); + run_to_block(System::block_number() + n); } /// Advance blocks until the specified era has been reached. /// /// Function has no effect if era is already passed. pub(crate) fn advance_to_era(era: EraNumber) { - // TODO: Properly implement this later when additional logic has been implemented - ActiveProtocolState::::mutate(|state| state.era = era); + assert!(era >= ActiveProtocolState::::get().era); + while ActiveProtocolState::::get().era < era { + run_for_blocks(1); + } +} + +/// Advance blocks until next era has been reached. +pub(crate) fn advance_to_next_era() { + advance_to_era(ActiveProtocolState::::get().era + 1); } /// Advance blocks until the specified period has been reached. /// /// Function has no effect if period is already passed. pub(crate) fn advance_to_period(period: PeriodNumber) { - // TODO: Properly implement this later when additional logic has been implemented - ActiveProtocolState::::mutate(|state| state.period = period); + assert!(period >= ActiveProtocolState::::get().period_number()); + while ActiveProtocolState::::get().period_number() < period { + run_for_blocks(1); + } +} + +/// Advance blocks until next period has been reached. +pub(crate) fn advance_to_next_period() { + advance_to_period(ActiveProtocolState::::get().period_number() + 1); +} + +/// Advance blocks until next period type has been reached. +pub(crate) fn _advance_to_next_period_type() { + let period_type = ActiveProtocolState::::get().period_type(); + while ActiveProtocolState::::get().period_type() == period_type { + run_for_blocks(1); + } } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index ef15c21f0f..532d856f05 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -19,8 +19,8 @@ use crate::test::mock::*; use crate::types::*; use crate::{ - pallet as pallet_dapp_staking, ActiveProtocolState, BlockNumberFor, CurrentEraInfo, DAppId, - Event, IntegratedDApps, Ledger, NextDAppId, + pallet as pallet_dapp_staking, ActiveProtocolState, BlockNumberFor, ContractStake, + CurrentEraInfo, DAppId, Event, IntegratedDApps, Ledger, NextDAppId, StakerInfo, }; use frame_support::{assert_ok, traits::Get}; @@ -39,6 +39,15 @@ pub(crate) struct MemorySnapshot { DAppInfo<::AccountId>, >, ledger: HashMap<::AccountId, AccountLedgerFor>, + staker_info: HashMap< + ( + ::AccountId, + ::SmartContract, + ), + SingularStakingInfo, + >, + contract_stake: + HashMap<::SmartContract, ContractStakingInfoSeries>, } impl MemorySnapshot { @@ -50,6 +59,10 @@ impl MemorySnapshot { current_era_info: CurrentEraInfo::::get(), integrated_dapps: IntegratedDApps::::iter().collect(), ledger: Ledger::::iter().collect(), + staker_info: StakerInfo::::iter() + .map(|(k1, k2, v)| ((k1, k2), v)) + .collect(), + contract_stake: ContractStake::::iter().collect(), } } @@ -197,14 +210,6 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) { locked_balance + expected_lock_amount, "Locked balance should be increased by the amount locked." ); - assert_eq!( - post_snapshot - .ledger - .get(&account) - .expect("Ledger entry has to exist after succcessful lock call") - .lock_era(), - post_snapshot.active_protocol_state.era + 1 - ); assert_eq!( post_snapshot.current_era_info.total_locked, @@ -232,7 +237,7 @@ pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { let expected_unlock_amount = { // Cannot unlock more than is available let possible_unlock_amount = pre_ledger - .unlockable_amount(pre_snapshot.active_protocol_state.period) + .unlockable_amount(pre_snapshot.active_protocol_state.period_number()) .min(amount); // When unlocking would take account below the minimum lock threshold, unlock everything @@ -256,7 +261,7 @@ pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { let post_snapshot = MemorySnapshot::new(); // Verify ledger is as expected - let period_number = pre_snapshot.active_protocol_state.period; + let period_number = pre_snapshot.active_protocol_state.period_number(); let post_ledger = &post_snapshot.ledger[&account]; assert_eq!( pre_ledger.active_locked_amount(), @@ -382,10 +387,6 @@ pub(crate) fn assert_relock_unlocking(account: AccountId) { post_ledger.active_locked_amount(), pre_snapshot.ledger[&account].active_locked_amount() + amount ); - assert_eq!( - post_ledger.lock_era(), - post_snapshot.active_protocol_state.era + 1 - ); // Current era info assert_eq!( @@ -397,3 +398,347 @@ pub(crate) fn assert_relock_unlocking(account: AccountId) { pre_snapshot.current_era_info.total_locked + amount ); } + +/// Stake some funds on the specified smart contract. +pub(crate) fn assert_stake( + account: AccountId, + smart_contract: &MockSmartContract, + amount: Balance, +) { + // TODO: this is a huge function - I could break it down, but I'm not sure it will help with readability. + let pre_snapshot = MemorySnapshot::new(); + let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let pre_staker_info = pre_snapshot + .staker_info + .get(&(account, smart_contract.clone())); + let pre_contract_stake = pre_snapshot + .contract_stake + .get(&smart_contract) + .map_or(ContractStakingInfoSeries::default(), |series| { + series.clone() + }); + let pre_era_info = pre_snapshot.current_era_info; + + let stake_era = pre_snapshot.active_protocol_state.era + 1; + let stake_period = pre_snapshot.active_protocol_state.period_number(); + let stake_period_type = pre_snapshot.active_protocol_state.period_type(); + + // Stake on smart contract & verify event + assert_ok!(DappStaking::stake( + RuntimeOrigin::signed(account), + smart_contract.clone(), + amount + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Stake { + account, + smart_contract: smart_contract.clone(), + amount, + })); + + // Verify post-state + let post_snapshot = MemorySnapshot::new(); + let post_ledger = post_snapshot.ledger.get(&account).unwrap(); + let post_staker_info = post_snapshot + .staker_info + .get(&(account, *smart_contract)) + .expect("Entry must exist since 'stake' operation was successfull."); + let post_contract_stake = post_snapshot + .contract_stake + .get(&smart_contract) + .expect("Entry must exist since 'stake' operation was successfull."); + let post_era_info = post_snapshot.current_era_info; + + // 1. verify ledger + // ===================== + // ===================== + assert_eq!(post_ledger.staked_period, Some(stake_period)); + assert_eq!( + post_ledger.staked_amount(stake_period), + pre_ledger.staked_amount(stake_period) + amount, + "Stake amount must increase by the 'amount'" + ); + assert_eq!( + post_ledger.stakeable_amount(stake_period), + pre_ledger.stakeable_amount(stake_period) - amount, + "Stakeable amount must decrease by the 'amount'" + ); + match pre_ledger.last_stake_era() { + Some(last_stake_era) if last_stake_era == stake_era => { + assert_eq!( + post_ledger.staked.0.len(), + pre_ledger.staked.0.len(), + "Existing entry must be modified." + ); + } + _ => { + assert_eq!( + post_ledger.staked.0.len(), + pre_ledger.staked.0.len() + 1, + "Additional entry must be added." + ); + } + } + + // 2. verify staker info + // ===================== + // ===================== + match pre_staker_info { + // We're just updating an existing entry + Some(pre_staker_info) if pre_staker_info.period_number() == stake_period => { + assert_eq!( + post_staker_info.total_staked_amount(), + pre_staker_info.total_staked_amount() + amount, + "Total staked amount must increase by the 'amount'" + ); + assert_eq!( + post_staker_info.staked_amount(stake_period_type), + pre_staker_info.staked_amount(stake_period_type) + amount, + "Staked amount must increase by the 'amount'" + ); + assert_eq!(post_staker_info.period_number(), stake_period); + assert_eq!( + post_staker_info.is_loyal(), + pre_staker_info.is_loyal(), + "Staking operation mustn't change loyalty flag." + ); + } + // A new entry is created. + _ => { + assert_eq!( + post_staker_info.total_staked_amount(), + amount, + "Total staked amount must be equal to exactly the 'amount'" + ); + assert!(amount >= ::MinimumStakeAmount::get()); + assert_eq!( + post_staker_info.staked_amount(stake_period_type), + amount, + "Staked amount must be equal to exactly the 'amount'" + ); + assert_eq!(post_staker_info.period_number(), stake_period); + assert_eq!( + post_staker_info.is_loyal(), + stake_period_type == PeriodType::Voting + ); + } + } + + // 3. verify contract stake + // ========================= + // ========================= + // TODO: since default value is all zeros, maybe we can just skip the branching code and do it once? + match pre_contract_stake.last_stake_period() { + Some(last_stake_period) if last_stake_period == stake_period => { + assert_eq!( + post_contract_stake.total_staked_amount(stake_period), + pre_contract_stake.total_staked_amount(stake_period) + amount, + "Staked amount must increase by the 'amount'" + ); + assert_eq!( + post_contract_stake.staked_amount(stake_period, stake_period_type), + pre_contract_stake.staked_amount(stake_period, stake_period_type) + amount, + "Staked amount must increase by the 'amount'" + ); + } + _ => { + assert_eq!(post_contract_stake.len(), 1); + assert_eq!( + post_contract_stake.total_staked_amount(stake_period), + amount, + "Total staked amount must be equal to exactly the 'amount'" + ); + assert_eq!( + post_contract_stake.staked_amount(stake_period, stake_period_type), + amount, + "Staked amount must be equal to exactly the 'amount'" + ); + } + } + assert_eq!(post_contract_stake.last_stake_period(), Some(stake_period)); + assert_eq!(post_contract_stake.last_stake_era(), Some(stake_era)); + + // TODO: expand this check to compare inner slices as well! + + // 4. verify era info + // ========================= + // ========================= + assert_eq!( + post_era_info.total_staked_amount(), + pre_era_info.total_staked_amount(), + "Total staked amount for the current era must remain the same." + ); + assert_eq!( + post_era_info.total_staked_amount_next_era(), + pre_era_info.total_staked_amount_next_era() + amount + ); + assert_eq!( + post_era_info.staked_amount_next_era(stake_period_type), + pre_era_info.staked_amount_next_era(stake_period_type) + amount + ); +} + +/// Unstake some funds from the specified smart contract. +pub(crate) fn assert_unstake( + account: AccountId, + smart_contract: &MockSmartContract, + amount: Balance, +) { + let pre_snapshot = MemorySnapshot::new(); + let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let pre_staker_info = pre_snapshot + .staker_info + .get(&(account, smart_contract.clone())) + .expect("Entry must exist since 'unstake' is being called."); + let pre_contract_stake = pre_snapshot + .contract_stake + .get(&smart_contract) + .expect("Entry must exist since 'unstake' is being called."); + let pre_era_info = pre_snapshot.current_era_info; + + let _unstake_era = pre_snapshot.active_protocol_state.era; + let unstake_period = pre_snapshot.active_protocol_state.period_number(); + let unstake_period_type = pre_snapshot.active_protocol_state.period_type(); + + let minimum_stake_amount: Balance = + ::MinimumStakeAmount::get(); + let is_full_unstake = + pre_staker_info.total_staked_amount().saturating_sub(amount) < minimum_stake_amount; + + // Unstake all if we expect to go below the minimum stake amount + let amount = if is_full_unstake { + pre_staker_info.total_staked_amount() + } else { + amount + }; + + // Unstake from smart contract & verify event + assert_ok!(DappStaking::unstake( + RuntimeOrigin::signed(account), + smart_contract.clone(), + amount + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Unstake { + account, + smart_contract: smart_contract.clone(), + amount, + })); + + // Verify post-state + let post_snapshot = MemorySnapshot::new(); + let post_ledger = post_snapshot.ledger.get(&account).unwrap(); + let post_contract_stake = post_snapshot + .contract_stake + .get(&smart_contract) + .expect("Entry must exist since 'stake' operation was successfull."); + let post_era_info = post_snapshot.current_era_info; + + // 1. verify ledger + // ===================== + // ===================== + assert_eq!(post_ledger.staked_period, Some(unstake_period)); + assert_eq!( + post_ledger.staked_amount(unstake_period), + pre_ledger.staked_amount(unstake_period) - amount, + "Stake amount must decrease by the 'amount'" + ); + assert_eq!( + post_ledger.stakeable_amount(unstake_period), + pre_ledger.stakeable_amount(unstake_period) + amount, + "Stakeable amount must increase by the 'amount'" + ); + // TODO: maybe extend check with concrete value checks? E.g. if we modify past entry, we should check past & current entries are properly adjusted. + + // 2. verify staker info + // ===================== + // ===================== + if is_full_unstake { + assert!( + !StakerInfo::::contains_key(&account, smart_contract), + "Entry must be deleted since it was a full unstake." + ); + } else { + let post_staker_info = post_snapshot + .staker_info + .get(&(account, *smart_contract)) + .expect("Entry must exist since 'stake' operation was successfull and it wasn't a full unstake."); + assert_eq!(post_staker_info.period_number(), unstake_period); + assert_eq!( + post_staker_info.total_staked_amount(), + pre_staker_info.total_staked_amount() - amount, + "Total staked amount must decrease by the 'amount'" + ); + assert_eq!( + post_staker_info.staked_amount(unstake_period_type), + pre_staker_info + .staked_amount(unstake_period_type) + .saturating_sub(amount), + "Staked amount must decrease by the 'amount'" + ); + + let is_loyal = pre_staker_info.is_loyal() + && !(unstake_period_type == PeriodType::BuildAndEarn + && post_staker_info.staked_amount(PeriodType::Voting) + < pre_staker_info.staked_amount(PeriodType::Voting)); + assert_eq!( + post_staker_info.is_loyal(), + is_loyal, + "If 'Voting' stake amount is reduced in B&E period, loyalty flag must be set to false." + ); + } + + // 3. verify contract stake + // ========================= + // ========================= + assert_eq!( + post_contract_stake.total_staked_amount(unstake_period), + pre_contract_stake.total_staked_amount(unstake_period) - amount, + "Staked amount must decreased by the 'amount'" + ); + assert_eq!( + post_contract_stake.staked_amount(unstake_period, unstake_period_type), + pre_contract_stake + .staked_amount(unstake_period, unstake_period_type) + .saturating_sub(amount), + "Staked amount must decreased by the 'amount'" + ); + // TODO: extend with concrete value checks later + + // 4. verify era info + // ========================= + // ========================= + + // It's possible next era has staked more than the current era. This is because 'stake' will always stake for the NEXT era. + if pre_era_info.total_staked_amount() < amount { + assert!(post_era_info.total_staked_amount().is_zero()); + } else { + assert_eq!( + post_era_info.total_staked_amount(), + pre_era_info.total_staked_amount() - amount, + "Total staked amount for the current era must decrease by 'amount'." + ); + } + assert_eq!( + post_era_info.total_staked_amount_next_era(), + pre_era_info.total_staked_amount_next_era() - amount, + "Total staked amount for the next era must decrease by 'amount'. No overflow is allowed." + ); + + if unstake_period_type == PeriodType::BuildAndEarn + && pre_era_info.staked_amount_next_era(PeriodType::BuildAndEarn) < amount + { + let overflow = amount - pre_era_info.staked_amount_next_era(PeriodType::BuildAndEarn); + + assert!(post_era_info + .staked_amount_next_era(PeriodType::BuildAndEarn) + .is_zero()); + assert_eq!( + post_era_info.staked_amount_next_era(PeriodType::Voting), + pre_era_info.staked_amount_next_era(PeriodType::Voting) - overflow + ); + } else { + assert_eq!( + post_era_info.staked_amount_next_era(unstake_period_type), + pre_era_info.staked_amount_next_era(unstake_period_type) - amount + ); + } +} diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 4a6dcdc901..27f76a6d62 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -19,13 +19,16 @@ use crate::test::mock::*; use crate::test::testing_utils::*; use crate::{ - pallet as pallet_dapp_staking, ActiveProtocolState, DAppId, Error, IntegratedDApps, Ledger, - NextDAppId, SparseBoundedAmountEraVec, StakeChunk, + pallet as pallet_dapp_staking, ActiveProtocolState, DAppId, EraNumber, Error, IntegratedDApps, + Ledger, NextDAppId, PeriodType, StakerInfo, }; -use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get, BoundedVec}; +use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; use sp_runtime::traits::Zero; +// TODO: test scenarios +// 1. user is staking, period passes, they can unlock their funds which were previously staked + #[test] fn maintenace_mode_works() { ExtBuilder::build().execute_with(|| { @@ -83,6 +86,85 @@ fn maintenace_mode_call_filtering_works() { DappStaking::unlock(RuntimeOrigin::signed(1), 100), Error::::Disabled ); + assert_noop!( + DappStaking::claim_unlocked(RuntimeOrigin::signed(1)), + Error::::Disabled + ); + assert_noop!( + DappStaking::relock_unlocking(RuntimeOrigin::signed(1)), + Error::::Disabled + ); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(1), MockSmartContract::default(), 100), + Error::::Disabled + ); + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(1), MockSmartContract::default(), 100), + Error::::Disabled + ); + }) +} + +#[test] +fn on_initialize_state_change_works() { + ExtBuilder::build().execute_with(|| { + // TODO: test `EraInfo` change and verify events. This would be good to do each time we call the helper functions to go to next era or period. + + // Sanity check + let protocol_state = ActiveProtocolState::::get(); + assert_eq!(protocol_state.era, 1); + assert_eq!(protocol_state.period_number(), 1); + assert_eq!(protocol_state.period_type(), PeriodType::Voting); + assert_eq!(System::block_number(), 1); + + let blocks_per_voting_period = DappStaking::blocks_per_voting_period(); + assert_eq!( + protocol_state.next_era_start, + blocks_per_voting_period + 1, + "Counting starts from block 1, hence the '+ 1'." + ); + + // Advance eras until we reach the Build&Earn period part + run_to_block(protocol_state.next_era_start - 1); + let protocol_state = ActiveProtocolState::::get(); + assert_eq!( + protocol_state.period_type(), + PeriodType::Voting, + "Period type should still be the same." + ); + assert_eq!(protocol_state.era, 1); + + run_for_blocks(1); + let protocol_state = ActiveProtocolState::::get(); + assert_eq!(protocol_state.period_type(), PeriodType::BuildAndEarn); + assert_eq!(protocol_state.era, 2); + assert_eq!(protocol_state.period_number(), 1); + + // Advance eras just until we reach the next voting period + let eras_per_bep_period: EraNumber = + ::StandardErasPerBuildAndEarnPeriod::get(); + let blocks_per_era: BlockNumber = + ::StandardEraLength::get(); + for era in 2..(2 + eras_per_bep_period - 1) { + let pre_block = System::block_number(); + advance_to_next_era(); + assert_eq!(System::block_number(), pre_block + blocks_per_era); + let protocol_state = ActiveProtocolState::::get(); + assert_eq!(protocol_state.period_type(), PeriodType::BuildAndEarn); + assert_eq!(protocol_state.period_number(), 1); + assert_eq!(protocol_state.era, era + 1); + } + + // Finaly advance over to the next era and ensure we're back to voting period + advance_to_next_era(); + let protocol_state = ActiveProtocolState::::get(); + assert_eq!(protocol_state.period_type(), PeriodType::Voting); + assert_eq!(protocol_state.era, 2 + eras_per_bep_period); + assert_eq!( + protocol_state.next_era_start, + System::block_number() + blocks_per_voting_period + ); + assert_eq!(protocol_state.period_number(), 2); }) } @@ -339,35 +421,6 @@ fn lock_with_incorrect_amount_fails() { }) } -#[test] -fn lock_with_too_many_chunks_fails() { - ExtBuilder::build().execute_with(|| { - let max_locked_chunks = ::MaxLockedChunks::get(); - let minimum_locked_amount: Balance = - ::MinimumLockedAmount::get(); - - // Fill up the locked chunks to the limit - let locker = 1; - assert_lock(locker, minimum_locked_amount); - for current_era in 1..max_locked_chunks { - advance_to_era(current_era + 1); - assert_lock(locker, 1); - } - - // Ensure we can still lock in the current era since number of chunks should not increase - for _ in 0..10 { - assert_lock(locker, 1); - } - - // Advance to the next era and ensure it's not possible to add additional chunks - advance_to_era(ActiveProtocolState::::get().era + 1); - assert_noop!( - DappStaking::lock(RuntimeOrigin::signed(locker), 1), - Error::::TooManyLockedBalanceChunks, - ); - }) -} - #[test] fn unlock_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { @@ -381,7 +434,7 @@ fn unlock_basic_example_is_ok() { assert_unlock(account, first_unlock_amount); // Advance era and unlock additional amount - advance_to_era(ActiveProtocolState::::get().era + 1); + advance_to_next_era(); assert_unlock(account, first_unlock_amount); // Lock a bit more, and unlock again @@ -397,7 +450,7 @@ fn unlock_with_remaining_amount_below_threshold_is_ok() { let account = 2; let lock_amount = 101; assert_lock(account, lock_amount); - advance_to_era(ActiveProtocolState::::get().era + 1); + advance_to_next_era(); assert_lock(account, lock_amount); advance_to_era(ActiveProtocolState::::get().era + 3); @@ -419,28 +472,21 @@ fn unlock_with_amount_higher_than_avaiable_is_ok() { let account = 2; let lock_amount = 101; assert_lock(account, lock_amount); - advance_to_era(ActiveProtocolState::::get().era + 1); + advance_to_next_era(); assert_lock(account, lock_amount); - // TODO: Hacky, maybe improve later when staking is implemented? + // Register contract & stake on it + let smart_contract = MockSmartContract::Wasm(1); + assert_register(1, &smart_contract); let stake_amount = 91; - Ledger::::mutate(&account, |ledger| { - ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { - amount: stake_amount, - era: ActiveProtocolState::::get().era, - }]) - .expect("Only one chunk so creation should succeed."), - ); - ledger.staked_period = Some(ActiveProtocolState::::get().period); - }); + assert_stake(account, &smart_contract, stake_amount); // Try to unlock more than is available, due to active staked amount assert_unlock(account, lock_amount - stake_amount + 1); // Ensure there is no effect of staked amount once we move to the following period assert_lock(account, lock_amount - stake_amount); // restore previous state - advance_to_period(ActiveProtocolState::::get().period + 1); + advance_to_period(ActiveProtocolState::::get().period_number() + 1); assert_unlock(account, lock_amount - stake_amount + 1); }) } @@ -458,7 +504,7 @@ fn unlock_advanced_examples_are_ok() { assert_unlock(account, unlock_amount); // Advance era and unlock additional amount - advance_to_era(ActiveProtocolState::::get().era + 1); + advance_to_next_era(); assert_unlock(account, unlock_amount * 2); // Advance few more eras, and unlock everything @@ -469,7 +515,7 @@ fn unlock_advanced_examples_are_ok() { .is_zero()); // Advance one more era and ensure we can still lock & unlock - advance_to_era(ActiveProtocolState::::get().era + 1); + advance_to_next_era(); assert_lock(account, lock_amount); assert_unlock(account, unlock_amount); }) @@ -481,23 +527,17 @@ fn unlock_everything_with_active_stake_fails() { let account = 2; let lock_amount = 101; assert_lock(account, lock_amount); - advance_to_era(ActiveProtocolState::::get().era + 1); + advance_to_next_era(); // We stake so the amount is just below the minimum locked amount, causing full unlock impossible. let minimum_locked_amount: Balance = ::MinimumLockedAmount::get(); let stake_amount = minimum_locked_amount - 1; - // TODO: Hacky, maybe improve later when staking is implemented? - Ledger::::mutate(&account, |ledger| { - ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { - amount: stake_amount, - era: ActiveProtocolState::::get().era, - }]) - .expect("Only one chunk so creation should succeed."), - ); - ledger.staked_period = Some(ActiveProtocolState::::get().period); - }); + + // Register contract & stake on it + let smart_contract = MockSmartContract::Wasm(1); + assert_register(1, &smart_contract); + assert_stake(account, &smart_contract, stake_amount); // Try to unlock more than is available, due to active staked amount assert_noop!( @@ -513,7 +553,7 @@ fn unlock_with_zero_amount_fails() { let account = 2; let lock_amount = 101; assert_lock(account, lock_amount); - advance_to_era(ActiveProtocolState::::get().era + 1); + advance_to_next_era(); // Unlock with zero fails assert_noop!( @@ -522,17 +562,9 @@ fn unlock_with_zero_amount_fails() { ); // Stake everything, so available unlock amount is always zero - // TODO: Hacky, maybe improve later when staking is implemented? - Ledger::::mutate(&account, |ledger| { - ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { - amount: lock_amount, - era: ActiveProtocolState::::get().era, - }]) - .expect("Only one chunk so creation should succeed."), - ); - ledger.staked_period = Some(ActiveProtocolState::::get().period); - }); + let smart_contract = MockSmartContract::Wasm(1); + assert_register(1, &smart_contract); + assert_stake(account, &smart_contract, lock_amount); // Try to unlock anything, expect zero amount error assert_noop!( @@ -542,33 +574,6 @@ fn unlock_with_zero_amount_fails() { }) } -#[test] -fn unlock_with_exceeding_locked_storage_limits_fails() { - ExtBuilder::build().execute_with(|| { - let account = 2; - let lock_amount = 103; - assert_lock(account, lock_amount); - - let unlock_amount = 3; - for _ in 0..::MaxLockedChunks::get() { - advance_to_era(ActiveProtocolState::::get().era + 1); - assert_unlock(account, unlock_amount); - } - - // We can still unlock in the current era, theoretically - for _ in 0..5 { - assert_unlock(account, unlock_amount); - } - - // Following unlock should fail due to exceeding storage limits - advance_to_era(ActiveProtocolState::::get().era + 1); - assert_noop!( - DappStaking::unlock(RuntimeOrigin::signed(account), unlock_amount), - Error::::TooManyLockedBalanceChunks, - ); - }) -} - #[test] fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { ExtBuilder::build().execute_with(|| { @@ -693,26 +698,6 @@ fn relock_unlocking_no_chunks_fails() { }) } -#[test] -fn relock_unlocking_too_many_chunks_fails() { - ExtBuilder::build().execute_with(|| { - let max_locked_chunks = ::MaxLockedChunks::get(); - - // Fill up the locked chunks to the limit - let account = 3; - for current_era in 1..=max_locked_chunks { - assert_lock(account, 11); - advance_to_era(current_era + 1); - } - assert_unlock(account, 7); - - assert_noop!( - DappStaking::relock_unlocking(RuntimeOrigin::signed(account)), - Error::::TooManyLockedBalanceChunks, - ); - }) -} - #[test] fn relock_unlocking_insufficient_lock_amount_fails() { ExtBuilder::build().execute_with(|| { @@ -758,3 +743,495 @@ fn relock_unlocking_insufficient_lock_amount_fails() { ); }) } + +#[test] +fn stake_basic_example_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + + // 1st scenario - stake some amount, and then some more + let (stake_amount_1, stake_amount_2) = (31, 29); + assert_stake(account, &smart_contract, stake_amount_1); + assert_stake(account, &smart_contract, stake_amount_2); + + // 2nd scenario - stake in the next era + advance_to_next_era(); + let stake_amount_3 = 23; + assert_stake(account, &smart_contract, stake_amount_3); + + // 3rd scenario - advance era again but create a gap, and then stake + advance_to_era(ActiveProtocolState::::get().era + 2); + let stake_amount_4 = 19; + assert_stake(account, &smart_contract, stake_amount_4); + + // 4th scenario - advance period, and stake + // advance_to_next_era(); + // advance_to_next_period(); + // let stake_amount_5 = 17; + // assert_stake(account, &smart_contract, stake_amount_5); + // TODO: this can only be tested after reward claiming has been implemented!!! + }) +} + +#[test] +fn stake_with_zero_amount_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + let account = 2; + assert_lock(account, 300); + + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 0), + Error::::ZeroAmount, + ); + }) +} + +#[test] +fn stake_on_invalid_dapp_fails() { + ExtBuilder::build().execute_with(|| { + let account = 2; + assert_lock(account, 300); + + // Try to stake on non-existing contract + let smart_contract = MockSmartContract::default(); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), + Error::::NotOperatedDApp + ); + + // Try to stake on unregistered smart contract + assert_register(1, &smart_contract); + assert_unregister(&smart_contract); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), + Error::::NotOperatedDApp + ); + }) +} + +#[test] +fn stake_in_final_era_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let smart_contract = MockSmartContract::default(); + let account = 2; + assert_register(1, &smart_contract); + assert_lock(account, 300); + + // Force Build&Earn period + ActiveProtocolState::::mutate(|state| { + state.period_info.period_type = PeriodType::BuildAndEarn; + state.period_info.ending_era = state.era + 1; + }); + + // Try to stake in the final era of the period, which should fail. + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), + Error::::PeriodEndsInNextEra + ); + }) +} + +#[test] +fn stake_fails_if_unclaimed_rewards_from_past_period_remain() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let smart_contract = MockSmartContract::default(); + let account = 2; + assert_register(1, &smart_contract); + assert_lock(account, 300); + + // Stake some amount, then force next period + assert_stake(account, &smart_contract, 100); + advance_to_next_period(); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), + Error::::UnclaimedRewardsFromPastPeriods + ); + }) +} + +#[test] +fn stake_fails_if_not_enough_stakeable_funds_available() { + ExtBuilder::build().execute_with(|| { + // Register smart contracts & lock some amount + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + let account = 3; + assert_register(1, &smart_contract_1); + assert_register(2, &smart_contract_2); + let lock_amount = 100; + assert_lock(account, lock_amount); + + // Stake some amount on the first contract, and second contract + assert_stake(account, &smart_contract_1, 50); + assert_stake(account, &smart_contract_2, 40); + + // Try to stake more than is available, expect failure + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract_1.clone(), 11), + Error::::UnavailableStakeFunds + ); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract_2.clone(), 11), + Error::::UnavailableStakeFunds + ); + + // Stake exactly up to available funds, expect a pass + assert_stake(account, &smart_contract_2, 10); + }) +} + +#[test] +fn stake_fails_due_to_too_many_chunks() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let smart_contract = MockSmartContract::default(); + let account = 3; + assert_register(1, &smart_contract); + let lock_amount = 500; + assert_lock(account, lock_amount); + + // Keep on staking & creating chunks until capacity is reached + for _ in 0..(::MaxStakingChunks::get()) { + advance_to_next_era(); + assert_stake(account, &smart_contract, 10); + } + + // Ensure we can still stake in the current era since an entry exists + assert_stake(account, &smart_contract, 10); + + // Staking in the next era results in error due to too many chunks + advance_to_next_era(); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract.clone(), 10), + Error::::TooManyStakeChunks + ); + }) +} + +#[test] +fn stake_fails_due_to_too_small_staking_amount() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + let account = 3; + assert_register(1, &smart_contract_1); + assert_register(2, &smart_contract_2); + assert_lock(account, 300); + + // Stake with too small amount, expect a failure + let min_stake_amount: Balance = + ::MinimumStakeAmount::get(); + assert_noop!( + DappStaking::stake( + RuntimeOrigin::signed(account), + smart_contract_1.clone(), + min_stake_amount - 1 + ), + Error::::InsufficientStakeAmount + ); + + // Staking with minimum amount must work. Also, after a successful stake, we can stake with arbitrary small amount on the contract. + assert_stake(account, &smart_contract_1, min_stake_amount); + assert_stake(account, &smart_contract_1, 1); + + // Even though account is staking already, trying to stake with too small amount on a different + // smart contract should once again fail. + assert_noop!( + DappStaking::stake( + RuntimeOrigin::signed(account), + smart_contract_2.clone(), + min_stake_amount - 1 + ), + Error::::InsufficientStakeAmount + ); + }) +} + +#[test] +fn unstake_basic_example_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 400; + assert_lock(account, lock_amount); + + // Prep step - stake some amount + let stake_amount_1 = 83; + assert_stake(account, &smart_contract, stake_amount_1); + + // 1st scenario - unstake some amount, in the current era. + let unstake_amount_1 = 3; + assert_unstake(account, &smart_contract, unstake_amount_1); + + // 2nd scenario - advance to next era/period type, and unstake some more + let unstake_amount_2 = 7; + let unstake_amount_3 = 11; + advance_to_next_era(); + assert_eq!( + ActiveProtocolState::::get().period_type(), + PeriodType::BuildAndEarn, + "Sanity check, period type change must happe." + ); + assert_unstake(account, &smart_contract, unstake_amount_2); + assert_unstake(account, &smart_contract, unstake_amount_3); + + // 3rd scenario - advance few eras to create a gap, and unstake some more + advance_to_era(ActiveProtocolState::::get().era + 3); + assert_unstake(account, &smart_contract, unstake_amount_3); + assert_unstake(account, &smart_contract, unstake_amount_2); + + // 4th scenario - perform a full unstake + advance_to_next_era(); + let full_unstake_amount = StakerInfo::::get(&account, &smart_contract) + .unwrap() + .total_staked_amount(); + assert_unstake(account, &smart_contract, full_unstake_amount); + }) +} + +#[test] +fn unstake_with_leftover_amount_below_minimum_works() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + + let min_stake_amount: Balance = + ::MinimumStakeAmount::get(); + assert_stake(account, &smart_contract, min_stake_amount); + + // Unstake some amount, bringing it below the minimum + assert_unstake(account, &smart_contract, 1); + }) +} + +#[test] +fn unstake_with_entry_overflow_attempt_works() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + + assert_stake(account, &smart_contract, amount); + + // Advance one era, unstake some amount. The goal is to make a new entry. + advance_to_next_era(); + assert_unstake(account, &smart_contract, 11); + + // Advance 2 eras, stake some amount. This should create a new entry for the next era. + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_stake(account, &smart_contract, 3); + + // Unstake some amount, which should result in the creation of the 4th entry, but the oldest one should be prunned. + assert_unstake(account, &smart_contract, 1); + }) +} + +#[test] +fn unstake_with_zero_amount_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + let account = 2; + assert_lock(account, 300); + assert_stake(account, &smart_contract, 100); + + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract, 0), + Error::::ZeroAmount, + ); + }) +} + +#[test] +fn unstake_on_invalid_dapp_fails() { + ExtBuilder::build().execute_with(|| { + let account = 2; + assert_lock(account, 300); + + // Try to unstake from non-existing contract + let smart_contract = MockSmartContract::default(); + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract, 100), + Error::::NotOperatedDApp + ); + + // Try to unstake from unregistered smart contract + assert_register(1, &smart_contract); + assert_stake(account, &smart_contract, 100); + assert_unregister(&smart_contract); + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract, 100), + Error::::NotOperatedDApp + ); + }) +} + +#[test] +fn unstake_with_exceeding_amount_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contracts & lock some amount + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + let account = 2; + assert_lock(account, 300); + + // 1st scenario - stake some amount on the first contract, and try to unstake more than was staked + let stake_amount_1 = 100; + assert_stake(account, &smart_contract_1, stake_amount_1); + assert_noop!( + DappStaking::unstake( + RuntimeOrigin::signed(account), + smart_contract_1, + stake_amount_1 + 1 + ), + Error::::UnstakeAmountTooLarge + ); + + // 2nd scenario - have some stake on two distinct contracts, but unstaking more than staked per contract still fails + let stake_amount_2 = 50; + assert_stake(account, &smart_contract_2, stake_amount_2); + assert_noop!( + DappStaking::unstake( + RuntimeOrigin::signed(account), + smart_contract_2, + stake_amount_2 + 1 + ), + Error::::UnstakeAmountTooLarge + ); + }) +} + +#[test] +fn unstake_from_non_staked_contract_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contracts & lock some amount + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + let account = 2; + assert_lock(account, 300); + + // Stake some amount on the first contract. + let stake_amount = 100; + assert_stake(account, &smart_contract_1, stake_amount); + + // Try to unstake from the 2nd contract, which isn't staked on. + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract_2, 1,), + Error::::NoStakingInfo + ); + }) +} + +#[test] +fn unstake_from_a_contract_staked_in_past_period_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + let account = 2; + assert_lock(account, 300); + + // Stake some amount on the 2nd contract. + let stake_amount = 100; + assert_stake(account, &smart_contract_2, stake_amount); + + // Advance to the next period, and stake on the 1st contract. + advance_to_next_period(); + // TODO: need to implement reward claiming for this check to work! + // assert_stake(account, &smart_contract_1, stake_amount); + // Try to unstake from the 2nd contract, which is no longer staked on due to period change. + // assert_noop!( + // DappStaking::unstake( + // RuntimeOrigin::signed(account), + // smart_contract_2, + // 1, + // ), + // Error::::UnstakeFromPastPeriod + // ); + }) +} + +#[test] +fn unstake_from_past_period_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let smart_contract = MockSmartContract::Wasm(1); + assert_register(1, &smart_contract); + let account = 2; + assert_lock(account, 300); + + // Stake some amount, and advance to the next period + let stake_amount = 100; + assert_stake(account, &smart_contract, stake_amount); + advance_to_next_period(); + + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract, stake_amount), + Error::::UnstakeFromPastPeriod + ); + }) +} + +#[test] +fn unstake_fails_due_to_too_many_chunks() { + ExtBuilder::build().execute_with(|| { + // Register smart contract,lock & stake some amount + let smart_contract = MockSmartContract::default(); + let account = 2; + assert_register(1, &smart_contract); + let lock_amount = 1000; + assert_lock(account, lock_amount); + assert_stake(account, &smart_contract, lock_amount); + + // Keep on unstaking & creating chunks until capacity is reached + for _ in 0..(::MaxStakingChunks::get()) { + advance_to_next_era(); + assert_unstake(account, &smart_contract, 11); + } + + // Ensure we can still unstake in the current era since an entry exists + assert_unstake(account, &smart_contract, 10); + + // Staking in the next era results in error due to too many chunks + advance_to_next_era(); + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract.clone(), 10), + Error::::TooManyStakeChunks + ); + }) +} diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 87623d8e82..ba80bfe295 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -94,7 +94,7 @@ fn sparse_bounded_amount_era_vec_add_amount_works() { // 4th scenario - add to the previous era, should fail and be a noop assert_eq!( vec.add_amount(init_amount, first_era), - Err(SparseBoundedError::OldEra) + Err(AccountLedgerError::OldEra) ); assert_eq!(vec.0.len(), 2); assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); @@ -106,7 +106,7 @@ fn sparse_bounded_amount_era_vec_add_amount_works() { } assert_eq!( vec.add_amount(init_amount, 100), - Err(SparseBoundedError::NoCapacity) + Err(AccountLedgerError::NoCapacity) ); } @@ -302,123 +302,163 @@ fn sparse_bounded_amount_era_vec_subtract_amount_advanced_non_consecutive_works( ); } +#[test] +fn sparse_bounded_amount_era_vec_full_subtract_with_single_future_era() { + get_u32_type!(MaxLen, 5); + let mut vec = SparseBoundedAmountEraVec::::new(); + + // A scenario where some amount is added, for the first time, for era X. + // Immediately afterward, the same amount is subtracted from era X - 1. + let (era_1, era_2) = (1, 2); + let amount = 19; + assert_ok!(vec.add_amount(amount, era_2)); + + assert_ok!(vec.subtract_amount(amount, era_1)); + assert!( + vec.0.is_empty(), + "Future entry should have been cleaned up." + ); +} + +#[test] +fn period_type_sanity_check() { + assert_eq!(PeriodType::Voting.next(), PeriodType::BuildAndEarn); + assert_eq!(PeriodType::BuildAndEarn.next(), PeriodType::Voting); +} + +#[test] +fn period_info_basic_checks() { + let period_number = 2; + let ending_era = 5; + let info = PeriodInfo::new(period_number, PeriodType::Voting, ending_era); + + // Sanity checks + assert_eq!(info.number, period_number); + assert_eq!(info.period_type, PeriodType::Voting); + assert_eq!(info.ending_era, ending_era); + + // Voting period checks + assert!(!info.is_next_period(ending_era - 1)); + assert!(!info.is_next_period(ending_era)); + assert!(!info.is_next_period(ending_era + 1)); + for era in vec![ending_era - 1, ending_era, ending_era + 1] { + assert!( + !info.is_next_period(era), + "Cannot trigger 'true' in the Voting period type." + ); + } + + // Build&Earn period checks + let info = PeriodInfo::new(period_number, PeriodType::BuildAndEarn, ending_era); + assert!(!info.is_next_period(ending_era - 1)); + assert!(info.is_next_period(ending_era)); + assert!(info.is_next_period(ending_era + 1)); +} + #[test] fn protocol_state_default() { - let protoc_state = ProtocolState::::default(); + let protocol_state = ProtocolState::::default(); - assert_eq!(protoc_state.era, 0); + assert_eq!(protocol_state.era, 0); assert_eq!( - protoc_state.next_era_start, 1, + protocol_state.next_era_start, 1, "Era should start immediately on the first block" ); } +#[test] +fn protocol_state_basic_checks() { + let mut protocol_state = ProtocolState::::default(); + let period_number = 5; + let ending_era = 11; + let next_era_start = 31; + protocol_state.period_info = PeriodInfo::new(period_number, PeriodType::Voting, ending_era); + protocol_state.next_era_start = next_era_start; + + assert_eq!(protocol_state.period_number(), period_number); + assert_eq!(protocol_state.period_type(), PeriodType::Voting); + + // New era check + assert!(!protocol_state.is_new_era(next_era_start - 1)); + assert!(protocol_state.is_new_era(next_era_start)); + assert!(protocol_state.is_new_era(next_era_start + 1)); + + // Toggle new period type check - 'Voting' to 'BuildAndEarn' + let ending_era_1 = 23; + let next_era_start_1 = 41; + protocol_state.next_period_type(ending_era_1, next_era_start_1); + assert_eq!(protocol_state.period_type(), PeriodType::BuildAndEarn); + assert_eq!( + protocol_state.period_number(), + period_number, + "Switching from 'Voting' to 'BuildAndEarn' should not trigger period bump." + ); + assert_eq!(protocol_state.ending_era(), ending_era_1); + assert!(!protocol_state.is_new_era(next_era_start_1 - 1)); + assert!(protocol_state.is_new_era(next_era_start_1)); + + // Toggle from 'BuildAndEarn' over to 'Voting' + let ending_era_2 = 24; + let next_era_start_2 = 91; + protocol_state.next_period_type(ending_era_2, next_era_start_2); + assert_eq!(protocol_state.period_type(), PeriodType::Voting); + assert_eq!( + protocol_state.period_number(), + period_number + 1, + "Switching from 'BuildAndEarn' to 'Voting' must trigger period bump." + ); + assert_eq!(protocol_state.ending_era(), ending_era_2); + assert!(protocol_state.is_new_era(next_era_start_2)); +} + #[test] fn account_ledger_default() { - get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); get_u32_type!(StakingDummy, 8); - let acc_ledger = - AccountLedger::::default(); + let acc_ledger = AccountLedger::::default(); assert!(acc_ledger.is_empty()); assert!(acc_ledger.active_locked_amount().is_zero()); - assert!(acc_ledger.lock_era().is_zero()); - assert!(acc_ledger.latest_locked_chunk().is_none()); } #[test] fn account_ledger_add_lock_amount_works() { - get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // First step, sanity checks - let first_era = 1; assert!(acc_ledger.active_locked_amount().is_zero()); assert!(acc_ledger.total_locked_amount().is_zero()); - assert!(acc_ledger.add_lock_amount(0, first_era).is_ok()); + acc_ledger.add_lock_amount(0); assert!(acc_ledger.active_locked_amount().is_zero()); // Adding lock value works as expected let init_amount = 20; - assert!(acc_ledger.add_lock_amount(init_amount, first_era).is_ok()); + acc_ledger.add_lock_amount(init_amount); assert_eq!(acc_ledger.active_locked_amount(), init_amount); assert_eq!(acc_ledger.total_locked_amount(), init_amount); - assert_eq!(acc_ledger.lock_era(), first_era); assert!(!acc_ledger.is_empty()); - assert_eq!(acc_ledger.locked.0.len(), 1); - assert_eq!( - acc_ledger.latest_locked_chunk(), - Some(&LockedChunk { - amount: init_amount, - era: first_era, - }) - ); - - // Add to the same era - let addition = 7; - assert!(acc_ledger.add_lock_amount(addition, first_era).is_ok()); - assert_eq!(acc_ledger.active_locked_amount(), init_amount + addition); - assert_eq!(acc_ledger.total_locked_amount(), init_amount + addition); - assert_eq!(acc_ledger.lock_era(), first_era); - assert_eq!(acc_ledger.locked.0.len(), 1); - - // Adding to previous era should fail - assert_eq!( - acc_ledger.add_lock_amount(addition, first_era - 1), - Err(SparseBoundedError::OldEra) - ); - - // Add up to storage limit - for i in 2..=LockedDummy::get() { - assert!(acc_ledger.add_lock_amount(addition, first_era + i).is_ok()); - assert_eq!( - acc_ledger.active_locked_amount(), - init_amount + addition * i as u128 - ); - assert_eq!(acc_ledger.lock_era(), first_era + i); - assert_eq!(acc_ledger.locked.0.len(), i as usize); - } - - // Any further additions should fail due to exhausting bounded storage capacity - let acc_ledger_clone = acc_ledger.clone(); - assert_eq!( - acc_ledger.add_lock_amount(addition, acc_ledger.lock_era() + 1), - Err(SparseBoundedError::NoCapacity) - ); - assert_eq!(acc_ledger, acc_ledger_clone); } #[test] fn account_ledger_subtract_lock_amount_basic_usage_works() { - get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop - assert!(acc_ledger.subtract_lock_amount(0, 1).is_ok()); - assert!(acc_ledger.subtract_lock_amount(10, 1).is_ok()); - assert!(acc_ledger.locked.0.len().is_zero()); + acc_ledger.subtract_lock_amount(0); + acc_ledger.subtract_lock_amount(10); assert!(acc_ledger.is_empty()); // First basic scenario - // Add some lock amount, then reduce it for the same era - let first_era = 1; + // Add some lock amount, then reduce it let first_lock_amount = 19; let unlock_amount = 7; - assert!(acc_ledger - .add_lock_amount(first_lock_amount, first_era) - .is_ok()); - assert!(acc_ledger - .subtract_lock_amount(unlock_amount, first_era) - .is_ok()); - assert_eq!(acc_ledger.locked.0.len(), 1); + acc_ledger.add_lock_amount(first_lock_amount); + acc_ledger.subtract_lock_amount(unlock_amount); assert_eq!( acc_ledger.total_locked_amount(), first_lock_amount - unlock_amount @@ -430,321 +470,24 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { assert_eq!(acc_ledger.unlocking_amount(), 0); // Second basic scenario - // Reduce the lock from the era which isn't latest in the vector let first_lock_amount = first_lock_amount - unlock_amount; let second_lock_amount = 31; - let second_era = 2; - assert!(acc_ledger - .add_lock_amount(second_lock_amount - first_lock_amount, second_era) - .is_ok()); + acc_ledger.add_lock_amount(second_lock_amount - first_lock_amount); assert_eq!(acc_ledger.active_locked_amount(), second_lock_amount); - assert_eq!(acc_ledger.locked.0.len(), 2); // Subtract from the first era and verify state is as expected - assert!(acc_ledger - .subtract_lock_amount(unlock_amount, first_era) - .is_ok()); - assert_eq!(acc_ledger.locked.0.len(), 2); + acc_ledger.subtract_lock_amount(unlock_amount); assert_eq!( acc_ledger.active_locked_amount(), second_lock_amount - unlock_amount ); - assert_eq!( - acc_ledger.locked.0[0].amount, - first_lock_amount - unlock_amount - ); - assert_eq!( - acc_ledger.locked.0[1].amount, - second_lock_amount - unlock_amount - ); - - // Third basic scenario - // Reduce the the latest era, don't expect the first one to change - assert!(acc_ledger - .subtract_lock_amount(unlock_amount, second_era) - .is_ok()); - assert_eq!(acc_ledger.locked.0.len(), 2); - assert_eq!( - acc_ledger.active_locked_amount(), - second_lock_amount - unlock_amount * 2 - ); - assert_eq!( - acc_ledger.locked.0[0].amount, - first_lock_amount - unlock_amount - ); - assert_eq!( - acc_ledger.locked.0[1].amount, - second_lock_amount - unlock_amount * 2 - ); -} - -#[test] -fn account_ledger_subtract_lock_amount_overflow_fails() { - get_u32_type!(LockedDummy, 5); - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); - - let first_lock_amount = 17 * 19; - let era = 1; - let unlock_amount = 5; - assert!(acc_ledger.add_lock_amount(first_lock_amount, era).is_ok()); - for idx in 1..=LockedDummy::get() { - assert!(acc_ledger.subtract_lock_amount(unlock_amount, idx).is_ok()); - assert_eq!(acc_ledger.locked.0.len(), idx as usize); - assert_eq!( - acc_ledger.active_locked_amount(), - first_lock_amount - unlock_amount * idx as u128 - ); - } - - // Updating existing lock should still work - let locked_snapshot = acc_ledger.locked.0.clone(); - for i in 1..10 { - assert!(acc_ledger - .subtract_lock_amount(unlock_amount, LockedDummy::get()) - .is_ok()); - assert_eq!(acc_ledger.locked.0.len(), LockedDummy::get() as usize); - - let last_idx = LockedDummy::get() as usize - 1; - assert_eq!( - &acc_ledger.locked.0[0..last_idx], - &locked_snapshot[0..last_idx] - ); - assert_eq!( - acc_ledger.locked.0[last_idx].amount as u128 + unlock_amount * i, - locked_snapshot[last_idx].amount - ); - } - - // Attempt to add additional chunks should fail, and is a noop. - let acc_ledger_clone = acc_ledger.clone(); - assert_eq!( - acc_ledger.subtract_lock_amount(unlock_amount, LockedDummy::get() + 1), - Err(SparseBoundedError::NoCapacity) - ); - assert_eq!(acc_ledger, acc_ledger_clone); -} - -#[test] -fn account_ledger_subtract_lock_amount_advanced_example_works() { - get_u32_type!(LockedDummy, 5); - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); - - // Prepare an example where we have two non-consecutive entries, and we unlock in the era right before the second entry. - // This covers a scenario where user has called `lock` in the current era, - // creating an entry for the next era, and then decides to immediately unlock a portion of the locked amount. - let first_lock_amount = 17; - let second_lock_amount = 23; - let first_era = 1; - let second_era = 5; - let unlock_era = second_era - 1; - let unlock_amount = 5; - assert!(acc_ledger - .add_lock_amount(first_lock_amount, first_era) - .is_ok()); - assert!(acc_ledger - .add_lock_amount(second_lock_amount, second_era) - .is_ok()); - assert_eq!(acc_ledger.locked.0.len(), 2); - - assert!(acc_ledger - .subtract_lock_amount(unlock_amount, unlock_era) - .is_ok()); - assert_eq!( - acc_ledger.active_locked_amount(), - first_lock_amount + second_lock_amount - unlock_amount - ); - - // Check entries in more detail - assert_eq!(acc_ledger.locked.0.len(), 3); - assert_eq!(acc_ledger.locked.0[0].amount, first_lock_amount,); - assert_eq!( - acc_ledger.locked.0[2].amount, - first_lock_amount + second_lock_amount - unlock_amount - ); - // Verify the new entry is as expected - assert_eq!( - acc_ledger.locked.0[1].amount, - first_lock_amount - unlock_amount - ); - assert_eq!(acc_ledger.locked.0[1].era, unlock_era); -} - -#[test] -fn account_ledger_subtract_lock_amount_with_only_one_locked_chunk() { - get_u32_type!(LockedDummy, 5); - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); - - // Scenario: user locks for era 2 while era 1 is active, immediately followed by unlock call. - // Locked amount should be updated for the next era, but active locked amount should be unchanged (zero). - let lock_amount = 17; - let unlock_amount = 5; - let lock_era = 2; - let unlock_era = 1; - assert!(acc_ledger.add_lock_amount(lock_amount, lock_era).is_ok()); - assert!(acc_ledger - .subtract_lock_amount(unlock_amount, unlock_era) - .is_ok()); - - assert_eq!(acc_ledger.locked.0.len(), 1); - assert_eq!( - acc_ledger.locked.0[0], - LockedChunk { - amount: lock_amount - unlock_amount, - era: lock_era, - } - ); -} - -#[test] -fn account_ledger_subtract_lock_amount_correct_zero_cleanup() { - get_u32_type!(LockedDummy, 5); - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); - - // Ensure that zero entries are cleaned up correctly when required. - // There are a couple of distinct scenarios: - // 1. There is only one entry, and it's zero. The vector should be cleared & empty. - // 2. There are multiple entries, and the last one is zero. It's valid since it marks when someone fully unlocked. - // 3. Zero entry can exist in between two non-zero entries (not covered in this UT). - - // 1st scenario (A) - only one zero entry, unlock is in the same era - let lock_amount = 17; - let lock_era = 2; - assert!(acc_ledger.add_lock_amount(lock_amount, lock_era).is_ok()); - assert!(acc_ledger - .subtract_lock_amount(lock_amount, lock_era) - .is_ok()); - assert!(acc_ledger.locked.0.is_empty()); - - // 1st scenario (B) - only one zero entry, unlock is in the previous era - assert!(acc_ledger.add_lock_amount(lock_amount, lock_era).is_ok()); - assert!(acc_ledger - .subtract_lock_amount(lock_amount, lock_era - 1) - .is_ok()); - assert!(acc_ledger.locked.0.is_empty()); - - // 2nd scenario - last entry is zero - let first_lock_era = 3; - let second_lock_era = 11; - let unlock_era = second_lock_era + 2; - assert!(acc_ledger - .add_lock_amount(lock_amount, first_lock_era) - .is_ok()); - assert!(acc_ledger - .add_lock_amount(lock_amount, second_lock_era) - .is_ok()); - // Following should add new entry, to mark when the user fully unlocked - assert!(acc_ledger - .subtract_lock_amount(acc_ledger.active_locked_amount(), unlock_era) - .is_ok()); - assert_eq!(acc_ledger.locked.0.len(), 3); - assert!(acc_ledger.active_locked_amount().is_zero()); -} - -#[test] -fn account_ledger_subtract_lock_amount_zero_entry_between_two_non_zero() { - get_u32_type!(LockedDummy, 5); - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); - - let (first_lock_amount, second_lock_amount, third_lock_amount) = (17, 23, 29); - let (first_lock_era, second_lock_era, third_lock_era) = (1, 3, 7); - - // Prepare scenario with 3 locked chunks - assert!(acc_ledger - .add_lock_amount(first_lock_amount, first_lock_era) - .is_ok()); - assert!(acc_ledger - .add_lock_amount(second_lock_amount, second_lock_era) - .is_ok()); - assert!(acc_ledger - .add_lock_amount(third_lock_amount, third_lock_era) - .is_ok()); - assert_eq!(acc_ledger.locked.0.len(), 3); - - // Unlock everything for the era right before the latest chunk era - // This should result in scenario like: - // [17, 17 + 23, 0, 29] - assert!(acc_ledger - .subtract_lock_amount(first_lock_amount + second_lock_amount, third_lock_era - 1) - .is_ok()); - assert_eq!(acc_ledger.locked.0.len(), 4); - assert_eq!(acc_ledger.active_locked_amount(), third_lock_amount); - assert_eq!( - acc_ledger.locked.0[0], - LockedChunk { - amount: first_lock_amount, - era: first_lock_era - } - ); - assert_eq!( - acc_ledger.locked.0[1], - LockedChunk { - amount: first_lock_amount + second_lock_amount, - era: second_lock_era - } - ); - assert_eq!( - acc_ledger.locked.0[2], - LockedChunk { - amount: 0, - era: third_lock_era - 1 - } - ); - assert_eq!( - acc_ledger.locked.0[3], - LockedChunk { - amount: third_lock_amount, - era: third_lock_era - } - ); -} - -#[test] -fn account_ledger_subtract_lock_amount_consecutive_zeroes_merged() { - get_u32_type!(LockedDummy, 5); - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); - - // Prepare scenario with 3 locked chunks, where the middle one is zero - let lock_amount = 61; - let last_era = 11; - assert!(acc_ledger.add_lock_amount(lock_amount, 2).is_ok()); - assert!(acc_ledger.subtract_lock_amount(lock_amount, 5).is_ok()); - assert!(acc_ledger.add_lock_amount(lock_amount, last_era).is_ok()); - let second_chunk = acc_ledger.locked.0[1]; - - // Unlock everything in the era right before the latest chunk era, but that chunk should not persist - // [61, 0, 61] --> [61, 0, 0, 61] shouldn't happen since the 2nd zero is redundant. - assert!(acc_ledger - .subtract_lock_amount(lock_amount, last_era - 1) - .is_ok()); - assert_eq!(acc_ledger.locked.0.len(), 2); - assert_eq!(acc_ledger.locked.0[1], second_chunk); } #[test] fn account_ledger_add_unlocking_chunk_works() { - get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop @@ -799,18 +542,16 @@ fn account_ledger_add_unlocking_chunk_works() { let acc_ledger_snapshot = acc_ledger.clone(); assert_eq!( acc_ledger.add_unlocking_chunk(1, block_number + UnlockingDummy::get() as u64 + 1), - Err(SparseBoundedError::NoCapacity) + Err(AccountLedgerError::NoCapacity) ); assert_eq!(acc_ledger, acc_ledger_snapshot); } #[test] -fn active_stake_works() { - get_u32_type!(LockedDummy, 5); +fn account_ledger_active_stake_works() { get_u32_type!(UnlockingDummy, 5); get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check assert!(acc_ledger.active_stake(0).is_zero()); @@ -832,108 +573,469 @@ fn active_stake_works() { } #[test] -fn unlockable_amount_works() { - get_u32_type!(LockedDummy, 5); +fn account_ledger_stakeable_amount_works() { get_u32_type!(UnlockingDummy, 5); get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); - // Sanity check scenario - assert!(acc_ledger.unlockable_amount(0).is_zero()); + // Sanity check for empty ledger + assert!(acc_ledger.stakeable_amount(1).is_zero()); - // Nothing is staked - let lock_amount = 29; - let lock_era = 3; - assert!(acc_ledger.add_lock_amount(lock_amount, lock_era).is_ok()); - assert_eq!(acc_ledger.unlockable_amount(0), lock_amount); + // First scenario - some locked amount, no staking chunks + let first_period = 1; + let locked_amount = 19; + acc_ledger.add_lock_amount(locked_amount); + assert_eq!( + acc_ledger.stakeable_amount(first_period), + locked_amount, + "Stakeable amount has to be equal to the locked amount" + ); - // Some amount is staked, period matches - let stake_period = 5; - let stake_amount = 17; + // Second scenario - some staked amount is introduced, period is still valid + let first_era = 1; + let staked_amount = 7; acc_ledger.staked = SparseBoundedAmountEraVec( BoundedVec::try_from(vec![StakeChunk { - amount: stake_amount, - era: lock_era, + amount: staked_amount, + era: first_era, }]) .expect("Only one chunk so creation should succeed."), ); - acc_ledger.staked_period = Some(stake_period); + acc_ledger.staked_period = Some(first_period); + assert_eq!( - acc_ledger.unlockable_amount(stake_period), - lock_amount - stake_amount + acc_ledger.stakeable_amount(first_period), + locked_amount - staked_amount, + "Total stakeable amount should be equal to the locked amount minus what is already staked." ); - // Period doesn't match - assert_eq!(acc_ledger.unlockable_amount(stake_period - 1), lock_amount); - assert_eq!(acc_ledger.unlockable_amount(stake_period + 2), lock_amount); - - // Absurd example, for the sake of completeness - staked without any lock - acc_ledger.locked.0 = Default::default(); - assert!(acc_ledger.unlockable_amount(stake_period).is_zero()); - assert!(acc_ledger.unlockable_amount(stake_period - 2).is_zero()); - assert!(acc_ledger.unlockable_amount(stake_period + 1).is_zero()); + // Third scenario - continuation of the previous, but we move to the next period. + assert_eq!( + acc_ledger.stakeable_amount(first_period + 1), + locked_amount, + "Stakeable amount has to be equal to the locked amount since old period staking isn't valid anymore" + ); } #[test] -fn claim_unlocked_works() { - get_u32_type!(LockedDummy, 5); +fn account_ledger_staked_amount_works() { get_u32_type!(UnlockingDummy, 5); get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); - - // Sanity check scenario - assert!(acc_ledger.claim_unlocked(0).is_zero()); - - // Add a chunk, assert it can be claimed correctly - let amount = 19; - let block_number = 1; - assert_ok!(acc_ledger.add_unlocking_chunk(amount, block_number)); - assert!(acc_ledger.claim_unlocked(0).is_zero()); - assert_eq!(acc_ledger.claim_unlocked(block_number), amount); - assert!(acc_ledger.unlocking.is_empty()); + let mut acc_ledger = AccountLedger::::default(); - // Add multiple chunks, assert claim works correctly - let (amount1, amount2, amount3) = (7, 13, 19); - let (block1, block2, block3) = (1, 3, 5); + // Sanity check for empty ledger + assert!(acc_ledger.staked_amount(1).is_zero()); - // Prepare unlocking chunks - assert_ok!(acc_ledger.add_unlocking_chunk(amount1, block1)); - assert_ok!(acc_ledger.add_unlocking_chunk(amount2, block2)); - assert_ok!(acc_ledger.add_unlocking_chunk(amount3, block3)); + // First scenario - active period matches the ledger + let first_era = 1; + let first_period = 1; + let locked_amount = 19; + let staked_amount = 13; + acc_ledger.add_lock_amount(locked_amount); + acc_ledger.staked = SparseBoundedAmountEraVec( + BoundedVec::try_from(vec![StakeChunk { + amount: staked_amount, + era: first_era, + }]) + .expect("Only one chunk so creation should succeed."), + ); + acc_ledger.staked_period = Some(first_period); - // Only claim 1 chunk - assert_eq!(acc_ledger.claim_unlocked(block1 + 1), amount1); - assert_eq!(acc_ledger.unlocking.len(), 2); + assert_eq!(acc_ledger.staked_amount(first_period), staked_amount); - // Claim remaining two chunks - assert_eq!(acc_ledger.claim_unlocked(block3 + 1), amount2 + amount3); - assert!(acc_ledger.unlocking.is_empty()); + // Second scenario - active period doesn't match the ledger + assert!(acc_ledger.staked_amount(first_period + 1).is_zero()); } #[test] -fn consume_unlocking_chunks_works() { - get_u32_type!(LockedDummy, 5); +fn account_ledger_add_stake_amount_works() { get_u32_type!(UnlockingDummy, 5); get_u32_type!(StakingDummy, 8); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); - // Sanity check scenario - assert!(acc_ledger.consume_unlocking_chunks().is_zero()); + // Sanity check + assert!(acc_ledger.add_stake_amount(0, 0, 0).is_ok()); + assert!(acc_ledger.staked_period.is_none()); + assert!(acc_ledger.staked.0.is_empty()); - // Add multiple chunks, cal should return correct amount - let (amount1, amount2) = (7, 13); - assert_ok!(acc_ledger.add_unlocking_chunk(amount1, 1)); - assert_ok!(acc_ledger.add_unlocking_chunk(amount2, 2)); + // First scenario - stake some amount, and ensure values are as expected + let first_era = 2; + let first_period = 1; + let lock_amount = 17; + let stake_amount = 11; + acc_ledger.add_lock_amount(lock_amount); - assert_eq!(acc_ledger.consume_unlocking_chunks(), amount1 + amount2); - assert!(acc_ledger.unlocking.is_empty()); -} + assert!(acc_ledger + .add_stake_amount(stake_amount, first_era, first_period) + .is_ok()); + assert_eq!(acc_ledger.staked_period, Some(first_period)); + assert_eq!(acc_ledger.staked.0.len(), 1); + assert_eq!( + acc_ledger.staked.0[0], + StakeChunk { + amount: stake_amount, + era: first_era, + } + ); + assert_eq!(acc_ledger.staked_amount(first_period), stake_amount); + + // Second scenario - stake some more to the same era, only amount should change + assert!(acc_ledger + .add_stake_amount(1, first_era, first_period) + .is_ok()); + assert_eq!(acc_ledger.staked.0.len(), 1); + assert_eq!(acc_ledger.staked_amount(first_period), stake_amount + 1); + + // Third scenario - stake to the next era, new chunk should be added + let next_era = first_era + 3; + let remaining_not_staked = lock_amount - stake_amount - 1; + assert!(acc_ledger + .add_stake_amount(remaining_not_staked, next_era, first_period) + .is_ok()); + assert_eq!(acc_ledger.staked.0.len(), 2); + assert_eq!(acc_ledger.staked_amount(first_period), lock_amount); +} + +#[test] +fn account_ledger_add_stake_amount_invalid_era_fails() { + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let first_era = 5; + let first_period = 2; + let lock_amount = 13; + let stake_amount = 7; + acc_ledger.add_lock_amount(lock_amount); + assert!(acc_ledger + .add_stake_amount(stake_amount, first_era, first_period) + .is_ok()); + let acc_ledger_snapshot = acc_ledger.clone(); + + // Try to add to the next era, it should fail + assert_eq!( + acc_ledger.add_stake_amount(1, first_era, first_period + 1), + Err(AccountLedgerError::InvalidPeriod) + ); + assert_eq!( + acc_ledger, acc_ledger_snapshot, + "Previous failed action must be a noop" + ); + + // Try to add to the previous era, it should fail + assert_eq!( + acc_ledger.add_stake_amount(1, first_era, first_period - 1), + Err(AccountLedgerError::InvalidPeriod) + ); + assert_eq!( + acc_ledger, acc_ledger_snapshot, + "Previous failed action must be a noop" + ); +} + +#[test] +fn account_ledger_add_stake_amount_too_large_amount_fails() { + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = AccountLedger::::default(); + + // Sanity check + assert_eq!( + acc_ledger.add_stake_amount(10, 1, 1), + Err(AccountLedgerError::UnavailableStakeFunds) + ); + + // Lock some amount, and try to stake more than that + let first_era = 5; + let first_period = 2; + let lock_amount = 13; + acc_ledger.add_lock_amount(lock_amount); + assert_eq!( + acc_ledger.add_stake_amount(lock_amount + 1, first_era, first_period), + Err(AccountLedgerError::UnavailableStakeFunds) + ); + + // Additional check - have some active stake, and then try to overstake + assert!(acc_ledger + .add_stake_amount(lock_amount - 2, first_era, first_period) + .is_ok()); + assert_eq!( + acc_ledger.add_stake_amount(3, first_era, first_period), + Err(AccountLedgerError::UnavailableStakeFunds) + ); +} + +#[test] +fn account_ledger_add_stake_amount_while_exceeding_capacity_fails() { + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = AccountLedger::::default(); + + // Try to stake up to the capacity, it should work + // Lock some amount, and try to stake more than that + let first_era = 5; + let first_period = 2; + let lock_amount = 31; + let stake_amount = 3; + acc_ledger.add_lock_amount(lock_amount); + for inc in 0..StakingDummy::get() { + assert!(acc_ledger + .add_stake_amount(stake_amount, first_era + inc, first_period) + .is_ok()); + assert_eq!( + acc_ledger.staked_amount(first_period), + stake_amount * (inc as u128 + 1) + ); + } + + // Can still stake to the last staked era + assert!(acc_ledger + .add_stake_amount( + stake_amount, + first_era + StakingDummy::get() - 1, + first_period + ) + .is_ok()); + + // But staking to the next era must fail with exceeded capacity + assert_eq!( + acc_ledger.add_stake_amount(stake_amount, first_era + StakingDummy::get(), first_period), + Err(AccountLedgerError::NoCapacity) + ); +} #[test] -fn era_info_manipulation_works() { +fn account_ledger_unstake_amount_works() { + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 19; + let era_1 = 2; + let period_1 = 1; + acc_ledger.add_lock_amount(amount_1); + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_1) + .is_ok()); + + // Sanity check + assert!(acc_ledger.unstake_amount(0, era_1, period_1).is_ok()); + + // 1st scenario - unstake some amount from the current era. + let unstake_amount_1 = 3; + assert!(acc_ledger + .unstake_amount(unstake_amount_1, era_1, period_1) + .is_ok()); + assert_eq!( + acc_ledger.staked_amount(period_1), + amount_1 - unstake_amount_1 + ); + assert_eq!( + acc_ledger.staked.0.len(), + 1, + "Only existing entry should be updated." + ); + + // 2nd scenario - unstake some more, but from the next era + let era_2 = era_1 + 1; + assert!(acc_ledger + .unstake_amount(unstake_amount_1, era_2, period_1) + .is_ok()); + assert_eq!( + acc_ledger.staked_amount(period_1), + amount_1 - unstake_amount_1 * 2 + ); + assert_eq!( + acc_ledger.staked.0.len(), + 2, + "New entry must be created to cover the new era stake." + ); + + // 3rd scenario - unstake some more, bump era by a larger number + let era_3 = era_2 + 3; + assert!(acc_ledger + .unstake_amount(unstake_amount_1, era_3, period_1) + .is_ok()); + assert_eq!( + acc_ledger.staked_amount(period_1), + amount_1 - unstake_amount_1 * 3 + ); + assert_eq!( + acc_ledger.staked.0.len(), + 3, + "New entry must be created to cover the new era stake." + ); +} + +#[test] +fn account_ledger_unstake_from_invalid_era_fails() { + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 13; + let era_1 = 2; + let period_1 = 1; + acc_ledger.add_lock_amount(amount_1); + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_1) + .is_ok()); + + assert_eq!( + acc_ledger.unstake_amount(amount_1, era_1 + 1, period_1 + 1), + Err(AccountLedgerError::InvalidPeriod) + ); +} + +#[test] +fn account_ledger_unstake_too_much_fails() { + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 23; + let era_1 = 2; + let period_1 = 1; + acc_ledger.add_lock_amount(amount_1); + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_1) + .is_ok()); + + assert_eq!( + acc_ledger.unstake_amount(amount_1 + 1, era_1, period_1), + Err(AccountLedgerError::UnstakeAmountLargerThanStake) + ); +} + +#[test] +fn account_ledger_unstake_exceeds_capacity() { + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 100; + let era_1 = 2; + let period_1 = 1; + acc_ledger.add_lock_amount(amount_1); + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_1) + .is_ok()); + + for x in 0..StakingDummy::get() { + assert!( + acc_ledger.unstake_amount(3, era_1 + x, period_1).is_ok(), + "Capacity isn't full so unstake must work." + ); + } + + assert_eq!( + acc_ledger.unstake_amount(3, era_1 + StakingDummy::get(), period_1), + Err(AccountLedgerError::NoCapacity) + ); +} + +#[test] +fn account_ledger_unlockable_amount_works() { + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = AccountLedger::::default(); + + // Sanity check scenario + assert!(acc_ledger.unlockable_amount(0).is_zero()); + + // Nothing is staked + let lock_amount = 29; + let lock_era = 3; + acc_ledger.add_lock_amount(lock_amount); + assert_eq!(acc_ledger.unlockable_amount(0), lock_amount); + + // Some amount is staked, period matches + let stake_period = 5; + let stake_amount = 17; + acc_ledger.staked = SparseBoundedAmountEraVec( + BoundedVec::try_from(vec![StakeChunk { + amount: stake_amount, + era: lock_era, + }]) + .expect("Only one chunk so creation should succeed."), + ); + acc_ledger.staked_period = Some(stake_period); + assert_eq!( + acc_ledger.unlockable_amount(stake_period), + lock_amount - stake_amount + ); + + // Period doesn't match + assert_eq!(acc_ledger.unlockable_amount(stake_period - 1), lock_amount); + assert_eq!(acc_ledger.unlockable_amount(stake_period + 2), lock_amount); + + // Absurd example, for the sake of completeness - staked without any lock + acc_ledger.locked = Balance::zero(); + assert!(acc_ledger.unlockable_amount(stake_period).is_zero()); + assert!(acc_ledger.unlockable_amount(stake_period - 2).is_zero()); + assert!(acc_ledger.unlockable_amount(stake_period + 1).is_zero()); +} + +#[test] +fn account_ledger_claim_unlocked_works() { + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = AccountLedger::::default(); + + // Sanity check scenario + assert!(acc_ledger.claim_unlocked(0).is_zero()); + + // Add a chunk, assert it can be claimed correctly + let amount = 19; + let block_number = 1; + assert_ok!(acc_ledger.add_unlocking_chunk(amount, block_number)); + assert!(acc_ledger.claim_unlocked(0).is_zero()); + assert_eq!(acc_ledger.claim_unlocked(block_number), amount); + assert!(acc_ledger.unlocking.is_empty()); + + // Add multiple chunks, assert claim works correctly + let (amount1, amount2, amount3) = (7, 13, 19); + let (block1, block2, block3) = (1, 3, 5); + + // Prepare unlocking chunks + assert_ok!(acc_ledger.add_unlocking_chunk(amount1, block1)); + assert_ok!(acc_ledger.add_unlocking_chunk(amount2, block2)); + assert_ok!(acc_ledger.add_unlocking_chunk(amount3, block3)); + + // Only claim 1 chunk + assert_eq!(acc_ledger.claim_unlocked(block1 + 1), amount1); + assert_eq!(acc_ledger.unlocking.len(), 2); + + // Claim remaining two chunks + assert_eq!(acc_ledger.claim_unlocked(block3 + 1), amount2 + amount3); + assert!(acc_ledger.unlocking.is_empty()); +} + +#[test] +fn account_ledger_consume_unlocking_chunks_works() { + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = AccountLedger::::default(); + + // Sanity check scenario + assert!(acc_ledger.consume_unlocking_chunks().is_zero()); + + // Add multiple chunks, cal should return correct amount + let (amount1, amount2) = (7, 13); + assert_ok!(acc_ledger.add_unlocking_chunk(amount1, 1)); + assert_ok!(acc_ledger.add_unlocking_chunk(amount2, 2)); + + assert_eq!(acc_ledger.consume_unlocking_chunks(), amount1 + amount2); + assert!(acc_ledger.unlocking.is_empty()); +} + +#[test] +fn era_info_lock_unlock_works() { let mut era_info = EraInfo::default(); // Sanity check @@ -984,3 +1086,691 @@ fn era_info_manipulation_works() { assert_eq!(era_info.unlocking, old_era_info.unlocking - 1); assert_eq!(era_info.active_era_locked, old_era_info.active_era_locked); } + +#[test] +fn era_info_stake_works() { + let mut era_info = EraInfo::default(); + + // Sanity check + assert!(era_info.total_locked.is_zero()); + + // Add some voting period stake + let vp_stake_amount = 7; + era_info.add_stake_amount(vp_stake_amount, PeriodType::Voting); + assert_eq!(era_info.total_staked_amount_next_era(), vp_stake_amount); + assert_eq!( + era_info.staked_amount_next_era(PeriodType::Voting), + vp_stake_amount + ); + assert!( + era_info.total_staked_amount().is_zero(), + "Calling stake makes it available only from the next era." + ); + + // Add some build&earn period stake + let bep_stake_amount = 13; + era_info.add_stake_amount(bep_stake_amount, PeriodType::BuildAndEarn); + assert_eq!( + era_info.total_staked_amount_next_era(), + vp_stake_amount + bep_stake_amount + ); + assert_eq!( + era_info.staked_amount_next_era(PeriodType::BuildAndEarn), + bep_stake_amount + ); + assert!( + era_info.total_staked_amount().is_zero(), + "Calling stake makes it available only from the next era." + ); +} + +#[test] +fn era_info_unstake_works() { + let mut era_info = EraInfo::default(); + + // Make dummy era info with stake amounts + let vp_stake_amount = 15; + let bep_stake_amount_1 = 23; + let bep_stake_amount_2 = bep_stake_amount_1 + 6; + era_info.current_stake_amount = StakeAmount::new(vp_stake_amount, bep_stake_amount_1); + era_info.next_stake_amount = StakeAmount::new(vp_stake_amount, bep_stake_amount_2); + let total_staked = era_info.total_staked_amount(); + let total_staked_next_era = era_info.total_staked_amount_next_era(); + + // 1st scenario - unstake some amount, no overflow + let unstake_amount_1 = bep_stake_amount_1; + era_info.unstake_amount(unstake_amount_1, PeriodType::BuildAndEarn); + + // Current era + assert_eq!( + era_info.total_staked_amount(), + total_staked - unstake_amount_1 + ); + assert_eq!(era_info.staked_amount(PeriodType::Voting), vp_stake_amount); + assert!(era_info.staked_amount(PeriodType::BuildAndEarn).is_zero()); + + // Next era + assert_eq!( + era_info.total_staked_amount_next_era(), + total_staked_next_era - unstake_amount_1 + ); + assert_eq!( + era_info.staked_amount_next_era(PeriodType::Voting), + vp_stake_amount + ); + assert_eq!( + era_info.staked_amount_next_era(PeriodType::BuildAndEarn), + bep_stake_amount_2 - unstake_amount_1 + ); + + // 2nd scenario - unstake some more, but with overflow + let overflow = 2; + let unstake_amount_2 = bep_stake_amount_2 - unstake_amount_1 + overflow; + era_info.unstake_amount(unstake_amount_2, PeriodType::BuildAndEarn); + + // Current era + assert_eq!( + era_info.total_staked_amount(), + total_staked - unstake_amount_1 - unstake_amount_2 + ); + + // Next era + assert_eq!( + era_info.total_staked_amount_next_era(), + vp_stake_amount - overflow + ); + assert_eq!( + era_info.staked_amount_next_era(PeriodType::Voting), + vp_stake_amount - overflow + ); + assert!(era_info + .staked_amount_next_era(PeriodType::BuildAndEarn) + .is_zero()); +} + +#[test] +fn stake_amount_works() { + let mut stake_amount = StakeAmount::default(); + + // Sanity check + assert!(stake_amount.total().is_zero()); + assert!(stake_amount.for_type(PeriodType::Voting).is_zero()); + assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + + // Stake some amount in voting period + let vp_stake_1 = 11; + stake_amount.stake(vp_stake_1, PeriodType::Voting); + assert_eq!(stake_amount.total(), vp_stake_1); + assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); + assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + + // Stake some amount in build&earn period + let bep_stake_1 = 13; + stake_amount.stake(bep_stake_1, PeriodType::BuildAndEarn); + assert_eq!(stake_amount.total(), vp_stake_1 + bep_stake_1); + assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); + assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); + + // Unstake some amount from voting period + let vp_unstake_1 = 5; + stake_amount.unstake(5, PeriodType::Voting); + assert_eq!( + stake_amount.total(), + vp_stake_1 + bep_stake_1 - vp_unstake_1 + ); + assert_eq!( + stake_amount.for_type(PeriodType::Voting), + vp_stake_1 - vp_unstake_1 + ); + assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); + + // Unstake some amount from build&earn period + let bep_unstake_1 = 2; + stake_amount.unstake(bep_unstake_1, PeriodType::BuildAndEarn); + assert_eq!( + stake_amount.total(), + vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1 + ); + assert_eq!( + stake_amount.for_type(PeriodType::Voting), + vp_stake_1 - vp_unstake_1 + ); + assert_eq!( + stake_amount.for_type(PeriodType::BuildAndEarn), + bep_stake_1 - bep_unstake_1 + ); + + // Unstake some more from build&earn period, and chip away from the voting period + let total_stake = vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1; + let bep_unstake_2 = bep_stake_1 - bep_unstake_1 + 1; + stake_amount.unstake(bep_unstake_2, PeriodType::BuildAndEarn); + assert_eq!(stake_amount.total(), total_stake - bep_unstake_2); + assert_eq!( + stake_amount.for_type(PeriodType::Voting), + vp_stake_1 - vp_unstake_1 - 1 + ); + assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); +} + +#[test] +fn singular_staking_info_basics_are_ok() { + let period_number = 3; + let period_type = PeriodType::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, period_type); + + // Sanity checks + assert_eq!(staking_info.period_number(), period_number); + assert!(staking_info.is_loyal()); + assert!(staking_info.total_staked_amount().is_zero()); + assert!(!SingularStakingInfo::new(period_number, PeriodType::BuildAndEarn).is_loyal()); + + // Add some staked amount during `Voting` period + let vote_stake_amount_1 = 11; + staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + assert_eq!(staking_info.total_staked_amount(), vote_stake_amount_1); + assert_eq!( + staking_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 + ); + assert!(staking_info + .staked_amount(PeriodType::BuildAndEarn) + .is_zero()); + + // Add some staked amount during `BuildAndEarn` period + let bep_stake_amount_1 = 23; + staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); + assert_eq!( + staking_info.total_staked_amount(), + vote_stake_amount_1 + bep_stake_amount_1 + ); + assert_eq!( + staking_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 + ); + assert_eq!( + staking_info.staked_amount(PeriodType::BuildAndEarn), + bep_stake_amount_1 + ); +} + +#[test] +fn singular_staking_info_unstake_during_voting_is_ok() { + let period_number = 3; + let period_type = PeriodType::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, period_type); + + // Prep actions + let vote_stake_amount_1 = 11; + staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + + // Unstake some amount during `Voting` period, loyalty should remain as expected. + let unstake_amount_1 = 5; + assert_eq!( + staking_info.unstake(unstake_amount_1, PeriodType::Voting), + (unstake_amount_1, Balance::zero()) + ); + assert_eq!( + staking_info.total_staked_amount(), + vote_stake_amount_1 - unstake_amount_1 + ); + assert!(staking_info.is_loyal()); + + // Fully unstake, attempting to undersaturate, and ensure loyalty flag is still true. + let remaining_stake = staking_info.total_staked_amount(); + assert_eq!( + staking_info.unstake(remaining_stake + 1, PeriodType::Voting), + (remaining_stake, Balance::zero()) + ); + assert!(staking_info.total_staked_amount().is_zero()); + assert!(staking_info.is_loyal()); +} + +#[test] +fn singular_staking_info_unstake_during_bep_is_ok() { + let period_number = 3; + let period_type = PeriodType::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, period_type); + + // Prep actions + let vote_stake_amount_1 = 11; + staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + let bep_stake_amount_1 = 23; + staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); + + // 1st scenario - Unstake some of the amount staked during B&E period + let unstake_1 = 5; + assert_eq!( + staking_info.unstake(5, PeriodType::BuildAndEarn), + (Balance::zero(), unstake_1) + ); + assert_eq!( + staking_info.total_staked_amount(), + vote_stake_amount_1 + bep_stake_amount_1 - unstake_1 + ); + assert_eq!( + staking_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 + ); + assert_eq!( + staking_info.staked_amount(PeriodType::BuildAndEarn), + bep_stake_amount_1 - unstake_1 + ); + assert!(staking_info.is_loyal()); + + // 2nd scenario - unstake all of the amount staked during B&E period, and then some more. + // The point is to take a chunk from the voting period stake too. + let current_total_stake = staking_info.total_staked_amount(); + let current_bep_stake = staking_info.staked_amount(PeriodType::BuildAndEarn); + let voting_stake_overflow = 2; + let unstake_2 = current_bep_stake + voting_stake_overflow; + + assert_eq!( + staking_info.unstake(unstake_2, PeriodType::BuildAndEarn), + (voting_stake_overflow, current_bep_stake) + ); + assert_eq!( + staking_info.total_staked_amount(), + current_total_stake - unstake_2 + ); + assert_eq!( + staking_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 - voting_stake_overflow + ); + assert!(staking_info + .staked_amount(PeriodType::BuildAndEarn) + .is_zero()); + assert!( + !staking_info.is_loyal(), + "Loyalty flag should have been removed due to non-zero voting period unstake" + ); +} + +#[test] +fn contract_stake_info_is_ok() { + let period = 2; + let era = 3; + let mut contract_stake_info = ContractStakingInfo::new(era, period); + + // Sanity check + assert_eq!(contract_stake_info.period(), period); + assert_eq!(contract_stake_info.era(), era); + assert!(contract_stake_info.total_staked_amount().is_zero()); + assert!(contract_stake_info.is_empty()); + + // 1st scenario - Add some staked amount to the voting period + let vote_stake_amount_1 = 11; + contract_stake_info.stake(vote_stake_amount_1, PeriodType::Voting); + assert_eq!( + contract_stake_info.total_staked_amount(), + vote_stake_amount_1 + ); + assert_eq!( + contract_stake_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 + ); + assert!(!contract_stake_info.is_empty()); + + // 2nd scenario - add some staked amount to the B&E period + let bep_stake_amount_1 = 23; + contract_stake_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); + assert_eq!( + contract_stake_info.total_staked_amount(), + vote_stake_amount_1 + bep_stake_amount_1 + ); + assert_eq!( + contract_stake_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 + ); + assert_eq!( + contract_stake_info.staked_amount(PeriodType::BuildAndEarn), + bep_stake_amount_1 + ); + + // 3rd scenario - reduce some of the staked amount from both periods and verify it's as expected. + let total_staked = contract_stake_info.total_staked_amount(); + let vp_reduction = 3; + contract_stake_info.unstake(vp_reduction, PeriodType::Voting); + assert_eq!( + contract_stake_info.total_staked_amount(), + total_staked - vp_reduction + ); + assert_eq!( + contract_stake_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 - vp_reduction + ); + + let bp_reduction = 7; + contract_stake_info.unstake(bp_reduction, PeriodType::BuildAndEarn); + assert_eq!( + contract_stake_info.total_staked_amount(), + total_staked - vp_reduction - bp_reduction + ); + assert_eq!( + contract_stake_info.staked_amount(PeriodType::BuildAndEarn), + bep_stake_amount_1 - bp_reduction + ); + + // 4th scenario - unstake everything, and some more, from Build&Earn period, chiping away from the voting period. + let overflow = 1; + let overflow_reduction = contract_stake_info.staked_amount(PeriodType::BuildAndEarn) + overflow; + contract_stake_info.unstake(overflow_reduction, PeriodType::BuildAndEarn); + assert_eq!( + contract_stake_info.total_staked_amount(), + vote_stake_amount_1 - vp_reduction - overflow + ); + assert!(contract_stake_info + .staked_amount(PeriodType::BuildAndEarn) + .is_zero()); + assert_eq!( + contract_stake_info.staked_amount(PeriodType::Voting), + vote_stake_amount_1 - vp_reduction - overflow + ); +} + +#[test] +fn contract_staking_info_series_get_works() { + let info_1 = ContractStakingInfo::new(4, 2); + let mut info_2 = ContractStakingInfo::new(7, 3); + info_2.stake(11, PeriodType::Voting); + let mut info_3 = ContractStakingInfo::new(9, 3); + info_3.stake(13, PeriodType::BuildAndEarn); + + let series = ContractStakingInfoSeries::new(vec![info_1, info_2, info_3]); + + // Sanity check + assert_eq!(series.len(), 3); + assert!(!series.is_empty()); + + // 1st scenario - get existing entries + assert_eq!(series.get(4, 2), Some(info_1)); + assert_eq!(series.get(7, 3), Some(info_2)); + assert_eq!(series.get(9, 3), Some(info_3)); + + // 2nd scenario - get non-existing entries for covered eras + { + let era_1 = 6; + let entry_1 = series.get(era_1, 2).expect("Has to be Some"); + assert!(entry_1.total_staked_amount().is_zero()); + assert_eq!(entry_1.era(), era_1); + assert_eq!(entry_1.period(), 2); + + let era_2 = 8; + let entry_1 = series.get(era_2, 3).expect("Has to be Some"); + assert_eq!(entry_1.total_staked_amount(), 11); + assert_eq!(entry_1.era(), era_2); + assert_eq!(entry_1.period(), 3); + } + + // 3rd scenario - get non-existing entries for covered eras but mismatching period + assert!(series.get(8, 2).is_none()); + + // 4th scenario - get non-existing entries for non-covered eras + assert!(series.get(3, 2).is_none()); +} + +#[test] +fn contract_staking_info_series_stake_is_ok() { + let mut series = ContractStakingInfoSeries::default(); + + // Sanity check + assert!(series.is_empty()); + assert!(series.len().is_zero()); + + // 1st scenario - stake some amount and verify state change + let era_1 = 3; + let period_1 = 5; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 20); + let amount_1 = 31; + assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); + + assert_eq!(series.len(), 1); + assert!(!series.is_empty()); + + let entry_1_1 = series.get(era_1, period_1).unwrap(); + assert_eq!(entry_1_1.era(), era_1); + assert_eq!(entry_1_1.total_staked_amount(), amount_1); + + // 2nd scenario - stake some more to the same era but different period type, and verify state change. + let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 20); + assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); + assert_eq!( + series.len(), + 1, + "No new entry should be created since it's the same era." + ); + let entry_1_2 = series.get(era_1, period_1).unwrap(); + assert_eq!(entry_1_2.era(), era_1); + assert_eq!(entry_1_2.total_staked_amount(), amount_1 * 2); + + // 3rd scenario - stake more to the next era, while still in the same period. + let era_2 = era_1 + 2; + let amount_2 = 37; + assert!(series.stake(amount_2, period_info_1, era_2).is_ok()); + assert_eq!(series.len(), 2); + let entry_2_1 = series.get(era_1, period_1).unwrap(); + let entry_2_2 = series.get(era_2, period_1).unwrap(); + assert_eq!(entry_2_1, entry_1_2, "Old entry must remain unchanged."); + assert_eq!(entry_2_2.era(), era_2); + assert_eq!(entry_2_2.period(), period_1); + assert_eq!( + entry_2_2.total_staked_amount(), + entry_2_1.total_staked_amount() + amount_2, + "Since it's the same period, stake amount must carry over from the previous entry." + ); + + // 4th scenario - stake some more to the next era, but this time also bump the period. + let era_3 = era_2 + 3; + let period_2 = period_1 + 1; + let period_info_2 = PeriodInfo::new(period_2, PeriodType::BuildAndEarn, 20); + let amount_3 = 41; + + assert!(series.stake(amount_3, period_info_2, era_3).is_ok()); + assert_eq!(series.len(), 3); + let entry_3_1 = series.get(era_1, period_1).unwrap(); + let entry_3_2 = series.get(era_2, period_1).unwrap(); + let entry_3_3 = series.get(era_3, period_2).unwrap(); + assert_eq!(entry_3_1, entry_2_1, "Old entry must remain unchanged."); + assert_eq!(entry_3_2, entry_2_2, "Old entry must remain unchanged."); + assert_eq!(entry_3_3.era(), era_3); + assert_eq!(entry_3_3.period(), period_2); + assert_eq!( + entry_3_3.total_staked_amount(), + amount_3, + "No carry over from previous entry since period has changed." + ); + + // 5th scenario - stake to the next era, expect cleanup of oldest entry + let era_4 = era_3 + 1; + let amount_4 = 5; + assert!(series.stake(amount_4, period_info_2, era_4).is_ok()); + assert_eq!(series.len(), 3); + let entry_4_1 = series.get(era_2, period_1).unwrap(); + let entry_4_2 = series.get(era_3, period_2).unwrap(); + let entry_4_3 = series.get(era_4, period_2).unwrap(); + assert_eq!(entry_4_1, entry_3_2, "Old entry must remain unchanged."); + assert_eq!(entry_4_2, entry_3_3, "Old entry must remain unchanged."); + assert_eq!(entry_4_3.era(), era_4); + assert_eq!(entry_4_3.period(), period_2); + assert_eq!(entry_4_3.total_staked_amount(), amount_3 + amount_4); +} + +#[test] +fn contract_staking_info_series_stake_with_inconsistent_data_fails() { + let mut series = ContractStakingInfoSeries::default(); + + // Create an entry with some staked amount + let era = 5; + let period_info = PeriodInfo { + number: 7, + period_type: PeriodType::Voting, + ending_era: 31, + }; + let amount = 37; + assert!(series.stake(amount, period_info, era).is_ok()); + + // 1st scenario - attempt to stake using old era + assert!(series.stake(amount, period_info, era - 1).is_err()); + + // 2nd scenario - attempt to stake using old period + let period_info = PeriodInfo { + number: period_info.number - 1, + period_type: PeriodType::Voting, + ending_era: 31, + }; + assert!(series.stake(amount, period_info, era).is_err()); +} + +#[test] +fn contract_staking_info_series_unstake_is_ok() { + let mut series = ContractStakingInfoSeries::default(); + + // Prep action - create a stake entry + let era_1 = 2; + let period = 3; + let period_info = PeriodInfo::new(period, PeriodType::Voting, 20); + let stake_amount = 100; + assert!(series.stake(stake_amount, period_info, era_1).is_ok()); + + // 1st scenario - unstake in the same era + let amount_1 = 5; + assert!(series.unstake(amount_1, period_info, era_1).is_ok()); + assert_eq!(series.len(), 1); + assert_eq!(series.total_staked_amount(period), stake_amount - amount_1); + assert_eq!( + series.staked_amount(period, PeriodType::Voting), + stake_amount - amount_1 + ); + + // 2nd scenario - unstake in the future era, creating a 'gap' in the series + // [(era: 2)] ---> [(era: 2), (era: 5)] + let period_info = PeriodInfo::new(period, PeriodType::BuildAndEarn, 40); + let era_2 = era_1 + 3; + let amount_2 = 7; + assert!(series.unstake(amount_2, period_info, era_2).is_ok()); + assert_eq!(series.len(), 2); + assert_eq!( + series.total_staked_amount(period), + stake_amount - amount_1 - amount_2 + ); + assert_eq!( + series.staked_amount(period, PeriodType::Voting), + stake_amount - amount_1 - amount_2 + ); + + // 3rd scenario - unstake in the era right before the last, inserting the new value in-between the old ones + // [(era: 2), (era: 5)] ---> [(era: 2), (era: 4), (era: 5)] + let era_3 = era_2 - 1; + let amount_3 = 11; + assert!(series.unstake(amount_3, period_info, era_3).is_ok()); + assert_eq!(series.len(), 3); + assert_eq!( + series.total_staked_amount(period), + stake_amount - amount_1 - amount_2 - amount_3 + ); + assert_eq!( + series.staked_amount(period, PeriodType::Voting), + stake_amount - amount_1 - amount_2 - amount_3 + ); + + // Check concrete entries + assert_eq!( + series.get(era_1, period).unwrap().total_staked_amount(), + stake_amount - amount_1, + "Oldest entry must remain unchanged." + ); + assert_eq!( + series.get(era_2, period).unwrap().total_staked_amount(), + stake_amount - amount_1 - amount_2 - amount_3, + "Future era entry must be updated with all of the reductions." + ); + assert_eq!( + series.get(era_3, period).unwrap().total_staked_amount(), + stake_amount - amount_1 - amount_3, + "Second to last era entry must be updated with first & last reduction\ + because it derives its initial value from the oldest entry." + ); +} + +#[test] +fn contract_staking_info_unstake_with_worst_case_scenario_for_capacity_overflow() { + let (era_1, era_2, era_3) = (4, 7, 9); + let (period_1, period_2) = (2, 3); + let info_1 = ContractStakingInfo::new(era_1, period_1); + let mut info_2 = ContractStakingInfo::new(era_2, period_2); + let stake_amount_2 = 11; + info_2.stake(stake_amount_2, PeriodType::Voting); + let mut info_3 = ContractStakingInfo::new(era_3, period_2); + let stake_amount_3 = 13; + info_3.stake(stake_amount_3, PeriodType::BuildAndEarn); + + // A gap between 2nd and 3rd era, and from that gap unstake will be done. + // This will force a new entry to be created, potentially overflowing the vector capacity. + let mut series = ContractStakingInfoSeries::new(vec![info_1, info_2, info_3]); + + // Unstake between era 2 & 3, in attempt to overflow the inner vector capacity + let period_info = PeriodInfo { + number: period_2, + period_type: PeriodType::BuildAndEarn, + ending_era: 51, + }; + let unstake_amount = 3; + assert!(series.unstake(3, period_info, era_2 + 1).is_ok()); + assert_eq!(series.len(), 3); + + assert_eq!( + series.get(era_1, period_1), + None, + "Oldest entry should have been prunned" + ); + assert_eq!( + series + .get(era_2, period_2) + .expect("Entry must exist.") + .total_staked_amount(), + stake_amount_2 + ); + assert_eq!( + series + .get(era_2 + 1, period_2) + .expect("Entry must exist.") + .total_staked_amount(), + stake_amount_2 - unstake_amount + ); + assert_eq!( + series + .get(era_3, period_2) + .expect("Entry must exist.") + .total_staked_amount(), + stake_amount_3 - unstake_amount + ); +} + +#[test] +fn contract_staking_info_series_unstake_with_inconsistent_data_fails() { + let mut series = ContractStakingInfoSeries::default(); + let era = 5; + let period = 2; + let period_info = PeriodInfo { + number: period, + period_type: PeriodType::Voting, + ending_era: 31, + }; + + // 1st - Unstake from empty series + assert!(series.unstake(1, period_info, era).is_err()); + + // 2nd - Unstake with old period + let amount = 37; + assert!(series.stake(amount, period_info, era).is_ok()); + + let old_period_info = { + let mut temp = period_info.clone(); + temp.number -= 1; + temp + }; + assert!(series.unstake(1, old_period_info, era - 1).is_err()); + + // 3rd - Unstake with 'too' old era + assert!(series.unstake(1, period_info, era - 2).is_err()); + assert!(series.unstake(1, period_info, era - 1).is_ok()); +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 59321fe311..5f5bea6586 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -33,7 +33,6 @@ use crate::pallet::Config; /// Convenience type for `AccountLedger` usage. pub type AccountLedgerFor = AccountLedger< BlockNumberFor, - ::MaxLockedChunks, ::MaxUnlockingChunks, ::MaxStakingChunks, >; @@ -62,11 +61,17 @@ pub trait AmountEraPair: MaxEncodedLen + Default + Copy { /// Simple enum representing errors possible when using sparse bounded vector. #[derive(Debug, PartialEq, Eq)] -pub enum SparseBoundedError { +pub enum AccountLedgerError { /// Old era values cannot be added. OldEra, /// Bounded storage capacity exceeded. NoCapacity, + /// Invalid period specified. + InvalidPeriod, + /// Stake amount is to large in respect to what's available. + UnavailableStakeFunds, + /// Unstake amount is to large in respect to what's staked. + UnstakeAmountLargerThanStake, } /// Helper struct for easier manipulation of sparse pairs. @@ -106,13 +111,13 @@ where &mut self, amount: Balance, era: EraNumber, - ) -> Result<(), SparseBoundedError> { + ) -> Result<(), AccountLedgerError> { if amount.is_zero() { return Ok(()); } let mut chunk = if let Some(&chunk) = self.0.last() { - ensure!(chunk.get_era() <= era, SparseBoundedError::OldEra); + ensure!(chunk.get_era() <= era, AccountLedgerError::OldEra); chunk } else { P::default() @@ -128,7 +133,7 @@ where chunk.set_era(era); self.0 .try_push(chunk) - .map_err(|_| SparseBoundedError::NoCapacity)?; + .map_err(|_| AccountLedgerError::NoCapacity)?; } Ok(()) @@ -155,7 +160,7 @@ where &mut self, amount: Balance, era: EraNumber, - ) -> Result<(), SparseBoundedError> { + ) -> Result<(), AccountLedgerError> { if amount.is_zero() || self.0.is_empty() { return Ok(()); } @@ -217,7 +222,7 @@ where } // Update `locked` to the new vector - self.0 = BoundedVec::try_from(inner).map_err(|_| SparseBoundedError::NoCapacity)?; + self.0 = BoundedVec::try_from(inner).map_err(|_| AccountLedgerError::NoCapacity)?; Ok(()) } @@ -227,11 +232,45 @@ where #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub enum PeriodType { /// Period during which the focus is on voting. - /// Inner value is the era in which the voting period ends. - Voting(#[codec(compact)] EraNumber), + Voting, /// Period during which dApps and stakers earn rewards. - /// Inner value is the era in which the Build&Eearn period ends. - BuildAndEarn(#[codec(compact)] EraNumber), + BuildAndEarn, +} + +impl PeriodType { + pub fn next(&self) -> Self { + match self { + PeriodType::Voting => PeriodType::BuildAndEarn, + PeriodType::BuildAndEarn => PeriodType::Voting, + } + } +} + +/// Wrapper type around current `PeriodType` and era number when it's expected to end. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub struct PeriodInfo { + #[codec(compact)] + pub number: PeriodNumber, + pub period_type: PeriodType, + #[codec(compact)] + pub ending_era: EraNumber, +} + +impl PeriodInfo { + /// Create new instance of `PeriodInfo` + pub fn new(number: PeriodNumber, period_type: PeriodType, ending_era: EraNumber) -> Self { + Self { + number, + period_type, + ending_era, + } + } + + /// `true` if the provided era belongs to the next period, `false` otherwise. + /// It's only possible to provide this information for the `BuildAndEarn` period type. + pub fn is_next_period(&self, era: EraNumber) -> bool { + self.period_type == PeriodType::BuildAndEarn && self.ending_era <= era + } } /// Force types to speed up the next era, and even period. @@ -254,11 +293,8 @@ pub struct ProtocolState { /// I believe we should utilize `pallet-scheduler` to schedule the next era. Make an item for this. #[codec(compact)] pub next_era_start: BlockNumber, - /// Ongoing period number. - #[codec(compact)] - pub period: PeriodNumber, /// Ongoing period type and when is it expected to end. - pub period_type: PeriodType, + pub period_info: PeriodInfo, /// `true` if pallet is in maintenance mode (disabled), `false` otherwise. /// TODO: provide some configurable barrier to handle this on the runtime level instead? Make an item for this? pub maintenance: bool, @@ -272,14 +308,59 @@ where Self { era: 0, next_era_start: BlockNumber::from(1_u32), - period: 0, - period_type: PeriodType::Voting(0), + period_info: PeriodInfo { + number: 0, + period_type: PeriodType::Voting, + ending_era: 2, + }, maintenance: false, } } } -/// dApp state in which some dApp is in. +impl ProtocolState +where + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, +{ + /// Current period type. + pub fn period_type(&self) -> PeriodType { + self.period_info.period_type + } + + /// Current period number. + pub fn period_number(&self) -> PeriodNumber { + self.period_info.number + } + + /// Ending era of current period + pub fn ending_era(&self) -> EraNumber { + self.period_info.ending_era + } + + /// Checks whether a new era should be triggered, based on the provided `BlockNumber` argument + /// or possibly other protocol state parameters. + pub fn is_new_era(&self, now: BlockNumber) -> bool { + self.next_era_start <= now + } + + /// Triggers the next period type, updating appropriate parameters. + pub fn next_period_type(&mut self, ending_era: EraNumber, next_era_start: BlockNumber) { + let period_number = if self.period_type() == PeriodType::BuildAndEarn { + self.period_number().saturating_add(1) + } else { + self.period_number() + }; + + self.period_info = PeriodInfo { + number: period_number, + period_type: self.period_type().next(), + ending_era, + }; + self.next_era_start = next_era_start; + } +} + +/// State in which some dApp is in. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub enum DAppState { /// dApp is registered and active. @@ -302,42 +383,6 @@ pub struct DAppInfo { pub reward_destination: Option, } -/// How much was locked in a specific era -#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct LockedChunk { - #[codec(compact)] - pub amount: Balance, - #[codec(compact)] - pub era: EraNumber, -} - -impl Default for LockedChunk { - fn default() -> Self { - Self { - amount: Balance::zero(), - era: EraNumber::zero(), - } - } -} - -impl AmountEraPair for LockedChunk { - fn get_amount(&self) -> Balance { - self.amount - } - fn get_era(&self) -> EraNumber { - self.era - } - fn set_era(&mut self, era: EraNumber) { - self.era = era; - } - fn saturating_accrue(&mut self, increase: Balance) { - self.amount.saturating_accrue(increase); - } - fn saturating_reduce(&mut self, reduction: Balance) { - self.amount.saturating_reduce(reduction); - } -} - /// How much was unlocked in some block. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub struct UnlockingChunk { @@ -397,15 +442,14 @@ impl AmountEraPair for StakeChunk { /// General info about user's stakes #[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] -#[scale_info(skip_type_params(LockedLen, UnlockingLen, StakedLen))] +#[scale_info(skip_type_params(UnlockingLen, StakedLen))] pub struct AccountLedger< BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, - LockedLen: Get, UnlockingLen: Get, StakedLen: Get, > { - /// How much was staked in each era - pub locked: SparseBoundedAmountEraVec, + /// How much active locked amount an account has. + pub locked: Balance, /// How much started unlocking on a certain block pub unlocking: BoundedVec, UnlockingLen>, /// How much user had staked in some period @@ -414,17 +458,16 @@ pub struct AccountLedger< pub staked_period: Option, } -impl Default - for AccountLedger +impl Default + for AccountLedger where BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, - LockedLen: Get, UnlockingLen: Get, StakedLen: Get, { fn default() -> Self { Self { - locked: SparseBoundedAmountEraVec(BoundedVec::::default()), + locked: Balance::zero(), unlocking: BoundedVec::, UnlockingLen>::default(), staked: SparseBoundedAmountEraVec(BoundedVec::::default()), staked_period: None, @@ -432,29 +475,23 @@ where } } -impl - AccountLedger +impl AccountLedger where BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, - LockedLen: Get, UnlockingLen: Get, StakedLen: Get, { /// Empty if no locked/unlocking/staked info exists. pub fn is_empty(&self) -> bool { - self.locked.0.is_empty() && self.unlocking.is_empty() && self.staked.0.is_empty() - } - - /// Returns latest locked chunk if it exists, `None` otherwise - pub fn latest_locked_chunk(&self) -> Option<&LockedChunk> { - self.locked.0.last() + self.locked.is_zero() && self.unlocking.is_empty() && self.staked.0.is_empty() } /// Returns active locked amount. /// If `zero`, means that associated account hasn't got any active locked funds. + /// + /// It is possible that some funds are undergoing the unlocking period, but they aren't considered active in that case. pub fn active_locked_amount(&self) -> Balance { - self.latest_locked_chunk() - .map_or(Balance::zero(), |locked| locked.amount) + self.locked } /// Returns unlocking amount. @@ -472,53 +509,14 @@ where .saturating_add(self.unlocking_amount()) } - /// Returns latest era in which locked amount was updated or zero in case no lock amount exists - pub fn lock_era(&self) -> EraNumber { - self.latest_locked_chunk() - .map_or(EraNumber::zero(), |locked| locked.era) + /// Adds the specified amount to the total locked amount. + pub fn add_lock_amount(&mut self, amount: Balance) { + self.locked.saturating_accrue(amount); } - /// Active staked balance. - /// - /// In case latest stored information is from the past period, active stake is considered to be zero. - pub fn active_stake(&self, active_period: PeriodNumber) -> Balance { - match self.staked_period { - Some(last_staked_period) if last_staked_period == active_period => self - .staked - .0 - .last() - .map_or(Balance::zero(), |chunk| chunk.amount), - _ => Balance::zero(), - } - } - - /// Adds the specified amount to the total locked amount, if possible. - /// Caller must ensure that the era matches the next one, not the current one. - /// - /// If entry for the specified era already exists, it's updated. - /// - /// If entry for the specified era doesn't exist, it's created and insertion is attempted. - /// In case vector has no more capacity, error is returned, and whole operation is a noop. - pub fn add_lock_amount( - &mut self, - amount: Balance, - era: EraNumber, - ) -> Result<(), SparseBoundedError> { - self.locked.add_amount(amount, era) - } - - /// Subtracts the specified amount of the total locked amount, if possible. - /// - /// If entry for the specified era already exists, it's updated. - /// - /// If entry for the specified era doesn't exist, it's created and insertion is attempted. - /// In case vector has no more capacity, error is returned, and whole operation is a noop. - pub fn subtract_lock_amount( - &mut self, - amount: Balance, - era: EraNumber, - ) -> Result<(), SparseBoundedError> { - self.locked.subtract_amount(amount, era) + /// Subtracts the specified amount of the total locked amount. + pub fn subtract_lock_amount(&mut self, amount: Balance) { + self.locked.saturating_reduce(amount); } /// Adds the specified amount to the unlocking chunks. @@ -531,7 +529,7 @@ where &mut self, amount: Balance, unlock_block: BlockNumber, - ) -> Result<(), SparseBoundedError> { + ) -> Result<(), AccountLedgerError> { if amount.is_zero() { return Ok(()); } @@ -551,7 +549,7 @@ where }; self.unlocking .try_insert(idx, new_unlocking_chunk) - .map_err(|_| SparseBoundedError::NoCapacity)?; + .map_err(|_| AccountLedgerError::NoCapacity)?; } } @@ -589,6 +587,112 @@ where amount } + + /// Active staked balance. + /// + /// In case latest stored information is from the past period, active stake is considered to be zero. + pub fn active_stake(&self, active_period: PeriodNumber) -> Balance { + match self.staked_period { + Some(last_staked_period) if last_staked_period == active_period => self + .staked + .0 + .last() + .map_or(Balance::zero(), |chunk| chunk.amount), + _ => Balance::zero(), + } + } + + /// Amount that is available for staking. + /// + /// This is equal to the total active locked amount, minus the staked amount already active. + pub fn stakeable_amount(&self, active_period: PeriodNumber) -> Balance { + self.active_locked_amount() + .saturating_sub(self.active_stake(active_period)) + } + + /// Amount that is staked, in respect to currently active period. + pub fn staked_amount(&self, active_period: PeriodNumber) -> Balance { + match self.staked_period { + Some(last_staked_period) if last_staked_period == active_period => self + .staked + .0 + .last() + // We should never fallback to the default value since that would mean ledger is in invalid state. + // TODO: perhaps this can be implemented in a better way to have some error handling? Returning 0 might not be the most secure way to handle it. + .map_or(Balance::zero(), |chunk| chunk.amount), + _ => Balance::zero(), + } + } + + /// Adds the specified amount to total staked amount, if possible. + /// + /// Staking is only allowed if one of the two following conditions is met: + /// 1. Staker is staking again in the period in which they already staked. + /// 2. Staker is staking for the first time in this period, and there are no staking chunks from the previous eras. + /// + /// Additonally, the staked amount must not exceed what's available for staking. + pub fn add_stake_amount( + &mut self, + amount: Balance, + era: EraNumber, + current_period: PeriodNumber, + ) -> Result<(), AccountLedgerError> { + if amount.is_zero() { + return Ok(()); + } + + match self.staked_period { + Some(last_staked_period) if last_staked_period != current_period => { + return Err(AccountLedgerError::InvalidPeriod); + } + _ => (), + } + + if self.stakeable_amount(current_period) < amount { + return Err(AccountLedgerError::UnavailableStakeFunds); + } + + self.staked.add_amount(amount, era)?; + self.staked_period = Some(current_period); + + Ok(()) + } + + /// Subtracts the specified amount from the total staked amount, if possible. + /// + /// Unstaking will reduce total stake for the current era, and next era(s). + /// The specified amount must not exceed what's available for staking. + pub fn unstake_amount( + &mut self, + amount: Balance, + era: EraNumber, + current_period: PeriodNumber, + ) -> Result<(), AccountLedgerError> { + if amount.is_zero() { + return Ok(()); + } + + // Cannot unstake if the period has passed. + match self.staked_period { + Some(last_staked_period) if last_staked_period != current_period => { + return Err(AccountLedgerError::InvalidPeriod); + } + _ => (), + } + + // User must be precise with their unstake amount. + if self.staked_amount(current_period) < amount { + return Err(AccountLedgerError::UnstakeAmountLargerThanStake); + } + + self.staked.subtract_amount(amount, era) + } + + /// Last era for which a stake entry exists. + /// If no stake entries exist, returns `None`. + pub fn last_stake_era(&self) -> Option { + self.staked.0.last().map(|chunk| chunk.era) + } } /// Rewards pool for stakers & dApps @@ -602,6 +706,73 @@ pub struct RewardInfo { pub dapps: Balance, } +// TODO: it would be nice to implement add/subtract logic on this struct and use it everywhere +// we need to keep track of staking amount for periods. Right now I have logic duplication which is not good. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct StakeAmount { + /// Amount of staked funds accounting for the voting period. + #[codec(compact)] + voting: Balance, + /// Amount of staked funds accounting for the build&earn period. + #[codec(compact)] + build_and_earn: Balance, +} + +impl StakeAmount { + /// Create new instance of `StakeAmount` with specified `voting` and `build_and_earn` amounts. + pub fn new(voting: Balance, build_and_earn: Balance) -> Self { + Self { + voting, + build_and_earn, + } + } + + /// Total amount staked in both period types. + pub fn total(&self) -> Balance { + self.voting.saturating_add(self.build_and_earn) + } + + /// Amount staked for the specified period type. + pub fn for_type(&self, period_type: PeriodType) -> Balance { + match period_type { + PeriodType::Voting => self.voting, + PeriodType::BuildAndEarn => self.build_and_earn, + } + } + + // TODO: rename to add? + /// Stake the specified `amount` for the specified `period_type`. + pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { + match period_type { + PeriodType::Voting => self.voting.saturating_accrue(amount), + PeriodType::BuildAndEarn => self.build_and_earn.saturating_accrue(amount), + } + } + + // TODO: rename to subtract? + /// Unstake the specified `amount` for the specified `period_type`. + /// + /// In case period type is `Voting`, the amount is subtracted from the voting period. + /// + /// In case period type is `Build&Earn`, the amount is first subtracted from the + /// build&earn amount, and any rollover is subtracted from the voting period. + pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) { + match period_type { + PeriodType::Voting => self.voting.saturating_reduce(amount), + PeriodType::BuildAndEarn => { + if self.build_and_earn >= amount { + self.build_and_earn.saturating_reduce(amount); + } else { + // Rollover from build&earn to voting, is guaranteed to be larger than zero due to previous check + let remainder = amount.saturating_sub(self.build_and_earn); + self.build_and_earn = Balance::zero(); + self.voting.saturating_reduce(remainder); + } + } + } + } +} + /// Info about current era, including the rewards, how much is locked, unlocking, etc. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct EraInfo { @@ -619,6 +790,10 @@ pub struct EraInfo { /// This amount still counts into locked amount. #[codec(compact)] pub unlocking: Balance, + /// Stake amount valid for the ongoing era. + pub current_stake_amount: StakeAmount, + /// Stake amount valid from the next era. + pub next_stake_amount: StakeAmount, } impl EraInfo { @@ -638,4 +813,452 @@ impl EraInfo { pub fn unlocking_removed(&mut self, amount: Balance) { self.unlocking.saturating_reduce(amount); } + + /// Add the specified `amount` to the appropriate stake amount, based on the `PeriodType`. + pub fn add_stake_amount(&mut self, amount: Balance, period_type: PeriodType) { + self.next_stake_amount.stake(amount, period_type); + } + + /// Subtract the specified `amount` from the appropriate stake amount, based on the `PeriodType`. + pub fn unstake_amount(&mut self, amount: Balance, period_type: PeriodType) { + self.current_stake_amount.unstake(amount, period_type); + self.next_stake_amount.unstake(amount, period_type); + } + + /// Total staked amount in this era. + pub fn total_staked_amount(&self) -> Balance { + self.current_stake_amount.total() + } + + /// Staked amount of specified `type` in this era. + pub fn staked_amount(&self, period_type: PeriodType) -> Balance { + self.current_stake_amount.for_type(period_type) + } + + /// Total staked amount in the next era. + pub fn total_staked_amount_next_era(&self) -> Balance { + self.next_stake_amount.total() + } + + /// Staked amount of specifeid `type` in the next era. + pub fn staked_amount_next_era(&self, period_type: PeriodType) -> Balance { + self.next_stake_amount.for_type(period_type) + } + + /// Updates `Self` to reflect the transition to the next era. + /// + /// ## Args + /// `next_period_type` - `None` if no period type change, `Some(type)` if `type` is starting from the next era. + pub fn migrate_to_next_era(&mut self, next_period_type: Option) { + self.rewards = Default::default(); + self.active_era_locked = self.total_locked; + match next_period_type { + // If next era marks start of new voting period period, it means we're entering a new period + Some(PeriodType::Voting) => { + self.current_stake_amount = Default::default(); + self.next_stake_amount = Default::default(); + } + Some(PeriodType::BuildAndEarn) | None => { + self.current_stake_amount = self.next_stake_amount; + } + }; + } +} + +/// Information about how much a particular staker staked on a particular smart contract. +/// +/// Keeps track of amount staked in the 'voting period', as well as 'build&earn period'. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct SingularStakingInfo { + /// Total amount staked during the voting period. + #[codec(compact)] + vp_staked_amount: Balance, + /// Total amount staked during the build&earn period. + #[codec(compact)] + bep_staked_amount: Balance, + /// Period number for which this entry is relevant. + #[codec(compact)] + period: PeriodNumber, + /// Indicates whether a staker is a loyal staker or not. + loyal_staker: bool, +} + +impl SingularStakingInfo { + /// Creates new instance of the struct. + /// + /// ## Args + /// + /// `period` - period number for which this entry is relevant. + /// `period_type` - period type during which this entry is created. + pub fn new(period: PeriodNumber, period_type: PeriodType) -> Self { + Self { + vp_staked_amount: Balance::zero(), + bep_staked_amount: Balance::zero(), + period, + // Loyalty staking is only possible if stake is first made during the voting period. + loyal_staker: period_type == PeriodType::Voting, + } + } + + /// Stake the specified amount on the contract, for the specified period type. + pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { + match period_type { + PeriodType::Voting => self.vp_staked_amount.saturating_accrue(amount), + PeriodType::BuildAndEarn => self.bep_staked_amount.saturating_accrue(amount), + } + } + + /// Unstakes some of the specified amount from the contract. + /// + /// In case the `amount` being unstaked is larger than the amount staked in the `voting period`, + /// and `voting period` has passed, this will remove the _loyalty_ flag from the staker. + /// + /// Returns the amount that was unstaked from the `voting period` stake, and from the `build&earn period` stake. + pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) -> (Balance, Balance) { + // If B&E period stake can cover the unstaking amount, just reduce it. + if self.bep_staked_amount >= amount { + self.bep_staked_amount.saturating_reduce(amount); + (Balance::zero(), amount) + } else { + // In case we have to dip into the voting period stake, make sure B&E period stake is reduced first. + // Also make sure to remove loyalty flag from the staker. + let vp_staked_amount_snapshot = self.vp_staked_amount; + let bep_amount_snapshot = self.bep_staked_amount; + let leftover_amount = amount.saturating_sub(self.bep_staked_amount); + + self.vp_staked_amount.saturating_reduce(leftover_amount); + self.bep_staked_amount = Balance::zero(); + + // It's ok if staker reduces their stake amount during voting period. + // Once loyalty flag is removed, it cannot be returned. + self.loyal_staker = self.loyal_staker && period_type == PeriodType::Voting; + + // Actual amount that was unstaked: (voting period unstake, B&E period unstake) + ( + vp_staked_amount_snapshot.saturating_sub(self.vp_staked_amount), + bep_amount_snapshot, + ) + } + } + + /// Total staked on the contract by the user. Both period type stakes are included. + pub fn total_staked_amount(&self) -> Balance { + self.vp_staked_amount.saturating_add(self.bep_staked_amount) + } + + /// Returns amount staked in the specified period. + pub fn staked_amount(&self, period_type: PeriodType) -> Balance { + match period_type { + PeriodType::Voting => self.vp_staked_amount, + PeriodType::BuildAndEarn => self.bep_staked_amount, + } + } + + /// If `true` staker has staked during voting period and has never reduced their sta + pub fn is_loyal(&self) -> bool { + self.loyal_staker + } + + /// Period for which this entry is relevant. + pub fn period_number(&self) -> PeriodNumber { + self.period + } + + /// `true` if no stake exists, `false` otherwise. + pub fn is_empty(&self) -> bool { + self.vp_staked_amount.is_zero() && self.bep_staked_amount.is_zero() + } +} + +/// Information about how much was staked on a contract during a specific era or period. +/// +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +pub struct ContractStakingInfo { + #[codec(compact)] + vp_staked_amount: Balance, + #[codec(compact)] + bep_staked_amount: Balance, + #[codec(compact)] + era: EraNumber, + #[codec(compact)] + period: PeriodNumber, +} + +impl ContractStakingInfo { + /// Create new instance of `ContractStakingInfo` with specified era & period. + /// These parameters are immutable. + /// + /// Staked amounts are initialized to zero and can be increased or decreased. + pub fn new(era: EraNumber, period: PeriodNumber) -> Self { + Self { + vp_staked_amount: Balance::zero(), + bep_staked_amount: Balance::zero(), + era, + period, + } + } + + /// Total staked amount on the contract. + pub fn total_staked_amount(&self) -> Balance { + self.vp_staked_amount.saturating_add(self.bep_staked_amount) + } + + /// Staked amount of the specified period type. + /// + /// Note: + /// It is possible that voting period stake is reduced during the build&earn period. + /// This is because stakers can unstake their funds during the build&earn period, which can + /// chip away from the voting period stake. + pub fn staked_amount(&self, period_type: PeriodType) -> Balance { + match period_type { + PeriodType::Voting => self.vp_staked_amount, + PeriodType::BuildAndEarn => self.bep_staked_amount, + } + } + + /// Era for which this entry is relevant. + pub fn era(&self) -> EraNumber { + self.era + } + + /// Period for which this entry is relevant. + pub fn period(&self) -> PeriodNumber { + self.period + } + + /// Stake specified `amount` on the contract, for the specified `period_type`. + pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { + match period_type { + PeriodType::Voting => self.vp_staked_amount.saturating_accrue(amount), + PeriodType::BuildAndEarn => self.bep_staked_amount.saturating_accrue(amount), + } + } + + /// Unstake specified `amount` from the contract, for the specified `period_type`. + pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) { + match period_type { + PeriodType::Voting => self.vp_staked_amount.saturating_reduce(amount), + PeriodType::BuildAndEarn => { + let overflow = amount.saturating_sub(self.bep_staked_amount); + self.bep_staked_amount.saturating_reduce(amount); + self.vp_staked_amount.saturating_reduce(overflow); + } + } + } + + /// `true` if no stake exists, `false` otherwise. + pub fn is_empty(&self) -> bool { + self.vp_staked_amount.is_zero() && self.bep_staked_amount.is_zero() + } +} + +const STAKING_SERIES_HISTORY: u32 = 3; + +/// Composite type that holds information about how much was staked on a contract during some past eras & periods, including the current era & period. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct ContractStakingInfoSeries( + BoundedVec>, +); +impl ContractStakingInfoSeries { + /// Helper function to create a new instance of `ContractStakingInfoSeries`. + #[cfg(test)] + pub fn new(inner: Vec) -> Self { + Self(BoundedVec::try_from(inner).expect("Test should ensure this is always valid")) + } + + /// Returns inner `Vec` of `ContractStakingInfo` instances. Useful for testing. + #[cfg(test)] + pub fn inner(&self) -> Vec { + self.0.clone().into_inner() + } + + /// Length of the series. + pub fn len(&self) -> usize { + self.0.len() + } + + /// `true` if series is empty, `false` otherwise. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the `ContractStakingInfo` type for the specified era & period, if it exists. + pub fn get(&self, era: EraNumber, period: PeriodNumber) -> Option { + let idx = self.0.binary_search_by(|info| info.era().cmp(&era)); + + // There are couple of distinct scenarios: + // 1. Era exists, so we just return it. + // 2. Era doesn't exist, and ideal index is zero, meaning there's nothing in history that would cover this era. + // 3. Era doesn't exist, and ideal index is greater than zero, meaning we can potentially use one of the previous entries to derive the information. + // 3.1. In case periods are matching, we return that value. + // 3.2. In case periods aren't matching, we return `None` since stakes don't carry over between periods. + match idx { + Ok(idx) => self.0.get(idx).map(|x| *x), + Err(ideal_idx) => { + if ideal_idx.is_zero() { + None + } else { + match self.0.get(ideal_idx - 1) { + Some(info) if info.period() == period => { + let mut info = *info; + info.era = era; + Some(info) + } + _ => None, + } + } + } + } + } + + /// Last era for which a stake entry exists, `None` if no entries exist. + pub fn last_stake_era(&self) -> Option { + self.0.last().map(|info| info.era()) + } + + /// Last period for which a stake entry exists, `None` if no entries exist. + pub fn last_stake_period(&self) -> Option { + self.0.last().map(|info| info.period()) + } + + /// Total staked amount on the contract, in the active period. + pub fn total_staked_amount(&self, active_period: PeriodNumber) -> Balance { + match self.0.last() { + Some(last_element) if last_element.period() == active_period => { + last_element.total_staked_amount() + } + _ => Balance::zero(), + } + } + + /// Staked amount on the contract, for specified period type, in the active period. + pub fn staked_amount(&self, period: PeriodNumber, period_type: PeriodType) -> Balance { + match self.0.last() { + Some(last_element) if last_element.period() == period => { + last_element.staked_amount(period_type) + } + _ => Balance::zero(), + } + } + + /// Stake the specified `amount` on the contract, for the specified `period_type` and `era`. + pub fn stake( + &mut self, + amount: Balance, + period_info: PeriodInfo, + era: EraNumber, + ) -> Result<(), ()> { + // Defensive check to ensure we don't end up in a corrupted state. Should never happen. + if let Some(last_element) = self.0.last() { + if last_element.era() > era || last_element.period() > period_info.number { + return Err(()); + } + } + + // Get the most relevant `ContractStakingInfo` instance + let mut staking_info = if let Some(last_element) = self.0.last() { + if last_element.era() == era { + // Era matches, so we just update the last element. + let last_element = *last_element; + let _ = self.0.pop(); + last_element + } else if last_element.period() == period_info.number { + // Periods match so we should 'copy' the last element to get correct staking amount + let mut temp = *last_element; + temp.era = era; + temp + } else { + // It's a new period, so we need a completely new instance + ContractStakingInfo::new(era, period_info.number) + } + } else { + // It's a new period, so we need a completely new instance + ContractStakingInfo::new(era, period_info.number) + }; + + // Update the stake amount + staking_info.stake(amount, period_info.period_type); + + // Prune before pushing the new entry + self.prune(); + + // This should be infalible due to previous checks that ensure we don't end up overflowing the vector. + self.0.try_push(staking_info).map_err(|_| ()) + } + + /// Unstake the specified `amount` from the contract, for the specified `period_type` and `era`. + pub fn unstake( + &mut self, + amount: Balance, + period_info: PeriodInfo, + era: EraNumber, + ) -> Result<(), ()> { + // Defensive check to ensure we don't end up in a corrupted state. Should never happen. + if let Some(last_element) = self.0.last() { + // It's possible last element refers to the upcoming era, hence the "-1" on the 'era'. + if last_element.era().saturating_sub(1) > era + || last_element.period() > period_info.number + { + return Err(()); + } + } else { + // Vector is empty, should never happen. + return Err(()); + } + + // 1st step - remove the last element IFF it's for the next era. + // Unstake the requested amount from it. + let last_era_info = match self.0.last() { + Some(last_element) if last_element.era() == era.saturating_add(1) => { + let mut last_element = *last_element; + last_element.unstake(amount, period_info.period_type); + let _ = self.0.pop(); + Some(last_element) + } + _ => None, + }; + + // 2nd step - 3 options: + // 1. - last element has a matching era so we just update it. + // 2. - last element has a past era and matching period, so we'll create a new entry based on it. + // 3. - last element has a past era and past period, meaning it's invalid. + let second_last_era_info = if let Some(last_element) = self.0.last_mut() { + if last_element.era() == era { + last_element.unstake(amount, period_info.period_type); + None + } else if last_element.period() == period_info.number { + let mut new_entry = *last_element; + new_entry.unstake(amount, period_info.period_type); + new_entry.era = era; + Some(new_entry) + } else { + None + } + } else { + None + }; + + // 3rd step - push the new entries, if they exist. + if let Some(info) = second_last_era_info { + self.prune(); + self.0.try_push(info).map_err(|_| ())?; + } + if let Some(info) = last_era_info { + self.prune(); + self.0.try_push(info).map_err(|_| ())?; + } + + Ok(()) + } + + /// Used to remove past entries, in case vector is full. + fn prune(&mut self) { + // Prune the oldest entry if we have more than the limit + if self.0.len() == STAKING_SERIES_HISTORY as usize { + // TODO: this can be perhaps optimized so we prune entries which are very old. + // However, this makes the code more complex & more error prone. + // If kept like this, we always make sure we cover the history, and we never exceed it. + self.0.remove(0); + } + } } From 9b5f9881256cea78c94e693e251ac7088c718f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Tue, 14 Nov 2023 07:38:22 +0100 Subject: [PATCH 04/14] dapp staking v3 - part 4 (#1053) * EraRewardSpan * Initial version of claim_staker_reward * Tests * Test utils for claim-staker * Bug fixes, improvements * Claim improvements & some tests * Refactoring in progress * Refactoring continued * Refactoring progress * Refactoring finished * Bonus rewards * Docs & some minor changes * Comments, tests, improved coverage * Tier params & config init solution * Tier reward calculation WIP * Tier assignemnt * Minor cleanup * Claim dapp rewards * Claim dapp reward tests * unstake from unregistered call * Extra traits * fixes * Extra calls * Refactoring * More refactoring, improvements, TODO solving * Local integration * Genesis config * Add forcing call * try runtime build fix * Minor changes * Minor * Formatting * Benchmarks INIT * Compiling benchmarks * Fix * dapp tier calculation benchmark * Measured tier assignment * Decending rewards in benchmarks * Series refactoring & partial tests * Comments, minor changes * Tests, improvements * More tests, some minor refactoring * Formatting * More benchmarks & experiments, refactoring * Readme, docs * Minor renaming, docs * More docs * More docs * Minor addition * Review comment fixes & changes * Minor change * Review comments * Update frontier to make CI pass --- Cargo.lock | 52 +- Cargo.toml | 1 + bin/collator/Cargo.toml | 1 + bin/collator/src/local/chain_spec.rs | 42 +- pallets/dapp-staking-v3/Cargo.toml | 11 + pallets/dapp-staking-v3/README.md | 167 ++ pallets/dapp-staking-v3/src/benchmarking.rs | 246 +++ pallets/dapp-staking-v3/src/dsv3_weight.rs | 125 ++ pallets/dapp-staking-v3/src/lib.rs | 940 ++++++++- pallets/dapp-staking-v3/src/test/mock.rs | 140 +- pallets/dapp-staking-v3/src/test/mod.rs | 2 +- .../dapp-staking-v3/src/test/testing_utils.rs | 459 ++++- pallets/dapp-staking-v3/src/test/tests.rs | 722 +++++-- .../dapp-staking-v3/src/test/tests_types.rs | 1695 ++++++++-------- pallets/dapp-staking-v3/src/types.rs | 1788 +++++++++++------ runtime/local/Cargo.toml | 6 + runtime/local/src/lib.rs | 59 +- 17 files changed, 4487 insertions(+), 1969 deletions(-) create mode 100644 pallets/dapp-staking-v3/README.md create mode 100644 pallets/dapp-staking-v3/src/benchmarking.rs create mode 100644 pallets/dapp-staking-v3/src/dsv3_weight.rs diff --git a/Cargo.lock b/Cargo.lock index 536ec19859..fe18d417f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,7 @@ dependencies = [ "moonbeam-rpc-trace", "moonbeam-rpc-txpool", "pallet-block-reward", + "pallet-dapp-staking-v3", "pallet-ethereum", "pallet-evm", "pallet-transaction-payment", @@ -3065,7 +3066,7 @@ dependencies = [ [[package]] name = "fc-consensus" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "async-trait", "fp-consensus", @@ -3081,7 +3082,7 @@ dependencies = [ [[package]] name = "fc-db" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "async-trait", "fp-storage", @@ -3101,7 +3102,7 @@ dependencies = [ [[package]] name = "fc-mapping-sync" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fc-db", "fc-storage", @@ -3122,7 +3123,7 @@ dependencies = [ [[package]] name = "fc-rpc" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -3172,7 +3173,7 @@ dependencies = [ [[package]] name = "fc-rpc-core" version = "1.1.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -3185,7 +3186,7 @@ dependencies = [ [[package]] name = "fc-storage" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -3337,7 +3338,7 @@ dependencies = [ [[package]] name = "fp-account" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "hex", "impl-serde", @@ -3356,7 +3357,7 @@ dependencies = [ [[package]] name = "fp-consensus" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "parity-scale-codec", @@ -3368,7 +3369,7 @@ dependencies = [ [[package]] name = "fp-ethereum" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -3382,7 +3383,7 @@ dependencies = [ [[package]] name = "fp-evm" version = "3.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "evm", "frame-support", @@ -3397,7 +3398,7 @@ dependencies = [ [[package]] name = "fp-rpc" version = "3.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -3414,7 +3415,7 @@ dependencies = [ [[package]] name = "fp-self-contained" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "frame-support", "parity-scale-codec", @@ -3426,7 +3427,7 @@ dependencies = [ [[package]] name = "fp-storage" version = "2.0.0" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "parity-scale-codec", "serde", @@ -5524,6 +5525,7 @@ dependencies = [ "pallet-contracts", "pallet-contracts-primitives", "pallet-custom-signatures", + "pallet-dapp-staking-v3", "pallet-dapps-staking", "pallet-democracy", "pallet-ethereum", @@ -5558,6 +5560,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-api", + "sp-arithmetic", "sp-block-builder", "sp-consensus-aura", "sp-core", @@ -6791,7 +6794,7 @@ dependencies = [ [[package]] name = "pallet-base-fee" version = "1.0.0" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "frame-support", @@ -7110,6 +7113,7 @@ name = "pallet-dapp-staking-v3" version = "0.0.1-alpha" dependencies = [ "astar-primitives", + "frame-benchmarking", "frame-support", "frame-system", "num-traits", @@ -7222,7 +7226,7 @@ dependencies = [ [[package]] name = "pallet-ethereum" version = "4.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ethereum", "ethereum-types", @@ -7269,7 +7273,7 @@ dependencies = [ [[package]] name = "pallet-evm" version = "6.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "environmental", "evm", @@ -7294,7 +7298,7 @@ dependencies = [ [[package]] name = "pallet-evm-chain-id" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "frame-support", "frame-system", @@ -7359,7 +7363,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-blake2" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", ] @@ -7367,7 +7371,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-bn128" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "sp-core", @@ -7402,7 +7406,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-dispatch" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "frame-support", @@ -7412,7 +7416,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-ed25519" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "ed25519-dalek", "fp-evm", @@ -7421,7 +7425,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-modexp" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "num", @@ -7430,7 +7434,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-sha3fips" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "tiny-keccak", @@ -7439,7 +7443,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-simple" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#46cd85967849be241dac2bb72f561b942a463729" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" dependencies = [ "fp-evm", "ripemd", diff --git a/Cargo.toml b/Cargo.toml index fd6f3ee412..91789f677a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -274,6 +274,7 @@ pallet-block-reward = { path = "./pallets/block-reward", default-features = fals pallet-collator-selection = { path = "./pallets/collator-selection", default-features = false } pallet-custom-signatures = { path = "./pallets/custom-signatures", default-features = false } pallet-dapps-staking = { path = "./pallets/dapps-staking", default-features = false } +pallet-dapp-staking-v3 = { path = "./pallets/dapp-staking-v3", default-features = false } pallet-xc-asset-config = { path = "./pallets/xc-asset-config", default-features = false } pallet-xvm = { path = "./pallets/xvm", default-features = false } pallet-xcm = { path = "./pallets/pallet-xcm", default-features = false } diff --git a/bin/collator/Cargo.toml b/bin/collator/Cargo.toml index 3c7fccb18e..d8f46e7b2a 100644 --- a/bin/collator/Cargo.toml +++ b/bin/collator/Cargo.toml @@ -95,6 +95,7 @@ shiden-runtime = { workspace = true, features = ["std"] } # astar pallets dependencies astar-primitives = { workspace = true } pallet-block-reward = { workspace = true } +pallet-dapp-staking-v3 = { workspace = true } # frame dependencies frame-system = { workspace = true, features = ["std"] } diff --git a/bin/collator/src/local/chain_spec.rs b/bin/collator/src/local/chain_spec.rs index 8d56c8747c..1cde2e4dd7 100644 --- a/bin/collator/src/local/chain_spec.rs +++ b/bin/collator/src/local/chain_spec.rs @@ -20,17 +20,19 @@ use local_runtime::{ wasm_binary_unwrap, AccountId, AuraConfig, AuraId, BalancesConfig, BaseFeeConfig, - BlockRewardConfig, CouncilConfig, DemocracyConfig, EVMConfig, GenesisConfig, GrandpaConfig, - GrandpaId, Precompiles, Signature, SudoConfig, SystemConfig, TechnicalCommitteeConfig, - TreasuryConfig, VestingConfig, + BlockRewardConfig, CouncilConfig, DappStakingConfig, DemocracyConfig, EVMConfig, GenesisConfig, + GrandpaConfig, GrandpaId, Precompiles, Signature, SudoConfig, SystemConfig, + TechnicalCommitteeConfig, TreasuryConfig, VestingConfig, AST, }; use sc_service::ChainType; use sp_core::{crypto::Ss58Codec, sr25519, Pair, Public}; use sp_runtime::{ traits::{IdentifyAccount, Verify}, - Perbill, + Perbill, Permill, }; +use pallet_dapp_staking_v3::TierThreshold; + type AccountPublic = ::Signer; /// Specialized `ChainSpec` for Shiden Network. @@ -112,7 +114,7 @@ fn testnet_genesis( balances: endowed_accounts .iter() .cloned() - .map(|k| (k, 1_000_000_000_000_000_000_000_000_000)) + .map(|k| (k, 1_000_000_000 * AST)) .collect(), }, block_reward: BlockRewardConfig { @@ -181,6 +183,36 @@ fn testnet_genesis( }, democracy: DemocracyConfig::default(), treasury: TreasuryConfig::default(), + dapp_staking: DappStakingConfig { + reward_portion: vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ], + slot_distribution: vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ], + tier_thresholds: vec![ + TierThreshold::DynamicTvlAmount { + amount: 100 * AST, + minimum_amount: 80 * AST, + }, + TierThreshold::DynamicTvlAmount { + amount: 50 * AST, + minimum_amount: 40 * AST, + }, + TierThreshold::DynamicTvlAmount { + amount: 20 * AST, + minimum_amount: 20 * AST, + }, + TierThreshold::FixedTvlAmount { amount: 10 * AST }, + ], + slots_per_tier: vec![10, 20, 30, 40], + }, } } diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml index acc457ac8f..0de398ce18 100644 --- a/pallets/dapp-staking-v3/Cargo.toml +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -23,6 +23,8 @@ sp-std = { workspace = true } astar-primitives = { workspace = true } +frame-benchmarking = { workspace = true, optional = true } + [dev-dependencies] pallet-balances = { workspace = true } @@ -42,4 +44,13 @@ std = [ "frame-system/std", "pallet-balances/std", "astar-primitives/std", + "frame-benchmarking/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "astar-primitives/runtime-benchmarks", ] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md new file mode 100644 index 0000000000..afae398db2 --- /dev/null +++ b/pallets/dapp-staking-v3/README.md @@ -0,0 +1,167 @@ +# dApp Staking v3 + +## Introduction + +Astar and Shiden networks provide a unique way for developers to earn rewards by developing products that native token holders decide to support. + +The principle is simple - stakers lock their tokens to _stake_ on a dApp, and if the dApp attracts enough support, it is rewarded in native currency, derived from the inflation. +In turn stakers are rewarded for locking & staking their tokens. + +## Functionality Overview + +### Eras + +Eras are the basic _time unit_ in dApp staking and their length is measured in the number of blocks. + +They are not expected to last long, e.g. current live networks era length is roughly 1 day (7200 blocks). +After an era ends, it's usually possible to claim rewards for it, if user or dApp are eligible. + +### Periods + +Periods are another _time unit_ in dApp staking. They are expected to be more lengthy than eras. + +Each period consists of two subperiods: +* `Voting` +* `Build&Earn` + +Each period is denoted by a number, which increments each time a new period begins. +Period beginning is marked by the `voting` subperiod, after which follows the `build&earn` period. + +Stakes are **only** valid throughout a period. When new period starts, all stakes are reset to zero. This helps prevent projects remaining staked due to intertia. + +#### Voting + +When `Voting` starts, all _stakes_ are reset to **zero**. +Projects participating in dApp staking are expected to market themselves to (re)attract stakers. + +Stakers must assess whether the project they want to stake on brings value to the ecosystem, and then `vote` for it. +Casting a vote, or staking, during the `Voting` subperiod makes the staker eligible for bonus rewards. so they are encouraged to participate. + +`Voting` subperiod length is expressed in _standard_ era lengths, even though the entire voting period is treated as a single _voting era_. +E.g. if `voting` subperiod lasts for **10 eras**, and each era lasts for **100** blocks, total length of the `voting` subperiod will be **1000** blocks. + +Neither stakers nor dApps earn rewards during this subperiod - no new rewards are generated after `voting` subperiod ends. + +#### Build&Earn + +`Build&Earn` subperiod consits of one or more eras, therefore its length is expressed in eras. + +After each _era_ ends, eligible stakers and dApps can claim the rewards they earned. Rewards are **only** claimable for the finished eras. + +It is still possible to _stake_ during this period, and stakers are encouraged to do so since this will increase the rewards they earn. +The only exemption is the **final era** of the `build&earn` subperiod - it's not possible to _stake_ then since the stake would be invalid anyhow (stake is only valid from the next era which would be in the next period). + +### dApps & Smart Contracts + +Protocol is called dApp staking, but internally it essentially works with smart contracts, or even more precise, smart contract addresses. + +Throughout the code, when addressing a particular dApp, it's addressed as `smart contract`. Naming of the types & storage more closely follows `dApp` nomenclature. + +#### Registration + +Projects, or _dApps_, must be registered into protocol to participate. +Only a privileged `ManagerOrigin` can perform dApp registration. +The pallet itself does not make assumptions who the privileged origin is, and it can differ from runtime to runtime. + +There is a limit of how many smart contracts can be registered at once. Once the limit is reached, any additional attempt to register a new contract will fail. + +#### Reward Beneficiary & Ownership + +When contract is registered, it is assigned a unique compact numeric Id - 16 bit unsigned integer. This is important for the inner workings of the pallet, and is not directly exposed to the users. + +After a dApp has been registered, it is possible to modify reward beneficiary or even the owner of the dApp. The owner can perform reward delegation and can further transfer ownership. + +#### Unregistration + +dApp can be removed from the procotol by unregistering it. +This is a privileged action that only `ManagerOrigin` can perform. + +After a dApp has been unregistered, it's no longer eligible to receive rewards. +It's still possible however to claim past unclaimed rewards. + +Important to note that even if dApp has been unregistered, it still occupies a _slot_ +in the dApp staking protocol and counts towards maximum number of registered dApps. +This will be improved in the future when dApp data will be cleaned up after the period ends. + +### Stakers + +#### Locking Tokens + +In order for users to participate in dApp staking, the first step they need to take is lock some native currency. Reserved tokens cannot be locked, but tokens locked by another lock can be re-locked into dApp staking (double locked). + +**NOTE:** Locked funds cannot be used for paying fees, or for transfer. + +In order to participate, user must have a `MinimumLockedAmount` of native currency locked. This doesn't mean that they cannot lock _less_ in a single call, but total locked amount must always be equal or greater than `MinimumLockedAmount`. + +In case amount specified for locking is greater than what user has available, only what's available will be locked. + +#### Unlocking Tokens + +User can at any time decide to unlock their tokens. However, it's not possible to unlock tokens which are staked, so user has to unstake them first. + +Once _unlock_ is successfully executed, the tokens aren't immediately unlocked, but instead must undergo the unlocking process. Once unlocking process has finished, user can _claim_ their unlocked tokens into their free balance. + +There is a limited number of `unlocking chunks` a user can have at any point in time. If limit is reached, user must claim existing unlocked chunks, or wait for them to be unlocked before claiming them to free up space for new chunks. + +In case calling unlocking some amount would take the user below the `MinimumLockedAmount`, **everything** will be unlocked. + +For users who decide they would rather re-lock their tokens then wait for the unlocking process to finish, there's an option to do so. All currently unlocking chunks are consumed, and added back into locked amount. + +#### Staking Tokens + +Locked tokens, which aren't being used for staking, can be used to stake on a dApp. This translates to _voting_ or _nominating_ a dApp to receive rewards derived from the inflation. User can stake on multiple dApps if they want to. + +The staked amount **must be precise**, no adjustment will be made by the pallet in case a too large amount is specified. + +The staked amount is only eligible for rewards from the next era - in other words, only the amount that has been staked for the entire era is eligible to receive rewards. + +It is not possible to stake if there are unclaimed rewards from past eras. User must ensure to first claim their pending rewards, before staking. This is also beneficial to the users since it allows them to lock & stake the earned rewards as well. + +User's stake on a contract must be equal or greater than the `MinimumStakeAmount`. This is similar to the minimum lock amount, but this limit is per contract. + +Although user can stake on multiple smart contracts, the amount is limited. To be more precise, amount of database entries that can exist per user is limited. + +The protocol keeps track of how much was staked by the user in `voting` and `build&earn` subperiod. This is important for the bonus reward calculation. + +It is not possible to stake on a dApp that has been unregistered. +However, if dApp is unregistered after user has staked on it, user will keep earning +rewards for the staked amount. + +#### Unstaking Tokens + +User can at any time decide to unstake staked tokens. There's no _unstaking_ process associated with this action. + +Unlike stake operation, which stakes from the _next_ era, unstake will reduce the staked amount for the current and next era if stake exists. + +Same as with stake operation, it's not possible to unstake anything until unclaimed rewards have been claimed. User must ensure to first claim all rewards, before attempting to unstake. Unstake amount must also be precise as no adjustment will be done to the amount. + +The amount unstaked will always first reduce the amount staked in the ongoing subperiod. E.g. if `voting` subperiod has stake of **100**, and `build&earn` subperiod has stake of **50**, calling unstake with amount **70** during `build&earn` subperiod will see `build&earn` stake amount reduced to **zero**, while `voting` stake will be reduced to **80**. + +If unstake would reduce the staked amount below `MinimumStakeAmount`, everything is unstaked. + +Once period finishes, all stakes are reset back to zero. This means that no unstake operation is needed after period ends to _unstake_ funds - it's done automatically. + +If dApp has been unregistered, a special operation to unstake from unregistered contract must be used. + +#### Claiming Staker Rewards + +Stakers can claim rewards for passed eras during which they were staking. Even if multiple contracts were staked, claim reward call will claim rewards for all of them. + +Only rewards for passed eras can be claimed. It is possible that a successful reward claim call will claim rewards for multiple eras. This can happen if staker hasn't claimed rewards in some time, and many eras have passed since then, accumulating pending rewards. + +To achieve this, the pallet's underyling storage organizes **era reward information** into **spans**. A single span covers multiple eras, e.g. from **1** to **16**. In case user has staked during era 1, and hasn't claimed rewards until era 17, they will be eligible to claim 15 rewards in total (from era 2 to 16). All of this will be done in a single claim reward call. + +In case unclaimed history has built up past one span, multiple reward claim calls will be needed to claim all of the rewards. + +Rewards don't remain available forever, and if not claimed within some time period, they will be treated as expired. This will be a longer period, but will still exist. + +Rewards are calculated using a simple formula: `staker_reward_pool * staker_staked_amount / total_staked_amount`. + +#### Claim Bonus Reward + +If staker staked on a dApp during the voting period, and didn't reduce their staked amount below what was staked at the end of the voting period, this makes them eligible for the bonus reward. + +Bonus rewards need to be claimed per contract, unlike staker rewards. + +Bonus reward is calculated using a simple formula: `bonus_reward_pool * staker_voting_period_stake / total_voting_period_stake`. + diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs new file mode 100644 index 0000000000..e98fd1ce09 --- /dev/null +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -0,0 +1,246 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::{Pallet as DappStaking, *}; + +use astar_primitives::Balance; +use frame_benchmarking::v2::*; + +use frame_support::assert_ok; +use frame_system::{Pallet as System, RawOrigin}; + +// TODO: copy/paste from mock, make it more generic later + +/// Run to the specified block number. +/// Function assumes first block has been initialized. +fn run_to_block(n: BlockNumberFor) { + while System::::block_number() < n { + DappStaking::::on_finalize(System::::block_number()); + System::::set_block_number(System::::block_number() + One::one()); + // This is performed outside of dapps staking but we expect it before on_initialize + DappStaking::::on_initialize(System::::block_number()); + } +} + +/// Run for the specified number of blocks. +/// Function assumes first block has been initialized. +fn run_for_blocks(n: BlockNumberFor) { + run_to_block::(System::::block_number() + n); +} + +/// Advance blocks until the specified era has been reached. +/// +/// Function has no effect if era is already passed. +pub(crate) fn advance_to_era(era: EraNumber) { + assert!(era >= ActiveProtocolState::::get().era); + while ActiveProtocolState::::get().era < era { + run_for_blocks::(One::one()); + } +} + +/// Advance blocks until next era has been reached. +pub(crate) fn advance_to_next_era() { + advance_to_era::(ActiveProtocolState::::get().era + 1); +} + +// /// Advance blocks until the specified period has been reached. +// /// +// /// Function has no effect if period is already passed. +// pub(crate) fn advance_to_period(period: PeriodNumber) { +// assert!(period >= ActiveProtocolState::::get().period_number()); +// while ActiveProtocolState::::get().period_number() < period { +// run_for_blocks::(One::one()); +// } +// } + +// /// Advance blocks until next period has been reached. +// pub(crate) fn advance_to_next_period() { +// advance_to_period::(ActiveProtocolState::::get().period_number() + 1); +// } + +// /// Advance blocks until next period type has been reached. +// pub(crate) fn advance_to_next_subperiod() { +// let subperiod = ActiveProtocolState::::get().subperiod(); +// while ActiveProtocolState::::get().subperiod() == subperiod { +// run_for_blocks::(One::one()); +// } +// } + +// All our networks use 18 decimals for native currency so this should be fine. +const UNIT: Balance = 1_000_000_000_000_000_000; + +// Minimum amount that must be staked on a dApp to enter any tier +const MIN_TIER_THRESHOLD: Balance = 10 * UNIT; + +const NUMBER_OF_SLOTS: u16 = 100; + +pub fn initial_config() { + let era_length = T::StandardEraLength::get(); + let voting_period_length_in_eras = T::StandardErasPerVotingPeriod::get(); + + // Init protocol state + ActiveProtocolState::::put(ProtocolState { + era: 1, + next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + One::one(), + period_info: PeriodInfo { + number: 1, + subperiod: Subperiod::Voting, + subperiod_end_era: 2, + }, + maintenance: false, + }); + + // Init tier params + let tier_params = TierParameters:: { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { + amount: 100 * UNIT, + minimum_amount: 80 * UNIT, + }, + TierThreshold::DynamicTvlAmount { + amount: 50 * UNIT, + minimum_amount: 40 * UNIT, + }, + TierThreshold::DynamicTvlAmount { + amount: 20 * UNIT, + minimum_amount: 20 * UNIT, + }, + TierThreshold::FixedTvlAmount { + amount: MIN_TIER_THRESHOLD, + }, + ]) + .unwrap(), + }; + + // Init tier config, based on the initial params + let init_tier_config = TiersConfiguration:: { + number_of_slots: NUMBER_OF_SLOTS, + slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + reward_portion: tier_params.reward_portion.clone(), + tier_thresholds: tier_params.tier_thresholds.clone(), + }; + + assert!(tier_params.is_valid()); + assert!(init_tier_config.is_valid()); + + StaticTierParams::::put(tier_params); + TierConfig::::put(init_tier_config.clone()); + NextTierConfig::::put(init_tier_config); +} + +fn max_number_of_contracts() -> u32 { + T::MaxNumberOfContracts::get().min(NUMBER_OF_SLOTS).into() +} + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn dapp_tier_assignment(x: Linear<0, { max_number_of_contracts::() }>) { + // Prepare init config (protocol state, tier params & config, etc.) + initial_config::(); + + let developer: T::AccountId = whitelisted_caller(); + for id in 0..x { + let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + developer.clone().into(), + smart_contract, + )); + } + + // TODO: try to make this more "shuffled" so the generated vector ends up being more random + let mut amount = 1000 * MIN_TIER_THRESHOLD; + for id in 0..x { + let staker = account("staker", id.into(), 1337); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract, + amount, + )); + + // Slowly decrease the stake amount + amount.saturating_reduce(UNIT); + } + + // Advance to next era + advance_to_next_era::(); + + let reward_era = ActiveProtocolState::::get().era; + let reward_period = ActiveProtocolState::::get().period_number(); + let reward_pool = Balance::from(10_000 * UNIT as u128); + + #[block] + { + let dapp_tiers = + Pallet::::get_dapp_tier_assignment(reward_era, reward_period, reward_pool); + // TODO: how to move this outside of the 'block'? Cannot declare it outside, and then use it inside. + assert_eq!(dapp_tiers.dapps.len(), x as usize); + } + } + + #[benchmark] + fn experimental_read() { + // Prepare init config (protocol state, tier params & config, etc.) + initial_config::(); + + #[block] + { + let _ = ExperimentalContractEntries::::get(10); + } + } + + impl_benchmark_test_suite!( + Pallet, + crate::benchmarking::tests::new_test_ext(), + crate::test::mock::Test, + ); +} + +#[cfg(test)] +mod tests { + use crate::test::mock; + use sp_io::TestExternalities; + + pub fn new_test_ext() -> TestExternalities { + mock::ExtBuilder::build() + } +} diff --git a/pallets/dapp-staking-v3/src/dsv3_weight.rs b/pallets/dapp-staking-v3/src/dsv3_weight.rs new file mode 100644 index 0000000000..1dca8a56ce --- /dev/null +++ b/pallets/dapp-staking-v3/src/dsv3_weight.rs @@ -0,0 +1,125 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for pallet_dapp_staking_v3 +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-11-02, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Dinos-MBP`, CPU: `` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_dapp_staking_v3 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=dsv3_weight.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_dapp_staking_v3. +pub trait WeightInfo { + fn dapp_tier_assignment(x: u32, ) -> Weight; + fn experimental_read() -> Weight; +} + +/// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking IntegratedDApps (r:101 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:100 w:0) + /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(130), added: 2605, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 100]`. + fn dapp_tier_assignment(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `836 + x * (169 ±0)` + // Estimated: `3586 + x * (2605 ±0)` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(12_879_631, 3586) + // Standard Error: 18_480 + .saturating_add(Weight::from_parts(7_315_677, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2605).saturating_mul(x.into())) + } + /// Storage: DappStaking ExperimentalContractEntries (r:1 w:0) + /// Proof: DappStaking ExperimentalContractEntries (max_values: None, max_size: Some(3483), added: 5958, mode: MaxEncodedLen) + fn experimental_read() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `6948` + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(5_000_000, 6948) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking IntegratedDApps (r:101 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:100 w:0) + /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(130), added: 2605, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 100]`. + fn dapp_tier_assignment(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `836 + x * (169 ±0)` + // Estimated: `3586 + x * (2605 ±0)` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(12_879_631, 3586) + // Standard Error: 18_480 + .saturating_add(Weight::from_parts(7_315_677, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2605).saturating_mul(x.into())) + } + /// Storage: DappStaking ExperimentalContractEntries (r:1 w:0) + /// Proof: DappStaking ExperimentalContractEntries (max_values: None, max_size: Some(3483), added: 5958, mode: MaxEncodedLen) + fn experimental_read() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `6948` + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(5_000_000, 6948) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } +} diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index ba4e77f907..1a85e00348 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -17,6 +17,7 @@ // along with Astar. If not, see . //! # dApp Staking v3 Pallet +//! TODO //! //! - [`Config`] //! @@ -45,20 +46,32 @@ use frame_support::{ weights::Weight, }; use frame_system::pallet_prelude::*; -use sp_runtime::traits::{BadOrigin, Saturating, Zero}; +use sp_runtime::{ + traits::{BadOrigin, One, Saturating, Zero}, + Perbill, Permill, +}; +pub use sp_std::vec::Vec; use astar_primitives::Balance; -use crate::types::*; pub use pallet::*; #[cfg(test)] mod test; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + mod types; +use types::*; +pub use types::{PriceProvider, RewardPoolProvider, TierThreshold}; + +mod dsv3_weight; const STAKING_ID: LockIdentifier = *b"dapstake"; +// TODO: add tracing! + #[frame_support::pallet] pub mod pallet { use super::*; @@ -70,6 +83,11 @@ pub mod pallet { #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); + #[cfg(feature = "runtime-benchmarks")] + pub trait BenchmarkHelper { + fn get_smart_contract(id: u32) -> SmartContract; + } + #[pallet::config] pub trait Config: frame_system::Config { /// The overarching event type. @@ -89,6 +107,12 @@ pub mod pallet { /// Privileged origin for managing dApp staking pallet. type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; + /// Used to provide price information about the native token. + type NativePriceProvider: PriceProvider; + + /// Used to provide reward pools amount. + type RewardPoolProvider: RewardPoolProvider; + /// Length of a standard era in block numbers. #[pallet::constant] type StandardEraLength: Get; @@ -104,6 +128,15 @@ pub mod pallet { #[pallet::constant] type StandardErasPerBuildAndEarnPeriod: Get; + /// Maximum length of a single era reward span length entry. + #[pallet::constant] + type EraRewardSpanLength: Get; + + /// Number of periods for which we keep rewards available for claiming. + /// After that period, they are no longer claimable. + #[pallet::constant] + type RewardRetentionInPeriods: Get; + /// Maximum number of contracts that can be integrated into dApp staking at once. #[pallet::constant] type MaxNumberOfContracts: Get; @@ -116,17 +149,26 @@ pub mod pallet { #[pallet::constant] type MinimumLockedAmount: Get; - /// Amount of blocks that need to pass before unlocking chunks can be claimed by the owner. + /// Number of standard eras that need to pass before unlocking chunk can be claimed. + /// Even though it's expressed in 'eras', it's actually measured in number of blocks. #[pallet::constant] - type UnlockingPeriod: Get>; + type UnlockingPeriod: Get; - /// Maximum number of staking chunks that can exist per account at a time. + /// Maximum amount of stake entries contract is allowed to have at once. #[pallet::constant] - type MaxStakingChunks: Get; + type MaxNumberOfStakedContracts: Get; /// Minimum amount staker can stake on a contract. #[pallet::constant] type MinimumStakeAmount: Get; + + /// Number of different tiers. + #[pallet::constant] + type NumberOfTiers: Get; + + /// Helper trait for benchmarks. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: BenchmarkHelper; } #[pallet::event] @@ -136,7 +178,7 @@ pub mod pallet { NewEra { era: EraNumber }, /// New period has started. NewPeriod { - period_type: PeriodType, + subperiod: Subperiod, number: PeriodNumber, }, /// A smart contract has been registered for dApp staking @@ -192,6 +234,30 @@ pub mod pallet { smart_contract: T::SmartContract, amount: Balance, }, + /// Account has claimed some stake rewards. + Reward { + account: T::AccountId, + era: EraNumber, + amount: Balance, + }, + BonusReward { + account: T::AccountId, + smart_contract: T::SmartContract, + period: PeriodNumber, + amount: Balance, + }, + DAppReward { + beneficiary: T::AccountId, + smart_contract: T::SmartContract, + tier_id: TierId, + era: EraNumber, + amount: Balance, + }, + UnstakeFromUnregistered { + account: T::AccountId, + smart_contract: T::SmartContract, + amount: Balance, + }, } #[pallet::error] @@ -215,8 +281,6 @@ pub mod pallet { ZeroAmount, /// Total locked amount for staker is below minimum threshold. LockedAmountBelowThreshold, - /// Cannot add additional locked balance chunks due to capacity limit. - TooManyLockedBalanceChunks, /// Cannot add additional unlocking chunks due to capacity limit. TooManyUnlockingChunks, /// Remaining stake prevents entire balance of starting the unlocking process. @@ -229,8 +293,6 @@ pub mod pallet { UnavailableStakeFunds, /// There are unclaimed rewards remaining from past periods. They should be claimed before staking again. UnclaimedRewardsFromPastPeriods, - /// Cannot add additional stake chunks due to capacity limit. - TooManyStakeChunks, /// An unexpected error occured while trying to stake. InternalStakeError, /// Total staked amount on contract is below the minimum required value. @@ -245,6 +307,29 @@ pub mod pallet { NoStakingInfo, /// An unexpected error occured while trying to unstake. InternalUnstakeError, + /// Rewards are no longer claimable since they are too old. + RewardExpired, + /// There are no claimable rewards. + NoClaimableRewards, + /// An unexpected error occured while trying to claim staker rewards. + InternalClaimStakerError, + /// Account is has no eligible stake amount for bonus reward. + NotEligibleForBonusReward, + /// An unexpected error occured while trying to claim bonus reward. + InternalClaimBonusError, + /// Claim era is invalid - it must be in history, and rewards must exist for it. + InvalidClaimEra, + /// No dApp tier info exists for the specified era. This can be because era has expired + /// or because during the specified era there were no eligible rewards or protocol wasn't active. + NoDAppTierInfo, + /// dApp reward has already been claimed for this era. + DAppRewardAlreadyClaimed, + /// An unexpected error occured while trying to claim dApp reward. + InternalClaimDAppError, + /// Contract is still active, not unregistered. + ContractStillActive, + /// There are too many contract stake entries for the account. This can be cleaned up by either unstaking or cleaning expired entries. + TooManyStakedContracts, } /// General information about dApp staking protocol state. @@ -256,6 +341,7 @@ pub mod pallet { #[pallet::storage] pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; + // TODO: where to track TierLabels? E.g. a label to bootstrap a dApp into a specific tier. /// Map of all dApps integrated into dApp staking protocol. #[pallet::storage] pub type IntegratedDApps = CountedStorageMap< @@ -286,17 +372,132 @@ pub mod pallet { /// Information about how much has been staked on a smart contract in some era or period. #[pallet::storage] pub type ContractStake = - StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakingInfoSeries, ValueQuery>; + StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakeAmount, ValueQuery>; /// General information about the current era. #[pallet::storage] pub type CurrentEraInfo = StorageValue<_, EraInfo, ValueQuery>; + /// Information about rewards for each era. + /// + /// Since each entry is a 'span', covering up to `T::EraRewardSpanLength` entries, only certain era value keys can exist in storage. + /// For the sake of simplicity, valid `era` keys are calculated as: + /// + /// era_key = era - (era % T::EraRewardSpanLength) + /// + /// This means that e.g. in case `EraRewardSpanLength = 8`, only era values 0, 8, 16, 24, etc. can exist in storage. + /// Eras 1-7 will be stored in the same entry as era 0, eras 9-15 will be stored in the same entry as era 8, etc. + #[pallet::storage] + pub type EraRewards = + StorageMap<_, Twox64Concat, EraNumber, EraRewardSpan, OptionQuery>; + + /// Information about period's end. + #[pallet::storage] + pub type PeriodEnd = + StorageMap<_, Twox64Concat, PeriodNumber, PeriodEndInfo, OptionQuery>; + + /// Static tier parameters used to calculate tier configuration. + #[pallet::storage] + pub type StaticTierParams = + StorageValue<_, TierParameters, ValueQuery>; + + /// Tier configuration to be used during the newly started period + #[pallet::storage] + pub type NextTierConfig = + StorageValue<_, TiersConfiguration, ValueQuery>; + + /// Tier configuration user for current & preceding eras. + #[pallet::storage] + pub type TierConfig = + StorageValue<_, TiersConfiguration, ValueQuery>; + + /// Information about which tier a dApp belonged to in a specific era. + #[pallet::storage] + pub type DAppTiers = + StorageMap<_, Twox64Concat, EraNumber, DAppTierRewardsFor, OptionQuery>; + + // TODO: this is experimental, please don't review + #[pallet::storage] + pub type ExperimentalContractEntries = + StorageMap<_, Twox64Concat, EraNumber, ContractEntriesFor, OptionQuery>; + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + pub reward_portion: Vec, + pub slot_distribution: Vec, + pub tier_thresholds: Vec, + pub slots_per_tier: Vec, + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + // Prepare tier parameters & verify their correctness + let tier_params = TierParameters:: { + reward_portion: BoundedVec::::try_from( + self.reward_portion.clone(), + ) + .expect("Invalid number of reward portions provided."), + slot_distribution: BoundedVec::::try_from( + self.slot_distribution.clone(), + ) + .expect("Invalid number of slot distributions provided."), + tier_thresholds: BoundedVec::::try_from( + self.tier_thresholds.clone(), + ) + .expect("Invalid number of tier thresholds provided."), + }; + assert!( + tier_params.is_valid(), + "Invalid tier parameters values provided." + ); + + // Prepare tier configuration and verify its correctness + let number_of_slots = self.slots_per_tier.iter().fold(0_u16, |acc, &slots| { + acc.checked_add(slots).expect("Overflow") + }); + let tier_config = TiersConfiguration:: { + number_of_slots, + slots_per_tier: BoundedVec::::try_from( + self.slots_per_tier.clone(), + ) + .expect("Invalid number of slots per tier entries provided."), + reward_portion: tier_params.reward_portion.clone(), + tier_thresholds: tier_params.tier_thresholds.clone(), + }; + assert!( + tier_params.is_valid(), + "Invalid tier config values provided." + ); + + // Prepare initial protocol state + let protocol_state = ProtocolState { + era: 1, + next_era_start: Pallet::::blocks_per_voting_period() + 1_u32.into(), + period_info: PeriodInfo { + number: 1, + subperiod: Subperiod::Voting, + subperiod_end_era: 2, + }, + maintenance: false, + }; + + // Initialize necessary storage items + ActiveProtocolState::::put(protocol_state); + StaticTierParams::::put(tier_params); + TierConfig::::put(tier_config.clone()); + NextTierConfig::::put(tier_config); + } + } + #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(now: BlockNumberFor) -> Weight { let mut protocol_state = ActiveProtocolState::::get(); + // TODO: maybe do lazy history cleanup in this function? + // We should not modify pallet storage while in maintenance mode. // This is a safety measure, since maintenance mode is expected to be // enabled in case some misbehavior or corrupted storage is detected. @@ -310,67 +511,125 @@ pub mod pallet { } let mut era_info = CurrentEraInfo::::get(); - let next_era = protocol_state.era.saturating_add(1); - let maybe_period_event = match protocol_state.period_type() { - PeriodType::Voting => { - // For the sake of consistency - let ending_era = + + let current_era = protocol_state.era; + let next_era = current_era.saturating_add(1); + let (maybe_period_event, era_reward) = match protocol_state.subperiod() { + Subperiod::Voting => { + // For the sake of consistency, we put zero reward into storage + let era_reward = EraReward { + staker_reward_pool: Balance::zero(), + staked: era_info.total_staked_amount(), + dapp_reward_pool: Balance::zero(), + }; + + let subperiod_end_era = next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); let build_and_earn_start_block = now.saturating_add(T::StandardEraLength::get()); - protocol_state.next_period_type(ending_era, build_and_earn_start_block); + protocol_state + .advance_to_next_subperiod(subperiod_end_era, build_and_earn_start_block); + + era_info.migrate_to_next_era(Some(protocol_state.subperiod())); - era_info.migrate_to_next_era(Some(protocol_state.period_type())); + // Update tier configuration to be used when calculating rewards for the upcoming eras + let next_tier_config = NextTierConfig::::take(); + TierConfig::::put(next_tier_config); - Some(Event::::NewPeriod { - period_type: protocol_state.period_type(), - number: protocol_state.period_number(), - }) + ( + Some(Event::::NewPeriod { + subperiod: protocol_state.subperiod(), + number: protocol_state.period_number(), + }), + era_reward, + ) } - PeriodType::BuildAndEarn => { - // TODO: trigger reward calculation here. This will be implemented later. + Subperiod::BuildAndEarn => { + let (staker_reward_pool, dapp_reward_pool) = + T::RewardPoolProvider::normal_reward_pools(); + let era_reward = EraReward { + staker_reward_pool, + staked: era_info.total_staked_amount(), + dapp_reward_pool, + }; + + // Distribute dapps into tiers, write it into storage + let dapp_tier_rewards = Self::get_dapp_tier_assignment( + current_era, + protocol_state.period_number(), + dapp_reward_pool, + ); + DAppTiers::::insert(¤t_era, dapp_tier_rewards); // Switch to `Voting` period if conditions are met. if protocol_state.period_info.is_next_period(next_era) { + // Store info about period end + let bonus_reward_pool = T::RewardPoolProvider::bonus_reward_pool(); + PeriodEnd::::insert( + &protocol_state.period_number(), + PeriodEndInfo { + bonus_reward_pool, + total_vp_stake: era_info.staked_amount(Subperiod::Voting), + final_era: current_era, + }, + ); + // For the sake of consistency we treat the whole `Voting` period as a single era. // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. - let ending_era = next_era.saturating_add(1); + let subperiod_end_era = next_era.saturating_add(1); let voting_period_length = Self::blocks_per_voting_period(); let next_era_start_block = now.saturating_add(voting_period_length); - protocol_state.next_period_type(ending_era, next_era_start_block); - - era_info.migrate_to_next_era(Some(protocol_state.period_type())); - - // TODO: trigger tier configuration calculation based on internal & external params. - - Some(Event::::NewPeriod { - period_type: protocol_state.period_type(), - number: protocol_state.period_number(), - }) + protocol_state + .advance_to_next_subperiod(subperiod_end_era, next_era_start_block); + + era_info.migrate_to_next_era(Some(protocol_state.subperiod())); + + // Re-calculate tier configuration for the upcoming new period + let tier_params = StaticTierParams::::get(); + let average_price = T::NativePriceProvider::average_price(); + let new_tier_config = + TierConfig::::get().calculate_new(average_price, &tier_params); + NextTierConfig::::put(new_tier_config); + + ( + Some(Event::::NewPeriod { + subperiod: protocol_state.subperiod(), + number: protocol_state.period_number(), + }), + era_reward, + ) } else { let next_era_start_block = now.saturating_add(T::StandardEraLength::get()); protocol_state.next_era_start = next_era_start_block; era_info.migrate_to_next_era(None); - None + (None, era_reward) } } }; + // Update storage items + protocol_state.era = next_era; ActiveProtocolState::::put(protocol_state); CurrentEraInfo::::put(era_info); + let era_span_index = Self::era_reward_span_index(current_era); + let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); + // TODO: Error "cannot" happen here. Log an error if it does though. + let _ = span.push(current_era, era_reward); + EraRewards::::insert(&era_span_index, span); + Self::deposit_event(Event::::NewEra { era: next_era }); if let Some(period_event) = maybe_period_event { Self::deposit_event(period_event); } // TODO: benchmark later - T::DbWeight::get().reads_writes(2, 2) + T::DbWeight::get().reads_writes(3, 3) } } @@ -434,13 +693,13 @@ pub mod pallet { Ok(()) } - /// Used to modify the reward destination account for a dApp. + /// Used to modify the reward beneficiary account for a dApp. /// /// Caller has to be dApp owner. /// If set to `None`, rewards will be deposited to the dApp owner. #[pallet::call_index(2)] #[pallet::weight(Weight::zero())] - pub fn set_dapp_reward_destination( + pub fn set_dapp_reward_beneficiary( origin: OriginFor, smart_contract: T::SmartContract, beneficiary: Option, @@ -516,7 +775,7 @@ pub mod pallet { /// Unregister dApp from dApp staking protocol, making it ineligible for future rewards. /// This doesn't remove the dApp completely from the system just yet, but it can no longer be used for staking. /// - /// Can be called by dApp owner or dApp staking manager origin. + /// Can be called by dApp staking manager origin. #[pallet::call_index(4)] #[pallet::weight(Weight::zero())] pub fn unregister( @@ -546,11 +805,7 @@ pub mod pallet { }, )?; - // TODO: might require some modification later on, like additional checks to ensure contract can be unregistered. - - // TODO2: we should remove staked amount from appropriate entries, since contract has been 'invalidated' - - // TODO3: will need to add a call similar to what we have in DSv2, for stakers to 'unstake_from_unregistered_contract' + ContractStake::::remove(&smart_contract); Self::deposit_event(Event::::DAppUnregistered { smart_contract, @@ -564,9 +819,6 @@ pub mod pallet { /// /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked. /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount. - /// - /// It is possible for call to fail due to caller account already having too many locked balance chunks in storage. To solve this, - /// caller should claim pending rewards, before retrying to lock additional funds. #[pallet::call_index(5)] #[pallet::weight(Weight::zero())] pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { @@ -624,7 +876,7 @@ pub mod pallet { .saturating_sub(amount_to_unlock); let amount_to_unlock = if remaining_amount < T::MinimumLockedAmount::get() { ensure!( - ledger.active_stake(state.period_info.number).is_zero(), + ledger.staked_amount(state.period_info.number).is_zero(), Error::::RemainingStakePreventsFullUnlock ); ledger.active_locked_amount() @@ -639,7 +891,7 @@ pub mod pallet { ledger.subtract_lock_amount(amount_to_unlock); let current_block = frame_system::Pallet::::block_number(); - let unlock_block = current_block.saturating_add(T::UnlockingPeriod::get()); + let unlock_block = current_block.saturating_add(Self::unlock_period()); ledger .add_unlocking_chunk(amount_to_unlock, unlock_block) .map_err(|_| Error::::TooManyUnlockingChunks)?; @@ -671,15 +923,23 @@ pub mod pallet { let amount = ledger.claim_unlocked(current_block); ensure!(amount > Zero::zero(), Error::::NoUnlockedChunksToClaim); + // In case it's full unlock, account is exiting dApp staking, ensure all storage is cleaned up. + // TODO: will be used after benchmarks + let _removed_entries = if ledger.is_empty() { + let _ = StakerInfo::::clear_prefix(&account, ledger.contract_stake_count, None); + ledger.contract_stake_count + } else { + 0 + }; + + // TODO: discussion point - this will "kill" users ability to withdraw past rewards. + // This can be handled by the frontend though. + Self::update_ledger(&account, ledger); CurrentEraInfo::::mutate(|era_info| { era_info.unlocking_removed(amount); }); - // TODO: We should ensure user doesn't unlock everything if they still have storage leftovers (e.g. unclaimed rewards?) - - // TODO2: to make it more bounded, we could add a limit to how much distinct stake entries a user can have - Self::deposit_event(Event::::ClaimedUnlocked { account, amount }); Ok(()) @@ -736,29 +996,32 @@ pub mod pallet { ); let protocol_state = ActiveProtocolState::::get(); - // Staker always stakes from the NEXT era - let stake_era = protocol_state.era.saturating_add(1); + let stake_era = protocol_state.era; ensure!( - !protocol_state.period_info.is_next_period(stake_era), + !protocol_state + .period_info + .is_next_period(stake_era.saturating_add(1)), Error::::PeriodEndsInNextEra ); let mut ledger = Ledger::::get(&account); + // TODO: suggestion is to change this a bit so we clean up ledger if rewards have expired // 1. // Increase stake amount for the next era & current period in staker's ledger ledger - .add_stake_amount(amount, stake_era, protocol_state.period_number()) + .add_stake_amount(amount, stake_era, protocol_state.period_info) .map_err(|err| match err { - AccountLedgerError::InvalidPeriod => { + AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { Error::::UnclaimedRewardsFromPastPeriods } AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, - AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, // Defensive check, should never happen _ => Error::::InternalStakeError, })?; + // TODO: also change this to check if rewards have expired + // 2. // Update `StakerInfo` storage with the new stake amount on the specified contract. // @@ -769,41 +1032,50 @@ pub mod pallet { // This is ok since we only use this storage entry to keep track of how much each staker // has staked on each contract in the current period. We only ever need the latest information. // This is because `AccountLedger` is the one keeping information about how much was staked when. - let new_staking_info = match StakerInfo::::get(&account, &smart_contract) { - Some(mut staking_info) - if staking_info.period_number() == protocol_state.period_number() => - { - staking_info.stake(amount, protocol_state.period_info.period_type); - staking_info - } - _ => { - ensure!( - amount >= T::MinimumStakeAmount::get(), - Error::::InsufficientStakeAmount - ); - let mut staking_info = SingularStakingInfo::new( - protocol_state.period_info.number, - protocol_state.period_info.period_type, - ); - staking_info.stake(amount, protocol_state.period_info.period_type); - staking_info - } - }; + let (mut new_staking_info, is_new_entry) = + match StakerInfo::::get(&account, &smart_contract) { + // Entry with matching period exists + Some(staking_info) + if staking_info.period_number() == protocol_state.period_number() => + { + (staking_info, false) + } + // Entry exists but period doesn't match. Either reward should be claimed or cleaned up. + Some(_) => { + return Err(Error::::UnclaimedRewardsFromPastPeriods.into()); + } + // No entry exists + None => ( + SingularStakingInfo::new( + protocol_state.period_number(), + protocol_state.subperiod(), + ), + true, + ), + }; + new_staking_info.stake(amount, protocol_state.subperiod()); + ensure!( + new_staking_info.total_staked_amount() >= T::MinimumStakeAmount::get(), + Error::::InsufficientStakeAmount + ); + + if is_new_entry { + ledger.contract_stake_count.saturating_inc(); + ensure!( + ledger.contract_stake_count <= T::MaxNumberOfStakedContracts::get(), + Error::::TooManyStakedContracts + ); + } // 3. // Update `ContractStake` storage with the new stake amount on the specified contract. let mut contract_stake_info = ContractStake::::get(&smart_contract); - ensure!( - contract_stake_info - .stake(amount, protocol_state.period_info, stake_era) - .is_ok(), - Error::::InternalStakeError - ); + contract_stake_info.stake(amount, protocol_state.period_info, stake_era); // 4. // Update total staked amount for the next era. CurrentEraInfo::::mutate(|era_info| { - era_info.add_stake_amount(amount, protocol_state.period_type()); + era_info.add_stake_amount(amount, protocol_state.subperiod()); }); // 5. @@ -870,7 +1142,7 @@ pub mod pallet { amount }; - staking_info.unstake(amount, protocol_state.period_type()); + staking_info.unstake(amount, protocol_state.subperiod()); (staking_info, amount) } None => { @@ -881,43 +1153,42 @@ pub mod pallet { // 2. // Reduce stake amount ledger - .unstake_amount(amount, unstake_era, protocol_state.period_number()) + .unstake_amount(amount, unstake_era, protocol_state.period_info) .map_err(|err| match err { - AccountLedgerError::InvalidPeriod => Error::::UnstakeFromPastPeriod, + // These are all defensive checks, which should never happen since we already checked them above. + AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { + Error::::UnclaimedRewardsFromPastPeriods + } AccountLedgerError::UnstakeAmountLargerThanStake => { Error::::UnstakeAmountTooLarge } - AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, _ => Error::::InternalUnstakeError, })?; // 3. // Update `ContractStake` storage with the reduced stake amount on the specified contract. let mut contract_stake_info = ContractStake::::get(&smart_contract); - ensure!( - contract_stake_info - .unstake(amount, protocol_state.period_info, unstake_era) - .is_ok(), - Error::::InternalUnstakeError - ); + contract_stake_info.unstake(amount, protocol_state.period_info, unstake_era); // 4. // Update total staked amount for the next era. CurrentEraInfo::::mutate(|era_info| { - era_info.unstake_amount(amount, protocol_state.period_type()); + era_info.unstake_amount(amount, protocol_state.subperiod()); }); // 5. // Update remaining storage entries - Self::update_ledger(&account, ledger); ContractStake::::insert(&smart_contract, contract_stake_info); if new_staking_info.is_empty() { + ledger.contract_stake_count.saturating_dec(); StakerInfo::::remove(&account, &smart_contract); } else { StakerInfo::::insert(&account, &smart_contract, new_staking_info); } + Self::update_ledger(&account, ledger); + Self::deposit_event(Event::::Unstake { account, smart_contract, @@ -926,6 +1197,346 @@ pub mod pallet { Ok(()) } + + /// Claims some staker rewards, if user has any. + /// In the case of a successfull call, at least one era will be claimed, with the possibility of multiple claims happening + /// if appropriate entries exist in account's ledger. + #[pallet::call_index(11)] + #[pallet::weight(Weight::zero())] + pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let mut ledger = Ledger::::get(&account); + let staked_period = ledger + .staked_period() + .ok_or(Error::::NoClaimableRewards)?; + + // Check if the rewards have expired + let protocol_state = ActiveProtocolState::::get(); + ensure!( + staked_period >= Self::oldest_claimable_period(protocol_state.period_number()), + Error::::RewardExpired + ); + + // Calculate the reward claim span + let earliest_staked_era = ledger + .earliest_staked_era() + .ok_or(Error::::InternalClaimStakerError)?; + let era_rewards = + EraRewards::::get(Self::era_reward_span_index(earliest_staked_era)) + .ok_or(Error::::NoClaimableRewards)?; + + // The last era for which we can theoretically claim rewards. + // And indicator if we know the period's ending era. + let (last_period_era, period_end) = if staked_period == protocol_state.period_number() { + (protocol_state.era.saturating_sub(1), None) + } else { + PeriodEnd::::get(&staked_period) + .map(|info| (info.final_era, Some(info.final_era))) + .ok_or(Error::::InternalClaimStakerError)? + }; + + // The last era for which we can claim rewards for this account. + let last_claim_era = era_rewards.last_era().min(last_period_era); + + // Get chunks for reward claiming + let rewards_iter = + ledger + .claim_up_to_era(last_claim_era, period_end) + .map_err(|err| match err { + AccountLedgerError::NothingToClaim => Error::::NoClaimableRewards, + _ => Error::::InternalClaimStakerError, + })?; + + // Calculate rewards + let mut rewards: Vec<_> = Vec::new(); + let mut reward_sum = Balance::zero(); + for (era, amount) in rewards_iter { + let era_reward = era_rewards + .get(era) + .ok_or(Error::::InternalClaimStakerError)?; + + // Optimization, and zero-division protection + if amount.is_zero() || era_reward.staked.is_zero() { + continue; + } + let staker_reward = Perbill::from_rational(amount, era_reward.staked) + * era_reward.staker_reward_pool; + + rewards.push((era, staker_reward)); + reward_sum.saturating_accrue(staker_reward); + } + + // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. + // Account exists since it has locked funds. + T::Currency::deposit_into_existing(&account, reward_sum) + .map_err(|_| Error::::InternalClaimStakerError)?; + + Self::update_ledger(&account, ledger); + + rewards.into_iter().for_each(|(era, reward)| { + Self::deposit_event(Event::::Reward { + account: account.clone(), + era, + amount: reward, + }); + }); + + Ok(()) + } + + /// Used to claim bonus reward for a smart contract, if eligible. + #[pallet::call_index(12)] + #[pallet::weight(Weight::zero())] + pub fn claim_bonus_reward( + origin: OriginFor, + smart_contract: T::SmartContract, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let staker_info = StakerInfo::::get(&account, &smart_contract) + .ok_or(Error::::NoClaimableRewards)?; + let protocol_state = ActiveProtocolState::::get(); + + // Ensure: + // 1. Period for which rewards are being claimed has ended. + // 2. Account has been a loyal staker. + // 3. Rewards haven't expired. + let staked_period = staker_info.period_number(); + ensure!( + staked_period < protocol_state.period_number(), + Error::::NoClaimableRewards + ); + ensure!( + staker_info.is_loyal(), + Error::::NotEligibleForBonusReward + ); + ensure!( + staker_info.period_number() + >= Self::oldest_claimable_period(protocol_state.period_number()), + Error::::RewardExpired + ); + + let period_end_info = + PeriodEnd::::get(&staked_period).ok_or(Error::::InternalClaimBonusError)?; + // Defensive check - we should never get this far in function if no voting period stake exists. + ensure!( + !period_end_info.total_vp_stake.is_zero(), + Error::::InternalClaimBonusError + ); + + let eligible_amount = staker_info.staked_amount(Subperiod::Voting); + let bonus_reward = + Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) + * period_end_info.bonus_reward_pool; + + // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. + // Account exists since it has locked funds. + T::Currency::deposit_into_existing(&account, bonus_reward) + .map_err(|_| Error::::InternalClaimStakerError)?; + + // Cleanup entry since the reward has been claimed + StakerInfo::::remove(&account, &smart_contract); + + Self::deposit_event(Event::::BonusReward { + account: account.clone(), + smart_contract, + period: staked_period, + amount: bonus_reward, + }); + + Ok(()) + } + + /// Used to claim dApp reward for the specified era. + #[pallet::call_index(13)] + #[pallet::weight(Weight::zero())] + pub fn claim_dapp_reward( + origin: OriginFor, + smart_contract: T::SmartContract, + #[pallet::compact] era: EraNumber, + ) -> DispatchResult { + Self::ensure_pallet_enabled()?; + + // TODO: Shall we make sure only dApp owner or beneficiary can trigger the claim? + let _ = ensure_signed(origin)?; + + let dapp_info = + IntegratedDApps::::get(&smart_contract).ok_or(Error::::ContractNotFound)?; + + // Make sure provided era has ended + let protocol_state = ActiveProtocolState::::get(); + ensure!(era < protocol_state.era, Error::::InvalidClaimEra); + + // 'Consume' dApp reward for the specified era, if possible. + let mut dapp_tiers = DAppTiers::::get(&era).ok_or(Error::::NoDAppTierInfo)?; + ensure!( + Self::oldest_claimable_period(dapp_tiers.period) <= protocol_state.period_number(), + Error::::RewardExpired + ); + + let (amount, tier_id) = + dapp_tiers + .try_consume(dapp_info.id) + .map_err(|error| match error { + DAppTierError::NoDAppInTiers => Error::::NoClaimableRewards, + DAppTierError::RewardAlreadyClaimed => Error::::DAppRewardAlreadyClaimed, + _ => Error::::InternalClaimDAppError, + })?; + + // Get reward destination, and deposit the reward. + let beneficiary = dapp_info.reward_beneficiary(); + // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. + T::Currency::deposit_creating(beneficiary, amount); + + // Write back updated struct to prevent double reward claims + DAppTiers::::insert(&era, dapp_tiers); + + Self::deposit_event(Event::::DAppReward { + beneficiary: beneficiary.clone(), + smart_contract, + tier_id, + era, + amount, + }); + + Ok(()) + } + + /// Used to unstake funds from a contract that was unregistered after an account staked on it. + #[pallet::call_index(14)] + #[pallet::weight(Weight::zero())] + pub fn unstake_from_unregistered( + origin: OriginFor, + smart_contract: T::SmartContract, + ) -> DispatchResult { + // TODO: tests are missing but will be added later. + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + ensure!( + !Self::is_active(&smart_contract), + Error::::ContractStillActive + ); + + let protocol_state = ActiveProtocolState::::get(); + let unstake_era = protocol_state.era; + + // Extract total staked amount on the specified unregistered contract + let amount = match StakerInfo::::get(&account, &smart_contract) { + Some(staking_info) => { + ensure!( + staking_info.period_number() == protocol_state.period_number(), + Error::::UnstakeFromPastPeriod + ); + + staking_info.total_staked_amount() + } + None => { + return Err(Error::::NoStakingInfo.into()); + } + }; + + // Reduce stake amount in ledger + let mut ledger = Ledger::::get(&account); + ledger + .unstake_amount(amount, unstake_era, protocol_state.period_info) + .map_err(|err| match err { + // These are all defensive checks, which should never happen since we already checked them above. + AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { + Error::::UnclaimedRewardsFromPastPeriods + } + _ => Error::::InternalUnstakeError, + })?; + + // Update total staked amount for the next era. + // This means 'fake' stake total amount has been kept until now, even though contract was unregistered. + CurrentEraInfo::::mutate(|era_info| { + era_info.unstake_amount(amount, protocol_state.subperiod()); + }); + + // Update remaining storage entries + Self::update_ledger(&account, ledger); + StakerInfo::::remove(&account, &smart_contract); + + Self::deposit_event(Event::::UnstakeFromUnregistered { + account, + smart_contract, + amount, + }); + + Ok(()) + } + + // TODO: an alternative to this could would be to allow `unstake` call to cleanup old entries, however that means more complexity in that call + /// Used to unstake funds from a contract that was unregistered after an account staked on it. + #[pallet::call_index(15)] + #[pallet::weight(Weight::zero())] + pub fn cleanup_expired_entries(origin: OriginFor) -> DispatchResult { + // TODO: tests are missing but will be added later. + Self::ensure_pallet_enabled()?; + let account = ensure_signed(origin)?; + + let protocol_state = ActiveProtocolState::::get(); + let current_period = protocol_state.period_number(); + + // Find all entries which have expired. This is bounded by max allowed number of entries. + let to_be_deleted: Vec = StakerInfo::::iter_prefix(&account) + .filter_map(|(smart_contract, stake_info)| { + if stake_info.period_number() < current_period { + Some(smart_contract) + } else { + None + } + }) + .collect(); + + // Remove all expired entries. + for smart_contract in to_be_deleted { + StakerInfo::::remove(&account, &smart_contract); + } + + // Remove expired ledger stake entries, if needed. + let threshold_period = Self::oldest_claimable_period(current_period); + let mut ledger = Ledger::::get(&account); + if ledger.maybe_cleanup_expired(threshold_period) { + Self::update_ledger(&account, ledger); + } + + Ok(()) + } + + /// Used to force a change of era or subperiod. + /// The effect isn't immediate but will happen on the next block. + /// + /// Used for testing purposes, when we want to force an era change, or a subperiod change. + /// Not intended to be used in production, except in case of unforseen circumstances. + /// + /// Can only be called by manager origin. + #[pallet::call_index(16)] + #[pallet::weight(Weight::zero())] + pub fn force(origin: OriginFor, force_type: ForcingType) -> DispatchResult { + // TODO: tests are missing but will be added later. + Self::ensure_pallet_enabled()?; + T::ManagerOrigin::ensure_origin(origin)?; + + // Ensure a 'change' happens on the next block + ActiveProtocolState::::mutate(|state| { + let current_block = frame_system::Pallet::::block_number(); + state.next_era_start = current_block.saturating_add(One::one()); + + match force_type { + ForcingType::Era => (), + ForcingType::Subperiod => { + state.period_info.subperiod_end_era = state.era.saturating_add(1); + } + } + }); + + Ok(()) + } } impl Pallet { @@ -975,9 +1586,136 @@ pub mod pallet { } /// `true` if smart contract is active, `false` if it has been unregistered. - fn is_active(smart_contract: &T::SmartContract) -> bool { + pub(crate) fn is_active(smart_contract: &T::SmartContract) -> bool { IntegratedDApps::::get(smart_contract) .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) } + + /// Calculates the `EraRewardSpan` index for the specified era. + pub(crate) fn era_reward_span_index(era: EraNumber) -> EraNumber { + era.saturating_sub(era % T::EraRewardSpanLength::get()) + } + + /// Return the oldest period for which rewards can be claimed. + /// All rewards before that period are considered to be expired. + pub(crate) fn oldest_claimable_period(current_period: PeriodNumber) -> PeriodNumber { + current_period.saturating_sub(T::RewardRetentionInPeriods::get()) + } + + /// Unlocking period expressed in the number of blocks. + pub(crate) fn unlock_period() -> BlockNumberFor { + T::StandardEraLength::get().saturating_mul(T::UnlockingPeriod::get().into()) + } + + /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier. + /// + /// The returned object contains information about each dApp that made it into a tier. + pub(crate) fn get_dapp_tier_assignment( + era: EraNumber, + period: PeriodNumber, + dapp_reward_pool: Balance, + ) -> DAppTierRewardsFor { + // TODO - by breaking this into multiple steps, if they are too heavy for a single block, we can distribute them between multiple blocks. + // Benchmarks will show this, but I don't believe it will be needed, especially with increased block capacity we'll get with async backing. + // Even without async backing though, we should have enough capacity to handle this. + // UPDATE: might work with async backing, but right now we could handle up to 150 dApps before exceeding the PoV size. + + // UPDATE2: instead of taking the approach of reading an ever increasing amount of entries from storage, we can instead adopt an approach + // of eficiently storing composite information into `BTreeMap`. The approach is essentially the same as the one used below to store rewards. + // Each time `stake` or `unstake` are called, corresponding entries are updated. This way we can keep track of all the contract stake in a single DB entry. + // To make the solution more scalable, we could 'split' stake entries into spans, similar as rewards are handled now. + // + // Experiment with an 'experimental' entry shows PoV size of ~7kB induced for entry that can hold up to 100 entries. + + let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::::count() as usize); + + // 1. + // Iterate over all registered dApps, and collect their stake amount. + // This is bounded by max amount of dApps we allow to be registered. + for (smart_contract, dapp_info) in IntegratedDApps::::iter() { + // Skip unregistered dApps + if dapp_info.state != DAppState::Registered { + continue; + } + + // Skip dApps which don't have ANY amount staked (TODO: potential improvement is to prune all dApps below minimum threshold) + let stake_amount = match ContractStake::::get(&smart_contract).get(era, period) { + Some(stake_amount) if !stake_amount.total().is_zero() => stake_amount, + _ => continue, + }; + + // TODO: Need to handle labels! + // Proposition for label handling: + // Split them into 'musts' and 'good-to-have' + // In case of 'must', reduce appropriate tier size, and insert them at the end + // For good to have, we can insert them immediately, and then see if we need to adjust them later. + // Anyhow, labels bring complexity. For starters, we should only deliver the one for 'bootstraping' purposes. + dapp_stakes.push((dapp_info.id, stake_amount.total())); + } + + // 2. + // Sort by amount staked, in reverse - top dApp will end in the first place, 0th index. + dapp_stakes.sort_unstable_by(|(_, amount_1), (_, amount_2)| amount_2.cmp(amount_1)); + + // 3. + // Iterate over configured tier and potential dApps. + // Each dApp will be assigned to the best possible tier if it satisfies the required condition, + // and tier capacity hasn't been filled yet. + let mut dapp_tiers = Vec::with_capacity(dapp_stakes.len()); + let tier_config = TierConfig::::get(); + + let mut global_idx = 0; + let mut tier_id = 0; + for (tier_capacity, tier_threshold) in tier_config + .slots_per_tier + .iter() + .zip(tier_config.tier_thresholds.iter()) + { + let max_idx = global_idx + .saturating_add(*tier_capacity as usize) + .min(dapp_stakes.len()); + + // Iterate over dApps until one of two conditions has been met: + // 1. Tier has no more capacity + // 2. dApp doesn't satisfy the tier threshold (since they're sorted, none of the following dApps will satisfy the condition either) + for (dapp_id, stake_amount) in dapp_stakes[global_idx..max_idx].iter() { + if tier_threshold.is_satisfied(*stake_amount) { + global_idx.saturating_inc(); + dapp_tiers.push(DAppTier { + dapp_id: *dapp_id, + tier_id: Some(tier_id), + }); + } else { + break; + } + } + + tier_id.saturating_inc(); + } + + // 4. + // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is guaranteed due to lack of duplicated Ids). + // TODO & Idea: perhaps use BTreeMap instead? It will "sort" automatically based on dApp Id, and we can efficiently remove entries, + // reducing PoV size step by step. + // It's a trade-off between speed and PoV size. Although both are quite minor, so maybe it doesn't matter that much. + dapp_tiers.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); + + // 5. Calculate rewards. + let tier_rewards = tier_config + .reward_portion + .iter() + .map(|percent| *percent * dapp_reward_pool) + .collect::>(); + + // 6. + // Prepare and return tier & rewards info. + // In case rewards creation fails, we just write the default value. This should never happen though. + DAppTierRewards::, T::NumberOfTiers>::new( + dapp_tiers, + tier_rewards, + period, + ) + .unwrap_or_default() + } } } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index fb0cc65c27..6b1a16287f 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -24,10 +24,12 @@ use frame_support::{ weights::Weight, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use sp_arithmetic::fixed_point::FixedU64; use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, + Permill, }; use sp_io::TestExternalities; @@ -104,23 +106,26 @@ impl pallet_balances::Config for Test { type WeightInfo = (); } -impl pallet_dapp_staking::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type SmartContract = MockSmartContract; - type ManagerOrigin = frame_system::EnsureRoot; - type StandardEraLength = ConstU64<10>; - type StandardErasPerVotingPeriod = ConstU32<8>; - type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; - type MaxNumberOfContracts = ConstU16<10>; - type MaxUnlockingChunks = ConstU32<5>; - type MaxStakingChunks = ConstU32<8>; - type MinimumLockedAmount = ConstU128; - type UnlockingPeriod = ConstU64<20>; - type MinimumStakeAmount = ConstU128<3>; +pub struct DummyPriceProvider; +impl PriceProvider for DummyPriceProvider { + fn average_price() -> FixedU64 { + FixedU64::from_rational(1, 10) + } +} + +pub struct DummyRewardPoolProvider; +impl RewardPoolProvider for DummyRewardPoolProvider { + fn normal_reward_pools() -> (Balance, Balance) { + ( + Balance::from(1_000_000_000_000_u128), + Balance::from(1_000_000_000_u128), + ) + } + fn bonus_reward_pool() -> Balance { + Balance::from(3_000_000_u128) + } } -// TODO: why not just change this to e.g. u32 for test? #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] pub enum MockSmartContract { Wasm(AccountId), @@ -133,6 +138,38 @@ impl Default for MockSmartContract { } } +#[cfg(feature = "runtime-benchmarks")] +pub struct BenchmarkHelper(sp_std::marker::PhantomData); +#[cfg(feature = "runtime-benchmarks")] +impl crate::BenchmarkHelper for BenchmarkHelper { + fn get_smart_contract(id: u32) -> MockSmartContract { + MockSmartContract::Wasm(id as AccountId) + } +} + +impl pallet_dapp_staking::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type SmartContract = MockSmartContract; + type ManagerOrigin = frame_system::EnsureRoot; + type NativePriceProvider = DummyPriceProvider; + type RewardPoolProvider = DummyRewardPoolProvider; + type StandardEraLength = ConstU64<10>; + type StandardErasPerVotingPeriod = ConstU32<8>; + type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; + type EraRewardSpanLength = ConstU32<8>; + type RewardRetentionInPeriods = ConstU32<2>; + type MaxNumberOfContracts = ConstU16<10>; + type MaxUnlockingChunks = ConstU32<5>; + type MinimumLockedAmount = ConstU128; + type UnlockingPeriod = ConstU32<2>; + type MaxNumberOfStakedContracts = ConstU32<3>; + type MinimumStakeAmount = ConstU128<3>; + type NumberOfTiers = ConstU32<4>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = BenchmarkHelper; +} + pub struct ExtBuilder; impl ExtBuilder { pub fn build() -> TestExternalities { @@ -153,27 +190,67 @@ impl ExtBuilder { let mut ext = TestExternalities::from(storage); ext.execute_with(|| { System::set_block_number(1); - DappStaking::on_initialize(System::block_number()); - // TODO: not sure why the mess with type happens here, I can check it later + // Not sure why the mess with type happens here, but trait specification is needed to compile let era_length: BlockNumber = <::StandardEraLength as sp_core::Get<_>>::get(); let voting_period_length_in_eras: EraNumber = <::StandardErasPerVotingPeriod as sp_core::Get<_>>::get( ); - // TODO: handle this via GenesisConfig, and some helper functions to set the state + // Init protocol state pallet_dapp_staking::ActiveProtocolState::::put(ProtocolState { era: 1, next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + 1, period_info: PeriodInfo { number: 1, - period_type: PeriodType::Voting, - ending_era: 2, + subperiod: Subperiod::Voting, + subperiod_end_era: 2, }, maintenance: false, }); - }); + + // Init tier params + let tier_params = TierParameters::<::NumberOfTiers> { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { amount: 100, minimum_amount: 80 }, + TierThreshold::DynamicTvlAmount { amount: 50, minimum_amount: 40 }, + TierThreshold::DynamicTvlAmount { amount: 20, minimum_amount: 20 }, + TierThreshold::FixedTvlAmount { amount: 10 }, + ]) + .unwrap(), + }; + + // Init tier config, based on the initial params + let init_tier_config = TiersConfiguration::<::NumberOfTiers> { + number_of_slots: 100, + slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + reward_portion: tier_params.reward_portion.clone(), + tier_thresholds: tier_params.tier_thresholds.clone(), + }; + + pallet_dapp_staking::StaticTierParams::::put(tier_params); + pallet_dapp_staking::TierConfig::::put(init_tier_config.clone()); + pallet_dapp_staking::NextTierConfig::::put(init_tier_config); + + // TODO: include this into every test unless it explicitly doesn't need it. + // DappStaking::on_initialize(System::block_number()); + } + ); ext } @@ -227,9 +304,24 @@ pub(crate) fn advance_to_next_period() { } /// Advance blocks until next period type has been reached. -pub(crate) fn _advance_to_next_period_type() { - let period_type = ActiveProtocolState::::get().period_type(); - while ActiveProtocolState::::get().period_type() == period_type { +pub(crate) fn advance_to_advance_to_next_subperiod() { + let subperiod = ActiveProtocolState::::get().subperiod(); + while ActiveProtocolState::::get().subperiod() == subperiod { run_for_blocks(1); } } + +// Return all dApp staking events from the event buffer. +pub fn dapp_staking_events() -> Vec> { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let RuntimeEvent::DappStaking(inner) = e { + Some(inner) + } else { + None + } + }) + .collect() +} diff --git a/pallets/dapp-staking-v3/src/test/mod.rs b/pallets/dapp-staking-v3/src/test/mod.rs index 94a090243c..0774935213 100644 --- a/pallets/dapp-staking-v3/src/test/mod.rs +++ b/pallets/dapp-staking-v3/src/test/mod.rs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -mod mock; +pub(crate) mod mock; mod testing_utils; mod tests; mod tests_types; diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 532d856f05..3ac63253ab 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -19,12 +19,13 @@ use crate::test::mock::*; use crate::types::*; use crate::{ - pallet as pallet_dapp_staking, ActiveProtocolState, BlockNumberFor, ContractStake, - CurrentEraInfo, DAppId, Event, IntegratedDApps, Ledger, NextDAppId, StakerInfo, + pallet::Config, ActiveProtocolState, BlockNumberFor, ContractStake, CurrentEraInfo, DAppId, + DAppTiers, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, PeriodEnd, PeriodEndInfo, + StakerInfo, }; use frame_support::{assert_ok, traits::Get}; -use sp_runtime::traits::Zero; +use sp_runtime::{traits::Zero, Perbill}; use std::collections::HashMap; /// Helper struct used to store the entire pallet state snapshot. @@ -35,19 +36,21 @@ pub(crate) struct MemorySnapshot { next_dapp_id: DAppId, current_era_info: EraInfo, integrated_dapps: HashMap< - ::SmartContract, + ::SmartContract, DAppInfo<::AccountId>, >, ledger: HashMap<::AccountId, AccountLedgerFor>, staker_info: HashMap< ( ::AccountId, - ::SmartContract, + ::SmartContract, ), SingularStakingInfo, >, - contract_stake: - HashMap<::SmartContract, ContractStakingInfoSeries>, + contract_stake: HashMap<::SmartContract, ContractStakeAmount>, + era_rewards: HashMap::EraRewardSpanLength>>, + period_end: HashMap, + dapp_tiers: HashMap>, } impl MemorySnapshot { @@ -63,6 +66,9 @@ impl MemorySnapshot { .map(|(k1, k2, v)| ((k1, k2), v)) .collect(), contract_stake: ContractStake::::iter().collect(), + era_rewards: EraRewards::::iter().collect(), + period_end: PeriodEnd::::iter().collect(), + dapp_tiers: DAppTiers::::iter().collect(), } } @@ -108,13 +114,13 @@ pub(crate) fn assert_register(owner: AccountId, smart_contract: &MockSmartContra } /// Update dApp reward destination and assert success -pub(crate) fn assert_set_dapp_reward_destination( +pub(crate) fn assert_set_dapp_reward_beneficiary( owner: AccountId, smart_contract: &MockSmartContract, beneficiary: Option, ) { // Change reward destination - assert_ok!(DappStaking::set_dapp_reward_destination( + assert_ok!(DappStaking::set_dapp_reward_beneficiary( RuntimeOrigin::signed(owner), smart_contract.clone(), beneficiary, @@ -181,6 +187,7 @@ pub(crate) fn assert_unregister(smart_contract: &MockSmartContract) { IntegratedDApps::::get(&smart_contract).unwrap().state, DAppState::Unregistered(pre_snapshot.active_protocol_state.era), ); + assert!(!ContractStake::::contains_key(&smart_contract)); } /// Lock funds into dApp staking and assert success. @@ -242,7 +249,7 @@ pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { // When unlocking would take account below the minimum lock threshold, unlock everything let locked_amount = pre_ledger.active_locked_amount(); - let min_locked_amount = ::MinimumLockedAmount::get(); + let min_locked_amount = ::MinimumLockedAmount::get(); if locked_amount.saturating_sub(possible_unlock_amount) < min_locked_amount { locked_amount } else { @@ -357,6 +364,17 @@ pub(crate) fn assert_claim_unlocked(account: AccountId) { post_snapshot.current_era_info.unlocking, pre_snapshot.current_era_info.unlocking - amount ); + + // In case of full withdrawal from the protocol + if post_ledger.is_empty() { + assert!(!Ledger::::contains_key(&account)); + assert!( + StakerInfo::::iter_prefix_values(&account) + .count() + .is_zero(), + "All stake entries need to be cleaned up." + ); + } } /// Claims the unlocked funds back into free balance of the user and assert success. @@ -414,14 +432,12 @@ pub(crate) fn assert_stake( let pre_contract_stake = pre_snapshot .contract_stake .get(&smart_contract) - .map_or(ContractStakingInfoSeries::default(), |series| { - series.clone() - }); + .map_or(ContractStakeAmount::default(), |series| series.clone()); let pre_era_info = pre_snapshot.current_era_info; let stake_era = pre_snapshot.active_protocol_state.era + 1; let stake_period = pre_snapshot.active_protocol_state.period_number(); - let stake_period_type = pre_snapshot.active_protocol_state.period_type(); + let stake_subperiod = pre_snapshot.active_protocol_state.subperiod(); // Stake on smart contract & verify event assert_ok!(DappStaking::stake( @@ -451,7 +467,11 @@ pub(crate) fn assert_stake( // 1. verify ledger // ===================== // ===================== - assert_eq!(post_ledger.staked_period, Some(stake_period)); + assert_eq!( + post_ledger.staked, pre_ledger.staked, + "Must remain exactly the same." + ); + assert_eq!(post_ledger.staked_future.unwrap().period, stake_period); assert_eq!( post_ledger.staked_amount(stake_period), pre_ledger.staked_amount(stake_period) + amount, @@ -462,22 +482,7 @@ pub(crate) fn assert_stake( pre_ledger.stakeable_amount(stake_period) - amount, "Stakeable amount must decrease by the 'amount'" ); - match pre_ledger.last_stake_era() { - Some(last_stake_era) if last_stake_era == stake_era => { - assert_eq!( - post_ledger.staked.0.len(), - pre_ledger.staked.0.len(), - "Existing entry must be modified." - ); - } - _ => { - assert_eq!( - post_ledger.staked.0.len(), - pre_ledger.staked.0.len() + 1, - "Additional entry must be added." - ); - } - } + // TODO: expand with more detailed checks of staked and staked_future // 2. verify staker info // ===================== @@ -491,8 +496,8 @@ pub(crate) fn assert_stake( "Total staked amount must increase by the 'amount'" ); assert_eq!( - post_staker_info.staked_amount(stake_period_type), - pre_staker_info.staked_amount(stake_period_type) + amount, + post_staker_info.staked_amount(stake_subperiod), + pre_staker_info.staked_amount(stake_subperiod) + amount, "Staked amount must increase by the 'amount'" ); assert_eq!(post_staker_info.period_number(), stake_period); @@ -509,16 +514,16 @@ pub(crate) fn assert_stake( amount, "Total staked amount must be equal to exactly the 'amount'" ); - assert!(amount >= ::MinimumStakeAmount::get()); + assert!(amount >= ::MinimumStakeAmount::get()); assert_eq!( - post_staker_info.staked_amount(stake_period_type), + post_staker_info.staked_amount(stake_subperiod), amount, "Staked amount must be equal to exactly the 'amount'" ); assert_eq!(post_staker_info.period_number(), stake_period); assert_eq!( post_staker_info.is_loyal(), - stake_period_type == PeriodType::Voting + stake_subperiod == Subperiod::Voting ); } } @@ -526,38 +531,22 @@ pub(crate) fn assert_stake( // 3. verify contract stake // ========================= // ========================= - // TODO: since default value is all zeros, maybe we can just skip the branching code and do it once? - match pre_contract_stake.last_stake_period() { - Some(last_stake_period) if last_stake_period == stake_period => { - assert_eq!( - post_contract_stake.total_staked_amount(stake_period), - pre_contract_stake.total_staked_amount(stake_period) + amount, - "Staked amount must increase by the 'amount'" - ); - assert_eq!( - post_contract_stake.staked_amount(stake_period, stake_period_type), - pre_contract_stake.staked_amount(stake_period, stake_period_type) + amount, - "Staked amount must increase by the 'amount'" - ); - } - _ => { - assert_eq!(post_contract_stake.len(), 1); - assert_eq!( - post_contract_stake.total_staked_amount(stake_period), - amount, - "Total staked amount must be equal to exactly the 'amount'" - ); - assert_eq!( - post_contract_stake.staked_amount(stake_period, stake_period_type), - amount, - "Staked amount must be equal to exactly the 'amount'" - ); - } - } - assert_eq!(post_contract_stake.last_stake_period(), Some(stake_period)); - assert_eq!(post_contract_stake.last_stake_era(), Some(stake_era)); + assert_eq!( + post_contract_stake.total_staked_amount(stake_period), + pre_contract_stake.total_staked_amount(stake_period) + amount, + "Staked amount must increase by the 'amount'" + ); + assert_eq!( + post_contract_stake.staked_amount(stake_period, stake_subperiod), + pre_contract_stake.staked_amount(stake_period, stake_subperiod) + amount, + "Staked amount must increase by the 'amount'" + ); - // TODO: expand this check to compare inner slices as well! + assert_eq!( + post_contract_stake.latest_stake_period(), + Some(stake_period) + ); + assert_eq!(post_contract_stake.latest_stake_era(), Some(stake_era)); // 4. verify era info // ========================= @@ -572,8 +561,8 @@ pub(crate) fn assert_stake( pre_era_info.total_staked_amount_next_era() + amount ); assert_eq!( - post_era_info.staked_amount_next_era(stake_period_type), - pre_era_info.staked_amount_next_era(stake_period_type) + amount + post_era_info.staked_amount_next_era(stake_subperiod), + pre_era_info.staked_amount_next_era(stake_subperiod) + amount ); } @@ -597,10 +586,9 @@ pub(crate) fn assert_unstake( let _unstake_era = pre_snapshot.active_protocol_state.era; let unstake_period = pre_snapshot.active_protocol_state.period_number(); - let unstake_period_type = pre_snapshot.active_protocol_state.period_type(); + let unstake_subperiod = pre_snapshot.active_protocol_state.subperiod(); - let minimum_stake_amount: Balance = - ::MinimumStakeAmount::get(); + let minimum_stake_amount: Balance = ::MinimumStakeAmount::get(); let is_full_unstake = pre_staker_info.total_staked_amount().saturating_sub(amount) < minimum_stake_amount; @@ -635,7 +623,6 @@ pub(crate) fn assert_unstake( // 1. verify ledger // ===================== // ===================== - assert_eq!(post_ledger.staked_period, Some(unstake_period)); assert_eq!( post_ledger.staked_amount(unstake_period), pre_ledger.staked_amount(unstake_period) - amount, @@ -646,7 +633,7 @@ pub(crate) fn assert_unstake( pre_ledger.stakeable_amount(unstake_period) + amount, "Stakeable amount must increase by the 'amount'" ); - // TODO: maybe extend check with concrete value checks? E.g. if we modify past entry, we should check past & current entries are properly adjusted. + // TODO: expand with more detailed checks of staked and staked_future // 2. verify staker info // ===================== @@ -668,17 +655,17 @@ pub(crate) fn assert_unstake( "Total staked amount must decrease by the 'amount'" ); assert_eq!( - post_staker_info.staked_amount(unstake_period_type), + post_staker_info.staked_amount(unstake_subperiod), pre_staker_info - .staked_amount(unstake_period_type) + .staked_amount(unstake_subperiod) .saturating_sub(amount), "Staked amount must decrease by the 'amount'" ); let is_loyal = pre_staker_info.is_loyal() - && !(unstake_period_type == PeriodType::BuildAndEarn - && post_staker_info.staked_amount(PeriodType::Voting) - < pre_staker_info.staked_amount(PeriodType::Voting)); + && !(unstake_subperiod == Subperiod::BuildAndEarn + && post_staker_info.staked_amount(Subperiod::Voting) + < pre_staker_info.staked_amount(Subperiod::Voting)); assert_eq!( post_staker_info.is_loyal(), is_loyal, @@ -695,13 +682,12 @@ pub(crate) fn assert_unstake( "Staked amount must decreased by the 'amount'" ); assert_eq!( - post_contract_stake.staked_amount(unstake_period, unstake_period_type), + post_contract_stake.staked_amount(unstake_period, unstake_subperiod), pre_contract_stake - .staked_amount(unstake_period, unstake_period_type) + .staked_amount(unstake_period, unstake_subperiod) .saturating_sub(amount), "Staked amount must decreased by the 'amount'" ); - // TODO: extend with concrete value checks later // 4. verify era info // ========================= @@ -723,22 +709,315 @@ pub(crate) fn assert_unstake( "Total staked amount for the next era must decrease by 'amount'. No overflow is allowed." ); - if unstake_period_type == PeriodType::BuildAndEarn - && pre_era_info.staked_amount_next_era(PeriodType::BuildAndEarn) < amount + if unstake_subperiod == Subperiod::BuildAndEarn + && pre_era_info.staked_amount_next_era(Subperiod::BuildAndEarn) < amount { - let overflow = amount - pre_era_info.staked_amount_next_era(PeriodType::BuildAndEarn); + let overflow = amount - pre_era_info.staked_amount_next_era(Subperiod::BuildAndEarn); assert!(post_era_info - .staked_amount_next_era(PeriodType::BuildAndEarn) + .staked_amount_next_era(Subperiod::BuildAndEarn) .is_zero()); assert_eq!( - post_era_info.staked_amount_next_era(PeriodType::Voting), - pre_era_info.staked_amount_next_era(PeriodType::Voting) - overflow + post_era_info.staked_amount_next_era(Subperiod::Voting), + pre_era_info.staked_amount_next_era(Subperiod::Voting) - overflow ); } else { assert_eq!( - post_era_info.staked_amount_next_era(unstake_period_type), - pre_era_info.staked_amount_next_era(unstake_period_type) - amount + post_era_info.staked_amount_next_era(unstake_subperiod), + pre_era_info.staked_amount_next_era(unstake_subperiod) - amount ); } } + +/// Claim staker rewards. +pub(crate) fn assert_claim_staker_rewards(account: AccountId) { + let pre_snapshot = MemorySnapshot::new(); + let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let pre_total_issuance = ::Currency::total_issuance(); + let pre_free_balance = ::Currency::free_balance(&account); + + // Get the first eligible era for claiming rewards + let first_claim_era = pre_ledger + .earliest_staked_era() + .expect("Entry must exist, otherwise 'claim' is invalid."); + + // Get the apprropriate era rewards span for the 'first era' + let era_span_length: EraNumber = ::EraRewardSpanLength::get(); + let era_span_index = first_claim_era - (first_claim_era % era_span_length); + let era_rewards_span = pre_snapshot + .era_rewards + .get(&era_span_index) + .expect("Entry must exist, otherwise 'claim' is invalid."); + + // Calculate the final era for claiming rewards. Also determine if this will fully claim all staked period rewards. + let claim_period_end = if pre_ledger.staked_period().unwrap() + == pre_snapshot.active_protocol_state.period_number() + { + None + } else { + Some( + pre_snapshot + .period_end + .get(&pre_ledger.staked_period().unwrap()) + .expect("Entry must exist, since it's the current period.") + .final_era, + ) + }; + + let (last_claim_era, is_full_claim) = if claim_period_end.is_none() { + (pre_snapshot.active_protocol_state.era - 1, false) + } else { + let claim_period = pre_ledger.staked_period().unwrap(); + let period_end = pre_snapshot + .period_end + .get(&claim_period) + .expect("Entry must exist, since it's a past period."); + + let last_claim_era = era_rewards_span.last_era().min(period_end.final_era); + let is_full_claim = last_claim_era == period_end.final_era; + (last_claim_era, is_full_claim) + }; + + assert!( + last_claim_era < pre_snapshot.active_protocol_state.era, + "Sanity check." + ); + + // Calculate the expected rewards + let mut rewards = Vec::new(); + for (era, amount) in pre_ledger + .clone() + .claim_up_to_era(last_claim_era, claim_period_end) + .unwrap() + { + let era_reward_info = era_rewards_span + .get(era) + .expect("Entry must exist, otherwise 'claim' is invalid."); + + let reward = Perbill::from_rational(amount, era_reward_info.staked) + * era_reward_info.staker_reward_pool; + if reward.is_zero() { + continue; + } + + rewards.push((era, reward)); + } + let total_reward = rewards + .iter() + .fold(Balance::zero(), |acc, (_, reward)| acc + reward); + + //clean up possible leftover events + System::reset_events(); + + // Claim staker rewards & verify all events + assert_ok!(DappStaking::claim_staker_rewards(RuntimeOrigin::signed( + account + ),)); + + let events = dapp_staking_events(); + assert_eq!(events.len(), rewards.len()); + for (event, (era, reward)) in events.iter().zip(rewards.iter()) { + assert_eq!( + event, + &Event::::Reward { + account, + era: *era, + amount: *reward, + } + ); + } + + // Verify post state + + let post_total_issuance = ::Currency::total_issuance(); + assert_eq!( + post_total_issuance, + pre_total_issuance + total_reward, + "Total issuance must increase by the total reward amount." + ); + + let post_free_balance = ::Currency::free_balance(&account); + assert_eq!( + post_free_balance, + pre_free_balance + total_reward, + "Free balance must increase by the total reward amount." + ); + + let post_snapshot = MemorySnapshot::new(); + let post_ledger = post_snapshot.ledger.get(&account).unwrap(); + + if is_full_claim { + assert_eq!(post_ledger.staked, StakeAmount::default()); + assert!(post_ledger.staked_future.is_none()); + } else { + assert_eq!(post_ledger.staked.era, last_claim_era + 1); + assert!(post_ledger.staked_future.is_none()); + } +} + +/// Claim staker rewards. +pub(crate) fn assert_claim_bonus_reward(account: AccountId, smart_contract: &MockSmartContract) { + let pre_snapshot = MemorySnapshot::new(); + let pre_staker_info = pre_snapshot + .staker_info + .get(&(account, *smart_contract)) + .unwrap(); + let pre_total_issuance = ::Currency::total_issuance(); + let pre_free_balance = ::Currency::free_balance(&account); + + let staked_period = pre_staker_info.period_number(); + let stake_amount = pre_staker_info.staked_amount(Subperiod::Voting); + + let period_end_info = pre_snapshot + .period_end + .get(&staked_period) + .expect("Entry must exist, since it's a past period."); + + let reward = Perbill::from_rational(stake_amount, period_end_info.total_vp_stake) + * period_end_info.bonus_reward_pool; + + // Claim bonus reward & verify event + assert_ok!(DappStaking::claim_bonus_reward( + RuntimeOrigin::signed(account), + smart_contract.clone(), + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::BonusReward { + account, + smart_contract: *smart_contract, + period: staked_period, + amount: reward, + })); + + // Verify post state + + let post_total_issuance = ::Currency::total_issuance(); + assert_eq!( + post_total_issuance, + pre_total_issuance + reward, + "Total issuance must increase by the reward amount." + ); + + let post_free_balance = ::Currency::free_balance(&account); + assert_eq!( + post_free_balance, + pre_free_balance + reward, + "Free balance must increase by the reward amount." + ); + + assert!( + !StakerInfo::::contains_key(&account, smart_contract), + "Entry must be removed after successful reward claim." + ); +} + +/// Claim dapp reward for a particular era. +pub(crate) fn assert_claim_dapp_reward( + account: AccountId, + smart_contract: &MockSmartContract, + era: EraNumber, +) { + let pre_snapshot = MemorySnapshot::new(); + let dapp_info = pre_snapshot.integrated_dapps.get(smart_contract).unwrap(); + let beneficiary = dapp_info.reward_beneficiary(); + let pre_total_issuance = ::Currency::total_issuance(); + let pre_free_balance = ::Currency::free_balance(beneficiary); + + let (expected_reward, expected_tier_id) = { + let mut info = pre_snapshot + .dapp_tiers + .get(&era) + .expect("Entry must exist.") + .clone(); + + info.try_consume(dapp_info.id).unwrap() + }; + + // Claim dApp reward & verify event + assert_ok!(DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(account), + smart_contract.clone(), + era, + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::DAppReward { + beneficiary: beneficiary.clone(), + smart_contract: smart_contract.clone(), + tier_id: expected_tier_id, + era, + amount: expected_reward, + })); + + // Verify post-state + + let post_total_issuance = ::Currency::total_issuance(); + assert_eq!( + post_total_issuance, + pre_total_issuance + expected_reward, + "Total issuance must increase by the reward amount." + ); + + let post_free_balance = ::Currency::free_balance(beneficiary); + assert_eq!( + post_free_balance, + pre_free_balance + expected_reward, + "Free balance must increase by the reward amount." + ); + + let post_snapshot = MemorySnapshot::new(); + let mut info = post_snapshot + .dapp_tiers + .get(&era) + .expect("Entry must exist.") + .clone(); + assert_eq!( + info.try_consume(dapp_info.id), + Err(DAppTierError::RewardAlreadyClaimed), + "It must not be possible to claim the same reward twice!.", + ); +} + +/// Returns from which starting era to which ending era can rewards be claimed for the specified account. +/// +/// If `None` is returned, there is nothing to claim. +/// +/// **NOTE:** Doesn't consider reward expiration. +pub(crate) fn claimable_reward_range(account: AccountId) -> Option<(EraNumber, EraNumber)> { + let ledger = Ledger::::get(&account); + let protocol_state = ActiveProtocolState::::get(); + + let earliest_stake_era = if let Some(era) = ledger.earliest_staked_era() { + era + } else { + return None; + }; + + let last_claim_era = if ledger.staked_period() == Some(protocol_state.period_number()) { + protocol_state.era - 1 + } else { + // Period finished, we can claim up to its final era + let period_end = PeriodEnd::::get(ledger.staked_period().unwrap()).unwrap(); + period_end.final_era + }; + + Some((earliest_stake_era, last_claim_era)) +} + +/// Number of times it's required to call `claim_staker_rewards` to claim all pending rewards. +/// +/// In case no rewards are pending, return **zero**. +pub(crate) fn required_number_of_reward_claims(account: AccountId) -> u32 { + let range = if let Some(range) = claimable_reward_range(account) { + range + } else { + return 0; + }; + + let era_span_length: EraNumber = ::EraRewardSpanLength::get(); + let first = DappStaking::era_reward_span_index(range.0) + .checked_div(era_span_length) + .unwrap(); + let second = DappStaking::era_reward_span_index(range.1) + .checked_div(era_span_length) + .unwrap(); + + second - first + 1 +} diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 27f76a6d62..996a56f1e9 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -19,8 +19,8 @@ use crate::test::mock::*; use crate::test::testing_utils::*; use crate::{ - pallet as pallet_dapp_staking, ActiveProtocolState, DAppId, EraNumber, Error, IntegratedDApps, - Ledger, NextDAppId, PeriodType, StakerInfo, + pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, ForcingType, + IntegratedDApps, Ledger, NextDAppId, PeriodNumber, Subperiod, }; use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; @@ -29,6 +29,44 @@ use sp_runtime::traits::Zero; // TODO: test scenarios // 1. user is staking, period passes, they can unlock their funds which were previously staked +#[test] +fn print_test() { + ExtBuilder::build().execute_with(|| { + use crate::dsv3_weight::WeightInfo; + println!( + ">>> dApp tier assignment reading & calculation {:?}", + crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(200) + ); + + use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; + use scale_info::TypeInfo; + + #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] + struct RewardSize; + impl Get for RewardSize { + fn get() -> u32 { + 1_00_u32 + } + } + #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] + struct TierSize; + impl Get for TierSize { + fn get() -> u32 { + 4_u32 + } + } + println!( + ">>> Max encoded size for dapp tier rewards: {:?}", + crate::DAppTierRewards::::max_encoded_len() + ); + + println!( + ">>> Experimental storage entry read {:?}", + crate::dsv3_weight::SubstrateWeight::::experimental_read() + ); + }) +} + #[test] fn maintenace_mode_works() { ExtBuilder::build().execute_with(|| { @@ -63,7 +101,7 @@ fn maintenace_mode_call_filtering_works() { Error::::Disabled ); assert_noop!( - DappStaking::set_dapp_reward_destination( + DappStaking::set_dapp_reward_beneficiary( RuntimeOrigin::signed(1), MockSmartContract::Wasm(1), Some(2) @@ -102,6 +140,37 @@ fn maintenace_mode_call_filtering_works() { DappStaking::unstake(RuntimeOrigin::signed(1), MockSmartContract::default(), 100), Error::::Disabled ); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(1)), + Error::::Disabled + ); + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(1), MockSmartContract::default()), + Error::::Disabled + ); + assert_noop!( + DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(1), + MockSmartContract::default(), + 1 + ), + Error::::Disabled + ); + assert_noop!( + DappStaking::unstake_from_unregistered( + RuntimeOrigin::signed(1), + MockSmartContract::default() + ), + Error::::Disabled + ); + assert_noop!( + DappStaking::cleanup_expired_entries(RuntimeOrigin::signed(1)), + Error::::Disabled + ); + assert_noop!( + DappStaking::force(RuntimeOrigin::root(), ForcingType::Era), + Error::::Disabled + ); }) } @@ -114,7 +183,7 @@ fn on_initialize_state_change_works() { let protocol_state = ActiveProtocolState::::get(); assert_eq!(protocol_state.era, 1); assert_eq!(protocol_state.period_number(), 1); - assert_eq!(protocol_state.period_type(), PeriodType::Voting); + assert_eq!(protocol_state.subperiod(), Subperiod::Voting); assert_eq!(System::block_number(), 1); let blocks_per_voting_period = DappStaking::blocks_per_voting_period(); @@ -128,29 +197,28 @@ fn on_initialize_state_change_works() { run_to_block(protocol_state.next_era_start - 1); let protocol_state = ActiveProtocolState::::get(); assert_eq!( - protocol_state.period_type(), - PeriodType::Voting, + protocol_state.subperiod(), + Subperiod::Voting, "Period type should still be the same." ); assert_eq!(protocol_state.era, 1); run_for_blocks(1); let protocol_state = ActiveProtocolState::::get(); - assert_eq!(protocol_state.period_type(), PeriodType::BuildAndEarn); + assert_eq!(protocol_state.subperiod(), Subperiod::BuildAndEarn); assert_eq!(protocol_state.era, 2); assert_eq!(protocol_state.period_number(), 1); // Advance eras just until we reach the next voting period let eras_per_bep_period: EraNumber = - ::StandardErasPerBuildAndEarnPeriod::get(); - let blocks_per_era: BlockNumber = - ::StandardEraLength::get(); + ::StandardErasPerBuildAndEarnPeriod::get(); + let blocks_per_era: BlockNumber = ::StandardEraLength::get(); for era in 2..(2 + eras_per_bep_period - 1) { let pre_block = System::block_number(); advance_to_next_era(); assert_eq!(System::block_number(), pre_block + blocks_per_era); let protocol_state = ActiveProtocolState::::get(); - assert_eq!(protocol_state.period_type(), PeriodType::BuildAndEarn); + assert_eq!(protocol_state.subperiod(), Subperiod::BuildAndEarn); assert_eq!(protocol_state.period_number(), 1); assert_eq!(protocol_state.era, era + 1); } @@ -158,7 +226,7 @@ fn on_initialize_state_change_works() { // Finaly advance over to the next era and ensure we're back to voting period advance_to_next_era(); let protocol_state = ActiveProtocolState::::get(); - assert_eq!(protocol_state.period_type(), PeriodType::Voting); + assert_eq!(protocol_state.subperiod(), Subperiod::Voting); assert_eq!(protocol_state.era, 2 + eras_per_bep_period); assert_eq!( protocol_state.next_era_start, @@ -205,7 +273,7 @@ fn register_already_registered_contract_fails() { #[test] fn register_past_max_number_of_contracts_fails() { ExtBuilder::build().execute_with(|| { - let limit = ::MaxNumberOfContracts::get(); + let limit = ::MaxNumberOfContracts::get(); for id in 1..=limit { assert_register(1, &MockSmartContract::Wasm(id.into())); } @@ -240,7 +308,7 @@ fn register_past_sentinel_value_of_id_fails() { } #[test] -fn set_dapp_reward_destination_for_contract_is_ok() { +fn set_dapp_reward_beneficiary_for_contract_is_ok() { ExtBuilder::build().execute_with(|| { // Prepare & register smart contract let owner = 1; @@ -252,21 +320,21 @@ fn set_dapp_reward_destination_for_contract_is_ok() { .unwrap() .reward_destination .is_none()); - assert_set_dapp_reward_destination(owner, &smart_contract, Some(3)); - assert_set_dapp_reward_destination(owner, &smart_contract, Some(5)); - assert_set_dapp_reward_destination(owner, &smart_contract, None); + assert_set_dapp_reward_beneficiary(owner, &smart_contract, Some(3)); + assert_set_dapp_reward_beneficiary(owner, &smart_contract, Some(5)); + assert_set_dapp_reward_beneficiary(owner, &smart_contract, None); }) } #[test] -fn set_dapp_reward_destination_fails() { +fn set_dapp_reward_beneficiary_fails() { ExtBuilder::build().execute_with(|| { let owner = 1; let smart_contract = MockSmartContract::Wasm(3); // Contract doesn't exist yet assert_noop!( - DappStaking::set_dapp_reward_destination( + DappStaking::set_dapp_reward_beneficiary( RuntimeOrigin::signed(owner), smart_contract, Some(5) @@ -277,7 +345,7 @@ fn set_dapp_reward_destination_fails() { // Non-owner cannnot change reward destination assert_register(owner, &smart_contract); assert_noop!( - DappStaking::set_dapp_reward_destination( + DappStaking::set_dapp_reward_beneficiary( RuntimeOrigin::signed(owner + 1), smart_contract, Some(5) @@ -331,13 +399,29 @@ fn set_dapp_owner_fails() { } #[test] -fn unregister_is_ok() { +fn unregister_no_stake_is_ok() { ExtBuilder::build().execute_with(|| { // Prepare dApp let owner = 1; let smart_contract = MockSmartContract::Wasm(3); assert_register(owner, &smart_contract); + // Nothing staked on contract, just unregister it. + assert_unregister(&smart_contract); + }) +} + +#[test] +fn unregister_with_active_stake_is_ok() { + ExtBuilder::build().execute_with(|| { + // Prepare dApp + let owner = 1; + let smart_contract = MockSmartContract::Wasm(3); + assert_register(owner, &smart_contract); + assert_lock(owner, 100); + assert_stake(owner, &smart_contract, 100); + + // Some amount is staked, unregister must still work. assert_unregister(&smart_contract); }) } @@ -385,10 +469,7 @@ fn lock_is_ok() { // Ensure minimum lock amount works let locker = 3; - assert_lock( - locker, - ::MinimumLockedAmount::get(), - ); + assert_lock(locker, ::MinimumLockedAmount::get()); }) } @@ -412,8 +493,7 @@ fn lock_with_incorrect_amount_fails() { // Locking just below the minimum amount should fail let locker = 2; - let minimum_locked_amount: Balance = - ::MinimumLockedAmount::get(); + let minimum_locked_amount: Balance = ::MinimumLockedAmount::get(); assert_noop!( DappStaking::lock(RuntimeOrigin::signed(locker), minimum_locked_amount - 1), Error::::LockedAmountBelowThreshold, @@ -455,8 +535,7 @@ fn unlock_with_remaining_amount_below_threshold_is_ok() { advance_to_era(ActiveProtocolState::::get().era + 3); // Unlock such amount that remaining amount is below threshold, resulting in full unlock - let minimum_locked_amount: Balance = - ::MinimumLockedAmount::get(); + let minimum_locked_amount: Balance = ::MinimumLockedAmount::get(); let ledger = Ledger::::get(&account); assert_unlock( account, @@ -530,8 +609,7 @@ fn unlock_everything_with_active_stake_fails() { advance_to_next_era(); // We stake so the amount is just below the minimum locked amount, causing full unlock impossible. - let minimum_locked_amount: Balance = - ::MinimumLockedAmount::get(); + let minimum_locked_amount: Balance = ::MinimumLockedAmount::get(); let stake_amount = minimum_locked_amount - 1; // Register contract & stake on it @@ -583,7 +661,7 @@ fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { assert_lock(account, lock_amount); let unlock_amount = 3; - for _ in 0..::MaxUnlockingChunks::get() { + for _ in 0..::MaxUnlockingChunks::get() { run_for_blocks(1); assert_unlock(account, unlock_amount); } @@ -605,8 +683,7 @@ fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { #[test] fn claim_unlocked_is_ok() { ExtBuilder::build().execute_with(|| { - let unlocking_blocks: BlockNumber = - ::UnlockingPeriod::get(); + let unlocking_blocks = DappStaking::unlock_period(); // Lock some amount in a few eras let account = 2; @@ -620,8 +697,7 @@ fn claim_unlocked_is_ok() { assert_claim_unlocked(account); // Advanced example - let max_unlocking_chunks: u32 = - ::MaxUnlockingChunks::get(); + let max_unlocking_chunks: u32 = ::MaxUnlockingChunks::get(); for _ in 0..max_unlocking_chunks { run_for_blocks(1); assert_unlock(account, unlock_amount); @@ -635,6 +711,12 @@ fn claim_unlocked_is_ok() { run_for_blocks(2); assert_claim_unlocked(account); assert!(Ledger::::get(&account).unlocking.is_empty()); + + // Unlock everything + assert_unlock(account, lock_amount); + run_for_blocks(unlocking_blocks); + assert_claim_unlocked(account); + assert!(!Ledger::::contains_key(&account)); }) } @@ -651,8 +733,7 @@ fn claim_unlocked_no_eligible_chunks_fails() { // Cannot claim if unlock period hasn't passed yet let lock_amount = 103; assert_lock(account, lock_amount); - let unlocking_blocks: BlockNumber = - ::UnlockingPeriod::get(); + let unlocking_blocks = DappStaking::unlock_period(); run_for_blocks(unlocking_blocks - 1); assert_noop!( DappStaking::claim_unlocked(RuntimeOrigin::signed(account)), @@ -677,8 +758,7 @@ fn relock_unlocking_is_ok() { assert_relock_unlocking(account); - let max_unlocking_chunks: u32 = - ::MaxUnlockingChunks::get(); + let max_unlocking_chunks: u32 = ::MaxUnlockingChunks::get(); for _ in 0..max_unlocking_chunks { run_for_blocks(1); assert_unlock(account, unlock_amount); @@ -701,8 +781,7 @@ fn relock_unlocking_no_chunks_fails() { #[test] fn relock_unlocking_insufficient_lock_amount_fails() { ExtBuilder::build().execute_with(|| { - let minimum_locked_amount: Balance = - ::MinimumLockedAmount::get(); + let minimum_locked_amount: Balance = ::MinimumLockedAmount::get(); // lock amount should be above the threshold let account = 2; @@ -732,8 +811,7 @@ fn relock_unlocking_insufficient_lock_amount_fails() { }); // Make sure only one chunk is left - let unlocking_blocks: BlockNumber = - ::UnlockingPeriod::get(); + let unlocking_blocks = DappStaking::unlock_period(); run_for_blocks(unlocking_blocks - 1); assert_claim_unlocked(account); @@ -756,27 +834,10 @@ fn stake_basic_example_is_ok() { let lock_amount = 300; assert_lock(account, lock_amount); - // 1st scenario - stake some amount, and then some more + // Stake some amount, and then some more let (stake_amount_1, stake_amount_2) = (31, 29); assert_stake(account, &smart_contract, stake_amount_1); assert_stake(account, &smart_contract, stake_amount_2); - - // 2nd scenario - stake in the next era - advance_to_next_era(); - let stake_amount_3 = 23; - assert_stake(account, &smart_contract, stake_amount_3); - - // 3rd scenario - advance era again but create a gap, and then stake - advance_to_era(ActiveProtocolState::::get().era + 2); - let stake_amount_4 = 19; - assert_stake(account, &smart_contract, stake_amount_4); - - // 4th scenario - advance period, and stake - // advance_to_next_era(); - // advance_to_next_period(); - // let stake_amount_5 = 17; - // assert_stake(account, &smart_contract, stake_amount_5); - // TODO: this can only be tested after reward claiming has been implemented!!! }) } @@ -830,8 +891,8 @@ fn stake_in_final_era_fails() { // Force Build&Earn period ActiveProtocolState::::mutate(|state| { - state.period_info.period_type = PeriodType::BuildAndEarn; - state.period_info.ending_era = state.era + 1; + state.period_info.subperiod = Subperiod::BuildAndEarn; + state.period_info.subperiod_end_era = state.era + 1; }); // Try to stake in the final era of the period, which should fail. @@ -892,34 +953,6 @@ fn stake_fails_if_not_enough_stakeable_funds_available() { }) } -#[test] -fn stake_fails_due_to_too_many_chunks() { - ExtBuilder::build().execute_with(|| { - // Register smart contract & lock some amount - let smart_contract = MockSmartContract::default(); - let account = 3; - assert_register(1, &smart_contract); - let lock_amount = 500; - assert_lock(account, lock_amount); - - // Keep on staking & creating chunks until capacity is reached - for _ in 0..(::MaxStakingChunks::get()) { - advance_to_next_era(); - assert_stake(account, &smart_contract, 10); - } - - // Ensure we can still stake in the current era since an entry exists - assert_stake(account, &smart_contract, 10); - - // Staking in the next era results in error due to too many chunks - advance_to_next_era(); - assert_noop!( - DappStaking::stake(RuntimeOrigin::signed(account), smart_contract.clone(), 10), - Error::::TooManyStakeChunks - ); - }) -} - #[test] fn stake_fails_due_to_too_small_staking_amount() { ExtBuilder::build().execute_with(|| { @@ -932,8 +965,7 @@ fn stake_fails_due_to_too_small_staking_amount() { assert_lock(account, 300); // Stake with too small amount, expect a failure - let min_stake_amount: Balance = - ::MinimumStakeAmount::get(); + let min_stake_amount: Balance = ::MinimumStakeAmount::get(); assert_noop!( DappStaking::stake( RuntimeOrigin::signed(account), @@ -960,6 +992,8 @@ fn stake_fails_due_to_too_small_staking_amount() { }) } +// TODO: add tests to cover staking & unstaking with unclaimed rewards! + #[test] fn unstake_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { @@ -976,33 +1010,11 @@ fn unstake_basic_example_is_ok() { let stake_amount_1 = 83; assert_stake(account, &smart_contract, stake_amount_1); - // 1st scenario - unstake some amount, in the current era. + // Unstake some amount, in the current era. let unstake_amount_1 = 3; assert_unstake(account, &smart_contract, unstake_amount_1); - // 2nd scenario - advance to next era/period type, and unstake some more - let unstake_amount_2 = 7; - let unstake_amount_3 = 11; - advance_to_next_era(); - assert_eq!( - ActiveProtocolState::::get().period_type(), - PeriodType::BuildAndEarn, - "Sanity check, period type change must happe." - ); - assert_unstake(account, &smart_contract, unstake_amount_2); - assert_unstake(account, &smart_contract, unstake_amount_3); - - // 3rd scenario - advance few eras to create a gap, and unstake some more - advance_to_era(ActiveProtocolState::::get().era + 3); - assert_unstake(account, &smart_contract, unstake_amount_3); - assert_unstake(account, &smart_contract, unstake_amount_2); - - // 4th scenario - perform a full unstake - advance_to_next_era(); - let full_unstake_amount = StakerInfo::::get(&account, &smart_contract) - .unwrap() - .total_staked_amount(); - assert_unstake(account, &smart_contract, full_unstake_amount); + // TODO: scenario where we unstake AFTER advancing an era and claiming rewards }) } @@ -1018,8 +1030,7 @@ fn unstake_with_leftover_amount_below_minimum_works() { let amount = 300; assert_lock(account, amount); - let min_stake_amount: Balance = - ::MinimumStakeAmount::get(); + let min_stake_amount: Balance = ::MinimumStakeAmount::get(); assert_stake(account, &smart_contract, min_stake_amount); // Unstake some amount, bringing it below the minimum @@ -1027,33 +1038,6 @@ fn unstake_with_leftover_amount_below_minimum_works() { }) } -#[test] -fn unstake_with_entry_overflow_attempt_works() { - ExtBuilder::build().execute_with(|| { - // Register smart contract & lock some amount - let dev_account = 1; - let smart_contract = MockSmartContract::default(); - assert_register(dev_account, &smart_contract); - - let account = 2; - let amount = 300; - assert_lock(account, amount); - - assert_stake(account, &smart_contract, amount); - - // Advance one era, unstake some amount. The goal is to make a new entry. - advance_to_next_era(); - assert_unstake(account, &smart_contract, 11); - - // Advance 2 eras, stake some amount. This should create a new entry for the next era. - advance_to_era(ActiveProtocolState::::get().era + 2); - assert_stake(account, &smart_contract, 3); - - // Unstake some amount, which should result in the creation of the 4th entry, but the oldest one should be prunned. - assert_unstake(account, &smart_contract, 1); - }) -} - #[test] fn unstake_with_zero_amount_fails() { ExtBuilder::build().execute_with(|| { @@ -1208,30 +1192,448 @@ fn unstake_from_past_period_fails() { } #[test] -fn unstake_fails_due_to_too_many_chunks() { +fn claim_staker_rewards_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { - // Register smart contract,lock & stake some amount + // Register smart contract, lock&stake some amount + let dev_account = 1; let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + let account = 2; - assert_register(1, &smart_contract); - let lock_amount = 1000; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + // Advance into Build&Earn period, and allow one era to pass. Claim reward for 1 era. + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_claim_staker_rewards(account); + + // Advance a few more eras, and claim multiple rewards this time. + advance_to_era(ActiveProtocolState::::get().era + 3); + assert_eq!( + ActiveProtocolState::::get().period_number(), + 1, + "Sanity check, we must still be in the 1st period." + ); + assert_claim_staker_rewards(account); + + // Advance into the next period, make sure we can still claim old rewards. + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + }) +} + +#[test] +fn claim_staker_rewards_double_call_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + // Advance into the next period, claim all eligible rewards + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + }) +} + +#[test] +fn claim_staker_rewards_no_claimable_rewards_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + + // 1st scenario - try to claim with no stake at all. + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + + // 2nd scenario - stake some amount, and try to claim in the same era. + // It's important this is the 1st era, when no `EraRewards` entry exists. + assert_eq!(ActiveProtocolState::::get().era, 1, "Sanity check"); + assert!(EraRewards::::iter().next().is_none(), "Sanity check"); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + + // 3rd scenario - move over to the next era, but we still expect failure because + // stake is valid from era 2 (current era), and we're trying to claim rewards for era 1. + advance_to_next_era(); + assert!(EraRewards::::iter().next().is_some(), "Sanity check"); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::NoClaimableRewards, + ); + }) +} + +#[test] +fn claim_staker_rewards_after_expiry_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + + // Advance to the block just before the 'expiry' period starts + advance_to_period( + ActiveProtocolState::::get().period_number() + reward_retention_in_periods, + ); + advance_to_advance_to_next_subperiod(); + advance_to_era( + ActiveProtocolState::::get() + .period_info + .subperiod_end_era + - 1, + ); + assert_claim_staker_rewards(account); + + // Ensure we're still in the first period for the sake of test validity + assert_eq!( + Ledger::::get(&account).staked.period, + 1, + "Sanity check." + ); + + // Trigger next period, rewards should be marked as expired + advance_to_next_era(); + assert_eq!( + ActiveProtocolState::::get().period_number(), + reward_retention_in_periods + 2 + ); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::RewardExpired, + ); + }) +} + +#[test] +fn claim_bonus_reward_works() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + // 1st scenario - advance to the next period, first claim bonus reward, then staker rewards + advance_to_next_period(); + assert_claim_bonus_reward(account, &smart_contract); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + + // 2nd scenario - stake again, advance to next period, this time first claim staker rewards, then bonus reward. + assert_stake(account, &smart_contract, stake_amount); + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + assert!( + Ledger::::get(&account).staked.is_empty(), + "Sanity check." + ); + assert_claim_bonus_reward(account, &smart_contract); + }) +} + +#[test] +fn claim_bonus_reward_double_call_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + // Advance to the next period, claim bonus reward, then try to do it again + advance_to_next_period(); + assert_claim_bonus_reward(account, &smart_contract); + + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), + Error::::NoClaimableRewards, + ); + }) +} + +#[test] +fn claim_bonus_reward_when_nothing_to_claim_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + + // 1st - try to claim bonus reward when no stake is present + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), + Error::::NoClaimableRewards, + ); + + // 2nd - try to claim bonus reward for the ongoing period + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), + Error::::NoClaimableRewards, + ); + }) +} + +#[test] +fn claim_bonus_reward_with_only_build_and_earn_stake_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; + assert_lock(account, lock_amount); + + // Stake in Build&Earn period type, advance to next era and try to claim bonus reward + advance_to_advance_to_next_subperiod(); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "Sanity check." + ); + let stake_amount = 93; + assert_stake(account, &smart_contract, stake_amount); + + advance_to_next_period(); + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), + Error::::NotEligibleForBonusReward, + ); + }) +} + +#[test] +fn claim_bonus_reward_after_expiry_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let lock_amount = 300; assert_lock(account, lock_amount); assert_stake(account, &smart_contract, lock_amount); - // Keep on unstaking & creating chunks until capacity is reached - for _ in 0..(::MaxStakingChunks::get()) { - advance_to_next_era(); - assert_unstake(account, &smart_contract, 11); + // 1st scenario - Advance to one period before the expiry, claim should still work. + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + advance_to_period( + ActiveProtocolState::::get().period_number() + reward_retention_in_periods, + ); + assert_claim_bonus_reward(account, &smart_contract); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); } - // Ensure we can still unstake in the current era since an entry exists - assert_unstake(account, &smart_contract, 10); + // 2nd scenario - advance past the expiry, call must fail + assert_stake(account, &smart_contract, lock_amount); + advance_to_period( + ActiveProtocolState::::get().period_number() + reward_retention_in_periods + 1, + ); + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), + Error::::RewardExpired, + ); + }) +} + +#[test] +fn claim_dapp_reward_works() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); - // Staking in the next era results in error due to too many chunks + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance 2 eras so we have an entry for reward claiming + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_eq!(ActiveProtocolState::::get().era, 3, "Sanity check"); + + assert_claim_dapp_reward( + account, + &smart_contract, + ActiveProtocolState::::get().era - 1, + ); + + // Advance to next era, and ensure rewards can be paid out to a custom beneficiary + let new_beneficiary = 17; + assert_set_dapp_reward_beneficiary(dev_account, &smart_contract, Some(new_beneficiary)); advance_to_next_era(); + assert_claim_dapp_reward( + account, + &smart_contract, + ActiveProtocolState::::get().era - 1, + ); + }) +} + +#[test] +fn claim_dapp_reward_from_non_existing_contract_fails() { + ExtBuilder::build().execute_with(|| { + let smart_contract = MockSmartContract::default(); + assert_noop!( + DappStaking::claim_dapp_reward(RuntimeOrigin::signed(1), smart_contract, 1), + Error::::ContractNotFound, + ); + }) +} + +#[test] +fn claim_dapp_reward_from_invalid_era_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance 2 eras and try to claim from the ongoing era. + advance_to_era(ActiveProtocolState::::get().era + 2); assert_noop!( - DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract.clone(), 10), - Error::::TooManyStakeChunks + DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(1), + smart_contract, + ActiveProtocolState::::get().era + ), + Error::::InvalidClaimEra, + ); + + // Try to claim from the era which corresponds to the voting period. No tier info should + assert_noop!( + DappStaking::claim_dapp_reward(RuntimeOrigin::signed(1), smart_contract, 1), + Error::::NoDAppTierInfo, + ); + }) +} + +#[test] +fn claim_dapp_reward_if_dapp_not_in_any_tier_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract_1 = MockSmartContract::Wasm(3); + let smart_contract_2 = MockSmartContract::Wasm(5); + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract_1, amount); + + // Advance 2 eras and try to claim reward for non-staked dApp. + advance_to_era(ActiveProtocolState::::get().era + 2); + let account = 2; + let claim_era = ActiveProtocolState::::get().era - 1; + assert_noop!( + DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(account), + smart_contract_2, + claim_era + ), + Error::::NoClaimableRewards, ); + // Staked dApp should still be able to claim. + assert_claim_dapp_reward(account, &smart_contract_1, claim_era); + }) +} + +#[test] +fn claim_dapp_reward_twice_for_same_era_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance 3 eras and claim rewards. + advance_to_era(ActiveProtocolState::::get().era + 3); + + // We can only claim reward ONCE for a particular era + let claim_era_1 = ActiveProtocolState::::get().era - 2; + assert_claim_dapp_reward(account, &smart_contract, claim_era_1); + assert_noop!( + DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(account), + smart_contract, + claim_era_1 + ), + Error::::DAppRewardAlreadyClaimed, + ); + + // We can still claim for another valid era + let claim_era_2 = claim_era_1 + 1; + assert_claim_dapp_reward(account, &smart_contract, claim_era_2); }) } diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index ba80bfe295..6554faed11 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -16,9 +16,11 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . +use astar_primitives::{Balance, BlockNumber}; use frame_support::assert_ok; +use sp_arithmetic::fixed_point::FixedU64; +use sp_runtime::Permill; -use crate::test::mock::{Balance, *}; use crate::*; // Helper to generate custom `Get` types for testing the `AccountLedger` struct. @@ -34,314 +36,36 @@ macro_rules! get_u32_type { }; } -#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, Default)] -struct DummyEraAmount { - amount: Balance, - era: u32, -} -impl AmountEraPair for DummyEraAmount { - fn get_amount(&self) -> Balance { - self.amount - } - fn get_era(&self) -> u32 { - self.era - } - fn set_era(&mut self, era: u32) { - self.era = era; - } - fn saturating_accrue(&mut self, increase: Balance) { - self.amount.saturating_accrue(increase); - } - fn saturating_reduce(&mut self, reduction: Balance) { - self.amount.saturating_reduce(reduction); - } -} -impl DummyEraAmount { - pub fn new(amount: Balance, era: u32) -> Self { - Self { amount, era } - } -} - -#[test] -fn sparse_bounded_amount_era_vec_add_amount_works() { - get_u32_type!(MaxLen, 5); - - // Sanity check - let mut vec = SparseBoundedAmountEraVec::::new(); - assert!(vec.0.is_empty()); - assert_ok!(vec.add_amount(0, 0)); - assert!(vec.0.is_empty()); - - // 1st scenario - add to empty vector, should create one entry - let init_amount = 19; - let first_era = 3; - assert_ok!(vec.add_amount(init_amount, first_era)); - assert_eq!(vec.0.len(), 1); - assert_eq!(vec.0[0], DummyEraAmount::new(init_amount, first_era)); - - // 2nd scenario - add to the same era, should update the entry - assert_ok!(vec.add_amount(init_amount, first_era)); - assert_eq!(vec.0.len(), 1); - assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); - - // 3rd scenario - add to the next era, should create a new entry - let second_era = first_era + 1; - assert_ok!(vec.add_amount(init_amount, second_era)); - assert_eq!(vec.0.len(), 2); - assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); - assert_eq!(vec.0[1], DummyEraAmount::new(init_amount * 3, second_era)); - - // 4th scenario - add to the previous era, should fail and be a noop - assert_eq!( - vec.add_amount(init_amount, first_era), - Err(AccountLedgerError::OldEra) - ); - assert_eq!(vec.0.len(), 2); - assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); - assert_eq!(vec.0[1], DummyEraAmount::new(init_amount * 3, second_era)); - - // 5th scenario - exceed capacity, should fail - for i in vec.0.len()..MaxLen::get() as usize { - assert_ok!(vec.add_amount(init_amount, second_era + i as u32)); - } - assert_eq!( - vec.add_amount(init_amount, 100), - Err(AccountLedgerError::NoCapacity) - ); -} - -// Test two scenarios: -// -// 1. [amount, era] -> subtract(x, era) -> [amount - x, era] -// 2. [amount, era] -> subtract (amount * 2, era) -> [] -#[test] -fn sparse_bounded_amount_era_vec_subtract_amount_basic_scenario_works() { - get_u32_type!(MaxLen, 5); - - // Sanity check - let mut vec = SparseBoundedAmountEraVec::::new(); - assert_ok!(vec.subtract_amount(0, 0)); - assert!(vec.0.is_empty()); - - // 1st scenario - only one entry exists, and it's the same era as the unlock - let init_amount = 19; - let first_era = 1; - let sub_amount = 3; - assert_ok!(vec.add_amount(init_amount, first_era)); - assert_ok!(vec.subtract_amount(sub_amount, first_era)); - assert_eq!(vec.0.len(), 1); - assert_eq!( - vec.0[0], - DummyEraAmount::new(init_amount - sub_amount, first_era), - "Only single entry and it should be updated." - ); - - // 2nd scenario - subtract everything (and more - underflow!) from the current era, causing full removal. Should cleanup the vector. - assert_ok!(vec.subtract_amount(init_amount * 2, first_era)); - assert!(vec.0.is_empty(), "Full removal should cleanup the vector."); -} - -#[test] -fn sparse_bounded_amount_era_vec_subtract_amount_advanced_consecutive_works() { - get_u32_type!(MaxLen, 5); - let mut vec = SparseBoundedAmountEraVec::::new(); - - // 1st scenario - two entries, consecutive eras, subtract from the second era. - // Only the second entry should be updated. - let (first_era, second_era) = (1, 2); - let (first_amount, second_amount) = (19, 23); - assert_ok!(vec.add_amount(first_amount, first_era)); - assert_ok!(vec.add_amount(second_amount, second_era)); - - let sub_amount = 3; - assert_ok!(vec.subtract_amount(sub_amount, second_era)); - assert_eq!(vec.0.len(), 2); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount, first_era), - "First entry should remain unchanged." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(first_amount + second_amount - sub_amount, second_era), - "Second entry should have it's amount reduced by the subtracted amount." - ); - - // 2nd scenario - two entries, consecutive eras, subtract from the first era. - // Both the first and second entry should be updated. - assert_ok!(vec.subtract_amount(sub_amount, first_era)); - assert_eq!(vec.0.len(), 2); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount - sub_amount, first_era), - "First entry is updated since it was specified." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(first_amount + second_amount - sub_amount * 2, second_era), - "Second entry is updated because it comes AFTER the first one - same applies to all future entries." - ); - - // 3rd scenario - three entries, consecutive eras, subtract from the second era. - // Only second and third entry should be updated. First one should remain unchanged. - let third_era = 3; - let third_amount = 29; - assert_ok!(vec.add_amount(third_amount, third_era)); - assert_ok!(vec.subtract_amount(sub_amount, second_era)); - assert_eq!(vec.0.len(), 3); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount - sub_amount, first_era), - "First entry should remain unchanged, compared to previous scenario." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(first_amount + second_amount - sub_amount * 3, second_era), - "Second entry should be reduced by the subtracted amount, compared to previous scenario." - ); - assert_eq!( - vec.0[2], - DummyEraAmount::new( - first_amount + second_amount + third_amount - sub_amount * 3, - third_era - ), - "Same as for the second entry." - ); -} - #[test] -fn sparse_bounded_amount_era_vec_subtract_amount_advanced_non_consecutive_works() { - get_u32_type!(MaxLen, 5); - let mut vec = SparseBoundedAmountEraVec::::new(); - - // 1st scenario - two entries, non-consecutive eras, subtract from the mid era. - // Only the second entry should be updated but a new entry should be created. - let (first_era, second_era) = (1, 5); - let (first_amount, second_amount) = (19, 23); - assert_ok!(vec.add_amount(first_amount, first_era)); - assert_ok!(vec.add_amount(second_amount, second_era)); - - let sub_amount = 3; - let mid_era = second_era - 1; - assert_ok!(vec.subtract_amount(sub_amount, mid_era)); - assert_eq!(vec.0.len(), 3); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount, first_era), - "No impact on the first entry expected." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(first_amount - sub_amount, mid_era), - "Newly created entry should be equal to the first amount, minus what was subtracted." - ); - assert_eq!( - vec.0[2], - DummyEraAmount::new(vec.0[1].amount + second_amount, second_era), - "Previous 'second' entry should be total added minus the subtracted amount." - ); - - // 2nd scenario - fully unlock the mid-entry to create a zero entry. - assert_ok!(vec.subtract_amount(vec.0[1].amount, mid_era)); - assert_eq!(vec.0.len(), 3); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount, first_era), - "No impact on the first entry expected." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(0, mid_era), - "Zero entry should be kept since it's in between two non-zero entries." - ); - assert_eq!( - vec.0[2], - DummyEraAmount::new(second_amount, second_era), - "Only the second staked amount should remain since everything else was unstaked." - ); - - // 3rd scenario - create an additional non-zero chunk as prep for the next scenario. - let pre_mid_era = mid_era - 1; - assert!(pre_mid_era > first_era, "Sanity check."); - assert_ok!(vec.subtract_amount(sub_amount, pre_mid_era)); - assert_eq!(vec.0.len(), 4); - assert_eq!( - vec.0[1], - DummyEraAmount::new(first_amount - sub_amount, pre_mid_era), - "Newly created entry, derives it's initial value from the first entry." - ); - assert_eq!( - vec.0[2], - DummyEraAmount::new(0, mid_era), - "Zero entry should be kept at this point since it's still between two non-zero entries." - ); - assert_eq!( - vec.0[3], - DummyEraAmount::new(second_amount - sub_amount, second_era), - "Last entry should be further reduced by the newly subtracted amount." - ); - - // 4th scenario - create an additional zero entry, but ensure it's cleaned up correctly. - let final_sub_amount = vec.0[1].amount; - assert_ok!(vec.subtract_amount(final_sub_amount, pre_mid_era)); - assert_eq!(vec.0.len(), 3); - assert_eq!( - vec.0[0], - DummyEraAmount::new(first_amount, first_era), - "First entry should still remain unchanged." - ); - assert_eq!( - vec.0[1], - DummyEraAmount::new(0, pre_mid_era), - "The older zero entry should consume the newer ones, hence the pre_mid_era usage" - ); - assert_eq!( - vec.0[2], - DummyEraAmount::new(second_amount - sub_amount - final_sub_amount, second_era), - "Last entry should be further reduced by the newly subtracted amount." - ); -} - -#[test] -fn sparse_bounded_amount_era_vec_full_subtract_with_single_future_era() { - get_u32_type!(MaxLen, 5); - let mut vec = SparseBoundedAmountEraVec::::new(); - - // A scenario where some amount is added, for the first time, for era X. - // Immediately afterward, the same amount is subtracted from era X - 1. - let (era_1, era_2) = (1, 2); - let amount = 19; - assert_ok!(vec.add_amount(amount, era_2)); - - assert_ok!(vec.subtract_amount(amount, era_1)); - assert!( - vec.0.is_empty(), - "Future entry should have been cleaned up." - ); -} - -#[test] -fn period_type_sanity_check() { - assert_eq!(PeriodType::Voting.next(), PeriodType::BuildAndEarn); - assert_eq!(PeriodType::BuildAndEarn.next(), PeriodType::Voting); +fn subperiod_sanity_check() { + assert_eq!(Subperiod::Voting.next(), Subperiod::BuildAndEarn); + assert_eq!(Subperiod::BuildAndEarn.next(), Subperiod::Voting); } #[test] fn period_info_basic_checks() { let period_number = 2; - let ending_era = 5; - let info = PeriodInfo::new(period_number, PeriodType::Voting, ending_era); + let subperiod_end_era = 5; + let info = PeriodInfo { + number: period_number, + subperiod: Subperiod::Voting, + subperiod_end_era: subperiod_end_era, + }; // Sanity checks assert_eq!(info.number, period_number); - assert_eq!(info.period_type, PeriodType::Voting); - assert_eq!(info.ending_era, ending_era); + assert_eq!(info.subperiod, Subperiod::Voting); + assert_eq!(info.subperiod_end_era, subperiod_end_era); // Voting period checks - assert!(!info.is_next_period(ending_era - 1)); - assert!(!info.is_next_period(ending_era)); - assert!(!info.is_next_period(ending_era + 1)); - for era in vec![ending_era - 1, ending_era, ending_era + 1] { + assert!(!info.is_next_period(subperiod_end_era - 1)); + assert!(!info.is_next_period(subperiod_end_era)); + assert!(!info.is_next_period(subperiod_end_era + 1)); + for era in vec![ + subperiod_end_era - 1, + subperiod_end_era, + subperiod_end_era + 1, + ] { assert!( !info.is_next_period(era), "Cannot trigger 'true' in the Voting period type." @@ -349,10 +73,14 @@ fn period_info_basic_checks() { } // Build&Earn period checks - let info = PeriodInfo::new(period_number, PeriodType::BuildAndEarn, ending_era); - assert!(!info.is_next_period(ending_era - 1)); - assert!(info.is_next_period(ending_era)); - assert!(info.is_next_period(ending_era + 1)); + let info = PeriodInfo { + number: period_number, + subperiod: Subperiod::BuildAndEarn, + subperiod_end_era: subperiod_end_era, + }; + assert!(!info.is_next_period(subperiod_end_era - 1)); + assert!(info.is_next_period(subperiod_end_era)); + assert!(info.is_next_period(subperiod_end_era + 1)); } #[test] @@ -370,13 +98,17 @@ fn protocol_state_default() { fn protocol_state_basic_checks() { let mut protocol_state = ProtocolState::::default(); let period_number = 5; - let ending_era = 11; + let subperiod_end_era = 11; let next_era_start = 31; - protocol_state.period_info = PeriodInfo::new(period_number, PeriodType::Voting, ending_era); + protocol_state.period_info = PeriodInfo { + number: period_number, + subperiod: Subperiod::Voting, + subperiod_end_era: subperiod_end_era, + }; protocol_state.next_era_start = next_era_start; assert_eq!(protocol_state.period_number(), period_number); - assert_eq!(protocol_state.period_type(), PeriodType::Voting); + assert_eq!(protocol_state.subperiod(), Subperiod::Voting); // New era check assert!(!protocol_state.is_new_era(next_era_start - 1)); @@ -384,38 +116,57 @@ fn protocol_state_basic_checks() { assert!(protocol_state.is_new_era(next_era_start + 1)); // Toggle new period type check - 'Voting' to 'BuildAndEarn' - let ending_era_1 = 23; + let subperiod_end_era_1 = 23; let next_era_start_1 = 41; - protocol_state.next_period_type(ending_era_1, next_era_start_1); - assert_eq!(protocol_state.period_type(), PeriodType::BuildAndEarn); + protocol_state.advance_to_next_subperiod(subperiod_end_era_1, next_era_start_1); + assert_eq!(protocol_state.subperiod(), Subperiod::BuildAndEarn); assert_eq!( protocol_state.period_number(), period_number, "Switching from 'Voting' to 'BuildAndEarn' should not trigger period bump." ); - assert_eq!(protocol_state.ending_era(), ending_era_1); + assert_eq!(protocol_state.period_end_era(), subperiod_end_era_1); assert!(!protocol_state.is_new_era(next_era_start_1 - 1)); assert!(protocol_state.is_new_era(next_era_start_1)); // Toggle from 'BuildAndEarn' over to 'Voting' - let ending_era_2 = 24; + let subperiod_end_era_2 = 24; let next_era_start_2 = 91; - protocol_state.next_period_type(ending_era_2, next_era_start_2); - assert_eq!(protocol_state.period_type(), PeriodType::Voting); + protocol_state.advance_to_next_subperiod(subperiod_end_era_2, next_era_start_2); + assert_eq!(protocol_state.subperiod(), Subperiod::Voting); assert_eq!( protocol_state.period_number(), period_number + 1, "Switching from 'BuildAndEarn' to 'Voting' must trigger period bump." ); - assert_eq!(protocol_state.ending_era(), ending_era_2); + assert_eq!(protocol_state.period_end_era(), subperiod_end_era_2); assert!(protocol_state.is_new_era(next_era_start_2)); } +#[test] +fn dapp_info_basic_checks() { + let owner = 1; + let beneficiary = 3; + + let mut dapp_info = DAppInfo { + owner, + id: 7, + state: DAppState::Registered, + reward_destination: None, + }; + + // Owner receives reward in case no beneficiary is set + assert_eq!(*dapp_info.reward_beneficiary(), owner); + + // Beneficiary receives rewards in case it is set + dapp_info.reward_destination = Some(beneficiary); + assert_eq!(*dapp_info.reward_beneficiary(), beneficiary); +} + #[test] fn account_ledger_default() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let acc_ledger = AccountLedger::::default(); + let acc_ledger = AccountLedger::::default(); assert!(acc_ledger.is_empty()); assert!(acc_ledger.active_locked_amount().is_zero()); @@ -424,8 +175,7 @@ fn account_ledger_default() { #[test] fn account_ledger_add_lock_amount_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // First step, sanity checks assert!(acc_ledger.active_locked_amount().is_zero()); @@ -444,8 +194,7 @@ fn account_ledger_add_lock_amount_works() { #[test] fn account_ledger_subtract_lock_amount_basic_usage_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop @@ -486,8 +235,12 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { #[test] fn account_ledger_add_unlocking_chunk_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); + + // Base sanity check + let default_unlocking_chunk = UnlockingChunk::::default(); + assert!(default_unlocking_chunk.amount.is_zero()); + assert!(default_unlocking_chunk.unlock_block.is_zero()); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop @@ -528,7 +281,7 @@ fn account_ledger_add_unlocking_chunk_works() { for i in 2..=UnlockingDummy::get() { let new_unlock_amount = unlock_amount + i as u128; assert!(acc_ledger - .add_unlocking_chunk(new_unlock_amount, block_number + i as u64) + .add_unlocking_chunk(new_unlock_amount, block_number + i) .is_ok()); total_unlocking += new_unlock_amount; assert_eq!(acc_ledger.unlocking_amount(), total_unlocking); @@ -541,52 +294,114 @@ fn account_ledger_add_unlocking_chunk_works() { // Any further addition should fail, resulting in a noop let acc_ledger_snapshot = acc_ledger.clone(); assert_eq!( - acc_ledger.add_unlocking_chunk(1, block_number + UnlockingDummy::get() as u64 + 1), + acc_ledger.add_unlocking_chunk(1, block_number + UnlockingDummy::get() + 1), Err(AccountLedgerError::NoCapacity) ); assert_eq!(acc_ledger, acc_ledger_snapshot); } #[test] -fn account_ledger_active_stake_works() { +fn account_ledger_staked_amount_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check - assert!(acc_ledger.active_stake(0).is_zero()); - assert!(acc_ledger.active_stake(1).is_zero()); + assert!(acc_ledger.staked_amount(0).is_zero()); + assert!(acc_ledger.staked_amount(1).is_zero()); // Period matches - let amount = 29; + let amount_1 = 29; let period = 5; - acc_ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { amount, era: 1 }]) - .expect("Only one chunk so creation should succeed."), - ); - acc_ledger.staked_period = Some(period); - assert_eq!(acc_ledger.active_stake(period), amount); + acc_ledger.staked = StakeAmount::new(amount_1, 0, 1, period); + assert_eq!(acc_ledger.staked_amount(period), amount_1); // Period doesn't match - assert!(acc_ledger.active_stake(period - 1).is_zero()); - assert!(acc_ledger.active_stake(period + 1).is_zero()); + assert!(acc_ledger.staked_amount(period - 1).is_zero()); + assert!(acc_ledger.staked_amount(period + 1).is_zero()); + + // Add future entry + let amount_2 = 17; + acc_ledger.staked_future = Some(StakeAmount::new(0, amount_2, 2, period)); + assert_eq!(acc_ledger.staked_amount(period), amount_2); + assert!(acc_ledger.staked_amount(period - 1).is_zero()); + assert!(acc_ledger.staked_amount(period + 1).is_zero()); +} + +#[test] +fn account_ledger_staked_amount_for_type_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // 1st scenario - 'current' entry is set, 'future' is None + let (voting_1, build_and_earn_1, period) = (31, 43, 2); + acc_ledger.staked = StakeAmount { + voting: voting_1, + build_and_earn: build_and_earn_1, + era: 10, + period, + }; + acc_ledger.staked_future = None; + + // Correct period should return staked amounts + assert_eq!( + acc_ledger.staked_amount_for_type(Subperiod::Voting, period), + voting_1 + ); + assert_eq!( + acc_ledger.staked_amount_for_type(Subperiod::BuildAndEarn, period), + build_and_earn_1 + ); + + // Inocrrect period should simply return 0 + assert!(acc_ledger + .staked_amount_for_type(Subperiod::Voting, period - 1) + .is_zero()); + assert!(acc_ledger + .staked_amount_for_type(Subperiod::BuildAndEarn, period - 1) + .is_zero()); + + // 2nd scenario - both entries are set, but 'future' must be relevant one. + let (voting_2, build_and_earn_2, period) = (13, 19, 2); + acc_ledger.staked_future = Some(StakeAmount { + voting: voting_2, + build_and_earn: build_and_earn_2, + era: 20, + period, + }); + + // Correct period should return staked amounts + assert_eq!( + acc_ledger.staked_amount_for_type(Subperiod::Voting, period), + voting_2 + ); + assert_eq!( + acc_ledger.staked_amount_for_type(Subperiod::BuildAndEarn, period), + build_and_earn_2 + ); + + // Inocrrect period should simply return 0 + assert!(acc_ledger + .staked_amount_for_type(Subperiod::Voting, period - 1) + .is_zero()); + assert!(acc_ledger + .staked_amount_for_type(Subperiod::BuildAndEarn, period - 1) + .is_zero()); } #[test] fn account_ledger_stakeable_amount_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check for empty ledger assert!(acc_ledger.stakeable_amount(1).is_zero()); - // First scenario - some locked amount, no staking chunks - let first_period = 1; + // 1st scenario - some locked amount, no staking chunks + let period_1 = 1; let locked_amount = 19; acc_ledger.add_lock_amount(locked_amount); assert_eq!( - acc_ledger.stakeable_amount(first_period), + acc_ledger.stakeable_amount(period_1), locked_amount, "Stakeable amount has to be equal to the locked amount" ); @@ -594,356 +409,515 @@ fn account_ledger_stakeable_amount_works() { // Second scenario - some staked amount is introduced, period is still valid let first_era = 1; let staked_amount = 7; - acc_ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { - amount: staked_amount, - era: first_era, - }]) - .expect("Only one chunk so creation should succeed."), - ); - acc_ledger.staked_period = Some(first_period); + acc_ledger.staked = StakeAmount::new(0, staked_amount, first_era, period_1); assert_eq!( - acc_ledger.stakeable_amount(first_period), + acc_ledger.stakeable_amount(period_1), locked_amount - staked_amount, "Total stakeable amount should be equal to the locked amount minus what is already staked." ); // Third scenario - continuation of the previous, but we move to the next period. assert_eq!( - acc_ledger.stakeable_amount(first_period + 1), + acc_ledger.stakeable_amount(period_1 + 1), locked_amount, "Stakeable amount has to be equal to the locked amount since old period staking isn't valid anymore" ); } #[test] -fn account_ledger_staked_amount_works() { +fn account_ledger_staked_era_period_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); + + let (era_1, period) = (10, 2); + let stake_amount_1 = StakeAmount { + voting: 13, + build_and_earn: 17, + era: era_1, + period, + }; - // Sanity check for empty ledger - assert!(acc_ledger.staked_amount(1).is_zero()); + // Sanity check, empty ledger + assert!(acc_ledger.staked_period().is_none()); + assert!(acc_ledger.earliest_staked_era().is_none()); - // First scenario - active period matches the ledger - let first_era = 1; - let first_period = 1; - let locked_amount = 19; - let staked_amount = 13; - acc_ledger.add_lock_amount(locked_amount); - acc_ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { - amount: staked_amount, - era: first_era, - }]) - .expect("Only one chunk so creation should succeed."), - ); - acc_ledger.staked_period = Some(first_period); + // 1st scenario - only 'current' entry is set + acc_ledger.staked = stake_amount_1; + acc_ledger.staked_future = None; - assert_eq!(acc_ledger.staked_amount(first_period), staked_amount); + assert_eq!(acc_ledger.staked_period(), Some(period)); + assert_eq!(acc_ledger.earliest_staked_era(), Some(era_1)); - // Second scenario - active period doesn't match the ledger - assert!(acc_ledger.staked_amount(first_period + 1).is_zero()); + // 2nd scenario - only 'future' is set + let era_2 = era_1 + 7; + let stake_amount_2 = StakeAmount { + voting: 13, + build_and_earn: 17, + era: era_2, + period, + }; + acc_ledger.staked = Default::default(); + acc_ledger.staked_future = Some(stake_amount_2); + + assert_eq!(acc_ledger.staked_period(), Some(period)); + assert_eq!(acc_ledger.earliest_staked_era(), Some(era_2)); + + // 3rd scenario - both entries are set + acc_ledger.staked = stake_amount_1; + acc_ledger.staked_future = Some(stake_amount_2); + + assert_eq!(acc_ledger.staked_period(), Some(period)); + assert_eq!(acc_ledger.earliest_staked_era(), Some(era_1)); } #[test] -fn account_ledger_add_stake_amount_works() { +fn account_ledger_add_stake_amount_basic_example_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check - assert!(acc_ledger.add_stake_amount(0, 0, 0).is_ok()); - assert!(acc_ledger.staked_period.is_none()); - assert!(acc_ledger.staked.0.is_empty()); + let period_number = 2; + assert!(acc_ledger + .add_stake_amount( + 0, + 0, + PeriodInfo { + number: period_number, + subperiod: Subperiod::Voting, + subperiod_end_era: 0 + } + ) + .is_ok()); + assert!(acc_ledger.staked.is_empty()); + assert!(acc_ledger.staked_future.is_none()); - // First scenario - stake some amount, and ensure values are as expected - let first_era = 2; - let first_period = 1; + // 1st scenario - stake some amount in Voting period, and ensure values are as expected. + let first_era = 1; + let period_1 = 1; + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::Voting, + subperiod_end_era: 100, + }; let lock_amount = 17; let stake_amount = 11; acc_ledger.add_lock_amount(lock_amount); assert!(acc_ledger - .add_stake_amount(stake_amount, first_era, first_period) + .add_stake_amount(stake_amount, first_era, period_info_1) .is_ok()); - assert_eq!(acc_ledger.staked_period, Some(first_period)); - assert_eq!(acc_ledger.staked.0.len(), 1); + + assert!( + acc_ledger.staked.is_empty(), + "Current era must remain unchanged." + ); assert_eq!( - acc_ledger.staked.0[0], - StakeChunk { - amount: stake_amount, - era: first_era, - } + acc_ledger + .staked_future + .expect("Must exist after stake.") + .period, + period_1 ); - assert_eq!(acc_ledger.staked_amount(first_period), stake_amount); + assert_eq!(acc_ledger.staked_future.unwrap().voting, stake_amount); + assert!(acc_ledger.staked_future.unwrap().build_and_earn.is_zero()); + assert_eq!(acc_ledger.staked_amount(period_1), stake_amount); + assert_eq!( + acc_ledger.staked_amount_for_type(Subperiod::Voting, period_1), + stake_amount + ); + assert!(acc_ledger + .staked_amount_for_type(Subperiod::BuildAndEarn, period_1) + .is_zero()); - // Second scenario - stake some more to the same era, only amount should change + // Second scenario - stake some more, but to the next period type + let snapshot = acc_ledger.staked; + let period_info_2 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + subperiod_end_era: 100, + }; assert!(acc_ledger - .add_stake_amount(1, first_era, first_period) + .add_stake_amount(1, first_era, period_info_2) .is_ok()); - assert_eq!(acc_ledger.staked.0.len(), 1); - assert_eq!(acc_ledger.staked_amount(first_period), stake_amount + 1); + assert_eq!(acc_ledger.staked_amount(period_1), stake_amount + 1); + assert_eq!( + acc_ledger.staked_amount_for_type(Subperiod::Voting, period_1), + stake_amount + ); + assert_eq!( + acc_ledger.staked_amount_for_type(Subperiod::BuildAndEarn, period_1), + 1 + ); + assert_eq!(acc_ledger.staked, snapshot); +} + +#[test] +fn account_ledger_add_stake_amount_advanced_example_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // 1st scenario - stake some amount, and ensure values are as expected. + let first_era = 1; + let period_1 = 1; + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::Voting, + subperiod_end_era: 100, + }; + let lock_amount = 17; + let stake_amount_1 = 11; + acc_ledger.add_lock_amount(lock_amount); + + // We only have entry for the current era + acc_ledger.staked = StakeAmount::new(stake_amount_1, 0, first_era, period_1); - // Third scenario - stake to the next era, new chunk should be added - let next_era = first_era + 3; - let remaining_not_staked = lock_amount - stake_amount - 1; + let stake_amount_2 = 2; + let acc_ledger_snapshot = acc_ledger.clone(); assert!(acc_ledger - .add_stake_amount(remaining_not_staked, next_era, first_period) + .add_stake_amount(stake_amount_2, first_era, period_info_1) .is_ok()); - assert_eq!(acc_ledger.staked.0.len(), 2); - assert_eq!(acc_ledger.staked_amount(first_period), lock_amount); + assert_eq!( + acc_ledger.staked_amount(period_1), + stake_amount_1 + stake_amount_2 + ); + assert_eq!( + acc_ledger.staked, acc_ledger_snapshot.staked, + "This entry must remain unchanged." + ); + assert_eq!( + acc_ledger.staked_amount_for_type(Subperiod::Voting, period_1), + stake_amount_1 + stake_amount_2 + ); + assert_eq!( + acc_ledger + .staked_future + .unwrap() + .for_type(Subperiod::Voting), + stake_amount_1 + stake_amount_2 + ); + assert_eq!(acc_ledger.staked_future.unwrap().era, first_era + 1); } #[test] -fn account_ledger_add_stake_amount_invalid_era_fails() { +fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let first_era = 5; - let first_period = 2; + let period_1 = 2; + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::Voting, + subperiod_end_era: 100, + }; let lock_amount = 13; let stake_amount = 7; acc_ledger.add_lock_amount(lock_amount); assert!(acc_ledger - .add_stake_amount(stake_amount, first_era, first_period) + .add_stake_amount(stake_amount, first_era, period_info_1) .is_ok()); - let acc_ledger_snapshot = acc_ledger.clone(); - // Try to add to the next era, it should fail + // Try to add to the next era, it should fail. assert_eq!( - acc_ledger.add_stake_amount(1, first_era, first_period + 1), - Err(AccountLedgerError::InvalidPeriod) + acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), + Err(AccountLedgerError::InvalidEra) ); + + // Try to add to the next period, it should fail. assert_eq!( - acc_ledger, acc_ledger_snapshot, - "Previous failed action must be a noop" + acc_ledger.add_stake_amount( + 1, + first_era, + PeriodInfo { + number: period_1 + 1, + subperiod: Subperiod::Voting, + subperiod_end_era: 100 + } + ), + Err(AccountLedgerError::InvalidPeriod) ); - // Try to add to the previous era, it should fail + // Alternative situation - no future entry, only current era + acc_ledger.staked = StakeAmount::new(0, stake_amount, first_era, period_1); + acc_ledger.staked_future = None; + assert_eq!( - acc_ledger.add_stake_amount(1, first_era, first_period - 1), - Err(AccountLedgerError::InvalidPeriod) + acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), + Err(AccountLedgerError::InvalidEra) ); assert_eq!( - acc_ledger, acc_ledger_snapshot, - "Previous failed action must be a noop" + acc_ledger.add_stake_amount( + 1, + first_era, + PeriodInfo { + number: period_1 + 1, + subperiod: Subperiod::Voting, + subperiod_end_era: 100 + } + ), + Err(AccountLedgerError::InvalidPeriod) ); } #[test] fn account_ledger_add_stake_amount_too_large_amount_fails() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check assert_eq!( - acc_ledger.add_stake_amount(10, 1, 1), + acc_ledger.add_stake_amount( + 10, + 1, + PeriodInfo { + number: 1, + subperiod: Subperiod::Voting, + subperiod_end_era: 100 + } + ), Err(AccountLedgerError::UnavailableStakeFunds) ); // Lock some amount, and try to stake more than that let first_era = 5; - let first_period = 2; + let period_1 = 2; + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::Voting, + subperiod_end_era: 100, + }; let lock_amount = 13; acc_ledger.add_lock_amount(lock_amount); assert_eq!( - acc_ledger.add_stake_amount(lock_amount + 1, first_era, first_period), + acc_ledger.add_stake_amount(lock_amount + 1, first_era, period_info_1), Err(AccountLedgerError::UnavailableStakeFunds) ); // Additional check - have some active stake, and then try to overstake assert!(acc_ledger - .add_stake_amount(lock_amount - 2, first_era, first_period) + .add_stake_amount(lock_amount - 2, first_era, period_info_1) .is_ok()); assert_eq!( - acc_ledger.add_stake_amount(3, first_era, first_period), + acc_ledger.add_stake_amount(3, first_era, period_info_1), Err(AccountLedgerError::UnavailableStakeFunds) ); } #[test] -fn account_ledger_add_stake_amount_while_exceeding_capacity_fails() { +fn account_ledger_unstake_amount_basic_scenario_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); - // Try to stake up to the capacity, it should work - // Lock some amount, and try to stake more than that - let first_era = 5; - let first_period = 2; - let lock_amount = 31; - let stake_amount = 3; - acc_ledger.add_lock_amount(lock_amount); - for inc in 0..StakingDummy::get() { + // Prep actions + let amount_1 = 19; + let era_1 = 2; + let period_1 = 1; + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + subperiod_end_era: 100, + }; + acc_ledger.add_lock_amount(amount_1); + + let mut acc_ledger_2 = acc_ledger.clone(); + + // 'Current' staked entry will remain empty. + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_info_1) + .is_ok()); + + // Only 'current' entry has some values, future is set to None. + acc_ledger_2.staked = StakeAmount::new(0, amount_1, era_1, period_1); + acc_ledger_2.staked_future = None; + + for mut acc_ledger in vec![acc_ledger, acc_ledger_2] { + // Sanity check + assert!(acc_ledger.unstake_amount(0, era_1, period_info_1).is_ok()); + + // 1st scenario - unstake some amount from the current era. + let unstake_amount_1 = 3; assert!(acc_ledger - .add_stake_amount(stake_amount, first_era + inc, first_period) + .unstake_amount(unstake_amount_1, era_1, period_info_1) .is_ok()); assert_eq!( - acc_ledger.staked_amount(first_period), - stake_amount * (inc as u128 + 1) + acc_ledger.staked_amount(period_1), + amount_1 - unstake_amount_1 ); - } - // Can still stake to the last staked era - assert!(acc_ledger - .add_stake_amount( - stake_amount, - first_era + StakingDummy::get() - 1, - first_period - ) - .is_ok()); - - // But staking to the next era must fail with exceeded capacity - assert_eq!( - acc_ledger.add_stake_amount(stake_amount, first_era + StakingDummy::get(), first_period), - Err(AccountLedgerError::NoCapacity) - ); + // 2nd scenario - perform full unstake + assert!(acc_ledger + .unstake_amount(amount_1 - unstake_amount_1, era_1, period_info_1) + .is_ok()); + assert!(acc_ledger.staked_amount(period_1).is_zero()); + assert!(acc_ledger.staked.is_empty()); + assert_eq!(acc_ledger.staked, StakeAmount::default()); + assert!(acc_ledger.staked_future.is_none()); + } } - #[test] -fn account_ledger_unstake_amount_works() { +fn account_ledger_unstake_amount_advanced_scenario_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let amount_1 = 19; let era_1 = 2; let period_1 = 1; + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + subperiod_end_era: 100, + }; acc_ledger.add_lock_amount(amount_1); - assert!(acc_ledger - .add_stake_amount(amount_1, era_1, period_1) - .is_ok()); - // Sanity check - assert!(acc_ledger.unstake_amount(0, era_1, period_1).is_ok()); + // We have two entries at once + acc_ledger.staked = StakeAmount::new(amount_1 - 1, 0, era_1, period_1); + acc_ledger.staked_future = Some(StakeAmount::new(amount_1 - 1, 1, era_1 + 1, period_1)); - // 1st scenario - unstake some amount from the current era. + // 1st scenario - unstake some amount from the current era, both entries should be affected. let unstake_amount_1 = 3; assert!(acc_ledger - .unstake_amount(unstake_amount_1, era_1, period_1) + .unstake_amount(unstake_amount_1, era_1, period_info_1) .is_ok()); assert_eq!( acc_ledger.staked_amount(period_1), amount_1 - unstake_amount_1 ); - assert_eq!( - acc_ledger.staked.0.len(), - 1, - "Only existing entry should be updated." - ); - // 2nd scenario - unstake some more, but from the next era - let era_2 = era_1 + 1; - assert!(acc_ledger - .unstake_amount(unstake_amount_1, era_2, period_1) - .is_ok()); assert_eq!( - acc_ledger.staked_amount(period_1), - amount_1 - unstake_amount_1 * 2 + acc_ledger.staked.for_type(Subperiod::Voting), + amount_1 - 1 - 3 ); assert_eq!( - acc_ledger.staked.0.len(), - 2, - "New entry must be created to cover the new era stake." + acc_ledger + .staked_future + .unwrap() + .for_type(Subperiod::Voting), + amount_1 - 3 ); + assert!(acc_ledger + .staked_future + .unwrap() + .for_type(Subperiod::BuildAndEarn) + .is_zero()); - // 3rd scenario - unstake some more, bump era by a larger number - let era_3 = era_2 + 3; + // 2nd scenario - perform full unstake assert!(acc_ledger - .unstake_amount(unstake_amount_1, era_3, period_1) + .unstake_amount(amount_1 - unstake_amount_1, era_1, period_info_1) .is_ok()); + assert!(acc_ledger.staked_amount(period_1).is_zero()); + assert_eq!(acc_ledger.staked, StakeAmount::default()); + assert!(acc_ledger.staked_future.is_none()); + + // 3rd scenario - try to stake again, ensure it works + let era_2 = era_1 + 7; + let amount_2 = amount_1 - 5; + assert!(acc_ledger + .add_stake_amount(amount_2, era_2, period_info_1) + .is_ok()); + assert_eq!(acc_ledger.staked_amount(period_1), amount_2); + assert_eq!(acc_ledger.staked, StakeAmount::default()); assert_eq!( - acc_ledger.staked_amount(period_1), - amount_1 - unstake_amount_1 * 3 - ); - assert_eq!( - acc_ledger.staked.0.len(), - 3, - "New entry must be created to cover the new era stake." + acc_ledger + .staked_future + .unwrap() + .for_type(Subperiod::BuildAndEarn), + amount_2 ); } #[test] fn account_ledger_unstake_from_invalid_era_fails() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let amount_1 = 13; let era_1 = 2; let period_1 = 1; + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + subperiod_end_era: 100, + }; acc_ledger.add_lock_amount(amount_1); assert!(acc_ledger - .add_stake_amount(amount_1, era_1, period_1) + .add_stake_amount(amount_1, era_1, period_info_1) .is_ok()); + // Try to unstake from the next era, it should fail. assert_eq!( - acc_ledger.unstake_amount(amount_1, era_1 + 1, period_1 + 1), - Err(AccountLedgerError::InvalidPeriod) + acc_ledger.unstake_amount(1, era_1 + 1, period_info_1), + Err(AccountLedgerError::InvalidEra) ); -} -#[test] -fn account_ledger_unstake_too_much_fails() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + // Try to unstake from the next period, it should fail. + assert_eq!( + acc_ledger.unstake_amount( + 1, + era_1, + PeriodInfo { + number: period_1 + 1, + subperiod: Subperiod::Voting, + subperiod_end_era: 100 + } + ), + Err(AccountLedgerError::InvalidPeriod) + ); - // Prep actions - let amount_1 = 23; - let era_1 = 2; - let period_1 = 1; - acc_ledger.add_lock_amount(amount_1); - assert!(acc_ledger - .add_stake_amount(amount_1, era_1, period_1) - .is_ok()); + // Alternative situation - no future entry, only current era + acc_ledger.staked = StakeAmount::new(0, 1, era_1, period_1); + acc_ledger.staked_future = None; assert_eq!( - acc_ledger.unstake_amount(amount_1 + 1, era_1, period_1), - Err(AccountLedgerError::UnstakeAmountLargerThanStake) + acc_ledger.unstake_amount(1, era_1 + 1, period_info_1), + Err(AccountLedgerError::InvalidEra) + ); + assert_eq!( + acc_ledger.unstake_amount( + 1, + era_1, + PeriodInfo { + number: period_1 + 1, + subperiod: Subperiod::Voting, + subperiod_end_era: 100 + } + ), + Err(AccountLedgerError::InvalidPeriod) ); } #[test] -fn account_ledger_unstake_exceeds_capacity() { +fn account_ledger_unstake_too_much_fails() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions - let amount_1 = 100; + let amount_1 = 23; let era_1 = 2; let period_1 = 1; + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + subperiod_end_era: 100, + }; acc_ledger.add_lock_amount(amount_1); assert!(acc_ledger - .add_stake_amount(amount_1, era_1, period_1) + .add_stake_amount(amount_1, era_1, period_info_1) .is_ok()); - for x in 0..StakingDummy::get() { - assert!( - acc_ledger.unstake_amount(3, era_1 + x, period_1).is_ok(), - "Capacity isn't full so unstake must work." - ); - } - assert_eq!( - acc_ledger.unstake_amount(3, era_1 + StakingDummy::get(), period_1), - Err(AccountLedgerError::NoCapacity) + acc_ledger.unstake_amount(amount_1 + 1, era_1, period_info_1), + Err(AccountLedgerError::UnstakeAmountLargerThanStake) ); } #[test] fn account_ledger_unlockable_amount_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario assert!(acc_ledger.unlockable_amount(0).is_zero()); @@ -957,14 +931,14 @@ fn account_ledger_unlockable_amount_works() { // Some amount is staked, period matches let stake_period = 5; let stake_amount = 17; - acc_ledger.staked = SparseBoundedAmountEraVec( - BoundedVec::try_from(vec![StakeChunk { - amount: stake_amount, - era: lock_era, - }]) - .expect("Only one chunk so creation should succeed."), - ); - acc_ledger.staked_period = Some(stake_period); + let period_info = PeriodInfo { + number: stake_period, + subperiod: Subperiod::Voting, + subperiod_end_era: 100, + }; + assert!(acc_ledger + .add_stake_amount(stake_amount, lock_era, period_info) + .is_ok()); assert_eq!( acc_ledger.unlockable_amount(stake_period), lock_amount - stake_amount @@ -984,8 +958,7 @@ fn account_ledger_unlockable_amount_works() { #[test] fn account_ledger_claim_unlocked_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario assert!(acc_ledger.claim_unlocked(0).is_zero()); @@ -1019,8 +992,7 @@ fn account_ledger_claim_unlocked_works() { #[test] fn account_ledger_consume_unlocking_chunks_works() { get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario assert!(acc_ledger.consume_unlocking_chunks().is_zero()); @@ -1034,6 +1006,11 @@ fn account_ledger_consume_unlocking_chunks_works() { assert!(acc_ledger.unlocking.is_empty()); } +#[test] +fn account_ledger_claim_up_to_era_works() { + // TODO!!! +} + #[test] fn era_info_lock_unlock_works() { let mut era_info = EraInfo::default(); @@ -1096,10 +1073,10 @@ fn era_info_stake_works() { // Add some voting period stake let vp_stake_amount = 7; - era_info.add_stake_amount(vp_stake_amount, PeriodType::Voting); + era_info.add_stake_amount(vp_stake_amount, Subperiod::Voting); assert_eq!(era_info.total_staked_amount_next_era(), vp_stake_amount); assert_eq!( - era_info.staked_amount_next_era(PeriodType::Voting), + era_info.staked_amount_next_era(Subperiod::Voting), vp_stake_amount ); assert!( @@ -1109,13 +1086,13 @@ fn era_info_stake_works() { // Add some build&earn period stake let bep_stake_amount = 13; - era_info.add_stake_amount(bep_stake_amount, PeriodType::BuildAndEarn); + era_info.add_stake_amount(bep_stake_amount, Subperiod::BuildAndEarn); assert_eq!( era_info.total_staked_amount_next_era(), vp_stake_amount + bep_stake_amount ); assert_eq!( - era_info.staked_amount_next_era(PeriodType::BuildAndEarn), + era_info.staked_amount_next_era(Subperiod::BuildAndEarn), bep_stake_amount ); assert!( @@ -1132,22 +1109,26 @@ fn era_info_unstake_works() { let vp_stake_amount = 15; let bep_stake_amount_1 = 23; let bep_stake_amount_2 = bep_stake_amount_1 + 6; - era_info.current_stake_amount = StakeAmount::new(vp_stake_amount, bep_stake_amount_1); - era_info.next_stake_amount = StakeAmount::new(vp_stake_amount, bep_stake_amount_2); + let period_number = 1; + let era = 2; + era_info.current_stake_amount = + StakeAmount::new(vp_stake_amount, bep_stake_amount_1, era, period_number); + era_info.next_stake_amount = + StakeAmount::new(vp_stake_amount, bep_stake_amount_2, era + 1, period_number); let total_staked = era_info.total_staked_amount(); let total_staked_next_era = era_info.total_staked_amount_next_era(); // 1st scenario - unstake some amount, no overflow let unstake_amount_1 = bep_stake_amount_1; - era_info.unstake_amount(unstake_amount_1, PeriodType::BuildAndEarn); + era_info.unstake_amount(unstake_amount_1, Subperiod::BuildAndEarn); // Current era assert_eq!( era_info.total_staked_amount(), total_staked - unstake_amount_1 ); - assert_eq!(era_info.staked_amount(PeriodType::Voting), vp_stake_amount); - assert!(era_info.staked_amount(PeriodType::BuildAndEarn).is_zero()); + assert_eq!(era_info.staked_amount(Subperiod::Voting), vp_stake_amount); + assert!(era_info.staked_amount(Subperiod::BuildAndEarn).is_zero()); // Next era assert_eq!( @@ -1155,18 +1136,18 @@ fn era_info_unstake_works() { total_staked_next_era - unstake_amount_1 ); assert_eq!( - era_info.staked_amount_next_era(PeriodType::Voting), + era_info.staked_amount_next_era(Subperiod::Voting), vp_stake_amount ); assert_eq!( - era_info.staked_amount_next_era(PeriodType::BuildAndEarn), + era_info.staked_amount_next_era(Subperiod::BuildAndEarn), bep_stake_amount_2 - unstake_amount_1 ); // 2nd scenario - unstake some more, but with overflow let overflow = 2; let unstake_amount_2 = bep_stake_amount_2 - unstake_amount_1 + overflow; - era_info.unstake_amount(unstake_amount_2, PeriodType::BuildAndEarn); + era_info.unstake_amount(unstake_amount_2, Subperiod::BuildAndEarn); // Current era assert_eq!( @@ -1180,11 +1161,11 @@ fn era_info_unstake_works() { vp_stake_amount - overflow ); assert_eq!( - era_info.staked_amount_next_era(PeriodType::Voting), + era_info.staked_amount_next_era(Subperiod::Voting), vp_stake_amount - overflow ); assert!(era_info - .staked_amount_next_era(PeriodType::BuildAndEarn) + .staked_amount_next_era(Subperiod::BuildAndEarn) .is_zero()); } @@ -1194,101 +1175,101 @@ fn stake_amount_works() { // Sanity check assert!(stake_amount.total().is_zero()); - assert!(stake_amount.for_type(PeriodType::Voting).is_zero()); - assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + assert!(stake_amount.for_type(Subperiod::Voting).is_zero()); + assert!(stake_amount.for_type(Subperiod::BuildAndEarn).is_zero()); // Stake some amount in voting period let vp_stake_1 = 11; - stake_amount.stake(vp_stake_1, PeriodType::Voting); + stake_amount.add(vp_stake_1, Subperiod::Voting); assert_eq!(stake_amount.total(), vp_stake_1); - assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); - assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + assert_eq!(stake_amount.for_type(Subperiod::Voting), vp_stake_1); + assert!(stake_amount.for_type(Subperiod::BuildAndEarn).is_zero()); // Stake some amount in build&earn period let bep_stake_1 = 13; - stake_amount.stake(bep_stake_1, PeriodType::BuildAndEarn); + stake_amount.add(bep_stake_1, Subperiod::BuildAndEarn); assert_eq!(stake_amount.total(), vp_stake_1 + bep_stake_1); - assert_eq!(stake_amount.for_type(PeriodType::Voting), vp_stake_1); - assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); + assert_eq!(stake_amount.for_type(Subperiod::Voting), vp_stake_1); + assert_eq!(stake_amount.for_type(Subperiod::BuildAndEarn), bep_stake_1); // Unstake some amount from voting period let vp_unstake_1 = 5; - stake_amount.unstake(5, PeriodType::Voting); + stake_amount.subtract(5, Subperiod::Voting); assert_eq!( stake_amount.total(), vp_stake_1 + bep_stake_1 - vp_unstake_1 ); assert_eq!( - stake_amount.for_type(PeriodType::Voting), + stake_amount.for_type(Subperiod::Voting), vp_stake_1 - vp_unstake_1 ); - assert_eq!(stake_amount.for_type(PeriodType::BuildAndEarn), bep_stake_1); + assert_eq!(stake_amount.for_type(Subperiod::BuildAndEarn), bep_stake_1); // Unstake some amount from build&earn period let bep_unstake_1 = 2; - stake_amount.unstake(bep_unstake_1, PeriodType::BuildAndEarn); + stake_amount.subtract(bep_unstake_1, Subperiod::BuildAndEarn); assert_eq!( stake_amount.total(), vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1 ); assert_eq!( - stake_amount.for_type(PeriodType::Voting), + stake_amount.for_type(Subperiod::Voting), vp_stake_1 - vp_unstake_1 ); assert_eq!( - stake_amount.for_type(PeriodType::BuildAndEarn), + stake_amount.for_type(Subperiod::BuildAndEarn), bep_stake_1 - bep_unstake_1 ); // Unstake some more from build&earn period, and chip away from the voting period let total_stake = vp_stake_1 + bep_stake_1 - vp_unstake_1 - bep_unstake_1; let bep_unstake_2 = bep_stake_1 - bep_unstake_1 + 1; - stake_amount.unstake(bep_unstake_2, PeriodType::BuildAndEarn); + stake_amount.subtract(bep_unstake_2, Subperiod::BuildAndEarn); assert_eq!(stake_amount.total(), total_stake - bep_unstake_2); assert_eq!( - stake_amount.for_type(PeriodType::Voting), + stake_amount.for_type(Subperiod::Voting), vp_stake_1 - vp_unstake_1 - 1 ); - assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); + assert!(stake_amount.for_type(Subperiod::BuildAndEarn).is_zero()); } #[test] fn singular_staking_info_basics_are_ok() { let period_number = 3; - let period_type = PeriodType::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, period_type); + let subperiod = Subperiod::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, subperiod); // Sanity checks assert_eq!(staking_info.period_number(), period_number); assert!(staking_info.is_loyal()); assert!(staking_info.total_staked_amount().is_zero()); - assert!(!SingularStakingInfo::new(period_number, PeriodType::BuildAndEarn).is_loyal()); + assert!(!SingularStakingInfo::new(period_number, Subperiod::BuildAndEarn).is_loyal()); // Add some staked amount during `Voting` period let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + staking_info.stake(vote_stake_amount_1, Subperiod::Voting); assert_eq!(staking_info.total_staked_amount(), vote_stake_amount_1); assert_eq!( - staking_info.staked_amount(PeriodType::Voting), + staking_info.staked_amount(Subperiod::Voting), vote_stake_amount_1 ); assert!(staking_info - .staked_amount(PeriodType::BuildAndEarn) + .staked_amount(Subperiod::BuildAndEarn) .is_zero()); // Add some staked amount during `BuildAndEarn` period let bep_stake_amount_1 = 23; - staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); + staking_info.stake(bep_stake_amount_1, Subperiod::BuildAndEarn); assert_eq!( staking_info.total_staked_amount(), vote_stake_amount_1 + bep_stake_amount_1 ); assert_eq!( - staking_info.staked_amount(PeriodType::Voting), + staking_info.staked_amount(Subperiod::Voting), vote_stake_amount_1 ); assert_eq!( - staking_info.staked_amount(PeriodType::BuildAndEarn), + staking_info.staked_amount(Subperiod::BuildAndEarn), bep_stake_amount_1 ); } @@ -1296,17 +1277,17 @@ fn singular_staking_info_basics_are_ok() { #[test] fn singular_staking_info_unstake_during_voting_is_ok() { let period_number = 3; - let period_type = PeriodType::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, period_type); + let subperiod = Subperiod::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, subperiod); // Prep actions let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + staking_info.stake(vote_stake_amount_1, Subperiod::Voting); // Unstake some amount during `Voting` period, loyalty should remain as expected. let unstake_amount_1 = 5; assert_eq!( - staking_info.unstake(unstake_amount_1, PeriodType::Voting), + staking_info.unstake(unstake_amount_1, Subperiod::Voting), (unstake_amount_1, Balance::zero()) ); assert_eq!( @@ -1318,7 +1299,7 @@ fn singular_staking_info_unstake_during_voting_is_ok() { // Fully unstake, attempting to undersaturate, and ensure loyalty flag is still true. let remaining_stake = staking_info.total_staked_amount(); assert_eq!( - staking_info.unstake(remaining_stake + 1, PeriodType::Voting), + staking_info.unstake(remaining_stake + 1, Subperiod::Voting), (remaining_stake, Balance::zero()) ); assert!(staking_info.total_staked_amount().is_zero()); @@ -1328,19 +1309,19 @@ fn singular_staking_info_unstake_during_voting_is_ok() { #[test] fn singular_staking_info_unstake_during_bep_is_ok() { let period_number = 3; - let period_type = PeriodType::Voting; - let mut staking_info = SingularStakingInfo::new(period_number, period_type); + let subperiod = Subperiod::Voting; + let mut staking_info = SingularStakingInfo::new(period_number, subperiod); // Prep actions let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, PeriodType::Voting); + staking_info.stake(vote_stake_amount_1, Subperiod::Voting); let bep_stake_amount_1 = 23; - staking_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); + staking_info.stake(bep_stake_amount_1, Subperiod::BuildAndEarn); // 1st scenario - Unstake some of the amount staked during B&E period let unstake_1 = 5; assert_eq!( - staking_info.unstake(5, PeriodType::BuildAndEarn), + staking_info.unstake(5, Subperiod::BuildAndEarn), (Balance::zero(), unstake_1) ); assert_eq!( @@ -1348,11 +1329,11 @@ fn singular_staking_info_unstake_during_bep_is_ok() { vote_stake_amount_1 + bep_stake_amount_1 - unstake_1 ); assert_eq!( - staking_info.staked_amount(PeriodType::Voting), + staking_info.staked_amount(Subperiod::Voting), vote_stake_amount_1 ); assert_eq!( - staking_info.staked_amount(PeriodType::BuildAndEarn), + staking_info.staked_amount(Subperiod::BuildAndEarn), bep_stake_amount_1 - unstake_1 ); assert!(staking_info.is_loyal()); @@ -1360,12 +1341,12 @@ fn singular_staking_info_unstake_during_bep_is_ok() { // 2nd scenario - unstake all of the amount staked during B&E period, and then some more. // The point is to take a chunk from the voting period stake too. let current_total_stake = staking_info.total_staked_amount(); - let current_bep_stake = staking_info.staked_amount(PeriodType::BuildAndEarn); + let current_bep_stake = staking_info.staked_amount(Subperiod::BuildAndEarn); let voting_stake_overflow = 2; let unstake_2 = current_bep_stake + voting_stake_overflow; assert_eq!( - staking_info.unstake(unstake_2, PeriodType::BuildAndEarn), + staking_info.unstake(unstake_2, Subperiod::BuildAndEarn), (voting_stake_overflow, current_bep_stake) ); assert_eq!( @@ -1373,11 +1354,11 @@ fn singular_staking_info_unstake_during_bep_is_ok() { current_total_stake - unstake_2 ); assert_eq!( - staking_info.staked_amount(PeriodType::Voting), + staking_info.staked_amount(Subperiod::Voting), vote_stake_amount_1 - voting_stake_overflow ); assert!(staking_info - .staked_amount(PeriodType::BuildAndEarn) + .staked_amount(Subperiod::BuildAndEarn) .is_zero()); assert!( !staking_info.is_loyal(), @@ -1386,391 +1367,311 @@ fn singular_staking_info_unstake_during_bep_is_ok() { } #[test] -fn contract_stake_info_is_ok() { - let period = 2; - let era = 3; - let mut contract_stake_info = ContractStakingInfo::new(era, period); - - // Sanity check - assert_eq!(contract_stake_info.period(), period); - assert_eq!(contract_stake_info.era(), era); - assert!(contract_stake_info.total_staked_amount().is_zero()); - assert!(contract_stake_info.is_empty()); - - // 1st scenario - Add some staked amount to the voting period - let vote_stake_amount_1 = 11; - contract_stake_info.stake(vote_stake_amount_1, PeriodType::Voting); - assert_eq!( - contract_stake_info.total_staked_amount(), - vote_stake_amount_1 - ); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - ); - assert!(!contract_stake_info.is_empty()); - - // 2nd scenario - add some staked amount to the B&E period - let bep_stake_amount_1 = 23; - contract_stake_info.stake(bep_stake_amount_1, PeriodType::BuildAndEarn); - assert_eq!( - contract_stake_info.total_staked_amount(), - vote_stake_amount_1 + bep_stake_amount_1 - ); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - ); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::BuildAndEarn), - bep_stake_amount_1 - ); - - // 3rd scenario - reduce some of the staked amount from both periods and verify it's as expected. - let total_staked = contract_stake_info.total_staked_amount(); - let vp_reduction = 3; - contract_stake_info.unstake(vp_reduction, PeriodType::Voting); - assert_eq!( - contract_stake_info.total_staked_amount(), - total_staked - vp_reduction - ); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - vp_reduction - ); - - let bp_reduction = 7; - contract_stake_info.unstake(bp_reduction, PeriodType::BuildAndEarn); - assert_eq!( - contract_stake_info.total_staked_amount(), - total_staked - vp_reduction - bp_reduction - ); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::BuildAndEarn), - bep_stake_amount_1 - bp_reduction - ); - - // 4th scenario - unstake everything, and some more, from Build&Earn period, chiping away from the voting period. - let overflow = 1; - let overflow_reduction = contract_stake_info.staked_amount(PeriodType::BuildAndEarn) + overflow; - contract_stake_info.unstake(overflow_reduction, PeriodType::BuildAndEarn); - assert_eq!( - contract_stake_info.total_staked_amount(), - vote_stake_amount_1 - vp_reduction - overflow - ); - assert!(contract_stake_info - .staked_amount(PeriodType::BuildAndEarn) - .is_zero()); - assert_eq!( - contract_stake_info.staked_amount(PeriodType::Voting), - vote_stake_amount_1 - vp_reduction - overflow - ); -} - -#[test] -fn contract_staking_info_series_get_works() { - let info_1 = ContractStakingInfo::new(4, 2); - let mut info_2 = ContractStakingInfo::new(7, 3); - info_2.stake(11, PeriodType::Voting); - let mut info_3 = ContractStakingInfo::new(9, 3); - info_3.stake(13, PeriodType::BuildAndEarn); +fn contract_stake_info_get_works() { + let info_1 = StakeAmount::new(0, 0, 4, 2); + let info_2 = StakeAmount::new(11, 0, 7, 3); - let series = ContractStakingInfoSeries::new(vec![info_1, info_2, info_3]); + let contract_stake = ContractStakeAmount { + staked: info_1, + staked_future: Some(info_2), + }; // Sanity check - assert_eq!(series.len(), 3); - assert!(!series.is_empty()); + assert!(!contract_stake.is_empty()); // 1st scenario - get existing entries - assert_eq!(series.get(4, 2), Some(info_1)); - assert_eq!(series.get(7, 3), Some(info_2)); - assert_eq!(series.get(9, 3), Some(info_3)); + assert_eq!(contract_stake.get(4, 2), Some(info_1)); + assert_eq!(contract_stake.get(7, 3), Some(info_2)); // 2nd scenario - get non-existing entries for covered eras { let era_1 = 6; - let entry_1 = series.get(era_1, 2).expect("Has to be Some"); - assert!(entry_1.total_staked_amount().is_zero()); - assert_eq!(entry_1.era(), era_1); - assert_eq!(entry_1.period(), 2); + let entry_1 = contract_stake.get(era_1, 2).expect("Has to be Some"); + assert!(entry_1.total().is_zero()); + assert_eq!(entry_1.era, era_1); + assert_eq!(entry_1.period, 2); let era_2 = 8; - let entry_1 = series.get(era_2, 3).expect("Has to be Some"); - assert_eq!(entry_1.total_staked_amount(), 11); - assert_eq!(entry_1.era(), era_2); - assert_eq!(entry_1.period(), 3); + let entry_1 = contract_stake.get(era_2, 3).expect("Has to be Some"); + assert_eq!(entry_1.total(), 11); + assert_eq!(entry_1.era, era_2); + assert_eq!(entry_1.period, 3); } // 3rd scenario - get non-existing entries for covered eras but mismatching period - assert!(series.get(8, 2).is_none()); + assert!(contract_stake.get(8, 2).is_none()); // 4th scenario - get non-existing entries for non-covered eras - assert!(series.get(3, 2).is_none()); + assert!(contract_stake.get(3, 2).is_none()); } #[test] -fn contract_staking_info_series_stake_is_ok() { - let mut series = ContractStakingInfoSeries::default(); - - // Sanity check - assert!(series.is_empty()); - assert!(series.len().is_zero()); +fn contract_stake_info_stake_is_ok() { + let mut contract_stake = ContractStakeAmount::default(); // 1st scenario - stake some amount and verify state change let era_1 = 3; + let stake_era_1 = era_1 + 1; let period_1 = 5; - let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 20); + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::Voting, + subperiod_end_era: 20, + }; let amount_1 = 31; - assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); - - assert_eq!(series.len(), 1); - assert!(!series.is_empty()); - - let entry_1_1 = series.get(era_1, period_1).unwrap(); - assert_eq!(entry_1_1.era(), era_1); - assert_eq!(entry_1_1.total_staked_amount(), amount_1); + contract_stake.stake(amount_1, period_info_1, era_1); + assert!(!contract_stake.is_empty()); - // 2nd scenario - stake some more to the same era but different period type, and verify state change. - let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 20); - assert!(series.stake(amount_1, period_info_1, era_1).is_ok()); + assert!( + contract_stake.get(era_1, period_1).is_none(), + "Entry for current era must not exist." + ); + let entry_1_1 = contract_stake.get(stake_era_1, period_1).unwrap(); assert_eq!( - series.len(), - 1, - "No new entry should be created since it's the same era." + entry_1_1.era, stake_era_1, + "Stake is only valid from next era." ); - let entry_1_2 = series.get(era_1, period_1).unwrap(); - assert_eq!(entry_1_2.era(), era_1); - assert_eq!(entry_1_2.total_staked_amount(), amount_1 * 2); + assert_eq!(entry_1_1.total(), amount_1); + + // 2nd scenario - stake some more to the same era but different period type, and verify state change. + let period_info_1 = PeriodInfo { + number: period_1, + subperiod: Subperiod::BuildAndEarn, + subperiod_end_era: 20, + }; + contract_stake.stake(amount_1, period_info_1, era_1); + let entry_1_2 = contract_stake.get(stake_era_1, period_1).unwrap(); + assert_eq!(entry_1_2.era, stake_era_1); + assert_eq!(entry_1_2.total(), amount_1 * 2); // 3rd scenario - stake more to the next era, while still in the same period. let era_2 = era_1 + 2; + let stake_era_2 = era_2 + 1; let amount_2 = 37; - assert!(series.stake(amount_2, period_info_1, era_2).is_ok()); - assert_eq!(series.len(), 2); - let entry_2_1 = series.get(era_1, period_1).unwrap(); - let entry_2_2 = series.get(era_2, period_1).unwrap(); + contract_stake.stake(amount_2, period_info_1, era_2); + let entry_2_1 = contract_stake.get(stake_era_1, period_1).unwrap(); + let entry_2_2 = contract_stake.get(stake_era_2, period_1).unwrap(); assert_eq!(entry_2_1, entry_1_2, "Old entry must remain unchanged."); - assert_eq!(entry_2_2.era(), era_2); - assert_eq!(entry_2_2.period(), period_1); + assert_eq!(entry_2_2.era, stake_era_2); + assert_eq!(entry_2_2.period, period_1); assert_eq!( - entry_2_2.total_staked_amount(), - entry_2_1.total_staked_amount() + amount_2, + entry_2_2.total(), + entry_2_1.total() + amount_2, "Since it's the same period, stake amount must carry over from the previous entry." ); // 4th scenario - stake some more to the next era, but this time also bump the period. let era_3 = era_2 + 3; + let stake_era_3 = era_3 + 1; let period_2 = period_1 + 1; - let period_info_2 = PeriodInfo::new(period_2, PeriodType::BuildAndEarn, 20); + let period_info_2 = PeriodInfo { + number: period_2, + subperiod: Subperiod::BuildAndEarn, + subperiod_end_era: 20, + }; let amount_3 = 41; - assert!(series.stake(amount_3, period_info_2, era_3).is_ok()); - assert_eq!(series.len(), 3); - let entry_3_1 = series.get(era_1, period_1).unwrap(); - let entry_3_2 = series.get(era_2, period_1).unwrap(); - let entry_3_3 = series.get(era_3, period_2).unwrap(); - assert_eq!(entry_3_1, entry_2_1, "Old entry must remain unchanged."); - assert_eq!(entry_3_2, entry_2_2, "Old entry must remain unchanged."); - assert_eq!(entry_3_3.era(), era_3); - assert_eq!(entry_3_3.period(), period_2); - assert_eq!( - entry_3_3.total_staked_amount(), + contract_stake.stake(amount_3, period_info_2, era_3); + assert!( + contract_stake.get(stake_era_1, period_1).is_none(), + "Old period must be removed." + ); + assert!( + contract_stake.get(stake_era_2, period_1).is_none(), + "Old period must be removed." + ); + let entry_3_1 = contract_stake.get(stake_era_3, period_2).unwrap(); + assert_eq!(entry_3_1.era, stake_era_3); + assert_eq!(entry_3_1.period, period_2); + assert_eq!( + entry_3_1.total(), amount_3, "No carry over from previous entry since period has changed." ); - // 5th scenario - stake to the next era, expect cleanup of oldest entry + // 5th scenario - stake to the next era let era_4 = era_3 + 1; + let stake_era_4 = era_4 + 1; let amount_4 = 5; - assert!(series.stake(amount_4, period_info_2, era_4).is_ok()); - assert_eq!(series.len(), 3); - let entry_4_1 = series.get(era_2, period_1).unwrap(); - let entry_4_2 = series.get(era_3, period_2).unwrap(); - let entry_4_3 = series.get(era_4, period_2).unwrap(); - assert_eq!(entry_4_1, entry_3_2, "Old entry must remain unchanged."); - assert_eq!(entry_4_2, entry_3_3, "Old entry must remain unchanged."); - assert_eq!(entry_4_3.era(), era_4); - assert_eq!(entry_4_3.period(), period_2); - assert_eq!(entry_4_3.total_staked_amount(), amount_3 + amount_4); + contract_stake.stake(amount_4, period_info_2, era_4); + let entry_4_1 = contract_stake.get(stake_era_3, period_2).unwrap(); + let entry_4_2 = contract_stake.get(stake_era_4, period_2).unwrap(); + assert_eq!(entry_4_1, entry_3_1, "Old entry must remain unchanged."); + assert_eq!(entry_4_2.era, stake_era_4); + assert_eq!(entry_4_2.period, period_2); + assert_eq!(entry_4_2.total(), amount_3 + amount_4); } #[test] -fn contract_staking_info_series_stake_with_inconsistent_data_fails() { - let mut series = ContractStakingInfoSeries::default(); - - // Create an entry with some staked amount - let era = 5; - let period_info = PeriodInfo { - number: 7, - period_type: PeriodType::Voting, - ending_era: 31, - }; - let amount = 37; - assert!(series.stake(amount, period_info, era).is_ok()); - - // 1st scenario - attempt to stake using old era - assert!(series.stake(amount, period_info, era - 1).is_err()); - - // 2nd scenario - attempt to stake using old period - let period_info = PeriodInfo { - number: period_info.number - 1, - period_type: PeriodType::Voting, - ending_era: 31, - }; - assert!(series.stake(amount, period_info, era).is_err()); -} - -#[test] -fn contract_staking_info_series_unstake_is_ok() { - let mut series = ContractStakingInfoSeries::default(); +fn contract_stake_info_unstake_is_ok() { + let mut contract_stake = ContractStakeAmount::default(); // Prep action - create a stake entry let era_1 = 2; let period = 3; - let period_info = PeriodInfo::new(period, PeriodType::Voting, 20); + let period_info = PeriodInfo { + number: period, + subperiod: Subperiod::Voting, + subperiod_end_era: 20, + }; let stake_amount = 100; - assert!(series.stake(stake_amount, period_info, era_1).is_ok()); + contract_stake.stake(stake_amount, period_info, era_1); // 1st scenario - unstake in the same era let amount_1 = 5; - assert!(series.unstake(amount_1, period_info, era_1).is_ok()); - assert_eq!(series.len(), 1); - assert_eq!(series.total_staked_amount(period), stake_amount - amount_1); + contract_stake.unstake(amount_1, period_info, era_1); + assert_eq!( + contract_stake.total_staked_amount(period), + stake_amount - amount_1 + ); assert_eq!( - series.staked_amount(period, PeriodType::Voting), + contract_stake.staked_amount(period, Subperiod::Voting), stake_amount - amount_1 ); - // 2nd scenario - unstake in the future era, creating a 'gap' in the series - // [(era: 2)] ---> [(era: 2), (era: 5)] - let period_info = PeriodInfo::new(period, PeriodType::BuildAndEarn, 40); + // 2nd scenario - unstake in the future era, entries should be aligned to the current era + let period_info = PeriodInfo { + number: period, + subperiod: Subperiod::BuildAndEarn, + subperiod_end_era: 40, + }; let era_2 = era_1 + 3; let amount_2 = 7; - assert!(series.unstake(amount_2, period_info, era_2).is_ok()); - assert_eq!(series.len(), 2); + contract_stake.unstake(amount_2, period_info, era_2); assert_eq!( - series.total_staked_amount(period), + contract_stake.total_staked_amount(period), stake_amount - amount_1 - amount_2 ); assert_eq!( - series.staked_amount(period, PeriodType::Voting), + contract_stake.staked_amount(period, Subperiod::Voting), stake_amount - amount_1 - amount_2 ); +} - // 3rd scenario - unstake in the era right before the last, inserting the new value in-between the old ones - // [(era: 2), (era: 5)] ---> [(era: 2), (era: 4), (era: 5)] - let era_3 = era_2 - 1; - let amount_3 = 11; - assert!(series.unstake(amount_3, period_info, era_3).is_ok()); - assert_eq!(series.len(), 3); - assert_eq!( - series.total_staked_amount(period), - stake_amount - amount_1 - amount_2 - amount_3 - ); - assert_eq!( - series.staked_amount(period, PeriodType::Voting), - stake_amount - amount_1 - amount_2 - amount_3 - ); +#[test] +fn era_reward_span_push_and_get_works() { + get_u32_type!(SpanLength, 8); + let mut era_reward_span = EraRewardSpan::::new(); - // Check concrete entries - assert_eq!( - series.get(era_1, period).unwrap().total_staked_amount(), - stake_amount - amount_1, - "Oldest entry must remain unchanged." - ); - assert_eq!( - series.get(era_2, period).unwrap().total_staked_amount(), - stake_amount - amount_1 - amount_2 - amount_3, - "Future era entry must be updated with all of the reductions." - ); - assert_eq!( - series.get(era_3, period).unwrap().total_staked_amount(), - stake_amount - amount_1 - amount_3, - "Second to last era entry must be updated with first & last reduction\ - because it derives its initial value from the oldest entry." - ); + // Sanity checks + assert!(era_reward_span.is_empty()); + assert!(era_reward_span.len().is_zero()); + assert!(era_reward_span.first_era().is_zero()); + assert!(era_reward_span.last_era().is_zero()); + + // Insert some values and verify state change + let era_1 = 5; + let era_reward_1 = EraReward { + staker_reward_pool: 23, + staked: 41, + dapp_reward_pool: 17, + }; + assert!(era_reward_span.push(era_1, era_reward_1).is_ok()); + assert_eq!(era_reward_span.len(), 1); + assert_eq!(era_reward_span.first_era(), era_1); + assert_eq!(era_reward_span.last_era(), era_1); + + // Insert another value and verify state change + let era_2 = era_1 + 1; + let era_reward_2 = EraReward { + staker_reward_pool: 37, + staked: 53, + dapp_reward_pool: 19, + }; + assert!(era_reward_span.push(era_2, era_reward_2).is_ok()); + assert_eq!(era_reward_span.len(), 2); + assert_eq!(era_reward_span.first_era(), era_1); + assert_eq!(era_reward_span.last_era(), era_2); + + // Get the values and verify they are as expected + assert_eq!(era_reward_span.get(era_1), Some(&era_reward_1)); + assert_eq!(era_reward_span.get(era_2), Some(&era_reward_2)); } #[test] -fn contract_staking_info_unstake_with_worst_case_scenario_for_capacity_overflow() { - let (era_1, era_2, era_3) = (4, 7, 9); - let (period_1, period_2) = (2, 3); - let info_1 = ContractStakingInfo::new(era_1, period_1); - let mut info_2 = ContractStakingInfo::new(era_2, period_2); - let stake_amount_2 = 11; - info_2.stake(stake_amount_2, PeriodType::Voting); - let mut info_3 = ContractStakingInfo::new(era_3, period_2); - let stake_amount_3 = 13; - info_3.stake(stake_amount_3, PeriodType::BuildAndEarn); - - // A gap between 2nd and 3rd era, and from that gap unstake will be done. - // This will force a new entry to be created, potentially overflowing the vector capacity. - let mut series = ContractStakingInfoSeries::new(vec![info_1, info_2, info_3]); - - // Unstake between era 2 & 3, in attempt to overflow the inner vector capacity - let period_info = PeriodInfo { - number: period_2, - period_type: PeriodType::BuildAndEarn, - ending_era: 51, +fn era_reward_span_fails_when_expected() { + // Capacity is only 2 to make testing easier + get_u32_type!(SpanLength, 2); + let mut era_reward_span = EraRewardSpan::::new(); + + // Push first values to get started + let era_1 = 5; + let era_reward = EraReward { + staker_reward_pool: 23, + staked: 41, + dapp_reward_pool: 17, }; - let unstake_amount = 3; - assert!(series.unstake(3, period_info, era_2 + 1).is_ok()); - assert_eq!(series.len(), 3); + assert!(era_reward_span.push(era_1, era_reward).is_ok()); + // Attempting to push incorrect era results in an error + for wrong_era in &[era_1 - 1, era_1, era_1 + 2] { + assert_eq!( + era_reward_span.push(*wrong_era, era_reward), + Err(EraRewardSpanError::InvalidEra) + ); + } + + // Pushing above capacity results in an error + let era_2 = era_1 + 1; + assert!(era_reward_span.push(era_2, era_reward).is_ok()); + let era_3 = era_2 + 1; assert_eq!( - series.get(era_1, period_1), - None, - "Oldest entry should have been prunned" - ); - assert_eq!( - series - .get(era_2, period_2) - .expect("Entry must exist.") - .total_staked_amount(), - stake_amount_2 - ); - assert_eq!( - series - .get(era_2 + 1, period_2) - .expect("Entry must exist.") - .total_staked_amount(), - stake_amount_2 - unstake_amount - ); - assert_eq!( - series - .get(era_3, period_2) - .expect("Entry must exist.") - .total_staked_amount(), - stake_amount_3 - unstake_amount + era_reward_span.push(era_3, era_reward), + Err(EraRewardSpanError::NoCapacity) ); } #[test] -fn contract_staking_info_series_unstake_with_inconsistent_data_fails() { - let mut series = ContractStakingInfoSeries::default(); - let era = 5; - let period = 2; - let period_info = PeriodInfo { - number: period, - period_type: PeriodType::Voting, - ending_era: 31, +fn tier_slot_configuration_basic_tests() { + // TODO: this should be expanded & improved later + get_u32_type!(TiersNum, 4); + let params = TierParameters:: { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { + amount: 1000, + minimum_amount: 800, + }, + TierThreshold::DynamicTvlAmount { + amount: 500, + minimum_amount: 350, + }, + TierThreshold::DynamicTvlAmount { + amount: 100, + minimum_amount: 70, + }, + TierThreshold::FixedTvlAmount { amount: 50 }, + ]) + .unwrap(), }; - - // 1st - Unstake from empty series - assert!(series.unstake(1, period_info, era).is_err()); - - // 2nd - Unstake with old period - let amount = 37; - assert!(series.stake(amount, period_info, era).is_ok()); - - let old_period_info = { - let mut temp = period_info.clone(); - temp.number -= 1; - temp + assert!(params.is_valid(), "Example params must be valid!"); + + // Create a configuration with some values + let init_config = TiersConfiguration:: { + number_of_slots: 100, + slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + reward_portion: params.reward_portion.clone(), + tier_thresholds: params.tier_thresholds.clone(), }; - assert!(series.unstake(1, old_period_info, era - 1).is_err()); + assert!(init_config.is_valid(), "Init config must be valid!"); + + // Create a new config, based on a new price + let new_price = FixedU64::from_rational(20, 100); // in production will be expressed in USD + let new_config = init_config.calculate_new(new_price, ¶ms); + assert!(new_config.is_valid()); - // 3rd - Unstake with 'too' old era - assert!(series.unstake(1, period_info, era - 2).is_err()); - assert!(series.unstake(1, period_info, era - 1).is_ok()); + // TODO: expand tests, add more sanity checks (e.g. tier 3 requirement should never be lower than tier 4, etc.) } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 5f5bea6586..8949519e5a 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -16,26 +16,84 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . +//! # dApp Staking Module Types +//! +//! Contains various types, structs & enums used by the dApp staking implementation. +//! The main purpose of this is to abstract complexity away from the extrinsic call implementation, +//! and even more importantly to make the code more testable. +//! +//! # Overview +//! +//! The following is a high level overview of the implemented structs, enums & types. +//! For details, please refer to the documentation and code of each individual type. +//! +//! ## General Protocol Information +//! +//! * `EraNumber` - numeric Id of an era. +//! * `PeriodNumber` - numeric Id of a period. +//! * `Subperiod` - an enum describing which subperiod is active in the current period. +//! * `PeriodInfo` - contains information about the ongoing period, like period number, current subperiod and when will the current subperiod end. +//! * `PeriodEndInfo` - contains information about a finished past period, like the final era of the period, total amount staked & bonus reward pool. +//! * `ProtocolState` - contains the most general protocol state info: current era number, block when the era ends, ongoing period info, and whether protocol is in maintenance mode. +//! +//! ## DApp Information +//! +//! * `DAppId` - a compact unique numeric Id of a dApp. +//! * `DAppInfo` - contains general information about a dApp, like owner and reward beneficiary, Id and state. +//! * `ContractStakeAmount` - contains information about how much is staked on a particular contract. +//! +//! ## Staker Information +//! +//! * `UnlockingChunk` - describes some amount undergoing the unlocking process. +//! * `StakeAmount` - contains information about the staked amount in a particular era, and period. +//! * `AccountLedger` - keeps track of total locked & staked balance, unlocking chunks and number of stake entries. +//! * `SingularStakingInfo` - contains information about a particular staker's stake on a specific smart contract. Used to track loyalty. +//! +//! ## Era Information +//! +//! * `EraInfo` - contains information about the ongoing era, like how much is locked & staked. +//! * `EraReward` - contains information about a finished era, like reward pools and total staked amount. +//! * `EraRewardSpan` - a composite of multiple `EraReward` objects, used to describe a range of finished eras. +//! +//! ## Tier Information +//! +//! * `TierThreshold` - an enum describing tier entry thresholds. +//! * `TierParameters` - contains static information about tiers, like init thresholds, reward & slot distribution. +//! * `TiersConfiguration` - contains dynamic information about tiers, derived from `TierParameters` and onchain data. +//! * `DAppTier` - a compact struct describing a dApp's tier. +//! * `DAppTierRewards` - composite of `DAppTier` objects, describing the entire reward distribution for a particular era. +//! +//! TODO: some types are missing so double check before final merge that everything is covered and explained correctly + use frame_support::{pallet_prelude::*, BoundedVec}; use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; +use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, Zero}, - Saturating, + traits::{AtLeast32BitUnsigned, UniqueSaturatedInto, Zero}, + FixedPointNumber, Permill, Saturating, }; +pub use sp_std::{fmt::Debug, vec::Vec}; use astar_primitives::Balance; use crate::pallet::Config; -// TODO: instead of using `pub` visiblity for fields, either use `pub(crate)` or add dedicated methods for accessing them. +// Convenience type for `AccountLedger` usage. +pub type AccountLedgerFor = AccountLedger, ::MaxUnlockingChunks>; + +// Convenience type for `DAppTierRewards` usage. +pub type DAppTierRewardsFor = + DAppTierRewards, ::NumberOfTiers>; -/// Convenience type for `AccountLedger` usage. -pub type AccountLedgerFor = AccountLedger< - BlockNumberFor, - ::MaxUnlockingChunks, - ::MaxStakingChunks, ->; +// Helper struct for converting `u16` getter into `u32` +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct MaxNumberOfContractsU32(PhantomData); +impl Get for MaxNumberOfContractsU32 { + fn get() -> u32 { + T::MaxNumberOfContracts::get() as u32 + } +} /// Era number type pub type EraNumber = u32; @@ -43,27 +101,14 @@ pub type EraNumber = u32; pub type PeriodNumber = u32; /// Dapp Id type pub type DAppId = u16; - -// TODO: perhaps this trait is not needed and instead of having 2 separate '___Chunk' types, we can have just one? -/// Trait for types that can be used as a pair of amount & era. -pub trait AmountEraPair: MaxEncodedLen + Default + Copy { - /// Balance amount used somehow during the accompanied era. - fn get_amount(&self) -> Balance; - /// Era acting as timestamp for the accompanied amount. - fn get_era(&self) -> EraNumber; - // Sets the era to the specified value. - fn set_era(&mut self, era: EraNumber); - /// Increase the total amount by the specified increase, saturating at the maximum value. - fn saturating_accrue(&mut self, increase: Balance); - /// Reduce the total amount by the specified reduction, saturating at the minumum value. - fn saturating_reduce(&mut self, reduction: Balance); -} +/// Tier Id type +pub type TierId = u8; /// Simple enum representing errors possible when using sparse bounded vector. #[derive(Debug, PartialEq, Eq)] pub enum AccountLedgerError { - /// Old era values cannot be added. - OldEra, + /// Old or future era values cannot be added. + InvalidEra, /// Bounded storage capacity exceeded. NoCapacity, /// Invalid period specified. @@ -72,214 +117,73 @@ pub enum AccountLedgerError { UnavailableStakeFunds, /// Unstake amount is to large in respect to what's staked. UnstakeAmountLargerThanStake, + /// Nothing to claim. + NothingToClaim, + /// Rewards have already been claimed + AlreadyClaimed, } -/// Helper struct for easier manipulation of sparse pairs. -/// -/// The struct guarantees the following: -/// ----------------------------------- -/// 1. The vector is always sorted by era, in ascending order. -/// 2. There are no two consecutive zero chunks. -/// 3. There are no two chunks with the same era. -/// 4. The vector is always bounded by the specified maximum length. -/// -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] -#[scale_info(skip_type_params(ML))] -pub struct SparseBoundedAmountEraVec>(pub BoundedVec); - -impl SparseBoundedAmountEraVec -where - P: AmountEraPair, - ML: Get, -{ - /// Create new instance - pub fn new() -> Self { - Self(BoundedVec::::default()) - } - - /// Places the specified pair into the vector, in an appropriate place. - /// - /// There are two possible successful scenarios: - /// 1. If entry for the specified era already exists, it's updated. - /// [(100, 1)] -- add_amount(50, 1) --> [(150, 1)] - /// - /// 2. If entry for the specified era doesn't exist, it's created and insertion is attempted. - /// [(100, 1)] -- add_amount(50, 2) --> [(100, 1), (150, 2)] - /// - /// In case vector has no more capacity, error is returned, and whole operation is a noop. - pub fn add_amount( - &mut self, - amount: Balance, - era: EraNumber, - ) -> Result<(), AccountLedgerError> { - if amount.is_zero() { - return Ok(()); - } - - let mut chunk = if let Some(&chunk) = self.0.last() { - ensure!(chunk.get_era() <= era, AccountLedgerError::OldEra); - chunk - } else { - P::default() - }; - - chunk.saturating_accrue(amount); - - if chunk.get_era() == era && !self.0.is_empty() { - if let Some(last) = self.0.last_mut() { - *last = chunk; - } - } else { - chunk.set_era(era); - self.0 - .try_push(chunk) - .map_err(|_| AccountLedgerError::NoCapacity)?; - } - - Ok(()) - } - - /// Subtracts the specified amount of the total locked amount, if possible. - /// - /// There are multiple success scenarios/rules: - /// 1. If entry for the specified era already exists, it's updated. - /// a. [(100, 1)] -- subtract_amount(50, 1) --> [(50, 1)] - /// b. [(100, 1)] -- subtract_amount(100, 1) --> [] - /// - /// 2. All entries following the specified era will have their amount reduced as well. - /// [(100, 1), (150, 2)] -- subtract_amount(50, 1) --> [(50, 1), (100, 2)] - /// - /// 3. If entry for the specified era doesn't exist, it's created and insertion is attempted. - /// [(100, 1), (200, 3)] -- subtract_amount(100, 2) --> [(100, 1), (0, 2), (100, 3)] - /// - /// 4. No two consecutive zero chunks are allowed. - /// [(100, 1), (0, 2), (100, 3), (200, 4)] -- subtract_amount(100, 3) --> [(100, 1), (0, 2), (100, 4)] - /// - /// In case vector has no more capacity, error is returned, and whole operation is a noop. - pub fn subtract_amount( - &mut self, - amount: Balance, - era: EraNumber, - ) -> Result<(), AccountLedgerError> { - if amount.is_zero() || self.0.is_empty() { - return Ok(()); - } - // TODO: this method can surely be optimized (avoid too many iters) but focus on that later, - // when it's all working fine, and we have good test coverage. - // TODO2: realistically, the only eligible eras are the last two ones (current & previous). Code could be optimized for that. - - // Find the most relevant locked chunk for the specified era - let index = if let Some(index) = self.0.iter().rposition(|&chunk| chunk.get_era() <= era) { - index - } else { - // Covers scenario when there's only 1 chunk for the next era, and remove it if it's zero. - self.0 - .iter_mut() - .for_each(|chunk| chunk.saturating_reduce(amount)); - self.0.retain(|chunk| !chunk.get_amount().is_zero()); - return Ok(()); - }; - - // Update existing or insert a new chunk - let mut inner = self.0.clone().into_inner(); - let relevant_chunk_index = if inner[index].get_era() == era { - inner[index].saturating_reduce(amount); - index - } else { - // Take the most relevant chunk for the desired era, - // and use it as 'base' for the new chunk. - let mut chunk = inner[index]; - chunk.saturating_reduce(amount); - chunk.set_era(era); - - // Insert the new chunk AFTER the previous 'most relevant chunk'. - // The chunk we find is always either for the requested era, or some era before it. - inner.insert(index + 1, chunk); - index + 1 - }; - - // Update all chunks after the relevant one, and remove eligible zero chunks - inner[relevant_chunk_index + 1..] - .iter_mut() - .for_each(|chunk| chunk.saturating_reduce(amount)); - - // Prune all consecutive zero chunks - let mut new_inner = Vec::

::new(); - new_inner.push(inner[0]); - for i in 1..inner.len() { - if inner[i].get_amount().is_zero() && inner[i - 1].get_amount().is_zero() { - continue; - } else { - new_inner.push(inner[i]); - } - } - - inner = new_inner; - - // Cleanup if only one zero chunk exists - if inner.len() == 1 && inner[0].get_amount().is_zero() { - inner.pop(); - } - - // Update `locked` to the new vector - self.0 = BoundedVec::try_from(inner).map_err(|_| AccountLedgerError::NoCapacity)?; - - Ok(()) - } -} - -/// Distinct period types in dApp staking protocol. +/// Distinct subperiods in dApp staking protocol. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub enum PeriodType { - /// Period during which the focus is on voting. +pub enum Subperiod { + /// Subperiod during which the focus is on voting. Voting, - /// Period during which dApps and stakers earn rewards. + /// Subperiod during which dApps and stakers earn rewards. BuildAndEarn, } -impl PeriodType { +impl Subperiod { + /// Next subperiod, after `self`. pub fn next(&self) -> Self { match self { - PeriodType::Voting => PeriodType::BuildAndEarn, - PeriodType::BuildAndEarn => PeriodType::Voting, + Subperiod::Voting => Subperiod::BuildAndEarn, + Subperiod::BuildAndEarn => Subperiod::Voting, } } } -/// Wrapper type around current `PeriodType` and era number when it's expected to end. +/// Info about the ongoing period. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub struct PeriodInfo { + /// Period number. #[codec(compact)] pub number: PeriodNumber, - pub period_type: PeriodType, + /// subperiod. + pub subperiod: Subperiod, + /// Last era of the subperiod, after this a new subperiod should start. #[codec(compact)] - pub ending_era: EraNumber, + pub subperiod_end_era: EraNumber, } impl PeriodInfo { - /// Create new instance of `PeriodInfo` - pub fn new(number: PeriodNumber, period_type: PeriodType, ending_era: EraNumber) -> Self { - Self { - number, - period_type, - ending_era, - } - } - /// `true` if the provided era belongs to the next period, `false` otherwise. - /// It's only possible to provide this information for the `BuildAndEarn` period type. + /// It's only possible to provide this information for the `BuildAndEarn` subperiod. pub fn is_next_period(&self, era: EraNumber) -> bool { - self.period_type == PeriodType::BuildAndEarn && self.ending_era <= era + self.subperiod == Subperiod::BuildAndEarn && self.subperiod_end_era <= era } } +/// Information describing relevant information for a finished period. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub struct PeriodEndInfo { + /// Bonus reward pool allocated for 'loyal' stakers + #[codec(compact)] + pub bonus_reward_pool: Balance, + /// Total amount staked (remaining) from the voting period. + #[codec(compact)] + pub total_vp_stake: Balance, + /// Final era, inclusive, in which the period ended. + #[codec(compact)] + pub final_era: EraNumber, +} + /// Force types to speed up the next era, and even period. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub enum ForcingTypes { +pub enum ForcingType { /// Force the next era to start. - NewEra, - /// Force the current period phase to end, and new one to start - NewEraAndPeriodPhase, + Era, + /// Force the current subperiod to end, and new one to start. It will also force a new era to start. + Subperiod, } /// General information & state of the dApp staking protocol. @@ -289,14 +193,11 @@ pub struct ProtocolState { #[codec(compact)] pub era: EraNumber, /// Block number at which the next era should start. - /// TODO: instead of abusing on-initialize and wasting block-space, - /// I believe we should utilize `pallet-scheduler` to schedule the next era. Make an item for this. #[codec(compact)] pub next_era_start: BlockNumber, - /// Ongoing period type and when is it expected to end. + /// Information about the ongoing period. pub period_info: PeriodInfo, /// `true` if pallet is in maintenance mode (disabled), `false` otherwise. - /// TODO: provide some configurable barrier to handle this on the runtime level instead? Make an item for this? pub maintenance: bool, } @@ -310,8 +211,8 @@ where next_era_start: BlockNumber::from(1_u32), period_info: PeriodInfo { number: 0, - period_type: PeriodType::Voting, - ending_era: 2, + subperiod: Subperiod::Voting, + subperiod_end_era: 2, }, maintenance: false, } @@ -322,9 +223,9 @@ impl ProtocolState where BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, { - /// Current period type. - pub fn period_type(&self) -> PeriodType { - self.period_info.period_type + /// Current subperiod. + pub fn subperiod(&self) -> Subperiod { + self.period_info.subperiod } /// Current period number. @@ -333,8 +234,8 @@ where } /// Ending era of current period - pub fn ending_era(&self) -> EraNumber { - self.period_info.ending_era + pub fn period_end_era(&self) -> EraNumber { + self.period_info.subperiod_end_era } /// Checks whether a new era should be triggered, based on the provided `BlockNumber` argument @@ -343,9 +244,13 @@ where self.next_era_start <= now } - /// Triggers the next period type, updating appropriate parameters. - pub fn next_period_type(&mut self, ending_era: EraNumber, next_era_start: BlockNumber) { - let period_number = if self.period_type() == PeriodType::BuildAndEarn { + /// Triggers the next subperiod, updating appropriate parameters. + pub fn advance_to_next_subperiod( + &mut self, + subperiod_end_era: EraNumber, + next_era_start: BlockNumber, + ) { + let period_number = if self.subperiod() == Subperiod::BuildAndEarn { self.period_number().saturating_add(1) } else { self.period_number() @@ -353,8 +258,8 @@ where self.period_info = PeriodInfo { number: period_number, - period_type: self.period_type().next(), - ending_era, + subperiod: self.subperiod().next(), + subperiod_end_era, }; self.next_era_start = next_era_start; } @@ -383,11 +288,23 @@ pub struct DAppInfo { pub reward_destination: Option, } +impl DAppInfo { + /// Reward destination account for this dApp. + pub fn reward_beneficiary(&self) -> &AccountId { + match &self.reward_destination { + Some(account_id) => account_id, + None => &self.owner, + } + } +} + /// How much was unlocked in some block. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub struct UnlockingChunk { + /// Amount undergoing the unlocking period. #[codec(compact)] pub amount: Balance, + /// Block in which the unlocking period is finished for this chunk. #[codec(compact)] pub unlock_block: BlockNumber, } @@ -404,86 +321,67 @@ where } } -/// Information about how much was staked in a specific era. -#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct StakeChunk { - #[codec(compact)] - pub amount: Balance, - #[codec(compact)] - pub era: EraNumber, -} - -impl Default for StakeChunk { - fn default() -> Self { - Self { - amount: Balance::zero(), - era: EraNumber::zero(), - } - } -} - -impl AmountEraPair for StakeChunk { - fn get_amount(&self) -> Balance { - self.amount - } - fn get_era(&self) -> EraNumber { - self.era - } - fn set_era(&mut self, era: EraNumber) { - self.era = era; - } - fn saturating_accrue(&mut self, increase: Balance) { - self.amount.saturating_accrue(increase); - } - fn saturating_reduce(&mut self, reduction: Balance) { - self.amount.saturating_reduce(reduction); - } -} - /// General info about user's stakes -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] -#[scale_info(skip_type_params(UnlockingLen, StakedLen))] +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] +#[scale_info(skip_type_params(UnlockingLen))] pub struct AccountLedger< - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, - StakedLen: Get, > { /// How much active locked amount an account has. + #[codec(compact)] pub locked: Balance, - /// How much started unlocking on a certain block + /// Vector of all the unlocking chunks. pub unlocking: BoundedVec, UnlockingLen>, - /// How much user had staked in some period - pub staked: SparseBoundedAmountEraVec, - /// Last period in which account had staked. - pub staked_period: Option, + /// Primary field used to store how much was staked in a particular era. + pub staked: StakeAmount, + /// Secondary field used to store 'stake' information for the 'next era'. + /// This is needed since stake amount is only applicable from the next era after it's been staked. + /// + /// Both `stake` and `staked_future` must ALWAYS refer to the same period. + /// If `staked_future` is `Some`, it will always be **EXACTLY** one era after the `staked` field era. + pub staked_future: Option, + /// Number of contract stake entries in storage. + #[codec(compact)] + pub contract_stake_count: u32, } -impl Default - for AccountLedger +impl Default for AccountLedger where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, - StakedLen: Get, { fn default() -> Self { Self { locked: Balance::zero(), - unlocking: BoundedVec::, UnlockingLen>::default(), - staked: SparseBoundedAmountEraVec(BoundedVec::::default()), - staked_period: None, + unlocking: BoundedVec::default(), + staked: StakeAmount::default(), + staked_future: None, + contract_stake_count: Zero::zero(), } } } -impl AccountLedger +impl AccountLedger where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, + BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, - StakedLen: Get, { /// Empty if no locked/unlocking/staked info exists. pub fn is_empty(&self) -> bool { - self.locked.is_zero() && self.unlocking.is_empty() && self.staked.0.is_empty() + self.locked.is_zero() + && self.unlocking.is_empty() + && self.staked.total().is_zero() + && self.staked_future.is_none() } /// Returns active locked amount. @@ -559,7 +457,7 @@ where /// Amount available for unlocking. pub fn unlockable_amount(&self, current_period: PeriodNumber) -> Balance { self.active_locked_amount() - .saturating_sub(self.active_stake(current_period)) + .saturating_sub(self.staked_amount(current_period)) } /// Claims all of the fully unlocked chunks, and returns the total claimable amount. @@ -588,178 +486,378 @@ where amount } - /// Active staked balance. - /// - /// In case latest stored information is from the past period, active stake is considered to be zero. - pub fn active_stake(&self, active_period: PeriodNumber) -> Balance { - match self.staked_period { - Some(last_staked_period) if last_staked_period == active_period => self - .staked - .0 - .last() - .map_or(Balance::zero(), |chunk| chunk.amount), - _ => Balance::zero(), - } - } - /// Amount that is available for staking. /// /// This is equal to the total active locked amount, minus the staked amount already active. pub fn stakeable_amount(&self, active_period: PeriodNumber) -> Balance { self.active_locked_amount() - .saturating_sub(self.active_stake(active_period)) + .saturating_sub(self.staked_amount(active_period)) } /// Amount that is staked, in respect to currently active period. pub fn staked_amount(&self, active_period: PeriodNumber) -> Balance { - match self.staked_period { - Some(last_staked_period) if last_staked_period == active_period => self - .staked - .0 - .last() - // We should never fallback to the default value since that would mean ledger is in invalid state. - // TODO: perhaps this can be implemented in a better way to have some error handling? Returning 0 might not be the most secure way to handle it. - .map_or(Balance::zero(), |chunk| chunk.amount), - _ => Balance::zero(), + // First check the 'future' entry, afterwards check the 'first' entry + match self.staked_future { + Some(stake_amount) if stake_amount.period == active_period => stake_amount.total(), + _ => match self.staked { + stake_amount if stake_amount.period == active_period => stake_amount.total(), + _ => Balance::zero(), + }, } } + /// How much is staked for the specified subperiod, in respect to the specified era. + pub fn staked_amount_for_type(&self, subperiod: Subperiod, period: PeriodNumber) -> Balance { + // First check the 'future' entry, afterwards check the 'first' entry + match self.staked_future { + Some(stake_amount) if stake_amount.period == period => stake_amount.for_type(subperiod), + _ => match self.staked { + stake_amount if stake_amount.period == period => stake_amount.for_type(subperiod), + _ => Balance::zero(), + }, + } + } + + /// Verify that current era and period info arguments are valid for `stake` and `unstake` operations. + fn verify_stake_unstake_args( + &self, + era: EraNumber, + current_period_info: &PeriodInfo, + ) -> Result<(), AccountLedgerError> { + if !self.staked.is_empty() { + // In case entry for the current era exists, it must match the era exactly. + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + // In case it doesn't (i.e. first time staking), then the future era must match exactly + // one era after the one provided via argument. + } else if let Some(stake_amount) = self.staked_future { + if stake_amount.era != era.saturating_add(1) { + return Err(AccountLedgerError::InvalidEra); + } + if stake_amount.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + } + + Ok(()) + } + /// Adds the specified amount to total staked amount, if possible. /// - /// Staking is only allowed if one of the two following conditions is met: - /// 1. Staker is staking again in the period in which they already staked. - /// 2. Staker is staking for the first time in this period, and there are no staking chunks from the previous eras. + /// Staking can only be done for the ongoing period, and era. + /// 1. The `period` requirement enforces staking in the ongoing period. + /// 2. The `era` requirement enforces staking in the ongoing era. /// - /// Additonally, the staked amount must not exceed what's available for staking. + /// The 2nd condition is needed to prevent stakers from building a significant history of stakes, + /// without claiming the rewards. So if a historic era exists as an entry, stakers will first need to claim + /// the pending rewards, before they can stake again. + /// + /// Additionally, the staked amount must not exceed what's available for staking. pub fn add_stake_amount( &mut self, amount: Balance, era: EraNumber, - current_period: PeriodNumber, + current_period_info: PeriodInfo, ) -> Result<(), AccountLedgerError> { if amount.is_zero() { return Ok(()); } - match self.staked_period { - Some(last_staked_period) if last_staked_period != current_period => { - return Err(AccountLedgerError::InvalidPeriod); - } - _ => (), - } + self.verify_stake_unstake_args(era, ¤t_period_info)?; - if self.stakeable_amount(current_period) < amount { + if self.stakeable_amount(current_period_info.number) < amount { return Err(AccountLedgerError::UnavailableStakeFunds); } - self.staked.add_amount(amount, era)?; - self.staked_period = Some(current_period); + // Update existing entry if it exists, otherwise create it. + match self.staked_future.as_mut() { + Some(stake_amount) => { + stake_amount.add(amount, current_period_info.subperiod); + } + None => { + let mut stake_amount = self.staked; + stake_amount.era = era.saturating_add(1); + stake_amount.period = current_period_info.number; + stake_amount.add(amount, current_period_info.subperiod); + self.staked_future = Some(stake_amount); + } + } Ok(()) } /// Subtracts the specified amount from the total staked amount, if possible. /// - /// Unstaking will reduce total stake for the current era, and next era(s). - /// The specified amount must not exceed what's available for staking. + /// Unstake can only be called if the entry for the current era exists. + /// In case historic entry exists, rewards first need to be claimed, before unstaking is possible. + /// Similar as with stake functionality, this is to prevent staker from building a significant history of stakes. pub fn unstake_amount( &mut self, amount: Balance, era: EraNumber, - current_period: PeriodNumber, + current_period_info: PeriodInfo, ) -> Result<(), AccountLedgerError> { if amount.is_zero() { return Ok(()); } - // Cannot unstake if the period has passed. - match self.staked_period { - Some(last_staked_period) if last_staked_period != current_period => { - return Err(AccountLedgerError::InvalidPeriod); - } - _ => (), - } + self.verify_stake_unstake_args(era, ¤t_period_info)?; // User must be precise with their unstake amount. - if self.staked_amount(current_period) < amount { + if self.staked_amount(current_period_info.number) < amount { return Err(AccountLedgerError::UnstakeAmountLargerThanStake); } - self.staked.subtract_amount(amount, era) + self.staked.subtract(amount, current_period_info.subperiod); + + // Convenience cleanup + if self.staked.is_empty() { + self.staked = Default::default(); + } + + if let Some(mut stake_amount) = self.staked_future { + stake_amount.subtract(amount, current_period_info.subperiod); + + self.staked_future = if stake_amount.is_empty() { + None + } else { + Some(stake_amount) + }; + } + + Ok(()) } - /// Last era for which a stake entry exists. - /// If no stake entries exist, returns `None`. - pub fn last_stake_era(&self) -> Option { - self.staked.0.last().map(|chunk| chunk.era) + /// Period for which account has staking information or `None` if no staking information exists. + pub fn staked_period(&self) -> Option { + if self.staked.is_empty() { + self.staked_future.map(|stake_amount| stake_amount.period) + } else { + Some(self.staked.period) + } + } + + /// Earliest era for which the account has staking information or `None` if no staking information exists. + pub fn earliest_staked_era(&self) -> Option { + if self.staked.is_empty() { + self.staked_future.map(|stake_amount| stake_amount.era) + } else { + Some(self.staked.era) + } + } + + /// Cleanup staking information if it has expired. + /// + /// # Args + /// `threshold_period` - last period for which entries can still be considered valid. + /// + /// `true` if any change was made, `false` otherwise. + pub fn maybe_cleanup_expired(&mut self, threshold_period: PeriodNumber) -> bool { + match self.staked_period() { + Some(staked_period) if staked_period < threshold_period => { + self.staked = Default::default(); + self.staked_future = None; + true + } + _ => false, + } + } + + /// 'Claim' rewards up to the specified era. + /// Returns an iterator over the `(era, amount)` pairs, where `amount` + /// describes the staked amount eligible for reward in the appropriate era. + /// + /// If `period_end` is provided, it's used to determine whether all applicable chunks have been claimed. + pub fn claim_up_to_era( + &mut self, + era: EraNumber, + period_end: Option, + ) -> Result { + // Main entry exists, but era isn't 'in history' + if !self.staked.is_empty() && era <= self.staked.era { + return Err(AccountLedgerError::NothingToClaim); + } else if let Some(stake_amount) = self.staked_future { + // Future entry exists, but era isn't 'in history' + if era < stake_amount.era { + return Err(AccountLedgerError::NothingToClaim); + } + } + + // There are multiple options: + // 1. We only have future entry, no current entry + // 2. We have both current and future entry + // 3. We only have current entry, no future entry + let (span, maybe_first) = if let Some(stake_amount) = self.staked_future { + if self.staked.is_empty() { + ((stake_amount.era, era, stake_amount.total()), None) + } else { + ( + (stake_amount.era, era, stake_amount.total()), + Some((self.staked.era, self.staked.total())), + ) + } + } else { + ((self.staked.era, era, self.staked.total()), None) + }; + + let result = EraStakePairIter::new(span, maybe_first); + + // Rollover future to 'current' stake amount + if let Some(stake_amount) = self.staked_future.take() { + self.staked = stake_amount; + } + self.staked.era = era.saturating_add(1); + + // Make sure to clean up the entries if all rewards for the period have been claimed. + match period_end { + Some(subperiod_end_era) if era >= subperiod_end_era => { + self.staked = Default::default(); + self.staked_future = None; + } + _ => (), + } + + Ok(result) } } -/// Rewards pool for stakers & dApps -#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] -pub struct RewardInfo { - /// Rewards pool for accounts which have locked funds in dApp staking - #[codec(compact)] - pub participants: Balance, - /// Reward pool for dApps - #[codec(compact)] - pub dapps: Balance, +/// Helper internal struct for iterating over `(era, stake amount)` pairs. +/// +/// Due to how `AccountLedger` is implemented, few scenarios are possible when claming rewards: +/// +/// 1. `staked` has some amount, `staked_future` is `None` +/// * `maybe_first` is `None`, span describes the entire range +/// 2. `staked` has nothing, `staked_future` is some and has some amount +/// * `maybe_first` is `None`, span describes the entire range +/// 3. `staked` has some amount, `staked_future` has some amount +/// * `maybe_first` is `Some` and covers the `staked` entry, span describes the entire range except the first pair. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct EraStakePairIter { + /// Denotes whether the first entry is different than the others. + maybe_first: Option<(EraNumber, Balance)>, + /// Starting era of the span. + start_era: EraNumber, + /// Ending era of the span. + end_era: EraNumber, + /// Staked amount in the span. + amount: Balance, +} + +impl EraStakePairIter { + /// Create new iterator struct for `(era, staked amount)` pairs. + pub fn new( + span: (EraNumber, EraNumber, Balance), + maybe_first: Option<(EraNumber, Balance)>, + ) -> Self { + if let Some((era, _amount)) = maybe_first { + debug_assert!( + span.0 == era + 1, + "The 'other', if it exists, must cover era preceding the span." + ); + } + + Self { + maybe_first, + start_era: span.0, + end_era: span.1, + amount: span.2, + } + } } -// TODO: it would be nice to implement add/subtract logic on this struct and use it everywhere -// we need to keep track of staking amount for periods. Right now I have logic duplication which is not good. +impl Iterator for EraStakePairIter { + type Item = (EraNumber, Balance); + + fn next(&mut self) -> Option { + // Fist cover the scenario where we have a unique first value + if let Some((era, amount)) = self.maybe_first.take() { + return Some((era, amount)); + } + + // Afterwards, just keep returning the same amount for different eras + if self.start_era <= self.end_era { + let value = (self.start_era, self.amount); + self.start_era.saturating_inc(); + return Some(value); + } else { + None + } + } +} + +/// Describes stake amount in an particular era/period. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct StakeAmount { /// Amount of staked funds accounting for the voting period. #[codec(compact)] - voting: Balance, + pub voting: Balance, /// Amount of staked funds accounting for the build&earn period. #[codec(compact)] - build_and_earn: Balance, + pub build_and_earn: Balance, + /// Era to which this stake amount refers to. + #[codec(compact)] + pub era: EraNumber, + /// Period to which this stake amount refers to. + #[codec(compact)] + pub period: PeriodNumber, } impl StakeAmount { /// Create new instance of `StakeAmount` with specified `voting` and `build_and_earn` amounts. - pub fn new(voting: Balance, build_and_earn: Balance) -> Self { + pub fn new( + voting: Balance, + build_and_earn: Balance, + era: EraNumber, + period: PeriodNumber, + ) -> Self { Self { voting, build_and_earn, + era, + period, } } - /// Total amount staked in both period types. + /// `true` if nothing is staked, `false` otherwise + pub fn is_empty(&self) -> bool { + self.voting.is_zero() && self.build_and_earn.is_zero() + } + + /// Total amount staked in both subperiods. pub fn total(&self) -> Balance { self.voting.saturating_add(self.build_and_earn) } - /// Amount staked for the specified period type. - pub fn for_type(&self, period_type: PeriodType) -> Balance { - match period_type { - PeriodType::Voting => self.voting, - PeriodType::BuildAndEarn => self.build_and_earn, + /// Amount staked for the specified subperiod. + pub fn for_type(&self, subperiod: Subperiod) -> Balance { + match subperiod { + Subperiod::Voting => self.voting, + Subperiod::BuildAndEarn => self.build_and_earn, } } - // TODO: rename to add? - /// Stake the specified `amount` for the specified `period_type`. - pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { - match period_type { - PeriodType::Voting => self.voting.saturating_accrue(amount), - PeriodType::BuildAndEarn => self.build_and_earn.saturating_accrue(amount), + /// Stake the specified `amount` for the specified `subperiod`. + pub fn add(&mut self, amount: Balance, subperiod: Subperiod) { + match subperiod { + Subperiod::Voting => self.voting.saturating_accrue(amount), + Subperiod::BuildAndEarn => self.build_and_earn.saturating_accrue(amount), } } - // TODO: rename to subtract? - /// Unstake the specified `amount` for the specified `period_type`. + /// Unstake the specified `amount` for the specified `subperiod`. /// - /// In case period type is `Voting`, the amount is subtracted from the voting period. + /// In case subperiod is `Voting`, the amount is subtracted from the voting period. /// - /// In case period type is `Build&Earn`, the amount is first subtracted from the + /// In case subperiod is `Build&Earn`, the amount is first subtracted from the /// build&earn amount, and any rollover is subtracted from the voting period. - pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) { - match period_type { - PeriodType::Voting => self.voting.saturating_reduce(amount), - PeriodType::BuildAndEarn => { + pub fn subtract(&mut self, amount: Balance, subperiod: Subperiod) { + match subperiod { + Subperiod::Voting => self.voting.saturating_reduce(amount), + Subperiod::BuildAndEarn => { if self.build_and_earn >= amount { self.build_and_earn.saturating_reduce(amount); } else { @@ -776,8 +874,6 @@ impl StakeAmount { /// Info about current era, including the rewards, how much is locked, unlocking, etc. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct EraInfo { - /// Info about era rewards - pub rewards: RewardInfo, /// How much balance is considered to be locked in the current era. /// This value influences the reward distribution. #[codec(compact)] @@ -814,15 +910,15 @@ impl EraInfo { self.unlocking.saturating_reduce(amount); } - /// Add the specified `amount` to the appropriate stake amount, based on the `PeriodType`. - pub fn add_stake_amount(&mut self, amount: Balance, period_type: PeriodType) { - self.next_stake_amount.stake(amount, period_type); + /// Add the specified `amount` to the appropriate stake amount, based on the `Subperiod`. + pub fn add_stake_amount(&mut self, amount: Balance, subperiod: Subperiod) { + self.next_stake_amount.add(amount, subperiod); } - /// Subtract the specified `amount` from the appropriate stake amount, based on the `PeriodType`. - pub fn unstake_amount(&mut self, amount: Balance, period_type: PeriodType) { - self.current_stake_amount.unstake(amount, period_type); - self.next_stake_amount.unstake(amount, period_type); + /// Subtract the specified `amount` from the appropriate stake amount, based on the `Subperiod`. + pub fn unstake_amount(&mut self, amount: Balance, subperiod: Subperiod) { + self.current_stake_amount.subtract(amount, subperiod); + self.next_stake_amount.subtract(amount, subperiod); } /// Total staked amount in this era. @@ -831,8 +927,8 @@ impl EraInfo { } /// Staked amount of specified `type` in this era. - pub fn staked_amount(&self, period_type: PeriodType) -> Balance { - self.current_stake_amount.for_type(period_type) + pub fn staked_amount(&self, subperiod: Subperiod) -> Balance { + self.current_stake_amount.for_type(subperiod) } /// Total staked amount in the next era. @@ -841,24 +937,23 @@ impl EraInfo { } /// Staked amount of specifeid `type` in the next era. - pub fn staked_amount_next_era(&self, period_type: PeriodType) -> Balance { - self.next_stake_amount.for_type(period_type) + pub fn staked_amount_next_era(&self, subperiod: Subperiod) -> Balance { + self.next_stake_amount.for_type(subperiod) } /// Updates `Self` to reflect the transition to the next era. /// /// ## Args - /// `next_period_type` - `None` if no period type change, `Some(type)` if `type` is starting from the next era. - pub fn migrate_to_next_era(&mut self, next_period_type: Option) { - self.rewards = Default::default(); + /// `next_subperiod` - `None` if no subperiod change, `Some(type)` if `type` is starting from the next era. + pub fn migrate_to_next_era(&mut self, next_subperiod: Option) { self.active_era_locked = self.total_locked; - match next_period_type { + match next_subperiod { // If next era marks start of new voting period period, it means we're entering a new period - Some(PeriodType::Voting) => { + Some(Subperiod::Voting) => { self.current_stake_amount = Default::default(); self.next_stake_amount = Default::default(); } - Some(PeriodType::BuildAndEarn) | None => { + Some(Subperiod::BuildAndEarn) | None => { self.current_stake_amount = self.next_stake_amount; } }; @@ -870,15 +965,8 @@ impl EraInfo { /// Keeps track of amount staked in the 'voting period', as well as 'build&earn period'. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct SingularStakingInfo { - /// Total amount staked during the voting period. - #[codec(compact)] - vp_staked_amount: Balance, - /// Total amount staked during the build&earn period. - #[codec(compact)] - bep_staked_amount: Balance, - /// Period number for which this entry is relevant. - #[codec(compact)] - period: PeriodNumber, + /// Staked amount + staked: StakeAmount, /// Indicates whether a staker is a loyal staker or not. loyal_staker: bool, } @@ -889,23 +977,19 @@ impl SingularStakingInfo { /// ## Args /// /// `period` - period number for which this entry is relevant. - /// `period_type` - period type during which this entry is created. - pub fn new(period: PeriodNumber, period_type: PeriodType) -> Self { + /// `subperiod` - subperiod during which this entry is created. + pub fn new(period: PeriodNumber, subperiod: Subperiod) -> Self { Self { - vp_staked_amount: Balance::zero(), - bep_staked_amount: Balance::zero(), - period, + staked: StakeAmount::new(Balance::zero(), Balance::zero(), 0, period), // Loyalty staking is only possible if stake is first made during the voting period. - loyal_staker: period_type == PeriodType::Voting, + loyal_staker: subperiod == Subperiod::Voting, } } - /// Stake the specified amount on the contract, for the specified period type. - pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { - match period_type { - PeriodType::Voting => self.vp_staked_amount.saturating_accrue(amount), - PeriodType::BuildAndEarn => self.bep_staked_amount.saturating_accrue(amount), - } + /// Stake the specified amount on the contract, for the specified subperiod. + pub fn stake(&mut self, amount: Balance, subperiod: Subperiod) { + // TODO: if we keep `StakeAmount` type here, consider including the era as well for consistency + self.staked.add(amount, subperiod); } /// Unstakes some of the specified amount from the contract. @@ -914,44 +998,32 @@ impl SingularStakingInfo { /// and `voting period` has passed, this will remove the _loyalty_ flag from the staker. /// /// Returns the amount that was unstaked from the `voting period` stake, and from the `build&earn period` stake. - pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) -> (Balance, Balance) { - // If B&E period stake can cover the unstaking amount, just reduce it. - if self.bep_staked_amount >= amount { - self.bep_staked_amount.saturating_reduce(amount); - (Balance::zero(), amount) - } else { - // In case we have to dip into the voting period stake, make sure B&E period stake is reduced first. - // Also make sure to remove loyalty flag from the staker. - let vp_staked_amount_snapshot = self.vp_staked_amount; - let bep_amount_snapshot = self.bep_staked_amount; - let leftover_amount = amount.saturating_sub(self.bep_staked_amount); - - self.vp_staked_amount.saturating_reduce(leftover_amount); - self.bep_staked_amount = Balance::zero(); - - // It's ok if staker reduces their stake amount during voting period. - // Once loyalty flag is removed, it cannot be returned. - self.loyal_staker = self.loyal_staker && period_type == PeriodType::Voting; - - // Actual amount that was unstaked: (voting period unstake, B&E period unstake) - ( - vp_staked_amount_snapshot.saturating_sub(self.vp_staked_amount), - bep_amount_snapshot, - ) - } + pub fn unstake(&mut self, amount: Balance, subperiod: Subperiod) -> (Balance, Balance) { + let snapshot = self.staked; + + self.staked.subtract(amount, subperiod); + + self.loyal_staker = self.loyal_staker + && (subperiod == Subperiod::Voting + || subperiod == Subperiod::BuildAndEarn && self.staked.voting == snapshot.voting); + + // Amount that was unstaked + ( + snapshot.voting.saturating_sub(self.staked.voting), + snapshot + .build_and_earn + .saturating_sub(self.staked.build_and_earn), + ) } - /// Total staked on the contract by the user. Both period type stakes are included. + /// Total staked on the contract by the user. Both subperiod stakes are included. pub fn total_staked_amount(&self) -> Balance { - self.vp_staked_amount.saturating_add(self.bep_staked_amount) + self.staked.total() } /// Returns amount staked in the specified period. - pub fn staked_amount(&self, period_type: PeriodType) -> Balance { - match period_type { - PeriodType::Voting => self.vp_staked_amount, - PeriodType::BuildAndEarn => self.bep_staked_amount, - } + pub fn staked_amount(&self, subperiod: Subperiod) -> Balance { + self.staked.for_type(subperiod) } /// If `true` staker has staked during voting period and has never reduced their sta @@ -961,304 +1033,688 @@ impl SingularStakingInfo { /// Period for which this entry is relevant. pub fn period_number(&self) -> PeriodNumber { - self.period + self.staked.period } /// `true` if no stake exists, `false` otherwise. pub fn is_empty(&self) -> bool { - self.vp_staked_amount.is_zero() && self.bep_staked_amount.is_zero() + self.staked.is_empty() } } -/// Information about how much was staked on a contract during a specific era or period. +/// Composite type that holds information about how much was staked on a contract in up to two distinct eras. /// -#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] -pub struct ContractStakingInfo { - #[codec(compact)] - vp_staked_amount: Balance, - #[codec(compact)] - bep_staked_amount: Balance, - #[codec(compact)] - era: EraNumber, - #[codec(compact)] - period: PeriodNumber, +/// This is needed since 'stake' operation only makes the staked amount valid from the next era. +/// In a situation when `stake` is called in era `N`, the staked amount is valid from era `N+1`, hence the need for 'future' entry. +/// +/// **NOTE:** The 'future' entry term is only valid in the era when `stake` is called. It's possible contract stake isn't changed in consecutive eras, +/// so we might end up in a situation where era is `N + 10` but `staked` entry refers to era `N` and `staked_future` entry refers to era `N+1`. +/// This is still valid since these values are expected to be updated lazily. +#[derive(Encode, Decode, MaxEncodedLen, RuntimeDebug, PartialEq, Eq, Clone, TypeInfo, Default)] +pub struct ContractStakeAmount { + /// Staked amount in the 'current' era. + pub staked: StakeAmount, + /// Staked amount in the next or 'future' era. + pub staked_future: Option, } +impl ContractStakeAmount { + /// `true` if series is empty, `false` otherwise. + pub fn is_empty(&self) -> bool { + self.staked.is_empty() && self.staked_future.is_none() + } -impl ContractStakingInfo { - /// Create new instance of `ContractStakingInfo` with specified era & period. - /// These parameters are immutable. - /// - /// Staked amounts are initialized to zero and can be increased or decreased. - pub fn new(era: EraNumber, period: PeriodNumber) -> Self { - Self { - vp_staked_amount: Balance::zero(), - bep_staked_amount: Balance::zero(), - era, - period, + /// Latest period for which stake entry exists. + pub fn latest_stake_period(&self) -> Option { + if let Some(stake_amount) = self.staked_future { + Some(stake_amount.period) + } else if !self.staked.is_empty() { + Some(self.staked.period) + } else { + None } } - /// Total staked amount on the contract. - pub fn total_staked_amount(&self) -> Balance { - self.vp_staked_amount.saturating_add(self.bep_staked_amount) + /// Latest era for which stake entry exists. + pub fn latest_stake_era(&self) -> Option { + if let Some(stake_amount) = self.staked_future { + Some(stake_amount.era) + } else if !self.staked.is_empty() { + Some(self.staked.era) + } else { + None + } } - /// Staked amount of the specified period type. - /// - /// Note: - /// It is possible that voting period stake is reduced during the build&earn period. - /// This is because stakers can unstake their funds during the build&earn period, which can - /// chip away from the voting period stake. - pub fn staked_amount(&self, period_type: PeriodType) -> Balance { - match period_type { - PeriodType::Voting => self.vp_staked_amount, - PeriodType::BuildAndEarn => self.bep_staked_amount, + /// Returns the `StakeAmount` type for the specified era & period, if it exists. + pub fn get(&self, era: EraNumber, period: PeriodNumber) -> Option { + let mut maybe_result = match (self.staked, self.staked_future) { + (_, Some(staked_future)) if staked_future.era <= era => { + if staked_future.period == period { + Some(staked_future) + } else { + None + } + } + (staked, _) if staked.era <= era && staked.period == period => Some(staked), + _ => None, + }; + + if let Some(result) = maybe_result.as_mut() { + result.era = era; } - } - /// Era for which this entry is relevant. - pub fn era(&self) -> EraNumber { - self.era + maybe_result } - /// Period for which this entry is relevant. - pub fn period(&self) -> PeriodNumber { - self.period + /// Total staked amount on the contract, in the active period. + pub fn total_staked_amount(&self, active_period: PeriodNumber) -> Balance { + match (self.staked, self.staked_future) { + (_, Some(staked_future)) if staked_future.period == active_period => { + staked_future.total() + } + (staked, _) if staked.period == active_period => staked.total(), + _ => Balance::zero(), + } } - /// Stake specified `amount` on the contract, for the specified `period_type`. - pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { - match period_type { - PeriodType::Voting => self.vp_staked_amount.saturating_accrue(amount), - PeriodType::BuildAndEarn => self.bep_staked_amount.saturating_accrue(amount), + /// Staked amount on the contract, for specified subperiod, in the active period. + pub fn staked_amount(&self, active_period: PeriodNumber, subperiod: Subperiod) -> Balance { + match (self.staked, self.staked_future) { + (_, Some(staked_future)) if staked_future.period == active_period => { + staked_future.for_type(subperiod) + } + (staked, _) if staked.period == active_period => staked.for_type(subperiod), + _ => Balance::zero(), } } - /// Unstake specified `amount` from the contract, for the specified `period_type`. - pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) { - match period_type { - PeriodType::Voting => self.vp_staked_amount.saturating_reduce(amount), - PeriodType::BuildAndEarn => { - let overflow = amount.saturating_sub(self.bep_staked_amount); - self.bep_staked_amount.saturating_reduce(amount); - self.vp_staked_amount.saturating_reduce(overflow); + /// Stake the specified `amount` on the contract, for the specified `subperiod` and `era`. + pub fn stake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { + // TODO: tests need to be re-writen for this after the refactoring + let stake_era = current_era.saturating_add(1); + + match self.staked_future.as_mut() { + // Future entry matches the era, just updated it and return + Some(stake_amount) if stake_amount.era == stake_era => { + stake_amount.add(amount, period_info.subperiod); + return; + } + // Future entry has older era, but periods match so overwrite the 'current' entry with it + Some(stake_amount) if stake_amount.period == period_info.number => { + self.staked = *stake_amount; } + // Otherwise do nothing + _ => (), + } + + // Prepare new entry + let mut new_entry = match self.staked { + // 'current' entry period matches so we use it as base for the new entry + stake_amount if stake_amount.period == period_info.number => stake_amount, + // otherwise just create a dummy new entry + _ => Default::default(), + }; + new_entry.add(amount, period_info.subperiod); + new_entry.era = stake_era; + new_entry.period = period_info.number; + + self.staked_future = Some(new_entry); + + // Convenience cleanup + if self.staked.period < period_info.number { + self.staked = Default::default(); } } - /// `true` if no stake exists, `false` otherwise. - pub fn is_empty(&self) -> bool { - self.vp_staked_amount.is_zero() && self.bep_staked_amount.is_zero() + /// Unstake the specified `amount` from the contract, for the specified `subperiod` and `era`. + pub fn unstake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { + // TODO: tests need to be re-writen for this after the refactoring + + // First align entries - we only need to keep track of the current era, and the next one + match self.staked_future { + // Future entry exists, but it covers current or older era. + Some(stake_amount) + if stake_amount.era <= current_era && stake_amount.period == period_info.number => + { + self.staked = stake_amount; + self.staked.era = current_era; + self.staked_future = None; + } + _ => (), + } + + // Current entry is from the right period, but older era. Shift it to the current era. + if self.staked.era < current_era && self.staked.period == period_info.number { + self.staked.era = current_era; + } + + // Subtract both amounts + self.staked.subtract(amount, period_info.subperiod); + if let Some(stake_amount) = self.staked_future.as_mut() { + stake_amount.subtract(amount, period_info.subperiod); + } + + // Conevnience cleanup + if self.staked.is_empty() { + self.staked = Default::default(); + } + if let Some(stake_amount) = self.staked_future { + if stake_amount.is_empty() { + self.staked_future = None; + } + } } } -const STAKING_SERIES_HISTORY: u32 = 3; +/// Information required for staker reward payout for a particular era. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct EraReward { + /// Total reward pool for staker rewards + #[codec(compact)] + pub staker_reward_pool: Balance, + /// Total amount which was staked at the end of an era + #[codec(compact)] + pub staked: Balance, + /// Total reward pool for dApp rewards + #[codec(compact)] + pub dapp_reward_pool: Balance, +} + +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum EraRewardSpanError { + /// Provided era is invalid. Must be exactly one era after the last one in the span. + InvalidEra, + /// Span has no more capacity for additional entries. + NoCapacity, +} + +/// Used to efficiently store era span information. +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] +#[scale_info(skip_type_params(SL))] +pub struct EraRewardSpan> { + /// Span of EraRewardInfo entries. + span: BoundedVec, + /// The first era in the span. + #[codec(compact)] + first_era: EraNumber, + /// The final era in the span. + #[codec(compact)] + last_era: EraNumber, +} + +impl EraRewardSpan +where + SL: Get, +{ + /// Create new instance of the `EraRewardSpan` + pub fn new() -> Self { + Self { + span: Default::default(), + first_era: 0, + last_era: 0, + } + } -/// Composite type that holds information about how much was staked on a contract during some past eras & periods, including the current era & period. -#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] -pub struct ContractStakingInfoSeries( - BoundedVec>, -); -impl ContractStakingInfoSeries { - /// Helper function to create a new instance of `ContractStakingInfoSeries`. - #[cfg(test)] - pub fn new(inner: Vec) -> Self { - Self(BoundedVec::try_from(inner).expect("Test should ensure this is always valid")) + /// First era covered in the span. + pub fn first_era(&self) -> EraNumber { + self.first_era } - /// Returns inner `Vec` of `ContractStakingInfo` instances. Useful for testing. - #[cfg(test)] - pub fn inner(&self) -> Vec { - self.0.clone().into_inner() + /// Last era covered in the span + pub fn last_era(&self) -> EraNumber { + self.last_era } - /// Length of the series. + /// Span length. pub fn len(&self) -> usize { - self.0.len() + self.span.len() } - /// `true` if series is empty, `false` otherwise. + /// `true` if span is empty, `false` otherwise. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.span.is_empty() } - /// Returns the `ContractStakingInfo` type for the specified era & period, if it exists. - pub fn get(&self, era: EraNumber, period: PeriodNumber) -> Option { - let idx = self.0.binary_search_by(|info| info.era().cmp(&era)); - - // There are couple of distinct scenarios: - // 1. Era exists, so we just return it. - // 2. Era doesn't exist, and ideal index is zero, meaning there's nothing in history that would cover this era. - // 3. Era doesn't exist, and ideal index is greater than zero, meaning we can potentially use one of the previous entries to derive the information. - // 3.1. In case periods are matching, we return that value. - // 3.2. In case periods aren't matching, we return `None` since stakes don't carry over between periods. - match idx { - Ok(idx) => self.0.get(idx).map(|x| *x), - Err(ideal_idx) => { - if ideal_idx.is_zero() { - None - } else { - match self.0.get(ideal_idx - 1) { - Some(info) if info.period() == period => { - let mut info = *info; - info.era = era; - Some(info) - } - _ => None, - } - } + /// Push new `EraReward` entry into the span. + /// If span is non-empty, the provided `era` must be exactly one era after the last one in the span. + pub fn push( + &mut self, + era: EraNumber, + era_reward: EraReward, + ) -> Result<(), EraRewardSpanError> { + // First entry, no checks, just set eras to the provided value. + if self.span.is_empty() { + self.first_era = era; + self.last_era = era; + self.span + .try_push(era_reward) + // Defensive check, should never happen since it means capacity is 'zero'. + .map_err(|_| EraRewardSpanError::NoCapacity) + } else { + // Defensive check to ensure next era rewards refers to era after the last one in the span. + if era != self.last_era.saturating_add(1) { + return Err(EraRewardSpanError::InvalidEra); } + + self.last_era = era; + self.span + .try_push(era_reward) + .map_err(|_| EraRewardSpanError::NoCapacity) } } - /// Last era for which a stake entry exists, `None` if no entries exist. - pub fn last_stake_era(&self) -> Option { - self.0.last().map(|info| info.era()) + /// Get the `EraReward` entry for the specified `era`. + /// + /// In case `era` is not covered by the span, `None` is returned. + pub fn get(&self, era: EraNumber) -> Option<&EraReward> { + match era.checked_sub(self.first_era()) { + Some(index) => self.span.get(index as usize), + None => None, + } } +} - /// Last period for which a stake entry exists, `None` if no entries exist. - pub fn last_stake_period(&self) -> Option { - self.0.last().map(|info| info.period()) - } +/// Description of tier entry requirement. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub enum TierThreshold { + /// Entry into tier is mandated by minimum amount of staked funds. + /// Value is fixed, and is not expected to change in between periods. + FixedTvlAmount { amount: Balance }, + /// Entry into tier is mandated by minimum amount of staked funds. + /// Value is expected to dynamically change in-between periods, depending on the system parameters. + /// The `amount` should never go below the `minimum_amount`. + DynamicTvlAmount { + amount: Balance, + minimum_amount: Balance, + }, +} - /// Total staked amount on the contract, in the active period. - pub fn total_staked_amount(&self, active_period: PeriodNumber) -> Balance { - match self.0.last() { - Some(last_element) if last_element.period() == active_period => { - last_element.total_staked_amount() - } - _ => Balance::zero(), +impl TierThreshold { + /// Used to check if stake amount satisfies the threshold or not. + pub fn is_satisfied(&self, stake: Balance) -> bool { + match self { + Self::FixedTvlAmount { amount } => stake >= *amount, + Self::DynamicTvlAmount { amount, .. } => stake >= *amount, } } +} - /// Staked amount on the contract, for specified period type, in the active period. - pub fn staked_amount(&self, period: PeriodNumber, period_type: PeriodType) -> Balance { - match self.0.last() { - Some(last_element) if last_element.period() == period => { - last_element.staked_amount(period_type) - } - _ => Balance::zero(), +/// Top level description of tier slot parameters used to calculate tier configuration. +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] +#[scale_info(skip_type_params(NT))] +pub struct TierParameters> { + /// Reward distribution per tier, in percentage. + /// First entry refers to the first tier, and so on. + /// The sum of all values must be exactly equal to 1. + pub reward_portion: BoundedVec, + /// Distribution of number of slots per tier, in percentage. + /// First entry refers to the first tier, and so on. + /// The sum of all values must be exactly equal to 1. + pub slot_distribution: BoundedVec, + /// Requirements for entry into each tier. + /// First entry refers to the first tier, and so on. + pub tier_thresholds: BoundedVec, +} + +impl> TierParameters { + /// Check if configuration is valid. + /// All vectors are expected to have exactly the amount of entries as `number_of_tiers`. + pub fn is_valid(&self) -> bool { + let number_of_tiers: usize = NT::get() as usize; + number_of_tiers == self.reward_portion.len() + && number_of_tiers == self.slot_distribution.len() + && number_of_tiers == self.tier_thresholds.len() + // TODO: Make check more detailed, verify that entries sum up to 1 or 100% + } +} + +impl> Default for TierParameters { + fn default() -> Self { + Self { + reward_portion: BoundedVec::default(), + slot_distribution: BoundedVec::default(), + tier_thresholds: BoundedVec::default(), } } +} - /// Stake the specified `amount` on the contract, for the specified `period_type` and `era`. - pub fn stake( - &mut self, - amount: Balance, - period_info: PeriodInfo, - era: EraNumber, - ) -> Result<(), ()> { - // Defensive check to ensure we don't end up in a corrupted state. Should never happen. - if let Some(last_element) = self.0.last() { - if last_element.era() > era || last_element.period() > period_info.number { - return Err(()); - } +/// Configuration of dApp tiers. +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] +#[scale_info(skip_type_params(NT))] +pub struct TiersConfiguration> { + /// Total number of slots. + #[codec(compact)] + pub number_of_slots: u16, + /// Number of slots per tier. + /// First entry refers to the first tier, and so on. + pub slots_per_tier: BoundedVec, + /// Reward distribution per tier, in percentage. + /// First entry refers to the first tier, and so on. + /// The sum of all values must be exactly equal to 1. + pub reward_portion: BoundedVec, + /// Requirements for entry into each tier. + /// First entry refers to the first tier, and so on. + pub tier_thresholds: BoundedVec, +} + +impl> Default for TiersConfiguration { + fn default() -> Self { + Self { + number_of_slots: 0, + slots_per_tier: BoundedVec::default(), + reward_portion: BoundedVec::default(), + tier_thresholds: BoundedVec::default(), } + } +} - // Get the most relevant `ContractStakingInfo` instance - let mut staking_info = if let Some(last_element) = self.0.last() { - if last_element.era() == era { - // Era matches, so we just update the last element. - let last_element = *last_element; - let _ = self.0.pop(); - last_element - } else if last_element.period() == period_info.number { - // Periods match so we should 'copy' the last element to get correct staking amount - let mut temp = *last_element; - temp.era = era; - temp - } else { - // It's a new period, so we need a completely new instance - ContractStakingInfo::new(era, period_info.number) - } +impl> TiersConfiguration { + /// Check if parameters are valid. + pub fn is_valid(&self) -> bool { + let number_of_tiers: usize = NT::get() as usize; + number_of_tiers == self.slots_per_tier.len() + // All vecs length must match number of tiers. + && number_of_tiers == self.reward_portion.len() + && number_of_tiers == self.tier_thresholds.len() + // Total number of slots must match the sum of slots per tier. + && self.slots_per_tier.iter().fold(0, |acc, x| acc + x) == self.number_of_slots + } + + /// Calculate new `TiersConfiguration`, based on the old settings, current native currency price and tier configuration. + pub fn calculate_new(&self, native_price: FixedU64, params: &TierParameters) -> Self { + let new_number_of_slots = Self::calculate_number_of_slots(native_price); + + // Calculate how much each tier gets slots. + let new_slots_per_tier: Vec = params + .slot_distribution + .clone() + .into_inner() + .iter() + .map(|percent| *percent * new_number_of_slots as u128) + .map(|x| x.unique_saturated_into()) + .collect(); + let new_slots_per_tier = + BoundedVec::::try_from(new_slots_per_tier).unwrap_or_default(); + + // Update tier thresholds. + // In case number of slots increase, we decrease thresholds required to enter the tier. + // In case number of slots decrease, we increase the threshold required to enter the tier. + // + // According to formula: %_threshold = (100% / (100% - delta_%_slots) - 1) * 100% + // + // where delta_%_slots is simply: (old_num_slots - new_num_slots) / old_num_slots + // + // When these entries are put into the threshold formula, we get: + // = 1 / ( 1 - (old_num_slots - new_num_slots) / old_num_slots ) - 1 + // = 1 / ( new / old) - 1 + // = old / new - 1 + // = (old - new) / new + // + // This number can be negative. In order to keep all operations in unsigned integer domain, + // formulas are adjusted like: + // + // 1. Number of slots has increased, threshold is expected to decrease + // %_threshold = (new_num_slots - old_num_slots) / new_num_slots + // new_threshold = old_threshold * (1 - %_threshold) + // + // 2. Number of slots has decreased, threshold is expected to increase + // %_threshold = (old_num_slots - new_num_slots) / new_num_slots + // new_threshold = old_threshold * (1 + %_threshold) + // + let new_tier_thresholds = if new_number_of_slots > self.number_of_slots { + let delta_threshold_decrease = FixedU64::from_rational( + (new_number_of_slots - self.number_of_slots).into(), + new_number_of_slots.into(), + ); + + let mut new_tier_thresholds = self.tier_thresholds.clone(); + new_tier_thresholds + .iter_mut() + .for_each(|threshold| match threshold { + TierThreshold::DynamicTvlAmount { + amount, + minimum_amount, + } => { + *amount = amount + .saturating_sub(delta_threshold_decrease.saturating_mul_int(*amount)); + *amount = *amount.max(minimum_amount); + } + _ => (), + }); + + new_tier_thresholds + } else if new_number_of_slots < self.number_of_slots { + let delta_threshold_increase = FixedU64::from_rational( + (self.number_of_slots - new_number_of_slots).into(), + new_number_of_slots.into(), + ); + + let mut new_tier_thresholds = self.tier_thresholds.clone(); + new_tier_thresholds + .iter_mut() + .for_each(|threshold| match threshold { + TierThreshold::DynamicTvlAmount { amount, .. } => { + *amount = amount + .saturating_add(delta_threshold_increase.saturating_mul_int(*amount)); + } + _ => (), + }); + + new_tier_thresholds } else { - // It's a new period, so we need a completely new instance - ContractStakingInfo::new(era, period_info.number) + self.tier_thresholds.clone() }; - // Update the stake amount - staking_info.stake(amount, period_info.period_type); + Self { + number_of_slots: new_number_of_slots, + slots_per_tier: new_slots_per_tier, + reward_portion: params.reward_portion.clone(), + tier_thresholds: new_tier_thresholds, + } + } - // Prune before pushing the new entry - self.prune(); + /// Calculate number of slots, based on the provided native token price. + pub fn calculate_number_of_slots(native_price: FixedU64) -> u16 { + // floor(1000 x price + 50), formula proposed in Tokenomics 2.0 document. + let result: u64 = native_price.saturating_mul_int(1000).saturating_add(50); - // This should be infalible due to previous checks that ensure we don't end up overflowing the vector. - self.0.try_push(staking_info).map_err(|_| ()) + result.unique_saturated_into() } +} - /// Unstake the specified `amount` from the contract, for the specified `period_type` and `era`. - pub fn unstake( - &mut self, - amount: Balance, - period_info: PeriodInfo, - era: EraNumber, - ) -> Result<(), ()> { - // Defensive check to ensure we don't end up in a corrupted state. Should never happen. - if let Some(last_element) = self.0.last() { - // It's possible last element refers to the upcoming era, hence the "-1" on the 'era'. - if last_element.era().saturating_sub(1) > era - || last_element.period() > period_info.number - { - return Err(()); - } - } else { - // Vector is empty, should never happen. - return Err(()); - } - - // 1st step - remove the last element IFF it's for the next era. - // Unstake the requested amount from it. - let last_era_info = match self.0.last() { - Some(last_element) if last_element.era() == era.saturating_add(1) => { - let mut last_element = *last_element; - last_element.unstake(amount, period_info.period_type); - let _ = self.0.pop(); - Some(last_element) - } - _ => None, - }; +/// Used to represent into which tier does a particular dApp fall into. +/// +/// In case tier Id is `None`, it means that either reward was already claimed, or dApp is not eligible for rewards. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +pub struct DAppTier { + /// Unique dApp id in dApp staking protocol. + #[codec(compact)] + pub dapp_id: DAppId, + /// `Some(tier_id)` if dApp belongs to tier and has unclaimed rewards, `None` otherwise. + pub tier_id: Option, +} - // 2nd step - 3 options: - // 1. - last element has a matching era so we just update it. - // 2. - last element has a past era and matching period, so we'll create a new entry based on it. - // 3. - last element has a past era and past period, meaning it's invalid. - let second_last_era_info = if let Some(last_element) = self.0.last_mut() { - if last_element.era() == era { - last_element.unstake(amount, period_info.period_type); - None - } else if last_element.period() == period_info.number { - let mut new_entry = *last_element; - new_entry.unstake(amount, period_info.period_type); - new_entry.era = era; - Some(new_entry) - } else { - None - } - } else { - None - }; +/// Information about all of the dApps that got into tiers, and tier rewards +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] +#[scale_info(skip_type_params(MD, NT))] +pub struct DAppTierRewards, NT: Get> { + /// DApps and their corresponding tiers (or `None` if they have been claimed in the meantime) + pub dapps: BoundedVec, + /// Rewards for each tier. First entry refers to the first tier, and so on. + pub rewards: BoundedVec, + /// Period during which this struct was created. + #[codec(compact)] + pub period: PeriodNumber, +} - // 3rd step - push the new entries, if they exist. - if let Some(info) = second_last_era_info { - self.prune(); - self.0.try_push(info).map_err(|_| ())?; - } - if let Some(info) = last_era_info { - self.prune(); - self.0.try_push(info).map_err(|_| ())?; +impl, NT: Get> Default for DAppTierRewards { + fn default() -> Self { + Self { + dapps: BoundedVec::default(), + rewards: BoundedVec::default(), + period: 0, } + } +} - Ok(()) +impl, NT: Get> DAppTierRewards { + /// Attempt to construct `DAppTierRewards` struct. + /// If the provided arguments exceed the allowed capacity, return an error. + pub fn new( + dapps: Vec, + rewards: Vec, + period: PeriodNumber, + ) -> Result { + let dapps = BoundedVec::try_from(dapps).map_err(|_| ())?; + let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?; + Ok(Self { + dapps, + rewards, + period, + }) } - /// Used to remove past entries, in case vector is full. - fn prune(&mut self) { - // Prune the oldest entry if we have more than the limit - if self.0.len() == STAKING_SERIES_HISTORY as usize { - // TODO: this can be perhaps optimized so we prune entries which are very old. - // However, this makes the code more complex & more error prone. - // If kept like this, we always make sure we cover the history, and we never exceed it. - self.0.remove(0); + /// Consume reward for the specified dapp id, returning its amount and tier Id. + /// In case dapp isn't applicable for rewards, or they have already been consumed, returns `None`. + pub fn try_consume(&mut self, dapp_id: DAppId) -> Result<(Balance, TierId), DAppTierError> { + // Check if dApp Id exists. + let dapp_idx = self + .dapps + .binary_search_by(|entry| entry.dapp_id.cmp(&dapp_id)) + .map_err(|_| DAppTierError::NoDAppInTiers)?; + + match self.dapps.get_mut(dapp_idx) { + Some(dapp_tier) => { + if let Some(tier_id) = dapp_tier.tier_id.take() { + Ok(( + self.rewards + .get(tier_id as usize) + .map_or(Balance::zero(), |x| *x), + tier_id, + )) + } else { + Err(DAppTierError::RewardAlreadyClaimed) + } + } + // unreachable code, at this point it was proved that index exists + _ => Err(DAppTierError::InternalError), } } } + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DAppTierError { + /// Specified dApp Id doesn't exist in any tier. + NoDAppInTiers, + /// Reward has already been claimed for this dApp. + RewardAlreadyClaimed, + /// Internal, unexpected error occured. + InternalError, +} + +/////////////////////////////////////////////////////////////////////// +//////////// MOVE THIS TO SOME PRIMITIVES CRATE LATER //////////// +/////////////////////////////////////////////////////////////////////// + +/// Interface for fetching price of the native token. +/// +/// TODO: discussion about below +/// The assumption is that the underlying implementation will ensure +/// this price is averaged and/or weighted over a certain period of time. +/// Alternative is to provide e.g. number of blocks for which an approximately averaged value is needed, +/// and let the underlying implementation take care converting block range into value best represening it. +pub trait PriceProvider { + /// Get the price of the native token. + fn average_price() -> FixedU64; +} + +// TODO: however the implementation ends up looking, +// it should consider total staked amount when filling up the bonus pool. +// This is to ensure bonus rewards aren't too large in case there is little amount of staked funds. +pub trait RewardPoolProvider { + /// Get the reward pools for stakers and dApps. + /// + /// TODO: discussion about below + /// The assumption is that the underlying implementation keeps track of how often this is called. + /// E.g. let's assume it's supposed to be called at the end of each era. + /// In case era is forced, it will last shorter. If pallet is put into maintenance mode, era might last longer. + /// Reward should adjust to that accordingly. + /// Alternative is to provide number of blocks for which era lasted. + fn normal_reward_pools() -> (Balance, Balance); + + /// Get the bonus pool for stakers. + fn bonus_reward_pool() -> Balance; +} + +// TODO: these are experimental, don't review +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +pub struct ExperimentalContractStakeEntry { + #[codec(compact)] + pub dapp_id: DAppId, + #[codec(compact)] + pub voting: Balance, + #[codec(compact)] + pub build_and_earn: Balance, +} + +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] +#[scale_info(skip_type_params(MD, NT))] +pub struct ExperimentalContractStakeEntries, NT: Get> { + /// DApps and their corresponding tiers (or `None` if they have been claimed in the meantime) + pub dapps: BoundedVec, + /// Rewards for each tier. First entry refers to the first tier, and so on. + pub rewards: BoundedVec, + /// Period during which this struct was created. + #[codec(compact)] + pub period: PeriodNumber, +} + +// TODO: temp experimental type, don't review +pub type ContractEntriesFor = + ExperimentalContractStakeEntries, ::NumberOfTiers>; diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index 44012e1760..3ecbb31994 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -48,6 +48,7 @@ pallet-treasury = { workspace = true } pallet-utility = { workspace = true } pallet-vesting = { workspace = true } sp-api = { workspace = true } +sp-arithmetic = { workspace = true } sp-block-builder = { workspace = true } sp-consensus-aura = { workspace = true } sp-core = { workspace = true } @@ -70,6 +71,7 @@ pallet-block-reward = { workspace = true } pallet-chain-extension-dapps-staking = { workspace = true } pallet-chain-extension-xvm = { workspace = true } pallet-custom-signatures = { workspace = true } +pallet-dapp-staking-v3 = { workspace = true } pallet-dapps-staking = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } pallet-evm-precompile-dapps-staking = { workspace = true } @@ -117,6 +119,7 @@ std = [ "pallet-chain-extension-xvm/std", "pallet-custom-signatures/std", "pallet-dapps-staking/std", + "pallet-dapp-staking-v3/std", "pallet-base-fee/std", "pallet-ethereum/std", "pallet-evm/std", @@ -150,6 +153,7 @@ std = [ "sp-offchain/std", "sp-runtime/std", "sp-session/std", + "sp-arithmetic/std", "sp-std/std", "sp-transaction-pool/std", "sp-version/std", @@ -185,6 +189,7 @@ runtime-benchmarks = [ "pallet-ethereum-checked/runtime-benchmarks", "astar-primitives/runtime-benchmarks", "pallet-assets/runtime-benchmarks", + "pallet-dapp-staking-v3/runtime-benchmarks", ] try-runtime = [ "fp-self-contained/try-runtime", @@ -199,6 +204,7 @@ try-runtime = [ "pallet-contracts/try-runtime", "pallet-custom-signatures/try-runtime", "pallet-dapps-staking/try-runtime", + "pallet-dapp-staking-v3/try-runtime", "pallet-grandpa/try-runtime", "pallet-insecure-randomness-collective-flip/try-runtime", "pallet-sudo/try-runtime", diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 40c1e7e9a3..12ec1e4062 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -27,7 +27,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use frame_support::{ construct_runtime, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Currency, EitherOfDiverse, + AsEnsureOriginWithArg, ConstU128, ConstU16, ConstU32, ConstU64, Currency, EitherOfDiverse, EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, Nothing, OnFinalize, WithdrawReasons, }, weights::{ @@ -46,6 +46,7 @@ use pallet_evm_precompile_assets_erc20::AddressToAssetId; use pallet_grandpa::{fg_primitives, AuthorityList as GrandpaAuthorityList}; use parity_scale_codec::{Compact, Decode, Encode, MaxEncodedLen}; use sp_api::impl_runtime_apis; +use sp_arithmetic::fixed_point::FixedU64; use sp_core::{crypto::KeyTypeId, ConstBool, OpaqueMetadata, H160, H256, U256}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, @@ -439,6 +440,60 @@ impl> From<[u8; 32]> for SmartContract { } } +pub struct DummyPriceProvider; +impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { + fn average_price() -> FixedU64 { + FixedU64::from_rational(1, 10) + } +} + +pub struct DummyRewardPoolProvider; +impl pallet_dapp_staking_v3::RewardPoolProvider for DummyRewardPoolProvider { + fn normal_reward_pools() -> (Balance, Balance) { + ( + Balance::from(1_000_000_000_000 * AST), + Balance::from(1_000_000_000 * AST), + ) + } + fn bonus_reward_pool() -> Balance { + Balance::from(3_000_000 * AST) + } +} + +#[cfg(feature = "runtime-benchmarks")] +pub struct BenchmarkHelper(sp_std::marker::PhantomData); +#[cfg(feature = "runtime-benchmarks")] +impl pallet_dapp_staking_v3::BenchmarkHelper> + for BenchmarkHelper> +{ + fn get_smart_contract(id: u32) -> SmartContract { + SmartContract::Wasm(AccountId::from([id as u8; 32])) + } +} + +impl pallet_dapp_staking_v3::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type SmartContract = SmartContract; + type ManagerOrigin = frame_system::EnsureRoot; + type NativePriceProvider = DummyPriceProvider; + type RewardPoolProvider = DummyRewardPoolProvider; + type StandardEraLength = ConstU32<30>; // should be 1 minute per standard era + type StandardErasPerVotingPeriod = ConstU32<2>; + type StandardErasPerBuildAndEarnPeriod = ConstU32<10>; + type EraRewardSpanLength = ConstU32<8>; + type RewardRetentionInPeriods = ConstU32<2>; + type MaxNumberOfContracts = ConstU16<100>; + type MaxUnlockingChunks = ConstU32<5>; + type MinimumLockedAmount = ConstU128; + type UnlockingPeriod = ConstU32<2>; + type MaxNumberOfStakedContracts = ConstU32<3>; + type MinimumStakeAmount = ConstU128; + type NumberOfTiers = ConstU32<4>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = BenchmarkHelper>; +} + impl pallet_utility::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; @@ -1005,6 +1060,7 @@ construct_runtime!( Balances: pallet_balances, Vesting: pallet_vesting, DappsStaking: pallet_dapps_staking, + DappStaking: pallet_dapp_staking_v3, BlockReward: pallet_block_reward, TransactionPayment: pallet_transaction_payment, EVM: pallet_evm, @@ -1137,6 +1193,7 @@ mod benches { [pallet_dapps_staking, DappsStaking] [pallet_block_reward, BlockReward] [pallet_ethereum_checked, EthereumChecked] + [pallet_dapp_staking_v3, DappStaking] ); } From 2b456fbb29ed2f73f5a1ed6a34e5cda37b5328b3 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Tue, 14 Nov 2023 09:51:14 +0100 Subject: [PATCH 05/14] Fix for cleanup --- pallets/dapp-staking-v3/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 1a85e00348..9f48f21b96 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1492,6 +1492,7 @@ pub mod pallet { } }) .collect(); + let entries_to_delete = to_be_deleted.len(); // Remove all expired entries. for smart_contract in to_be_deleted { @@ -1501,6 +1502,7 @@ pub mod pallet { // Remove expired ledger stake entries, if needed. let threshold_period = Self::oldest_claimable_period(current_period); let mut ledger = Ledger::::get(&account); + ledger.contract_stake_count.saturating_reduce(entries_to_delete as u32); if ledger.maybe_cleanup_expired(threshold_period) { Self::update_ledger(&account, ledger); } From c3f35fde3960370b8ab104fc30b00c473eda3b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:36:27 +0100 Subject: [PATCH 06/14] dapp staking v3 - part 5 (#1087) * EraRewardSpan * Initial version of claim_staker_reward * Tests * Test utils for claim-staker * Bug fixes, improvements * Claim improvements & some tests * Refactoring in progress * Refactoring continued * Refactoring progress * Refactoring finished * Bonus rewards * Docs & some minor changes * Comments, tests, improved coverage * Tier params & config init solution * Tier reward calculation WIP * Tier assignemnt * Minor cleanup * Claim dapp rewards * Claim dapp reward tests * unstake from unregistered call * Extra traits * fixes * Extra calls * Refactoring * More refactoring, improvements, TODO solving * Local integration * Genesis config * Add forcing call * try runtime build fix * Minor changes * Minor * Formatting * Benchmarks INIT * Compiling benchmarks * Fix * dapp tier calculation benchmark * Measured tier assignment * Decending rewards in benchmarks * Series refactoring & partial tests * Comments, minor changes * Tests, improvements * More tests, some minor refactoring * Formatting * More benchmarks & experiments, refactoring * Readme, docs * Minor renaming, docs * More docs * More docs * Minor addition * Review comment fixes & changes * Minor change * Review comments * Update frontier to make CI pass * dApp staking v3 - part 5 * Make check work * Limit map size to improve benchmarks * Fix for cleanup * Minor fixes, more tests * More fixes & test * Minor changes * More tests * More tests, some clippy fixes * Finished with type tests for now * Log, remove some TODOs * Changes * Bug fix for unstake era, more tests * Formatting, extra test * Unregistered unstake, expired cleanup, test utils & tests * Cleanup tests, minor fixes * force tests * Remove TODOs * Tier assignment test WIP * Finish dapp tier assignment test, fix reward calculation bug * Some renaming, test utils for era change WIP * Fix minor issues, propagaste renaming * More checks * Finish on_init tests * Comments, docs, some benchmarks * Benchmark progress * More benchmarks * Even more benchmarks * All extrinsics benchmarked * Expand tests * Comment * Missed error test * Review comments --- Cargo.lock | 50 +- pallets/dapp-staking-v3/Cargo.toml | 4 + pallets/dapp-staking-v3/coverage.sh | 18 + pallets/dapp-staking-v3/src/benchmarking.rs | 840 ++++++++++- pallets/dapp-staking-v3/src/dsv3_weight.rs | 75 +- pallets/dapp-staking-v3/src/lib.rs | 349 +++-- pallets/dapp-staking-v3/src/test/mock.rs | 59 +- .../dapp-staking-v3/src/test/testing_utils.rs | 409 +++++- pallets/dapp-staking-v3/src/test/tests.rs | 802 ++++++++++- .../dapp-staking-v3/src/test/tests_types.rs | 1265 +++++++++++++++-- pallets/dapp-staking-v3/src/types.rs | 256 ++-- runtime/local/src/lib.rs | 8 +- 12 files changed, 3562 insertions(+), 573 deletions(-) create mode 100755 pallets/dapp-staking-v3/coverage.sh diff --git a/Cargo.lock b/Cargo.lock index fe18d417f9..f69aee8ac4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3066,7 +3066,7 @@ dependencies = [ [[package]] name = "fc-consensus" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "async-trait", "fp-consensus", @@ -3082,7 +3082,7 @@ dependencies = [ [[package]] name = "fc-db" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "async-trait", "fp-storage", @@ -3102,7 +3102,7 @@ dependencies = [ [[package]] name = "fc-mapping-sync" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fc-db", "fc-storage", @@ -3123,7 +3123,7 @@ dependencies = [ [[package]] name = "fc-rpc" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3173,7 +3173,7 @@ dependencies = [ [[package]] name = "fc-rpc-core" version = "1.1.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3186,7 +3186,7 @@ dependencies = [ [[package]] name = "fc-storage" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3338,7 +3338,7 @@ dependencies = [ [[package]] name = "fp-account" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "hex", "impl-serde", @@ -3357,7 +3357,7 @@ dependencies = [ [[package]] name = "fp-consensus" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "parity-scale-codec", @@ -3369,7 +3369,7 @@ dependencies = [ [[package]] name = "fp-ethereum" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3383,7 +3383,7 @@ dependencies = [ [[package]] name = "fp-evm" version = "3.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "evm", "frame-support", @@ -3398,7 +3398,7 @@ dependencies = [ [[package]] name = "fp-rpc" version = "3.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -3415,7 +3415,7 @@ dependencies = [ [[package]] name = "fp-self-contained" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "frame-support", "parity-scale-codec", @@ -3427,7 +3427,7 @@ dependencies = [ [[package]] name = "fp-storage" version = "2.0.0" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "parity-scale-codec", "serde", @@ -6794,7 +6794,7 @@ dependencies = [ [[package]] name = "pallet-base-fee" version = "1.0.0" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "frame-support", @@ -7112,10 +7112,12 @@ dependencies = [ name = "pallet-dapp-staking-v3" version = "0.0.1-alpha" dependencies = [ + "assert_matches", "astar-primitives", "frame-benchmarking", "frame-support", "frame-system", + "log", "num-traits", "pallet-balances", "parity-scale-codec", @@ -7226,7 +7228,7 @@ dependencies = [ [[package]] name = "pallet-ethereum" version = "4.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ethereum", "ethereum-types", @@ -7273,7 +7275,7 @@ dependencies = [ [[package]] name = "pallet-evm" version = "6.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "environmental", "evm", @@ -7298,7 +7300,7 @@ dependencies = [ [[package]] name = "pallet-evm-chain-id" version = "1.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "frame-support", "frame-system", @@ -7363,7 +7365,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-blake2" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", ] @@ -7371,7 +7373,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-bn128" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "sp-core", @@ -7406,7 +7408,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-dispatch" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "frame-support", @@ -7416,7 +7418,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-ed25519" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "ed25519-dalek", "fp-evm", @@ -7425,7 +7427,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-modexp" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "num", @@ -7434,7 +7436,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-sha3fips" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "tiny-keccak", @@ -7443,7 +7445,7 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-simple" version = "2.0.0-dev" -source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#718c42a273280d73b71b83ff9ed1fe498dcee8f4" +source = "git+https://github.com/AstarNetwork/frontier?branch=polkadot-v0.9.43#a5481542518ec420352d263adcb2f78835ac9bc2" dependencies = [ "fp-evm", "ripemd", diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml index 0de398ce18..c79755828d 100644 --- a/pallets/dapp-staking-v3/Cargo.toml +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true [dependencies] frame-support = { workspace = true } frame-system = { workspace = true } +log = { workspace = true } num-traits = { workspace = true } parity-scale-codec = { workspace = true } @@ -23,6 +24,7 @@ sp-std = { workspace = true } astar-primitives = { workspace = true } +assert_matches = { workspace = true, optional = true } frame-benchmarking = { workspace = true, optional = true } [dev-dependencies] @@ -32,6 +34,7 @@ pallet-balances = { workspace = true } default = ["std"] std = [ "serde", + "log/std", "parity-scale-codec/std", "scale-info/std", "num-traits/std", @@ -52,5 +55,6 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "astar-primitives/runtime-benchmarks", + "assert_matches", ] try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/dapp-staking-v3/coverage.sh b/pallets/dapp-staking-v3/coverage.sh new file mode 100755 index 0000000000..46710a672c --- /dev/null +++ b/pallets/dapp-staking-v3/coverage.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +targets=("protocol_state" "account_ledger" "dapp_info" "period_info" "era_info" \ + "stake_amount" "singular_staking_info" "contract_stake_amount" "era_reward_span" \ + "period_end_info" "era_stake_pair_iter" "tier_threshold" "tier_params" "tier_configuration" \ + "dapp_tier_rewards" ) + +for target in "${targets[@]}" +do + cargo tarpaulin -p pallet-dapp-staking-v3 -o=html --output-dir=./coverage/$target -- $target +done + +# Also need to check the coverage when only running extrinsic tests (disable type tests) + +# Also need similar approach to extrinsic testing, as above + + +# NOTE: this script will be deleted before the final release! \ No newline at end of file diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking.rs index e98fd1ce09..2b04874516 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking.rs @@ -24,7 +24,11 @@ use frame_benchmarking::v2::*; use frame_support::assert_ok; use frame_system::{Pallet as System, RawOrigin}; -// TODO: copy/paste from mock, make it more generic later +use ::assert_matches::assert_matches; + +// TODO: make benchmark utils file and move all these helper methods there to keep this file clean(er) + +// TODO2: non-extrinsic calls still need to be benchmarked. /// Run to the specified block number. /// Function assumes first block has been initialized. @@ -58,28 +62,28 @@ pub(crate) fn advance_to_next_era() { advance_to_era::(ActiveProtocolState::::get().era + 1); } -// /// Advance blocks until the specified period has been reached. -// /// -// /// Function has no effect if period is already passed. -// pub(crate) fn advance_to_period(period: PeriodNumber) { -// assert!(period >= ActiveProtocolState::::get().period_number()); -// while ActiveProtocolState::::get().period_number() < period { -// run_for_blocks::(One::one()); -// } -// } - -// /// Advance blocks until next period has been reached. -// pub(crate) fn advance_to_next_period() { -// advance_to_period::(ActiveProtocolState::::get().period_number() + 1); -// } - -// /// Advance blocks until next period type has been reached. -// pub(crate) fn advance_to_next_subperiod() { -// let subperiod = ActiveProtocolState::::get().subperiod(); -// while ActiveProtocolState::::get().subperiod() == subperiod { -// run_for_blocks::(One::one()); -// } -// } +/// Advance blocks until the specified period has been reached. +/// +/// Function has no effect if period is already passed. +pub(crate) fn advance_to_period(period: PeriodNumber) { + assert!(period >= ActiveProtocolState::::get().period_number()); + while ActiveProtocolState::::get().period_number() < period { + run_for_blocks::(One::one()); + } +} + +/// Advance blocks until next period has been reached. +pub(crate) fn advance_to_next_period() { + advance_to_period::(ActiveProtocolState::::get().period_number() + 1); +} + +/// Advance blocks until next period type has been reached. +pub(crate) fn advance_to_next_subperiod() { + let subperiod = ActiveProtocolState::::get().subperiod(); + while ActiveProtocolState::::get().subperiod() == subperiod { + run_for_blocks::(One::one()); + } +} // All our networks use 18 decimals for native currency so this should be fine. const UNIT: Balance = 1_000_000_000_000_000_000; @@ -87,11 +91,28 @@ const UNIT: Balance = 1_000_000_000_000_000_000; // Minimum amount that must be staked on a dApp to enter any tier const MIN_TIER_THRESHOLD: Balance = 10 * UNIT; -const NUMBER_OF_SLOTS: u16 = 100; +const NUMBER_OF_SLOTS: u32 = 100; +const SEED: u32 = 9000; + +/// Assert that the last event equals the provided one. +fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +// Return all dApp staking events from the event buffer. +fn dapp_staking_events() -> Vec> { + System::::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| ::RuntimeEvent::from(e).try_into().ok()) + .collect::>() +} + +// TODO: make it more generic per runtime? pub fn initial_config() { let era_length = T::StandardEraLength::get(); - let voting_period_length_in_eras = T::StandardErasPerVotingPeriod::get(); + let voting_period_length_in_eras = T::StandardErasPerVotingSubperiod::get(); // Init protocol state ActiveProtocolState::::put(ProtocolState { @@ -143,7 +164,7 @@ pub fn initial_config() { // Init tier config, based on the initial params let init_tier_config = TiersConfiguration:: { - number_of_slots: NUMBER_OF_SLOTS, + number_of_slots: NUMBER_OF_SLOTS.try_into().unwrap(), slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), reward_portion: tier_params.reward_portion.clone(), tier_thresholds: tier_params.tier_thresholds.clone(), @@ -165,6 +186,762 @@ fn max_number_of_contracts() -> u32 { mod benchmarks { use super::*; + #[benchmark] + fn maintenance_mode() { + initial_config::(); + + #[extrinsic_call] + _(RawOrigin::Root, true); + + assert_last_event::(Event::::MaintenanceMode { enabled: true }.into()); + } + + #[benchmark] + fn register() { + initial_config::(); + + let account: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + + #[extrinsic_call] + _(RawOrigin::Root, account.clone(), smart_contract.clone()); + + assert_last_event::( + Event::::DAppRegistered { + owner: account, + smart_contract, + dapp_id: 0, + } + .into(), + ); + } + + #[benchmark] + fn set_dapp_reward_beneficiary() { + initial_config::(); + + let owner: T::AccountId = whitelisted_caller(); + let beneficiary: Option = Some(account("beneficiary", 0, SEED)); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + #[extrinsic_call] + _( + RawOrigin::Signed(owner), + smart_contract.clone(), + beneficiary.clone(), + ); + + assert_last_event::( + Event::::DAppRewardDestinationUpdated { + smart_contract, + beneficiary, + } + .into(), + ); + } + + #[benchmark] + fn set_dapp_owner() { + initial_config::(); + + let init_owner: T::AccountId = whitelisted_caller(); + let new_owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + init_owner.clone().into(), + smart_contract.clone(), + )); + + #[extrinsic_call] + _( + RawOrigin::Signed(init_owner), + smart_contract.clone(), + new_owner.clone(), + ); + + assert_last_event::( + Event::::DAppOwnerChanged { + smart_contract, + new_owner, + } + .into(), + ); + } + + #[benchmark] + fn unregister() { + initial_config::(); + + let owner: T::AccountId = whitelisted_caller(); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + #[extrinsic_call] + _(RawOrigin::Root, smart_contract.clone()); + + assert_last_event::( + Event::::DAppUnregistered { + smart_contract, + era: ActiveProtocolState::::get().era, + } + .into(), + ); + } + + #[benchmark] + fn lock() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone()), amount); + + assert_last_event::( + Event::::Locked { + account: staker, + amount, + } + .into(), + ); + } + + #[benchmark] + fn unlock() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get() * 2; + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone()), 1); + + assert_last_event::( + Event::::Unlocking { + account: staker, + amount: 1, + } + .into(), + ); + } + + // TODO: maybe this is not needed. Compare it after running benchmarks to the 'not-full' unlock + #[benchmark] + fn full_unlock() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get() * 2; + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + #[extrinsic_call] + unlock(RawOrigin::Signed(staker.clone()), amount); + + assert_last_event::( + Event::::Unlocking { + account: staker, + amount, + } + .into(), + ); + } + + #[benchmark] + fn claim_unlocked(x: Linear<0, { T::MaxNumberOfStakedContracts::get() }>) { + // Prepare staker account and lock some amount + let staker: T::AccountId = whitelisted_caller(); + let amount = (T::MinimumStakeAmount::get() + 1) + * Into::::into(max_number_of_contracts::()) + + Into::::into(T::MaxUnlockingChunks::get()); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + // Move over to the build&earn subperiod to ensure 'non-loyal' staking. + // This is needed so we can achieve staker entry cleanup after claiming unlocked tokens. + advance_to_next_subperiod::(); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "Sanity check - we need to stake during build&earn for entries to be cleaned up in the next era." + ); + + // Register required number of contracts and have staker stake on them. + // This is needed to achieve the cleanup functionality. + for idx in 0..x { + let smart_contract = T::BenchmarkHelper::get_smart_contract(idx as u32); + let owner: T::AccountId = account("dapp_owner", idx.into(), SEED); + + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract, + T::MinimumStakeAmount::get() + 1, + )); + } + + // Unlock some amount - but we want to fill up the whole vector with chunks. + let unlock_amount = 1; + for _ in 0..T::MaxUnlockingChunks::get() { + assert_ok!(DappStaking::::unlock( + RawOrigin::Signed(staker.clone()).into(), + unlock_amount, + )); + run_for_blocks::(One::one()); + } + assert_eq!( + Ledger::::get(&staker).unlocking.len(), + T::MaxUnlockingChunks::get() as usize + ); + let unlock_amount = unlock_amount * Into::::into(T::MaxUnlockingChunks::get()); + + // Advance to next period to ensure the old stake entries are cleaned up. + advance_to_next_period::(); + + // Additionally, ensure enough blocks have passed so that the unlocking chunk can be claimed. + let unlock_block = Ledger::::get(&staker) + .unlocking + .last() + .expect("At least one entry must exist.") + .unlock_block; + run_to_block::(unlock_block); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone())); + + assert_last_event::( + Event::::ClaimedUnlocked { + account: staker, + amount: unlock_amount, + } + .into(), + ); + } + + #[benchmark] + fn relock_unlocking() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = + T::MinimumLockedAmount::get() * 2 + Into::::into(T::MaxUnlockingChunks::get()); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + // Unlock some amount - but we want to fill up the whole vector with chunks. + let unlock_amount = 1; + for _ in 0..T::MaxUnlockingChunks::get() { + assert_ok!(DappStaking::::unlock( + RawOrigin::Signed(staker.clone()).into(), + unlock_amount, + )); + run_for_blocks::(One::one()); + } + let unlock_amount = unlock_amount * Into::::into(T::MaxUnlockingChunks::get()); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone())); + + assert_last_event::( + Event::::Relock { + account: staker, + amount: unlock_amount, + } + .into(), + ); + } + + #[benchmark] + fn stake() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + #[extrinsic_call] + _( + RawOrigin::Signed(staker.clone()), + smart_contract.clone(), + amount, + ); + + assert_last_event::( + Event::::Stake { + account: staker, + smart_contract, + amount, + } + .into(), + ); + } + + #[benchmark] + fn unstake() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get() + 1; + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + + let unstake_amount = 1; + + #[extrinsic_call] + _( + RawOrigin::Signed(staker.clone()), + smart_contract.clone(), + unstake_amount, + ); + + assert_last_event::( + Event::::Unstake { + account: staker, + smart_contract, + amount: unstake_amount, + } + .into(), + ); + } + + #[benchmark] + fn claim_staker_rewards_past_period(x: Linear<1, { T::EraRewardSpanLength::get() }>) { + initial_config::(); + + // Prepare staker & register smart contract + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + // Lock some amount by the staker + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + // Advance to the era just before a new span entry is created. + // This ensures that when rewards are claimed, we'll be claiming from the new span. + // + // This is convenient because it allows us to control how many rewards are claimed. + advance_to_era::(T::EraRewardSpanLength::get() - 1); + + // Now ensure the expected amount of rewards are claimable. + advance_to_era::( + ActiveProtocolState::::get().era + T::EraRewardSpanLength::get() - x, + ); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + + // This ensures we claim from the past period. + advance_to_next_period::(); + + // For testing purposes + System::::reset_events(); + + #[extrinsic_call] + claim_staker_rewards(RawOrigin::Signed(staker.clone())); + + // No need to do precise check of values, but predetermiend amount of 'Reward' events is expected. + let dapp_staking_events = dapp_staking_events::(); + assert_eq!(dapp_staking_events.len(), x as usize); + dapp_staking_events.iter().for_each(|e| { + assert_matches!(e, Event::Reward { .. }); + }); + } + + #[benchmark] + fn claim_staker_rewards_ongoing_period(x: Linear<1, { T::EraRewardSpanLength::get() }>) { + initial_config::(); + + // Prepare staker & register smart contract + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + // Lock & stake some amount by the staker + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + // Advance to the era just before a new span entry is created. + // This ensures that when rewards are claimed, we'll be claiming from the new span. + // + // This is convenient because it allows us to control how many rewards are claimed. + advance_to_era::(T::EraRewardSpanLength::get() - 1); + + // Now ensure the expected amount of rewards are claimable. + advance_to_era::( + ActiveProtocolState::::get().era + T::EraRewardSpanLength::get() - x, + ); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + + // This ensures we move over the entire span. + advance_to_era::(T::EraRewardSpanLength::get() * 2); + + // For testing purposes + System::::reset_events(); + + #[extrinsic_call] + claim_staker_rewards(RawOrigin::Signed(staker.clone())); + + // No need to do precise check of values, but predetermiend amount of 'Reward' events is expected. + let dapp_staking_events = dapp_staking_events::(); + assert_eq!(dapp_staking_events.len(), x as usize); + dapp_staking_events.iter().for_each(|e| { + assert_matches!(e, Event::Reward { .. }); + }); + } + + #[benchmark] + fn claim_bonus_reward() { + initial_config::(); + + // Prepare staker & register smart contract + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + // Lock & stake some amount by the staker + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + + // Advance to the next period so we can claim the bonus reward. + advance_to_next_period::(); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone()), smart_contract.clone()); + + // No need to do precise check of values, but last event must be 'BonusReward'. + assert_matches!( + dapp_staking_events::().last(), + Some(Event::BonusReward { .. }) + ); + } + + #[benchmark] + fn claim_dapp_reward() { + initial_config::(); + + // Register a dApp & stake on it. + // This is the dApp for which we'll claim rewards for. + let owner: T::AccountId = whitelisted_caller(); + let smart_contract = T::BenchmarkHelper::get_smart_contract(0); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get() * 1000 * UNIT; + T::Currency::make_free_balance_be(&owner, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(owner.clone()).into(), + amount, + )); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(owner.clone()).into(), + smart_contract.clone(), + amount + )); + + // Register & stake up to max number of contracts. + // The reason is we want to have reward vector filled up to the capacity. + for idx in 1..T::MaxNumberOfContracts::get() { + let owner: T::AccountId = account("dapp_owner", idx.into(), SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(idx as u32); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let staker: T::AccountId = account("staker", idx.into(), SEED); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + } + + // This is a hacky part to ensure we accomodate max number of contracts. + TierConfig::::mutate(|config| { + let max_number_of_contracts: u16 = T::MaxNumberOfContracts::get().try_into().unwrap(); + config.number_of_slots = max_number_of_contracts; + config.slots_per_tier[0] = max_number_of_contracts; + config.slots_per_tier[1..].iter_mut().for_each(|x| *x = 0); + }); + + // Advance enough eras so dApp reward can be claimed. + advance_to_next_subperiod::(); + advance_to_next_era::(); + let claim_era = ActiveProtocolState::::get().era - 1; + + assert_eq!( + DAppTiers::::get(claim_era) + .expect("Must exist since it's from past build&earn era.") + .dapps + .len(), + T::MaxNumberOfContracts::get() as usize, + "Sanity check to ensure we have filled up the vector completely." + ); + + #[extrinsic_call] + _( + RawOrigin::Signed(owner.clone()), + smart_contract.clone(), + claim_era, + ); + + // No need to do precise check of values, but last event must be 'DAppReward'. + assert_matches!( + dapp_staking_events::().last(), + Some(Event::DAppReward { .. }) + ); + } + + #[benchmark] + fn unstake_from_unregistered() { + initial_config::(); + + let staker: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("dapp_owner", 0, SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(1); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + let amount = T::MinimumLockedAmount::get(); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + amount + )); + + assert_ok!(DappStaking::::unregister( + RawOrigin::Root.into(), + smart_contract.clone(), + )); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone()), smart_contract.clone()); + + assert_last_event::( + Event::::UnstakeFromUnregistered { + account: staker, + smart_contract, + amount, + } + .into(), + ); + } + + #[benchmark] + fn cleanup_expired_entries(x: Linear<1, { T::MaxNumberOfStakedContracts::get() }>) { + initial_config::(); + + // Move over to the build&earn subperiod to ensure 'non-loyal' staking. + advance_to_next_subperiod::(); + + // Prepare staker & lock some amount + let staker: T::AccountId = whitelisted_caller(); + let amount = T::MinimumLockedAmount::get() + * Into::::into(T::MaxNumberOfStakedContracts::get()); + T::Currency::make_free_balance_be(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + // Register dApps up the the limit + for idx in 0..x { + let owner: T::AccountId = account("dapp_owner", idx.into(), SEED); + let smart_contract = T::BenchmarkHelper::get_smart_contract(idx as u32); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + owner.clone().into(), + smart_contract.clone(), + )); + + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract.clone(), + T::MinimumStakeAmount::get(), + )); + } + + // Move over to the next period, marking the entries as expired since they don't have the loyalty flag. + advance_to_next_period::(); + + #[extrinsic_call] + _(RawOrigin::Signed(staker.clone())); + + assert_last_event::( + Event::::ExpiredEntriesRemoved { + account: staker, + count: x.try_into().unwrap(), + } + .into(), + ); + } + + #[benchmark] + fn force() { + initial_config::(); + + let forcing_type = ForcingType::Subperiod; + + #[extrinsic_call] + _(RawOrigin::Root, forcing_type); + + assert_last_event::(Event::::Force { forcing_type }.into()); + } + + // TODO: investigate why the PoV size is so large here, evne after removing read of `IntegratedDApps` storage. + // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs + // UPDATE: after some investigation, it seems that PoV size benchmarks are very unprecise + // - the worst case measured is usually very far off the actual value that is consumed on chain. + // There's an ongoing item to improve it (mentioned on roundtable meeting). #[benchmark] fn dapp_tier_assignment(x: Linear<0, { max_number_of_contracts::() }>) { // Prepare init config (protocol state, tier params & config, etc.) @@ -217,17 +994,6 @@ mod benchmarks { } } - #[benchmark] - fn experimental_read() { - // Prepare init config (protocol state, tier params & config, etc.) - initial_config::(); - - #[block] - { - let _ = ExperimentalContractEntries::::get(10); - } - } - impl_benchmark_test_suite!( Pallet, crate::benchmarking::tests::new_test_ext(), diff --git a/pallets/dapp-staking-v3/src/dsv3_weight.rs b/pallets/dapp-staking-v3/src/dsv3_weight.rs index 1dca8a56ce..c1f88e9f1a 100644 --- a/pallets/dapp-staking-v3/src/dsv3_weight.rs +++ b/pallets/dapp-staking-v3/src/dsv3_weight.rs @@ -20,7 +20,7 @@ //! Autogenerated weights for pallet_dapp_staking_v3 //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-11-02, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-11-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `Dinos-MBP`, CPU: `` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 @@ -32,12 +32,12 @@ // --chain=dev // --steps=50 // --repeat=20 -// --pallet=pallet_dapp_staking_v3 +// --pallet=pallet_dapp_staking-v3 // --extrinsic=* // --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 -// --output=dsv3_weight.rs +// --output=dapp_staking_v3.rs // --template=./scripts/templates/weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -50,76 +50,51 @@ use core::marker::PhantomData; /// Weight functions needed for pallet_dapp_staking_v3. pub trait WeightInfo { fn dapp_tier_assignment(x: u32, ) -> Weight; - fn experimental_read() -> Weight; } /// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { - /// Storage: DappStaking TierConfig (r:1 w:0) - /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) - /// Storage: DappStaking IntegratedDApps (r:101 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) - /// Storage: DappStaking ContractStake (r:100 w:0) - /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(130), added: 2605, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:101 w:0) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// The range of component `x` is `[0, 100]`. fn dapp_tier_assignment(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `836 + x * (169 ±0)` - // Estimated: `3586 + x * (2605 ±0)` + // Measured: `449 + x * (33 ±0)` + // Estimated: `3063 + x * (2073 ±0)` // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(12_879_631, 3586) - // Standard Error: 18_480 - .saturating_add(Weight::from_parts(7_315_677, 0).saturating_mul(x.into())) + Weight::from_parts(16_776_512, 3063) + // Standard Error: 3_400 + .saturating_add(Weight::from_parts(2_636_298, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(3_u64)) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2605).saturating_mul(x.into())) - } - /// Storage: DappStaking ExperimentalContractEntries (r:1 w:0) - /// Proof: DappStaking ExperimentalContractEntries (max_values: None, max_size: Some(3483), added: 5958, mode: MaxEncodedLen) - fn experimental_read() -> Weight { - // Proof Size summary in bytes: - // Measured: `224` - // Estimated: `6948` - // Minimum execution time: 5_000_000 picoseconds. - Weight::from_parts(5_000_000, 6948) - .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) } } // For backwards compatibility and tests impl WeightInfo for () { - /// Storage: DappStaking TierConfig (r:1 w:0) - /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) - /// Storage: DappStaking IntegratedDApps (r:101 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: None, max_size: Some(121), added: 2596, mode: MaxEncodedLen) - /// Storage: DappStaking ContractStake (r:100 w:0) - /// Proof: DappStaking ContractStake (max_values: None, max_size: Some(130), added: 2605, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:101 w:0) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// The range of component `x` is `[0, 100]`. fn dapp_tier_assignment(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `836 + x * (169 ±0)` - // Estimated: `3586 + x * (2605 ±0)` + // Measured: `449 + x * (33 ±0)` + // Estimated: `3063 + x * (2073 ±0)` // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(12_879_631, 3586) - // Standard Error: 18_480 - .saturating_add(Weight::from_parts(7_315_677, 0).saturating_mul(x.into())) + Weight::from_parts(16_776_512, 3063) + // Standard Error: 3_400 + .saturating_add(Weight::from_parts(2_636_298, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(3_u64)) - .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2605).saturating_mul(x.into())) - } - /// Storage: DappStaking ExperimentalContractEntries (r:1 w:0) - /// Proof: DappStaking ExperimentalContractEntries (max_values: None, max_size: Some(3483), added: 5958, mode: MaxEncodedLen) - fn experimental_read() -> Weight { - // Proof Size summary in bytes: - // Measured: `224` - // Estimated: `6948` - // Minimum execution time: 5_000_000 picoseconds. - Weight::from_parts(5_000_000, 6948) - .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) } } diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 9f48f21b96..e282f0959f 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -47,7 +47,7 @@ use frame_support::{ }; use frame_system::pallet_prelude::*; use sp_runtime::{ - traits::{BadOrigin, One, Saturating, Zero}, + traits::{BadOrigin, One, Saturating, UniqueSaturatedInto, Zero}, Perbill, Permill, }; pub use sp_std::vec::Vec; @@ -68,9 +68,10 @@ pub use types::{PriceProvider, RewardPoolProvider, TierThreshold}; mod dsv3_weight; +// Lock identifier for the dApp staking pallet const STAKING_ID: LockIdentifier = *b"dapstake"; -// TODO: add tracing! +const LOG_TARGET: &str = "dapp-staking"; #[frame_support::pallet] pub mod pallet { @@ -91,10 +92,14 @@ pub mod pallet { #[pallet::config] pub trait Config: frame_system::Config { /// The overarching event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type RuntimeEvent: From> + + IsType<::RuntimeEvent> + + TryInto>; /// Currency used for staking. /// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :) + // https://github.com/paritytech/substrate/pull/12951/ + // Look at nomination pools implementation for reference! type Currency: LockableCurrency< Self::AccountId, Moment = Self::BlockNumber, @@ -117,16 +122,16 @@ pub mod pallet { #[pallet::constant] type StandardEraLength: Get; - /// Length of the `Voting` period in standard eras. - /// Although `Voting` period only consumes one 'era', we still measure its length in standard eras + /// Length of the `Voting` subperiod in standard eras. + /// Although `Voting` subperiod only consumes one 'era', we still measure its length in standard eras /// for the sake of simplicity & consistency. #[pallet::constant] - type StandardErasPerVotingPeriod: Get; + type StandardErasPerVotingSubperiod: Get; - /// Length of the `Build&Earn` period in standard eras. - /// Each `Build&Earn` period consists of one or more distinct standard eras. + /// Length of the `Build&Earn` subperiod in standard eras. + /// Each `Build&Earn` subperiod consists of one or more distinct standard eras. #[pallet::constant] - type StandardErasPerBuildAndEarnPeriod: Get; + type StandardErasPerBuildAndEarnSubperiod: Get; /// Maximum length of a single era reward span length entry. #[pallet::constant] @@ -139,7 +144,7 @@ pub mod pallet { /// Maximum number of contracts that can be integrated into dApp staking at once. #[pallet::constant] - type MaxNumberOfContracts: Get; + type MaxNumberOfContracts: Get; /// Maximum number of unlocking chunks that can exist per account at a time. #[pallet::constant] @@ -174,10 +179,12 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(crate) fn deposit_event)] pub enum Event { + /// Maintenance mode has been either enabled or disabled. + MaintenanceMode { enabled: bool }, /// New era has started. NewEra { era: EraNumber }, - /// New period has started. - NewPeriod { + /// New subperiod has started. + NewSubperiod { subperiod: Subperiod, number: PeriodNumber, }, @@ -240,12 +247,14 @@ pub mod pallet { era: EraNumber, amount: Balance, }, + /// Bonus reward has been paid out to a loyal staker. BonusReward { account: T::AccountId, smart_contract: T::SmartContract, period: PeriodNumber, amount: Balance, }, + /// dApp reward has been paid out to a beneficiary. DAppReward { beneficiary: T::AccountId, smart_contract: T::SmartContract, @@ -253,11 +262,16 @@ pub mod pallet { era: EraNumber, amount: Balance, }, + /// Account has unstaked funds from an unregistered smart contract UnstakeFromUnregistered { account: T::AccountId, smart_contract: T::SmartContract, amount: Balance, }, + /// Some expired stake entries have been removed from storage. + ExpiredEntriesRemoved { account: T::AccountId, count: u16 }, + /// Privileged origin has forced a new era and possibly a subperiod to start from next block. + Force { forcing_type: ForcingType }, } #[pallet::error] @@ -291,8 +305,8 @@ pub mod pallet { NoUnlockingChunks, /// The amount being staked is too large compared to what's available for staking. UnavailableStakeFunds, - /// There are unclaimed rewards remaining from past periods. They should be claimed before staking again. - UnclaimedRewardsFromPastPeriods, + /// There are unclaimed rewards remaining from past eras or periods. They should be claimed before attempting any stake modification again. + UnclaimedRewards, /// An unexpected error occured while trying to stake. InternalStakeError, /// Total staked amount on contract is below the minimum required value. @@ -330,6 +344,8 @@ pub mod pallet { ContractStillActive, /// There are too many contract stake entries for the account. This can be cleaned up by either unstaking or cleaning expired entries. TooManyStakedContracts, + /// There are no expired entries to cleanup for the account. + NoExpiredEntries, } /// General information about dApp staking protocol state. @@ -341,15 +357,14 @@ pub mod pallet { #[pallet::storage] pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; - // TODO: where to track TierLabels? E.g. a label to bootstrap a dApp into a specific tier. /// Map of all dApps integrated into dApp staking protocol. #[pallet::storage] pub type IntegratedDApps = CountedStorageMap< - _, - Blake2_128Concat, - T::SmartContract, - DAppInfo, - OptionQuery, + Hasher = Blake2_128Concat, + Key = T::SmartContract, + Value = DAppInfo, + QueryKind = OptionQuery, + MaxValues = ConstU32<{ DAppId::MAX as u32 }>, >; /// General locked/staked information for each account. @@ -371,8 +386,13 @@ pub mod pallet { /// Information about how much has been staked on a smart contract in some era or period. #[pallet::storage] - pub type ContractStake = - StorageMap<_, Blake2_128Concat, T::SmartContract, ContractStakeAmount, ValueQuery>; + pub type ContractStake = StorageMap< + Hasher = Twox64Concat, + Key = DAppId, + Value = ContractStakeAmount, + QueryKind = ValueQuery, + MaxValues = ConstU32<{ DAppId::MAX as u32 }>, + >; /// General information about the current era. #[pallet::storage] @@ -416,11 +436,6 @@ pub mod pallet { pub type DAppTiers = StorageMap<_, Twox64Concat, EraNumber, DAppTierRewardsFor, OptionQuery>; - // TODO: this is experimental, please don't review - #[pallet::storage] - pub type ExperimentalContractEntries = - StorageMap<_, Twox64Concat, EraNumber, ContractEntriesFor, OptionQuery>; - #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { @@ -496,8 +511,6 @@ pub mod pallet { fn on_initialize(now: BlockNumberFor) -> Weight { let mut protocol_state = ActiveProtocolState::::get(); - // TODO: maybe do lazy history cleanup in this function? - // We should not modify pallet storage while in maintenance mode. // This is a safety measure, since maintenance mode is expected to be // enabled in case some misbehavior or corrupted storage is detected. @@ -510,13 +523,15 @@ pub mod pallet { return T::DbWeight::get().reads(1); } + // At this point it's clear that an era change will happen let mut era_info = CurrentEraInfo::::get(); let current_era = protocol_state.era; let next_era = current_era.saturating_add(1); let (maybe_period_event, era_reward) = match protocol_state.subperiod() { + // Voting subperiod only lasts for one 'prolonged' era Subperiod::Voting => { - // For the sake of consistency, we put zero reward into storage + // For the sake of consistency, we put zero reward into storage. There are no rewards for the voting subperiod. let era_reward = EraReward { staker_reward_pool: Balance::zero(), staked: era_info.total_staked_amount(), @@ -524,7 +539,7 @@ pub mod pallet { }; let subperiod_end_era = - next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); + next_era.saturating_add(T::StandardErasPerBuildAndEarnSubperiod::get()); let build_and_earn_start_block = now.saturating_add(T::StandardEraLength::get()); protocol_state @@ -537,7 +552,7 @@ pub mod pallet { TierConfig::::put(next_tier_config); ( - Some(Event::::NewPeriod { + Some(Event::::NewSubperiod { subperiod: protocol_state.subperiod(), number: protocol_state.period_number(), }), @@ -593,7 +608,7 @@ pub mod pallet { NextTierConfig::::put(new_tier_config); ( - Some(Event::::NewPeriod { + Some(Event::::NewSubperiod { subperiod: protocol_state.subperiod(), number: protocol_state.period_number(), }), @@ -611,7 +626,6 @@ pub mod pallet { }; // Update storage items - protocol_state.era = next_era; ActiveProtocolState::::put(protocol_state); @@ -619,8 +633,14 @@ pub mod pallet { let era_span_index = Self::era_reward_span_index(current_era); let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); - // TODO: Error "cannot" happen here. Log an error if it does though. - let _ = span.push(current_era, era_reward); + if let Err(_) = span.push(current_era, era_reward) { + // This must never happen but we log the error just in case. + log::error!( + target: LOG_TARGET, + "Failed to push era {} into the era reward span.", + current_era + ); + } EraRewards::::insert(&era_span_index, span); Self::deposit_event(Event::::NewEra { era: next_era }); @@ -642,12 +662,15 @@ pub mod pallet { pub fn maintenance_mode(origin: OriginFor, enabled: bool) -> DispatchResult { T::ManagerOrigin::ensure_origin(origin)?; ActiveProtocolState::::mutate(|state| state.maintenance = enabled); + + Self::deposit_event(Event::::MaintenanceMode { enabled }); Ok(()) } /// Used to register a new contract for dApp staking. /// /// If successful, smart contract will be assigned a simple, unique numerical identifier. + /// Owner is set to be initial beneficiary & manager of the dApp. #[pallet::call_index(1)] #[pallet::weight(Weight::zero())] pub fn register( @@ -679,6 +702,7 @@ pub mod pallet { id: dapp_id, state: DAppState::Registered, reward_destination: None, + tier_label: None, }, ); @@ -697,6 +721,7 @@ pub mod pallet { /// /// Caller has to be dApp owner. /// If set to `None`, rewards will be deposited to the dApp owner. + /// After this call, all existing & future rewards will be paid out to the beneficiary. #[pallet::call_index(2)] #[pallet::weight(Weight::zero())] pub fn set_dapp_reward_beneficiary( @@ -787,25 +812,18 @@ pub mod pallet { let current_era = ActiveProtocolState::::get().era; - IntegratedDApps::::try_mutate( - &smart_contract, - |maybe_dapp_info| -> DispatchResult { - let dapp_info = maybe_dapp_info - .as_mut() - .ok_or(Error::::ContractNotFound)?; - - ensure!( - dapp_info.state == DAppState::Registered, - Error::::NotOperatedDApp - ); + let mut dapp_info = + IntegratedDApps::::get(&smart_contract).ok_or(Error::::ContractNotFound)?; - dapp_info.state = DAppState::Unregistered(current_era); + ensure!( + dapp_info.state == DAppState::Registered, + Error::::NotOperatedDApp + ); - Ok(()) - }, - )?; + ContractStake::::remove(&dapp_info.id); - ContractStake::::remove(&smart_contract); + dapp_info.state = DAppState::Unregistered(current_era); + IntegratedDApps::::insert(&smart_contract, dapp_info); Self::deposit_event(Event::::DAppUnregistered { smart_contract, @@ -819,6 +837,8 @@ pub mod pallet { /// /// In case caller account doesn't have sufficient balance to cover the specified amount, everything is locked. /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount. + /// + /// Locked amount can immediately be used for staking. #[pallet::call_index(5)] #[pallet::weight(Weight::zero())] pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { @@ -932,9 +952,6 @@ pub mod pallet { 0 }; - // TODO: discussion point - this will "kill" users ability to withdraw past rewards. - // This can be handled by the frontend though. - Self::update_ledger(&account, ledger); CurrentEraInfo::::mutate(|era_info| { era_info.unlocking_removed(amount); @@ -975,9 +992,13 @@ pub mod pallet { } /// Stake the specified amount on a smart contract. - /// The `amount` specified **must** be available for staking and meet the required minimum, otherwise the call will fail. + /// The precise `amount` specified **must** be available for staking. + /// The total amount staked on a dApp must be greater than the minimum required value. /// - /// Depending on the period type, appropriate stake amount will be updated. + /// Depending on the period type, appropriate stake amount will be updated. During `Voting` subperiod, `voting` stake amount is updated, + /// and same for `Build&Earn` subperiod. + /// + /// Staked amount is only eligible for rewards from the next era onwards. #[pallet::call_index(9)] #[pallet::weight(Weight::zero())] pub fn stake( @@ -990,38 +1011,38 @@ pub mod pallet { ensure!(amount > 0, Error::::ZeroAmount); - ensure!( - Self::is_active(&smart_contract), - Error::::NotOperatedDApp - ); + let dapp_info = + IntegratedDApps::::get(&smart_contract).ok_or(Error::::NotOperatedDApp)?; + ensure!(dapp_info.is_active(), Error::::NotOperatedDApp); let protocol_state = ActiveProtocolState::::get(); - let stake_era = protocol_state.era; + let current_era = protocol_state.era; ensure!( !protocol_state .period_info - .is_next_period(stake_era.saturating_add(1)), + .is_next_period(current_era.saturating_add(1)), Error::::PeriodEndsInNextEra ); let mut ledger = Ledger::::get(&account); - // TODO: suggestion is to change this a bit so we clean up ledger if rewards have expired + // In case old stake rewards are unclaimed & have expired, clean them up. + let threshold_period = Self::oldest_claimable_period(protocol_state.period_number()); + let _ignore = ledger.maybe_cleanup_expired(threshold_period); + // 1. // Increase stake amount for the next era & current period in staker's ledger ledger - .add_stake_amount(amount, stake_era, protocol_state.period_info) + .add_stake_amount(amount, current_era, protocol_state.period_info) .map_err(|err| match err { AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { - Error::::UnclaimedRewardsFromPastPeriods + Error::::UnclaimedRewards } AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, // Defensive check, should never happen _ => Error::::InternalStakeError, })?; - // TODO: also change this to check if rewards have expired - // 2. // Update `StakerInfo` storage with the new stake amount on the specified contract. // @@ -1040,12 +1061,15 @@ pub mod pallet { { (staking_info, false) } - // Entry exists but period doesn't match. Either reward should be claimed or cleaned up. - Some(_) => { - return Err(Error::::UnclaimedRewardsFromPastPeriods.into()); + // Entry exists but period doesn't match. Bonus reward might still be claimable. + Some(staking_info) + if staking_info.period_number() >= threshold_period + && staking_info.is_loyal() => + { + return Err(Error::::UnclaimedRewards.into()); } - // No entry exists - None => ( + // No valid entry exists + _ => ( SingularStakingInfo::new( protocol_state.period_number(), protocol_state.subperiod(), @@ -1053,7 +1077,7 @@ pub mod pallet { true, ), }; - new_staking_info.stake(amount, protocol_state.subperiod()); + new_staking_info.stake(amount, current_era, protocol_state.subperiod()); ensure!( new_staking_info.total_staked_amount() >= T::MinimumStakeAmount::get(), Error::::InsufficientStakeAmount @@ -1069,8 +1093,8 @@ pub mod pallet { // 3. // Update `ContractStake` storage with the new stake amount on the specified contract. - let mut contract_stake_info = ContractStake::::get(&smart_contract); - contract_stake_info.stake(amount, protocol_state.period_info, stake_era); + let mut contract_stake_info = ContractStake::::get(&dapp_info.id); + contract_stake_info.stake(amount, protocol_state.period_info, current_era); // 4. // Update total staked amount for the next era. @@ -1082,7 +1106,7 @@ pub mod pallet { // Update remaining storage entries Self::update_ledger(&account, ledger); StakerInfo::::insert(&account, &smart_contract, new_staking_info); - ContractStake::::insert(&smart_contract, contract_stake_info); + ContractStake::::insert(&dapp_info.id, contract_stake_info); Self::deposit_event(Event::::Stake { account, @@ -1096,7 +1120,12 @@ pub mod pallet { /// Unstake the specified amount from a smart contract. /// The `amount` specified **must** not exceed what's staked, otherwise the call will fail. /// + /// If unstaking the specified `amount` would take staker below the minimum stake threshold, everything is unstaked. + /// /// Depending on the period type, appropriate stake amount will be updated. + /// In case amount is unstaked during `Voting` subperiod, the `voting` amount is reduced. + /// In case amount is unstaked during `Build&Earn` subperiod, first the `build_and_earn` is reduced, + /// and any spillover is subtracted from the `voting` amount. #[pallet::call_index(10)] #[pallet::weight(Weight::zero())] pub fn unstake( @@ -1109,13 +1138,12 @@ pub mod pallet { ensure!(amount > 0, Error::::ZeroAmount); - ensure!( - Self::is_active(&smart_contract), - Error::::NotOperatedDApp - ); + let dapp_info = + IntegratedDApps::::get(&smart_contract).ok_or(Error::::NotOperatedDApp)?; + ensure!(dapp_info.is_active(), Error::::NotOperatedDApp); let protocol_state = ActiveProtocolState::::get(); - let unstake_era = protocol_state.era; + let current_era = protocol_state.era; let mut ledger = Ledger::::get(&account); @@ -1142,7 +1170,7 @@ pub mod pallet { amount }; - staking_info.unstake(amount, protocol_state.subperiod()); + staking_info.unstake(amount, current_era, protocol_state.subperiod()); (staking_info, amount) } None => { @@ -1153,11 +1181,11 @@ pub mod pallet { // 2. // Reduce stake amount ledger - .unstake_amount(amount, unstake_era, protocol_state.period_info) + .unstake_amount(amount, current_era, protocol_state.period_info) .map_err(|err| match err { // These are all defensive checks, which should never happen since we already checked them above. AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { - Error::::UnclaimedRewardsFromPastPeriods + Error::::UnclaimedRewards } AccountLedgerError::UnstakeAmountLargerThanStake => { Error::::UnstakeAmountTooLarge @@ -1167,8 +1195,8 @@ pub mod pallet { // 3. // Update `ContractStake` storage with the reduced stake amount on the specified contract. - let mut contract_stake_info = ContractStake::::get(&smart_contract); - contract_stake_info.unstake(amount, protocol_state.period_info, unstake_era); + let mut contract_stake_info = ContractStake::::get(&dapp_info.id); + contract_stake_info.unstake(amount, protocol_state.period_info, current_era); // 4. // Update total staked amount for the next era. @@ -1178,7 +1206,7 @@ pub mod pallet { // 5. // Update remaining storage entries - ContractStake::::insert(&smart_contract, contract_stake_info); + ContractStake::::insert(&dapp_info.id, contract_stake_info); if new_staking_info.is_empty() { ledger.contract_stake_count.saturating_dec(); @@ -1199,8 +1227,7 @@ pub mod pallet { } /// Claims some staker rewards, if user has any. - /// In the case of a successfull call, at least one era will be claimed, with the possibility of multiple claims happening - /// if appropriate entries exist in account's ledger. + /// In the case of a successfull call, at least one era will be claimed, with the possibility of multiple claims happening. #[pallet::call_index(11)] #[pallet::weight(Weight::zero())] pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { @@ -1379,7 +1406,7 @@ pub mod pallet { let (amount, tier_id) = dapp_tiers - .try_consume(dapp_info.id) + .try_claim(dapp_info.id) .map_err(|error| match error { DAppTierError::NoDAppInTiers => Error::::NoClaimableRewards, DAppTierError::RewardAlreadyClaimed => Error::::DAppRewardAlreadyClaimed, @@ -1406,13 +1433,13 @@ pub mod pallet { } /// Used to unstake funds from a contract that was unregistered after an account staked on it. + /// This is required if staker wants to re-stake these funds on another active contract during the ongoing period. #[pallet::call_index(14)] #[pallet::weight(Weight::zero())] pub fn unstake_from_unregistered( origin: OriginFor, smart_contract: T::SmartContract, ) -> DispatchResult { - // TODO: tests are missing but will be added later. Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -1422,7 +1449,7 @@ pub mod pallet { ); let protocol_state = ActiveProtocolState::::get(); - let unstake_era = protocol_state.era; + let current_era = protocol_state.era; // Extract total staked amount on the specified unregistered contract let amount = match StakerInfo::::get(&account, &smart_contract) { @@ -1442,21 +1469,25 @@ pub mod pallet { // Reduce stake amount in ledger let mut ledger = Ledger::::get(&account); ledger - .unstake_amount(amount, unstake_era, protocol_state.period_info) + .unstake_amount(amount, current_era, protocol_state.period_info) .map_err(|err| match err { - // These are all defensive checks, which should never happen since we already checked them above. + // These are all defensive checks, which should never fail since we already checked them above. AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { - Error::::UnclaimedRewardsFromPastPeriods + Error::::UnclaimedRewards } _ => Error::::InternalUnstakeError, })?; // Update total staked amount for the next era. // This means 'fake' stake total amount has been kept until now, even though contract was unregistered. + // Although strange, it's been requested to keep it like this from the team. CurrentEraInfo::::mutate(|era_info| { era_info.unstake_amount(amount, protocol_state.subperiod()); }); + // TODO: HOWEVER, we should not pay out bonus rewards for such contracts. + // Seems wrong because it serves as discentive for unstaking & moving over to a new contract. + // Update remaining storage entries Self::update_ledger(&account, ledger); StakerInfo::::remove(&account, &smart_contract); @@ -1470,22 +1501,28 @@ pub mod pallet { Ok(()) } - // TODO: an alternative to this could would be to allow `unstake` call to cleanup old entries, however that means more complexity in that call - /// Used to unstake funds from a contract that was unregistered after an account staked on it. + /// Cleanup expired stake entries for the contract. + /// + /// Entry is considered to be expired if: + /// 1. It's from a past period & the account wasn't a loyal staker, meaning there's no claimable bonus reward. + /// 2. It's from a period older than the oldest claimable period, regardless whether the account was loyal or not. #[pallet::call_index(15)] #[pallet::weight(Weight::zero())] pub fn cleanup_expired_entries(origin: OriginFor) -> DispatchResult { - // TODO: tests are missing but will be added later. Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; let protocol_state = ActiveProtocolState::::get(); let current_period = protocol_state.period_number(); + let threshold_period = Self::oldest_claimable_period(current_period); - // Find all entries which have expired. This is bounded by max allowed number of entries. + // Find all entries which are from past periods & don't have claimable bonus rewards. + // This is bounded by max allowed number of stake entries per account. let to_be_deleted: Vec = StakerInfo::::iter_prefix(&account) .filter_map(|(smart_contract, stake_info)| { - if stake_info.period_number() < current_period { + if stake_info.period_number() < current_period && !stake_info.is_loyal() + || stake_info.period_number() < threshold_period + { Some(smart_contract) } else { None @@ -1494,18 +1531,25 @@ pub mod pallet { .collect(); let entries_to_delete = to_be_deleted.len(); + ensure!(!entries_to_delete.is_zero(), Error::::NoExpiredEntries); + // Remove all expired entries. for smart_contract in to_be_deleted { StakerInfo::::remove(&account, &smart_contract); } - // Remove expired ledger stake entries, if needed. - let threshold_period = Self::oldest_claimable_period(current_period); + // Remove expired stake entries from the ledger. let mut ledger = Ledger::::get(&account); - ledger.contract_stake_count.saturating_reduce(entries_to_delete as u32); - if ledger.maybe_cleanup_expired(threshold_period) { - Self::update_ledger(&account, ledger); - } + ledger + .contract_stake_count + .saturating_reduce(entries_to_delete.unique_saturated_into()); + ledger.maybe_cleanup_expired(threshold_period); // Not necessary but we do it for the sake of consistency + Self::update_ledger(&account, ledger); + + Self::deposit_event(Event::::ExpiredEntriesRemoved { + account, + count: entries_to_delete.unique_saturated_into(), + }); Ok(()) } @@ -1519,8 +1563,7 @@ pub mod pallet { /// Can only be called by manager origin. #[pallet::call_index(16)] #[pallet::weight(Weight::zero())] - pub fn force(origin: OriginFor, force_type: ForcingType) -> DispatchResult { - // TODO: tests are missing but will be added later. + pub fn force(origin: OriginFor, forcing_type: ForcingType) -> DispatchResult { Self::ensure_pallet_enabled()?; T::ManagerOrigin::ensure_origin(origin)?; @@ -1529,7 +1572,7 @@ pub mod pallet { let current_block = frame_system::Pallet::::block_number(); state.next_era_start = current_block.saturating_add(One::one()); - match force_type { + match forcing_type { ForcingType::Era => (), ForcingType::Subperiod => { state.period_info.subperiod_end_era = state.era.saturating_add(1); @@ -1537,6 +1580,8 @@ pub mod pallet { } }); + Self::deposit_event(Event::::Force { forcing_type }); + Ok(()) } } @@ -1584,13 +1629,14 @@ pub mod pallet { /// Returns the number of blocks per voting period. pub(crate) fn blocks_per_voting_period() -> BlockNumberFor { - T::StandardEraLength::get().saturating_mul(T::StandardErasPerVotingPeriod::get().into()) + T::StandardEraLength::get() + .saturating_mul(T::StandardErasPerVotingSubperiod::get().into()) } /// `true` if smart contract is active, `false` if it has been unregistered. pub(crate) fn is_active(smart_contract: &T::SmartContract) -> bool { IntegratedDApps::::get(smart_contract) - .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) + .map_or(false, |dapp_info| dapp_info.is_active()) } /// Calculates the `EraRewardSpan` index for the specified era. @@ -1611,48 +1657,51 @@ pub mod pallet { /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier. /// + /// ### Algorithm + /// + /// 1. Read in over all contract stake entries. In case staked amount is zero for the current era, ignore it. + /// This information is used to calculate 'score' per dApp, which is used to determine the tier. + /// + /// 2. Sort the entries by the score, in descending order - the top score dApp comes first. + /// + /// 3. Read in tier configuration. This contains information about how many slots per tier there are, + /// as well as the threshold for each tier. Threshold is the minimum amount of stake required to be eligible for a tier. + /// Iterate over tier thresholds & capacities, starting from the top tier, and assign dApps to them. + /// + /// ```ignore + //// for each tier: + /// for each unassigned dApp: + /// if tier has capacity && dApp satisfies the tier threshold: + /// add dapp to the tier + /// else: + /// exit loop since no more dApps will satisfy the threshold since they are sorted by score + /// ``` + /// + /// 4. Sort the entries by dApp ID, in ascending order. This is so we can efficiently search for them using binary search. + /// + /// 5. Calculate rewards for each tier. + /// This is done by dividing the total reward pool into tier reward pools, + /// after which the tier reward pool is divided by the number of available slots in the tier. + /// /// The returned object contains information about each dApp that made it into a tier. pub(crate) fn get_dapp_tier_assignment( era: EraNumber, period: PeriodNumber, dapp_reward_pool: Balance, ) -> DAppTierRewardsFor { - // TODO - by breaking this into multiple steps, if they are too heavy for a single block, we can distribute them between multiple blocks. - // Benchmarks will show this, but I don't believe it will be needed, especially with increased block capacity we'll get with async backing. - // Even without async backing though, we should have enough capacity to handle this. - // UPDATE: might work with async backing, but right now we could handle up to 150 dApps before exceeding the PoV size. - - // UPDATE2: instead of taking the approach of reading an ever increasing amount of entries from storage, we can instead adopt an approach - // of eficiently storing composite information into `BTreeMap`. The approach is essentially the same as the one used below to store rewards. - // Each time `stake` or `unstake` are called, corresponding entries are updated. This way we can keep track of all the contract stake in a single DB entry. - // To make the solution more scalable, we could 'split' stake entries into spans, similar as rewards are handled now. - // - // Experiment with an 'experimental' entry shows PoV size of ~7kB induced for entry that can hold up to 100 entries. - - let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::::count() as usize); + let mut dapp_stakes = Vec::with_capacity(T::MaxNumberOfContracts::get() as usize); // 1. - // Iterate over all registered dApps, and collect their stake amount. + // Iterate over all staked dApps. // This is bounded by max amount of dApps we allow to be registered. - for (smart_contract, dapp_info) in IntegratedDApps::::iter() { - // Skip unregistered dApps - if dapp_info.state != DAppState::Registered { - continue; - } - - // Skip dApps which don't have ANY amount staked (TODO: potential improvement is to prune all dApps below minimum threshold) - let stake_amount = match ContractStake::::get(&smart_contract).get(era, period) { + for (dapp_id, stake_amount) in ContractStake::::iter() { + // Skip dApps which don't have ANY amount staked + let stake_amount = match stake_amount.get(era, period) { Some(stake_amount) if !stake_amount.total().is_zero() => stake_amount, _ => continue, }; - // TODO: Need to handle labels! - // Proposition for label handling: - // Split them into 'musts' and 'good-to-have' - // In case of 'must', reduce appropriate tier size, and insert them at the end - // For good to have, we can insert them immediately, and then see if we need to adjust them later. - // Anyhow, labels bring complexity. For starters, we should only deliver the one for 'bootstraping' purposes. - dapp_stakes.push((dapp_info.id, stake_amount.total())); + dapp_stakes.push((dapp_id, stake_amount.total())); } // 2. @@ -1695,24 +1744,30 @@ pub mod pallet { tier_id.saturating_inc(); } + // TODO: what if multiple dApps satisfy the tier entry threshold but there's not enough slots to accomodate them all? + // 4. - // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is guaranteed due to lack of duplicated Ids). - // TODO & Idea: perhaps use BTreeMap instead? It will "sort" automatically based on dApp Id, and we can efficiently remove entries, - // reducing PoV size step by step. - // It's a trade-off between speed and PoV size. Although both are quite minor, so maybe it doesn't matter that much. + // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is "guaranteed" due to lack of duplicated Ids). dapp_tiers.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); // 5. Calculate rewards. let tier_rewards = tier_config .reward_portion .iter() - .map(|percent| *percent * dapp_reward_pool) + .zip(tier_config.slots_per_tier.iter()) + .map(|(percent, slots)| { + if slots.is_zero() { + Zero::zero() + } else { + *percent * dapp_reward_pool / >::into(*slots) + } + }) .collect::>(); // 6. // Prepare and return tier & rewards info. // In case rewards creation fails, we just write the default value. This should never happen though. - DAppTierRewards::, T::NumberOfTiers>::new( + DAppTierRewards::::new( dapp_tiers, tier_rewards, period, diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 6b1a16287f..73535acd36 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -16,24 +16,27 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use crate::{self as pallet_dapp_staking, *}; +use crate::{ + self as pallet_dapp_staking, + test::testing_utils::{assert_block_bump, MemorySnapshot}, + *, +}; use frame_support::{ construct_runtime, parameter_types, - traits::{ConstU128, ConstU16, ConstU32, ConstU64}, + traits::{ConstU128, ConstU32, ConstU64}, weights::Weight, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use sp_arithmetic::fixed_point::FixedU64; use sp_core::H256; +use sp_io::TestExternalities; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, Permill, }; -use sp_io::TestExternalities; - pub(crate) type AccountId = u64; pub(crate) type BlockNumber = u64; pub(crate) type Balance = u128; @@ -155,15 +158,15 @@ impl pallet_dapp_staking::Config for Test { type NativePriceProvider = DummyPriceProvider; type RewardPoolProvider = DummyRewardPoolProvider; type StandardEraLength = ConstU64<10>; - type StandardErasPerVotingPeriod = ConstU32<8>; - type StandardErasPerBuildAndEarnPeriod = ConstU32<16>; + type StandardErasPerVotingSubperiod = ConstU32<8>; + type StandardErasPerBuildAndEarnSubperiod = ConstU32<16>; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; - type MaxNumberOfContracts = ConstU16<10>; + type MaxNumberOfContracts = ConstU32<10>; type MaxUnlockingChunks = ConstU32<5>; type MinimumLockedAmount = ConstU128; type UnlockingPeriod = ConstU32<2>; - type MaxNumberOfStakedContracts = ConstU32<3>; + type MaxNumberOfStakedContracts = ConstU32<5>; type MinimumStakeAmount = ConstU128<3>; type NumberOfTiers = ConstU32<4>; #[cfg(feature = "runtime-benchmarks")] @@ -195,7 +198,7 @@ impl ExtBuilder { let era_length: BlockNumber = <::StandardEraLength as sp_core::Get<_>>::get(); let voting_period_length_in_eras: EraNumber = - <::StandardErasPerVotingPeriod as sp_core::Get<_>>::get( + <::StandardErasPerVotingSubperiod as sp_core::Get<_>>::get( ); // Init protocol state @@ -209,6 +212,23 @@ impl ExtBuilder { }, maintenance: false, }); + pallet_dapp_staking::CurrentEraInfo::::put(EraInfo { + total_locked: 0, + unlocking: 0, + current_stake_amount: StakeAmount { + voting: 0, + build_and_earn: 0, + era: 1, + period: 1, + }, + next_stake_amount: StakeAmount { + voting: 0, + build_and_earn: 0, + era: 2, + period: 1, + }, + + }); // Init tier params let tier_params = TierParameters::<::NumberOfTiers> { @@ -230,15 +250,15 @@ impl ExtBuilder { TierThreshold::DynamicTvlAmount { amount: 100, minimum_amount: 80 }, TierThreshold::DynamicTvlAmount { amount: 50, minimum_amount: 40 }, TierThreshold::DynamicTvlAmount { amount: 20, minimum_amount: 20 }, - TierThreshold::FixedTvlAmount { amount: 10 }, + TierThreshold::FixedTvlAmount { amount: 15 }, ]) .unwrap(), }; // Init tier config, based on the initial params let init_tier_config = TiersConfiguration::<::NumberOfTiers> { - number_of_slots: 100, - slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + number_of_slots: 40, + slots_per_tier: BoundedVec::try_from(vec![2, 5, 13, 20]).unwrap(), reward_portion: tier_params.reward_portion.clone(), tier_thresholds: tier_params.tier_thresholds.clone(), }; @@ -263,7 +283,10 @@ pub(crate) fn run_to_block(n: u64) { DappStaking::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); // This is performed outside of dapps staking but we expect it before on_initialize + + let pre_snapshot = MemorySnapshot::new(); DappStaking::on_initialize(System::block_number()); + assert_block_bump(&pre_snapshot); } } @@ -304,7 +327,7 @@ pub(crate) fn advance_to_next_period() { } /// Advance blocks until next period type has been reached. -pub(crate) fn advance_to_advance_to_next_subperiod() { +pub(crate) fn advance_to_next_subperiod() { let subperiod = ActiveProtocolState::::get().subperiod(); while ActiveProtocolState::::get().subperiod() == subperiod { run_for_blocks(1); @@ -316,12 +339,6 @@ pub fn dapp_staking_events() -> Vec> { System::events() .into_iter() .map(|r| r.event) - .filter_map(|e| { - if let RuntimeEvent::DappStaking(inner) = e { - Some(inner) - } else { - None - } - }) - .collect() + .filter_map(|e| ::RuntimeEvent::from(e).try_into().ok()) + .collect::>() } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 3ac63253ab..14b3fa0bad 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -20,8 +20,8 @@ use crate::test::mock::*; use crate::types::*; use crate::{ pallet::Config, ActiveProtocolState, BlockNumberFor, ContractStake, CurrentEraInfo, DAppId, - DAppTiers, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, PeriodEnd, PeriodEndInfo, - StakerInfo, + DAppTiers, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, NextTierConfig, PeriodEnd, + PeriodEndInfo, StakerInfo, TierConfig, }; use frame_support::{assert_ok, traits::Get}; @@ -47,10 +47,12 @@ pub(crate) struct MemorySnapshot { ), SingularStakingInfo, >, - contract_stake: HashMap<::SmartContract, ContractStakeAmount>, + contract_stake: HashMap, era_rewards: HashMap::EraRewardSpanLength>>, period_end: HashMap, dapp_tiers: HashMap>, + tier_config: TiersConfiguration<::NumberOfTiers>, + next_tier_config: TiersConfiguration<::NumberOfTiers>, } impl MemorySnapshot { @@ -69,6 +71,8 @@ impl MemorySnapshot { era_rewards: EraRewards::::iter().collect(), period_end: PeriodEnd::::iter().collect(), dapp_tiers: DAppTiers::::iter().collect(), + tier_config: TierConfig::::get(), + next_tier_config: NextTierConfig::::get(), } } @@ -187,7 +191,9 @@ pub(crate) fn assert_unregister(smart_contract: &MockSmartContract) { IntegratedDApps::::get(&smart_contract).unwrap().state, DAppState::Unregistered(pre_snapshot.active_protocol_state.era), ); - assert!(!ContractStake::::contains_key(&smart_contract)); + assert!(!ContractStake::::contains_key( + &IntegratedDApps::::get(&smart_contract).unwrap().id + )); } /// Lock funds into dApp staking and assert success. @@ -223,11 +229,6 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) { pre_snapshot.current_era_info.total_locked + expected_lock_amount, "Total locked balance should be increased by the amount locked." ); - assert_eq!( - post_snapshot.current_era_info.active_era_locked, - pre_snapshot.current_era_info.active_era_locked, - "Active era locked amount should remain exactly the same." - ); } /// Start the unlocking process for locked funds and assert success. @@ -309,12 +310,6 @@ pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { .saturating_sub(expected_unlock_amount), post_era_info.total_locked ); - assert_eq!( - pre_era_info - .active_era_locked - .saturating_sub(expected_unlock_amount), - post_era_info.active_era_locked - ); } /// Claims the unlocked funds back into free balance of the user and assert success. @@ -423,7 +418,6 @@ pub(crate) fn assert_stake( smart_contract: &MockSmartContract, amount: Balance, ) { - // TODO: this is a huge function - I could break it down, but I'm not sure it will help with readability. let pre_snapshot = MemorySnapshot::new(); let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); let pre_staker_info = pre_snapshot @@ -431,7 +425,7 @@ pub(crate) fn assert_stake( .get(&(account, smart_contract.clone())); let pre_contract_stake = pre_snapshot .contract_stake - .get(&smart_contract) + .get(&pre_snapshot.integrated_dapps[&smart_contract].id) .map_or(ContractStakeAmount::default(), |series| series.clone()); let pre_era_info = pre_snapshot.current_era_info; @@ -460,7 +454,7 @@ pub(crate) fn assert_stake( .expect("Entry must exist since 'stake' operation was successfull."); let post_contract_stake = post_snapshot .contract_stake - .get(&smart_contract) + .get(&pre_snapshot.integrated_dapps[&smart_contract].id) .expect("Entry must exist since 'stake' operation was successfull."); let post_era_info = post_snapshot.current_era_info; @@ -482,7 +476,6 @@ pub(crate) fn assert_stake( pre_ledger.stakeable_amount(stake_period) - amount, "Stakeable amount must decrease by the 'amount'" ); - // TODO: expand with more detailed checks of staked and staked_future // 2. verify staker info // ===================== @@ -580,11 +573,10 @@ pub(crate) fn assert_unstake( .expect("Entry must exist since 'unstake' is being called."); let pre_contract_stake = pre_snapshot .contract_stake - .get(&smart_contract) + .get(&pre_snapshot.integrated_dapps[&smart_contract].id) .expect("Entry must exist since 'unstake' is being called."); let pre_era_info = pre_snapshot.current_era_info; - let _unstake_era = pre_snapshot.active_protocol_state.era; let unstake_period = pre_snapshot.active_protocol_state.period_number(); let unstake_subperiod = pre_snapshot.active_protocol_state.subperiod(); @@ -616,8 +608,8 @@ pub(crate) fn assert_unstake( let post_ledger = post_snapshot.ledger.get(&account).unwrap(); let post_contract_stake = post_snapshot .contract_stake - .get(&smart_contract) - .expect("Entry must exist since 'stake' operation was successfull."); + .get(&pre_snapshot.integrated_dapps[&smart_contract].id) + .expect("Entry must exist since 'unstake' operation was successfull."); let post_era_info = post_snapshot.current_era_info; // 1. verify ledger @@ -633,7 +625,6 @@ pub(crate) fn assert_unstake( pre_ledger.stakeable_amount(unstake_period) + amount, "Stakeable amount must increase by the 'amount'" ); - // TODO: expand with more detailed checks of staked and staked_future // 2. verify staker info // ===================== @@ -709,6 +700,7 @@ pub(crate) fn assert_unstake( "Total staked amount for the next era must decrease by 'amount'. No overflow is allowed." ); + // Check for unstake underflow. if unstake_subperiod == Subperiod::BuildAndEarn && pre_era_info.staked_amount_next_era(Subperiod::BuildAndEarn) < amount { @@ -929,7 +921,7 @@ pub(crate) fn assert_claim_dapp_reward( .expect("Entry must exist.") .clone(); - info.try_consume(dapp_info.id).unwrap() + info.try_claim(dapp_info.id).unwrap() }; // Claim dApp reward & verify event @@ -969,12 +961,377 @@ pub(crate) fn assert_claim_dapp_reward( .expect("Entry must exist.") .clone(); assert_eq!( - info.try_consume(dapp_info.id), + info.try_claim(dapp_info.id), Err(DAppTierError::RewardAlreadyClaimed), "It must not be possible to claim the same reward twice!.", ); } +/// Unstake some funds from the specified unregistered smart contract. +pub(crate) fn assert_unstake_from_unregistered( + account: AccountId, + smart_contract: &MockSmartContract, +) { + let pre_snapshot = MemorySnapshot::new(); + let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let pre_staker_info = pre_snapshot + .staker_info + .get(&(account, smart_contract.clone())) + .expect("Entry must exist since 'unstake_from_unregistered' is being called."); + let pre_era_info = pre_snapshot.current_era_info; + + let amount = pre_staker_info.total_staked_amount(); + + // Unstake from smart contract & verify event + assert_ok!(DappStaking::unstake_from_unregistered( + RuntimeOrigin::signed(account), + smart_contract.clone(), + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::UnstakeFromUnregistered { + account, + smart_contract: smart_contract.clone(), + amount, + })); + + // Verify post-state + let post_snapshot = MemorySnapshot::new(); + let post_ledger = post_snapshot.ledger.get(&account).unwrap(); + let post_era_info = post_snapshot.current_era_info; + let period = pre_snapshot.active_protocol_state.period_number(); + let unstake_subperiod = pre_snapshot.active_protocol_state.subperiod(); + + // 1. verify ledger + // ===================== + // ===================== + assert_eq!( + post_ledger.staked_amount(period), + pre_ledger.staked_amount(period) - amount, + "Stake amount must decrease by the 'amount'" + ); + assert_eq!( + post_ledger.stakeable_amount(period), + pre_ledger.stakeable_amount(period) + amount, + "Stakeable amount must increase by the 'amount'" + ); + + // 2. verify staker info + // ===================== + // ===================== + assert!( + !StakerInfo::::contains_key(&account, smart_contract), + "Entry must be deleted since contract is unregistered." + ); + + // 3. verify era info + // ========================= + // ========================= + // It's possible next era has staked more than the current era. This is because 'stake' will always stake for the NEXT era. + if pre_era_info.total_staked_amount() < amount { + assert!(post_era_info.total_staked_amount().is_zero()); + } else { + assert_eq!( + post_era_info.total_staked_amount(), + pre_era_info.total_staked_amount() - amount, + "Total staked amount for the current era must decrease by 'amount'." + ); + } + assert_eq!( + post_era_info.total_staked_amount_next_era(), + pre_era_info.total_staked_amount_next_era() - amount, + "Total staked amount for the next era must decrease by 'amount'. No overflow is allowed." + ); + + // Check for unstake underflow. + if unstake_subperiod == Subperiod::BuildAndEarn + && pre_era_info.staked_amount_next_era(Subperiod::BuildAndEarn) < amount + { + let overflow = amount - pre_era_info.staked_amount_next_era(Subperiod::BuildAndEarn); + + assert!(post_era_info + .staked_amount_next_era(Subperiod::BuildAndEarn) + .is_zero()); + assert_eq!( + post_era_info.staked_amount_next_era(Subperiod::Voting), + pre_era_info.staked_amount_next_era(Subperiod::Voting) - overflow + ); + } else { + assert_eq!( + post_era_info.staked_amount_next_era(unstake_subperiod), + pre_era_info.staked_amount_next_era(unstake_subperiod) - amount + ); + } +} + +/// Cleanup expired DB entries for the account and verify post state. +pub(crate) fn assert_cleanup_expired_entries(account: AccountId) { + let pre_snapshot = MemorySnapshot::new(); + + let current_period = pre_snapshot.active_protocol_state.period_number(); + let threshold_period = DappStaking::oldest_claimable_period(current_period); + + // Find entries which should be kept, and which should be deleted + let mut to_be_deleted = Vec::new(); + let mut to_be_kept = Vec::new(); + pre_snapshot + .staker_info + .iter() + .for_each(|((inner_account, contract), entry)| { + if *inner_account == account { + if entry.period_number() < current_period && !entry.is_loyal() + || entry.period_number() < threshold_period + { + to_be_deleted.push(contract); + } else { + to_be_kept.push(contract); + } + } + }); + + // Cleanup expired entries and verify event + assert_ok!(DappStaking::cleanup_expired_entries(RuntimeOrigin::signed( + account + ))); + System::assert_last_event(RuntimeEvent::DappStaking(Event::ExpiredEntriesRemoved { + account, + count: to_be_deleted.len().try_into().unwrap(), + })); + + // Verify post-state + let post_snapshot = MemorySnapshot::new(); + + // Ensure that correct entries have been kept + assert_eq!(post_snapshot.staker_info.len(), to_be_kept.len()); + to_be_kept.iter().for_each(|contract| { + assert!(post_snapshot + .staker_info + .contains_key(&(account, **contract))); + }); + + // Ensure that ledger has been correctly updated + let pre_ledger = pre_snapshot.ledger.get(&account).unwrap(); + let post_ledger = post_snapshot.ledger.get(&account).unwrap(); + + let num_of_deleted_entries: u32 = to_be_deleted.len().try_into().unwrap(); + assert_eq!( + pre_ledger.contract_stake_count - num_of_deleted_entries, + post_ledger.contract_stake_count + ); +} + +/// Asserts correct transitions of the protocol after a block has been produced. +pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { + let current_block_number = System::block_number(); + + // No checks if era didn't change. + if pre_snapshot.active_protocol_state.next_era_start > current_block_number { + return; + } + + // Verify post state + let post_snapshot = MemorySnapshot::new(); + + let is_new_subperiod = pre_snapshot + .active_protocol_state + .period_info + .subperiod_end_era + <= post_snapshot.active_protocol_state.era; + + // 1. Verify protocol state + let pre_protoc_state = pre_snapshot.active_protocol_state; + let post_protoc_state = post_snapshot.active_protocol_state; + assert_eq!(post_protoc_state.era, pre_protoc_state.era + 1); + + match pre_protoc_state.subperiod() { + Subperiod::Voting => { + assert_eq!( + post_protoc_state.subperiod(), + Subperiod::BuildAndEarn, + "Voting subperiod only lasts for a single era." + ); + + let eras_per_bep: EraNumber = + ::StandardErasPerBuildAndEarnSubperiod::get(); + assert_eq!( + post_protoc_state.period_info.subperiod_end_era, + post_protoc_state.era + eras_per_bep, + "Build&earn must last for the predefined amount of standard eras." + ); + + let standard_era_length: BlockNumber = ::StandardEraLength::get(); + assert_eq!( + post_protoc_state.next_era_start, + current_block_number + standard_era_length, + "Era in build&earn period must last for the predefined amount of blocks." + ); + } + Subperiod::BuildAndEarn => { + if is_new_subperiod { + assert_eq!( + post_protoc_state.subperiod(), + Subperiod::Voting, + "Since we expect a new subperiod, it must be 'Voting'." + ); + assert_eq!( + post_protoc_state.period_number(), + pre_protoc_state.period_number() + 1, + "Ending 'Build&Earn' triggers a new period." + ); + assert_eq!( + post_protoc_state.period_info.subperiod_end_era, + post_protoc_state.era + 1, + "Voting era must last for a single era." + ); + + let blocks_per_standard_era: BlockNumber = + ::StandardEraLength::get(); + let eras_per_voting_subperiod: EraNumber = + ::StandardErasPerVotingSubperiod::get(); + let eras_per_voting_subperiod: BlockNumber = eras_per_voting_subperiod.into(); + let era_length: BlockNumber = blocks_per_standard_era * eras_per_voting_subperiod; + assert_eq!( + post_protoc_state.next_era_start, + current_block_number + era_length, + "The upcoming 'Voting' subperiod must last for the 'standard eras per voting subperiod x standard era length' amount of blocks." + ); + } else { + assert_eq!( + post_protoc_state.period_info, pre_protoc_state.period_info, + "New subperiod hasn't started, hence it should remain 'Build&Earn'." + ); + } + } + } + + // 2. Verify current era info + let pre_era_info = pre_snapshot.current_era_info; + let post_era_info = post_snapshot.current_era_info; + + assert_eq!(post_era_info.total_locked, pre_era_info.total_locked); + assert_eq!(post_era_info.unlocking, pre_era_info.unlocking); + + // New period has started + if is_new_subperiod && pre_protoc_state.subperiod() == Subperiod::BuildAndEarn { + assert_eq!( + post_era_info.current_stake_amount, + StakeAmount { + voting: Zero::zero(), + build_and_earn: Zero::zero(), + era: pre_protoc_state.era + 1, + period: pre_protoc_state.period_number() + 1, + } + ); + assert_eq!( + post_era_info.next_stake_amount, + StakeAmount { + voting: Zero::zero(), + build_and_earn: Zero::zero(), + era: pre_protoc_state.era + 2, + period: pre_protoc_state.period_number() + 1, + } + ); + } else { + assert_eq!( + post_era_info.current_stake_amount, + pre_era_info.next_stake_amount + ); + assert_eq!( + post_era_info.next_stake_amount.total(), + post_era_info.current_stake_amount.total() + ); + assert_eq!( + post_era_info.next_stake_amount.era, + post_protoc_state.era + 1, + ); + assert_eq!( + post_era_info.next_stake_amount.period, + pre_protoc_state.period_number(), + ); + } + + // 3. Verify tier config + match pre_protoc_state.subperiod() { + Subperiod::Voting => { + assert!(!NextTierConfig::::exists()); + assert_eq!(post_snapshot.tier_config, pre_snapshot.next_tier_config); + } + Subperiod::BuildAndEarn if is_new_subperiod => { + assert!(NextTierConfig::::exists()); + assert_eq!(post_snapshot.tier_config, pre_snapshot.tier_config); + } + _ => { + assert_eq!(post_snapshot.tier_config, pre_snapshot.tier_config); + } + } + + // 4. Verify era reward + let era_span_index = DappStaking::era_reward_span_index(pre_protoc_state.era); + let maybe_pre_era_reward_span = pre_snapshot.era_rewards.get(&era_span_index); + let post_era_reward_span = post_snapshot + .era_rewards + .get(&era_span_index) + .expect("Era reward info must exist after era has finished."); + + // Sanity check + if let Some(pre_era_reward_span) = maybe_pre_era_reward_span { + assert_eq!( + pre_era_reward_span.last_era(), + pre_protoc_state.era - 1, + "If entry exists, it should cover eras up to the previous one, exactly." + ); + } + + assert_eq!( + post_era_reward_span.last_era(), + pre_protoc_state.era, + "Entry must cover the current era." + ); + assert_eq!( + post_era_reward_span + .get(pre_protoc_state.era) + .expect("Above check proved it must exist.") + .staked, + pre_snapshot.current_era_info.total_staked_amount(), + "Total staked amount must be equal to total amount staked at the end of the era." + ); + + // 5. Verify period end + if is_new_subperiod && pre_protoc_state.subperiod() == Subperiod::BuildAndEarn { + let period_end_info = post_snapshot.period_end[&pre_protoc_state.period_number()]; + assert_eq!( + period_end_info.total_vp_stake, + pre_snapshot + .current_era_info + .staked_amount(Subperiod::Voting), + ); + } + + // 6. Verify event(s) + if is_new_subperiod { + let events = dapp_staking_events(); + assert!( + events.len() >= 2, + "At least 2 events should exist from era & subperiod change." + ); + assert_eq!( + events[events.len() - 2], + Event::NewEra { + era: post_protoc_state.era, + } + ); + assert_eq!( + events[events.len() - 1], + Event::NewSubperiod { + subperiod: pre_protoc_state.subperiod().next(), + number: post_protoc_state.period_number(), + } + ) + } else { + System::assert_last_event(RuntimeEvent::DappStaking(Event::NewEra { + era: post_protoc_state.era, + })); + } +} + /// Returns from which starting era to which ending era can rewards be claimed for the specified account. /// /// If `None` is returned, there is nothing to claim. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 996a56f1e9..0795bd0653 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -16,26 +16,27 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use crate::test::mock::*; -use crate::test::testing_utils::*; +use crate::test::{mock::*, testing_utils::*}; use crate::{ - pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, ForcingType, - IntegratedDApps, Ledger, NextDAppId, PeriodNumber, Subperiod, + pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, Event, ForcingType, + IntegratedDApps, Ledger, NextDAppId, PeriodNumber, StakerInfo, Subperiod, TierConfig, }; -use frame_support::{assert_noop, assert_ok, error::BadOrigin, traits::Get}; +use frame_support::{ + assert_noop, assert_ok, assert_storage_noop, + error::BadOrigin, + traits::{Currency, Get, OnFinalize, OnInitialize}, +}; use sp_runtime::traits::Zero; -// TODO: test scenarios -// 1. user is staking, period passes, they can unlock their funds which were previously staked - +// TODO: remove this prior to the merge #[test] fn print_test() { ExtBuilder::build().execute_with(|| { use crate::dsv3_weight::WeightInfo; println!( ">>> dApp tier assignment reading & calculation {:?}", - crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(200) + crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(100) ); use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -61,8 +62,8 @@ fn print_test() { ); println!( - ">>> Experimental storage entry read {:?}", - crate::dsv3_weight::SubstrateWeight::::experimental_read() + ">>> Max encoded size of ContractStake: {:?}", + crate::ContractStakeAmount::max_encoded_len() ); }) } @@ -75,11 +76,17 @@ fn maintenace_mode_works() { // Enable maintenance mode & check post-state assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), true)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::MaintenanceMode { + enabled: true, + })); assert!(ActiveProtocolState::::get().maintenance); // Call still works, even in maintenance mode - assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), true)); - assert!(ActiveProtocolState::::get().maintenance); + assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), false)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::MaintenanceMode { + enabled: false, + })); + assert!(!ActiveProtocolState::::get().maintenance); // Incorrect origin doesn't work assert_noop!( @@ -175,10 +182,27 @@ fn maintenace_mode_call_filtering_works() { } #[test] -fn on_initialize_state_change_works() { +fn on_initialize_is_noop_if_no_era_change() { ExtBuilder::build().execute_with(|| { - // TODO: test `EraInfo` change and verify events. This would be good to do each time we call the helper functions to go to next era or period. + let protocol_state = ActiveProtocolState::::get(); + let current_block_number = System::block_number(); + + assert!( + current_block_number < protocol_state.next_era_start, + "Sanity check, otherwise test doesn't make sense." + ); + // Sanity check + assert_storage_noop!(DappStaking::on_finalize(current_block_number)); + + // If no era change, on_initialize should be a noop + assert_storage_noop!(DappStaking::on_initialize(current_block_number + 1)); + }) +} + +#[test] +fn on_initialize_base_state_change_works() { + ExtBuilder::build().execute_with(|| { // Sanity check let protocol_state = ActiveProtocolState::::get(); assert_eq!(protocol_state.era, 1); @@ -211,7 +235,7 @@ fn on_initialize_state_change_works() { // Advance eras just until we reach the next voting period let eras_per_bep_period: EraNumber = - ::StandardErasPerBuildAndEarnPeriod::get(); + ::StandardErasPerBuildAndEarnSubperiod::get(); let blocks_per_era: BlockNumber = ::StandardEraLength::get(); for era in 2..(2 + eras_per_bep_period - 1) { let pre_block = System::block_number(); @@ -666,7 +690,7 @@ fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { assert_unlock(account, unlock_amount); } - // We can still unlock in the current erblocka, theoretically + // We can still unlock in the current era, theoretically for _ in 0..5 { assert_unlock(account, unlock_amount); } @@ -834,10 +858,44 @@ fn stake_basic_example_is_ok() { let lock_amount = 300; assert_lock(account, lock_amount); - // Stake some amount, and then some more - let (stake_amount_1, stake_amount_2) = (31, 29); + // Stake some amount, and then some more in the same era. + let (stake_1, stake_2) = (31, 29); + assert_stake(account, &smart_contract, stake_1); + assert_stake(account, &smart_contract, stake_2); + }) +} + +#[test] +fn stake_after_expiry_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + // Lock & stake some amount + let account = 2; + let lock_amount = 300; + let (stake_amount_1, stake_amount_2) = (200, 100); + assert_lock(account, lock_amount); assert_stake(account, &smart_contract, stake_amount_1); + + // Advance so far that the stake rewards expire. + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + advance_to_period( + ActiveProtocolState::::get().period_number() + reward_retention_in_periods + 1, + ); + + // Sanity check that the rewards have expired + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::RewardExpired, + ); + + // Calling stake again should work, expired stake entries should be cleaned up assert_stake(account, &smart_contract, stake_amount_2); + assert_stake(account, &smart_contract, stake_amount_1); }) } @@ -904,7 +962,7 @@ fn stake_in_final_era_fails() { } #[test] -fn stake_fails_if_unclaimed_rewards_from_past_period_remain() { +fn stake_fails_if_unclaimed_rewards_from_past_eras_remain() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount let smart_contract = MockSmartContract::default(); @@ -917,7 +975,7 @@ fn stake_fails_if_unclaimed_rewards_from_past_period_remain() { advance_to_next_period(); assert_noop!( DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), - Error::::UnclaimedRewardsFromPastPeriods + Error::::UnclaimedRewards ); }) } @@ -992,7 +1050,53 @@ fn stake_fails_due_to_too_small_staking_amount() { }) } -// TODO: add tests to cover staking & unstaking with unclaimed rewards! +#[test] +fn stake_fails_due_to_too_many_staked_contracts() { + ExtBuilder::build().execute_with(|| { + let max_number_of_contracts: u32 = ::MaxNumberOfStakedContracts::get(); + + // Lock amount by staker + let account = 1; + assert_lock(account, 100 as Balance * max_number_of_contracts as Balance); + + // Advance to build&earn subperiod so we ensure non-loyal staking + advance_to_next_subperiod(); + + // Register smart contracts up the the max allowed number + for id in 1..=max_number_of_contracts { + let smart_contract = MockSmartContract::Wasm(id.into()); + assert_register(2, &MockSmartContract::Wasm(id.into())); + assert_stake(account, &smart_contract, 10); + } + + let excess_smart_contract = MockSmartContract::Wasm((max_number_of_contracts + 1).into()); + assert_register(2, &excess_smart_contract); + + // Max number of staked contract entries has been exceeded. + assert_noop!( + DappStaking::stake( + RuntimeOrigin::signed(account), + excess_smart_contract.clone(), + 10 + ), + Error::::TooManyStakedContracts + ); + + // Advance into next period, error should still happen + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + assert_noop!( + DappStaking::stake( + RuntimeOrigin::signed(account), + excess_smart_contract.clone(), + 10 + ), + Error::::TooManyStakedContracts + ); + }) +} #[test] fn unstake_basic_example_is_ok() { @@ -1013,8 +1117,6 @@ fn unstake_basic_example_is_ok() { // Unstake some amount, in the current era. let unstake_amount_1 = 3; assert_unstake(account, &smart_contract, unstake_amount_1); - - // TODO: scenario where we unstake AFTER advancing an era and claiming rewards }) } @@ -1140,33 +1242,26 @@ fn unstake_from_non_staked_contract_fails() { } #[test] -fn unstake_from_a_contract_staked_in_past_period_fails() { +fn unstake_with_unclaimed_rewards_fails() { ExtBuilder::build().execute_with(|| { - // Register smart contract & lock some amount - let smart_contract_1 = MockSmartContract::Wasm(1); - let smart_contract_2 = MockSmartContract::Wasm(2); - assert_register(1, &smart_contract_1); - assert_register(1, &smart_contract_2); + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::Wasm(1); + assert_register(1, &smart_contract); let account = 2; assert_lock(account, 300); - - // Stake some amount on the 2nd contract. let stake_amount = 100; - assert_stake(account, &smart_contract_2, stake_amount); + assert_stake(account, &smart_contract, stake_amount); - // Advance to the next period, and stake on the 1st contract. - advance_to_next_period(); - // TODO: need to implement reward claiming for this check to work! - // assert_stake(account, &smart_contract_1, stake_amount); - // Try to unstake from the 2nd contract, which is no longer staked on due to period change. - // assert_noop!( - // DappStaking::unstake( - // RuntimeOrigin::signed(account), - // smart_contract_2, - // 1, - // ), - // Error::::UnstakeFromPastPeriod - // ); + // Advance 1 era, try to unstake and it should work since we're modifying the current era stake. + advance_to_next_era(); + assert_unstake(account, &smart_contract, 1); + + // Advance 1 more era, creating claimable rewards. Unstake should fail now. + advance_to_next_era(); + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract, 1), + Error::::UnclaimedRewards + ); }) } @@ -1314,7 +1409,7 @@ fn claim_staker_rewards_after_expiry_fails() { advance_to_period( ActiveProtocolState::::get().period_number() + reward_retention_in_periods, ); - advance_to_advance_to_next_subperiod(); + advance_to_next_subperiod(); advance_to_era( ActiveProtocolState::::get() .period_info @@ -1444,7 +1539,7 @@ fn claim_bonus_reward_with_only_build_and_earn_stake_fails() { assert_lock(account, lock_amount); // Stake in Build&Earn period type, advance to next era and try to claim bonus reward - advance_to_advance_to_next_subperiod(); + advance_to_next_subperiod(); assert_eq!( ActiveProtocolState::::get().subperiod(), Subperiod::BuildAndEarn, @@ -1637,3 +1732,614 @@ fn claim_dapp_reward_twice_for_same_era_fails() { assert_claim_dapp_reward(account, &smart_contract, claim_era_2); }) } + +#[test] +fn unstake_from_unregistered_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Unregister the smart contract, and unstake from it. + assert_unregister(&smart_contract); + assert_unstake_from_unregistered(account, &smart_contract); + }) +} + +#[test] +fn unstake_from_unregistered_fails_for_active_contract() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + assert_noop!( + DappStaking::unstake_from_unregistered(RuntimeOrigin::signed(account), smart_contract), + Error::::ContractStillActive + ); + }) +} + +#[test] +fn unstake_from_unregistered_fails_for_not_staked_contract() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + assert_unregister(&smart_contract); + + assert_noop!( + DappStaking::unstake_from_unregistered(RuntimeOrigin::signed(2), smart_contract), + Error::::NoStakingInfo + ); + }) +} + +#[test] +fn unstake_from_unregistered_fails_for_past_period() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Unregister smart contract & advance to next period + assert_unregister(&smart_contract); + advance_to_next_period(); + + assert_noop!( + DappStaking::unstake_from_unregistered(RuntimeOrigin::signed(account), smart_contract), + Error::::UnstakeFromPastPeriod + ); + }) +} + +#[test] +fn cleanup_expired_entries_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contracts + let contracts: Vec<_> = (1..=5).map(|id| MockSmartContract::Wasm(id)).collect(); + contracts.iter().for_each(|smart_contract| { + assert_register(1, smart_contract); + }); + let account = 2; + assert_lock(account, 1000); + + // Scenario: + // - 1st contract will be staked in the period that expires due to exceeded reward retention + // - 2nd contract will be staked in the period on the edge of expiry, with loyalty flag + // - 3rd contract will be be staked in the period on the edge of expiry, without loyalty flag + // - 4th contract will be staked in the period right before the current one, with loyalty flag + // - 5th contract will be staked in the period right before the current one, without loyalty flag + // + // Expectation: 1, 3, 5 should be removed, 2 & 4 should remain + + // 1st + assert_stake(account, &contracts[0], 13); + + // 2nd & 3rd + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + assert_stake(account, &contracts[1], 17); + advance_to_next_subperiod(); + + assert_stake(account, &contracts[2], 19); + + // 4th & 5th + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + assert!( + reward_retention_in_periods >= 2, + "Sanity check, otherwise the test doesn't make sense." + ); + advance_to_period(reward_retention_in_periods + 1); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + assert_stake(account, &contracts[3], 23); + advance_to_next_subperiod(); + assert_stake(account, &contracts[4], 29); + + // Finally do the test + advance_to_next_period(); + assert_cleanup_expired_entries(account); + + // Additional sanity check according to the described scenario + assert!(!StakerInfo::::contains_key(account, &contracts[0])); + assert!(!StakerInfo::::contains_key(account, &contracts[2])); + assert!(!StakerInfo::::contains_key(account, &contracts[4])); + + assert!(StakerInfo::::contains_key(account, &contracts[1])); + assert!(StakerInfo::::contains_key(account, &contracts[3])); + }) +} + +#[test] +fn cleanup_expired_entries_fails_with_no_entries() { + ExtBuilder::build().execute_with(|| { + // Register smart contracts + let (contract_1, contract_2) = (MockSmartContract::Wasm(1), MockSmartContract::Wasm(2)); + assert_register(1, &contract_1); + assert_register(1, &contract_2); + + let account = 2; + assert_lock(account, 1000); + assert_stake(account, &contract_1, 13); + assert_stake(account, &contract_2, 17); + + // Advance only one period, rewards should still be valid. + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + assert!( + reward_retention_in_periods >= 1, + "Sanity check, otherwise the test doesn't make sense." + ); + advance_to_next_period(); + + assert_noop!( + DappStaking::cleanup_expired_entries(RuntimeOrigin::signed(account)), + Error::::NoExpiredEntries + ); + }) +} + +#[test] +fn force_era_works() { + ExtBuilder::build().execute_with(|| { + // 1. Force new era in the voting subperiod + let init_state = ActiveProtocolState::::get(); + assert!( + init_state.next_era_start > System::block_number() + 1, + "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." + ); + assert_eq!( + init_state.subperiod(), + Subperiod::Voting, + "Sanity check." + ); + assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Era)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { + forcing_type: ForcingType::Era, + })); + + // Verify state change + assert_eq!( + ActiveProtocolState::::get().next_era_start, + System::block_number() + 1, + ); + assert_eq!( + ActiveProtocolState::::get().period_end_era(), + init_state.period_end_era(), + ); + + // Go to the next block, and ensure new era is started + run_for_blocks(1); + assert_eq!( + ActiveProtocolState::::get().era, + init_state.era + 1, + "New era must be started." + ); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + ); + + // 2. Force new era in the build&earn subperiod + let init_state = ActiveProtocolState::::get(); + assert!( + init_state.next_era_start > System::block_number() + 1, + "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." + ); + assert!(init_state.period_end_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); + assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Era)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { + forcing_type: ForcingType::Era, + })); + + // Verify state change + assert_eq!( + ActiveProtocolState::::get().next_era_start, + System::block_number() + 1, + ); + assert_eq!( + ActiveProtocolState::::get().period_end_era(), + init_state.period_end_era(), + "Only era is bumped, but we don't expect to switch over to the next subperiod." + ); + + run_for_blocks(1); + assert_eq!( + ActiveProtocolState::::get().era, + init_state.era + 1, + "New era must be started." + ); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "We're expected to remain in the same subperiod." + ); + }) +} + +#[test] +fn force_subperiod_works() { + ExtBuilder::build().execute_with(|| { + // 1. Force new subperiod in the voting subperiod + let init_state = ActiveProtocolState::::get(); + assert!( + init_state.next_era_start > System::block_number() + 1, + "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." + ); + assert_eq!( + init_state.subperiod(), + Subperiod::Voting, + "Sanity check." + ); + assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Subperiod)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { + forcing_type: ForcingType::Subperiod, + })); + + // Verify state change + assert_eq!( + ActiveProtocolState::::get().next_era_start, + System::block_number() + 1, + ); + assert_eq!( + ActiveProtocolState::::get().period_end_era(), + init_state.era + 1, + "The switch to the next subperiod must happen in the next era." + ); + + // Go to the next block, and ensure new era is started + run_for_blocks(1); + assert_eq!( + ActiveProtocolState::::get().era, + init_state.era + 1, + "New era must be started." + ); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "New subperiod must be started." + ); + assert_eq!(ActiveProtocolState::::get().period_number(), init_state.period_number(), "Period must remain the same."); + + // 2. Force new era in the build&earn subperiod + let init_state = ActiveProtocolState::::get(); + assert!( + init_state.next_era_start > System::block_number() + 1, + "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." + ); + assert!(init_state.period_end_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); + assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Subperiod)); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { + forcing_type: ForcingType::Subperiod, + })); + + // Verify state change + assert_eq!( + ActiveProtocolState::::get().next_era_start, + System::block_number() + 1, + ); + assert_eq!( + ActiveProtocolState::::get().period_end_era(), + init_state.era + 1, + "The switch to the next subperiod must happen in the next era." + ); + + run_for_blocks(1); + assert_eq!( + ActiveProtocolState::::get().era, + init_state.era + 1, + "New era must be started." + ); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::Voting, + "New subperiod must be started." + ); + assert_eq!(ActiveProtocolState::::get().period_number(), init_state.period_number() + 1, "New period must be started."); + }) +} + +#[test] +fn force_with_incorrect_origin_fails() { + ExtBuilder::build().execute_with(|| { + assert_noop!( + DappStaking::force(RuntimeOrigin::signed(1), ForcingType::Era), + BadOrigin + ); + }) +} + +#[test] +fn get_dapp_tier_assignment_basic_example_works() { + ExtBuilder::build().execute_with(|| { + // This test will rely on the configuration inside the mock file. + // If that changes, this test will have to be updated as well. + + // Scenario: + // - 1st tier is filled up, with one dApp satisfying the threshold but not making it due to lack of tier capacity + // - 2nd tier has 2 dApps - 1 that could make it into the 1st tier and one that's supposed to be in the 2nd tier + // - 3rd tier has no dApps + // - 4th tier has 2 dApps + // - 1 dApp doesn't make it into any tier + + // Register smart contracts + let tier_config = TierConfig::::get(); + let number_of_smart_contracts = tier_config.slots_per_tier[0] + 1 + 1 + 0 + 2 + 1; + let smart_contracts: Vec<_> = (1..=number_of_smart_contracts) + .map(|x| { + let smart_contract = MockSmartContract::Wasm(x.into()); + assert_register(x.into(), &smart_contract); + smart_contract + }) + .collect(); + let mut dapp_index: usize = 0; + + fn lock_and_stake(account: usize, smart_contract: &MockSmartContract, amount: Balance) { + let account = account.try_into().unwrap(); + Balances::make_free_balance_be(&account, amount); + assert_lock(account, amount); + assert_stake(account, smart_contract, amount); + } + + // 1st tier is completely filled up, with 1 more dApp not making it inside + for x in 0..tier_config.slots_per_tier[0] as Balance { + lock_and_stake( + dapp_index, + &smart_contracts[dapp_index], + tier_config.tier_thresholds[0].threshold() + x + 1, + ); + dapp_index += 1; + } + // One that won't make it into the 1st tier. + lock_and_stake( + dapp_index, + &smart_contracts[dapp_index], + tier_config.tier_thresholds[0].threshold(), + ); + dapp_index += 1; + + // 2nd tier - 1 dedicated dApp + lock_and_stake( + dapp_index, + &smart_contracts[dapp_index], + tier_config.tier_thresholds[0].threshold() - 1, + ); + dapp_index += 1; + + // 3rd tier is empty + // 4th tier has 2 dApps + for x in 0..2 { + lock_and_stake( + dapp_index, + &smart_contracts[dapp_index], + tier_config.tier_thresholds[3].threshold() + x, + ); + dapp_index += 1; + } + + // One dApp doesn't make it into any tier + lock_and_stake( + dapp_index, + &smart_contracts[dapp_index], + tier_config.tier_thresholds[3].threshold() - 1, + ); + + // Finally, the actual test + let protocol_state = ActiveProtocolState::::get(); + let dapp_reward_pool = 1000000; + let tier_assignment = DappStaking::get_dapp_tier_assignment( + protocol_state.era + 1, + protocol_state.period_number(), + dapp_reward_pool, + ); + + // Basic checks + let number_of_tiers: u32 = ::NumberOfTiers::get(); + assert_eq!(tier_assignment.period, protocol_state.period_number()); + assert_eq!(tier_assignment.rewards.len(), number_of_tiers as usize); + assert_eq!( + tier_assignment.dapps.len(), + number_of_smart_contracts as usize - 1, + "One contract doesn't make it into any tier." + ); + + // 1st tier checks + let (entry_1, entry_2) = (tier_assignment.dapps[0], tier_assignment.dapps[1]); + assert_eq!(entry_1.dapp_id, 0); + assert_eq!(entry_1.tier_id, Some(0)); + + assert_eq!(entry_2.dapp_id, 1); + assert_eq!(entry_2.tier_id, Some(0)); + + // 2nd tier checks + let (entry_3, entry_4) = (tier_assignment.dapps[2], tier_assignment.dapps[3]); + assert_eq!(entry_3.dapp_id, 2); + assert_eq!(entry_3.tier_id, Some(1)); + + assert_eq!(entry_4.dapp_id, 3); + assert_eq!(entry_4.tier_id, Some(1)); + + // 4th tier checks + let (entry_5, entry_6) = (tier_assignment.dapps[4], tier_assignment.dapps[5]); + assert_eq!(entry_5.dapp_id, 4); + assert_eq!(entry_5.tier_id, Some(3)); + + assert_eq!(entry_6.dapp_id, 5); + assert_eq!(entry_6.tier_id, Some(3)); + + // Sanity check - last dapp should not exists in the tier assignment + assert!(tier_assignment + .dapps + .binary_search_by(|x| x.dapp_id.cmp(&(dapp_index.try_into().unwrap()))) + .is_err()); + + // Check that rewards are calculated correctly + tier_config + .reward_portion + .iter() + .zip(tier_config.slots_per_tier.iter()) + .enumerate() + .for_each(|(idx, (reward_portion, slots))| { + let total_tier_allocation = *reward_portion * dapp_reward_pool; + let tier_reward: Balance = total_tier_allocation / (*slots as Balance); + + assert_eq!(tier_assignment.rewards[idx], tier_reward,); + }); + }) +} + +#[test] +fn get_dapp_tier_assignment_zero_slots_per_tier_works() { + ExtBuilder::build().execute_with(|| { + // This test will rely on the configuration inside the mock file. + // If that changes, this test might have to be updated as well. + + // Ensure that first tier has 0 slots. + TierConfig::::mutate(|config| { + let slots_in_first_tier = config.slots_per_tier[0]; + config.number_of_slots = config.number_of_slots - slots_in_first_tier; + config.slots_per_tier[0] = 0; + }); + + // Calculate tier assignment (we don't need dApps for this test) + let protocol_state = ActiveProtocolState::::get(); + let dapp_reward_pool = 1000000; + let tier_assignment = DappStaking::get_dapp_tier_assignment( + protocol_state.era, + protocol_state.period_number(), + dapp_reward_pool, + ); + + // Basic checks + let number_of_tiers: u32 = ::NumberOfTiers::get(); + assert_eq!(tier_assignment.period, protocol_state.period_number()); + assert_eq!(tier_assignment.rewards.len(), number_of_tiers as usize); + assert!(tier_assignment.dapps.is_empty()); + + assert!( + tier_assignment.rewards[0].is_zero(), + "1st tier has no slots so no rewards should be assigned to it." + ); + + // Regardless of that, other tiers shouldn't benefit from this + assert!(tier_assignment.rewards.iter().sum::() < dapp_reward_pool); + }) +} + +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +/////// More complex & composite scenarios, maybe move them into a separate file + +#[test] +fn unlock_after_staked_period_ends_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 101; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance to the next period, and ensure stake is reset and can be fully unlocked + advance_to_next_period(); + assert!(Ledger::::get(&account) + .staked_amount(ActiveProtocolState::::get().period_number()) + .is_zero()); + assert_unlock(account, amount); + assert_eq!(Ledger::::get(&account).unlocking_amount(), amount); + }) +} + +#[test] +fn unstake_from_a_contract_staked_in_past_period_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract & lock some amount + let smart_contract_1 = MockSmartContract::Wasm(1); + let smart_contract_2 = MockSmartContract::Wasm(2); + assert_register(1, &smart_contract_1); + assert_register(1, &smart_contract_2); + let account = 2; + assert_lock(account, 300); + + // Stake some amount on the 2nd contract. + let stake_amount = 100; + assert_stake(account, &smart_contract_2, stake_amount); + + // Advance to the next period, and stake on the 1st contract. + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + + // Try to unstake from the 2nd contract, which is no longer staked on due to period change. + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract_2, 1,), + Error::::UnstakeFromPastPeriod + ); + + // Staking on the 1st contract should succeed since we haven't staked on it before so there are no bonus rewards to claim + assert_stake(account, &smart_contract_1, stake_amount); + + // Even with active stake on the 1st contract, unstake from 2nd should still fail since period change reset its stake. + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract_2, 1,), + Error::::UnstakeFromPastPeriod + ); + }) +} + +#[test] +fn stake_and_unstake_after_reward_claim_is_ok() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let dev_account = 1; + let smart_contract = MockSmartContract::default(); + assert_register(dev_account, &smart_contract); + + let account = 2; + let amount = 400; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount - 100); + + // Advance 2 eras so we have claimable rewards. Both stake & unstake should fail. + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 1), + Error::::UnclaimedRewards + ); + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract, 1), + Error::::UnclaimedRewards + ); + + // Claim rewards, unstake should work now. + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + assert_stake(account, &smart_contract, 1); + assert_unstake(account, &smart_contract, 1); + }) +} diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 6554faed11..50074ec7a6 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -153,6 +153,7 @@ fn dapp_info_basic_checks() { id: 7, state: DAppState::Registered, reward_destination: None, + tier_label: None, }; // Owner receives reward in case no beneficiary is set @@ -161,6 +162,20 @@ fn dapp_info_basic_checks() { // Beneficiary receives rewards in case it is set dapp_info.reward_destination = Some(beneficiary); assert_eq!(*dapp_info.reward_beneficiary(), beneficiary); + + // Check if dApp is active + assert!(dapp_info.is_active()); + + dapp_info.state = DAppState::Unregistered(10); + assert!(!dapp_info.is_active()); +} + +#[test] +fn unlocking_chunk_basic_check() { + // Sanity check + let unlocking_chunk = UnlockingChunk::::default(); + assert!(unlocking_chunk.amount.is_zero()); + assert!(unlocking_chunk.unlock_block.is_zero()); } #[test] @@ -204,31 +219,31 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { // First basic scenario // Add some lock amount, then reduce it - let first_lock_amount = 19; + let lock_amount_1 = 19; let unlock_amount = 7; - acc_ledger.add_lock_amount(first_lock_amount); + acc_ledger.add_lock_amount(lock_amount_1); acc_ledger.subtract_lock_amount(unlock_amount); assert_eq!( acc_ledger.total_locked_amount(), - first_lock_amount - unlock_amount + lock_amount_1 - unlock_amount ); assert_eq!( acc_ledger.active_locked_amount(), - first_lock_amount - unlock_amount + lock_amount_1 - unlock_amount ); assert_eq!(acc_ledger.unlocking_amount(), 0); // Second basic scenario - let first_lock_amount = first_lock_amount - unlock_amount; - let second_lock_amount = 31; - acc_ledger.add_lock_amount(second_lock_amount - first_lock_amount); - assert_eq!(acc_ledger.active_locked_amount(), second_lock_amount); + let lock_amount_1 = lock_amount_1 - unlock_amount; + let lock_amount_2 = 31; + acc_ledger.add_lock_amount(lock_amount_2 - lock_amount_1); + assert_eq!(acc_ledger.active_locked_amount(), lock_amount_2); // Subtract from the first era and verify state is as expected acc_ledger.subtract_lock_amount(unlock_amount); assert_eq!( acc_ledger.active_locked_amount(), - second_lock_amount - unlock_amount + lock_amount_2 - unlock_amount ); } @@ -312,7 +327,12 @@ fn account_ledger_staked_amount_works() { // Period matches let amount_1 = 29; let period = 5; - acc_ledger.staked = StakeAmount::new(amount_1, 0, 1, period); + acc_ledger.staked = StakeAmount { + voting: amount_1, + build_and_earn: 0, + era: 1, + period, + }; assert_eq!(acc_ledger.staked_amount(period), amount_1); // Period doesn't match @@ -321,7 +341,12 @@ fn account_ledger_staked_amount_works() { // Add future entry let amount_2 = 17; - acc_ledger.staked_future = Some(StakeAmount::new(0, amount_2, 2, period)); + acc_ledger.staked_future = Some(StakeAmount { + voting: 0, + build_and_earn: amount_2, + era: 2, + period, + }); assert_eq!(acc_ledger.staked_amount(period), amount_2); assert!(acc_ledger.staked_amount(period - 1).is_zero()); assert!(acc_ledger.staked_amount(period + 1).is_zero()); @@ -407,9 +432,14 @@ fn account_ledger_stakeable_amount_works() { ); // Second scenario - some staked amount is introduced, period is still valid - let first_era = 1; + let era_1 = 1; let staked_amount = 7; - acc_ledger.staked = StakeAmount::new(0, staked_amount, first_era, period_1); + acc_ledger.staked = StakeAmount { + voting: 0, + build_and_earn: staked_amount, + era: era_1, + period: period_1, + }; assert_eq!( acc_ledger.stakeable_amount(period_1), @@ -493,7 +523,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { assert!(acc_ledger.staked_future.is_none()); // 1st scenario - stake some amount in Voting period, and ensure values are as expected. - let first_era = 1; + let era_1 = 1; let period_1 = 1; let period_info_1 = PeriodInfo { number: period_1, @@ -505,7 +535,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { acc_ledger.add_lock_amount(lock_amount); assert!(acc_ledger - .add_stake_amount(stake_amount, first_era, period_info_1) + .add_stake_amount(stake_amount, era_1, period_info_1) .is_ok()); assert!( @@ -537,9 +567,8 @@ fn account_ledger_add_stake_amount_basic_example_works() { subperiod: Subperiod::BuildAndEarn, subperiod_end_era: 100, }; - assert!(acc_ledger - .add_stake_amount(1, first_era, period_info_2) - .is_ok()); + let era_2 = era_1 + 1; + assert!(acc_ledger.add_stake_amount(1, era_2, period_info_2).is_ok()); assert_eq!(acc_ledger.staked_amount(period_1), stake_amount + 1); assert_eq!( acc_ledger.staked_amount_for_type(Subperiod::Voting, period_1), @@ -558,7 +587,7 @@ fn account_ledger_add_stake_amount_advanced_example_works() { let mut acc_ledger = AccountLedger::::default(); // 1st scenario - stake some amount, and ensure values are as expected. - let first_era = 1; + let era_1 = 1; let period_1 = 1; let period_info_1 = PeriodInfo { number: period_1, @@ -570,12 +599,17 @@ fn account_ledger_add_stake_amount_advanced_example_works() { acc_ledger.add_lock_amount(lock_amount); // We only have entry for the current era - acc_ledger.staked = StakeAmount::new(stake_amount_1, 0, first_era, period_1); + acc_ledger.staked = StakeAmount { + voting: stake_amount_1, + build_and_earn: 0, + era: era_1, + period: period_1, + }; let stake_amount_2 = 2; let acc_ledger_snapshot = acc_ledger.clone(); assert!(acc_ledger - .add_stake_amount(stake_amount_2, first_era, period_info_1) + .add_stake_amount(stake_amount_2, era_1, period_info_1) .is_ok()); assert_eq!( acc_ledger.staked_amount(period_1), @@ -596,7 +630,7 @@ fn account_ledger_add_stake_amount_advanced_example_works() { .for_type(Subperiod::Voting), stake_amount_1 + stake_amount_2 ); - assert_eq!(acc_ledger.staked_future.unwrap().era, first_era + 1); + assert_eq!(acc_ledger.staked_future.unwrap().era, era_1 + 1); } #[test] @@ -605,7 +639,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { let mut acc_ledger = AccountLedger::::default(); // Prep actions - let first_era = 5; + let era_1 = 5; let period_1 = 2; let period_info_1 = PeriodInfo { number: period_1, @@ -616,12 +650,12 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { let stake_amount = 7; acc_ledger.add_lock_amount(lock_amount); assert!(acc_ledger - .add_stake_amount(stake_amount, first_era, period_info_1) + .add_stake_amount(stake_amount, era_1, period_info_1) .is_ok()); - // Try to add to the next era, it should fail. + // Try to add to era after next, it should fail. assert_eq!( - acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), + acc_ledger.add_stake_amount(1, era_1 + 2, period_info_1), Err(AccountLedgerError::InvalidEra) ); @@ -629,7 +663,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { assert_eq!( acc_ledger.add_stake_amount( 1, - first_era, + era_1, PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, @@ -640,17 +674,22 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { ); // Alternative situation - no future entry, only current era - acc_ledger.staked = StakeAmount::new(0, stake_amount, first_era, period_1); + acc_ledger.staked = StakeAmount { + voting: 0, + build_and_earn: stake_amount, + era: era_1, + period: period_1, + }; acc_ledger.staked_future = None; assert_eq!( - acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), + acc_ledger.add_stake_amount(1, era_1 + 1, period_info_1), Err(AccountLedgerError::InvalidEra) ); assert_eq!( acc_ledger.add_stake_amount( 1, - first_era, + era_1, PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, @@ -681,7 +720,7 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { ); // Lock some amount, and try to stake more than that - let first_era = 5; + let era_1 = 5; let period_1 = 2; let period_info_1 = PeriodInfo { number: period_1, @@ -691,16 +730,16 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { let lock_amount = 13; acc_ledger.add_lock_amount(lock_amount); assert_eq!( - acc_ledger.add_stake_amount(lock_amount + 1, first_era, period_info_1), + acc_ledger.add_stake_amount(lock_amount + 1, era_1, period_info_1), Err(AccountLedgerError::UnavailableStakeFunds) ); // Additional check - have some active stake, and then try to overstake assert!(acc_ledger - .add_stake_amount(lock_amount - 2, first_era, period_info_1) + .add_stake_amount(lock_amount - 2, era_1, period_info_1) .is_ok()); assert_eq!( - acc_ledger.add_stake_amount(3, first_era, period_info_1), + acc_ledger.add_stake_amount(3, era_1, period_info_1), Err(AccountLedgerError::UnavailableStakeFunds) ); } @@ -729,7 +768,12 @@ fn account_ledger_unstake_amount_basic_scenario_works() { .is_ok()); // Only 'current' entry has some values, future is set to None. - acc_ledger_2.staked = StakeAmount::new(0, amount_1, era_1, period_1); + acc_ledger_2.staked = StakeAmount { + voting: 0, + build_and_earn: amount_1, + era: era_1, + period: period_1, + }; acc_ledger_2.staked_future = None; for mut acc_ledger in vec![acc_ledger, acc_ledger_2] { @@ -773,8 +817,18 @@ fn account_ledger_unstake_amount_advanced_scenario_works() { acc_ledger.add_lock_amount(amount_1); // We have two entries at once - acc_ledger.staked = StakeAmount::new(amount_1 - 1, 0, era_1, period_1); - acc_ledger.staked_future = Some(StakeAmount::new(amount_1 - 1, 1, era_1 + 1, period_1)); + acc_ledger.staked = StakeAmount { + voting: amount_1 - 1, + build_and_earn: 0, + era: era_1, + period: period_1, + }; + acc_ledger.staked_future = Some(StakeAmount { + voting: amount_1 - 1, + build_and_earn: 1, + era: era_1 + 1, + period: period_1, + }); // 1st scenario - unstake some amount from the current era, both entries should be affected. let unstake_amount_1 = 3; @@ -847,9 +901,15 @@ fn account_ledger_unstake_from_invalid_era_fails() { .add_stake_amount(amount_1, era_1, period_info_1) .is_ok()); - // Try to unstake from the next era, it should fail. + // Try to unstake from the current & next era, it should work. + assert!(acc_ledger.unstake_amount(1, era_1, period_info_1).is_ok()); + assert!(acc_ledger + .unstake_amount(1, era_1 + 1, period_info_1) + .is_ok()); + + // Try to unstake from the stake era + 2, it should fail since it would mean we have unclaimed rewards. assert_eq!( - acc_ledger.unstake_amount(1, era_1 + 1, period_info_1), + acc_ledger.unstake_amount(1, era_1 + 2, period_info_1), Err(AccountLedgerError::InvalidEra) ); @@ -868,7 +928,12 @@ fn account_ledger_unstake_from_invalid_era_fails() { ); // Alternative situation - no future entry, only current era - acc_ledger.staked = StakeAmount::new(0, 1, era_1, period_1); + acc_ledger.staked = StakeAmount { + voting: 0, + build_and_earn: 1, + era: era_1, + period: period_1, + }; acc_ledger.staked_future = None; assert_eq!( @@ -1007,8 +1072,582 @@ fn account_ledger_consume_unlocking_chunks_works() { } #[test] -fn account_ledger_claim_up_to_era_works() { - // TODO!!! +fn account_ledger_expired_cleanup_works() { + get_u32_type!(UnlockingDummy, 5); + + // 1st scenario - nothing is expired + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 3, + build_and_earn: 7, + era: 100, + period: 5, + }; + acc_ledger.staked_future = Some(StakeAmount { + voting: 3, + build_and_earn: 13, + era: 101, + period: 5, + }); + + let acc_ledger_snapshot = acc_ledger.clone(); + + assert!(!acc_ledger.maybe_cleanup_expired(acc_ledger.staked.period - 1)); + assert_eq!( + acc_ledger, acc_ledger_snapshot, + "No change must happen since period hasn't expired." + ); + + assert!(!acc_ledger.maybe_cleanup_expired(acc_ledger.staked.period)); + assert_eq!( + acc_ledger, acc_ledger_snapshot, + "No change must happen since period hasn't expired." + ); + + // 2nd scenario - stake has expired + assert!(acc_ledger.maybe_cleanup_expired(acc_ledger.staked.period + 1)); + assert!(acc_ledger.staked.is_empty()); + assert!(acc_ledger.staked_future.is_none()); +} + +#[test] +fn account_ledger_claim_up_to_era_only_staked_without_cleanup_works() { + get_u32_type!(UnlockingDummy, 5); + let stake_era = 100; + + let acc_ledger_snapshot = { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 3, + build_and_earn: 7, + era: stake_era, + period: 5, + }; + acc_ledger + }; + + // 1st scenario - claim one era, period hasn't ended yet + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, None) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some((stake_era, acc_ledger_snapshot.staked.total())) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked; + expected_stake_amount.era += 1; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Only era should be bumped by 1." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } + + // 2nd scenario - claim multiple eras (5), period hasn't ended yet + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era + 4, None) // staked era + 4 additional eras + .expect("Must provide iter with 5 values."); + + // Iter values are correct + for inc in 0..5 { + assert_eq!( + result_iter.next(), + Some((stake_era + inc, acc_ledger_snapshot.staked.total())) + ); + } + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked; + expected_stake_amount.era += 5; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Only era should be bumped by 5." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } +} + +#[test] +fn account_ledger_claim_up_to_era_only_staked_with_cleanup_works() { + get_u32_type!(UnlockingDummy, 5); + let stake_era = 100; + + let acc_ledger_snapshot = { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 3, + build_and_earn: 7, + era: stake_era, + period: 5, + }; + acc_ledger + }; + + // 1st scenario - claim one era, period has ended + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, Some(stake_era)) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some((stake_era, acc_ledger_snapshot.staked.total())) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are cleaned up + assert!( + acc_ledger.staked.is_empty(), + "Period has ended so stake entry should be cleaned up." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } + + // 2nd scenario - claim multiple eras (5), period has ended + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era + 4, Some(stake_era)) // staked era + 4 additional eras + .expect("Must provide iter with 5 values."); + + for inc in 0..5 { + assert_eq!( + result_iter.next(), + Some((stake_era + inc, acc_ledger_snapshot.staked.total())) + ); + } + assert!(result_iter.next().is_none()); + + // Ledger values are cleaned up + assert!( + acc_ledger.staked.is_empty(), + "Period has ended so stake entry should be cleaned up." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } + + // 3rd scenario - claim one era, period has ended in some future era + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, Some(stake_era + 1)) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some((stake_era, acc_ledger_snapshot.staked.total())) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correctly updated + let mut expected_stake_amount = acc_ledger_snapshot.staked; + expected_stake_amount.era += 1; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Entry must exist since we still haven't reached the period end era." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } +} + +#[test] +fn account_ledger_claim_up_to_era_only_staked_future_without_cleanup_works() { + get_u32_type!(UnlockingDummy, 5); + let stake_era = 50; + + let acc_ledger_snapshot = { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked_future = Some(StakeAmount { + voting: 5, + build_and_earn: 11, + era: stake_era, + period: 4, + }); + acc_ledger + }; + + // 1st scenario - claim one era, period hasn't ended yet + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, None) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some(( + stake_era, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked_future.unwrap(); + expected_stake_amount.era += 1; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Era must be bumped by 1, and entry must switch from staked_future over to staked." + ); + assert!( + acc_ledger.staked_future.is_none(), + "staked_future must be cleaned up after the claim." + ); + } + + // 2nd scenario - claim multiple eras (5), period hasn't ended yet + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era + 4, None) // staked era + 4 additional eras + .expect("Must provide iter with 5 entries."); + + // Iter values are correct + for inc in 0..5 { + assert_eq!( + result_iter.next(), + Some(( + stake_era + inc, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + } + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked_future.unwrap(); + expected_stake_amount.era += 5; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Era must be bumped by 5, and entry must switch from staked_future over to staked." + ); + assert!( + acc_ledger.staked_future.is_none(), + "staked_future must be cleaned up after the claim." + ); + } +} + +#[test] +fn account_ledger_claim_up_to_era_only_staked_future_with_cleanup_works() { + get_u32_type!(UnlockingDummy, 5); + let stake_era = 50; + + let acc_ledger_snapshot = { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked_future = Some(StakeAmount { + voting: 2, + build_and_earn: 17, + era: stake_era, + period: 3, + }); + acc_ledger + }; + + // 1st scenario - claim one era, period has ended + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, Some(stake_era)) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some(( + stake_era, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are cleaned up + assert!( + acc_ledger.staked.is_empty(), + "Period has ended so stake entry should be cleaned up." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } + + // 2nd scenario - claim multiple eras (5), period has ended + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era + 4, Some(stake_era)) // staked era + 4 additional eras + .expect("Must provide iter with 5 entries."); + + for inc in 0..5 { + assert_eq!( + result_iter.next(), + Some(( + stake_era + inc, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + } + assert!(result_iter.next().is_none()); + + // Ledger values are cleaned up + assert!( + acc_ledger.staked.is_empty(), + "Period has ended so stake entry should be cleaned up." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } + + // 3rd scenario - claim one era, period has ended in some future era + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era, Some(stake_era + 1)) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some(( + stake_era, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correctly updated + let mut expected_stake_amount = acc_ledger_snapshot.staked_future.unwrap(); + expected_stake_amount.era += 1; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "Entry must exist since we still haven't reached the period end era." + ); + assert!( + acc_ledger.staked_future.is_none(), + "Was and should remain None." + ); + } +} + +#[test] +fn account_ledger_claim_up_to_era_staked_and_staked_future_works() { + get_u32_type!(UnlockingDummy, 5); + let stake_era_1 = 100; + let stake_era_2 = stake_era_1 + 1; + + let acc_ledger_snapshot = { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 3, + build_and_earn: 7, + era: stake_era_1, + period: 5, + }; + acc_ledger.staked_future = Some(StakeAmount { + voting: 3, + build_and_earn: 11, + era: stake_era_2, + period: 5, + }); + acc_ledger + }; + + // 1st scenario - claim only one era + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era_1, None) + .expect("Must provide iter with exactly one era."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some((stake_era_1, acc_ledger_snapshot.staked.total())) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked; + expected_stake_amount.era += 1; + assert_eq!( + acc_ledger.staked, + acc_ledger_snapshot.staked_future.unwrap(), + "staked_future entry must be moved over to staked." + ); + assert!( + acc_ledger.staked_future.is_none(), + "staked_future is cleaned up since it's been moved over to staked entry." + ); + } + + // 2nd scenario - claim multiple eras (3), period hasn't ended yet, do the cleanup + { + let mut acc_ledger = acc_ledger_snapshot.clone(); + let mut result_iter = acc_ledger + .claim_up_to_era(stake_era_2 + 1, None) // staked era + 2 additional eras + .expect("Must provide iter with exactly two entries."); + + // Iter values are correct + assert_eq!( + result_iter.next(), + Some((stake_era_1, acc_ledger_snapshot.staked.total())) + ); + assert_eq!( + result_iter.next(), + Some(( + stake_era_2, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + assert_eq!( + result_iter.next(), + Some(( + stake_era_2 + 1, + acc_ledger_snapshot.staked_future.unwrap().total() + )) + ); + assert!(result_iter.next().is_none()); + + // Ledger values are correct + let mut expected_stake_amount = acc_ledger_snapshot.staked_future.unwrap(); + expected_stake_amount.era += 2; + assert_eq!( + acc_ledger.staked, expected_stake_amount, + "staked_future must move over to staked, and era must be incremented by 2." + ); + assert!( + acc_ledger.staked_future.is_none(), + "staked_future is cleaned up since it's been moved over to staked entry." + ); + } +} + +#[test] +fn account_ledger_claim_up_to_era_fails_for_historic_eras() { + get_u32_type!(UnlockingDummy, 5); + let stake_era = 50; + + // Only staked entry + { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 2, + build_and_earn: 17, + era: stake_era, + period: 3, + }; + assert_eq!( + acc_ledger.claim_up_to_era(stake_era - 1, None), + Err(AccountLedgerError::NothingToClaim) + ); + } + + // Only staked-future entry + { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked_future = Some(StakeAmount { + voting: 2, + build_and_earn: 17, + era: stake_era, + period: 3, + }); + assert_eq!( + acc_ledger.claim_up_to_era(stake_era - 1, None), + Err(AccountLedgerError::NothingToClaim) + ); + } + + // Both staked and staked-future entries + { + let mut acc_ledger = AccountLedger::::default(); + acc_ledger.staked = StakeAmount { + voting: 2, + build_and_earn: 17, + era: stake_era, + period: 3, + }; + acc_ledger.staked_future = Some(StakeAmount { + voting: 2, + build_and_earn: 19, + era: stake_era + 1, + period: 3, + }); + assert_eq!( + acc_ledger.claim_up_to_era(stake_era - 1, None), + Err(AccountLedgerError::NothingToClaim) + ); + } +} + +#[test] +fn era_stake_pair_iter_works() { + // 1st scenario - only span is given + let (era_1, last_era, amount) = (2, 5, 11); + let mut iter_1 = EraStakePairIter::new((era_1, last_era, amount), None).unwrap(); + for era in era_1..=last_era { + assert_eq!(iter_1.next(), Some((era, amount))); + } + assert!(iter_1.next().is_none()); + + // 2nd scenario - first value & span are given + let (maybe_era_1, maybe_first_amount) = (1, 7); + let maybe_first = Some((maybe_era_1, maybe_first_amount)); + let mut iter_2 = EraStakePairIter::new((era_1, last_era, amount), maybe_first).unwrap(); + + assert_eq!(iter_2.next(), Some((maybe_era_1, maybe_first_amount))); + for era in era_1..=last_era { + assert_eq!(iter_2.next(), Some((era, amount))); + } +} + +#[test] +fn era_stake_pair_iter_returns_error_for_illegal_data() { + // 1st scenario - spans are reversed; first era comes AFTER the last era + let (era_1, last_era, amount) = (2, 5, 11); + assert!(EraStakePairIter::new((last_era, era_1, amount), None).is_err()); + + // 2nd scenario - maybe_first covers the same era as the span + assert!(EraStakePairIter::new((era_1, last_era, amount), Some((era_1, 10))).is_err()); + + // 3rd scenario - maybe_first is before the span, but not exactly 1 era before the first era in the span + assert!(EraStakePairIter::new((era_1, last_era, amount), Some((era_1 - 2, 10))).is_err()); + + assert!( + EraStakePairIter::new((era_1, last_era, amount), Some((era_1 - 1, 10))).is_ok(), + "Sanity check." + ); } #[test] @@ -1017,7 +1656,6 @@ fn era_info_lock_unlock_works() { // Sanity check assert!(era_info.total_locked.is_zero()); - assert!(era_info.active_era_locked.is_zero()); assert!(era_info.unlocking.is_zero()); // Basic add lock @@ -1030,7 +1668,6 @@ fn era_info_lock_unlock_works() { // Basic unlocking started let unlock_amount = 2; era_info.total_locked = 17; - era_info.active_era_locked = 13; let era_info_snapshot = era_info; // First unlock & checks @@ -1039,10 +1676,6 @@ fn era_info_lock_unlock_works() { era_info.total_locked, era_info_snapshot.total_locked - unlock_amount ); - assert_eq!( - era_info.active_era_locked, - era_info_snapshot.active_era_locked - unlock_amount - ); assert_eq!(era_info.unlocking, unlock_amount); // Second unlock and checks @@ -1051,17 +1684,13 @@ fn era_info_lock_unlock_works() { era_info.total_locked, era_info_snapshot.total_locked - unlock_amount * 2 ); - assert_eq!( - era_info.active_era_locked, - era_info_snapshot.active_era_locked - unlock_amount * 2 - ); assert_eq!(era_info.unlocking, unlock_amount * 2); // Claim unlocked chunks let old_era_info = era_info.clone(); era_info.unlocking_removed(1); assert_eq!(era_info.unlocking, old_era_info.unlocking - 1); - assert_eq!(era_info.active_era_locked, old_era_info.active_era_locked); + assert_eq!(era_info.total_locked, old_era_info.total_locked); } #[test] @@ -1111,10 +1740,18 @@ fn era_info_unstake_works() { let bep_stake_amount_2 = bep_stake_amount_1 + 6; let period_number = 1; let era = 2; - era_info.current_stake_amount = - StakeAmount::new(vp_stake_amount, bep_stake_amount_1, era, period_number); - era_info.next_stake_amount = - StakeAmount::new(vp_stake_amount, bep_stake_amount_2, era + 1, period_number); + era_info.current_stake_amount = StakeAmount { + voting: vp_stake_amount, + build_and_earn: bep_stake_amount_1, + era, + period: period_number, + }; + era_info.next_stake_amount = StakeAmount { + voting: vp_stake_amount, + build_and_earn: bep_stake_amount_2, + era: era + 1, + period: period_number, + }; let total_staked = era_info.total_staked_amount(); let total_staked_next_era = era_info.total_staked_amount_next_era(); @@ -1169,6 +1806,88 @@ fn era_info_unstake_works() { .is_zero()); } +#[test] +fn era_info_migrate_to_next_era_works() { + // Make dummy era info with stake amounts + let era_info_snapshot = EraInfo { + total_locked: 456, + unlocking: 13, + current_stake_amount: StakeAmount { + voting: 13, + build_and_earn: 29, + era: 2, + period: 1, + }, + next_stake_amount: StakeAmount { + voting: 13, + build_and_earn: 41, + era: 3, + period: 1, + }, + }; + + // 1st scenario - rollover to next era, no subperiod change + { + let mut era_info = era_info_snapshot; + era_info.migrate_to_next_era(None); + + assert_eq!(era_info.total_locked, era_info_snapshot.total_locked); + assert_eq!(era_info.unlocking, era_info_snapshot.unlocking); + assert_eq!( + era_info.current_stake_amount, + era_info_snapshot.next_stake_amount + ); + + let mut new_next_stake_amount = era_info_snapshot.next_stake_amount; + new_next_stake_amount.era += 1; + assert_eq!(era_info.next_stake_amount, new_next_stake_amount); + } + + // 2nd scenario - rollover to next era, change from Voting into Build&Earn subperiod + { + let mut era_info = era_info_snapshot; + era_info.migrate_to_next_era(Some(Subperiod::BuildAndEarn)); + + assert_eq!(era_info.total_locked, era_info_snapshot.total_locked); + assert_eq!(era_info.unlocking, era_info_snapshot.unlocking); + assert_eq!( + era_info.current_stake_amount, + era_info_snapshot.next_stake_amount + ); + + let mut new_next_stake_amount = era_info_snapshot.next_stake_amount; + new_next_stake_amount.era += 1; + assert_eq!(era_info.next_stake_amount, new_next_stake_amount); + } + + // 3rd scenario - rollover to next era, change from Build&Earn to Voting subperiod + { + let mut era_info = era_info_snapshot; + era_info.migrate_to_next_era(Some(Subperiod::Voting)); + + assert_eq!(era_info.total_locked, era_info_snapshot.total_locked); + assert_eq!(era_info.unlocking, era_info_snapshot.unlocking); + assert_eq!( + era_info.current_stake_amount, + StakeAmount { + voting: Zero::zero(), + build_and_earn: Zero::zero(), + era: era_info_snapshot.current_stake_amount.era + 1, + period: era_info_snapshot.current_stake_amount.period + 1, + } + ); + assert_eq!( + era_info.next_stake_amount, + StakeAmount { + voting: Zero::zero(), + build_and_earn: Zero::zero(), + era: era_info_snapshot.current_stake_amount.era + 2, + period: era_info_snapshot.current_stake_amount.period + 1, + } + ); + } +} + #[test] fn stake_amount_works() { let mut stake_amount = StakeAmount::default(); @@ -1243,11 +1962,14 @@ fn singular_staking_info_basics_are_ok() { assert_eq!(staking_info.period_number(), period_number); assert!(staking_info.is_loyal()); assert!(staking_info.total_staked_amount().is_zero()); + assert!(staking_info.is_empty()); + assert!(staking_info.era().is_zero()); assert!(!SingularStakingInfo::new(period_number, Subperiod::BuildAndEarn).is_loyal()); // Add some staked amount during `Voting` period + let era_1 = 7; let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, Subperiod::Voting); + staking_info.stake(vote_stake_amount_1, era_1, Subperiod::Voting); assert_eq!(staking_info.total_staked_amount(), vote_stake_amount_1); assert_eq!( staking_info.staked_amount(Subperiod::Voting), @@ -1256,10 +1978,16 @@ fn singular_staking_info_basics_are_ok() { assert!(staking_info .staked_amount(Subperiod::BuildAndEarn) .is_zero()); + assert_eq!( + staking_info.era(), + era_1 + 1, + "Stake era should remain valid." + ); // Add some staked amount during `BuildAndEarn` period + let era_2 = 9; let bep_stake_amount_1 = 23; - staking_info.stake(bep_stake_amount_1, Subperiod::BuildAndEarn); + staking_info.stake(bep_stake_amount_1, era_2, Subperiod::BuildAndEarn); assert_eq!( staking_info.total_staked_amount(), vote_stake_amount_1 + bep_stake_amount_1 @@ -1272,6 +2000,7 @@ fn singular_staking_info_basics_are_ok() { staking_info.staked_amount(Subperiod::BuildAndEarn), bep_stake_amount_1 ); + assert_eq!(staking_info.era(), era_2 + 1); } #[test] @@ -1281,13 +2010,14 @@ fn singular_staking_info_unstake_during_voting_is_ok() { let mut staking_info = SingularStakingInfo::new(period_number, subperiod); // Prep actions + let era_1 = 2; let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, Subperiod::Voting); + staking_info.stake(vote_stake_amount_1, era_1, Subperiod::Voting); // Unstake some amount during `Voting` period, loyalty should remain as expected. let unstake_amount_1 = 5; assert_eq!( - staking_info.unstake(unstake_amount_1, Subperiod::Voting), + staking_info.unstake(unstake_amount_1, era_1, Subperiod::Voting), (unstake_amount_1, Balance::zero()) ); assert_eq!( @@ -1295,15 +2025,22 @@ fn singular_staking_info_unstake_during_voting_is_ok() { vote_stake_amount_1 - unstake_amount_1 ); assert!(staking_info.is_loyal()); + assert_eq!( + staking_info.era(), + era_1 + 1, + "Stake era should remain valid." + ); // Fully unstake, attempting to undersaturate, and ensure loyalty flag is still true. + let era_2 = era_1 + 2; let remaining_stake = staking_info.total_staked_amount(); assert_eq!( - staking_info.unstake(remaining_stake + 1, Subperiod::Voting), + staking_info.unstake(remaining_stake + 1, era_2, Subperiod::Voting), (remaining_stake, Balance::zero()) ); assert!(staking_info.total_staked_amount().is_zero()); assert!(staking_info.is_loyal()); + assert_eq!(staking_info.era(), era_2); } #[test] @@ -1313,15 +2050,16 @@ fn singular_staking_info_unstake_during_bep_is_ok() { let mut staking_info = SingularStakingInfo::new(period_number, subperiod); // Prep actions + let era_1 = 3; let vote_stake_amount_1 = 11; - staking_info.stake(vote_stake_amount_1, Subperiod::Voting); + staking_info.stake(vote_stake_amount_1, era_1 - 1, Subperiod::Voting); let bep_stake_amount_1 = 23; - staking_info.stake(bep_stake_amount_1, Subperiod::BuildAndEarn); + staking_info.stake(bep_stake_amount_1, era_1, Subperiod::BuildAndEarn); // 1st scenario - Unstake some of the amount staked during B&E period let unstake_1 = 5; assert_eq!( - staking_info.unstake(5, Subperiod::BuildAndEarn), + staking_info.unstake(5, era_1, Subperiod::BuildAndEarn), (Balance::zero(), unstake_1) ); assert_eq!( @@ -1337,6 +2075,11 @@ fn singular_staking_info_unstake_during_bep_is_ok() { bep_stake_amount_1 - unstake_1 ); assert!(staking_info.is_loyal()); + assert_eq!( + staking_info.era(), + era_1 + 1, + "Stake era should remain valid." + ); // 2nd scenario - unstake all of the amount staked during B&E period, and then some more. // The point is to take a chunk from the voting period stake too. @@ -1344,9 +2087,10 @@ fn singular_staking_info_unstake_during_bep_is_ok() { let current_bep_stake = staking_info.staked_amount(Subperiod::BuildAndEarn); let voting_stake_overflow = 2; let unstake_2 = current_bep_stake + voting_stake_overflow; + let era_2 = era_1 + 3; assert_eq!( - staking_info.unstake(unstake_2, Subperiod::BuildAndEarn), + staking_info.unstake(unstake_2, era_2, Subperiod::BuildAndEarn), (voting_stake_overflow, current_bep_stake) ); assert_eq!( @@ -1364,49 +2108,129 @@ fn singular_staking_info_unstake_during_bep_is_ok() { !staking_info.is_loyal(), "Loyalty flag should have been removed due to non-zero voting period unstake" ); + assert_eq!(staking_info.era(), era_2); } #[test] -fn contract_stake_info_get_works() { - let info_1 = StakeAmount::new(0, 0, 4, 2); - let info_2 = StakeAmount::new(11, 0, 7, 3); +fn contract_stake_amount_basic_get_checks_work() { + // Sanity checks for empty struct + let contract_stake = ContractStakeAmount { + staked: Default::default(), + staked_future: None, + tier_label: None, + }; + assert!(contract_stake.is_empty()); + assert!(contract_stake.latest_stake_period().is_none()); + assert!(contract_stake.latest_stake_era().is_none()); + assert!(contract_stake.total_staked_amount(0).is_zero()); + assert!(contract_stake.staked_amount(0, Subperiod::Voting).is_zero()); + assert!(contract_stake + .staked_amount(0, Subperiod::BuildAndEarn) + .is_zero()); + let era = 3; + let period = 2; + let amount = StakeAmount { + voting: 11, + build_and_earn: 17, + era, + period, + }; let contract_stake = ContractStakeAmount { - staked: info_1, - staked_future: Some(info_2), + staked: amount, + staked_future: None, + tier_label: None, }; + assert!(!contract_stake.is_empty()); - // Sanity check + // Checks for illegal periods + for illegal_period in [period - 1, period + 1] { + assert!(contract_stake.total_staked_amount(illegal_period).is_zero()); + assert!(contract_stake + .staked_amount(illegal_period, Subperiod::Voting) + .is_zero()); + assert!(contract_stake + .staked_amount(illegal_period, Subperiod::BuildAndEarn) + .is_zero()); + } + + // Check for the valid period + assert_eq!(contract_stake.latest_stake_period(), Some(period)); + assert_eq!(contract_stake.latest_stake_era(), Some(era)); + assert_eq!(contract_stake.total_staked_amount(period), amount.total()); + assert_eq!( + contract_stake.staked_amount(period, Subperiod::Voting), + amount.voting + ); + assert_eq!( + contract_stake.staked_amount(period, Subperiod::BuildAndEarn), + amount.build_and_earn + ); +} + +#[test] +fn contract_stake_amount_advanced_get_checks_work() { + let (era_1, era_2) = (4, 7); + let period = 2; + let amount_1 = StakeAmount { + voting: 11, + build_and_earn: 0, + era: era_1, + period, + }; + let amount_2 = StakeAmount { + voting: 11, + build_and_earn: 13, + era: era_2, + period, + }; + + let contract_stake = ContractStakeAmount { + staked: amount_1, + staked_future: Some(amount_2), + tier_label: None, + }; + + // Sanity checks - all values from the 'future' entry should be relevant assert!(!contract_stake.is_empty()); + assert_eq!(contract_stake.latest_stake_period(), Some(period)); + assert_eq!(contract_stake.latest_stake_era(), Some(era_2)); + assert_eq!(contract_stake.total_staked_amount(period), amount_2.total()); + assert_eq!( + contract_stake.staked_amount(period, Subperiod::Voting), + amount_2.voting + ); + assert_eq!( + contract_stake.staked_amount(period, Subperiod::BuildAndEarn), + amount_2.build_and_earn + ); // 1st scenario - get existing entries - assert_eq!(contract_stake.get(4, 2), Some(info_1)); - assert_eq!(contract_stake.get(7, 3), Some(info_2)); + assert_eq!(contract_stake.get(era_1, period), Some(amount_1)); + assert_eq!(contract_stake.get(era_2, period), Some(amount_2)); // 2nd scenario - get non-existing entries for covered eras - { - let era_1 = 6; - let entry_1 = contract_stake.get(era_1, 2).expect("Has to be Some"); - assert!(entry_1.total().is_zero()); - assert_eq!(entry_1.era, era_1); - assert_eq!(entry_1.period, 2); - - let era_2 = 8; - let entry_1 = contract_stake.get(era_2, 3).expect("Has to be Some"); - assert_eq!(entry_1.total(), 11); - assert_eq!(entry_1.era, era_2); - assert_eq!(entry_1.period, 3); - } + let era_3 = era_2 - 1; + let entry_1 = contract_stake.get(era_3, 2).expect("Has to be Some"); + assert_eq!(entry_1.total(), amount_1.total()); + assert_eq!(entry_1.era, era_3); + assert_eq!(entry_1.period, period); + + let era_4 = era_2 + 1; + let entry_1 = contract_stake.get(era_4, period).expect("Has to be Some"); + assert_eq!(entry_1.total(), amount_2.total()); + assert_eq!(entry_1.era, era_4); + assert_eq!(entry_1.period, period); // 3rd scenario - get non-existing entries for covered eras but mismatching period - assert!(contract_stake.get(8, 2).is_none()); + assert!(contract_stake.get(8, period + 1).is_none()); // 4th scenario - get non-existing entries for non-covered eras - assert!(contract_stake.get(3, 2).is_none()); + assert!(contract_stake.get(3, period).is_none()); } #[test] -fn contract_stake_info_stake_is_ok() { +fn contract_stake_amount_stake_is_ok() { let mut contract_stake = ContractStakeAmount::default(); // 1st scenario - stake some amount and verify state change @@ -1421,6 +2245,11 @@ fn contract_stake_info_stake_is_ok() { let amount_1 = 31; contract_stake.stake(amount_1, period_info_1, era_1); assert!(!contract_stake.is_empty()); + assert!( + contract_stake.staked.is_empty(), + "Only future entry should be modified." + ); + assert!(contract_stake.staked_future.is_some()); assert!( contract_stake.get(era_1, period_1).is_none(), @@ -1432,6 +2261,8 @@ fn contract_stake_info_stake_is_ok() { "Stake is only valid from next era." ); assert_eq!(entry_1_1.total(), amount_1); + assert_eq!(entry_1_1.for_type(Subperiod::Voting), amount_1); + assert!(entry_1_1.for_type(Subperiod::BuildAndEarn).is_zero()); // 2nd scenario - stake some more to the same era but different period type, and verify state change. let period_info_1 = PeriodInfo { @@ -1443,6 +2274,13 @@ fn contract_stake_info_stake_is_ok() { let entry_1_2 = contract_stake.get(stake_era_1, period_1).unwrap(); assert_eq!(entry_1_2.era, stake_era_1); assert_eq!(entry_1_2.total(), amount_1 * 2); + assert_eq!(entry_1_2.for_type(Subperiod::Voting), amount_1); + assert_eq!(entry_1_2.for_type(Subperiod::BuildAndEarn), amount_1); + assert!( + contract_stake.staked.is_empty(), + "Only future entry should be modified." + ); + assert!(contract_stake.staked_future.is_some()); // 3rd scenario - stake more to the next era, while still in the same period. let era_2 = era_1 + 2; @@ -1459,6 +2297,11 @@ fn contract_stake_info_stake_is_ok() { entry_2_1.total() + amount_2, "Since it's the same period, stake amount must carry over from the previous entry." ); + assert!( + !contract_stake.staked.is_empty(), + "staked should keep the old future entry" + ); + assert!(contract_stake.staked_future.is_some()); // 4th scenario - stake some more to the next era, but this time also bump the period. let era_3 = era_2 + 3; @@ -1488,6 +2331,11 @@ fn contract_stake_info_stake_is_ok() { amount_3, "No carry over from previous entry since period has changed." ); + assert!( + contract_stake.staked.is_empty(), + "New period, all stakes should be reset so 'staked' should be empty." + ); + assert!(contract_stake.staked_future.is_some()); // 5th scenario - stake to the next era let era_4 = era_3 + 1; @@ -1500,10 +2348,15 @@ fn contract_stake_info_stake_is_ok() { assert_eq!(entry_4_2.era, stake_era_4); assert_eq!(entry_4_2.period, period_2); assert_eq!(entry_4_2.total(), amount_3 + amount_4); + assert!( + !contract_stake.staked.is_empty(), + "staked should keep the old future entry" + ); + assert!(contract_stake.staked_future.is_some()); } #[test] -fn contract_stake_info_unstake_is_ok() { +fn contract_stake_amount_unstake_is_ok() { let mut contract_stake = ContractStakeAmount::default(); // Prep action - create a stake entry @@ -1528,24 +2381,65 @@ fn contract_stake_info_unstake_is_ok() { contract_stake.staked_amount(period, Subperiod::Voting), stake_amount - amount_1 ); + assert!(contract_stake.staked.is_empty()); + assert!(contract_stake.staked_future.is_some()); - // 2nd scenario - unstake in the future era, entries should be aligned to the current era + // 2nd scenario - unstake in the next era let period_info = PeriodInfo { number: period, subperiod: Subperiod::BuildAndEarn, subperiod_end_era: 40, }; - let era_2 = era_1 + 3; + let era_2 = era_1 + 1; + + contract_stake.unstake(amount_1, period_info, era_2); + assert_eq!( + contract_stake.total_staked_amount(period), + stake_amount - amount_1 * 2 + ); + assert_eq!( + contract_stake.staked_amount(period, Subperiod::Voting), + stake_amount - amount_1 * 2 + ); + assert!( + !contract_stake.staked.is_empty(), + "future entry should be moved over to the current entry" + ); + assert!( + contract_stake.staked_future.is_none(), + "future entry should be cleaned up since it refers to the current era" + ); + + // 3rd scenario - bump up unstake eras by more than 1, entries should be aligned to the current era + let era_3 = era_2 + 3; let amount_2 = 7; - contract_stake.unstake(amount_2, period_info, era_2); + contract_stake.unstake(amount_2, period_info, era_3); assert_eq!( contract_stake.total_staked_amount(period), - stake_amount - amount_1 - amount_2 + stake_amount - amount_1 * 2 - amount_2 ); assert_eq!( contract_stake.staked_amount(period, Subperiod::Voting), - stake_amount - amount_1 - amount_2 + stake_amount - amount_1 * 2 - amount_2 + ); + assert_eq!( + contract_stake.staked.era, era_3, + "Should be aligned to the current era." + ); + assert!( + contract_stake.staked_future.is_none(), + "future enry should remain 'None'" + ); + + // 4th scenario - do a full unstake with existing future entry, expect a cleanup + contract_stake.stake(stake_amount, period_info, era_3); + contract_stake.unstake( + contract_stake.total_staked_amount(period), + period_info, + era_3, ); + assert!(contract_stake.staked.is_empty()); + assert!(contract_stake.staked_future.is_none()); } #[test] @@ -1586,6 +2480,14 @@ fn era_reward_span_push_and_get_works() { // Get the values and verify they are as expected assert_eq!(era_reward_span.get(era_1), Some(&era_reward_1)); assert_eq!(era_reward_span.get(era_2), Some(&era_reward_2)); + + // Try and get the values outside of the span + assert!(era_reward_span + .get(era_reward_span.first_era() - 1) + .is_none()); + assert!(era_reward_span + .get(era_reward_span.last_era() + 1) + .is_none()); } #[test] @@ -1622,7 +2524,102 @@ fn era_reward_span_fails_when_expected() { } #[test] -fn tier_slot_configuration_basic_tests() { +fn tier_threshold_is_ok() { + let amount = 100; + + // Fixed TVL + let fixed_threshold = TierThreshold::FixedTvlAmount { amount }; + assert!(fixed_threshold.is_satisfied(amount)); + assert!(fixed_threshold.is_satisfied(amount + 1)); + assert!(!fixed_threshold.is_satisfied(amount - 1)); + + // Dynamic TVL + let dynamic_threshold = TierThreshold::DynamicTvlAmount { + amount, + minimum_amount: amount / 2, // not important + }; + assert!(dynamic_threshold.is_satisfied(amount)); + assert!(dynamic_threshold.is_satisfied(amount + 1)); + assert!(!dynamic_threshold.is_satisfied(amount - 1)); +} + +#[test] +fn tier_params_check_is_ok() { + // Prepare valid params + get_u32_type!(TiersNum, 3); + let params = TierParameters:: { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(60), + Permill::from_percent(30), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(70), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { + amount: 1000, + minimum_amount: 100, + }, + TierThreshold::DynamicTvlAmount { + amount: 100, + minimum_amount: 10, + }, + TierThreshold::FixedTvlAmount { amount: 10 }, + ]) + .unwrap(), + }; + assert!(params.is_valid()); + + // 1st scenario - sums are below 100%, and that is ok + let mut new_params = params.clone(); + new_params.reward_portion = BoundedVec::try_from(vec![ + Permill::from_percent(59), + Permill::from_percent(30), + Permill::from_percent(10), + ]) + .unwrap(); + new_params.slot_distribution = BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(19), + Permill::from_percent(70), + ]) + .unwrap(); + assert!(params.is_valid()); + + // 2nd scenario - reward portion is too much + let mut new_params = params.clone(); + new_params.reward_portion = BoundedVec::try_from(vec![ + Permill::from_percent(61), + Permill::from_percent(30), + Permill::from_percent(10), + ]) + .unwrap(); + assert!(!new_params.is_valid()); + + // 3rd scenario - tier distribution is too much + let mut new_params = params.clone(); + new_params.slot_distribution = BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(71), + ]) + .unwrap(); + assert!(!new_params.is_valid()); + + // 4th scenario - incorrect vector length + let mut new_params = params.clone(); + new_params.tier_thresholds = + BoundedVec::try_from(vec![TierThreshold::FixedTvlAmount { amount: 10 }]).unwrap(); + assert!(!new_params.is_valid()); +} + +#[test] +fn tier_configuration_basic_tests() { // TODO: this should be expanded & improved later get_u32_type!(TiersNum, 4); let params = TierParameters:: { @@ -1669,9 +2666,79 @@ fn tier_slot_configuration_basic_tests() { assert!(init_config.is_valid(), "Init config must be valid!"); // Create a new config, based on a new price - let new_price = FixedU64::from_rational(20, 100); // in production will be expressed in USD - let new_config = init_config.calculate_new(new_price, ¶ms); + let high_price = FixedU64::from_rational(20, 100); // in production will be expressed in USD + let new_config = init_config.calculate_new(high_price, ¶ms); + assert!(new_config.is_valid()); + + let low_price = FixedU64::from_rational(1, 100); // in production will be expressed in USD + let new_config = init_config.calculate_new(low_price, ¶ms); assert!(new_config.is_valid()); // TODO: expand tests, add more sanity checks (e.g. tier 3 requirement should never be lower than tier 4, etc.) } + +#[test] +fn dapp_tier_rewards_basic_tests() { + get_u32_type!(NumberOfDApps, 8); + get_u32_type!(NumberOfTiers, 3); + + // Example dApps & rewards + let dapps = vec![ + DAppTier { + dapp_id: 1, + tier_id: Some(0), + }, + DAppTier { + dapp_id: 2, + tier_id: Some(0), + }, + DAppTier { + dapp_id: 3, + tier_id: Some(1), + }, + DAppTier { + dapp_id: 5, + tier_id: Some(1), + }, + DAppTier { + dapp_id: 6, + tier_id: Some(2), + }, + ]; + let tier_rewards = vec![300, 20, 1]; + let period = 2; + + let mut dapp_tier_rewards = DAppTierRewards::::new( + dapps.clone(), + tier_rewards.clone(), + period, + ) + .expect("Bounds are respected."); + + // 1st scenario - claim reward for a dApps + let tier_id = dapps[0].tier_id.unwrap(); + assert_eq!( + dapp_tier_rewards.try_claim(dapps[0].dapp_id), + Ok((tier_rewards[tier_id as usize], tier_id)) + ); + + let tier_id = dapps[3].tier_id.unwrap(); + assert_eq!( + dapp_tier_rewards.try_claim(dapps[3].dapp_id), + Ok((tier_rewards[tier_id as usize], tier_id)) + ); + + // 2nd scenario - try to claim already claimed reward + assert_eq!( + dapp_tier_rewards.try_claim(dapps[0].dapp_id), + Err(DAppTierError::RewardAlreadyClaimed), + "Cannot claim the same reward twice." + ); + + // 3rd scenario - claim for a dApp that is not in the list + assert_eq!( + dapp_tier_rewards.try_claim(4), + Err(DAppTierError::NoDAppInTiers), + "dApp doesn't exist in the list so no rewards can be claimed." + ); +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 8949519e5a..b629a82774 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -63,14 +63,13 @@ //! * `DAppTier` - a compact struct describing a dApp's tier. //! * `DAppTierRewards` - composite of `DAppTier` objects, describing the entire reward distribution for a particular era. //! -//! TODO: some types are missing so double check before final merge that everything is covered and explained correctly use frame_support::{pallet_prelude::*, BoundedVec}; use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, UniqueSaturatedInto, Zero}, + traits::{AtLeast32BitUnsigned, CheckedAdd, UniqueSaturatedInto, Zero}, FixedPointNumber, Permill, Saturating, }; pub use sp_std::{fmt::Debug, vec::Vec}; @@ -84,16 +83,7 @@ pub type AccountLedgerFor = AccountLedger, ::M // Convenience type for `DAppTierRewards` usage. pub type DAppTierRewardsFor = - DAppTierRewards, ::NumberOfTiers>; - -// Helper struct for converting `u16` getter into `u32` -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct MaxNumberOfContractsU32(PhantomData); -impl Get for MaxNumberOfContractsU32 { - fn get() -> u32 { - T::MaxNumberOfContracts::get() as u32 - } -} + DAppTierRewards<::MaxNumberOfContracts, ::NumberOfTiers>; /// Era number type pub type EraNumber = u32; @@ -121,6 +111,8 @@ pub enum AccountLedgerError { NothingToClaim, /// Rewards have already been claimed AlreadyClaimed, + /// Attempt to crate the iterator failed due to incorrect data. + InvalidIterator, } /// Distinct subperiods in dApp staking protocol. @@ -148,7 +140,7 @@ pub struct PeriodInfo { /// Period number. #[codec(compact)] pub number: PeriodNumber, - /// subperiod. + /// Subperiod type. pub subperiod: Subperiod, /// Last era of the subperiod, after this a new subperiod should start. #[codec(compact)] @@ -157,7 +149,7 @@ pub struct PeriodInfo { impl PeriodInfo { /// `true` if the provided era belongs to the next period, `false` otherwise. - /// It's only possible to provide this information for the `BuildAndEarn` subperiod. + /// It's only possible to provide this information correctly for the ongoing `BuildAndEarn` subperiod. pub fn is_next_period(&self, era: EraNumber) -> bool { self.subperiod == Subperiod::BuildAndEarn && self.subperiod_end_era <= era } @@ -238,7 +230,7 @@ where self.period_info.subperiod_end_era } - /// Checks whether a new era should be triggered, based on the provided `BlockNumber` argument + /// Checks whether a new era should be triggered, based on the provided _current_ block number argument /// or possibly other protocol state parameters. pub fn is_new_era(&self, now: BlockNumber) -> bool { self.next_era_start <= now @@ -286,6 +278,8 @@ pub struct DAppInfo { pub state: DAppState, // If `None`, rewards goes to the developer account, otherwise to the account Id in `Some`. pub reward_destination: Option, + /// If `Some(_)` dApp has a tier label which can influence the tier assignment. + pub tier_label: Option, } impl DAppInfo { @@ -296,6 +290,11 @@ impl DAppInfo { None => &self.owner, } } + + /// `true` if dApp is still active (registered), `false` otherwise. + pub fn is_active(&self) -> bool { + self.state == DAppState::Registered + } } /// How much was unlocked in some block. @@ -337,10 +336,10 @@ pub struct AccountLedger< BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, > { - /// How much active locked amount an account has. + /// How much active locked amount an account has. This can be used for staking. #[codec(compact)] pub locked: Balance, - /// Vector of all the unlocking chunks. + /// Vector of all the unlocking chunks. This is also considered _locked_ but cannot be used for staking. pub unlocking: BoundedVec, UnlockingLen>, /// Primary field used to store how much was staked in a particular era. pub staked: StakeAmount, @@ -518,8 +517,10 @@ where } } - /// Verify that current era and period info arguments are valid for `stake` and `unstake` operations. - fn verify_stake_unstake_args( + /// Check for stake/unstake operation era & period arguments. + /// + /// Ensures that the provided era & period are valid according to the current ledger state. + fn stake_unstake_argument_check( &self, era: EraNumber, current_period_info: &PeriodInfo, @@ -532,17 +533,15 @@ where if self.staked.period != current_period_info.number { return Err(AccountLedgerError::InvalidPeriod); } - // In case it doesn't (i.e. first time staking), then the future era must match exactly - // one era after the one provided via argument. + // In case it doesn't (i.e. first time staking), then the future era must either be the current or the next era. } else if let Some(stake_amount) = self.staked_future { - if stake_amount.era != era.saturating_add(1) { + if stake_amount.era != era.saturating_add(1) && stake_amount.era != era { return Err(AccountLedgerError::InvalidEra); } if stake_amount.period != current_period_info.number { return Err(AccountLedgerError::InvalidPeriod); } } - Ok(()) } @@ -567,7 +566,7 @@ where return Ok(()); } - self.verify_stake_unstake_args(era, ¤t_period_info)?; + self.stake_unstake_argument_check(era, ¤t_period_info)?; if self.stakeable_amount(current_period_info.number) < amount { return Err(AccountLedgerError::UnavailableStakeFunds); @@ -605,7 +604,7 @@ where return Ok(()); } - self.verify_stake_unstake_args(era, ¤t_period_info)?; + self.stake_unstake_argument_check(era, ¤t_period_info)?; // User must be precise with their unstake amount. if self.staked_amount(current_period_info.number) < amount { @@ -653,12 +652,12 @@ where /// Cleanup staking information if it has expired. /// /// # Args - /// `threshold_period` - last period for which entries can still be considered valid. + /// `valid_threshold_period` - last period for which entries can still be considered valid. /// /// `true` if any change was made, `false` otherwise. - pub fn maybe_cleanup_expired(&mut self, threshold_period: PeriodNumber) -> bool { + pub fn maybe_cleanup_expired(&mut self, valid_threshold_period: PeriodNumber) -> bool { match self.staked_period() { - Some(staked_period) if staked_period < threshold_period => { + Some(staked_period) if staked_period < valid_threshold_period => { self.staked = Default::default(); self.staked_future = None; true @@ -678,22 +677,23 @@ where period_end: Option, ) -> Result { // Main entry exists, but era isn't 'in history' - if !self.staked.is_empty() && era <= self.staked.era { - return Err(AccountLedgerError::NothingToClaim); + if !self.staked.is_empty() { + ensure!(era >= self.staked.era, AccountLedgerError::NothingToClaim); } else if let Some(stake_amount) = self.staked_future { // Future entry exists, but era isn't 'in history' - if era < stake_amount.era { - return Err(AccountLedgerError::NothingToClaim); - } + ensure!(era >= stake_amount.era, AccountLedgerError::NothingToClaim); } // There are multiple options: // 1. We only have future entry, no current entry - // 2. We have both current and future entry - // 3. We only have current entry, no future entry + // 2. We have both current and future entry, but are only claiming 1 era + // 3. We have both current and future entry, and are claiming multiple eras + // 4. We only have current entry, no future entry let (span, maybe_first) = if let Some(stake_amount) = self.staked_future { if self.staked.is_empty() { ((stake_amount.era, era, stake_amount.total()), None) + } else if self.staked.era == era { + ((era, era, self.staked.total()), None) } else { ( (stake_amount.era, era, stake_amount.total()), @@ -704,7 +704,8 @@ where ((self.staked.era, era, self.staked.total()), None) }; - let result = EraStakePairIter::new(span, maybe_first); + let result = EraStakePairIter::new(span, maybe_first) + .map_err(|_| AccountLedgerError::InvalidIterator)?; // Rollover future to 'current' stake amount if let Some(stake_amount) = self.staked_future.take() { @@ -714,7 +715,7 @@ where // Make sure to clean up the entries if all rewards for the period have been claimed. match period_end { - Some(subperiod_end_era) if era >= subperiod_end_era => { + Some(period_end_era) if era >= period_end_era => { self.staked = Default::default(); self.staked_future = None; } @@ -752,20 +753,25 @@ impl EraStakePairIter { pub fn new( span: (EraNumber, EraNumber, Balance), maybe_first: Option<(EraNumber, Balance)>, - ) -> Self { - if let Some((era, _amount)) = maybe_first { - debug_assert!( - span.0 == era + 1, - "The 'other', if it exists, must cover era preceding the span." - ); + ) -> Result { + // First era must be smaller or equal to the last era. + if span.0 > span.1 { + return Err(()); + } + // If 'maybe_first' is defined, it must exactly match the `span.0 - 1` era value. + match maybe_first { + Some((era, _)) if span.0.saturating_sub(era) != 1 => { + return Err(()); + } + _ => (), } - Self { + Ok(Self { maybe_first, start_era: span.0, end_era: span.1, amount: span.2, - } + }) } } @@ -807,21 +813,6 @@ pub struct StakeAmount { } impl StakeAmount { - /// Create new instance of `StakeAmount` with specified `voting` and `build_and_earn` amounts. - pub fn new( - voting: Balance, - build_and_earn: Balance, - era: EraNumber, - period: PeriodNumber, - ) -> Self { - Self { - voting, - build_and_earn, - era, - period, - } - } - /// `true` if nothing is staked, `false` otherwise pub fn is_empty(&self) -> bool { self.voting.is_zero() && self.build_and_earn.is_zero() @@ -862,6 +853,10 @@ impl StakeAmount { self.build_and_earn.saturating_reduce(amount); } else { // Rollover from build&earn to voting, is guaranteed to be larger than zero due to previous check + // E.g. voting = 10, build&earn = 5, amount = 7 + // underflow = build&earn - amount = 5 - 7 = -2 + // voting = 10 - 2 = 8 + // build&earn = 0 let remainder = amount.saturating_sub(self.build_and_earn); self.build_and_earn = Balance::zero(); self.voting.saturating_reduce(remainder); @@ -874,12 +869,8 @@ impl StakeAmount { /// Info about current era, including the rewards, how much is locked, unlocking, etc. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] pub struct EraInfo { - /// How much balance is considered to be locked in the current era. - /// This value influences the reward distribution. - #[codec(compact)] - pub active_era_locked: Balance, - /// How much balance is locked in dApp staking, in total. - /// For rewards, this amount isn't relevant for the current era, but only from the next one. + /// How much balance is locked in dApp staking. + /// Does not include the amount that is undergoing the unlocking period. #[codec(compact)] pub total_locked: Balance, /// How much balance is undergoing unlocking process. @@ -900,7 +891,6 @@ impl EraInfo { /// Update with the new amount that has just started undergoing the unlocking period. pub fn unlocking_started(&mut self, amount: Balance) { - self.active_era_locked.saturating_reduce(amount); self.total_locked.saturating_reduce(amount); self.unlocking.saturating_accrue(amount); } @@ -946,15 +936,19 @@ impl EraInfo { /// ## Args /// `next_subperiod` - `None` if no subperiod change, `Some(type)` if `type` is starting from the next era. pub fn migrate_to_next_era(&mut self, next_subperiod: Option) { - self.active_era_locked = self.total_locked; match next_subperiod { // If next era marks start of new voting period period, it means we're entering a new period Some(Subperiod::Voting) => { - self.current_stake_amount = Default::default(); - self.next_stake_amount = Default::default(); + for stake_amount in [&mut self.current_stake_amount, &mut self.next_stake_amount] { + stake_amount.voting = Zero::zero(); + stake_amount.build_and_earn = Zero::zero(); + stake_amount.era.saturating_inc(); + stake_amount.period.saturating_inc(); + } } Some(Subperiod::BuildAndEarn) | None => { self.current_stake_amount = self.next_stake_amount; + self.next_stake_amount.era.saturating_inc(); } }; } @@ -980,16 +974,22 @@ impl SingularStakingInfo { /// `subperiod` - subperiod during which this entry is created. pub fn new(period: PeriodNumber, subperiod: Subperiod) -> Self { Self { - staked: StakeAmount::new(Balance::zero(), Balance::zero(), 0, period), + staked: StakeAmount { + voting: Balance::zero(), + build_and_earn: Balance::zero(), + era: 0, + period, + }, // Loyalty staking is only possible if stake is first made during the voting period. loyal_staker: subperiod == Subperiod::Voting, } } /// Stake the specified amount on the contract, for the specified subperiod. - pub fn stake(&mut self, amount: Balance, subperiod: Subperiod) { - // TODO: if we keep `StakeAmount` type here, consider including the era as well for consistency + pub fn stake(&mut self, amount: Balance, current_era: EraNumber, subperiod: Subperiod) { self.staked.add(amount, subperiod); + // Stake is only valid from the next era so we keep it consistent here + self.staked.era = current_era.saturating_add(1); } /// Unstakes some of the specified amount from the contract. @@ -998,10 +998,17 @@ impl SingularStakingInfo { /// and `voting period` has passed, this will remove the _loyalty_ flag from the staker. /// /// Returns the amount that was unstaked from the `voting period` stake, and from the `build&earn period` stake. - pub fn unstake(&mut self, amount: Balance, subperiod: Subperiod) -> (Balance, Balance) { + pub fn unstake( + &mut self, + amount: Balance, + current_era: EraNumber, + subperiod: Subperiod, + ) -> (Balance, Balance) { let snapshot = self.staked; self.staked.subtract(amount, subperiod); + // Keep the latest era for which the entry is valid + self.staked.era = self.staked.era.max(current_era); self.loyal_staker = self.loyal_staker && (subperiod == Subperiod::Voting @@ -1036,6 +1043,11 @@ impl SingularStakingInfo { self.staked.period } + /// Era in which the entry was last time updated + pub fn era(&self) -> EraNumber { + self.staked.era + } + /// `true` if no stake exists, `false` otherwise. pub fn is_empty(&self) -> bool { self.staked.is_empty() @@ -1056,7 +1068,10 @@ pub struct ContractStakeAmount { pub staked: StakeAmount, /// Staked amount in the next or 'future' era. pub staked_future: Option, + /// Tier label for the contract, if any. + pub tier_label: Option, } + impl ContractStakeAmount { /// `true` if series is empty, `false` otherwise. pub fn is_empty(&self) -> bool { @@ -1130,7 +1145,6 @@ impl ContractStakeAmount { /// Stake the specified `amount` on the contract, for the specified `subperiod` and `era`. pub fn stake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { - // TODO: tests need to be re-writen for this after the refactoring let stake_era = current_era.saturating_add(1); match self.staked_future.as_mut() { @@ -1168,8 +1182,6 @@ impl ContractStakeAmount { /// Unstake the specified `amount` from the contract, for the specified `subperiod` and `era`. pub fn unstake(&mut self, amount: Balance, period_info: PeriodInfo, current_era: EraNumber) { - // TODO: tests need to be re-writen for this after the refactoring - // First align entries - we only need to keep track of the current era, and the next one match self.staked_future { // Future entry exists, but it covers current or older era. @@ -1347,6 +1359,17 @@ impl TierThreshold { Self::DynamicTvlAmount { amount, .. } => stake >= *amount, } } + + /// Return threshold for the tier. + pub fn threshold(&self) -> Balance { + match self { + Self::FixedTvlAmount { amount } => *amount, + Self::DynamicTvlAmount { amount, .. } => *amount, + } + } + + // TODO: maybe add a check that compares `Self` to another threshold and ensures it has lower requirements? + // Could be useful to have this check as a sanity check when params are configured. } /// Top level description of tier slot parameters used to calculate tier configuration. @@ -1364,11 +1387,13 @@ impl TierThreshold { pub struct TierParameters> { /// Reward distribution per tier, in percentage. /// First entry refers to the first tier, and so on. - /// The sum of all values must be exactly equal to 1. + /// The sum of all values must not exceed 100%. + /// In case it is less, portion of rewards will never be distributed. pub reward_portion: BoundedVec, /// Distribution of number of slots per tier, in percentage. /// First entry refers to the first tier, and so on. - /// The sum of all values must be exactly equal to 1. + /// The sum of all values must not exceed 100%. + /// In case it is less, slot capacity will never be fully filled. pub slot_distribution: BoundedVec, /// Requirements for entry into each tier. /// First entry refers to the first tier, and so on. @@ -1379,11 +1404,36 @@ impl> TierParameters { /// Check if configuration is valid. /// All vectors are expected to have exactly the amount of entries as `number_of_tiers`. pub fn is_valid(&self) -> bool { + // Reward portions sum should not exceed 100%. + if self + .reward_portion + .iter() + .fold(Some(Permill::zero()), |acc, permill| match acc { + Some(acc) => acc.checked_add(permill), + None => None, + }) + .is_none() + { + return false; + } + + // Slot distribution sum should not exceed 100%. + if self + .slot_distribution + .iter() + .fold(Some(Permill::zero()), |acc, permill| match acc { + Some(acc) => acc.checked_add(permill), + None => None, + }) + .is_none() + { + return false; + } + let number_of_tiers: usize = NT::get() as usize; number_of_tiers == self.reward_portion.len() && number_of_tiers == self.slot_distribution.len() && number_of_tiers == self.tier_thresholds.len() - // TODO: Make check more detailed, verify that entries sum up to 1 or 100% } } @@ -1602,6 +1652,8 @@ impl, NT: Get> DAppTierRewards { rewards: Vec, period: PeriodNumber, ) -> Result { + // TODO: should this part of the code ensure that dapps are sorted by Id? + let dapps = BoundedVec::try_from(dapps).map_err(|_| ())?; let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?; Ok(Self { @@ -1613,7 +1665,7 @@ impl, NT: Get> DAppTierRewards { /// Consume reward for the specified dapp id, returning its amount and tier Id. /// In case dapp isn't applicable for rewards, or they have already been consumed, returns `None`. - pub fn try_consume(&mut self, dapp_id: DAppId) -> Result<(Balance, TierId), DAppTierError> { + pub fn try_claim(&mut self, dapp_id: DAppId) -> Result<(Balance, TierId), DAppTierError> { // Check if dApp Id exists. let dapp_idx = self .dapps @@ -1649,6 +1701,12 @@ pub enum DAppTierError { InternalError, } +/// Tier labels can be assigned to dApps in order to provide them benefits (or drawbacks) when being assigned into a tier. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +pub enum TierLabel { + // Empty for now, on purpose. +} + /////////////////////////////////////////////////////////////////////// //////////// MOVE THIS TO SOME PRIMITIVES CRATE LATER //////////// /////////////////////////////////////////////////////////////////////// @@ -1682,39 +1740,3 @@ pub trait RewardPoolProvider { /// Get the bonus pool for stakers. fn bonus_reward_pool() -> Balance; } - -// TODO: these are experimental, don't review -#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] -pub struct ExperimentalContractStakeEntry { - #[codec(compact)] - pub dapp_id: DAppId, - #[codec(compact)] - pub voting: Balance, - #[codec(compact)] - pub build_and_earn: Balance, -} - -#[derive( - Encode, - Decode, - MaxEncodedLen, - RuntimeDebugNoBound, - PartialEqNoBound, - EqNoBound, - CloneNoBound, - TypeInfo, -)] -#[scale_info(skip_type_params(MD, NT))] -pub struct ExperimentalContractStakeEntries, NT: Get> { - /// DApps and their corresponding tiers (or `None` if they have been claimed in the meantime) - pub dapps: BoundedVec, - /// Rewards for each tier. First entry refers to the first tier, and so on. - pub rewards: BoundedVec, - /// Period during which this struct was created. - #[codec(compact)] - pub period: PeriodNumber, -} - -// TODO: temp experimental type, don't review -pub type ContractEntriesFor = - ExperimentalContractStakeEntries, ::NumberOfTiers>; diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 12ec1e4062..ab8217a9ff 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -27,7 +27,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use frame_support::{ construct_runtime, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU128, ConstU16, ConstU32, ConstU64, Currency, EitherOfDiverse, + AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Currency, EitherOfDiverse, EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, Nothing, OnFinalize, WithdrawReasons, }, weights::{ @@ -479,11 +479,11 @@ impl pallet_dapp_staking_v3::Config for Runtime { type NativePriceProvider = DummyPriceProvider; type RewardPoolProvider = DummyRewardPoolProvider; type StandardEraLength = ConstU32<30>; // should be 1 minute per standard era - type StandardErasPerVotingPeriod = ConstU32<2>; - type StandardErasPerBuildAndEarnPeriod = ConstU32<10>; + type StandardErasPerVotingSubperiod = ConstU32<2>; + type StandardErasPerBuildAndEarnSubperiod = ConstU32<10>; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; - type MaxNumberOfContracts = ConstU16<100>; + type MaxNumberOfContracts = ConstU32<100>; type MaxUnlockingChunks = ConstU32<5>; type MinimumLockedAmount = ConstU128; type UnlockingPeriod = ConstU32<2>; From ccb8e738a9efeda0797e471e334d9518eb63ab14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Thu, 30 Nov 2023 17:13:05 +0100 Subject: [PATCH 07/14] Pallet inflation (#1091) * Pallet inflation * Hooks * Functionality, mock & benchmarks * Empty commit since it seems last push didn't work properly * Tests, fixes * More tests, minor fixes&changes * Zero divison protection, frontier update * More tests, minor changes * Integration & renaming * Genesis integration, more tests * Final integration * Cleanup deps * Division with zero test * Comments * More minor changes * Improve test coverage * Integrate into pallet-timestamp * Remove timestamp * Fix * review comments * toml format --- Cargo.lock | 18 + Cargo.toml | 1 + bin/collator/src/local/chain_spec.rs | 7 +- pallets/inflation/Cargo.toml | 48 +++ pallets/inflation/src/benchmarking.rs | 168 ++++++++ pallets/inflation/src/lib.rs | 575 ++++++++++++++++++++++++++ pallets/inflation/src/mock.rs | 209 ++++++++++ pallets/inflation/src/tests.rs | 473 +++++++++++++++++++++ pallets/inflation/src/weights.rs | 154 +++++++ runtime/local/Cargo.toml | 4 + runtime/local/src/lib.rs | 53 ++- 11 files changed, 1705 insertions(+), 5 deletions(-) create mode 100644 pallets/inflation/Cargo.toml create mode 100644 pallets/inflation/src/benchmarking.rs create mode 100644 pallets/inflation/src/lib.rs create mode 100644 pallets/inflation/src/mock.rs create mode 100644 pallets/inflation/src/tests.rs create mode 100644 pallets/inflation/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index f69aee8ac4..c37290c4a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5545,6 +5545,7 @@ dependencies = [ "pallet-evm-precompile-substrate-ecdsa", "pallet-evm-precompile-xvm", "pallet-grandpa", + "pallet-inflation", "pallet-insecure-randomness-collective-flip", "pallet-preimage", "pallet-proxy", @@ -7652,6 +7653,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-inflation" +version = "0.1.0" +dependencies = [ + "astar-primitives", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-runtime", +] + [[package]] name = "pallet-insecure-randomness-collective-flip" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 91789f677a..2c9871ccaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -279,6 +279,7 @@ pallet-xc-asset-config = { path = "./pallets/xc-asset-config", default-features pallet-xvm = { path = "./pallets/xvm", default-features = false } pallet-xcm = { path = "./pallets/pallet-xcm", default-features = false } pallet-ethereum-checked = { path = "./pallets/ethereum-checked", default-features = false } +pallet-inflation = { path = "./pallets/inflation", default-features = false } astar-primitives = { path = "./primitives", default-features = false } diff --git a/bin/collator/src/local/chain_spec.rs b/bin/collator/src/local/chain_spec.rs index 1cde2e4dd7..f7e7c79e4d 100644 --- a/bin/collator/src/local/chain_spec.rs +++ b/bin/collator/src/local/chain_spec.rs @@ -21,8 +21,8 @@ use local_runtime::{ wasm_binary_unwrap, AccountId, AuraConfig, AuraId, BalancesConfig, BaseFeeConfig, BlockRewardConfig, CouncilConfig, DappStakingConfig, DemocracyConfig, EVMConfig, GenesisConfig, - GrandpaConfig, GrandpaId, Precompiles, Signature, SudoConfig, SystemConfig, - TechnicalCommitteeConfig, TreasuryConfig, VestingConfig, AST, + GrandpaConfig, GrandpaId, InflationConfig, InflationParameters, Precompiles, Signature, + SudoConfig, SystemConfig, TechnicalCommitteeConfig, TreasuryConfig, VestingConfig, AST, }; use sc_service::ChainType; use sp_core::{crypto::Ss58Codec, sr25519, Pair, Public}; @@ -213,6 +213,9 @@ fn testnet_genesis( ], slots_per_tier: vec![10, 20, 30, 40], }, + inflation: InflationConfig { + params: InflationParameters::default(), + }, } } diff --git a/pallets/inflation/Cargo.toml b/pallets/inflation/Cargo.toml new file mode 100644 index 0000000000..6b1065d9bb --- /dev/null +++ b/pallets/inflation/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "pallet-inflation" +version = "0.1.0" +license = "GPL-3.0-or-later" +description = "Manages inflation rate & inflation distribution" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +log = { workspace = true } +parity-scale-codec = { workspace = true } +serde = { workspace = true } + +astar-primitives = { workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +scale-info = { workspace = true } +sp-runtime = { workspace = true } + +frame-benchmarking = { workspace = true, optional = true } + +[dev-dependencies] +pallet-balances = { workspace = true } +sp-core = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "log/std", + "sp-core/std", + "scale-info/std", + "serde/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "astar-primitives/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "astar-primitives/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/inflation/src/benchmarking.rs b/pallets/inflation/src/benchmarking.rs new file mode 100644 index 0000000000..113c5f32a7 --- /dev/null +++ b/pallets/inflation/src/benchmarking.rs @@ -0,0 +1,168 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::*; + +use frame_benchmarking::v2::*; +use frame_system::{Pallet as System, RawOrigin}; + +/// Assert that the last event equals the provided one. +fn assert_last_event(generic_event: ::RuntimeEvent) { + System::::assert_last_event(generic_event.into()); +} + +// Set up initial config in the database, so it's not empty. +fn initial_config() { + // Some dummy inflation params + let params = InflationParameters { + max_inflation_rate: Perquintill::from_percent(7), + treasury_part: Perquintill::from_percent(5), + collators_part: Perquintill::from_percent(3), + dapps_part: Perquintill::from_percent(20), + base_stakers_part: Perquintill::from_percent(25), + adjustable_stakers_part: Perquintill::from_percent(35), + bonus_part: Perquintill::from_percent(12), + ideal_staking_rate: Perquintill::from_percent(50), + }; + assert!(params.is_valid()); + + // Some dummy inflation config + let total_issuance = T::Currency::total_issuance(); + let issuance_safety_cap = + total_issuance.saturating_add(params.max_inflation_rate * total_issuance); + let config = InflationConfiguration { + recalculation_block: 123, + issuance_safety_cap, + collator_reward_per_block: 11111, + treasury_reward_per_block: 33333, + dapp_reward_pool_per_era: 55555, + base_staker_reward_pool_per_era: 77777, + adjustable_staker_reward_pool_per_era: 99999, + bonus_reward_pool_per_period: 123987, + ideal_staking_rate: Perquintill::from_percent(50), + }; + + InflationParams::::put(params); + ActiveInflationConfig::::put(config); + + // Create some issuance so it's not zero + let dummy_account = whitelisted_caller(); + T::Currency::make_free_balance_be(&dummy_account, 1_000_000_000_000_000_000_000); +} + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn force_set_inflation_params() { + initial_config::(); + + let params = InflationParameters::default(); + assert!(params.is_valid()); + + #[extrinsic_call] + _(RawOrigin::Root, params); + + assert_last_event::(Event::::InflationParametersForceChanged.into()); + } + + #[benchmark] + fn force_set_inflation_config() { + initial_config::(); + let config = InflationConfiguration::default(); + + #[extrinsic_call] + _(RawOrigin::Root, config.clone()); + + assert_last_event::(Event::::InflationConfigurationForceChanged { config }.into()); + } + + #[benchmark] + fn force_inflation_recalculation() { + initial_config::(); + + #[extrinsic_call] + _(RawOrigin::Root); + + let config = ActiveInflationConfig::::get(); + assert_last_event::(Event::::ForcedInflationRecalculation { config }.into()); + } + + #[benchmark] + fn hook_with_recalculation() { + initial_config::(); + + ActiveInflationConfig::::mutate(|config| { + config.recalculation_block = 0; + }); + let init_issuance = T::Currency::total_issuance(); + + let block = 1; + #[block] + { + Pallet::::on_initialize(block); + Pallet::::on_finalize(block); + } + + assert!(ActiveInflationConfig::::get().recalculation_block > 0); + + // The 'sane' assumption is that at least something will be issued for treasury & collators + assert!(T::Currency::total_issuance() > init_issuance); + } + + #[benchmark] + fn hook_without_recalculation() { + initial_config::(); + + ActiveInflationConfig::::mutate(|config| { + config.recalculation_block = 2; + }); + let init_config = ActiveInflationConfig::::get(); + let init_issuance = T::Currency::total_issuance(); + + // Has to be at least 2 blocks less than the recalculation block. + let block = 0; + #[block] + { + Pallet::::on_initialize(block); + Pallet::::on_finalize(block); + } + + assert_eq!(ActiveInflationConfig::::get(), init_config); + + // The 'sane' assumption is that at least something will be issued for treasury & collators + assert!(T::Currency::total_issuance() > init_issuance); + } + + impl_benchmark_test_suite!( + Pallet, + crate::benchmarking::tests::new_test_ext(), + crate::mock::Test, + ); +} + +#[cfg(test)] +mod tests { + use crate::mock; + use frame_support::sp_io::TestExternalities; + + pub fn new_test_ext() -> TestExternalities { + mock::ExternalityBuilder::build() + } +} diff --git a/pallets/inflation/src/lib.rs b/pallets/inflation/src/lib.rs new file mode 100644 index 0000000000..e19841c71d --- /dev/null +++ b/pallets/inflation/src/lib.rs @@ -0,0 +1,575 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! TODO + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +use astar_primitives::{Balance, BlockNumber}; +use frame_support::{pallet_prelude::*, traits::Currency}; +use frame_system::{ensure_root, pallet_prelude::*}; +use sp_runtime::{traits::CheckedAdd, Perquintill}; + +pub mod weights; +pub use weights::WeightInfo; + +#[cfg(any(feature = "runtime-benchmarks"))] +pub mod benchmarking; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[frame_support::pallet] +pub mod pallet { + + use super::*; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + // Negative imbalance type of this pallet. + pub(crate) type NegativeImbalanceOf = <::Currency as Currency< + ::AccountId, + >>::NegativeImbalance; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The currency trait. + /// This has been soft-deprecated but it still needs to be used here in order to access `NegativeImbalance` + /// which is defined in the currency trait. + type Currency: Currency; + + /// Handler for 'per-block' payouts. + type PayoutPerBlock: PayoutPerBlock>; + + /// Cycle ('year') configuration - covers periods, subperiods, eras & blocks. + type CycleConfiguration: CycleConfiguration; + + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// Inflation parameters have been force changed. This will have effect on the next inflation recalculation. + InflationParametersForceChanged, + /// Inflation configuration has been force changed. This will have an immediate effect from this block. + InflationConfigurationForceChanged { config: InflationConfiguration }, + /// Inflation recalculation has been forced. + ForcedInflationRecalculation { config: InflationConfiguration }, + /// New inflation configuration has been set. + NewInflationConfiguration { config: InflationConfiguration }, + } + + #[pallet::error] + pub enum Error { + /// Sum of all parts must be one whole (100%). + InvalidInflationParameters, + } + + /// Active inflation configuration parameteres. + /// They describe current rewards, when inflation needs to be recalculated, etc. + #[pallet::storage] + #[pallet::whitelist_storage] + pub type ActiveInflationConfig = StorageValue<_, InflationConfiguration, ValueQuery>; + + /// Static inflation parameters - used to calculate active inflation configuration at certain points in time. + #[pallet::storage] + pub type InflationParams = StorageValue<_, InflationParameters, ValueQuery>; + + #[pallet::genesis_config] + #[cfg_attr(feature = "std", derive(Default))] + pub struct GenesisConfig { + pub params: InflationParameters, + } + + /// This should be executed **AFTER** other pallets that cause issuance to increase have been initialized. + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + assert!(self.params.is_valid()); + + let now = frame_system::Pallet::::block_number(); + let config = Pallet::::recalculate_inflation(now); + + ActiveInflationConfig::::put(config); + InflationParams::::put(self.params); + } + } + + #[pallet::hooks] + impl Hooks for Pallet { + fn on_initialize(now: BlockNumber) -> Weight { + Self::payout_block_rewards(); + + let recalculation_weight = + if Self::is_recalculation_in_next_block(now, &ActiveInflationConfig::::get()) { + T::WeightInfo::hook_with_recalculation() + } else { + T::WeightInfo::hook_without_recalculation() + }; + + // Benchmarks won't acount for whitelisted storage access so this needs to be added manually. + // + // ActiveInflationConfig - 1 DB read + let whitelisted_weight = ::DbWeight::get().reads(1); + + recalculation_weight.saturating_add(whitelisted_weight) + } + + fn on_finalize(now: BlockNumber) { + // Recalculation is done at the block right before the re-calculation is supposed to happen. + // This is to ensure all the rewards are paid out according to the new inflation configuration from next block. + // + // If this was done in `on_initialize`, collator & treasury would receive incorrect rewards for that one block. + // + // This should be done as late as possible, to ensure all operations that modify issuance are done. + if Self::is_recalculation_in_next_block(now, &ActiveInflationConfig::::get()) { + let config = Self::recalculate_inflation(now); + ActiveInflationConfig::::put(config.clone()); + + Self::deposit_event(Event::::NewInflationConfiguration { config }); + } + } + + fn integrity_test() { + assert!(T::CycleConfiguration::periods_per_cycle() > 0); + assert!(T::CycleConfiguration::eras_per_voting_subperiod() > 0); + assert!(T::CycleConfiguration::eras_per_build_and_earn_subperiod() > 0); + assert!(T::CycleConfiguration::blocks_per_era() > 0); + } + } + + #[pallet::call] + impl Pallet { + /// Used to force-set the inflation parameters. + /// The parameters must be valid, all parts summing up to one whole (100%), otherwise the call will fail. + /// + /// Must be called by `root` origin. + /// + /// Purpose of the call is testing & handling unforseen circumstances. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::force_set_inflation_params())] + pub fn force_set_inflation_params( + origin: OriginFor, + params: InflationParameters, + ) -> DispatchResult { + ensure_root(origin)?; + + ensure!(params.is_valid(), Error::::InvalidInflationParameters); + InflationParams::::put(params); + + Self::deposit_event(Event::::InflationParametersForceChanged); + + Ok(().into()) + } + + /// Used to force inflation recalculation. + /// This is done in the same way as it would be done in an appropriate block, but this call forces it. + /// + /// Must be called by `root` origin. + /// + /// Purpose of the call is testing & handling unforseen circumstances. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::force_inflation_recalculation())] + pub fn force_inflation_recalculation(origin: OriginFor) -> DispatchResult { + ensure_root(origin)?; + + let config = Self::recalculate_inflation(frame_system::Pallet::::block_number()); + + ActiveInflationConfig::::put(config.clone()); + + Self::deposit_event(Event::::ForcedInflationRecalculation { config }); + + Ok(().into()) + } + + /// Used to force-set the inflation configuration. + /// The parameters aren't checked for validity, since essentially anything can be valid. + /// + /// Must be called by `root` origin. + /// + /// Purpose of the call is testing & handling unforseen circumstances. + /// + /// **NOTE:** and a TODO, remove this before deploying on mainnet. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::force_set_inflation_config())] + pub fn force_set_inflation_config( + origin: OriginFor, + config: InflationConfiguration, + ) -> DispatchResult { + ensure_root(origin)?; + + ActiveInflationConfig::::put(config.clone()); + + Self::deposit_event(Event::::InflationConfigurationForceChanged { config }); + + Ok(().into()) + } + } + + impl Pallet { + /// Used to check if inflation recalculation is supposed to happen on the next block. + /// + /// This will be true even if recalculation is overdue, e.g. it should have happened in the current or older block. + fn is_recalculation_in_next_block( + now: BlockNumber, + config: &InflationConfiguration, + ) -> bool { + config.recalculation_block.saturating_sub(now) <= 1 + } + + /// Payout block rewards to the beneficiaries. + /// + /// Return the total amount issued. + fn payout_block_rewards() -> Balance { + let config = ActiveInflationConfig::::get(); + + let collator_amount = T::Currency::issue(config.collator_reward_per_block); + let treasury_amount = T::Currency::issue(config.treasury_reward_per_block); + + T::PayoutPerBlock::collators(collator_amount); + T::PayoutPerBlock::treasury(treasury_amount); + + config.collator_reward_per_block + config.treasury_reward_per_block + } + + /// Recalculates the inflation based on the total issuance & inflation parameters. + /// + /// Returns the new inflation configuration. + pub(crate) fn recalculate_inflation(now: BlockNumber) -> InflationConfiguration { + let params = InflationParams::::get(); + let total_issuance = T::Currency::total_issuance(); + + // 1. Calculate maximum emission over the period before the next recalculation. + let max_emission = params.max_inflation_rate * total_issuance; + let issuance_safety_cap = total_issuance.saturating_add(max_emission); + + // 2. Calculate distribution of max emission between different purposes. + let treasury_emission = params.treasury_part * max_emission; + let collators_emission = params.collators_part * max_emission; + let dapps_emission = params.dapps_part * max_emission; + let base_stakers_emission = params.base_stakers_part * max_emission; + let adjustable_stakers_emission = params.adjustable_stakers_part * max_emission; + let bonus_emission = params.bonus_part * max_emission; + + // 3. Calculate concrete rewards per block, era or period + + // 3.0 Convert all 'per cycle' values to the correct type (Balance). + // Also include a safety check that none of the values is zero since this would cause a division by zero. + // The configuration & integration tests must ensure this never happens, so the following code is just an additional safety measure. + let blocks_per_cycle = match T::CycleConfiguration::blocks_per_cycle() { + 0 => Balance::MAX, + blocks_per_cycle => Balance::from(blocks_per_cycle), + }; + + let build_and_earn_eras_per_cycle = + match T::CycleConfiguration::build_and_earn_eras_per_cycle() { + 0 => Balance::MAX, + build_and_earn_eras_per_cycle => Balance::from(build_and_earn_eras_per_cycle), + }; + + let periods_per_cycle = match T::CycleConfiguration::periods_per_cycle() { + 0 => Balance::MAX, + periods_per_cycle => Balance::from(periods_per_cycle), + }; + + // 3.1. Collator & Treausry rewards per block + let collator_reward_per_block = collators_emission / blocks_per_cycle; + let treasury_reward_per_block = treasury_emission / blocks_per_cycle; + + // 3.2. dApp reward pool per era + let dapp_reward_pool_per_era = dapps_emission / build_and_earn_eras_per_cycle; + + // 3.3. Staking reward pools per era + let base_staker_reward_pool_per_era = + base_stakers_emission / build_and_earn_eras_per_cycle; + let adjustable_staker_reward_pool_per_era = + adjustable_stakers_emission / build_and_earn_eras_per_cycle; + + // 3.4. Bonus reward pool per period + let bonus_reward_pool_per_period = bonus_emission / periods_per_cycle; + + // 4. Block at which the inflation must be recalculated. + let recalculation_block = now.saturating_add(T::CycleConfiguration::blocks_per_cycle()); + + // 5. Return calculated values + InflationConfiguration { + recalculation_block, + issuance_safety_cap, + collator_reward_per_block, + treasury_reward_per_block, + dapp_reward_pool_per_era, + base_staker_reward_pool_per_era, + adjustable_staker_reward_pool_per_era, + bonus_reward_pool_per_period, + ideal_staking_rate: params.ideal_staking_rate, + } + } + + /// Check if payout cap limit would be reached after payout. + fn is_payout_cap_limit_exceeded(payout: Balance) -> bool { + let config = ActiveInflationConfig::::get(); + let total_issuance = T::Currency::total_issuance(); + + let new_issuance = total_issuance.saturating_add(payout); + + if new_issuance > config.issuance_safety_cap { + log::error!("Issuance cap has been exceeded. Please report this issue ASAP!"); + } + + // Allow for 1% safety cap overflow, to prevent bad UX for users in case of rounding errors. + // This will be removed in the future once we know everything is working as expected. + let relaxed_issuance_safety_cap = config + .issuance_safety_cap + .saturating_mul(101) + .saturating_div(100); + + new_issuance > relaxed_issuance_safety_cap + } + } + + impl StakingRewardHandler for Pallet { + fn staker_and_dapp_reward_pools(total_value_staked: Balance) -> (Balance, Balance) { + let config = ActiveInflationConfig::::get(); + let total_issuance = T::Currency::total_issuance(); + + // First calculate the adjustable part of the staker reward pool, according to formula: + // adjustable_part = max_adjustable_part * min(1, total_staked_percent / ideal_staked_percent) + // (These operations are overflow & zero-division safe) + let staked_ratio = Perquintill::from_rational(total_value_staked, total_issuance); + let adjustment_factor = staked_ratio / config.ideal_staking_rate; + + let adjustable_part = adjustment_factor * config.adjustable_staker_reward_pool_per_era; + let staker_reward_pool = config + .base_staker_reward_pool_per_era + .saturating_add(adjustable_part); + + (staker_reward_pool, config.dapp_reward_pool_per_era) + } + + fn bonus_reward_pool() -> Balance { + ActiveInflationConfig::::get().bonus_reward_pool_per_period + } + + fn payout_reward(account: &T::AccountId, reward: Balance) -> Result<(), ()> { + // This is a safety measure to prevent excessive minting. + ensure!(!Self::is_payout_cap_limit_exceeded(reward), ()); + + // This can fail only if the amount is below existential deposit & the account doesn't exist, + // or if the account has no provider references. + // In both cases, the reward is lost but this can be ignored since it's extremelly unlikely + // to appear and doesn't bring any real harm. + T::Currency::deposit_creating(account, reward); + Ok(()) + } + } +} + +/// Configuration of the inflation. +/// Contains information about rewards, when inflation is recalculated, etc. +#[derive(Encode, Decode, MaxEncodedLen, Default, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub struct InflationConfiguration { + /// Block number at which the inflation must be recalculated, based on the total issuance at that block. + #[codec(compact)] + pub recalculation_block: BlockNumber, + /// Maximum amount of issuance we can have during this cycle. + #[codec(compact)] + pub issuance_safety_cap: Balance, + /// Reward for collator who produced the block. Always deposited the collator in full. + #[codec(compact)] + pub collator_reward_per_block: Balance, + /// Part of the inflation going towards the treasury. Always deposited in full. + #[codec(compact)] + pub treasury_reward_per_block: Balance, + /// dApp reward pool per era - based on this the tier rewards are calculated. + /// There's no guarantee that this whole amount will be minted & distributed. + #[codec(compact)] + pub dapp_reward_pool_per_era: Balance, + /// Base staker reward pool per era - this is always provided to stakers, regardless of the total value staked. + #[codec(compact)] + pub base_staker_reward_pool_per_era: Balance, + /// Adjustabke staker rewards, based on the total value staked. + /// This is provided to the stakers according to formula: 'pool * min(1, total_staked / ideal_staked)'. + #[codec(compact)] + pub adjustable_staker_reward_pool_per_era: Balance, + /// Bonus reward pool per period, for loyal stakers. + #[codec(compact)] + pub bonus_reward_pool_per_period: Balance, + /// The ideal staking rate, in respect to total issuance. + /// Used to derive exact amount of adjustable staker rewards. + #[codec(compact)] + pub ideal_staking_rate: Perquintill, +} + +/// Inflation parameters. +/// +/// The parts of the inflation that go towards different purposes must add up to exactly 100%. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub struct InflationParameters { + /// Maximum possible inflation rate, based on the total issuance at some point in time. + /// From this value, all the other inflation parameters are derived. + #[codec(compact)] + pub max_inflation_rate: Perquintill, + /// Portion of the inflation that goes towards the treasury. + #[codec(compact)] + pub treasury_part: Perquintill, + /// Portion of the inflation that goes towards collators. + #[codec(compact)] + pub collators_part: Perquintill, + /// Portion of the inflation that goes towards dApp rewards (tier rewards). + #[codec(compact)] + pub dapps_part: Perquintill, + /// Portion of the inflation that goes towards base staker rewards. + #[codec(compact)] + pub base_stakers_part: Perquintill, + /// Portion of the inflation that can go towards the adjustable staker rewards. + /// These rewards are adjusted based on the total value staked. + #[codec(compact)] + pub adjustable_stakers_part: Perquintill, + /// Portion of the inflation that goes towards bonus staker rewards (loyalty rewards). + #[codec(compact)] + pub bonus_part: Perquintill, + /// The ideal staking rate, in respect to total issuance. + /// Used to derive exact amount of adjustable staker rewards. + #[codec(compact)] + pub ideal_staking_rate: Perquintill, +} + +impl InflationParameters { + /// `true` if sum of all percentages is `one whole`, `false` otherwise. + pub fn is_valid(&self) -> bool { + let variables = [ + &self.treasury_part, + &self.collators_part, + &self.dapps_part, + &self.base_stakers_part, + &self.adjustable_stakers_part, + &self.bonus_part, + ]; + + variables + .iter() + .fold(Some(Perquintill::zero()), |acc, part| { + if let Some(acc) = acc { + acc.checked_add(*part) + } else { + None + } + }) + == Some(Perquintill::one()) + } +} + +// Default inflation parameters, just to make sure genesis builder is happy +impl Default for InflationParameters { + fn default() -> Self { + Self { + max_inflation_rate: Perquintill::from_percent(7), + treasury_part: Perquintill::from_percent(5), + collators_part: Perquintill::from_percent(3), + dapps_part: Perquintill::from_percent(20), + base_stakers_part: Perquintill::from_percent(25), + adjustable_stakers_part: Perquintill::from_percent(35), + bonus_part: Perquintill::from_percent(12), + ideal_staking_rate: Perquintill::from_percent(50), + } + } +} + +/// Defines functions used to payout the beneficiaries of block rewards +pub trait PayoutPerBlock { + /// Payout reward to the treasury. + fn treasury(reward: Imbalance); + + /// Payout reward to the collator responsible for producing the block. + fn collators(reward: Imbalance); +} + +// TODO: This should be moved to primitives. +// TODO2: However this ends up looking in the end, we should not duplicate these parameters in the runtime. +// Both the dApp staking & inflation pallet should use the same source. +pub trait CycleConfiguration { + /// How many different periods are there in a cycle (a 'year'). + /// + /// This value has to be at least 1. + fn periods_per_cycle() -> u32; + + /// For how many standard era lengths does the voting subperiod last. + /// + /// This value has to be at least 1. + fn eras_per_voting_subperiod() -> u32; + + /// How many standard eras are there in the build&earn subperiod. + /// + /// This value has to be at least 1. + fn eras_per_build_and_earn_subperiod() -> u32; + + /// How many blocks are there per standard era. + /// + /// This value has to be at least 1. + fn blocks_per_era() -> u32; + + /// For how many standard era lengths does the period last. + fn eras_per_period() -> u32 { + Self::eras_per_voting_subperiod().saturating_add(Self::eras_per_build_and_earn_subperiod()) + } + + /// For how many standard era lengths does the cylce (a 'year') last. + fn eras_per_cycle() -> u32 { + Self::eras_per_period().saturating_mul(Self::periods_per_cycle()) + } + + /// How many blocks are there per cycle (a 'year'). + fn blocks_per_cycle() -> u32 { + Self::blocks_per_era().saturating_mul(Self::eras_per_cycle()) + } + + /// For how many standard era lengths do all the build&earn subperiods in a cycle last. + fn build_and_earn_eras_per_cycle() -> u32 { + Self::eras_per_build_and_earn_subperiod().saturating_mul(Self::periods_per_cycle()) + } +} + +// TODO: This should be moved to primitives. +/// Interface for staking reward handler. +/// +/// Provides reward pool values for stakers - normal & bonus rewards, as well as dApp reward pool. +/// Also provides a safe function for paying out rewards. +pub trait StakingRewardHandler { + /// Returns the staker reward pool & dApp reward pool for an era. + /// + /// The total staker reward pool is dynamic and depends on the total value staked. + fn staker_and_dapp_reward_pools(total_value_staked: Balance) -> (Balance, Balance); + + /// Returns the bonus reward pool for a period. + fn bonus_reward_pool() -> Balance; + + /// Attempts to pay out the rewards to the beneficiary. + fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()>; +} diff --git a/pallets/inflation/src/mock.rs b/pallets/inflation/src/mock.rs new file mode 100644 index 0000000000..a2c0af4b6a --- /dev/null +++ b/pallets/inflation/src/mock.rs @@ -0,0 +1,209 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::{ + self as pallet_inflation, ActiveInflationConfig, CycleConfiguration, InflationConfiguration, + InflationParameters, InflationParams, NegativeImbalanceOf, PayoutPerBlock, +}; + +use frame_support::{ + construct_runtime, parameter_types, + sp_io::TestExternalities, + traits::Currency, + traits::{ConstU128, ConstU32, Hooks}, + weights::Weight, + PalletId, +}; + +use sp_core::H256; +use sp_runtime::{ + generic::Header, // TODO: create testing primitives & move it there? + traits::{AccountIdConversion, BlakeTwo256, IdentityLookup}, + Perquintill, +}; + +use astar_primitives::{Balance, BlockNumber}; +pub(crate) type AccountId = u64; // TODO: might also be nice to have this under testing primitives? + +/// Initial inflation params set by the mock. +pub const INIT_PARAMS: InflationParameters = InflationParameters { + max_inflation_rate: Perquintill::from_percent(7), + treasury_part: Perquintill::from_percent(5), + collators_part: Perquintill::from_percent(3), + dapps_part: Perquintill::from_percent(20), + base_stakers_part: Perquintill::from_percent(25), + adjustable_stakers_part: Perquintill::from_percent(35), + bonus_part: Perquintill::from_percent(12), + ideal_staking_rate: Perquintill::from_percent(50), +}; + +/// Initial inflation config set by the mock. +pub const INIT_CONFIG: InflationConfiguration = InflationConfiguration { + recalculation_block: 100, + issuance_safety_cap: 1_000_000, + collator_reward_per_block: 1000, + treasury_reward_per_block: 1500, + dapp_reward_pool_per_era: 3000, + base_staker_reward_pool_per_era: 5000, + adjustable_staker_reward_pool_per_era: 7000, + bonus_reward_pool_per_period: 4000, + ideal_staking_rate: Perquintill::from_percent(50), +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +parameter_types! { + pub const BlockHashCount: BlockNumber = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type MaxLocks = ConstU32<4>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; +} +// Dummy accounts used to simulate reward beneficiaries balances +pub(crate) const TREASURY_POT: PalletId = PalletId(*b"moktrsry"); +pub(crate) const COLLATOR_POT: PalletId = PalletId(*b"mokcolat"); + +pub struct DummyPayoutPerBlock; +impl PayoutPerBlock> for DummyPayoutPerBlock { + fn treasury(reward: NegativeImbalanceOf) { + Balances::resolve_creating(&TREASURY_POT.into_account_truncating(), reward); + } + + fn collators(reward: NegativeImbalanceOf) { + Balances::resolve_creating(&COLLATOR_POT.into_account_truncating(), reward); + } +} + +pub struct DummyCycleConfiguration; +impl CycleConfiguration for DummyCycleConfiguration { + fn periods_per_cycle() -> u32 { + 5 + } + + fn eras_per_voting_subperiod() -> u32 { + 2 + } + + fn eras_per_build_and_earn_subperiod() -> u32 { + 17 + } + + fn blocks_per_era() -> u32 { + 11 + } +} + +impl pallet_inflation::Config for Test { + type Currency = Balances; + type PayoutPerBlock = DummyPayoutPerBlock; + type CycleConfiguration = DummyCycleConfiguration; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +construct_runtime!( + pub struct Test + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + Inflation: pallet_inflation, + } +); + +pub struct ExternalityBuilder; +impl ExternalityBuilder { + pub fn build() -> TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + // This will cause some initial issuance + pallet_balances::GenesisConfig:: { + balances: vec![(1, 9000), (2, 800), (3, 10000)], + } + .assimilate_storage(&mut storage) + .ok(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| { + // Set initial pallet inflation values + ActiveInflationConfig::::put(INIT_CONFIG); + InflationParams::::put(INIT_PARAMS); + + System::set_block_number(1); + Inflation::on_initialize(1); + }); + ext + } +} + +/// Advance to the specified block number. +/// Function assumes first block has been initialized. +pub(crate) fn advance_to_block(n: BlockNumber) { + while System::block_number() < n { + Inflation::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + Inflation::on_initialize(System::block_number()); + } +} diff --git a/pallets/inflation/src/tests.rs b/pallets/inflation/src/tests.rs new file mode 100644 index 0000000000..518aa369c4 --- /dev/null +++ b/pallets/inflation/src/tests.rs @@ -0,0 +1,473 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::{pallet::Error, Event, *}; +use frame_support::{ + assert_noop, assert_ok, assert_storage_noop, + traits::{GenesisBuild, Hooks}, +}; +use mock::*; +use sp_runtime::{ + traits::{AccountIdConversion, BadOrigin, Zero}, + Perquintill, +}; + +#[test] +fn default_params_are_valid() { + assert!(InflationParameters::default().is_valid()); +} + +#[test] +fn force_set_inflation_params_work() { + ExternalityBuilder::build().execute_with(|| { + let mut new_params = InflationParams::::get(); + new_params.max_inflation_rate = Perquintill::from_percent(20); + assert!(new_params != InflationParams::::get(), "Sanity check"); + + // Execute call, ensure it works + assert_ok!(Inflation::force_set_inflation_params( + RuntimeOrigin::root(), + new_params + )); + System::assert_last_event(Event::InflationParametersForceChanged.into()); + + assert_eq!(InflationParams::::get(), new_params); + }) +} + +#[test] +fn force_set_inflation_params_fails() { + ExternalityBuilder::build().execute_with(|| { + let mut new_params = InflationParams::::get(); + new_params.base_stakers_part = Zero::zero(); + assert!( + !new_params.is_valid(), + "Must be invalid for check to make sense." + ); + + // Make sure it's not possible to force-set invalid params + assert_noop!( + Inflation::force_set_inflation_params(RuntimeOrigin::root(), new_params), + Error::::InvalidInflationParameters + ); + + // Make sure action is privileged + assert_noop!( + Inflation::force_set_inflation_params(RuntimeOrigin::signed(1).into(), new_params,), + BadOrigin + ); + }) +} + +#[test] +fn force_set_inflation_config_work() { + ExternalityBuilder::build().execute_with(|| { + let mut new_config = ActiveInflationConfig::::get(); + new_config.recalculation_block = new_config.recalculation_block + 50; + + // Execute call, ensure it works + assert_ok!(Inflation::force_set_inflation_config( + RuntimeOrigin::root(), + new_config + )); + System::assert_last_event( + Event::InflationConfigurationForceChanged { config: new_config }.into(), + ); + + assert_eq!(ActiveInflationConfig::::get(), new_config); + }) +} + +#[test] +fn force_set_inflation_config_fails() { + ExternalityBuilder::build().execute_with(|| { + let mut new_config = ActiveInflationConfig::::get(); + new_config.recalculation_block = new_config.recalculation_block + 50; + + // Make sure action is privileged + assert_noop!( + Inflation::force_set_inflation_config(RuntimeOrigin::signed(1), new_config), + BadOrigin + ); + }) +} + +#[test] +fn force_inflation_recalculation_work() { + ExternalityBuilder::build().execute_with(|| { + let old_config = ActiveInflationConfig::::get(); + + // Execute call, ensure it works + assert_ok!(Inflation::force_inflation_recalculation( + RuntimeOrigin::root(), + )); + + let new_config = ActiveInflationConfig::::get(); + assert!( + old_config != new_config, + "Config should change, otherwise test doesn't make sense." + ); + + System::assert_last_event( + Event::ForcedInflationRecalculation { config: new_config }.into(), + ); + }) +} +#[test] +fn force_inflation_fails_due_to_unprivileged_origin() { + ExternalityBuilder::build().execute_with(|| { + // Make sure action is privileged + assert_noop!( + Inflation::force_inflation_recalculation(RuntimeOrigin::signed(1)), + BadOrigin + ); + }) +} + +#[test] +fn inflation_recalculation_occurs_when_exepcted() { + ExternalityBuilder::build().execute_with(|| { + let init_config = ActiveInflationConfig::::get(); + + // Make sure `on_finalize` calls before the expected change are storage noops + advance_to_block(init_config.recalculation_block - 3); + assert_storage_noop!(Inflation::on_finalize(init_config.recalculation_block - 3)); + Inflation::on_initialize( + init_config.recalculation_block - 2 + ); + assert_storage_noop!(Inflation::on_finalize(init_config.recalculation_block - 2)); + Inflation::on_initialize( + init_config.recalculation_block - 1 + ); + + // One block before recalculation, on_finalize should calculate new inflation config + let init_config = ActiveInflationConfig::::get(); + let init_total_issuance = Balances::total_issuance(); + + // Finally trigger inflation recalculation. + Inflation::on_finalize(init_config.recalculation_block - 1); + + let new_config = ActiveInflationConfig::::get(); + assert!( + new_config != init_config, + "Recalculation must happen at this point." + ); + System::assert_last_event(Event::NewInflationConfiguration { config: new_config }.into()); + + assert_eq!( + Balances::total_issuance(), + init_total_issuance, + "Total issuance must not change when inflation is recalculated - nothing is minted until it's needed." + ); + + assert_eq!(new_config.issuance_safety_cap, init_total_issuance + InflationParams::::get().max_inflation_rate * init_total_issuance); + }) +} + +#[test] +fn on_initialize_reward_payout_works() { + ExternalityBuilder::build().execute_with(|| { + // Save initial state, before the payout + let config = ActiveInflationConfig::::get(); + + let init_issuance = Balances::total_issuance(); + let init_collator_pot = Balances::free_balance(&COLLATOR_POT.into_account_truncating()); + let init_treasury_pot = Balances::free_balance(&TREASURY_POT.into_account_truncating()); + + // Execute payout + Inflation::on_initialize(1); + + // Verify state post payout + let expected_reward = config.collator_reward_per_block + config.treasury_reward_per_block; + + // Balance changes are as expected + assert_eq!(Balances::total_issuance(), init_issuance + expected_reward); + assert_eq!( + Balances::free_balance(&COLLATOR_POT.into_account_truncating()), + init_collator_pot + config.collator_reward_per_block + ); + assert_eq!( + Balances::free_balance(&TREASURY_POT.into_account_truncating()), + init_treasury_pot + config.treasury_reward_per_block + ); + }) +} + +#[test] +fn inflation_parameters_validity_check_works() { + // Params to be used as anchor for the tests + let base_params = INIT_PARAMS; + assert!(base_params.is_valid(), "Sanity check."); + + // Reduction of some param, it should invalidate the whole config + let mut params = base_params; + params.base_stakers_part = params.base_stakers_part - Perquintill::from_percent(1); + assert!(!params.is_valid(), "Sum is below 100%, must fail."); + + // Increase of some param, it should invalidate the whole config + let mut params = base_params; + params.base_stakers_part = params.base_stakers_part + Perquintill::from_percent(1); + assert!(!params.is_valid(), "Sum is above 100%, must fail."); + + // Excessive increase of some param, it should invalidate the whole config + let mut params = base_params; + params.treasury_part = Perquintill::from_percent(100); + assert!(!params.is_valid(), "Sum is above 100%, must fail."); + + // Some param can be zero, as long as sum remains 100% + let mut params = base_params; + params.base_stakers_part = params.base_stakers_part + params.adjustable_stakers_part; + params.adjustable_stakers_part = Zero::zero(); + assert!(params.is_valid()); +} + +#[test] +fn inflation_recalucation_works() { + ExternalityBuilder::build().execute_with(|| { + let total_issuance = Balances::total_issuance(); + let params = InflationParams::::get(); + let now = System::block_number(); + + // Calculate new config + let new_config = Inflation::recalculate_inflation(now); + let max_emission = params.max_inflation_rate * total_issuance; + + // Verify basics are ok + assert_eq!( + new_config.recalculation_block, + now + ::CycleConfiguration::blocks_per_cycle() + ); + assert_eq!( + new_config.issuance_safety_cap, + total_issuance + max_emission, + ); + + // Verify collator rewards are as expected + assert_eq!( + new_config.collator_reward_per_block, + params.collators_part * max_emission + / Balance::from(::CycleConfiguration::blocks_per_cycle()), + ); + + // Verify treasury rewards are as expected + assert_eq!( + new_config.treasury_reward_per_block, + params.treasury_part * max_emission + / Balance::from(::CycleConfiguration::blocks_per_cycle()), + ); + + // Verify dApp rewards are as expected + assert_eq!( + new_config.dapp_reward_pool_per_era, + params.dapps_part * max_emission + / Balance::from( + ::CycleConfiguration::build_and_earn_eras_per_cycle() + ), + ); + + // Verify base & adjustable staker rewards are as expected + assert_eq!( + new_config.base_staker_reward_pool_per_era, + params.base_stakers_part * max_emission + / Balance::from( + ::CycleConfiguration::build_and_earn_eras_per_cycle() + ), + ); + assert_eq!( + new_config.adjustable_staker_reward_pool_per_era, + params.adjustable_stakers_part * max_emission + / Balance::from( + ::CycleConfiguration::build_and_earn_eras_per_cycle() + ), + ); + + // Verify bonus rewards are as expected + assert_eq!( + new_config.bonus_reward_pool_per_period, + params.bonus_part * max_emission + / Balance::from(::CycleConfiguration::periods_per_cycle()), + ); + }) +} + +#[test] +fn stakers_and_dapp_reward_pool_is_ok() { + ExternalityBuilder::build().execute_with(|| { + let total_issuance = Balances::total_issuance(); + let config = ActiveInflationConfig::::get(); + + // 1st scenario - no staked value + let (staker_pool, dapp_pool) = Inflation::staker_and_dapp_reward_pools(Zero::zero()); + assert_eq!(staker_pool, config.base_staker_reward_pool_per_era); + assert_eq!(dapp_pool, config.dapp_reward_pool_per_era); + + // 2nd scenario - there is some staked value, larger than zero, but less than ideal + let test_rate = config.ideal_staking_rate - Perquintill::from_percent(11); + let (staker_pool, dapp_pool) = + Inflation::staker_and_dapp_reward_pools(test_rate * total_issuance); + + assert_eq!( + staker_pool, + config.base_staker_reward_pool_per_era + + test_rate / config.ideal_staking_rate + * config.adjustable_staker_reward_pool_per_era + ); + assert_eq!(dapp_pool, config.dapp_reward_pool_per_era); + + // 3rd scenario - we're exactly at the ideal staking rate + let (staker_pool, dapp_pool) = + Inflation::staker_and_dapp_reward_pools(config.ideal_staking_rate * total_issuance); + + assert_eq!( + staker_pool, + config.base_staker_reward_pool_per_era + config.adjustable_staker_reward_pool_per_era + ); + assert_eq!(dapp_pool, config.dapp_reward_pool_per_era); + + // 4th scenario - we're above ideal staking rate, should be the same as at the ideal staking rate regarding the pools + let test_rate = config.ideal_staking_rate + Perquintill::from_percent(13); + let (staker_pool, dapp_pool) = + Inflation::staker_and_dapp_reward_pools(test_rate * total_issuance); + + assert_eq!( + staker_pool, + config.base_staker_reward_pool_per_era + config.adjustable_staker_reward_pool_per_era + ); + assert_eq!(dapp_pool, config.dapp_reward_pool_per_era); + + // 5th scenario - ideal staking rate is zero, entire adjustable amount is always used. + ActiveInflationConfig::::mutate(|config| { + config.ideal_staking_rate = Zero::zero(); + }); + + let (staker_pool, dapp_pool) = + Inflation::staker_and_dapp_reward_pools(Perquintill::from_percent(5) * total_issuance); + + assert_eq!( + staker_pool, + config.base_staker_reward_pool_per_era + config.adjustable_staker_reward_pool_per_era + ); + assert_eq!(dapp_pool, config.dapp_reward_pool_per_era); + }) +} + +#[test] +fn bonus_reward_pool_is_ok() { + ExternalityBuilder::build().execute_with(|| { + let config = ActiveInflationConfig::::get(); + + let bonus_pool = Inflation::bonus_reward_pool(); + assert_eq!(bonus_pool, config.bonus_reward_pool_per_period); + }) +} + +#[test] +fn basic_payout_reward_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // Prepare reward payout params + let config = ActiveInflationConfig::::get(); + let account = 1; + let reward = config.issuance_safety_cap - Balances::total_issuance(); + let init_balance = Balances::free_balance(&account); + let init_issuance = Balances::total_issuance(); + + // Payout reward and verify balances are as expected + assert_ok!(Inflation::payout_reward(&account, reward)); + + assert_eq!(Balances::free_balance(&account), init_balance + reward); + assert_eq!(Balances::total_issuance(), init_issuance + reward); + }) +} + +#[test] +fn payout_reward_with_exceeded_cap_but_not_exceeded_relaxed_cap_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // Prepare reward payout params + let config = ActiveInflationConfig::::get(); + let account = 1; + + let relaxed_cap = config.issuance_safety_cap * 101 / 100; + let reward = relaxed_cap - Balances::total_issuance(); + let init_balance = Balances::free_balance(&account); + let init_issuance = Balances::total_issuance(); + + // Payout reward and verify balances are as expected + assert_ok!(Inflation::payout_reward(&account, reward)); + + assert_eq!(Balances::free_balance(&account), init_balance + reward); + assert_eq!(Balances::total_issuance(), init_issuance + reward); + }) +} + +#[test] +fn payout_reward_fails_when_relaxed_cap_is_exceeded() { + ExternalityBuilder::build().execute_with(|| { + // Prepare reward payout params + let config = ActiveInflationConfig::::get(); + let account = 1; + + let relaxed_cap = config.issuance_safety_cap * 101 / 100; + let reward = relaxed_cap - Balances::total_issuance() + 1; + + // Payout should be a failure, with storage noop. + assert_noop!(Inflation::payout_reward(&account, reward), ()); + }) +} + +#[test] +fn cylcle_configuration_works() { + ExternalityBuilder::build().execute_with(|| { + type CycleConfig = ::CycleConfiguration; + + let eras_per_period = CycleConfig::eras_per_voting_subperiod() + + CycleConfig::eras_per_build_and_earn_subperiod(); + assert_eq!(CycleConfig::eras_per_period(), eras_per_period); + + let eras_per_cycle = eras_per_period * CycleConfig::periods_per_cycle(); + assert_eq!(CycleConfig::eras_per_cycle(), eras_per_cycle); + + let blocks_per_cycle = eras_per_cycle * CycleConfig::blocks_per_era(); + assert_eq!(CycleConfig::blocks_per_cycle(), blocks_per_cycle); + + let build_and_earn_eras_per_cycle = + CycleConfig::eras_per_build_and_earn_subperiod() * CycleConfig::periods_per_cycle(); + assert_eq!( + CycleConfig::build_and_earn_eras_per_cycle(), + build_and_earn_eras_per_cycle + ); + }) +} + +#[test] +fn test_genesis_build() { + ExternalityBuilder::build().execute_with(|| { + let genesis_config = InflationConfig::default(); + assert!(genesis_config.params.is_valid()); + + // Prep actions + ActiveInflationConfig::::kill(); + InflationParams::::kill(); + + // Execute genesis build + >::build(&genesis_config); + + // Verify state is as expected + assert_eq!(InflationParams::::get(), genesis_config.params); + assert!(ActiveInflationConfig::::get().recalculation_block > 0); + }) +} diff --git a/pallets/inflation/src/weights.rs b/pallets/inflation/src/weights.rs new file mode 100644 index 0000000000..1cee14c199 --- /dev/null +++ b/pallets/inflation/src/weights.rs @@ -0,0 +1,154 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for pallet_inflation +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-11-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Dinos-MacBook-Pro.local`, CPU: `` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_inflation +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=weights.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_inflation. +pub trait WeightInfo { + fn force_set_inflation_params() -> Weight; + fn force_set_inflation_config() -> Weight; + fn force_inflation_recalculation() -> Weight; + fn hook_with_recalculation() -> Weight; + fn hook_without_recalculation() -> Weight; +} + +/// Weights for pallet_inflation using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Inflation InflationParams (r:0 w:1) + /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) + fn force_set_inflation_params() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn force_set_inflation_config() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) + } + /// Storage: Inflation InflationParams (r:1 w:0) + /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) + fn force_inflation_recalculation() -> Weight { + // Proof Size summary in bytes: + // Measured: `59` + // Estimated: `1549` + // Minimum execution time: 15_000_000 picoseconds. + Weight::from_parts(16_000_000, 1549) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: Inflation InflationParams (r:1 w:0) + /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) + fn hook_with_recalculation() -> Weight { + // Proof Size summary in bytes: + // Measured: `59` + // Estimated: `1549` + // Minimum execution time: 14_000_000 picoseconds. + Weight::from_parts(15_000_000, 1549) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + fn hook_without_recalculation() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_000_000 picoseconds. + Weight::from_parts(3_000_000, 0) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Inflation InflationParams (r:0 w:1) + /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) + fn force_set_inflation_params() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + fn force_set_inflation_config() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) + } + /// Storage: Inflation InflationParams (r:1 w:0) + /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) + fn force_inflation_recalculation() -> Weight { + // Proof Size summary in bytes: + // Measured: `59` + // Estimated: `1549` + // Minimum execution time: 15_000_000 picoseconds. + Weight::from_parts(16_000_000, 1549) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: Inflation InflationParams (r:1 w:0) + /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) + fn hook_with_recalculation() -> Weight { + // Proof Size summary in bytes: + // Measured: `59` + // Estimated: `1549` + // Minimum execution time: 14_000_000 picoseconds. + Weight::from_parts(15_000_000, 1549) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + fn hook_without_recalculation() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_000_000 picoseconds. + Weight::from_parts(3_000_000, 0) + } +} diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index 3ecbb31994..e04f05e997 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -78,6 +78,7 @@ pallet-evm-precompile-dapps-staking = { workspace = true } pallet-evm-precompile-sr25519 = { workspace = true } pallet-evm-precompile-substrate-ecdsa = { workspace = true } pallet-evm-precompile-xvm = { workspace = true } +pallet-inflation = { workspace = true } pallet-xvm = { workspace = true } # Moonbeam tracing @@ -120,6 +121,7 @@ std = [ "pallet-custom-signatures/std", "pallet-dapps-staking/std", "pallet-dapp-staking-v3/std", + "pallet-inflation/std", "pallet-base-fee/std", "pallet-ethereum/std", "pallet-evm/std", @@ -190,6 +192,7 @@ runtime-benchmarks = [ "astar-primitives/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-dapp-staking-v3/runtime-benchmarks", + "pallet-inflation/runtime-benchmarks", ] try-runtime = [ "fp-self-contained/try-runtime", @@ -205,6 +208,7 @@ try-runtime = [ "pallet-custom-signatures/try-runtime", "pallet-dapps-staking/try-runtime", "pallet-dapp-staking-v3/try-runtime", + "pallet-inflation/try-runtime", "pallet-grandpa/try-runtime", "pallet-insecure-randomness-collective-flip/try-runtime", "pallet-sudo/try-runtime", diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index ab8217a9ff..d0cfdda419 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -73,6 +73,7 @@ use sp_version::RuntimeVersion; pub use frame_system::Call as SystemCall; pub use pallet_balances::Call as BalancesCall; pub use pallet_grandpa::AuthorityId as GrandpaId; +pub use pallet_inflation::InflationParameters; pub use pallet_timestamp::Call as TimestampCall; use pallet_transaction_payment::CurrencyAdapter; pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; @@ -471,6 +472,12 @@ impl pallet_dapp_staking_v3::BenchmarkHelper> } } +parameter_types! { + pub const StandardEraLength: BlockNumber = 30; // should be 1 minute per standard era + pub const StandardErasPerVotingSubperiod: u32 = 2; + pub const StandardErasPerBuildAndEarnSubperiod: u32 = 10; +} + impl pallet_dapp_staking_v3::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; @@ -478,9 +485,9 @@ impl pallet_dapp_staking_v3::Config for Runtime { type ManagerOrigin = frame_system::EnsureRoot; type NativePriceProvider = DummyPriceProvider; type RewardPoolProvider = DummyRewardPoolProvider; - type StandardEraLength = ConstU32<30>; // should be 1 minute per standard era - type StandardErasPerVotingSubperiod = ConstU32<2>; - type StandardErasPerBuildAndEarnSubperiod = ConstU32<10>; + type StandardEraLength = StandardEraLength; + type StandardErasPerVotingSubperiod = StandardErasPerVotingSubperiod; + type StandardErasPerBuildAndEarnSubperiod = StandardErasPerBuildAndEarnSubperiod; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; type MaxNumberOfContracts = ConstU32<100>; @@ -494,6 +501,44 @@ impl pallet_dapp_staking_v3::Config for Runtime { type BenchmarkHelper = BenchmarkHelper>; } +pub struct InflationPayoutPerBlock; +impl pallet_inflation::PayoutPerBlock for InflationPayoutPerBlock { + fn treasury(reward: NegativeImbalance) { + Balances::resolve_creating(&TreasuryPalletId::get().into_account_truncating(), reward); + } + + fn collators(_reward: NegativeImbalance) { + // no collators for local dev node + } +} + +pub struct InflationCycleConfig; +impl pallet_inflation::CycleConfiguration for InflationCycleConfig { + fn periods_per_cycle() -> u32 { + 4 + } + + fn eras_per_voting_subperiod() -> u32 { + StandardErasPerVotingSubperiod::get() + } + + fn eras_per_build_and_earn_subperiod() -> u32 { + StandardErasPerBuildAndEarnSubperiod::get() + } + + fn blocks_per_era() -> u32 { + StandardEraLength::get() + } +} + +impl pallet_inflation::Config for Runtime { + type Currency = Balances; + type PayoutPerBlock = InflationPayoutPerBlock; + type CycleConfiguration = InflationCycleConfig; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_inflation::weights::SubstrateWeight; +} + impl pallet_utility::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; @@ -1062,6 +1107,7 @@ construct_runtime!( DappsStaking: pallet_dapps_staking, DappStaking: pallet_dapp_staking_v3, BlockReward: pallet_block_reward, + Inflation: pallet_inflation, TransactionPayment: pallet_transaction_payment, EVM: pallet_evm, Ethereum: pallet_ethereum, @@ -1194,6 +1240,7 @@ mod benches { [pallet_block_reward, BlockReward] [pallet_ethereum_checked, EthereumChecked] [pallet_dapp_staking_v3, DappStaking] + [pallet_inflation, Inflation] ); } From 91047f4a381dc4b7a245224771ef28eeacd69e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:33:09 +0100 Subject: [PATCH 08/14] dApp Staking v3 - part 6 (#1093) * dApp staking v3 part 6 * Minor refactor of benchmarks * Weights integration * Fix * remove orig file * Minor refactoring, more benchmark code * Extract on_init logic * Some renaming * More benchmarks * Full benchmarks integration * Testing primitives * staking primitives * dev fix * Integration part1 * Integration part2 * Reward payout integration * Replace lock functionality with freeze * Cleanup TODOs * More negative tests * Frozen balance test * Zero div * Docs for inflation * Rename is_active & add some more docs * More docs * pallet docs * text * scripts * More tests * Test, docs * Review comment --- pallets/dapp-staking-v3/README.md | 30 +- .../dapp-staking-v3/coverage_extrinsics.sh | 11 + .../{coverage.sh => coverage_types.sh} | 0 .../{benchmarking.rs => benchmarking/mod.rs} | 380 ++++---- .../dapp-staking-v3/src/benchmarking/utils.rs | 218 +++++ pallets/dapp-staking-v3/src/dsv3_weight.rs | 100 --- pallets/dapp-staking-v3/src/lib.rs | 634 ++++++++------ pallets/dapp-staking-v3/src/test/mock.rs | 117 ++- .../dapp-staking-v3/src/test/testing_utils.rs | 48 +- pallets/dapp-staking-v3/src/test/tests.rs | 234 +++-- .../dapp-staking-v3/src/test/tests_types.rs | 162 ++-- pallets/dapp-staking-v3/src/types.rs | 89 +- pallets/dapp-staking-v3/src/weights.rs | 809 ++++++++++++++++++ pallets/inflation/src/lib.rs | 146 ++-- pallets/inflation/src/mock.rs | 7 +- primitives/src/dapp_staking.rs | 85 ++ primitives/src/lib.rs | 6 + primitives/src/testing.rs | 24 + runtime/local/src/lib.rs | 63 +- 19 files changed, 2189 insertions(+), 974 deletions(-) create mode 100755 pallets/dapp-staking-v3/coverage_extrinsics.sh rename pallets/dapp-staking-v3/{coverage.sh => coverage_types.sh} (100%) rename pallets/dapp-staking-v3/src/{benchmarking.rs => benchmarking/mod.rs} (74%) create mode 100644 pallets/dapp-staking-v3/src/benchmarking/utils.rs delete mode 100644 pallets/dapp-staking-v3/src/dsv3_weight.rs create mode 100644 pallets/dapp-staking-v3/src/weights.rs create mode 100644 primitives/src/dapp_staking.rs create mode 100644 primitives/src/testing.rs diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md index afae398db2..72accbfc36 100644 --- a/pallets/dapp-staking-v3/README.md +++ b/pallets/dapp-staking-v3/README.md @@ -27,18 +27,22 @@ Each period consists of two subperiods: Each period is denoted by a number, which increments each time a new period begins. Period beginning is marked by the `voting` subperiod, after which follows the `build&earn` period. -Stakes are **only** valid throughout a period. When new period starts, all stakes are reset to zero. This helps prevent projects remaining staked due to intertia. +Stakes are **only** valid throughout a period. When new period starts, all stakes are reset to **zero**. This helps prevent projects remaining staked due to intertia of stakers, and makes for a more dynamic staking system. + +Even though stakes are reset, locks (or freezes) of tokens remain. #### Voting -When `Voting` starts, all _stakes_ are reset to **zero**. +When `Voting` subperiod starts, all _stakes_ are reset to **zero**. Projects participating in dApp staking are expected to market themselves to (re)attract stakers. Stakers must assess whether the project they want to stake on brings value to the ecosystem, and then `vote` for it. Casting a vote, or staking, during the `Voting` subperiod makes the staker eligible for bonus rewards. so they are encouraged to participate. `Voting` subperiod length is expressed in _standard_ era lengths, even though the entire voting period is treated as a single _voting era_. -E.g. if `voting` subperiod lasts for **10 eras**, and each era lasts for **100** blocks, total length of the `voting` subperiod will be **1000** blocks. +E.g. if `voting` subperiod lasts for **5 eras**, and each era lasts for **100** blocks, total length of the `voting` subperiod will be **500** blocks. +* Block 1, Era 1 starts, Period 1 starts, `Voting` subperiod starts +* Block 501, Era 2 starts, Period 1 continues, `Build&Earn` subperiod starts Neither stakers nor dApps earn rewards during this subperiod - no new rewards are generated after `voting` subperiod ends. @@ -51,6 +55,16 @@ After each _era_ ends, eligible stakers and dApps can claim the rewards they ear It is still possible to _stake_ during this period, and stakers are encouraged to do so since this will increase the rewards they earn. The only exemption is the **final era** of the `build&earn` subperiod - it's not possible to _stake_ then since the stake would be invalid anyhow (stake is only valid from the next era which would be in the next period). +To continue the previous example where era length is **100** blocks, let's assume that `Build&Earn` subperiod lasts for 10 eras: +* Block 1, Era 1 starts, Period 1 starts, `Voting` subperiod starts +* Block 501, Era 2 starts, Period 1 continues, `Build&Earn` subperiod starts +* Block 601, Era 3 starts, Period 1 continues, `Build&Earn` subperiod continues +* Block 701, Era 4 starts, Period 1 continues, `Build&Earn` subperiod continues +* ... +* Block 1401, Era 11 starts, Period 1 continues, `Build&Earn` subperiod enters the final era +* Block 1501, Era 12 starts, Period 2 starts, `Voting` subperiod starts +* Block 2001, Era 13 starts, Period 2 continues, `Build&Earn` subperiod starts + ### dApps & Smart Contracts Protocol is called dApp staking, but internally it essentially works with smart contracts, or even more precise, smart contract addresses. @@ -63,12 +77,14 @@ Projects, or _dApps_, must be registered into protocol to participate. Only a privileged `ManagerOrigin` can perform dApp registration. The pallet itself does not make assumptions who the privileged origin is, and it can differ from runtime to runtime. +Once dApp has been registered, stakers can stake on it immediatelly. + +When contract is registered, it is assigned a unique compact numeric Id - 16 bit unsigned integer. This is important for the inner workings of the pallet, and is not directly exposed to the users. + There is a limit of how many smart contracts can be registered at once. Once the limit is reached, any additional attempt to register a new contract will fail. #### Reward Beneficiary & Ownership -When contract is registered, it is assigned a unique compact numeric Id - 16 bit unsigned integer. This is important for the inner workings of the pallet, and is not directly exposed to the users. - After a dApp has been registered, it is possible to modify reward beneficiary or even the owner of the dApp. The owner can perform reward delegation and can further transfer ownership. #### Unregistration @@ -81,13 +97,13 @@ It's still possible however to claim past unclaimed rewards. Important to note that even if dApp has been unregistered, it still occupies a _slot_ in the dApp staking protocol and counts towards maximum number of registered dApps. -This will be improved in the future when dApp data will be cleaned up after the period ends. +This will be improved in the future when dApp data will be cleaned up after some time. ### Stakers #### Locking Tokens -In order for users to participate in dApp staking, the first step they need to take is lock some native currency. Reserved tokens cannot be locked, but tokens locked by another lock can be re-locked into dApp staking (double locked). +In order for users to participate in dApp staking, the first step they need to take is lock (or freeze) some native currency. Reserved tokens cannot be locked, but tokens locked by another lock can be re-locked into dApp staking (double locked). **NOTE:** Locked funds cannot be used for paying fees, or for transfer. diff --git a/pallets/dapp-staking-v3/coverage_extrinsics.sh b/pallets/dapp-staking-v3/coverage_extrinsics.sh new file mode 100755 index 0000000000..6d0c77b3f0 --- /dev/null +++ b/pallets/dapp-staking-v3/coverage_extrinsics.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +targets=("register" "unregister" "set_dapp_reward_beneficiary" "set_dapp_owner" "maintenance_mode" \ + "lock" "unlock" "claim_unlocked" "relock_unlocking" \ + "stake" "unstake" "claim_staker_rewards" "claim_bonus_reward" "claim_dapp_reward" \ + "unstake_from_unregistered" "cleanup_expired_entries" "force" ) + +for target in "${targets[@]}" +do + cargo tarpaulin -p pallet-dapp-staking-v3 -o=html --output-dir=./coverage/$target -- test::tests::$target +done \ No newline at end of file diff --git a/pallets/dapp-staking-v3/coverage.sh b/pallets/dapp-staking-v3/coverage_types.sh similarity index 100% rename from pallets/dapp-staking-v3/coverage.sh rename to pallets/dapp-staking-v3/coverage_types.sh diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking/mod.rs similarity index 74% rename from pallets/dapp-staking-v3/src/benchmarking.rs rename to pallets/dapp-staking-v3/src/benchmarking/mod.rs index 2b04874516..4f9cbafdb9 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking/mod.rs @@ -26,161 +26,8 @@ use frame_system::{Pallet as System, RawOrigin}; use ::assert_matches::assert_matches; -// TODO: make benchmark utils file and move all these helper methods there to keep this file clean(er) - -// TODO2: non-extrinsic calls still need to be benchmarked. - -/// Run to the specified block number. -/// Function assumes first block has been initialized. -fn run_to_block(n: BlockNumberFor) { - while System::::block_number() < n { - DappStaking::::on_finalize(System::::block_number()); - System::::set_block_number(System::::block_number() + One::one()); - // This is performed outside of dapps staking but we expect it before on_initialize - DappStaking::::on_initialize(System::::block_number()); - } -} - -/// Run for the specified number of blocks. -/// Function assumes first block has been initialized. -fn run_for_blocks(n: BlockNumberFor) { - run_to_block::(System::::block_number() + n); -} - -/// Advance blocks until the specified era has been reached. -/// -/// Function has no effect if era is already passed. -pub(crate) fn advance_to_era(era: EraNumber) { - assert!(era >= ActiveProtocolState::::get().era); - while ActiveProtocolState::::get().era < era { - run_for_blocks::(One::one()); - } -} - -/// Advance blocks until next era has been reached. -pub(crate) fn advance_to_next_era() { - advance_to_era::(ActiveProtocolState::::get().era + 1); -} - -/// Advance blocks until the specified period has been reached. -/// -/// Function has no effect if period is already passed. -pub(crate) fn advance_to_period(period: PeriodNumber) { - assert!(period >= ActiveProtocolState::::get().period_number()); - while ActiveProtocolState::::get().period_number() < period { - run_for_blocks::(One::one()); - } -} - -/// Advance blocks until next period has been reached. -pub(crate) fn advance_to_next_period() { - advance_to_period::(ActiveProtocolState::::get().period_number() + 1); -} - -/// Advance blocks until next period type has been reached. -pub(crate) fn advance_to_next_subperiod() { - let subperiod = ActiveProtocolState::::get().subperiod(); - while ActiveProtocolState::::get().subperiod() == subperiod { - run_for_blocks::(One::one()); - } -} - -// All our networks use 18 decimals for native currency so this should be fine. -const UNIT: Balance = 1_000_000_000_000_000_000; - -// Minimum amount that must be staked on a dApp to enter any tier -const MIN_TIER_THRESHOLD: Balance = 10 * UNIT; - -const NUMBER_OF_SLOTS: u32 = 100; - -const SEED: u32 = 9000; - -/// Assert that the last event equals the provided one. -fn assert_last_event(generic_event: ::RuntimeEvent) { - frame_system::Pallet::::assert_last_event(generic_event.into()); -} - -// Return all dApp staking events from the event buffer. -fn dapp_staking_events() -> Vec> { - System::::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| ::RuntimeEvent::from(e).try_into().ok()) - .collect::>() -} - -// TODO: make it more generic per runtime? -pub fn initial_config() { - let era_length = T::StandardEraLength::get(); - let voting_period_length_in_eras = T::StandardErasPerVotingSubperiod::get(); - - // Init protocol state - ActiveProtocolState::::put(ProtocolState { - era: 1, - next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + One::one(), - period_info: PeriodInfo { - number: 1, - subperiod: Subperiod::Voting, - subperiod_end_era: 2, - }, - maintenance: false, - }); - - // Init tier params - let tier_params = TierParameters:: { - reward_portion: BoundedVec::try_from(vec![ - Permill::from_percent(40), - Permill::from_percent(30), - Permill::from_percent(20), - Permill::from_percent(10), - ]) - .unwrap(), - slot_distribution: BoundedVec::try_from(vec![ - Permill::from_percent(10), - Permill::from_percent(20), - Permill::from_percent(30), - Permill::from_percent(40), - ]) - .unwrap(), - tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { - amount: 100 * UNIT, - minimum_amount: 80 * UNIT, - }, - TierThreshold::DynamicTvlAmount { - amount: 50 * UNIT, - minimum_amount: 40 * UNIT, - }, - TierThreshold::DynamicTvlAmount { - amount: 20 * UNIT, - minimum_amount: 20 * UNIT, - }, - TierThreshold::FixedTvlAmount { - amount: MIN_TIER_THRESHOLD, - }, - ]) - .unwrap(), - }; - - // Init tier config, based on the initial params - let init_tier_config = TiersConfiguration:: { - number_of_slots: NUMBER_OF_SLOTS.try_into().unwrap(), - slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), - reward_portion: tier_params.reward_portion.clone(), - tier_thresholds: tier_params.tier_thresholds.clone(), - }; - - assert!(tier_params.is_valid()); - assert!(init_tier_config.is_valid()); - - StaticTierParams::::put(tier_params); - TierConfig::::put(init_tier_config.clone()); - NextTierConfig::::put(init_tier_config); -} - -fn max_number_of_contracts() -> u32 { - T::MaxNumberOfContracts::get().min(NUMBER_OF_SLOTS).into() -} +mod utils; +use utils::*; #[benchmarks] mod benchmarks { @@ -312,7 +159,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); #[extrinsic_call] _(RawOrigin::Signed(staker.clone()), amount); @@ -340,7 +187,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get() * 2; - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -358,39 +205,6 @@ mod benchmarks { ); } - // TODO: maybe this is not needed. Compare it after running benchmarks to the 'not-full' unlock - #[benchmark] - fn full_unlock() { - initial_config::(); - - let staker: T::AccountId = whitelisted_caller(); - let owner: T::AccountId = account("dapp_owner", 0, SEED); - let smart_contract = T::BenchmarkHelper::get_smart_contract(1); - assert_ok!(DappStaking::::register( - RawOrigin::Root.into(), - owner.clone().into(), - smart_contract.clone(), - )); - - let amount = T::MinimumLockedAmount::get() * 2; - T::Currency::make_free_balance_be(&staker, amount); - assert_ok!(DappStaking::::lock( - RawOrigin::Signed(staker.clone()).into(), - amount, - )); - - #[extrinsic_call] - unlock(RawOrigin::Signed(staker.clone()), amount); - - assert_last_event::( - Event::::Unlocking { - account: staker, - amount, - } - .into(), - ); - } - #[benchmark] fn claim_unlocked(x: Linear<0, { T::MaxNumberOfStakedContracts::get() }>) { // Prepare staker account and lock some amount @@ -398,7 +212,7 @@ mod benchmarks { let amount = (T::MinimumStakeAmount::get() + 1) * Into::::into(max_number_of_contracts::()) + Into::::into(T::MaxUnlockingChunks::get()); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -485,7 +299,7 @@ mod benchmarks { let amount = T::MinimumLockedAmount::get() * 2 + Into::::into(T::MaxUnlockingChunks::get()); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -528,7 +342,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -565,7 +379,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get() + 1; - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -612,7 +426,7 @@ mod benchmarks { // Lock some amount by the staker let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -667,7 +481,7 @@ mod benchmarks { // Lock & stake some amount by the staker let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -722,7 +536,7 @@ mod benchmarks { // Lock & stake some amount by the staker let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -761,7 +575,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get() * 1000 * UNIT; - T::Currency::make_free_balance_be(&owner, amount); + T::BenchmarkHelper::set_balance(&owner, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(owner.clone()).into(), amount, @@ -784,7 +598,7 @@ mod benchmarks { )); let staker: T::AccountId = account("staker", idx.into(), SEED); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -846,7 +660,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -887,7 +701,7 @@ mod benchmarks { let staker: T::AccountId = whitelisted_caller(); let amount = T::MinimumLockedAmount::get() * Into::::into(T::MaxNumberOfStakedContracts::get()); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -937,48 +751,143 @@ mod benchmarks { assert_last_event::(Event::::Force { forcing_type }.into()); } - // TODO: investigate why the PoV size is so large here, evne after removing read of `IntegratedDApps` storage. - // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs - // UPDATE: after some investigation, it seems that PoV size benchmarks are very unprecise - // - the worst case measured is usually very far off the actual value that is consumed on chain. - // There's an ongoing item to improve it (mentioned on roundtable meeting). #[benchmark] - fn dapp_tier_assignment(x: Linear<0, { max_number_of_contracts::() }>) { - // Prepare init config (protocol state, tier params & config, etc.) + fn on_initialize_voting_to_build_and_earn() { initial_config::(); - let developer: T::AccountId = whitelisted_caller(); - for id in 0..x { - let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); - assert_ok!(DappStaking::::register( - RawOrigin::Root.into(), - developer.clone().into(), - smart_contract, - )); + let state = ActiveProtocolState::::get(); + assert_eq!(state.subperiod(), Subperiod::Voting, "Sanity check."); + + // Register & stake contracts, just so we don't have empty stakes. + prepare_contracts_for_tier_assignment::(max_number_of_contracts::()); + + run_to_block::(state.next_era_start - 1); + DappStaking::::on_finalize(state.next_era_start - 1); + System::::set_block_number(state.next_era_start); + + #[block] + { + DappStaking::::era_and_period_handler(state.next_era_start, TierAssignment::Dummy); } - // TODO: try to make this more "shuffled" so the generated vector ends up being more random - let mut amount = 1000 * MIN_TIER_THRESHOLD; - for id in 0..x { - let staker = account("staker", id.into(), 1337); - T::Currency::make_free_balance_be(&staker, amount); - assert_ok!(DappStaking::::lock( - RawOrigin::Signed(staker.clone()).into(), - amount, - )); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn + ); + } - let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); - assert_ok!(DappStaking::::stake( - RawOrigin::Signed(staker.clone()).into(), - smart_contract, - amount, - )); + #[benchmark] + fn on_initialize_build_and_earn_to_voting() { + initial_config::(); + + // Get started + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::Voting, + "Sanity check." + ); - // Slowly decrease the stake amount - amount.saturating_reduce(UNIT); + // Register & stake contracts, just so we don't have empty stakes. + prepare_contracts_for_tier_assignment::(max_number_of_contracts::()); + + // Advance to build&earn subperiod + advance_to_next_subperiod::(); + let snapshot_state = ActiveProtocolState::::get(); + + // Advance over to the last era of the subperiod, and then again to the last block of that era. + advance_to_era::( + ActiveProtocolState::::get() + .period_info + .next_subperiod_start_era + - 1, + ); + run_to_block::(ActiveProtocolState::::get().next_era_start - 1); + + // Some sanity checks, we should still be in the build&earn subperiod, and in the first period. + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn + ); + assert_eq!( + ActiveProtocolState::::get().period_number(), + snapshot_state.period_number(), + ); + + let new_era_start_block = ActiveProtocolState::::get().next_era_start; + DappStaking::::on_finalize(new_era_start_block - 1); + System::::set_block_number(new_era_start_block); + + #[block] + { + DappStaking::::era_and_period_handler(new_era_start_block, TierAssignment::Dummy); } - // Advance to next era + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::Voting + ); + assert_eq!( + ActiveProtocolState::::get().period_number(), + snapshot_state.period_number() + 1, + ); + } + + #[benchmark] + fn on_initialize_build_and_earn_to_build_and_earn() { + initial_config::(); + + // Register & stake contracts, just so we don't have empty stakes. + prepare_contracts_for_tier_assignment::(max_number_of_contracts::()); + + // Advance to build&earn subperiod + advance_to_next_subperiod::(); + let snapshot_state = ActiveProtocolState::::get(); + + // Advance over to the next era, and then again to the last block of that era. + advance_to_next_era::(); + run_to_block::(ActiveProtocolState::::get().next_era_start - 1); + + // Some sanity checks, we should still be in the build&earn subperiod, and in the first period. + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn + ); + assert_eq!( + ActiveProtocolState::::get().period_number(), + snapshot_state.period_number(), + ); + + let new_era_start_block = ActiveProtocolState::::get().next_era_start; + DappStaking::::on_finalize(new_era_start_block - 1); + System::::set_block_number(new_era_start_block); + + #[block] + { + DappStaking::::era_and_period_handler(new_era_start_block, TierAssignment::Dummy); + } + + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn + ); + assert_eq!( + ActiveProtocolState::::get().period_number(), + snapshot_state.period_number(), + ); + } + + // Investigate why the PoV size is so large here, even after removing read of `IntegratedDApps` storage. + // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs + // UPDATE: after some investigation, it seems that PoV size benchmarks are very unprecise + // - the worst case measured is usually very far off the actual value that is consumed on chain. + // There's an ongoing item to improve it (mentioned on roundtable meeting). + #[benchmark] + fn dapp_tier_assignment(x: Linear<0, { max_number_of_contracts::() }>) { + // Prepare init config (protocol state, tier params & config, etc.) + initial_config::(); + + // Register & stake contracts, to prepare for tier assignment. + prepare_contracts_for_tier_assignment::(x); advance_to_next_era::(); let reward_era = ActiveProtocolState::::get().era; @@ -987,9 +896,8 @@ mod benchmarks { #[block] { - let dapp_tiers = + let (dapp_tiers, _) = Pallet::::get_dapp_tier_assignment(reward_era, reward_period, reward_pool); - // TODO: how to move this outside of the 'block'? Cannot declare it outside, and then use it inside. assert_eq!(dapp_tiers.dapps.len(), x as usize); } } diff --git a/pallets/dapp-staking-v3/src/benchmarking/utils.rs b/pallets/dapp-staking-v3/src/benchmarking/utils.rs new file mode 100644 index 0000000000..688f964a76 --- /dev/null +++ b/pallets/dapp-staking-v3/src/benchmarking/utils.rs @@ -0,0 +1,218 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::{Pallet as DappStaking, *}; + +use astar_primitives::Balance; + +use frame_system::Pallet as System; + +/// Run to the specified block number. +/// Function assumes first block has been initialized. +pub(super) fn run_to_block(n: BlockNumberFor) { + while System::::block_number() < n { + DappStaking::::on_finalize(System::::block_number()); + System::::set_block_number(System::::block_number() + 1); + // This is performed outside of dapps staking but we expect it before on_initialize + DappStaking::::on_initialize(System::::block_number()); + } +} + +/// Run for the specified number of blocks. +/// Function assumes first block has been initialized. +pub(super) fn run_for_blocks(n: BlockNumberFor) { + run_to_block::(System::::block_number() + n); +} + +/// Advance blocks until the specified era has been reached. +/// +/// Function has no effect if era is already passed. +pub(super) fn advance_to_era(era: EraNumber) { + assert!(era >= ActiveProtocolState::::get().era); + while ActiveProtocolState::::get().era < era { + run_for_blocks::(One::one()); + } +} + +/// Advance blocks until next era has been reached. +pub(super) fn advance_to_next_era() { + advance_to_era::(ActiveProtocolState::::get().era + 1); +} + +/// Advance blocks until the specified period has been reached. +/// +/// Function has no effect if period is already passed. +pub(super) fn advance_to_period(period: PeriodNumber) { + assert!(period >= ActiveProtocolState::::get().period_number()); + while ActiveProtocolState::::get().period_number() < period { + run_for_blocks::(One::one()); + } +} + +/// Advance blocks until next period has been reached. +pub(super) fn advance_to_next_period() { + advance_to_period::(ActiveProtocolState::::get().period_number() + 1); +} + +/// Advance blocks until next period type has been reached. +pub(super) fn advance_to_next_subperiod() { + let subperiod = ActiveProtocolState::::get().subperiod(); + while ActiveProtocolState::::get().subperiod() == subperiod { + run_for_blocks::(One::one()); + } +} + +/// All our networks use 18 decimals for native currency so this should be fine. +pub(super) const UNIT: Balance = 1_000_000_000_000_000_000; + +/// Minimum amount that must be staked on a dApp to enter any tier +pub(super) const MIN_TIER_THRESHOLD: Balance = 10 * UNIT; + +/// Number of slots in the tier system. +pub(super) const NUMBER_OF_SLOTS: u32 = 100; + +/// Random seed. +pub(super) const SEED: u32 = 9000; + +/// Assert that the last event equals the provided one. +pub(super) fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +// Return all dApp staking events from the event buffer. +pub(super) fn dapp_staking_events() -> Vec> { + System::::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| ::RuntimeEvent::from(e).try_into().ok()) + .collect::>() +} + +/// Initialize dApp staking pallet with initial config. +/// +/// **NOTE:** This assumes similar tier configuration for all runtimes. +/// If we decide to change this, we'll need to provide a more generic init function. +pub(super) fn initial_config() { + let era_length = T::CycleConfiguration::blocks_per_era(); + let voting_period_length_in_eras = T::CycleConfiguration::eras_per_voting_subperiod(); + + // Init protocol state + ActiveProtocolState::::put(ProtocolState { + era: 1, + next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + 1, + period_info: PeriodInfo { + number: 1, + subperiod: Subperiod::Voting, + next_subperiod_start_era: 2, + }, + maintenance: false, + }); + + // Init tier params + let tier_params = TierParameters:: { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { + amount: 100 * UNIT, + minimum_amount: 80 * UNIT, + }, + TierThreshold::DynamicTvlAmount { + amount: 50 * UNIT, + minimum_amount: 40 * UNIT, + }, + TierThreshold::DynamicTvlAmount { + amount: 20 * UNIT, + minimum_amount: 20 * UNIT, + }, + TierThreshold::FixedTvlAmount { + amount: MIN_TIER_THRESHOLD, + }, + ]) + .unwrap(), + }; + + // Init tier config, based on the initial params + let init_tier_config = TiersConfiguration:: { + number_of_slots: NUMBER_OF_SLOTS.try_into().unwrap(), + slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + reward_portion: tier_params.reward_portion.clone(), + tier_thresholds: tier_params.tier_thresholds.clone(), + }; + + assert!(tier_params.is_valid()); + assert!(init_tier_config.is_valid()); + + StaticTierParams::::put(tier_params); + TierConfig::::put(init_tier_config.clone()); + NextTierConfig::::put(init_tier_config); +} + +/// Maximum number of contracts that 'makes sense' - considers both contract number limit & number of slots. +pub(super) fn max_number_of_contracts() -> u32 { + T::MaxNumberOfContracts::get().min(NUMBER_OF_SLOTS).into() +} + +/// Registers & staked on the specified number of smart contracts +/// +/// Stake amounts are decided in such a way to maximize tier filling rate. +/// This means that all of the contracts should end up in some tier. +pub(super) fn prepare_contracts_for_tier_assignment(x: u32) { + let developer: T::AccountId = whitelisted_caller(); + for id in 0..x { + let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + developer.clone().into(), + smart_contract, + )); + } + + // TODO: try to make this more "shuffled" so the generated vector ends up being more random + let mut amount = 1000 * MIN_TIER_THRESHOLD; + for id in 0..x { + let staker = account("staker", id.into(), 1337); + T::BenchmarkHelper::set_balance(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract, + amount, + )); + + // Slowly decrease the stake amount + amount.saturating_reduce(UNIT); + } +} diff --git a/pallets/dapp-staking-v3/src/dsv3_weight.rs b/pallets/dapp-staking-v3/src/dsv3_weight.rs deleted file mode 100644 index c1f88e9f1a..0000000000 --- a/pallets/dapp-staking-v3/src/dsv3_weight.rs +++ /dev/null @@ -1,100 +0,0 @@ - -// This file is part of Astar. - -// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// Astar is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Astar 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 General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Astar. If not, see . - -//! Autogenerated weights for pallet_dapp_staking_v3 -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-11-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Dinos-MBP`, CPU: `` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 - -// Executed Command: -// ./target/release/astar-collator -// benchmark -// pallet -// --chain=dev -// --steps=50 -// --repeat=20 -// --pallet=pallet_dapp_staking-v3 -// --extrinsic=* -// --execution=wasm -// --wasm-execution=compiled -// --heap-pages=4096 -// --output=dapp_staking_v3.rs -// --template=./scripts/templates/weight-template.hbs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use core::marker::PhantomData; - -/// Weight functions needed for pallet_dapp_staking_v3. -pub trait WeightInfo { - fn dapp_tier_assignment(x: u32, ) -> Weight; -} - -/// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) - /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) - /// Storage: DappStaking ContractStake (r:101 w:0) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) - /// Storage: DappStaking TierConfig (r:1 w:0) - /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) - /// The range of component `x` is `[0, 100]`. - fn dapp_tier_assignment(x: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `449 + x * (33 ±0)` - // Estimated: `3063 + x * (2073 ±0)` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(16_776_512, 3063) - // Standard Error: 3_400 - .saturating_add(Weight::from_parts(2_636_298, 0).saturating_mul(x.into())) - .saturating_add(T::DbWeight::get().reads(3_u64)) - .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) - } -} - -// For backwards compatibility and tests -impl WeightInfo for () { - /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) - /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) - /// Storage: DappStaking ContractStake (r:101 w:0) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) - /// Storage: DappStaking TierConfig (r:1 w:0) - /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) - /// The range of component `x` is `[0, 100]`. - fn dapp_tier_assignment(x: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `449 + x * (33 ±0)` - // Estimated: `3063 + x * (2073 ±0)` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(16_776_512, 3063) - // Standard Error: 3_400 - .saturating_add(Weight::from_parts(2_636_298, 0).saturating_mul(x.into())) - .saturating_add(RocksDbWeight::get().reads(3_u64)) - .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) - } -} diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index e282f0959f..89ec801e53 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -17,32 +17,30 @@ // along with Astar. If not, see . //! # dApp Staking v3 Pallet -//! TODO //! -//! - [`Config`] +//! For detailed high level documentation, please refer to the attached README.md file. +//! The crate level docs will cover overal pallet structure & implementation details. //! //! ## Overview //! -//! Pallet that implements dapps staking protocol. +//! Pallet that implements the dApp staking v3 protocol. +//! It covers everything from locking, staking, tier configuration & assignment, reward calculation & payout. //! -//! <> +//! The `types` module contains all of the types used to implement the pallet. +//! All of these _types_ are exentisvely tested in their dedicated `test_types` module. //! -//! ## Interface -//! -//! ### Dispatchable Function -//! -//! <> -//! -//! ### Other -//! -//! <> +//! Rest of the pallet logic is concenrated in the lib.rs file. +//! This logic is tested in the `tests` module, with the help of extensive `testing_utils`. //! #![cfg_attr(not(feature = "std"), no_std)] use frame_support::{ pallet_prelude::*, - traits::{Currency, LockIdentifier, LockableCurrency, StorageVersion, WithdrawReasons}, + traits::{ + fungible::{Inspect as FunInspect, MutateFreeze as FunMutateFreeze}, + StorageVersion, + }, weights::Weight, }; use frame_system::pallet_prelude::*; @@ -52,7 +50,10 @@ use sp_runtime::{ }; pub use sp_std::vec::Vec; -use astar_primitives::Balance; +use astar_primitives::{ + dapp_staking::{CycleConfiguration, StakingRewardHandler}, + Balance, BlockNumber, +}; pub use pallet::*; @@ -64,15 +65,23 @@ mod benchmarking; mod types; use types::*; -pub use types::{PriceProvider, RewardPoolProvider, TierThreshold}; +pub use types::{PriceProvider, TierThreshold}; -mod dsv3_weight; - -// Lock identifier for the dApp staking pallet -const STAKING_ID: LockIdentifier = *b"dapstake"; +pub mod weights; +pub use weights::WeightInfo; const LOG_TARGET: &str = "dapp-staking"; +/// Helper enum for benchmarking. +pub(crate) enum TierAssignment { + /// Real tier assignment calculation should be done. + Real, + /// Dummy tier assignment calculation should be done, e.g. default value should be returned. + #[cfg(feature = "runtime-benchmarks")] + Dummy, +} + +#[doc = include_str!("../README.md")] #[frame_support::pallet] pub mod pallet { use super::*; @@ -85,24 +94,27 @@ pub mod pallet { pub struct Pallet(_); #[cfg(feature = "runtime-benchmarks")] - pub trait BenchmarkHelper { + pub trait BenchmarkHelper { fn get_smart_contract(id: u32) -> SmartContract; + + fn set_balance(account: &AccountId, balance: Balance); } #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config { /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent> + TryInto>; + /// The overarching freeze reason. + type RuntimeFreezeReason: From; + /// Currency used for staking. - /// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :) - // https://github.com/paritytech/substrate/pull/12951/ - // Look at nomination pools implementation for reference! - type Currency: LockableCurrency< + /// Reference: + type Currency: FunMutateFreeze< Self::AccountId, - Moment = Self::BlockNumber, + Id = Self::RuntimeFreezeReason, Balance = Balance, >; @@ -115,23 +127,11 @@ pub mod pallet { /// Used to provide price information about the native token. type NativePriceProvider: PriceProvider; - /// Used to provide reward pools amount. - type RewardPoolProvider: RewardPoolProvider; + /// Used to handle reward payouts & reward pool amount fetching. + type StakingRewardHandler: StakingRewardHandler; - /// Length of a standard era in block numbers. - #[pallet::constant] - type StandardEraLength: Get; - - /// Length of the `Voting` subperiod in standard eras. - /// Although `Voting` subperiod only consumes one 'era', we still measure its length in standard eras - /// for the sake of simplicity & consistency. - #[pallet::constant] - type StandardErasPerVotingSubperiod: Get; - - /// Length of the `Build&Earn` subperiod in standard eras. - /// Each `Build&Earn` subperiod consists of one or more distinct standard eras. - #[pallet::constant] - type StandardErasPerBuildAndEarnSubperiod: Get; + /// Describes era length, subperiods & period length, as well as cycle length. + type CycleConfiguration: CycleConfiguration; /// Maximum length of a single era reward span length entry. #[pallet::constant] @@ -159,7 +159,7 @@ pub mod pallet { #[pallet::constant] type UnlockingPeriod: Get; - /// Maximum amount of stake entries contract is allowed to have at once. + /// Maximum amount of stake contract entries an account is allowed to have at once. #[pallet::constant] type MaxNumberOfStakedContracts: Get; @@ -171,9 +171,12 @@ pub mod pallet { #[pallet::constant] type NumberOfTiers: Get; + /// Weight info for various calls & operations in the pallet. + type WeightInfo: WeightInfo; + /// Helper trait for benchmarks. #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper: BenchmarkHelper; + type BenchmarkHelper: BenchmarkHelper; } #[pallet::event] @@ -323,6 +326,8 @@ pub mod pallet { InternalUnstakeError, /// Rewards are no longer claimable since they are too old. RewardExpired, + /// Reward payout has failed due to an unexpected reason. + RewardPayoutFailed, /// There are no claimable rewards. NoClaimableRewards, /// An unexpected error occured while trying to claim staker rewards. @@ -350,14 +355,17 @@ pub mod pallet { /// General information about dApp staking protocol state. #[pallet::storage] - pub type ActiveProtocolState = - StorageValue<_, ProtocolState>, ValueQuery>; + #[pallet::whitelist_storage] + pub type ActiveProtocolState = StorageValue<_, ProtocolState, ValueQuery>; /// Counter for unique dApp identifiers. #[pallet::storage] pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; /// Map of all dApps integrated into dApp staking protocol. + /// + /// Even though dApp is integrated, it does not mean it's still actively participating in dApp staking. + /// It might have been unregistered at some point in history. #[pallet::storage] pub type IntegratedDApps = CountedStorageMap< Hasher = Blake2_128Concat, @@ -489,11 +497,13 @@ pub mod pallet { // Prepare initial protocol state let protocol_state = ProtocolState { era: 1, - next_era_start: Pallet::::blocks_per_voting_period() + 1_u32.into(), + next_era_start: Pallet::::blocks_per_voting_period() + .checked_add(1) + .expect("Must not overflow - especially not at genesis."), period_info: PeriodInfo { number: 1, subperiod: Subperiod::Voting, - subperiod_end_era: 2, + next_subperiod_start_era: 2, }, maintenance: false, }; @@ -507,158 +517,26 @@ pub mod pallet { } #[pallet::hooks] - impl Hooks> for Pallet { - fn on_initialize(now: BlockNumberFor) -> Weight { - let mut protocol_state = ActiveProtocolState::::get(); - - // We should not modify pallet storage while in maintenance mode. - // This is a safety measure, since maintenance mode is expected to be - // enabled in case some misbehavior or corrupted storage is detected. - if protocol_state.maintenance { - return T::DbWeight::get().reads(1); - } - - // Nothing to do if it's not new era - if !protocol_state.is_new_era(now) { - return T::DbWeight::get().reads(1); - } - - // At this point it's clear that an era change will happen - let mut era_info = CurrentEraInfo::::get(); - - let current_era = protocol_state.era; - let next_era = current_era.saturating_add(1); - let (maybe_period_event, era_reward) = match protocol_state.subperiod() { - // Voting subperiod only lasts for one 'prolonged' era - Subperiod::Voting => { - // For the sake of consistency, we put zero reward into storage. There are no rewards for the voting subperiod. - let era_reward = EraReward { - staker_reward_pool: Balance::zero(), - staked: era_info.total_staked_amount(), - dapp_reward_pool: Balance::zero(), - }; - - let subperiod_end_era = - next_era.saturating_add(T::StandardErasPerBuildAndEarnSubperiod::get()); - let build_and_earn_start_block = - now.saturating_add(T::StandardEraLength::get()); - protocol_state - .advance_to_next_subperiod(subperiod_end_era, build_and_earn_start_block); - - era_info.migrate_to_next_era(Some(protocol_state.subperiod())); - - // Update tier configuration to be used when calculating rewards for the upcoming eras - let next_tier_config = NextTierConfig::::take(); - TierConfig::::put(next_tier_config); - - ( - Some(Event::::NewSubperiod { - subperiod: protocol_state.subperiod(), - number: protocol_state.period_number(), - }), - era_reward, - ) - } - Subperiod::BuildAndEarn => { - let (staker_reward_pool, dapp_reward_pool) = - T::RewardPoolProvider::normal_reward_pools(); - let era_reward = EraReward { - staker_reward_pool, - staked: era_info.total_staked_amount(), - dapp_reward_pool, - }; - - // Distribute dapps into tiers, write it into storage - let dapp_tier_rewards = Self::get_dapp_tier_assignment( - current_era, - protocol_state.period_number(), - dapp_reward_pool, - ); - DAppTiers::::insert(¤t_era, dapp_tier_rewards); - - // Switch to `Voting` period if conditions are met. - if protocol_state.period_info.is_next_period(next_era) { - // Store info about period end - let bonus_reward_pool = T::RewardPoolProvider::bonus_reward_pool(); - PeriodEnd::::insert( - &protocol_state.period_number(), - PeriodEndInfo { - bonus_reward_pool, - total_vp_stake: era_info.staked_amount(Subperiod::Voting), - final_era: current_era, - }, - ); - - // For the sake of consistency we treat the whole `Voting` period as a single era. - // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. - let subperiod_end_era = next_era.saturating_add(1); - let voting_period_length = Self::blocks_per_voting_period(); - let next_era_start_block = now.saturating_add(voting_period_length); - - protocol_state - .advance_to_next_subperiod(subperiod_end_era, next_era_start_block); - - era_info.migrate_to_next_era(Some(protocol_state.subperiod())); - - // Re-calculate tier configuration for the upcoming new period - let tier_params = StaticTierParams::::get(); - let average_price = T::NativePriceProvider::average_price(); - let new_tier_config = - TierConfig::::get().calculate_new(average_price, &tier_params); - NextTierConfig::::put(new_tier_config); - - ( - Some(Event::::NewSubperiod { - subperiod: protocol_state.subperiod(), - number: protocol_state.period_number(), - }), - era_reward, - ) - } else { - let next_era_start_block = now.saturating_add(T::StandardEraLength::get()); - protocol_state.next_era_start = next_era_start_block; - - era_info.migrate_to_next_era(None); - - (None, era_reward) - } - } - }; - - // Update storage items - protocol_state.era = next_era; - ActiveProtocolState::::put(protocol_state); - - CurrentEraInfo::::put(era_info); - - let era_span_index = Self::era_reward_span_index(current_era); - let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); - if let Err(_) = span.push(current_era, era_reward) { - // This must never happen but we log the error just in case. - log::error!( - target: LOG_TARGET, - "Failed to push era {} into the era reward span.", - current_era - ); - } - EraRewards::::insert(&era_span_index, span); - - Self::deposit_event(Event::::NewEra { era: next_era }); - if let Some(period_event) = maybe_period_event { - Self::deposit_event(period_event); - } - - // TODO: benchmark later - T::DbWeight::get().reads_writes(3, 3) + impl Hooks for Pallet { + fn on_initialize(now: BlockNumber) -> Weight { + Self::era_and_period_handler(now, TierAssignment::Real) } } + /// A reason for freezing funds. + #[pallet::composite_enum] + pub enum FreezeReason { + /// Account is participating in dApp staking. + #[codec(index = 0)] + DAppStaking, + } + #[pallet::call] impl Pallet { /// Used to enable or disable maintenance mode. /// Can only be called by manager origin. #[pallet::call_index(0)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::maintenance_mode())] pub fn maintenance_mode(origin: OriginFor, enabled: bool) -> DispatchResult { T::ManagerOrigin::ensure_origin(origin)?; ActiveProtocolState::::mutate(|state| state.maintenance = enabled); @@ -672,7 +550,7 @@ pub mod pallet { /// If successful, smart contract will be assigned a simple, unique numerical identifier. /// Owner is set to be initial beneficiary & manager of the dApp. #[pallet::call_index(1)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::register())] pub fn register( origin: OriginFor, owner: T::AccountId, @@ -723,7 +601,7 @@ pub mod pallet { /// If set to `None`, rewards will be deposited to the dApp owner. /// After this call, all existing & future rewards will be paid out to the beneficiary. #[pallet::call_index(2)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::set_dapp_reward_beneficiary())] pub fn set_dapp_reward_beneficiary( origin: OriginFor, smart_contract: T::SmartContract, @@ -762,7 +640,7 @@ pub mod pallet { /// 1. when the dApp owner account is compromised, manager can change the owner to a new account /// 2. if project wants to transfer ownership to a new account (DAO, multisig, etc.). #[pallet::call_index(3)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::set_dapp_owner())] pub fn set_dapp_owner( origin: OriginFor, smart_contract: T::SmartContract, @@ -802,7 +680,7 @@ pub mod pallet { /// /// Can be called by dApp staking manager origin. #[pallet::call_index(4)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::unregister())] pub fn unregister( origin: OriginFor, smart_contract: T::SmartContract, @@ -840,7 +718,7 @@ pub mod pallet { /// /// Locked amount can immediately be used for staking. #[pallet::call_index(5)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::lock())] pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -849,7 +727,7 @@ pub mod pallet { // Calculate & check amount available for locking let available_balance = - T::Currency::free_balance(&account).saturating_sub(ledger.active_locked_amount()); + T::Currency::balance(&account).saturating_sub(ledger.active_locked_amount()); let amount_to_lock = available_balance.min(amount); ensure!(!amount_to_lock.is_zero(), Error::::ZeroAmount); @@ -860,7 +738,7 @@ pub mod pallet { Error::::LockedAmountBelowThreshold ); - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; CurrentEraInfo::::mutate(|era_info| { era_info.add_locked(amount_to_lock); }); @@ -879,7 +757,7 @@ pub mod pallet { /// If the amount is greater than the available amount for unlocking, everything is unlocked. /// If the remaining locked amount would take the account below the minimum locked amount, everything is unlocked. #[pallet::call_index(6)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::unlock())] pub fn unlock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -917,7 +795,7 @@ pub mod pallet { .map_err(|_| Error::::TooManyUnlockingChunks)?; // Update storage - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; CurrentEraInfo::::mutate(|era_info| { era_info.unlocking_started(amount_to_unlock); }); @@ -932,8 +810,8 @@ pub mod pallet { /// Claims all of fully unlocked chunks, removing the lock from them. #[pallet::call_index(7)] - #[pallet::weight(Weight::zero())] - pub fn claim_unlocked(origin: OriginFor) -> DispatchResult { + #[pallet::weight(T::WeightInfo::claim_unlocked(T::MaxNumberOfStakedContracts::get()))] + pub fn claim_unlocked(origin: OriginFor) -> DispatchResultWithPostInfo { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -944,26 +822,25 @@ pub mod pallet { ensure!(amount > Zero::zero(), Error::::NoUnlockedChunksToClaim); // In case it's full unlock, account is exiting dApp staking, ensure all storage is cleaned up. - // TODO: will be used after benchmarks - let _removed_entries = if ledger.is_empty() { + let removed_entries = if ledger.is_empty() { let _ = StakerInfo::::clear_prefix(&account, ledger.contract_stake_count, None); ledger.contract_stake_count } else { 0 }; - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; CurrentEraInfo::::mutate(|era_info| { era_info.unlocking_removed(amount); }); Self::deposit_event(Event::::ClaimedUnlocked { account, amount }); - Ok(()) + Ok(Some(T::WeightInfo::claim_unlocked(removed_entries)).into()) } #[pallet::call_index(8)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::relock_unlocking())] pub fn relock_unlocking(origin: OriginFor) -> DispatchResult { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -980,7 +857,7 @@ pub mod pallet { Error::::LockedAmountBelowThreshold ); - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; CurrentEraInfo::::mutate(|era_info| { era_info.add_locked(amount); era_info.unlocking_removed(amount); @@ -1000,7 +877,7 @@ pub mod pallet { /// /// Staked amount is only eligible for rewards from the next era onwards. #[pallet::call_index(9)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::stake())] pub fn stake( origin: OriginFor, smart_contract: T::SmartContract, @@ -1013,7 +890,7 @@ pub mod pallet { let dapp_info = IntegratedDApps::::get(&smart_contract).ok_or(Error::::NotOperatedDApp)?; - ensure!(dapp_info.is_active(), Error::::NotOperatedDApp); + ensure!(dapp_info.is_registered(), Error::::NotOperatedDApp); let protocol_state = ActiveProtocolState::::get(); let current_era = protocol_state.era; @@ -1104,7 +981,7 @@ pub mod pallet { // 5. // Update remaining storage entries - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; StakerInfo::::insert(&account, &smart_contract, new_staking_info); ContractStake::::insert(&dapp_info.id, contract_stake_info); @@ -1127,7 +1004,7 @@ pub mod pallet { /// In case amount is unstaked during `Build&Earn` subperiod, first the `build_and_earn` is reduced, /// and any spillover is subtracted from the `voting` amount. #[pallet::call_index(10)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::unstake())] pub fn unstake( origin: OriginFor, smart_contract: T::SmartContract, @@ -1140,7 +1017,7 @@ pub mod pallet { let dapp_info = IntegratedDApps::::get(&smart_contract).ok_or(Error::::NotOperatedDApp)?; - ensure!(dapp_info.is_active(), Error::::NotOperatedDApp); + ensure!(dapp_info.is_registered(), Error::::NotOperatedDApp); let protocol_state = ActiveProtocolState::::get(); let current_era = protocol_state.era; @@ -1183,10 +1060,10 @@ pub mod pallet { ledger .unstake_amount(amount, current_era, protocol_state.period_info) .map_err(|err| match err { - // These are all defensive checks, which should never happen since we already checked them above. AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { Error::::UnclaimedRewards } + // This is a defensive check, which should never happen since we calculate the correct value above. AccountLedgerError::UnstakeAmountLargerThanStake => { Error::::UnstakeAmountTooLarge } @@ -1215,7 +1092,7 @@ pub mod pallet { StakerInfo::::insert(&account, &smart_contract, new_staking_info); } - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; Self::deposit_event(Event::::Unstake { account, @@ -1229,8 +1106,12 @@ pub mod pallet { /// Claims some staker rewards, if user has any. /// In the case of a successfull call, at least one era will be claimed, with the possibility of multiple claims happening. #[pallet::call_index(11)] - #[pallet::weight(Weight::zero())] - pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { + #[pallet::weight({ + let max_span_length = T::EraRewardSpanLength::get(); + T::WeightInfo::claim_staker_rewards_ongoing_period(max_span_length) + .max(T::WeightInfo::claim_staker_rewards_past_period(max_span_length)) + })] + pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResultWithPostInfo { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -1294,13 +1175,12 @@ pub mod pallet { rewards.push((era, staker_reward)); reward_sum.saturating_accrue(staker_reward); } + let rewards_len: u32 = rewards.len().unique_saturated_into(); - // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. - // Account exists since it has locked funds. - T::Currency::deposit_into_existing(&account, reward_sum) - .map_err(|_| Error::::InternalClaimStakerError)?; + T::StakingRewardHandler::payout_reward(&account, reward_sum) + .map_err(|_| Error::::RewardPayoutFailed)?; - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; rewards.into_iter().for_each(|(era, reward)| { Self::deposit_event(Event::::Reward { @@ -1310,12 +1190,17 @@ pub mod pallet { }); }); - Ok(()) + Ok(Some(if period_end.is_some() { + T::WeightInfo::claim_staker_rewards_past_period(rewards_len) + } else { + T::WeightInfo::claim_staker_rewards_ongoing_period(rewards_len) + }) + .into()) } /// Used to claim bonus reward for a smart contract, if eligible. #[pallet::call_index(12)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::claim_bonus_reward())] pub fn claim_bonus_reward( origin: OriginFor, smart_contract: T::SmartContract, @@ -1359,10 +1244,8 @@ pub mod pallet { Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) * period_end_info.bonus_reward_pool; - // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. - // Account exists since it has locked funds. - T::Currency::deposit_into_existing(&account, bonus_reward) - .map_err(|_| Error::::InternalClaimStakerError)?; + T::StakingRewardHandler::payout_reward(&account, bonus_reward) + .map_err(|_| Error::::RewardPayoutFailed)?; // Cleanup entry since the reward has been claimed StakerInfo::::remove(&account, &smart_contract); @@ -1379,7 +1262,7 @@ pub mod pallet { /// Used to claim dApp reward for the specified era. #[pallet::call_index(13)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::claim_dapp_reward())] pub fn claim_dapp_reward( origin: OriginFor, smart_contract: T::SmartContract, @@ -1387,7 +1270,7 @@ pub mod pallet { ) -> DispatchResult { Self::ensure_pallet_enabled()?; - // TODO: Shall we make sure only dApp owner or beneficiary can trigger the claim? + // To keep in line with legacy behavior, dApp rewards can be claimed by anyone. let _ = ensure_signed(origin)?; let dapp_info = @@ -1400,7 +1283,7 @@ pub mod pallet { // 'Consume' dApp reward for the specified era, if possible. let mut dapp_tiers = DAppTiers::::get(&era).ok_or(Error::::NoDAppTierInfo)?; ensure!( - Self::oldest_claimable_period(dapp_tiers.period) <= protocol_state.period_number(), + dapp_tiers.period >= Self::oldest_claimable_period(protocol_state.period_number()), Error::::RewardExpired ); @@ -1415,8 +1298,8 @@ pub mod pallet { // Get reward destination, and deposit the reward. let beneficiary = dapp_info.reward_beneficiary(); - // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. - T::Currency::deposit_creating(beneficiary, amount); + T::StakingRewardHandler::payout_reward(&beneficiary, amount) + .map_err(|_| Error::::RewardPayoutFailed)?; // Write back updated struct to prevent double reward claims DAppTiers::::insert(&era, dapp_tiers); @@ -1435,7 +1318,7 @@ pub mod pallet { /// Used to unstake funds from a contract that was unregistered after an account staked on it. /// This is required if staker wants to re-stake these funds on another active contract during the ongoing period. #[pallet::call_index(14)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::unstake_from_unregistered())] pub fn unstake_from_unregistered( origin: OriginFor, smart_contract: T::SmartContract, @@ -1444,7 +1327,7 @@ pub mod pallet { let account = ensure_signed(origin)?; ensure!( - !Self::is_active(&smart_contract), + !Self::is_registered(&smart_contract), Error::::ContractStillActive ); @@ -1485,11 +1368,8 @@ pub mod pallet { era_info.unstake_amount(amount, protocol_state.subperiod()); }); - // TODO: HOWEVER, we should not pay out bonus rewards for such contracts. - // Seems wrong because it serves as discentive for unstaking & moving over to a new contract. - // Update remaining storage entries - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; StakerInfo::::remove(&account, &smart_contract); Self::deposit_event(Event::::UnstakeFromUnregistered { @@ -1507,8 +1387,10 @@ pub mod pallet { /// 1. It's from a past period & the account wasn't a loyal staker, meaning there's no claimable bonus reward. /// 2. It's from a period older than the oldest claimable period, regardless whether the account was loyal or not. #[pallet::call_index(15)] - #[pallet::weight(Weight::zero())] - pub fn cleanup_expired_entries(origin: OriginFor) -> DispatchResult { + #[pallet::weight(T::WeightInfo::cleanup_expired_entries( + T::MaxNumberOfStakedContracts::get() + ))] + pub fn cleanup_expired_entries(origin: OriginFor) -> DispatchResultWithPostInfo { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -1544,14 +1426,17 @@ pub mod pallet { .contract_stake_count .saturating_reduce(entries_to_delete.unique_saturated_into()); ledger.maybe_cleanup_expired(threshold_period); // Not necessary but we do it for the sake of consistency - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; Self::deposit_event(Event::::ExpiredEntriesRemoved { account, count: entries_to_delete.unique_saturated_into(), }); - Ok(()) + Ok(Some(T::WeightInfo::cleanup_expired_entries( + entries_to_delete.unique_saturated_into(), + )) + .into()) } /// Used to force a change of era or subperiod. @@ -1562,7 +1447,7 @@ pub mod pallet { /// /// Can only be called by manager origin. #[pallet::call_index(16)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::force())] pub fn force(origin: OriginFor, forcing_type: ForcingType) -> DispatchResult { Self::ensure_pallet_enabled()?; T::ManagerOrigin::ensure_origin(origin)?; @@ -1575,7 +1460,7 @@ pub mod pallet { match forcing_type { ForcingType::Era => (), ForcingType::Subperiod => { - state.period_info.subperiod_end_era = state.era.saturating_add(1); + state.period_info.next_subperiod_start_era = state.era.saturating_add(1); } } }); @@ -1609,34 +1494,41 @@ pub mod pallet { Ok(Some(who)) } - /// Update the account ledger, and dApp staking balance lock. + /// Update the account ledger, and dApp staking balance freeze. + /// + /// In case account ledger is empty, entries from the DB are removed and freeze is thawed. /// - /// In case account ledger is empty, entries from the DB are removed and lock is released. - pub(crate) fn update_ledger(account: &T::AccountId, ledger: AccountLedgerFor) { + /// This call can fail if the `freeze` or `thaw` operations fail. This should never happen since + /// runtime definition must ensure it supports necessary freezes. + pub(crate) fn update_ledger( + account: &T::AccountId, + ledger: AccountLedgerFor, + ) -> Result<(), DispatchError> { if ledger.is_empty() { Ledger::::remove(&account); - T::Currency::remove_lock(STAKING_ID, account); + T::Currency::thaw(&FreezeReason::DAppStaking.into(), account)?; } else { - T::Currency::set_lock( - STAKING_ID, + T::Currency::set_freeze( + &FreezeReason::DAppStaking.into(), account, ledger.active_locked_amount(), - WithdrawReasons::all(), - ); + )?; Ledger::::insert(account, ledger); } + + Ok(()) } /// Returns the number of blocks per voting period. - pub(crate) fn blocks_per_voting_period() -> BlockNumberFor { - T::StandardEraLength::get() - .saturating_mul(T::StandardErasPerVotingSubperiod::get().into()) + pub(crate) fn blocks_per_voting_period() -> BlockNumber { + T::CycleConfiguration::blocks_per_era() + .saturating_mul(T::CycleConfiguration::eras_per_voting_subperiod().into()) } - /// `true` if smart contract is active, `false` if it has been unregistered. - pub(crate) fn is_active(smart_contract: &T::SmartContract) -> bool { + /// `true` if smart contract is registered, `false` otherwise. + pub(crate) fn is_registered(smart_contract: &T::SmartContract) -> bool { IntegratedDApps::::get(smart_contract) - .map_or(false, |dapp_info| dapp_info.is_active()) + .map_or(false, |dapp_info| dapp_info.is_registered()) } /// Calculates the `EraRewardSpan` index for the specified era. @@ -1651,8 +1543,8 @@ pub mod pallet { } /// Unlocking period expressed in the number of blocks. - pub(crate) fn unlock_period() -> BlockNumberFor { - T::StandardEraLength::get().saturating_mul(T::UnlockingPeriod::get().into()) + pub(crate) fn unlock_period() -> BlockNumber { + T::CycleConfiguration::blocks_per_era().saturating_mul(T::UnlockingPeriod::get().into()) } /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier. @@ -1668,7 +1560,7 @@ pub mod pallet { /// as well as the threshold for each tier. Threshold is the minimum amount of stake required to be eligible for a tier. /// Iterate over tier thresholds & capacities, starting from the top tier, and assign dApps to them. /// - /// ```ignore + /// ```text //// for each tier: /// for each unassigned dApp: /// if tier has capacity && dApp satisfies the tier threshold: @@ -1676,25 +1568,28 @@ pub mod pallet { /// else: /// exit loop since no more dApps will satisfy the threshold since they are sorted by score /// ``` + /// (Sort the entries by dApp ID, in ascending order. This is so we can efficiently search for them using binary search.) /// - /// 4. Sort the entries by dApp ID, in ascending order. This is so we can efficiently search for them using binary search. - /// - /// 5. Calculate rewards for each tier. + /// 4. Calculate rewards for each tier. /// This is done by dividing the total reward pool into tier reward pools, /// after which the tier reward pool is divided by the number of available slots in the tier. /// /// The returned object contains information about each dApp that made it into a tier. + /// Alongside tier assignment info, number of read DB contract stake entries is returned. pub(crate) fn get_dapp_tier_assignment( era: EraNumber, period: PeriodNumber, dapp_reward_pool: Balance, - ) -> DAppTierRewardsFor { + ) -> (DAppTierRewardsFor, DAppId) { let mut dapp_stakes = Vec::with_capacity(T::MaxNumberOfContracts::get() as usize); // 1. // Iterate over all staked dApps. // This is bounded by max amount of dApps we allow to be registered. + let mut counter = 0; for (dapp_id, stake_amount) in ContractStake::::iter() { + counter.saturating_inc(); + // Skip dApps which don't have ANY amount staked let stake_amount = match stake_amount.get(era, period) { Some(stake_amount) if !stake_amount.total().is_zero() => stake_amount, @@ -1744,13 +1639,12 @@ pub mod pallet { tier_id.saturating_inc(); } - // TODO: what if multiple dApps satisfy the tier entry threshold but there's not enough slots to accomodate them all? - - // 4. - // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is "guaranteed" due to lack of duplicated Ids). - dapp_tiers.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); + // In case when tier has 1 more free slot, but two dApps with exactly same score satisfy the threshold, + // one of them will be assigned to the tier, and the other one will be assigned to the lower tier, if it exists. + // + // There is no explicit definition of which dApp gets the advantage - it's decided by dApp IDs hash & the unstable sort algorithm. - // 5. Calculate rewards. + // 4. Calculate rewards. let tier_rewards = tier_config .reward_portion .iter() @@ -1764,15 +1658,195 @@ pub mod pallet { }) .collect::>(); - // 6. + // 5. // Prepare and return tier & rewards info. // In case rewards creation fails, we just write the default value. This should never happen though. - DAppTierRewards::::new( - dapp_tiers, - tier_rewards, - period, + ( + DAppTierRewards::::new( + dapp_tiers, + tier_rewards, + period, + ) + .unwrap_or_default(), + counter, ) - .unwrap_or_default() + } + + /// Used to handle era & period transitions. + pub(crate) fn era_and_period_handler( + now: BlockNumber, + tier_assignment: TierAssignment, + ) -> Weight { + let mut protocol_state = ActiveProtocolState::::get(); + + // `ActiveProtocolState` is whitelisted, so we need to account for its read. + let mut consumed_weight = T::DbWeight::get().reads(1); + + // We should not modify pallet storage while in maintenance mode. + // This is a safety measure, since maintenance mode is expected to be + // enabled in case some misbehavior or corrupted storage is detected. + if protocol_state.maintenance { + return consumed_weight; + } + + // Nothing to do if it's not new era + if !protocol_state.is_new_era(now) { + return consumed_weight; + } + + // At this point it's clear that an era change will happen + let mut era_info = CurrentEraInfo::::get(); + + let current_era = protocol_state.era; + let next_era = current_era.saturating_add(1); + let (maybe_period_event, era_reward) = match protocol_state.subperiod() { + // Voting subperiod only lasts for one 'prolonged' era + Subperiod::Voting => { + // For the sake of consistency, we put zero reward into storage. There are no rewards for the voting subperiod. + let era_reward = EraReward { + staker_reward_pool: Balance::zero(), + staked: era_info.total_staked_amount(), + dapp_reward_pool: Balance::zero(), + }; + + let next_subperiod_start_era = next_era + .saturating_add(T::CycleConfiguration::eras_per_build_and_earn_subperiod()); + let build_and_earn_start_block = + now.saturating_add(T::CycleConfiguration::blocks_per_era()); + protocol_state.advance_to_next_subperiod( + next_subperiod_start_era, + build_and_earn_start_block, + ); + + era_info.migrate_to_next_era(Some(protocol_state.subperiod())); + + // Update tier configuration to be used when calculating rewards for the upcoming eras + let next_tier_config = NextTierConfig::::take(); + TierConfig::::put(next_tier_config); + + consumed_weight + .saturating_accrue(T::WeightInfo::on_initialize_voting_to_build_and_earn()); + + ( + Some(Event::::NewSubperiod { + subperiod: protocol_state.subperiod(), + number: protocol_state.period_number(), + }), + era_reward, + ) + } + Subperiod::BuildAndEarn => { + let staked = era_info.total_staked_amount(); + let (staker_reward_pool, dapp_reward_pool) = + T::StakingRewardHandler::staker_and_dapp_reward_pools(staked); + let era_reward = EraReward { + staker_reward_pool, + staked, + dapp_reward_pool, + }; + + // Distribute dapps into tiers, write it into storage + // + // To help with benchmarking, it's possible to omit real tier calculation using the `Dummy` approach. + // This must never be used in production code, obviously. + let (dapp_tier_rewards, counter) = match tier_assignment { + TierAssignment::Real => Self::get_dapp_tier_assignment( + current_era, + protocol_state.period_number(), + dapp_reward_pool, + ), + #[cfg(feature = "runtime-benchmarks")] + TierAssignment::Dummy => (DAppTierRewardsFor::::default(), 0), + }; + DAppTiers::::insert(¤t_era, dapp_tier_rewards); + + consumed_weight + .saturating_accrue(T::WeightInfo::dapp_tier_assignment(counter.into())); + + // Switch to `Voting` period if conditions are met. + if protocol_state.period_info.is_next_period(next_era) { + // Store info about period end + let bonus_reward_pool = T::StakingRewardHandler::bonus_reward_pool(); + PeriodEnd::::insert( + &protocol_state.period_number(), + PeriodEndInfo { + bonus_reward_pool, + total_vp_stake: era_info.staked_amount(Subperiod::Voting), + final_era: current_era, + }, + ); + + // For the sake of consistency we treat the whole `Voting` period as a single era. + // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. + let next_subperiod_start_era = next_era.saturating_add(1); + let voting_period_length = Self::blocks_per_voting_period(); + let next_era_start_block = now.saturating_add(voting_period_length); + + protocol_state.advance_to_next_subperiod( + next_subperiod_start_era, + next_era_start_block, + ); + + era_info.migrate_to_next_era(Some(protocol_state.subperiod())); + + // Re-calculate tier configuration for the upcoming new period + let tier_params = StaticTierParams::::get(); + let average_price = T::NativePriceProvider::average_price(); + let new_tier_config = + TierConfig::::get().calculate_new(average_price, &tier_params); + NextTierConfig::::put(new_tier_config); + + consumed_weight.saturating_accrue( + T::WeightInfo::on_initialize_build_and_earn_to_voting(), + ); + + ( + Some(Event::::NewSubperiod { + subperiod: protocol_state.subperiod(), + number: protocol_state.period_number(), + }), + era_reward, + ) + } else { + let next_era_start_block = + now.saturating_add(T::CycleConfiguration::blocks_per_era()); + protocol_state.next_era_start = next_era_start_block; + + era_info.migrate_to_next_era(None); + + consumed_weight.saturating_accrue( + T::WeightInfo::on_initialize_build_and_earn_to_build_and_earn(), + ); + + (None, era_reward) + } + } + }; + + // Update storage items + protocol_state.era = next_era; + ActiveProtocolState::::put(protocol_state); + + CurrentEraInfo::::put(era_info); + + let era_span_index = Self::era_reward_span_index(current_era); + let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); + if let Err(_) = span.push(current_era, era_reward) { + // This must never happen but we log the error just in case. + log::error!( + target: LOG_TARGET, + "Failed to push era {} into the era reward span.", + current_era + ); + } + EraRewards::::insert(&era_span_index, span); + + Self::deposit_event(Event::::NewEra { era: next_era }); + if let Some(period_event) = maybe_period_event { + Self::deposit_event(period_event); + } + + consumed_weight } } } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 73535acd36..050eb112f2 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -24,7 +24,10 @@ use crate::{ use frame_support::{ construct_runtime, parameter_types, - traits::{ConstU128, ConstU32, ConstU64}, + traits::{ + fungible::{Mutate as FunMutate, Unbalanced as FunUnbalanced}, + ConstU128, ConstU32, + }, weights::Weight, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -32,14 +35,14 @@ use sp_arithmetic::fixed_point::FixedU64; use sp_core::H256; use sp_io::TestExternalities; use sp_runtime::{ - testing::Header, traits::{BlakeTwo256, IdentityLookup}, Permill, }; +use sp_std::cell::RefCell; + +use astar_primitives::{testing::Header, Balance, BlockNumber}; pub(crate) type AccountId = u64; -pub(crate) type BlockNumber = u64; -pub(crate) type Balance = u128; pub(crate) const EXISTENTIAL_DEPOSIT: Balance = 2; pub(crate) const MINIMUM_LOCK_AMOUNT: Balance = 10; @@ -61,7 +64,7 @@ construct_runtime!( ); parameter_types! { - pub const BlockHashCount: u64 = 250; + pub const BlockHashCount: BlockNumber = 250; pub BlockWeights: frame_system::limits::BlockWeights = frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); } @@ -103,9 +106,9 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU128; type AccountStore = System; type HoldIdentifier = (); - type FreezeIdentifier = (); + type FreezeIdentifier = RuntimeFreezeReason; type MaxHolds = ConstU32<0>; - type MaxFreezes = ConstU32<0>; + type MaxFreezes = ConstU32<1>; type WeightInfo = (); } @@ -116,17 +119,31 @@ impl PriceProvider for DummyPriceProvider { } } -pub struct DummyRewardPoolProvider; -impl RewardPoolProvider for DummyRewardPoolProvider { - fn normal_reward_pools() -> (Balance, Balance) { +thread_local! { + pub(crate) static DOES_PAYOUT_SUCCEED: RefCell = RefCell::new(false); +} + +pub struct DummyStakingRewardHandler; +impl StakingRewardHandler for DummyStakingRewardHandler { + fn staker_and_dapp_reward_pools(_total_staked_value: Balance) -> (Balance, Balance) { ( Balance::from(1_000_000_000_000_u128), Balance::from(1_000_000_000_u128), ) } + fn bonus_reward_pool() -> Balance { Balance::from(3_000_000_u128) } + + fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()> { + if DOES_PAYOUT_SUCCEED.with(|v| v.borrow().clone()) { + let _ = Balances::mint_into(beneficiary, reward); + Ok(()) + } else { + Err(()) + } + } } #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] @@ -142,24 +159,49 @@ impl Default for MockSmartContract { } #[cfg(feature = "runtime-benchmarks")] -pub struct BenchmarkHelper(sp_std::marker::PhantomData); +pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); #[cfg(feature = "runtime-benchmarks")] -impl crate::BenchmarkHelper for BenchmarkHelper { +impl crate::BenchmarkHelper + for BenchmarkHelper +{ fn get_smart_contract(id: u32) -> MockSmartContract { MockSmartContract::Wasm(id as AccountId) } + + fn set_balance(account: &AccountId, amount: Balance) { + Balances::write_balance(account, amount) + .expect("Must succeed in test/benchmark environment."); + } +} + +pub struct DummyCycleConfiguration; +impl CycleConfiguration for DummyCycleConfiguration { + fn periods_per_cycle() -> u32 { + 4 + } + + fn eras_per_voting_subperiod() -> u32 { + 8 + } + + fn eras_per_build_and_earn_subperiod() -> u32 { + 16 + } + + fn blocks_per_era() -> u32 { + 10 + } } impl pallet_dapp_staking::Config for Test { type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; type Currency = Balances; type SmartContract = MockSmartContract; type ManagerOrigin = frame_system::EnsureRoot; type NativePriceProvider = DummyPriceProvider; - type RewardPoolProvider = DummyRewardPoolProvider; - type StandardEraLength = ConstU64<10>; - type StandardErasPerVotingSubperiod = ConstU32<8>; - type StandardErasPerBuildAndEarnSubperiod = ConstU32<16>; + type StakingRewardHandler = DummyStakingRewardHandler; + type CycleConfiguration = DummyCycleConfiguration; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; type MaxNumberOfContracts = ConstU32<10>; @@ -169,13 +211,17 @@ impl pallet_dapp_staking::Config for Test { type MaxNumberOfStakedContracts = ConstU32<5>; type MinimumStakeAmount = ConstU128<3>; type NumberOfTiers = ConstU32<4>; + type WeightInfo = weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper = BenchmarkHelper; + type BenchmarkHelper = BenchmarkHelper; } pub struct ExtBuilder; impl ExtBuilder { pub fn build() -> TestExternalities { + // Normal behavior is for reward payout to succeed + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = true); + let mut storage = frame_system::GenesisConfig::default() .build_storage::() .unwrap(); @@ -194,12 +240,9 @@ impl ExtBuilder { ext.execute_with(|| { System::set_block_number(1); - // Not sure why the mess with type happens here, but trait specification is needed to compile - let era_length: BlockNumber = - <::StandardEraLength as sp_core::Get<_>>::get(); - let voting_period_length_in_eras: EraNumber = - <::StandardErasPerVotingSubperiod as sp_core::Get<_>>::get( - ); + let era_length = ::CycleConfiguration::blocks_per_era(); + let voting_period_length_in_eras = + ::CycleConfiguration::eras_per_voting_subperiod(); // Init protocol state pallet_dapp_staking::ActiveProtocolState::::put(ProtocolState { @@ -208,7 +251,7 @@ impl ExtBuilder { period_info: PeriodInfo { number: 1, subperiod: Subperiod::Voting, - subperiod_end_era: 2, + next_subperiod_start_era: 2, }, maintenance: false, }); @@ -227,7 +270,6 @@ impl ExtBuilder { era: 2, period: 1, }, - }); // Init tier params @@ -247,9 +289,18 @@ impl ExtBuilder { ]) .unwrap(), tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { amount: 100, minimum_amount: 80 }, - TierThreshold::DynamicTvlAmount { amount: 50, minimum_amount: 40 }, - TierThreshold::DynamicTvlAmount { amount: 20, minimum_amount: 20 }, + TierThreshold::DynamicTvlAmount { + amount: 100, + minimum_amount: 80, + }, + TierThreshold::DynamicTvlAmount { + amount: 50, + minimum_amount: 40, + }, + TierThreshold::DynamicTvlAmount { + amount: 20, + minimum_amount: 20, + }, TierThreshold::FixedTvlAmount { amount: 15 }, ]) .unwrap(), @@ -267,10 +318,8 @@ impl ExtBuilder { pallet_dapp_staking::TierConfig::::put(init_tier_config.clone()); pallet_dapp_staking::NextTierConfig::::put(init_tier_config); - // TODO: include this into every test unless it explicitly doesn't need it. - // DappStaking::on_initialize(System::block_number()); - } - ); + DappStaking::on_initialize(System::block_number()); + }); ext } @@ -278,7 +327,7 @@ impl ExtBuilder { /// Run to the specified block number. /// Function assumes first block has been initialized. -pub(crate) fn run_to_block(n: u64) { +pub(crate) fn run_to_block(n: BlockNumber) { while System::block_number() < n { DappStaking::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); @@ -292,7 +341,7 @@ pub(crate) fn run_to_block(n: u64) { /// Run for the specified number of blocks. /// Function assumes first block has been initialized. -pub(crate) fn run_for_blocks(n: u64) { +pub(crate) fn run_for_blocks(n: BlockNumber) { run_to_block(System::block_number() + n); } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 14b3fa0bad..f20de522f1 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -19,20 +19,25 @@ use crate::test::mock::*; use crate::types::*; use crate::{ - pallet::Config, ActiveProtocolState, BlockNumberFor, ContractStake, CurrentEraInfo, DAppId, - DAppTiers, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, NextTierConfig, PeriodEnd, - PeriodEndInfo, StakerInfo, TierConfig, + pallet::Config, ActiveProtocolState, ContractStake, CurrentEraInfo, DAppId, DAppTiers, + EraRewards, Event, FreezeReason, IntegratedDApps, Ledger, NextDAppId, NextTierConfig, + PeriodEnd, PeriodEndInfo, StakerInfo, TierConfig, }; -use frame_support::{assert_ok, traits::Get}; +use frame_support::{ + assert_ok, + traits::{fungible::InspectFreeze, Get}, +}; use sp_runtime::{traits::Zero, Perbill}; use std::collections::HashMap; +use astar_primitives::{dapp_staking::CycleConfiguration, Balance, BlockNumber}; + /// Helper struct used to store the entire pallet state snapshot. /// Used when comparison of before/after states is required. #[derive(Debug)] pub(crate) struct MemorySnapshot { - active_protocol_state: ProtocolState>, + active_protocol_state: ProtocolState, next_dapp_id: DAppId, current_era_info: EraInfo, integrated_dapps: HashMap< @@ -202,6 +207,8 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) { let free_balance = Balances::free_balance(&account); let locked_balance = pre_snapshot.locked_balance(&account); + let init_frozen_balance = Balances::balance_frozen(&FreezeReason::DAppStaking.into(), &account); + let available_balance = free_balance .checked_sub(locked_balance) .expect("Locked amount cannot be greater than available free balance"); @@ -229,11 +236,17 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) { pre_snapshot.current_era_info.total_locked + expected_lock_amount, "Total locked balance should be increased by the amount locked." ); + + assert_eq!( + init_frozen_balance + expected_lock_amount, + Balances::balance_frozen(&FreezeReason::DAppStaking.into(), &account) + ); } /// Start the unlocking process for locked funds and assert success. pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { let pre_snapshot = MemorySnapshot::new(); + let init_frozen_balance = Balances::balance_frozen(&FreezeReason::DAppStaking.into(), &account); assert!( pre_snapshot.ledger.contains_key(&account), @@ -310,6 +323,11 @@ pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { .saturating_sub(expected_unlock_amount), post_era_info.total_locked ); + + assert_eq!( + init_frozen_balance - expected_unlock_amount, + Balances::balance_frozen(&FreezeReason::DAppStaking.into(), &account) + ); } /// Claims the unlocked funds back into free balance of the user and assert success. @@ -1133,7 +1151,7 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { let is_new_subperiod = pre_snapshot .active_protocol_state .period_info - .subperiod_end_era + .next_subperiod_start_era <= post_snapshot.active_protocol_state.era; // 1. Verify protocol state @@ -1149,15 +1167,15 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { "Voting subperiod only lasts for a single era." ); - let eras_per_bep: EraNumber = - ::StandardErasPerBuildAndEarnSubperiod::get(); + let eras_per_bep = + ::CycleConfiguration::eras_per_build_and_earn_subperiod(); assert_eq!( - post_protoc_state.period_info.subperiod_end_era, + post_protoc_state.period_info.next_subperiod_start_era, post_protoc_state.era + eras_per_bep, "Build&earn must last for the predefined amount of standard eras." ); - let standard_era_length: BlockNumber = ::StandardEraLength::get(); + let standard_era_length = ::CycleConfiguration::blocks_per_era(); assert_eq!( post_protoc_state.next_era_start, current_block_number + standard_era_length, @@ -1177,15 +1195,15 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { "Ending 'Build&Earn' triggers a new period." ); assert_eq!( - post_protoc_state.period_info.subperiod_end_era, + post_protoc_state.period_info.next_subperiod_start_era, post_protoc_state.era + 1, "Voting era must last for a single era." ); - let blocks_per_standard_era: BlockNumber = - ::StandardEraLength::get(); - let eras_per_voting_subperiod: EraNumber = - ::StandardErasPerVotingSubperiod::get(); + let blocks_per_standard_era = + ::CycleConfiguration::blocks_per_era(); + let eras_per_voting_subperiod = + ::CycleConfiguration::eras_per_voting_subperiod(); let eras_per_voting_subperiod: BlockNumber = eras_per_voting_subperiod.into(); let era_length: BlockNumber = blocks_per_standard_era * eras_per_voting_subperiod; assert_eq!( diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 0795bd0653..1a53113ad6 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -18,7 +18,7 @@ use crate::test::{mock::*, testing_utils::*}; use crate::{ - pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, Event, ForcingType, + pallet::Config, ActiveProtocolState, DAppId, EraRewards, Error, Event, ForcingType, IntegratedDApps, Ledger, NextDAppId, PeriodNumber, StakerInfo, Subperiod, TierConfig, }; @@ -29,44 +29,7 @@ use frame_support::{ }; use sp_runtime::traits::Zero; -// TODO: remove this prior to the merge -#[test] -fn print_test() { - ExtBuilder::build().execute_with(|| { - use crate::dsv3_weight::WeightInfo; - println!( - ">>> dApp tier assignment reading & calculation {:?}", - crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(100) - ); - - use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; - use scale_info::TypeInfo; - - #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] - struct RewardSize; - impl Get for RewardSize { - fn get() -> u32 { - 1_00_u32 - } - } - #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] - struct TierSize; - impl Get for TierSize { - fn get() -> u32 { - 4_u32 - } - } - println!( - ">>> Max encoded size for dapp tier rewards: {:?}", - crate::DAppTierRewards::::max_encoded_len() - ); - - println!( - ">>> Max encoded size of ContractStake: {:?}", - crate::ContractStakeAmount::max_encoded_len() - ); - }) -} +use astar_primitives::{dapp_staking::CycleConfiguration, Balance, BlockNumber}; #[test] fn maintenace_mode_works() { @@ -103,6 +66,7 @@ fn maintenace_mode_call_filtering_works() { assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), true)); assert!(ActiveProtocolState::::get().maintenance); + assert_storage_noop!(DappStaking::on_initialize(1)); assert_noop!( DappStaking::register(RuntimeOrigin::root(), 1, MockSmartContract::Wasm(1)), Error::::Disabled @@ -234,9 +198,9 @@ fn on_initialize_base_state_change_works() { assert_eq!(protocol_state.period_number(), 1); // Advance eras just until we reach the next voting period - let eras_per_bep_period: EraNumber = - ::StandardErasPerBuildAndEarnSubperiod::get(); - let blocks_per_era: BlockNumber = ::StandardEraLength::get(); + let eras_per_bep_period = + ::CycleConfiguration::eras_per_build_and_earn_subperiod(); + let blocks_per_era: BlockNumber = ::CycleConfiguration::blocks_per_era(); for era in 2..(2 + eras_per_bep_period - 1) { let pre_block = System::block_number(); advance_to_next_era(); @@ -950,7 +914,7 @@ fn stake_in_final_era_fails() { // Force Build&Earn period ActiveProtocolState::::mutate(|state| { state.period_info.subperiod = Subperiod::BuildAndEarn; - state.period_info.subperiod_end_era = state.era + 1; + state.period_info.next_subperiod_start_era = state.era + 1; }); // Try to stake in the final era of the period, which should fail. @@ -962,7 +926,7 @@ fn stake_in_final_era_fails() { } #[test] -fn stake_fails_if_unclaimed_rewards_from_past_eras_remain() { +fn stake_fails_if_unclaimed_staker_rewards_from_past_remain() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount let smart_contract = MockSmartContract::default(); @@ -970,8 +934,17 @@ fn stake_fails_if_unclaimed_rewards_from_past_eras_remain() { assert_register(1, &smart_contract); assert_lock(account, 300); - // Stake some amount, then force next period + // Stake some amount, then force a few eras assert_stake(account, &smart_contract, 100); + advance_to_era(ActiveProtocolState::::get().era + 2); + + // Stake must fail due to unclaimed rewards + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), + Error::::UnclaimedRewards + ); + + // Should also fail in the next period advance_to_next_period(); assert_noop!( DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), @@ -980,6 +953,31 @@ fn stake_fails_if_unclaimed_rewards_from_past_eras_remain() { }) } +#[test] +fn stake_fails_if_claimable_bonus_rewards_from_past_remain() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + let account = 2; + assert_register(1, &smart_contract); + assert_lock(account, 300); + assert_stake(account, &smart_contract, 100); + + // Advance to next period, claim all staker rewards + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + + // Try to stake again on the same contract, expect an error due to unclaimed bonus rewards + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), + Error::::UnclaimedRewards + ); + }) +} + #[test] fn stake_fails_if_not_enough_stakeable_funds_available() { ExtBuilder::build().execute_with(|| { @@ -1413,7 +1411,7 @@ fn claim_staker_rewards_after_expiry_fails() { advance_to_era( ActiveProtocolState::::get() .period_info - .subperiod_end_era + .next_subperiod_start_era - 1, ); assert_claim_staker_rewards(account); @@ -1438,6 +1436,34 @@ fn claim_staker_rewards_after_expiry_fails() { }) } +#[test] +fn claim_staker_rewards_fails_due_to_payout_failure() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance into Build&Earn period, and allow one era to pass. + advance_to_era(ActiveProtocolState::::get().era + 2); + + // Disable successfull reward payout + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = false); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::RewardPayoutFailed, + ); + + // Re-enable it again, claim should work again + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = true); + assert_claim_staker_rewards(account); + }) +} + #[test] fn claim_bonus_reward_works() { ExtBuilder::build().execute_with(|| { @@ -1592,6 +1618,34 @@ fn claim_bonus_reward_after_expiry_fails() { }) } +#[test] +fn claim_bonus_reward_fails_due_to_payout_failure() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance to next period so we can claim bonus reward + advance_to_next_period(); + + // Disable successfull reward payout + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = false); + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), + Error::::RewardPayoutFailed, + ); + + // Re-enable it again, claim should work again + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = true); + assert_claim_bonus_reward(account, &smart_contract); + }) +} + #[test] fn claim_dapp_reward_works() { ExtBuilder::build().execute_with(|| { @@ -1733,6 +1787,72 @@ fn claim_dapp_reward_twice_for_same_era_fails() { }) } +#[test] +fn claim_dapp_reward_for_expired_era_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + + // Advance to period before the rewards expire. Claim reward must still work. + advance_to_period( + ActiveProtocolState::::get().period_number() + reward_retention_in_periods, + ); + assert_claim_dapp_reward(account, &smart_contract, 2); + + // Advance to the next era, expiring some rewards. + advance_to_next_period(); + assert_noop!( + DappStaking::claim_dapp_reward(RuntimeOrigin::signed(account), smart_contract, 3), + Error::::RewardExpired, + ); + }) +} + +#[test] +fn claim_dapp_reward_fails_due_to_payout_failure() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance 2 eras so we have an entry for reward claiming + advance_to_era(ActiveProtocolState::::get().era + 2); + + // Disable successfull reward payout + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = false); + assert_noop!( + DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(account), + smart_contract, + ActiveProtocolState::::get().era - 1 + ), + Error::::RewardPayoutFailed, + ); + + // Re-enable it again, claim should work again + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = true); + assert_claim_dapp_reward( + account, + &smart_contract, + ActiveProtocolState::::get().era - 1, + ); + }) +} + #[test] fn unstake_from_unregistered_is_ok() { ExtBuilder::build().execute_with(|| { @@ -1924,8 +2044,8 @@ fn force_era_works() { System::block_number() + 1, ); assert_eq!( - ActiveProtocolState::::get().period_end_era(), - init_state.period_end_era(), + ActiveProtocolState::::get().next_subperiod_start_era(), + init_state.next_subperiod_start_era(), ); // Go to the next block, and ensure new era is started @@ -1946,7 +2066,7 @@ fn force_era_works() { init_state.next_era_start > System::block_number() + 1, "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." ); - assert!(init_state.period_end_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); + assert!(init_state.next_subperiod_start_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Era)); System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { forcing_type: ForcingType::Era, @@ -1958,8 +2078,8 @@ fn force_era_works() { System::block_number() + 1, ); assert_eq!( - ActiveProtocolState::::get().period_end_era(), - init_state.period_end_era(), + ActiveProtocolState::::get().next_subperiod_start_era(), + init_state.next_subperiod_start_era(), "Only era is bumped, but we don't expect to switch over to the next subperiod." ); @@ -2002,7 +2122,7 @@ fn force_subperiod_works() { System::block_number() + 1, ); assert_eq!( - ActiveProtocolState::::get().period_end_era(), + ActiveProtocolState::::get().next_subperiod_start_era(), init_state.era + 1, "The switch to the next subperiod must happen in the next era." ); @@ -2027,7 +2147,7 @@ fn force_subperiod_works() { init_state.next_era_start > System::block_number() + 1, "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." ); - assert!(init_state.period_end_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); + assert!(init_state.next_subperiod_start_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Subperiod)); System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { forcing_type: ForcingType::Subperiod, @@ -2039,7 +2159,7 @@ fn force_subperiod_works() { System::block_number() + 1, ); assert_eq!( - ActiveProtocolState::::get().period_end_era(), + ActiveProtocolState::::get().next_subperiod_start_era(), init_state.era + 1, "The switch to the next subperiod must happen in the next era." ); @@ -2147,7 +2267,7 @@ fn get_dapp_tier_assignment_basic_example_works() { // Finally, the actual test let protocol_state = ActiveProtocolState::::get(); let dapp_reward_pool = 1000000; - let tier_assignment = DappStaking::get_dapp_tier_assignment( + let (tier_assignment, counter) = DappStaking::get_dapp_tier_assignment( protocol_state.era + 1, protocol_state.period_number(), dapp_reward_pool, @@ -2162,6 +2282,7 @@ fn get_dapp_tier_assignment_basic_example_works() { number_of_smart_contracts as usize - 1, "One contract doesn't make it into any tier." ); + assert_eq!(counter, number_of_smart_contracts); // 1st tier checks let (entry_1, entry_2) = (tier_assignment.dapps[0], tier_assignment.dapps[1]); @@ -2224,7 +2345,7 @@ fn get_dapp_tier_assignment_zero_slots_per_tier_works() { // Calculate tier assignment (we don't need dApps for this test) let protocol_state = ActiveProtocolState::::get(); let dapp_reward_pool = 1000000; - let tier_assignment = DappStaking::get_dapp_tier_assignment( + let (tier_assignment, counter) = DappStaking::get_dapp_tier_assignment( protocol_state.era, protocol_state.period_number(), dapp_reward_pool, @@ -2235,6 +2356,7 @@ fn get_dapp_tier_assignment_zero_slots_per_tier_works() { assert_eq!(tier_assignment.period, protocol_state.period_number()); assert_eq!(tier_assignment.rewards.len(), number_of_tiers as usize); assert!(tier_assignment.dapps.is_empty()); + assert!(counter.is_zero()); assert!( tier_assignment.rewards[0].is_zero(), diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 50074ec7a6..aadfbda6d1 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use astar_primitives::{Balance, BlockNumber}; +use astar_primitives::Balance; use frame_support::assert_ok; use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::Permill; @@ -45,26 +45,26 @@ fn subperiod_sanity_check() { #[test] fn period_info_basic_checks() { let period_number = 2; - let subperiod_end_era = 5; + let next_subperiod_start_era = 5; let info = PeriodInfo { number: period_number, subperiod: Subperiod::Voting, - subperiod_end_era: subperiod_end_era, + next_subperiod_start_era: next_subperiod_start_era, }; // Sanity checks assert_eq!(info.number, period_number); assert_eq!(info.subperiod, Subperiod::Voting); - assert_eq!(info.subperiod_end_era, subperiod_end_era); + assert_eq!(info.next_subperiod_start_era, next_subperiod_start_era); // Voting period checks - assert!(!info.is_next_period(subperiod_end_era - 1)); - assert!(!info.is_next_period(subperiod_end_era)); - assert!(!info.is_next_period(subperiod_end_era + 1)); + assert!(!info.is_next_period(next_subperiod_start_era - 1)); + assert!(!info.is_next_period(next_subperiod_start_era)); + assert!(!info.is_next_period(next_subperiod_start_era + 1)); for era in vec![ - subperiod_end_era - 1, - subperiod_end_era, - subperiod_end_era + 1, + next_subperiod_start_era - 1, + next_subperiod_start_era, + next_subperiod_start_era + 1, ] { assert!( !info.is_next_period(era), @@ -76,16 +76,16 @@ fn period_info_basic_checks() { let info = PeriodInfo { number: period_number, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: subperiod_end_era, + next_subperiod_start_era: next_subperiod_start_era, }; - assert!(!info.is_next_period(subperiod_end_era - 1)); - assert!(info.is_next_period(subperiod_end_era)); - assert!(info.is_next_period(subperiod_end_era + 1)); + assert!(!info.is_next_period(next_subperiod_start_era - 1)); + assert!(info.is_next_period(next_subperiod_start_era)); + assert!(info.is_next_period(next_subperiod_start_era + 1)); } #[test] fn protocol_state_default() { - let protocol_state = ProtocolState::::default(); + let protocol_state = ProtocolState::default(); assert_eq!(protocol_state.era, 0); assert_eq!( @@ -96,14 +96,14 @@ fn protocol_state_default() { #[test] fn protocol_state_basic_checks() { - let mut protocol_state = ProtocolState::::default(); + let mut protocol_state = ProtocolState::default(); let period_number = 5; - let subperiod_end_era = 11; + let next_subperiod_start_era = 11; let next_era_start = 31; protocol_state.period_info = PeriodInfo { number: period_number, subperiod: Subperiod::Voting, - subperiod_end_era: subperiod_end_era, + next_subperiod_start_era: next_subperiod_start_era, }; protocol_state.next_era_start = next_era_start; @@ -116,30 +116,36 @@ fn protocol_state_basic_checks() { assert!(protocol_state.is_new_era(next_era_start + 1)); // Toggle new period type check - 'Voting' to 'BuildAndEarn' - let subperiod_end_era_1 = 23; + let next_subperiod_start_era_1 = 23; let next_era_start_1 = 41; - protocol_state.advance_to_next_subperiod(subperiod_end_era_1, next_era_start_1); + protocol_state.advance_to_next_subperiod(next_subperiod_start_era_1, next_era_start_1); assert_eq!(protocol_state.subperiod(), Subperiod::BuildAndEarn); assert_eq!( protocol_state.period_number(), period_number, "Switching from 'Voting' to 'BuildAndEarn' should not trigger period bump." ); - assert_eq!(protocol_state.period_end_era(), subperiod_end_era_1); + assert_eq!( + protocol_state.next_subperiod_start_era(), + next_subperiod_start_era_1 + ); assert!(!protocol_state.is_new_era(next_era_start_1 - 1)); assert!(protocol_state.is_new_era(next_era_start_1)); // Toggle from 'BuildAndEarn' over to 'Voting' - let subperiod_end_era_2 = 24; + let next_subperiod_start_era_2 = 24; let next_era_start_2 = 91; - protocol_state.advance_to_next_subperiod(subperiod_end_era_2, next_era_start_2); + protocol_state.advance_to_next_subperiod(next_subperiod_start_era_2, next_era_start_2); assert_eq!(protocol_state.subperiod(), Subperiod::Voting); assert_eq!( protocol_state.period_number(), period_number + 1, "Switching from 'BuildAndEarn' to 'Voting' must trigger period bump." ); - assert_eq!(protocol_state.period_end_era(), subperiod_end_era_2); + assert_eq!( + protocol_state.next_subperiod_start_era(), + next_subperiod_start_era_2 + ); assert!(protocol_state.is_new_era(next_era_start_2)); } @@ -163,17 +169,17 @@ fn dapp_info_basic_checks() { dapp_info.reward_destination = Some(beneficiary); assert_eq!(*dapp_info.reward_beneficiary(), beneficiary); - // Check if dApp is active - assert!(dapp_info.is_active()); + // Check if dApp is registered + assert!(dapp_info.is_registered()); dapp_info.state = DAppState::Unregistered(10); - assert!(!dapp_info.is_active()); + assert!(!dapp_info.is_registered()); } #[test] fn unlocking_chunk_basic_check() { // Sanity check - let unlocking_chunk = UnlockingChunk::::default(); + let unlocking_chunk = UnlockingChunk::default(); assert!(unlocking_chunk.amount.is_zero()); assert!(unlocking_chunk.unlock_block.is_zero()); } @@ -181,7 +187,7 @@ fn unlocking_chunk_basic_check() { #[test] fn account_ledger_default() { get_u32_type!(UnlockingDummy, 5); - let acc_ledger = AccountLedger::::default(); + let acc_ledger = AccountLedger::::default(); assert!(acc_ledger.is_empty()); assert!(acc_ledger.active_locked_amount().is_zero()); @@ -190,7 +196,7 @@ fn account_ledger_default() { #[test] fn account_ledger_add_lock_amount_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // First step, sanity checks assert!(acc_ledger.active_locked_amount().is_zero()); @@ -209,7 +215,7 @@ fn account_ledger_add_lock_amount_works() { #[test] fn account_ledger_subtract_lock_amount_basic_usage_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop @@ -250,10 +256,10 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { #[test] fn account_ledger_add_unlocking_chunk_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Base sanity check - let default_unlocking_chunk = UnlockingChunk::::default(); + let default_unlocking_chunk = UnlockingChunk::default(); assert!(default_unlocking_chunk.amount.is_zero()); assert!(default_unlocking_chunk.unlock_block.is_zero()); @@ -318,7 +324,7 @@ fn account_ledger_add_unlocking_chunk_works() { #[test] fn account_ledger_staked_amount_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check assert!(acc_ledger.staked_amount(0).is_zero()); @@ -355,7 +361,7 @@ fn account_ledger_staked_amount_works() { #[test] fn account_ledger_staked_amount_for_type_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // 1st scenario - 'current' entry is set, 'future' is None let (voting_1, build_and_earn_1, period) = (31, 43, 2); @@ -416,7 +422,7 @@ fn account_ledger_staked_amount_for_type_works() { #[test] fn account_ledger_stakeable_amount_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check for empty ledger assert!(acc_ledger.stakeable_amount(1).is_zero()); @@ -458,7 +464,7 @@ fn account_ledger_stakeable_amount_works() { #[test] fn account_ledger_staked_era_period_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); let (era_1, period) = (10, 2); let stake_amount_1 = StakeAmount { @@ -504,7 +510,7 @@ fn account_ledger_staked_era_period_works() { #[test] fn account_ledger_add_stake_amount_basic_example_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check let period_number = 2; @@ -515,7 +521,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { PeriodInfo { number: period_number, subperiod: Subperiod::Voting, - subperiod_end_era: 0 + next_subperiod_start_era: 0 } ) .is_ok()); @@ -528,7 +534,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; let lock_amount = 17; let stake_amount = 11; @@ -565,7 +571,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { let period_info_2 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; let era_2 = era_1 + 1; assert!(acc_ledger.add_stake_amount(1, era_2, period_info_2).is_ok()); @@ -584,7 +590,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { #[test] fn account_ledger_add_stake_amount_advanced_example_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // 1st scenario - stake some amount, and ensure values are as expected. let era_1 = 1; @@ -592,7 +598,7 @@ fn account_ledger_add_stake_amount_advanced_example_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; let lock_amount = 17; let stake_amount_1 = 11; @@ -636,7 +642,7 @@ fn account_ledger_add_stake_amount_advanced_example_works() { #[test] fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let era_1 = 5; @@ -644,7 +650,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; let lock_amount = 13; let stake_amount = 7; @@ -667,7 +673,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - subperiod_end_era: 100 + next_subperiod_start_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -693,7 +699,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - subperiod_end_era: 100 + next_subperiod_start_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -703,7 +709,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { #[test] fn account_ledger_add_stake_amount_too_large_amount_fails() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check assert_eq!( @@ -713,7 +719,7 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { PeriodInfo { number: 1, subperiod: Subperiod::Voting, - subperiod_end_era: 100 + next_subperiod_start_era: 100 } ), Err(AccountLedgerError::UnavailableStakeFunds) @@ -725,7 +731,7 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; let lock_amount = 13; acc_ledger.add_lock_amount(lock_amount); @@ -747,7 +753,7 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { #[test] fn account_ledger_unstake_amount_basic_scenario_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let amount_1 = 19; @@ -756,7 +762,7 @@ fn account_ledger_unstake_amount_basic_scenario_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; acc_ledger.add_lock_amount(amount_1); @@ -803,7 +809,7 @@ fn account_ledger_unstake_amount_basic_scenario_works() { #[test] fn account_ledger_unstake_amount_advanced_scenario_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let amount_1 = 19; @@ -812,7 +818,7 @@ fn account_ledger_unstake_amount_advanced_scenario_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; acc_ledger.add_lock_amount(amount_1); @@ -885,7 +891,7 @@ fn account_ledger_unstake_amount_advanced_scenario_works() { #[test] fn account_ledger_unstake_from_invalid_era_fails() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let amount_1 = 13; @@ -894,7 +900,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; acc_ledger.add_lock_amount(amount_1); assert!(acc_ledger @@ -921,7 +927,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - subperiod_end_era: 100 + next_subperiod_start_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -947,7 +953,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - subperiod_end_era: 100 + next_subperiod_start_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -957,7 +963,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { #[test] fn account_ledger_unstake_too_much_fails() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let amount_1 = 23; @@ -966,7 +972,7 @@ fn account_ledger_unstake_too_much_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; acc_ledger.add_lock_amount(amount_1); assert!(acc_ledger @@ -982,7 +988,7 @@ fn account_ledger_unstake_too_much_fails() { #[test] fn account_ledger_unlockable_amount_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario assert!(acc_ledger.unlockable_amount(0).is_zero()); @@ -999,7 +1005,7 @@ fn account_ledger_unlockable_amount_works() { let period_info = PeriodInfo { number: stake_period, subperiod: Subperiod::Voting, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; assert!(acc_ledger .add_stake_amount(stake_amount, lock_era, period_info) @@ -1023,7 +1029,7 @@ fn account_ledger_unlockable_amount_works() { #[test] fn account_ledger_claim_unlocked_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario assert!(acc_ledger.claim_unlocked(0).is_zero()); @@ -1057,7 +1063,7 @@ fn account_ledger_claim_unlocked_works() { #[test] fn account_ledger_consume_unlocking_chunks_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario assert!(acc_ledger.consume_unlocking_chunks().is_zero()); @@ -1076,7 +1082,7 @@ fn account_ledger_expired_cleanup_works() { get_u32_type!(UnlockingDummy, 5); // 1st scenario - nothing is expired - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 3, build_and_earn: 7, @@ -1116,7 +1122,7 @@ fn account_ledger_claim_up_to_era_only_staked_without_cleanup_works() { let stake_era = 100; let acc_ledger_snapshot = { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 3, build_and_earn: 7, @@ -1189,7 +1195,7 @@ fn account_ledger_claim_up_to_era_only_staked_with_cleanup_works() { let stake_era = 100; let acc_ledger_snapshot = { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 3, build_and_earn: 7, @@ -1284,7 +1290,7 @@ fn account_ledger_claim_up_to_era_only_staked_future_without_cleanup_works() { let stake_era = 50; let acc_ledger_snapshot = { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked_future = Some(StakeAmount { voting: 5, build_and_earn: 11, @@ -1363,7 +1369,7 @@ fn account_ledger_claim_up_to_era_only_staked_future_with_cleanup_works() { let stake_era = 50; let acc_ledger_snapshot = { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked_future = Some(StakeAmount { voting: 2, build_and_earn: 17, @@ -1468,7 +1474,7 @@ fn account_ledger_claim_up_to_era_staked_and_staked_future_works() { let stake_era_2 = stake_era_1 + 1; let acc_ledger_snapshot = { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 3, build_and_earn: 7, @@ -1561,7 +1567,7 @@ fn account_ledger_claim_up_to_era_fails_for_historic_eras() { // Only staked entry { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 2, build_and_earn: 17, @@ -1576,7 +1582,7 @@ fn account_ledger_claim_up_to_era_fails_for_historic_eras() { // Only staked-future entry { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked_future = Some(StakeAmount { voting: 2, build_and_earn: 17, @@ -1591,7 +1597,7 @@ fn account_ledger_claim_up_to_era_fails_for_historic_eras() { // Both staked and staked-future entries { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 2, build_and_earn: 17, @@ -2240,7 +2246,7 @@ fn contract_stake_amount_stake_is_ok() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - subperiod_end_era: 20, + next_subperiod_start_era: 20, }; let amount_1 = 31; contract_stake.stake(amount_1, period_info_1, era_1); @@ -2268,7 +2274,7 @@ fn contract_stake_amount_stake_is_ok() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 20, + next_subperiod_start_era: 20, }; contract_stake.stake(amount_1, period_info_1, era_1); let entry_1_2 = contract_stake.get(stake_era_1, period_1).unwrap(); @@ -2310,7 +2316,7 @@ fn contract_stake_amount_stake_is_ok() { let period_info_2 = PeriodInfo { number: period_2, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 20, + next_subperiod_start_era: 20, }; let amount_3 = 41; @@ -2365,7 +2371,7 @@ fn contract_stake_amount_unstake_is_ok() { let period_info = PeriodInfo { number: period, subperiod: Subperiod::Voting, - subperiod_end_era: 20, + next_subperiod_start_era: 20, }; let stake_amount = 100; contract_stake.stake(stake_amount, period_info, era_1); @@ -2388,7 +2394,7 @@ fn contract_stake_amount_unstake_is_ok() { let period_info = PeriodInfo { number: period, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 40, + next_subperiod_start_era: 40, }; let era_2 = era_1 + 1; diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index b629a82774..3b18c7d7ac 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -65,21 +65,20 @@ //! use frame_support::{pallet_prelude::*, BoundedVec}; -use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, CheckedAdd, UniqueSaturatedInto, Zero}, + traits::{CheckedAdd, UniqueSaturatedInto, Zero}, FixedPointNumber, Permill, Saturating, }; pub use sp_std::{fmt::Debug, vec::Vec}; -use astar_primitives::Balance; +use astar_primitives::{Balance, BlockNumber}; use crate::pallet::Config; // Convenience type for `AccountLedger` usage. -pub type AccountLedgerFor = AccountLedger, ::MaxUnlockingChunks>; +pub type AccountLedgerFor = AccountLedger<::MaxUnlockingChunks>; // Convenience type for `DAppTierRewards` usage. pub type DAppTierRewardsFor = @@ -142,16 +141,16 @@ pub struct PeriodInfo { pub number: PeriodNumber, /// Subperiod type. pub subperiod: Subperiod, - /// Last era of the subperiod, after this a new subperiod should start. + /// Era in which the new subperiod starts. #[codec(compact)] - pub subperiod_end_era: EraNumber, + pub next_subperiod_start_era: EraNumber, } impl PeriodInfo { /// `true` if the provided era belongs to the next period, `false` otherwise. /// It's only possible to provide this information correctly for the ongoing `BuildAndEarn` subperiod. pub fn is_next_period(&self, era: EraNumber) -> bool { - self.subperiod == Subperiod::BuildAndEarn && self.subperiod_end_era <= era + self.subperiod == Subperiod::BuildAndEarn && self.next_subperiod_start_era <= era } } @@ -180,7 +179,7 @@ pub enum ForcingType { /// General information & state of the dApp staking protocol. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct ProtocolState { +pub struct ProtocolState { /// Ongoing era number. #[codec(compact)] pub era: EraNumber, @@ -193,28 +192,22 @@ pub struct ProtocolState { pub maintenance: bool, } -impl Default for ProtocolState -where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, -{ +impl Default for ProtocolState { fn default() -> Self { Self { era: 0, - next_era_start: BlockNumber::from(1_u32), + next_era_start: 1, period_info: PeriodInfo { number: 0, subperiod: Subperiod::Voting, - subperiod_end_era: 2, + next_subperiod_start_era: 2, }, maintenance: false, } } } -impl ProtocolState -where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, -{ +impl ProtocolState { /// Current subperiod. pub fn subperiod(&self) -> Subperiod { self.period_info.subperiod @@ -226,8 +219,8 @@ where } /// Ending era of current period - pub fn period_end_era(&self) -> EraNumber { - self.period_info.subperiod_end_era + pub fn next_subperiod_start_era(&self) -> EraNumber { + self.period_info.next_subperiod_start_era } /// Checks whether a new era should be triggered, based on the provided _current_ block number argument @@ -239,7 +232,7 @@ where /// Triggers the next subperiod, updating appropriate parameters. pub fn advance_to_next_subperiod( &mut self, - subperiod_end_era: EraNumber, + next_subperiod_start_era: EraNumber, next_era_start: BlockNumber, ) { let period_number = if self.subperiod() == Subperiod::BuildAndEarn { @@ -251,7 +244,7 @@ where self.period_info = PeriodInfo { number: period_number, subperiod: self.subperiod().next(), - subperiod_end_era, + next_subperiod_start_era, }; self.next_era_start = next_era_start; } @@ -291,15 +284,15 @@ impl DAppInfo { } } - /// `true` if dApp is still active (registered), `false` otherwise. - pub fn is_active(&self) -> bool { + /// `true` if dApp is registered, `false` otherwise. + pub fn is_registered(&self) -> bool { self.state == DAppState::Registered } } /// How much was unlocked in some block. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct UnlockingChunk { +pub struct UnlockingChunk { /// Amount undergoing the unlocking period. #[codec(compact)] pub amount: Balance, @@ -308,10 +301,7 @@ pub struct UnlockingChunk Default for UnlockingChunk -where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, -{ +impl Default for UnlockingChunk { fn default() -> Self { Self { amount: Balance::zero(), @@ -332,15 +322,12 @@ where TypeInfo, )] #[scale_info(skip_type_params(UnlockingLen))] -pub struct AccountLedger< - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, - UnlockingLen: Get, -> { +pub struct AccountLedger> { /// How much active locked amount an account has. This can be used for staking. #[codec(compact)] pub locked: Balance, /// Vector of all the unlocking chunks. This is also considered _locked_ but cannot be used for staking. - pub unlocking: BoundedVec, UnlockingLen>, + pub unlocking: BoundedVec, /// Primary field used to store how much was staked in a particular era. pub staked: StakeAmount, /// Secondary field used to store 'stake' information for the 'next era'. @@ -354,9 +341,8 @@ pub struct AccountLedger< pub contract_stake_count: u32, } -impl Default for AccountLedger +impl Default for AccountLedger where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, { fn default() -> Self { @@ -370,9 +356,8 @@ where } } -impl AccountLedger +impl AccountLedger where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, { /// Empty if no locked/unlocking/staked info exists. @@ -1367,9 +1352,6 @@ impl TierThreshold { Self::DynamicTvlAmount { amount, .. } => *amount, } } - - // TODO: maybe add a check that compares `Self` to another threshold and ensures it has lower requirements? - // Could be useful to have this check as a sanity check when params are configured. } /// Top level description of tier slot parameters used to calculate tier configuration. @@ -1500,7 +1482,8 @@ impl> TiersConfiguration { /// Calculate new `TiersConfiguration`, based on the old settings, current native currency price and tier configuration. pub fn calculate_new(&self, native_price: FixedU64, params: &TierParameters) -> Self { - let new_number_of_slots = Self::calculate_number_of_slots(native_price); + // It must always be at least 1 slot. + let new_number_of_slots = Self::calculate_number_of_slots(native_price).max(1); // Calculate how much each tier gets slots. let new_slots_per_tier: Vec = params @@ -1652,7 +1635,9 @@ impl, NT: Get> DAppTierRewards { rewards: Vec, period: PeriodNumber, ) -> Result { - // TODO: should this part of the code ensure that dapps are sorted by Id? + // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is "guaranteed" due to lack of duplicated Ids). + let mut dapps = dapps; + dapps.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); let dapps = BoundedVec::try_from(dapps).map_err(|_| ())?; let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?; @@ -1722,21 +1707,3 @@ pub trait PriceProvider { /// Get the price of the native token. fn average_price() -> FixedU64; } - -// TODO: however the implementation ends up looking, -// it should consider total staked amount when filling up the bonus pool. -// This is to ensure bonus rewards aren't too large in case there is little amount of staked funds. -pub trait RewardPoolProvider { - /// Get the reward pools for stakers and dApps. - /// - /// TODO: discussion about below - /// The assumption is that the underlying implementation keeps track of how often this is called. - /// E.g. let's assume it's supposed to be called at the end of each era. - /// In case era is forced, it will last shorter. If pallet is put into maintenance mode, era might last longer. - /// Reward should adjust to that accordingly. - /// Alternative is to provide number of blocks for which era lasted. - fn normal_reward_pools() -> (Balance, Balance); - - /// Get the bonus pool for stakers. - fn bonus_reward_pool() -> Balance; -} diff --git a/pallets/dapp-staking-v3/src/weights.rs b/pallets/dapp-staking-v3/src/weights.rs new file mode 100644 index 0000000000..d8891c7115 --- /dev/null +++ b/pallets/dapp-staking-v3/src/weights.rs @@ -0,0 +1,809 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for pallet_dapp_staking_v3 +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-12-01, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Dinos-MacBook-Pro.local`, CPU: `` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_dapp_staking_v3 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=weights.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_dapp_staking_v3. +pub trait WeightInfo { + fn maintenance_mode() -> Weight; + fn register() -> Weight; + fn set_dapp_reward_beneficiary() -> Weight; + fn set_dapp_owner() -> Weight; + fn unregister() -> Weight; + fn lock() -> Weight; + fn unlock() -> Weight; + fn claim_unlocked(x: u32, ) -> Weight; + fn relock_unlocking() -> Weight; + fn stake() -> Weight; + fn unstake() -> Weight; + fn claim_staker_rewards_past_period(x: u32, ) -> Weight; + fn claim_staker_rewards_ongoing_period(x: u32, ) -> Weight; + fn claim_bonus_reward() -> Weight; + fn claim_dapp_reward() -> Weight; + fn unstake_from_unregistered() -> Weight; + fn cleanup_expired_entries(x: u32, ) -> Weight; + fn force() -> Weight; + fn on_initialize_voting_to_build_and_earn() -> Weight; + fn on_initialize_build_and_earn_to_voting() -> Weight; + fn on_initialize_build_and_earn_to_build_and_earn() -> Weight; + fn dapp_tier_assignment(x: u32, ) -> Weight; +} + +/// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn maintenance_mode() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:1) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking NextDAppId (r:1 w:1) + /// Proof: DappStaking NextDAppId (max_values: Some(1), max_size: Some(2), added: 497, mode: MaxEncodedLen) + fn register() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3093` + // Minimum execution time: 16_000_000 picoseconds. + Weight::from_parts(18_000_000, 3093) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + fn set_dapp_reward_beneficiary() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 13_000_000 picoseconds. + Weight::from_parts(13_000_000, 3093) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + fn set_dapp_owner() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 12_000_000 picoseconds. + Weight::from_parts(14_000_000, 3093) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:0 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + fn unregister() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(19_000_000, 3093) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn lock() -> Weight { + // Proof Size summary in bytes: + // Measured: `12` + // Estimated: `4764` + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(43_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn unlock() -> Weight { + // Proof Size summary in bytes: + // Measured: `163` + // Estimated: `4764` + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(43_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 3]`. + fn claim_unlocked(_x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `160 + x * (17 ±0)` + // Estimated: `4764` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(40_955_543, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn relock_unlocking() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `4764` + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(40_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:1 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn stake() -> Weight { + // Proof Size summary in bytes: + // Measured: `258` + // Estimated: `4764` + // Minimum execution time: 45_000_000 picoseconds. + Weight::from_parts(49_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:1 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn unstake() -> Weight { + // Proof Size summary in bytes: + // Measured: `437` + // Estimated: `4764` + // Minimum execution time: 48_000_000 picoseconds. + Weight::from_parts(54_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:0) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:0) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 8]`. + fn claim_staker_rewards_past_period(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `485 + x * (8 ±0)` + // Estimated: `4764` + // Minimum execution time: 50_000_000 picoseconds. + Weight::from_parts(51_664_058, 4764) + // Standard Error: 15_971 + .saturating_add(Weight::from_parts(4_243_613, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:0) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 8]`. + fn claim_staker_rewards_ongoing_period(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `438 + x * (8 ±0)` + // Estimated: `4764` + // Minimum execution time: 47_000_000 picoseconds. + Weight::from_parts(48_357_596, 4764) + // Standard Error: 16_408 + .saturating_add(Weight::from_parts(4_302_059, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:0) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + fn claim_bonus_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `158` + // Estimated: `3603` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(36_000_000, 3603) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:1 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + fn claim_dapp_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `1086` + // Estimated: `3948` + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(41_000_000, 3948) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn unstake_from_unregistered() -> Weight { + // Proof Size summary in bytes: + // Measured: `397` + // Estimated: `4764` + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(47_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: DappStaking StakerInfo (r:4 w:3) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 3]`. + fn cleanup_expired_entries(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `250 + x * (75 ±0)` + // Estimated: `4764 + x * (2613 ±0)` + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(38_854_143, 4764) + // Standard Error: 54_134 + .saturating_add(Weight::from_parts(7_359_116, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2613).saturating_mul(x.into())) + } + fn force() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking NextTierConfig (r:1 w:1) + /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:0 w:1) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + fn on_initialize_voting_to_build_and_earn() -> Weight { + // Proof Size summary in bytes: + // Measured: `151` + // Estimated: `3870` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 3870) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking StaticTierParams (r:1 w:0) + /// Proof: DappStaking StaticTierParams (max_values: Some(1), max_size: Some(167), added: 662, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:0 w:1) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + /// Storage: DappStaking NextTierConfig (r:0 w:1) + /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + fn on_initialize_build_and_earn_to_voting() -> Weight { + // Proof Size summary in bytes: + // Measured: `685` + // Estimated: `3870` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(37_000_000, 3870) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + fn on_initialize_build_and_earn_to_build_and_earn() -> Weight { + // Proof Size summary in bytes: + // Measured: `73` + // Estimated: `3870` + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(18_000_000, 3870) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking ContractStake (r:101 w:0) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 100]`. + fn dapp_tier_assignment(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `158 + x * (33 ±0)` + // Estimated: `3063 + x * (2073 ±0)` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(12_709_998, 3063) + // Standard Error: 8_047 + .saturating_add(Weight::from_parts(2_731_946, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn maintenance_mode() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:1) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking NextDAppId (r:1 w:1) + /// Proof: DappStaking NextDAppId (max_values: Some(1), max_size: Some(2), added: 497, mode: MaxEncodedLen) + fn register() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3093` + // Minimum execution time: 16_000_000 picoseconds. + Weight::from_parts(18_000_000, 3093) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + fn set_dapp_reward_beneficiary() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 13_000_000 picoseconds. + Weight::from_parts(13_000_000, 3093) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + fn set_dapp_owner() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 12_000_000 picoseconds. + Weight::from_parts(14_000_000, 3093) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:0 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + fn unregister() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(19_000_000, 3093) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn lock() -> Weight { + // Proof Size summary in bytes: + // Measured: `12` + // Estimated: `4764` + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(43_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn unlock() -> Weight { + // Proof Size summary in bytes: + // Measured: `163` + // Estimated: `4764` + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(43_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 3]`. + fn claim_unlocked(_x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `160 + x * (17 ±0)` + // Estimated: `4764` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(40_955_543, 4764) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn relock_unlocking() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `4764` + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(40_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:1 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn stake() -> Weight { + // Proof Size summary in bytes: + // Measured: `258` + // Estimated: `4764` + // Minimum execution time: 45_000_000 picoseconds. + Weight::from_parts(49_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:1 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn unstake() -> Weight { + // Proof Size summary in bytes: + // Measured: `437` + // Estimated: `4764` + // Minimum execution time: 48_000_000 picoseconds. + Weight::from_parts(54_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:0) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:0) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 8]`. + fn claim_staker_rewards_past_period(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `485 + x * (8 ±0)` + // Estimated: `4764` + // Minimum execution time: 50_000_000 picoseconds. + Weight::from_parts(51_664_058, 4764) + // Standard Error: 15_971 + .saturating_add(Weight::from_parts(4_243_613, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:0) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 8]`. + fn claim_staker_rewards_ongoing_period(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `438 + x * (8 ±0)` + // Estimated: `4764` + // Minimum execution time: 47_000_000 picoseconds. + Weight::from_parts(48_357_596, 4764) + // Standard Error: 16_408 + .saturating_add(Weight::from_parts(4_302_059, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:0) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + fn claim_bonus_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `158` + // Estimated: `3603` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(36_000_000, 3603) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:1 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + fn claim_dapp_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `1086` + // Estimated: `3948` + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(41_000_000, 3948) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn unstake_from_unregistered() -> Weight { + // Proof Size summary in bytes: + // Measured: `397` + // Estimated: `4764` + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(47_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: DappStaking StakerInfo (r:4 w:3) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 3]`. + fn cleanup_expired_entries(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `250 + x * (75 ±0)` + // Estimated: `4764 + x * (2613 ±0)` + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(38_854_143, 4764) + // Standard Error: 54_134 + .saturating_add(Weight::from_parts(7_359_116, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2613).saturating_mul(x.into())) + } + fn force() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking NextTierConfig (r:1 w:1) + /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:0 w:1) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + fn on_initialize_voting_to_build_and_earn() -> Weight { + // Proof Size summary in bytes: + // Measured: `151` + // Estimated: `3870` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 3870) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking StaticTierParams (r:1 w:0) + /// Proof: DappStaking StaticTierParams (max_values: Some(1), max_size: Some(167), added: 662, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:0 w:1) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + /// Storage: DappStaking NextTierConfig (r:0 w:1) + /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + fn on_initialize_build_and_earn_to_voting() -> Weight { + // Proof Size summary in bytes: + // Measured: `685` + // Estimated: `3870` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(37_000_000, 3870) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + fn on_initialize_build_and_earn_to_build_and_earn() -> Weight { + // Proof Size summary in bytes: + // Measured: `73` + // Estimated: `3870` + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(18_000_000, 3870) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking ContractStake (r:101 w:0) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 100]`. + fn dapp_tier_assignment(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `158 + x * (33 ±0)` + // Estimated: `3063 + x * (2073 ±0)` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(12_709_998, 3063) + // Standard Error: 8_047 + .saturating_add(Weight::from_parts(2_731_946, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) + } +} diff --git a/pallets/inflation/src/lib.rs b/pallets/inflation/src/lib.rs index e19841c71d..717aa764f1 100644 --- a/pallets/inflation/src/lib.rs +++ b/pallets/inflation/src/lib.rs @@ -16,13 +16,92 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -//! TODO +//! # Inflation Handler Pallet +//! +//! ## Overview +//! +//! This pallet's main responsibility is handling inflation calculation & distribution. +//! +//! Inflation configuration is calculated periodically, according to the inflation parameters. +//! Based on this configuration, rewards are paid out - either per block or on demand. +//! +//! ## Cycles, Periods, Eras +//! +//! At the start of each cycle, the inflation configuration is recalculated. +//! +//! Cycle can be considered as a 'year' in the Astar network. +//! When cycle starts, inflation is calculated according to the total issuance at that point in time. +//! E.g. if 'yearly' inflation is set to be 7%, and total issuance is 200 ASTR, then the max inflation for that cycle will be 14 ASTR. +//! +//! Each cycle consists of one or more `periods`. +//! Periods are integral part of dApp staking protocol, allowing dApps to promotove themselves, attract stakers and earn rewards. +//! At the end of each period, all stakes are reset, and dApps need to repeat the process. +//! +//! Each period consists of two subperiods: `Voting` and `Build&Earn`. +//! Length of these subperiods is expressed in eras. An `era` is the core _time unit_ in dApp staking protocol. +//! When an era ends, in `Build&Earn` subperiod, rewards for dApps are calculated & assigned. +//! +//! Era's length is expressed in blocks. E.g. an era can last for 7200 blocks, which is approximately 1 day for 12 second block time. +//! +//! `Build&Earn` subperiod length is expressed in eras. E.g. if `Build&Earn` subperiod lasts for 5 eras, it means that during that subperiod, +//! dApp rewards will be calculated & assigned 5 times in total. Also, 5 distinct eras will change during that subperiod. If e.g. `Build&Earn` started at era 100, +//! with 5 eras per `Build&Earn` subperiod, then the subperiod will end at era 105. +//! +//! `Voting` subperiod always comes before `Build&Earn` subperiod. Its length is also expressed in eras, although it has to be interpreted a bit differently. +//! Even though `Voting` can last for more than 1 era in respect of length, it always takes exactly 1 era. +//! What this means is that if `Voting` lasts for 3 eras, and each era lasts 7200 blocks, then `Voting` will last for 21600 blocks. +//! But unlike `Build&Earn` subperiod, `Voting` will only take up one 'numerical' era. So if `Voting` starts at era 110, it will end at era 11. +//! +//! #### Example +//! * Cycle length: 4 periods +//! * `Voting` length: 10 eras +//! * `Build&Earn` length: 81 eras +//! * Era length: 7200 blocks +//! +//! This would mean that cycle lasts for roughly 364 days (4 * (10 + 81)). +//! +//! ## Recalculation +//! +//! When new cycle begins, inflation configuration is recalculated according to the inflation parameters & total issuance at that point in time. +//! Based on the max inflation rate, rewards for different network actors are calculated. +//! +//! Some rewards are calculated to be paid out per block, while some are per era or per period. +//! +//! ## Rewards +//! +//! ### Staker & Treasury Rewards +//! +//! These are paid out at the begininng of each block & are fixed amounts. +//! +//! ### Staker Rewards +//! +//! Staker rewards are paid out per staker, _on-demand_. +//! However, reward pool for an era is calculated at the end of each era. +//! +//! `era_reward_pool = base_staker_reward_pool_per_era + adjustable_staker_reward_pool_per_era` +//! +//! While the base staker reward pool is fixed, the adjustable part is calculated according to the total value staked & the ideal staking rate. +//! +//! ### dApp Rewards +//! +//! dApp rewards are paid out per dApp, _on-demand_. The reward is decided by the dApp staking protocol, or the tier system to be more precise. +//! This pallet only provides the total reward pool for all dApps per era. +//! +//! # Interface +//! +//! ## StakingRewardHandler +//! +//! This pallet implements `StakingRewardHandler` trait, which is used by the dApp staking protocol to get reward pools & distribute rewards. +//! #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; -use astar_primitives::{Balance, BlockNumber}; +use astar_primitives::{ + dapp_staking::{CycleConfiguration, StakingRewardHandler}, + Balance, BlockNumber, +}; use frame_support::{pallet_prelude::*, traits::Currency}; use frame_system::{ensure_root, pallet_prelude::*}; use sp_runtime::{traits::CheckedAdd, Perquintill}; @@ -510,66 +589,3 @@ pub trait PayoutPerBlock { /// Payout reward to the collator responsible for producing the block. fn collators(reward: Imbalance); } - -// TODO: This should be moved to primitives. -// TODO2: However this ends up looking in the end, we should not duplicate these parameters in the runtime. -// Both the dApp staking & inflation pallet should use the same source. -pub trait CycleConfiguration { - /// How many different periods are there in a cycle (a 'year'). - /// - /// This value has to be at least 1. - fn periods_per_cycle() -> u32; - - /// For how many standard era lengths does the voting subperiod last. - /// - /// This value has to be at least 1. - fn eras_per_voting_subperiod() -> u32; - - /// How many standard eras are there in the build&earn subperiod. - /// - /// This value has to be at least 1. - fn eras_per_build_and_earn_subperiod() -> u32; - - /// How many blocks are there per standard era. - /// - /// This value has to be at least 1. - fn blocks_per_era() -> u32; - - /// For how many standard era lengths does the period last. - fn eras_per_period() -> u32 { - Self::eras_per_voting_subperiod().saturating_add(Self::eras_per_build_and_earn_subperiod()) - } - - /// For how many standard era lengths does the cylce (a 'year') last. - fn eras_per_cycle() -> u32 { - Self::eras_per_period().saturating_mul(Self::periods_per_cycle()) - } - - /// How many blocks are there per cycle (a 'year'). - fn blocks_per_cycle() -> u32 { - Self::blocks_per_era().saturating_mul(Self::eras_per_cycle()) - } - - /// For how many standard era lengths do all the build&earn subperiods in a cycle last. - fn build_and_earn_eras_per_cycle() -> u32 { - Self::eras_per_build_and_earn_subperiod().saturating_mul(Self::periods_per_cycle()) - } -} - -// TODO: This should be moved to primitives. -/// Interface for staking reward handler. -/// -/// Provides reward pool values for stakers - normal & bonus rewards, as well as dApp reward pool. -/// Also provides a safe function for paying out rewards. -pub trait StakingRewardHandler { - /// Returns the staker reward pool & dApp reward pool for an era. - /// - /// The total staker reward pool is dynamic and depends on the total value staked. - fn staker_and_dapp_reward_pools(total_value_staked: Balance) -> (Balance, Balance); - - /// Returns the bonus reward pool for a period. - fn bonus_reward_pool() -> Balance; - - /// Attempts to pay out the rewards to the beneficiary. - fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()>; -} diff --git a/pallets/inflation/src/mock.rs b/pallets/inflation/src/mock.rs index a2c0af4b6a..3cac1bca84 100644 --- a/pallets/inflation/src/mock.rs +++ b/pallets/inflation/src/mock.rs @@ -32,13 +32,12 @@ use frame_support::{ use sp_core::H256; use sp_runtime::{ - generic::Header, // TODO: create testing primitives & move it there? traits::{AccountIdConversion, BlakeTwo256, IdentityLookup}, Perquintill, }; -use astar_primitives::{Balance, BlockNumber}; -pub(crate) type AccountId = u64; // TODO: might also be nice to have this under testing primitives? +use astar_primitives::{testing::Header, Balance, BlockNumber}; +pub(crate) type AccountId = u64; /// Initial inflation params set by the mock. pub const INIT_PARAMS: InflationParameters = InflationParameters { @@ -86,7 +85,7 @@ impl frame_system::Config for Test { type Hashing = BlakeTwo256; type AccountId = AccountId; type Lookup = IdentityLookup; - type Header = Header; + type Header = Header; type RuntimeEvent = RuntimeEvent; type BlockHashCount = BlockHashCount; type DbWeight = (); diff --git a/primitives/src/dapp_staking.rs b/primitives/src/dapp_staking.rs new file mode 100644 index 0000000000..92ead96b15 --- /dev/null +++ b/primitives/src/dapp_staking.rs @@ -0,0 +1,85 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::{Balance, BlockNumber}; + +/// Configuration for cycles, periods, subperiods & eras. +/// +/// * `cycle` - Time unit similar to 'year' in the real world. Consists of one or more periods. At the beginning of each cycle, inflation is recalculated. +/// * `period` - Period consists of two distinct subperiods: `Voting` & `Build&Earn`. They are integral parts of dApp staking. +/// Length is expressed in standard eras or just _eras_. +/// * `era` - Era is the basic time unit in the dApp staking protocol. At the end of each era, reward pools for stakers & dApps are calculated. +/// Era length is expressed in blocks. +pub trait CycleConfiguration { + /// How many different periods are there in a cycle (a 'year'). + /// + /// This value has to be at least 1. + fn periods_per_cycle() -> u32; + + /// For how many standard era lengths does the voting subperiod last. + /// + /// This value has to be at least 1. + fn eras_per_voting_subperiod() -> u32; + + /// How many standard eras are there in the build&earn subperiod. + /// + /// This value has to be at least 1. + fn eras_per_build_and_earn_subperiod() -> u32; + + /// How many blocks are there per standard era. + /// + /// This value has to be at least 1. + fn blocks_per_era() -> BlockNumber; + + /// For how many standard era lengths does the period last. + fn eras_per_period() -> u32 { + Self::eras_per_voting_subperiod().saturating_add(Self::eras_per_build_and_earn_subperiod()) + } + + /// For how many standard era lengths does the cylce (a 'year') last. + fn eras_per_cycle() -> u32 { + Self::eras_per_period().saturating_mul(Self::periods_per_cycle()) + } + + /// How many blocks are there per cycle (a 'year'). + fn blocks_per_cycle() -> BlockNumber { + Self::blocks_per_era().saturating_mul(Self::eras_per_cycle()) + } + + /// For how many standard era lengths do all the build&earn subperiods in a cycle last. + fn build_and_earn_eras_per_cycle() -> u32 { + Self::eras_per_build_and_earn_subperiod().saturating_mul(Self::periods_per_cycle()) + } +} + +/// Interface for staking reward handler. +/// +/// Provides reward pool values for stakers - normal & bonus rewards, as well as dApp reward pool. +/// Also provides a safe function for paying out rewards. +pub trait StakingRewardHandler { + /// Returns the staker reward pool & dApp reward pool for an era. + /// + /// The total staker reward pool is dynamic and depends on the total value staked. + fn staker_and_dapp_reward_pools(total_value_staked: Balance) -> (Balance, Balance); + + /// Returns the bonus reward pool for a period. + fn bonus_reward_pool() -> Balance; + + /// Attempts to pay out the rewards to the beneficiary. + fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()>; +} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 010feba290..a531b87334 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -35,6 +35,12 @@ pub mod evm; /// Precompiles pub mod precompiles; +/// dApp staking & inflation primitives. +pub mod dapp_staking; + +/// Useful primitives for testing. +pub mod testing; + /// Benchmark primitives #[cfg(feature = "runtime-benchmarks")] pub mod benchmarks; diff --git a/primitives/src/testing.rs b/primitives/src/testing.rs new file mode 100644 index 0000000000..aa800582f1 --- /dev/null +++ b/primitives/src/testing.rs @@ -0,0 +1,24 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::BlockNumber; + +use sp_runtime::{generic::Header as GenericHeader, traits::BlakeTwo256}; + +/// Test `Header` type aligned with Astar primitives. +pub type Header = GenericHeader; diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index e36a348952..f5e318d898 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -24,12 +24,12 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); -use astar_primitives::evm::HashedDefaultMappings; use frame_support::{ construct_runtime, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Currency, EitherOfDiverse, - EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, Nothing, OnFinalize, WithdrawReasons, + fungible::Unbalanced as FunUnbalanced, AsEnsureOriginWithArg, ConstU128, ConstU32, + ConstU64, Currency, EitherOfDiverse, EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, + Nothing, OnFinalize, WithdrawReasons, }, weights::{ constants::{ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -63,8 +63,9 @@ use sp_runtime::{ use sp_std::prelude::*; pub use astar_primitives::{ - evm::EvmRevertCodeHandler, AccountId, Address, AssetId, Balance, BlockNumber, Hash, Header, - Index, Signature, + dapp_staking::{CycleConfiguration, StakingRewardHandler}, + evm::{EvmRevertCodeHandler, HashedDefaultMappings}, + AccountId, Address, AssetId, Balance, BlockNumber, Hash, Header, Index, Signature, }; pub use pallet_block_rewards_hybrid::RewardDistributionConfig; @@ -296,9 +297,9 @@ impl pallet_balances::Config for Runtime { type AccountStore = System; type WeightInfo = weights::pallet_balances::SubstrateWeight; type HoldIdentifier = (); - type FreezeIdentifier = (); + type FreezeIdentifier = RuntimeFreezeReason; type MaxHolds = ConstU32<0>; - type MaxFreezes = ConstU32<0>; + type MaxFreezes = ConstU32<1>; } parameter_types! { @@ -511,46 +512,31 @@ impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { } } -pub struct DummyRewardPoolProvider; -impl pallet_dapp_staking_v3::RewardPoolProvider for DummyRewardPoolProvider { - fn normal_reward_pools() -> (Balance, Balance) { - ( - Balance::from(1_000_000_000_000 * AST), - Balance::from(1_000_000_000 * AST), - ) - } - fn bonus_reward_pool() -> Balance { - Balance::from(3_000_000 * AST) - } -} - #[cfg(feature = "runtime-benchmarks")] -pub struct BenchmarkHelper(sp_std::marker::PhantomData); +pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); #[cfg(feature = "runtime-benchmarks")] -impl pallet_dapp_staking_v3::BenchmarkHelper> - for BenchmarkHelper> +impl pallet_dapp_staking_v3::BenchmarkHelper, AccountId> + for BenchmarkHelper, AccountId> { fn get_smart_contract(id: u32) -> SmartContract { SmartContract::Wasm(AccountId::from([id as u8; 32])) } -} -parameter_types! { - pub const StandardEraLength: BlockNumber = 30; // should be 1 minute per standard era - pub const StandardErasPerVotingSubperiod: u32 = 2; - pub const StandardErasPerBuildAndEarnSubperiod: u32 = 10; + fn set_balance(account: &AccountId, amount: Balance) { + Balances::write_balance(account, amount) + .expect("Must succeed in test/benchmark environment."); + } } impl pallet_dapp_staking_v3::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; type Currency = Balances; type SmartContract = SmartContract; type ManagerOrigin = frame_system::EnsureRoot; type NativePriceProvider = DummyPriceProvider; - type RewardPoolProvider = DummyRewardPoolProvider; - type StandardEraLength = StandardEraLength; - type StandardErasPerVotingSubperiod = StandardErasPerVotingSubperiod; - type StandardErasPerBuildAndEarnSubperiod = StandardErasPerBuildAndEarnSubperiod; + type StakingRewardHandler = Inflation; + type CycleConfiguration = InflationCycleConfig; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; type MaxNumberOfContracts = ConstU32<100>; @@ -560,8 +546,9 @@ impl pallet_dapp_staking_v3::Config for Runtime { type MaxNumberOfStakedContracts = ConstU32<3>; type MinimumStakeAmount = ConstU128; type NumberOfTiers = ConstU32<4>; + type WeightInfo = pallet_dapp_staking_v3::weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper = BenchmarkHelper>; + type BenchmarkHelper = BenchmarkHelper, AccountId>; } pub struct InflationPayoutPerBlock; @@ -576,21 +563,21 @@ impl pallet_inflation::PayoutPerBlock for InflationPayoutPerB } pub struct InflationCycleConfig; -impl pallet_inflation::CycleConfiguration for InflationCycleConfig { +impl CycleConfiguration for InflationCycleConfig { fn periods_per_cycle() -> u32 { 4 } fn eras_per_voting_subperiod() -> u32 { - StandardErasPerVotingSubperiod::get() + 2 } fn eras_per_build_and_earn_subperiod() -> u32 { - StandardErasPerBuildAndEarnSubperiod::get() + 22 } - fn blocks_per_era() -> u32 { - StandardEraLength::get() + fn blocks_per_era() -> BlockNumber { + 30 } } From e01aa2b505149ca26fe272697b74c4602a2a1fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:02:11 +0100 Subject: [PATCH 09/14] dApp staking v3 - part 7 (#1099) * dApp staking v3 part 6 * Minor refactor of benchmarks * Weights integration * Fix * remove orig file * Minor refactoring, more benchmark code * Extract on_init logic * Some renaming * More benchmarks * Full benchmarks integration * Testing primitives * staking primitives * dev fix * Integration part1 * Integration part2 * Reward payout integration * Replace lock functionality with freeze * Cleanup TODOs * More negative tests * Frozen balance test * Zero div * Docs for inflation * Rename is_active & add some more docs * More docs * pallet docs * text * scripts * More tests * Test, docs * Review comment * Runtime API * Changes * Change dep * Comment * Formatting * Expired entry cleanup * Cleanup logic test * Remove tier labels * fix * Improve README * Improved cleanup * Review comments * Docs * Formatting * Typo --- Cargo.lock | 10 ++ Cargo.toml | 2 + pallets/dapp-staking-v3/README.md | 69 +++++++++++- .../rpc/runtime-api/Cargo.toml | 25 +++++ .../rpc/runtime-api/src/lib.rs | 40 +++++++ .../dapp-staking-v3/src/benchmarking/mod.rs | 38 +++++++ pallets/dapp-staking-v3/src/lib.rs | 100 ++++++++++++++++-- pallets/dapp-staking-v3/src/test/mock.rs | 3 +- .../dapp-staking-v3/src/test/testing_utils.rs | 89 ++++++++++++++-- pallets/dapp-staking-v3/src/test/tests.rs | 7 ++ .../dapp-staking-v3/src/test/tests_types.rs | 4 - pallets/dapp-staking-v3/src/types.rs | 89 +++++++++++++--- pallets/dapp-staking-v3/src/weights.rs | 7 ++ runtime/local/Cargo.toml | 3 + runtime/local/src/lib.rs | 20 +++- 15 files changed, 461 insertions(+), 45 deletions(-) create mode 100644 pallets/dapp-staking-v3/rpc/runtime-api/Cargo.toml create mode 100644 pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5ba5a7b2ac..71cf98c941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2434,6 +2434,15 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "dapp-staking-v3-runtime-api" +version = "0.0.1-alpha" +dependencies = [ + "astar-primitives", + "pallet-dapp-staking-v3", + "sp-api", +] + [[package]] name = "dapps-staking-chain-extension-types" version = "1.1.0" @@ -6010,6 +6019,7 @@ version = "5.25.0" dependencies = [ "array-bytes 6.1.0", "astar-primitives", + "dapp-staking-v3-runtime-api", "fp-rpc", "fp-self-contained", "frame-benchmarking", diff --git a/Cargo.toml b/Cargo.toml index 423cec30c5..e669f8fdd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -284,6 +284,8 @@ pallet-inflation = { path = "./pallets/inflation", default-features = false } pallet-dynamic-evm-base-fee = { path = "./pallets/dynamic-evm-base-fee", default-features = false } pallet-unified-accounts = { path = "./pallets/unified-accounts", default-features = false } +dapp-staking-v3-runtime-api = { path = "./pallets/dapp-staking-v3/rpc/runtime-api", default-features = false } + astar-primitives = { path = "./primitives", default-features = false } astar-test-utils = { path = "./tests/utils", default-features = false } diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md index 72accbfc36..c8de9b0036 100644 --- a/pallets/dapp-staking-v3/README.md +++ b/pallets/dapp-staking-v3/README.md @@ -27,7 +27,7 @@ Each period consists of two subperiods: Each period is denoted by a number, which increments each time a new period begins. Period beginning is marked by the `voting` subperiod, after which follows the `build&earn` period. -Stakes are **only** valid throughout a period. When new period starts, all stakes are reset to **zero**. This helps prevent projects remaining staked due to intertia of stakers, and makes for a more dynamic staking system. +Stakes are **only** valid throughout a period. When new period starts, all stakes are reset to **zero**. This helps prevent projects remaining staked due to intertia of stakers, and makes for a more dynamic staking system. Staker doesn't need to do anything for this to happen, it is automatic. Even though stakes are reset, locks (or freezes) of tokens remain. @@ -39,7 +39,7 @@ Projects participating in dApp staking are expected to market themselves to (re) Stakers must assess whether the project they want to stake on brings value to the ecosystem, and then `vote` for it. Casting a vote, or staking, during the `Voting` subperiod makes the staker eligible for bonus rewards. so they are encouraged to participate. -`Voting` subperiod length is expressed in _standard_ era lengths, even though the entire voting period is treated as a single _voting era_. +`Voting` subperiod length is expressed in _standard_ era lengths, even though the entire voting subperiod is treated as a single _voting era_. E.g. if `voting` subperiod lasts for **5 eras**, and each era lasts for **100** blocks, total length of the `voting` subperiod will be **500** blocks. * Block 1, Era 1 starts, Period 1 starts, `Voting` subperiod starts * Block 501, Era 2 starts, Period 1 continues, `Build&Earn` subperiod starts @@ -173,11 +173,70 @@ Rewards don't remain available forever, and if not claimed within some time peri Rewards are calculated using a simple formula: `staker_reward_pool * staker_staked_amount / total_staked_amount`. -#### Claim Bonus Reward +#### Claiming Bonus Reward -If staker staked on a dApp during the voting period, and didn't reduce their staked amount below what was staked at the end of the voting period, this makes them eligible for the bonus reward. +If staker staked on a dApp during the voting subperiod, and didn't reduce their staked amount below what was staked at the end of the voting subperiod, this makes them eligible for the bonus reward. Bonus rewards need to be claimed per contract, unlike staker rewards. -Bonus reward is calculated using a simple formula: `bonus_reward_pool * staker_voting_period_stake / total_voting_period_stake`. +Bonus reward is calculated using a simple formula: `bonus_reward_pool * staker_voting_subperiod_stake / total_voting_subperiod_stake`. +#### Handling Expired Entries + +There is a limit to how much contracts can a staker stake on at once. +Or to be more precise, for how many contract a database entry can exist at once. + +It's possible that stakers get themselves into a situation where some number of expired database entries associated to +their account has accumulated. In that case, it's required to call a special extrinsic to cleanup these expired entries. + +### Developers + +Main thing for developers to do is develop a good product & attract stakers to stake on them. + +#### Claiming dApp Reward + +If at the end of an build&earn subperiod era dApp has high enough score to enter a tier, it gets rewards assigned to it. +Rewards aren't paid out automatically but must be claimed instead, similar to staker & bonus rewards. + +When dApp reward is being claimed, both smart contract & claim era must be specified. + +dApp reward is calculated based on the tier in which ended. All dApps that end up in one tier will get the exact same reward. + +### Tier System + +At the end of each build&earn subperiod era, dApps are evaluated using a simple metric - total value staked on them. +Based on this metric, they are sorted, and assigned to tiers. + +There is a limited number of tiers, and each tier has a limited capacity of slots. +Each tier also has a _threshold_ which a dApp must satisfy in order to enter it. + +Better tiers bring bigger rewards, so dApps are encouraged to compete for higher tiers and attract staker's support. +For each tier, the reward pool and capacity are fixed. Each dApp within a tier always gets the same amount of reward. +Even if tier capacity hasn't been fully taken, rewards are paid out as if they were. + +For example, if tier 1 has capacity for 10 dApps, and reward pool is **500 ASTR**, it means that each dApp that ends up +in this tier will earn **50 ASTR**. Even if only 3 dApps manage to enter this tier, they will still earn each **50 ASTR**. +The rest, **350 ASTR** in this case, won't be minted (or will be _burned_ if the reader prefers such explanation). + +If there are more dApps eligible for a tier than there is capacity, the dApps with the higher score get the advantage. +dApps which missed out get priority for entry into the next lower tier (if there still is any). + +In the case a dApp doesn't satisfy the entry threshold for any tier, even though there is still capacity, the dApp will simply +be left out of tiers and won't earn **any** reward. + +In a special and unlikely case that two or more dApps have the exact same score and satisfy tier entry threshold, but there isn't enough +leftover tier capacity to accomodate them all, this is considered _undefined_ behavior. Some of the dApps will manage to enter the tier, while +others will be left out. There is no strict rule which defines this behavior - instead dApps are encouraged to ensure their tier entry by +having a larger stake than the other dApp(s). + +### Reward Expiry + +Unclaimed rewards aren't kept indefinitely in storage. Eventually, they expire. +Stakers & developers should make sure they claim those rewards before this happens. + +In case they don't, they will simply miss on the earnings. + +However, this should not be a problem given how the system is designed. +There is no longer _stake&forger_ - users are expected to revisit dApp staking at least at the +beginning of each new period to pick out old or new dApps on which to stake on. +If they don't do that, they miss out on the bonus reward & won't earn staker rewards. \ No newline at end of file diff --git a/pallets/dapp-staking-v3/rpc/runtime-api/Cargo.toml b/pallets/dapp-staking-v3/rpc/runtime-api/Cargo.toml new file mode 100644 index 0000000000..559d5f299b --- /dev/null +++ b/pallets/dapp-staking-v3/rpc/runtime-api/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "dapp-staking-v3-runtime-api" +version = "0.0.1-alpha" +description = "dApp Staking v3 runtime API" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +sp-api = { workspace = true } + +astar-primitives = { workspace = true } +pallet-dapp-staking-v3 = { workspace = true } + +[features] +default = ["std"] +std = [ + "sp-api/std", + "pallet-dapp-staking-v3/std", + "astar-primitives/std", +] diff --git a/pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs b/pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs new file mode 100644 index 0000000000..dde32bf695 --- /dev/null +++ b/pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs @@ -0,0 +1,40 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +use astar_primitives::BlockNumber; +use pallet_dapp_staking_v3::EraNumber; + +sp_api::decl_runtime_apis! { + + /// dApp Staking Api. + /// + /// Used to provide information otherwise not available via RPC. + pub trait DappStakingApi { + + /// For how many standard era lengths does the voting subperiod last. + fn eras_per_voting_subperiod() -> EraNumber; + + /// How many standard eras are there in the build&earn subperiod. + fn eras_per_build_and_earn_subperiod() -> EraNumber; + + /// How many blocks are there per standard era. + fn blocks_per_era() -> BlockNumber; + } +} diff --git a/pallets/dapp-staking-v3/src/benchmarking/mod.rs b/pallets/dapp-staking-v3/src/benchmarking/mod.rs index 4f9cbafdb9..3eab3c6f85 100644 --- a/pallets/dapp-staking-v3/src/benchmarking/mod.rs +++ b/pallets/dapp-staking-v3/src/benchmarking/mod.rs @@ -902,6 +902,44 @@ mod benchmarks { } } + #[benchmark] + fn on_idle_cleanup() { + // Prepare init config (protocol state, tier params & config, etc.) + initial_config::(); + + // Advance enough periods to trigger the cleanup + let retention_period = T::RewardRetentionInPeriods::get(); + advance_to_period::( + ActiveProtocolState::::get().period_number() + retention_period + 2, + ); + + let first_era_span_index = 0; + assert!( + EraRewards::::contains_key(first_era_span_index), + "Sanity check - era reward span entry must exist." + ); + let first_period = 1; + assert!( + PeriodEnd::::contains_key(first_period), + "Sanity check - period end info must exist." + ); + let block_number = System::::block_number(); + + #[block] + { + DappStaking::::on_idle(block_number, Weight::MAX); + } + + assert!( + !EraRewards::::contains_key(first_era_span_index), + "Entry should have been cleaned up." + ); + assert!( + !PeriodEnd::::contains_key(first_period), + "Period end info should have been cleaned up." + ); + } + impl_benchmark_test_suite!( Pallet, crate::benchmarking::tests::new_test_ext(), diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 89ec801e53..8994f55015 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -64,8 +64,7 @@ mod test; mod benchmarking; mod types; -use types::*; -pub use types::{PriceProvider, TierThreshold}; +pub use types::*; pub mod weights; pub use weights::WeightInfo; @@ -444,6 +443,10 @@ pub mod pallet { pub type DAppTiers = StorageMap<_, Twox64Concat, EraNumber, DAppTierRewardsFor, OptionQuery>; + /// History cleanup marker - holds information about which DB entries should be cleaned up next, when applicable. + #[pallet::storage] + pub type HistoryCleanupMarker = StorageValue<_, CleanupMarker, ValueQuery>; + #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { @@ -521,6 +524,10 @@ pub mod pallet { fn on_initialize(now: BlockNumber) -> Weight { Self::era_and_period_handler(now, TierAssignment::Real) } + + fn on_idle(_block: BlockNumberFor, remaining_weight: Weight) -> Weight { + Self::expired_entry_cleanup(&remaining_weight) + } } /// A reason for freezing funds. @@ -580,7 +587,6 @@ pub mod pallet { id: dapp_id, state: DAppState::Registered, reward_destination: None, - tier_label: None, }, ); @@ -1131,9 +1137,8 @@ pub mod pallet { let earliest_staked_era = ledger .earliest_staked_era() .ok_or(Error::::InternalClaimStakerError)?; - let era_rewards = - EraRewards::::get(Self::era_reward_span_index(earliest_staked_era)) - .ok_or(Error::::NoClaimableRewards)?; + let era_rewards = EraRewards::::get(Self::era_reward_index(earliest_staked_era)) + .ok_or(Error::::NoClaimableRewards)?; // The last era for which we can theoretically claim rewards. // And indicator if we know the period's ending era. @@ -1439,6 +1444,11 @@ pub mod pallet { .into()) } + // TODO: this call should be removed prior to mainnet launch. + // It's super useful for testing purposes, but even though force is used in this pallet & works well, + // it won't apply to the inflation recalculation logic - which is wrong. + // Probably for this call to make sense, an outside logic should handle both inflation & dApp staking state changes. + /// Used to force a change of era or subperiod. /// The effect isn't immediate but will happen on the next block. /// @@ -1532,7 +1542,7 @@ pub mod pallet { } /// Calculates the `EraRewardSpan` index for the specified era. - pub(crate) fn era_reward_span_index(era: EraNumber) -> EraNumber { + pub(crate) fn era_reward_index(era: EraNumber) -> EraNumber { era.saturating_sub(era % T::EraRewardSpanLength::get()) } @@ -1829,7 +1839,7 @@ pub mod pallet { CurrentEraInfo::::put(era_info); - let era_span_index = Self::era_reward_span_index(current_era); + let era_span_index = Self::era_reward_index(current_era); let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); if let Err(_) = span.push(current_era, era_reward) { // This must never happen but we log the error just in case. @@ -1848,5 +1858,79 @@ pub mod pallet { consumed_weight } + + /// Attempt to cleanup some expired entries, if enough remaining weight & applicable entries exist. + /// + /// Returns consumed weight. + fn expired_entry_cleanup(remaining_weight: &Weight) -> Weight { + // Need to be able to process full pass + if remaining_weight.any_lt(T::WeightInfo::on_idle_cleanup()) { + return Weight::zero(); + } + + // Get the cleanup marker + let mut cleanup_marker = HistoryCleanupMarker::::get(); + + // Whitelisted storage, no need to account for the read. + let protocol_state = ActiveProtocolState::::get(); + let latest_expired_period = match protocol_state + .period_number() + .checked_sub(T::RewardRetentionInPeriods::get().saturating_add(1)) + { + Some(latest_expired_period) => latest_expired_period, + None => { + // Protocol hasn't advanced enough to have any expired entries. + return T::WeightInfo::on_idle_cleanup(); + } + }; + + // Get the oldest valid era - any era before it is safe to be cleaned up. + let oldest_valid_era = match PeriodEnd::::get(latest_expired_period) { + Some(period_end_info) => period_end_info.final_era.saturating_add(1), + None => { + // Can happen if it's period 0 or if the entry has already been cleaned up. + return T::WeightInfo::on_idle_cleanup(); + } + }; + + // Attempt to cleanup one expired `EraRewards` entry. + if let Some(era_reward) = EraRewards::::get(cleanup_marker.era_reward_index) { + // If oldest valid era comes AFTER this span, it's safe to delete it. + if era_reward.last_era() < oldest_valid_era { + EraRewards::::remove(cleanup_marker.era_reward_index); + cleanup_marker + .era_reward_index + .saturating_accrue(T::EraRewardSpanLength::get()); + } + } else { + // Should never happen, but if it does, log an error and move on. + log::error!( + target: LOG_TARGET, + "Era rewards span for era {} is missing, but cleanup marker is set.", + cleanup_marker.era_reward_index + ); + } + + // Attempt to cleanup one expired `DAppTiers` entry. + if cleanup_marker.dapp_tiers_index < oldest_valid_era { + DAppTiers::::remove(cleanup_marker.dapp_tiers_index); + cleanup_marker.dapp_tiers_index.saturating_inc(); + } + + // One extra grace period before we cleanup period end info. + // This so we can always read the `final_era` of that period. + if let Some(period_end_cleanup) = latest_expired_period.checked_sub(1) { + PeriodEnd::::remove(period_end_cleanup); + } + + // Store the updated cleanup marker + HistoryCleanupMarker::::put(cleanup_marker); + + // We could try & cleanup more entries, but since it's not a critical operation and can happen whenever, + // we opt for the simpler solution where only 1 entry per block is cleaned up. + // It can be changed though. + + T::WeightInfo::on_idle_cleanup() + } } } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 050eb112f2..8be66cd86a 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -18,7 +18,7 @@ use crate::{ self as pallet_dapp_staking, - test::testing_utils::{assert_block_bump, MemorySnapshot}, + test::testing_utils::{assert_block_bump, assert_on_idle_cleanup, MemorySnapshot}, *, }; @@ -330,6 +330,7 @@ impl ExtBuilder { pub(crate) fn run_to_block(n: BlockNumber) { while System::block_number() < n { DappStaking::on_finalize(System::block_number()); + assert_on_idle_cleanup(); System::set_block_number(System::block_number() + 1); // This is performed outside of dapps staking but we expect it before on_initialize diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index f20de522f1..1ed240409e 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -20,13 +20,14 @@ use crate::test::mock::*; use crate::types::*; use crate::{ pallet::Config, ActiveProtocolState, ContractStake, CurrentEraInfo, DAppId, DAppTiers, - EraRewards, Event, FreezeReason, IntegratedDApps, Ledger, NextDAppId, NextTierConfig, - PeriodEnd, PeriodEndInfo, StakerInfo, TierConfig, + EraRewards, Event, FreezeReason, HistoryCleanupMarker, IntegratedDApps, Ledger, NextDAppId, + NextTierConfig, PeriodEnd, PeriodEndInfo, StakerInfo, TierConfig, }; use frame_support::{ - assert_ok, - traits::{fungible::InspectFreeze, Get}, + assert_ok, assert_storage_noop, + traits::{fungible::InspectFreeze, Get, OnIdle}, + weights::Weight, }; use sp_runtime::{traits::Zero, Perbill}; use std::collections::HashMap; @@ -1282,7 +1283,7 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { } // 4. Verify era reward - let era_span_index = DappStaking::era_reward_span_index(pre_protoc_state.era); + let era_span_index = DappStaking::era_reward_index(pre_protoc_state.era); let maybe_pre_era_reward_span = pre_snapshot.era_rewards.get(&era_span_index); let post_era_reward_span = post_snapshot .era_rewards @@ -1350,6 +1351,80 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { } } +/// Verify `on_idle` cleanup. +pub(crate) fn assert_on_idle_cleanup() { + // Pre-data snapshot (limited to speed up testing) + let pre_cleanup_marker = HistoryCleanupMarker::::get(); + let pre_era_rewards: HashMap::EraRewardSpanLength>> = + EraRewards::::iter().collect(); + let pre_period_ends: HashMap = PeriodEnd::::iter().collect(); + + // Calculated the oldest era which is valid (not expired) + let protocol_state = ActiveProtocolState::::get(); + let retention_period: PeriodNumber = ::RewardRetentionInPeriods::get(); + + let oldest_valid_era = match protocol_state + .period_number() + .checked_sub(retention_period + 1) + { + Some(expired_period) if expired_period > 0 => { + pre_period_ends[&expired_period].final_era + 1 + } + _ => { + // No cleanup so no storage changes are expected + assert_storage_noop!(DappStaking::on_idle(System::block_number(), Weight::MAX)); + return; + } + }; + + // Check if any span or tiers cleanup is needed. + let is_era_span_cleanup_expected = + pre_era_rewards[&pre_cleanup_marker.era_reward_index].last_era() < oldest_valid_era; + let is_dapp_tiers_cleanup_expected = pre_cleanup_marker.dapp_tiers_index > 0 + && pre_cleanup_marker.dapp_tiers_index < oldest_valid_era; + + // Check if period end info should be cleaned up + let maybe_period_end_cleanup = match protocol_state + .period_number() + .checked_sub(retention_period + 2) + { + Some(period) if period > 0 => Some(period), + _ => None, + }; + + // Cleanup and verify post state. + + DappStaking::on_idle(System::block_number(), Weight::MAX); + + // Post checks + let post_cleanup_marker = HistoryCleanupMarker::::get(); + + if is_era_span_cleanup_expected { + assert!(!EraRewards::::contains_key( + pre_cleanup_marker.era_reward_index + )); + let span_length: EraNumber = ::EraRewardSpanLength::get(); + assert_eq!( + post_cleanup_marker.era_reward_index, + pre_cleanup_marker.era_reward_index + span_length + ); + } + if is_dapp_tiers_cleanup_expected { + assert!( + !DAppTiers::::contains_key(pre_cleanup_marker.dapp_tiers_index), + "Sanity check." + ); + assert_eq!( + post_cleanup_marker.dapp_tiers_index, + pre_cleanup_marker.dapp_tiers_index + 1 + ) + } + + if let Some(period) = maybe_period_end_cleanup { + assert!(!PeriodEnd::::contains_key(period)); + } +} + /// Returns from which starting era to which ending era can rewards be claimed for the specified account. /// /// If `None` is returned, there is nothing to claim. @@ -1387,10 +1462,10 @@ pub(crate) fn required_number_of_reward_claims(account: AccountId) -> u32 { }; let era_span_length: EraNumber = ::EraRewardSpanLength::get(); - let first = DappStaking::era_reward_span_index(range.0) + let first = DappStaking::era_reward_index(range.0) .checked_div(era_span_length) .unwrap(); - let second = DappStaking::era_reward_span_index(range.1) + let second = DappStaking::era_reward_index(range.1) .checked_div(era_span_length) .unwrap(); diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 1a53113ad6..2e97511007 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -2368,6 +2368,13 @@ fn get_dapp_tier_assignment_zero_slots_per_tier_works() { }) } +#[test] +fn advance_for_some_periods_works() { + ExtBuilder::build().execute_with(|| { + advance_to_period(10); + }) +} + //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index aadfbda6d1..8bf9183b69 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -159,7 +159,6 @@ fn dapp_info_basic_checks() { id: 7, state: DAppState::Registered, reward_destination: None, - tier_label: None, }; // Owner receives reward in case no beneficiary is set @@ -2123,7 +2122,6 @@ fn contract_stake_amount_basic_get_checks_work() { let contract_stake = ContractStakeAmount { staked: Default::default(), staked_future: None, - tier_label: None, }; assert!(contract_stake.is_empty()); assert!(contract_stake.latest_stake_period().is_none()); @@ -2145,7 +2143,6 @@ fn contract_stake_amount_basic_get_checks_work() { let contract_stake = ContractStakeAmount { staked: amount, staked_future: None, - tier_label: None, }; assert!(!contract_stake.is_empty()); @@ -2194,7 +2191,6 @@ fn contract_stake_amount_advanced_get_checks_work() { let contract_stake = ContractStakeAmount { staked: amount_1, staked_future: Some(amount_2), - tier_label: None, }; // Sanity checks - all values from the 'future' entry should be relevant diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 3b18c7d7ac..569546ab03 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -271,8 +271,6 @@ pub struct DAppInfo { pub state: DAppState, // If `None`, rewards goes to the developer account, otherwise to the account Id in `Some`. pub reward_destination: Option, - /// If `Some(_)` dApp has a tier label which can influence the tier assignment. - pub tier_label: Option, } impl DAppInfo { @@ -310,7 +308,60 @@ impl Default for UnlockingChunk { } } -/// General info about user's stakes +/// General info about an account's lock & stakes. +/// +/// ## Overview +/// +/// The most complex part about this type are the `staked` and `staked_future` fields. +/// To understand why the two fields exist and how they are used, it's important to consider some facts: +/// * when an account _stakes_, the staked amount is only eligible for rewards from the next era +/// * all stakes are reset when a period ends - but this is done in a lazy fashion, account ledgers aren't directly updated +/// * `stake` and `unstake` operations are allowed only if the account has claimed all pending rewards +/// +/// In order to keep track of current era stake, and _next era_ stake, two fields are needed. +/// Since it's not allowed to stake/unstake if there are pending rewards, it's guaranteed that the `staked` and `staked_future` eras are **always consecutive**. +/// In order to understand if _stake_ is still valid, it's enough to check the `period` field of either `staked` or `staked_future`. +/// +/// ## Example +/// +/// ### Scenario 1 +/// +/// * current era is **20**, and current period is **1** +/// * `staked` is equal to: `{ voting: 100, build_and_earn: 50, era: 5, period: 1 }` +/// * `staked_future` is equal to: `{ voting: 100, build_and_earn: 100, era: 6, period: 1 }` +/// +/// The correct way to interpret this is: +/// * account had staked **150** in total in era 5 +/// * account had increased their stake to **200** in total in era 6 +/// * since then, era 6, account hadn't staked or unstaked anything or hasn't claimed any rewards +/// * since we're in era **20** and period is still **1**, the account's stake for eras **7** to **20** is still **200** +/// +/// ### Scenario 2 +/// +/// * current era is **20**, and current period is **1** +/// * `staked` is equal to: `{ voting: 0, build_and_earn: 0, era: 0, period: 0 }` +/// * `staked_future` is equal to: `{ voting: 0, build_and_earn: 350, era: 13, period: 1 }` +/// +/// The correct way to interpret this is: +/// * `staked` entry is _empty_ +/// * account had called `stake` during era 12, and staked **350** for the next era +/// * account hadn't staked, unstaked or claimed rewards since then +/// * since we're in era **20** and period is still **1**, the account's stake for eras **13** to **20** is still **350** +/// +/// ### Scenario 3 +/// +/// * current era is **30**, and current period is **2** +/// * period **1** ended after era **24**, and period **2** started in era **25** +/// * `staked` is equal to: `{ voting: 100, build_and_earn: 300, era: 20, period: 1 }` +/// * `staked_future` is equal to `None` +/// +/// The correct way to interpret this is: +/// * in era **20**, account had claimed rewards for the past eras, so only the `staked` entry remained +/// * since then, account hadn't staked, unstaked or claimed rewards +/// * period 1 ended in era **24**, which means that after that era, the `staked` entry is no longer valid +/// * account had staked **400** in total from era **20** up to era **24** (inclusive) +/// * account's stake in era **25** is **zero** +/// #[derive( Encode, Decode, @@ -507,12 +558,12 @@ where /// Ensures that the provided era & period are valid according to the current ledger state. fn stake_unstake_argument_check( &self, - era: EraNumber, + current_era: EraNumber, current_period_info: &PeriodInfo, ) -> Result<(), AccountLedgerError> { if !self.staked.is_empty() { // In case entry for the current era exists, it must match the era exactly. - if self.staked.era != era { + if self.staked.era != current_era { return Err(AccountLedgerError::InvalidEra); } if self.staked.period != current_period_info.number { @@ -520,7 +571,8 @@ where } // In case it doesn't (i.e. first time staking), then the future era must either be the current or the next era. } else if let Some(stake_amount) = self.staked_future { - if stake_amount.era != era.saturating_add(1) && stake_amount.era != era { + if stake_amount.era != current_era.saturating_add(1) && stake_amount.era != current_era + { return Err(AccountLedgerError::InvalidEra); } if stake_amount.period != current_period_info.number { @@ -544,14 +596,14 @@ where pub fn add_stake_amount( &mut self, amount: Balance, - era: EraNumber, + current_era: EraNumber, current_period_info: PeriodInfo, ) -> Result<(), AccountLedgerError> { if amount.is_zero() { return Ok(()); } - self.stake_unstake_argument_check(era, ¤t_period_info)?; + self.stake_unstake_argument_check(current_era, ¤t_period_info)?; if self.stakeable_amount(current_period_info.number) < amount { return Err(AccountLedgerError::UnavailableStakeFunds); @@ -564,7 +616,7 @@ where } None => { let mut stake_amount = self.staked; - stake_amount.era = era.saturating_add(1); + stake_amount.era = current_era.saturating_add(1); stake_amount.period = current_period_info.number; stake_amount.add(amount, current_period_info.subperiod); self.staked_future = Some(stake_amount); @@ -582,14 +634,14 @@ where pub fn unstake_amount( &mut self, amount: Balance, - era: EraNumber, + current_era: EraNumber, current_period_info: PeriodInfo, ) -> Result<(), AccountLedgerError> { if amount.is_zero() { return Ok(()); } - self.stake_unstake_argument_check(era, ¤t_period_info)?; + self.stake_unstake_argument_check(current_era, ¤t_period_info)?; // User must be precise with their unstake amount. if self.staked_amount(current_period_info.number) < amount { @@ -1053,8 +1105,6 @@ pub struct ContractStakeAmount { pub staked: StakeAmount, /// Staked amount in the next or 'future' era. pub staked_future: Option, - /// Tier label for the contract, if any. - pub tier_label: Option, } impl ContractStakeAmount { @@ -1686,10 +1736,15 @@ pub enum DAppTierError { InternalError, } -/// Tier labels can be assigned to dApps in order to provide them benefits (or drawbacks) when being assigned into a tier. -#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] -pub enum TierLabel { - // Empty for now, on purpose. +/// Describes which entries are next in line for cleanup. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct CleanupMarker { + /// Era reward span index that should be checked & cleaned up next. + #[codec(compact)] + pub era_reward_index: EraNumber, + /// dApp tier rewards index that should be checked & cleaned up next. + #[codec(compact)] + pub dapp_tiers_index: EraNumber, } /////////////////////////////////////////////////////////////////////// diff --git a/pallets/dapp-staking-v3/src/weights.rs b/pallets/dapp-staking-v3/src/weights.rs index d8891c7115..3b13aa4622 100644 --- a/pallets/dapp-staking-v3/src/weights.rs +++ b/pallets/dapp-staking-v3/src/weights.rs @@ -71,6 +71,7 @@ pub trait WeightInfo { fn on_initialize_build_and_earn_to_voting() -> Weight; fn on_initialize_build_and_earn_to_build_and_earn() -> Weight; fn dapp_tier_assignment(x: u32, ) -> Weight; + fn on_idle_cleanup() -> Weight; } /// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. @@ -439,6 +440,9 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) } + fn on_idle_cleanup() -> Weight { + T::DbWeight::get().reads_writes(3, 2) + } } // For backwards compatibility and tests @@ -806,4 +810,7 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) } + fn on_idle_cleanup() -> Weight { + RocksDbWeight::get().reads_writes(3, 2) + } } diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index 9212158faa..3a258c29c8 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -82,6 +82,8 @@ pallet-inflation = { workspace = true } pallet-unified-accounts = { workspace = true } pallet-xvm = { workspace = true } +dapp-staking-v3-runtime-api = { workspace = true } + # Moonbeam tracing moonbeam-evm-tracer = { workspace = true, optional = true } moonbeam-rpc-primitives-debug = { workspace = true, optional = true } @@ -122,6 +124,7 @@ std = [ "pallet-chain-extension-unified-accounts/std", "pallet-dapps-staking/std", "pallet-dapp-staking-v3/std", + "dapp-staking-v3-runtime-api/std", "pallet-inflation/std", "pallet-dynamic-evm-base-fee/std", "pallet-ethereum/std", diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index f5e318d898..288c5cf6da 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -27,9 +27,8 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use frame_support::{ construct_runtime, parameter_types, traits::{ - fungible::Unbalanced as FunUnbalanced, AsEnsureOriginWithArg, ConstU128, ConstU32, - ConstU64, Currency, EitherOfDiverse, EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, - Nothing, OnFinalize, WithdrawReasons, + AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Currency, EitherOfDiverse, + EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, Nothing, OnFinalize, WithdrawReasons, }, weights::{ constants::{ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -523,6 +522,7 @@ impl pallet_dapp_staking_v3::BenchmarkHelper, AccountId } fn set_balance(account: &AccountId, amount: Balance) { + use frame_support::traits::fungible::Unbalanced as FunUnbalanced; Balances::write_balance(account, amount) .expect("Must succeed in test/benchmark environment."); } @@ -1739,6 +1739,20 @@ impl_runtime_apis! { } } + impl dapp_staking_v3_runtime_api::DappStakingApi for Runtime { + fn eras_per_voting_subperiod() -> pallet_dapp_staking_v3::EraNumber { + InflationCycleConfig::eras_per_voting_subperiod() + } + + fn eras_per_build_and_earn_subperiod() -> pallet_dapp_staking_v3::EraNumber { + InflationCycleConfig::eras_per_build_and_earn_subperiod() + } + + fn blocks_per_era() -> BlockNumber { + InflationCycleConfig::blocks_per_era() + } + } + #[cfg(feature = "runtime-benchmarks")] impl frame_benchmarking::Benchmark for Runtime { fn benchmark_metadata(extra: bool) -> ( From d9a76ff3f49b7dee423a558d4cdf72bfb5f99e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:42:38 +0100 Subject: [PATCH 10/14] Fix - bonus reward update ledger (#1102) * Bonus claim reduced contract_stake_count * Extra test --- pallets/dapp-staking-v3/src/lib.rs | 3 ++ .../dapp-staking-v3/src/test/testing_utils.rs | 5 +++ pallets/dapp-staking-v3/src/test/tests.rs | 36 ++++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 1 + 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 8994f55015..43ab418e4a 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1254,6 +1254,9 @@ pub mod pallet { // Cleanup entry since the reward has been claimed StakerInfo::::remove(&account, &smart_contract); + Ledger::::mutate(&account, |ledger| { + ledger.contract_stake_count.saturating_dec(); + }); Self::deposit_event(Event::::BonusReward { account: account.clone(), diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 1ed240409e..29cfd9e40d 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -919,6 +919,11 @@ pub(crate) fn assert_claim_bonus_reward(account: AccountId, smart_contract: &Moc !StakerInfo::::contains_key(&account, smart_contract), "Entry must be removed after successful reward claim." ); + assert_eq!( + pre_snapshot.ledger[&account].contract_stake_count, + Ledger::::get(&account).contract_stake_count + 1, + "Count must be reduced since the staker info entry was removed." + ); } /// Claim dapp reward for a particular era. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 2e97511007..40dba6ab31 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -1060,7 +1060,7 @@ fn stake_fails_due_to_too_many_staked_contracts() { // Advance to build&earn subperiod so we ensure non-loyal staking advance_to_next_subperiod(); - // Register smart contracts up the the max allowed number + // Register smart contracts up to the max allowed number for id in 1..=max_number_of_contracts { let smart_contract = MockSmartContract::Wasm(id.into()); assert_register(2, &MockSmartContract::Wasm(id.into())); @@ -2472,3 +2472,37 @@ fn stake_and_unstake_after_reward_claim_is_ok() { assert_unstake(account, &smart_contract, 1); }) } + +#[test] +fn stake_after_period_ends_with_max_staked_contracts() { + ExtBuilder::build().execute_with(|| { + let max_number_of_contracts: u32 = ::MaxNumberOfStakedContracts::get(); + + // Lock amount by staker + let account = 1; + assert_lock(account, 100 as Balance * max_number_of_contracts as Balance); + + // Register smart contracts up to the max allowed number + for id in 1..=max_number_of_contracts { + let smart_contract = MockSmartContract::Wasm(id.into()); + assert_register(2, &smart_contract); + assert_stake(account, &smart_contract, 10); + } + + // Advance to the next period, and claim ALL rewards + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + for id in 1..=max_number_of_contracts { + let smart_contract = MockSmartContract::Wasm(id.into()); + assert_claim_bonus_reward(account, &smart_contract); + } + + // Make sure it's possible to stake again + for id in 1..=max_number_of_contracts { + let smart_contract = MockSmartContract::Wasm(id.into()); + assert_stake(account, &smart_contract, 10); + } + }) +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 569546ab03..a634e2ad75 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -390,6 +390,7 @@ pub struct AccountLedger> { /// Number of contract stake entries in storage. #[codec(compact)] pub contract_stake_count: u32, + // TODO: rename to staker_info_count? } impl Default for AccountLedger From e3661ef4741925dee474e2092a4428f144a8a578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Mon, 18 Dec 2023 08:45:34 +0100 Subject: [PATCH 11/14] dApp Staking Migration (#1101) * dApp Staking Migration * Events, benchmark prep * benchmarks * Benchmarks & mock * weights * limit * Use extrinsic call * Solved todos, docs, improvements * Maintenance mode, on_runtime_upgrade logic * Tests, refactoring * Finish * More tests * Type cleanup * try-runtime checks * deps * Improve docs * Fixes & try-runtime testing modifications * Fix test * repeated * Minor improvements * Improvements * taplo * Minor rename --- Cargo.lock | 35 +- Cargo.toml | 1 + pallets/contracts-migration/Cargo.toml | 31 - pallets/contracts-migration/src/lib.rs | 318 -------- pallets/dapp-staking-migration/Cargo.toml | 56 ++ .../src/benchmarking.rs | 153 ++++ pallets/dapp-staking-migration/src/lib.rs | 692 ++++++++++++++++++ pallets/dapp-staking-migration/src/mock.rs | 287 ++++++++ pallets/dapp-staking-migration/src/tests.rs | 177 +++++ pallets/dapp-staking-migration/src/weights.rs | 219 ++++++ pallets/dapp-staking-v3/src/lib.rs | 2 +- pallets/dapps-staking/src/lib.rs | 12 +- pallets/dapps-staking/src/mock.rs | 3 +- pallets/dapps-staking/src/pallet/mod.rs | 14 +- precompiles/dapps-staking/src/mock.rs | 3 +- runtime/astar/src/lib.rs | 1 + runtime/local/Cargo.toml | 4 + runtime/local/src/lib.rs | 12 + runtime/shibuya/src/lib.rs | 1 + runtime/shiden/src/lib.rs | 1 + tests/xcm-simulator/src/mocks/parachain.rs | 7 +- 21 files changed, 1649 insertions(+), 380 deletions(-) delete mode 100644 pallets/contracts-migration/Cargo.toml delete mode 100644 pallets/contracts-migration/src/lib.rs create mode 100644 pallets/dapp-staking-migration/Cargo.toml create mode 100644 pallets/dapp-staking-migration/src/benchmarking.rs create mode 100644 pallets/dapp-staking-migration/src/lib.rs create mode 100644 pallets/dapp-staking-migration/src/mock.rs create mode 100644 pallets/dapp-staking-migration/src/tests.rs create mode 100644 pallets/dapp-staking-migration/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index 71cf98c941..7de48246da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6044,6 +6044,7 @@ dependencies = [ "pallet-collective", "pallet-contracts", "pallet-contracts-primitives", + "pallet-dapp-staking-migration", "pallet-dapp-staking-v3", "pallet-dapps-staking", "pallet-democracy", @@ -7662,20 +7663,6 @@ dependencies = [ "wasmparser-nostd", ] -[[package]] -name = "pallet-contracts-migration" -version = "1.0.0" -dependencies = [ - "frame-support", - "frame-system", - "pallet-contracts", - "parity-scale-codec", - "scale-info", - "sp-arithmetic", - "sp-runtime", - "sp-std", -] - [[package]] name = "pallet-contracts-primitives" version = "7.0.0" @@ -7716,6 +7703,26 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-dapp-staking-migration" +version = "1.0.0" +dependencies = [ + "astar-primitives", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-dapp-staking-v3", + "pallet-dapps-staking", + "parity-scale-codec", + "scale-info", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-dapp-staking-v3" version = "0.0.1-alpha" diff --git a/Cargo.toml b/Cargo.toml index e669f8fdd4..bc532a8360 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -277,6 +277,7 @@ pallet-block-rewards-hybrid = { path = "./pallets/block-rewards-hybrid", default pallet-collator-selection = { path = "./pallets/collator-selection", default-features = false } pallet-dapps-staking = { path = "./pallets/dapps-staking", default-features = false } pallet-dapp-staking-v3 = { path = "./pallets/dapp-staking-v3", default-features = false } +pallet-dapp-staking-migration = { path = "./pallets/dapp-staking-migration", default-features = false } pallet-xc-asset-config = { path = "./pallets/xc-asset-config", default-features = false } pallet-xvm = { path = "./pallets/xvm", default-features = false } pallet-ethereum-checked = { path = "./pallets/ethereum-checked", default-features = false } diff --git a/pallets/contracts-migration/Cargo.toml b/pallets/contracts-migration/Cargo.toml deleted file mode 100644 index e0959269df..0000000000 --- a/pallets/contracts-migration/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "pallet-contracts-migration" -version = "1.0.0" -license = "Apache-2.0" -description = "FRAME pallet for managing multi-block pallet contracts storage migration" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -repository.workspace = true - -[dependencies] -frame-support = { workspace = true } -frame-system = { workspace = true } -pallet-contracts = { workspace = true } -parity-scale-codec = { workspace = true } -scale-info = { workspace = true } -sp-arithmetic = { workspace = true } -sp-runtime = { workspace = true } -sp-std = { workspace = true } - -[features] -default = ["std"] -std = [ - "parity-scale-codec/std", - "scale-info/std", - "sp-std/std", - "frame-support/std", - "frame-system/std", - "pallet-contracts/std", -] -try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/contracts-migration/src/lib.rs b/pallets/contracts-migration/src/lib.rs deleted file mode 100644 index 70873db7fd..0000000000 --- a/pallets/contracts-migration/src/lib.rs +++ /dev/null @@ -1,318 +0,0 @@ -// This file is part of Astar. - -// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// Astar is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Astar 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 General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Astar. If not, see . - -#![cfg_attr(not(feature = "std"), no_std)] - -/// Purpose of this pallet is to provide multi-stage migration features for pallet-contracts v9 migration. -/// Once it's finished for both `Shibuya` and `Shiden`, it should be deleted. -pub use pallet::*; - -use frame_support::{ - log, - pallet_prelude::*, - storage::{generator::StorageMap, unhashed}, - storage_alias, - traits::Get, - WeakBoundedVec, -}; - -use frame_system::pallet_prelude::*; -use pallet_contracts::Determinism; -use parity_scale_codec::{Decode, Encode, FullCodec}; -use sp_runtime::Saturating; -#[cfg(feature = "try-runtime")] -use sp_std::vec::Vec; - -pub use crate::pallet::CustomMigration; - -const LOG_TARGET: &str = "pallet-contracts-migration"; - -#[frame_support::pallet] -pub mod pallet { - use super::*; - - #[pallet::pallet] - pub struct Pallet(PhantomData); - - #[pallet::config] - pub trait Config: frame_system::Config + pallet_contracts::Config { - /// The overarching event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - } - - #[pallet::storage] - #[pallet::getter(fn migration_state)] - pub type MigrationStateStorage = StorageValue<_, MigrationState, ValueQuery>; - - #[pallet::event] - #[pallet::generate_deposit(pub(crate) fn deposit_event)] - pub enum Event { - /// Number of contracts that were migrated in the migration call - ContractsMigrated(u32), - } - - // The following structs & types were taken from `pallet-contracts` since they aren't exposed outside of the `pallet-contracts` crate. - - #[storage_alias] - type CodeStorage = - StorageMap, Identity, CodeHash, PrefabWasmModule>; - - type CodeHash = ::Hash; - type RelaxedCodeVec = WeakBoundedVec::MaxCodeLen>; - - #[derive(Encode, Decode, RuntimeDebug, MaxEncodedLen)] - pub struct OldPrefabWasmModule { - #[codec(compact)] - pub instruction_weights_version: u32, - #[codec(compact)] - pub initial: u32, - #[codec(compact)] - pub maximum: u32, - pub code: RelaxedCodeVec, - } - - #[derive(Encode, Decode, RuntimeDebug, MaxEncodedLen)] - pub struct PrefabWasmModule { - #[codec(compact)] - pub instruction_weights_version: u32, - #[codec(compact)] - pub initial: u32, - #[codec(compact)] - pub maximum: u32, - pub code: RelaxedCodeVec, - pub determinism: Determinism, - } - - #[pallet::hooks] - impl Hooks> for Pallet { - fn on_initialize(_now: BlockNumberFor) -> Weight { - // This is done in order to account for the read in call filter - >::on_chain_storage_version(); - T::DbWeight::get().reads(1) - } - } - - #[pallet::call] - impl Pallet { - #[pallet::call_index(0)] - #[pallet::weight({ - let max_allowed_call_weight = Pallet::::max_call_weight(); - weight_limit - .unwrap_or(max_allowed_call_weight) - .min(max_allowed_call_weight) - })] - pub fn migrate( - origin: OriginFor, - weight_limit: Option, - ) -> DispatchResultWithPostInfo { - ensure_signed(origin)?; - - let consumed_weight = Self::do_migrate(weight_limit); - - Ok(Some(consumed_weight).into()) - } - } - - impl Pallet { - fn do_migrate(requested_weight_limit: Option) -> Weight { - let version = >::on_chain_storage_version(); - let mut consumed_weight = T::DbWeight::get().reads(1); - - if version != 8 { - log::trace!( - target: LOG_TARGET, - "Version is {:?} so skipping migration procedures.", - version, - ); - Self::deposit_event(Event::::ContractsMigrated(0)); - return consumed_weight; - } - - let max_allowed_call_weight = Self::max_call_weight(); - let weight_limit = requested_weight_limit - .unwrap_or(max_allowed_call_weight) - .min(max_allowed_call_weight); - log::trace!( - target: LOG_TARGET, - "CodeStorage migration weight limit will be {:?}.", - weight_limit, - ); - - let migration_state = MigrationStateStorage::::get().for_iteration(); - - if let MigrationState::CodeStorage(last_processed_key) = migration_state { - // First, get correct iterator. - let key_iter = if let Some(previous_key) = last_processed_key { - CodeStorage::::iter_keys_from(previous_key.into_inner()) - } else { - CodeStorage::::iter_keys() - }; - - let mut counter = 0_u32; - - for key in key_iter { - let key_as_vec = CodeStorage::::storage_map_final_key(key); - let used_weight = - Self::translate(&key_as_vec, |old: OldPrefabWasmModule| { - Some(PrefabWasmModule:: { - instruction_weights_version: old.instruction_weights_version, - initial: old.initial, - maximum: old.maximum, - code: old.code, - determinism: Determinism::Enforced, - }) - }); - - // Increment total consumed weight. - consumed_weight.saturating_accrue(used_weight); - counter += 1; - - // Check if we've consumed enough weight already. - if consumed_weight.any_gt(weight_limit) { - log::trace!( - target: LOG_TARGET, - "CodeStorage migration stopped after consuming {:?} weight and after processing {:?} DB entries.", - consumed_weight, counter, - ); - MigrationStateStorage::::put(MigrationState::CodeStorage(Some( - WeakBoundedVec::force_from(key_as_vec, None), - ))); - consumed_weight.saturating_accrue(T::DbWeight::get().writes(1)); - - Self::deposit_event(Event::::ContractsMigrated(counter)); - - // we want try-runtime to execute the entire migration - if cfg!(feature = "try-runtime") { - return Self::do_migrate(Some(weight_limit)) - .saturating_add(consumed_weight); - } else { - return consumed_weight; - } - } - } - - log::trace!(target: LOG_TARGET, "CodeStorage migration finished.",); - Self::deposit_event(Event::::ContractsMigrated(counter)); - - // Clean up storage value so we can safely remove the pallet later - MigrationStateStorage::::kill(); - StorageVersion::new(9).put::>(); - consumed_weight.saturating_accrue(T::DbWeight::get().writes(2)); - } - - consumed_weight - } - - /// Max allowed weight that migration should be allowed to consume - fn max_call_weight() -> Weight { - // 50% of block should be fine - T::BlockWeights::get().max_block / 2 - } - - /// Used to translate a single value in the DB - /// Returns conservative weight estimate of the operation - fn translate Option>( - key: &[u8], - mut f: F, - ) -> Weight { - let value = match unhashed::get::(key) { - Some(value) => value, - None => { - return Weight::from_parts( - T::DbWeight::get().reads(1).ref_time(), - OldPrefabWasmModule::::max_encoded_len() as u64, - ); - } - }; - - let mut proof_size = value.using_encoded(|o| o.len() as u64); - - match f(value) { - Some(new) => { - proof_size.saturating_accrue(new.using_encoded(|n| n.len() as u64)); - unhashed::put::(key, &new); - } - // Cannot happen in this file - None => unhashed::kill(key), - } - - Weight::from_parts(T::DbWeight::get().reads_writes(1, 1).ref_time(), proof_size) - } - } - - #[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, RuntimeDebug, MaxEncodedLen)] - pub enum MigrationState { - /// No migration in progress - NotInProgress, - /// In the middle of `CodeStorage` migration. The const for max size is an overestimate but that's fine. - CodeStorage(Option>>), - } - - impl MigrationState { - /// Convert `self` into value applicable for iteration - fn for_iteration(self) -> Self { - if self == Self::NotInProgress { - Self::CodeStorage(None) - } else { - self - } - } - } - - impl Default for MigrationState { - fn default() -> Self { - MigrationState::NotInProgress - } - } - - pub struct CustomMigration(PhantomData); - impl frame_support::traits::OnRuntimeUpgrade for CustomMigration { - fn on_runtime_upgrade() -> Weight { - // Ensures that first step only starts the migration with minimal changes in case of production build. - // In case of `try-runtime`, we want predefined limit. - let limit = if cfg!(feature = "try-runtime") { - None - } else { - Some(Weight::zero()) - }; - Pallet::::do_migrate(limit) - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade(_state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { - for value in CodeStorage::::iter_values() { - ensure!( - value.determinism == Determinism::Enforced, - "All pre-existing codes need to be deterministic." - ); - } - - ensure!( - !MigrationStateStorage::::exists(), - "MigrationStateStorage has to be killed at the end of migration." - ); - - ensure!( - >::on_chain_storage_version() == 9, - "pallet-contracts storage version must be 9 at the end of migration" - ); - - Ok(()) - } - } -} diff --git a/pallets/dapp-staking-migration/Cargo.toml b/pallets/dapp-staking-migration/Cargo.toml new file mode 100644 index 0000000000..ab503dbe17 --- /dev/null +++ b/pallets/dapp-staking-migration/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "pallet-dapp-staking-migration" +version = "1.0.0" +license = "GPL-3.0-or-later" +description = "Pallet for managing dApp staking v2 to v3 migration." +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +frame-benchmarking = { workspace = true, optional = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +astar-primitives = { workspace = true, optional = true } +pallet-dapp-staking-v3 = { workspace = true } +pallet-dapps-staking = { workspace = true } + +[dev-dependencies] +astar-primitives = { workspace = true } +pallet-balances = { workspace = true } +sp-arithmetic = { workspace = true } +sp-core = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "scale-info/std", + "sp-std/std", + "sp-io/std", + "frame-support/std", + "frame-system/std", + "pallet-dapp-staking-v3/std", + "pallet-dapps-staking/std", + "frame-benchmarking/std", + "astar-primitives?/std", + "sp-core/std", + "pallet-balances/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "pallet-dapp-staking-v3/runtime-benchmarks", + "pallet-dapps-staking/runtime-benchmarks", + "astar-primitives/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime", "astar-primitives"] diff --git a/pallets/dapp-staking-migration/src/benchmarking.rs b/pallets/dapp-staking-migration/src/benchmarking.rs new file mode 100644 index 0000000000..6570322068 --- /dev/null +++ b/pallets/dapp-staking-migration/src/benchmarking.rs @@ -0,0 +1,153 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::{Pallet as Migration, *}; + +use frame_benchmarking::{account as benchmark_account, v2::*}; +use frame_support::{assert_ok, storage::unhashed::put_raw, traits::Currency}; + +/// Generate an unique smart contract using the provided index as a sort-of indetifier +fn smart_contract(index: u8) -> T::SmartContract { + // This is a hacky approach to provide different smart contracts without touching the smart contract trait. + let mut encoded_smart_contract = T::SmartContract::default().encode(); + *encoded_smart_contract.last_mut().unwrap() = index; + + Decode::decode(&mut TrailingZeroInput::new(encoded_smart_contract.as_ref())) + .expect("Shouldn't occur as long as EVM is the default type.") +} + +/// Initialize the old dApp staking pallet with some storage. +pub(super) fn initial_config() { + let dapps_number = ::MaxNumberOfContracts::get(); + let dapps_number = (dapps_number as u8).min(100); + + // Add some dummy dApps to the old pallet. + for idx in 0..dapps_number { + let developer: T::AccountId = benchmark_account("developer", idx.into(), 123); + ::Currency::make_free_balance_be( + &developer, + ::RegisterDeposit::get() * 2, + ); + let smart_contract = smart_contract::(idx); + assert_ok!(pallet_dapps_staking::Pallet::::register( + RawOrigin::Root.into(), + developer, + smart_contract.clone(), + )); + + let staker: T::AccountId = benchmark_account("staker", idx.into(), 123); + let lock_amount = ::MinimumStakingAmount::get() + .max(::MinimumLockedAmount::get()); + ::Currency::make_free_balance_be( + &staker, + lock_amount * 100, + ); + assert_ok!(pallet_dapps_staking::Pallet::::bond_and_stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract, + lock_amount, + )); + } +} + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn migrate_dapps_success() { + initial_config::(); + + #[block] + { + assert!(Migration::::migrate_dapps().is_ok()); + } + } + + #[benchmark] + fn migrate_dapps_noop() { + #[block] + { + assert!(Migration::::migrate_dapps().is_err()); + } + } + + #[benchmark] + fn migrate_ledger_success() { + initial_config::(); + + #[block] + { + assert!(Migration::::migrate_ledger().is_ok()); + } + } + + #[benchmark] + fn migrate_ledger_noop() { + #[block] + { + assert!(Migration::::migrate_ledger().is_err()); + } + } + + #[benchmark] + fn cleanup_old_storage_success() { + let hashed_prefix = twox_128(pallet_dapps_staking::Pallet::::name().as_bytes()); + let _ = clear_prefix(&hashed_prefix, None); + + put_raw(&hashed_prefix, &[0xFF; 128]); + + #[block] + { + if cfg!(test) { + // TODO: for some reason, tests always fail here, nothing gets removed from storage. + // When tested against real runtime, it works just fine. + let _ = Migration::::cleanup_old_storage(1); + } else { + assert!(Migration::::cleanup_old_storage(1).is_ok()); + } + } + } + + #[benchmark] + fn cleanup_old_storage_noop() { + let hashed_prefix = twox_128(pallet_dapps_staking::Pallet::::name().as_bytes()); + let _ = clear_prefix(&hashed_prefix, None); + + #[block] + { + assert!(Migration::::cleanup_old_storage(1).is_err()); + } + } + + impl_benchmark_test_suite!( + Pallet, + crate::benchmarking::tests::new_test_ext(), + crate::mock::Test, + ); +} + +#[cfg(test)] +mod tests { + use crate::mock; + use sp_io::TestExternalities; + + pub fn new_test_ext() -> TestExternalities { + mock::ExtBuilder::build() + } +} diff --git a/pallets/dapp-staking-migration/src/lib.rs b/pallets/dapp-staking-migration/src/lib.rs new file mode 100644 index 0000000000..be26dedcd4 --- /dev/null +++ b/pallets/dapp-staking-migration/src/lib.rs @@ -0,0 +1,692 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +//! ## Summary +//! +//! Purpose of this pallet is to provide multi-stage migration for moving +//! from the old _dapps_staking_v2_ over to the new _dapp_staking_v3_. +//! +//! ## Approach +//! +//! ### Multi-Stage Migration +//! +//! Since a lot of data has to be cleaned up & migrated, it is necessary to do this in multiple steps. +//! To reduce the risk of something going wrong, nothing is done in _mandatory hooks_, like `on_initialize` or `on_idle`. +//! Instead, a dedicated extrinsic call is introduced, which can be called to move the migration forward. +//! As long as this call moves the migration forward, its cost is refunded to the user. +//! Once migration finishes, the extrinsic call will no longer do anything but won't refund the call cost either. +//! +//! ### Migration Steps +//! +//! The general approach used when migrating is: +//! 1. Clean up old pallet's storage using custom code +//! 2. Use dedicated dApp staking v3 extrinsic calls for registering dApps & locking funds. +//! +//! The main benefits of this approach are that we don't duplicate logic that is already present in dApp staking v3, +//! and that we ensure proper events are emitted for each action which will make indexers happy. No special handling will +//! be required to migrate dApps or locked/staked funds over from the old pallet to the new one, from the indexers perspective. +//! +//! ### Final Cleanup +//! +//! The pallet doesn't clean after itself, so when it's removed from the runtime, +//! the old storage should be cleaned up using `RemovePallet` type. +//! + +pub use pallet::*; + +use frame_support::{ + dispatch::PostDispatchInfo, + log, + pallet_prelude::*, + traits::{Get, LockableCurrency, ReservableCurrency}, +}; + +use frame_system::{pallet_prelude::*, RawOrigin}; +use parity_scale_codec::{Decode, Encode}; +use sp_io::{hashing::twox_128, storage::clear_prefix, KillStorageResult}; +use sp_runtime::{ + traits::{TrailingZeroInput, UniqueSaturatedInto}, + Saturating, +}; + +use pallet_dapps_staking::{Ledger as OldLedger, RegisteredDapps as OldRegisteredDapps}; + +#[cfg(feature = "try-runtime")] +use astar_primitives::Balance; +#[cfg(feature = "try-runtime")] +use sp_std::vec::Vec; + +pub use crate::pallet::DappStakingMigrationHandler; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod weights; +pub use weights::WeightInfo; + +const LOG_TARGET: &str = "dapp-staking-migration"; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: + // Tight coupling, but it's fine since pallet is supposed to be just temporary and will be removed after migration. + frame_system::Config + pallet_dapp_staking_v3::Config + pallet_dapps_staking::Config + { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Weight info for various calls & operations in the pallet. + type WeightInfo: WeightInfo; + } + + /// Used to store the current migration state. + #[pallet::storage] + pub type MigrationStateStorage = StorageValue<_, MigrationState, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// Number of entries migrated from v2 over to v3 + EntriesMigrated(u32), + /// Number of entries deleted from v2 + EntriesDeleted(u32), + } + + #[pallet::call] + impl Pallet { + /// Attempt to execute migration steps, consuming up to the specified amount of weight. + /// If no weight is specified, max allowed weight is used. + /// + /// Regardless of the specified weight limit, it will be clamped between the minimum & maximum allowed values. + /// This means that even if user specifies `Weight::zero()` as the limit, + /// the call will be charged & executed using the minimum allowed weight. + #[pallet::call_index(0)] + #[pallet::weight({ + Pallet::::clamp_call_weight(*weight_limit) + })] + pub fn migrate( + origin: OriginFor, + weight_limit: Option, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + + let weight_to_use = Self::clamp_call_weight(weight_limit); + let consumed_weight = Self::do_migrate(weight_to_use); + + // Refund the user in case migration call was needed. + match consumed_weight { + Ok(weight) => Ok(PostDispatchInfo { + actual_weight: Some(weight), + pays_fee: Pays::No, + }), + // No refunds or adjustments! + Err(_) => Ok(().into()), + } + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn integrity_test() { + assert!(Pallet::::max_call_weight().all_gte(Pallet::::min_call_weight())); + + assert!(Pallet::::max_call_weight() + .all_lte(::BlockWeights::get().max_block)); + + assert!(Pallet::::migration_weight_margin().all_lte(Pallet::::min_call_weight())); + } + } + + impl Pallet { + /// Execute migrations steps until the specified weight limit has been consumed. + /// + /// Depending on the number of entries migrated and/or deleted, appropriate events are emited. + /// + /// In case at least some progress is made, `Ok(_)` is returned. + /// If no progress is made, `Err(_)` is returned. + fn do_migrate(weight_limit: Weight) -> Result { + // Find out if migration is still in progress + let init_migration_state = MigrationStateStorage::::get(); + let mut consumed_weight = T::DbWeight::get().reads(1); + + if init_migration_state == MigrationState::Finished { + log::trace!( + target: LOG_TARGET, + "Migration has been finished, skipping any action." + ); + return Err(consumed_weight); + } + + // Ensure we can call dApp staking v3 extrinsics within this call. + consumed_weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2)); + pallet_dapp_staking_v3::ActiveProtocolState::::mutate(|state| { + state.maintenance = false; + }); + + let mut migration_state = init_migration_state; + let (mut entries_migrated, mut entries_deleted) = (0_u32, 0_u32); + + // Execute migration steps only if we have enough weight to do so. + // + // 1. Migrate registered dApps + // 2. Migrate ledgers + // 3. Cleanup + while weight_limit + .saturating_sub(consumed_weight) + .all_gte(Self::migration_weight_margin()) + { + match migration_state { + MigrationState::NotInProgress | MigrationState::RegisteredDApps => { + migration_state = MigrationState::RegisteredDApps; + + match Self::migrate_dapps() { + Ok(weight) => { + consumed_weight.saturating_accrue(weight); + entries_migrated.saturating_inc(); + } + Err(weight) => { + consumed_weight.saturating_accrue(weight); + migration_state = MigrationState::Ledgers; + } + } + } + MigrationState::Ledgers => match Self::migrate_ledger() { + Ok(weight) => { + consumed_weight.saturating_accrue(weight); + entries_migrated.saturating_inc(); + } + Err(weight) => { + consumed_weight.saturating_accrue(weight); + migration_state = MigrationState::Cleanup; + } + }, + MigrationState::Cleanup => { + // Ensure we don't attempt to delete too much at once. + const SAFETY_MARGIN: u32 = 1000; + let remaining_weight = weight_limit.saturating_sub(consumed_weight); + let capacity = match remaining_weight.checked_div_per_component( + &::WeightInfo::cleanup_old_storage_success(), + ) { + Some(entries_to_delete) => { + SAFETY_MARGIN.min(entries_to_delete.unique_saturated_into()) + } + None => { + // Not enough weight to delete even a single entry + break; + } + }; + + match Self::cleanup_old_storage(capacity) { + Ok((weight, count)) => { + consumed_weight.saturating_accrue(weight); + entries_deleted.saturating_accrue(count); + } + Err((weight, count)) => { + consumed_weight.saturating_accrue(weight); + entries_deleted.saturating_accrue(count); + migration_state = MigrationState::Finished; + } + } + } + MigrationState::Finished => { + // Nothing more to do here + break; + } + } + } + + // Deposit events if needed + if entries_migrated > 0 { + Self::deposit_event(Event::::EntriesMigrated(entries_migrated)); + } + if entries_deleted > 0 { + Self::deposit_event(Event::::EntriesDeleted(entries_deleted)); + } + + // Put the pallet back into maintenance mode. + pallet_dapp_staking_v3::ActiveProtocolState::::mutate(|state| { + state.maintenance = true; + }); + + if migration_state != init_migration_state { + // Already charged in pessimistic manner at the beginning of the function. + MigrationStateStorage::::put(migration_state); + } + + Ok(consumed_weight) + } + + /// Used to migrate `RegisteredDapps` from the _old_ dApps staking v2 pallet over to the new `IntegratedDApps`. + /// + /// Steps: + /// 1. Attempt to `drain` a single DB entry from the old storage. If it's unregistered, move on. + /// 2. Unreserve the old `RegisterDeposit` amount from the developer account. + /// 2. Re-decode old smart contract type into new one. Operation should be infalible in practice since the same underlying type is used. + /// 3. `register` the old-new smart contract into dApp staking v3 pallet. + /// + /// Returns `Ok(_)` if an entry was migrated, `Err(_)` if there are no more entries to migrate. + pub(crate) fn migrate_dapps() -> Result { + match OldRegisteredDapps::::drain().next() { + Some((smart_contract, old_dapp_info)) => { + // In case dApp was unregistered, nothing more to do here + if old_dapp_info.is_unregistered() { + // Not precise, but happens rarely + return Ok(::WeightInfo::migrate_dapps_success()); + } + + // Release reserved funds from the old dApps staking + ::Currency::unreserve( + &old_dapp_info.developer, + ::RegisterDeposit::get(), + ); + + // Trick to get around different associated types which are essentially the same underlying struct. + let new_smart_contract = match Decode::decode(&mut TrailingZeroInput::new( + smart_contract.encode().as_ref(), + )) { + Ok(new_smart_contract) => new_smart_contract, + Err(_) => { + log::error!( + target: LOG_TARGET, + "Failed to decode smart contract: {:?}.", + smart_contract, + ); + + // This should never happen, but if it does, we want to know about it. + #[cfg(feature = "try-runtime")] + panic!("Failed to decode smart contract: {:?}", smart_contract); + #[cfg(not(feature = "try-runtime"))] + // Not precise, but must never happen in production + return Ok(::WeightInfo::migrate_dapps_success()); + } + }; + + match pallet_dapp_staking_v3::Pallet::::register( + RawOrigin::Root.into(), + old_dapp_info.developer.clone(), + new_smart_contract, + ) { + Ok(_) => {} + Err(error) => { + log::error!( + target: LOG_TARGET, + "Failed to register smart contract: {:?} with error: {:?}.", + smart_contract, + error, + ); + + // This should never happen, but if it does, we want to know about it. + #[cfg(feature = "try-runtime")] + panic!( + "Failed to register smart contract: {:?} with error: {:?}.", + smart_contract, error + ); + } + } + + Ok(::WeightInfo::migrate_dapps_success()) + } + None => { + // Nothing more to migrate here + Err(::WeightInfo::migrate_dapps_noop()) + } + } + } + + /// Used to migrate `Ledger` from the _old_ dApps staking v2 pallet over to the new `Ledger`. + /// + /// Steps: + /// 1. Attempt to `drain` a single DB entry from the old storage. + /// 2. Release the old lock from the staker account, in full. + /// 3. Lock (or freeze) the old _staked_ amount into the new dApp staking v3 pallet. + /// + /// **NOTE:** the amount that was undergoing the unbonding process is not migrated but is immediately fully released. + /// + /// Returns `Ok(_)` if an entry was migrated, `Err(_)` if there are no more entries to migrate. + pub(crate) fn migrate_ledger() -> Result { + match OldLedger::::drain().next() { + Some((staker, old_account_ledger)) => { + let locked = old_account_ledger.locked; + + // Old unbonding amount can just be released, to keep things simple. + // Alternative is to re-calculat this into unlocking chunks. + let _total_unbonding = old_account_ledger.unbonding_info.sum(); + + ::Currency::remove_lock( + pallet_dapps_staking::pallet::STAKING_ID, + &staker, + ); + + // No point in attempting to lock the old amount into dApp staking v3 if amount is insufficient. + if locked >= ::MinimumLockedAmount::get() { + match pallet_dapp_staking_v3::Pallet::::lock( + RawOrigin::Signed(staker.clone()).into(), + locked, + ) { + Ok(_) => {} + Err(error) => { + log::error!( + target: LOG_TARGET, + "Failed to lock for staker {:?} with error: {:?}.", + staker, + error, + ); + + // This should never happen, but if it does, we want to know about it. + #[cfg(feature = "try-runtime")] + panic!( + "Failed to lock for staker {:?} with error: {:?}.", + staker, error, + ); + } + } + } + + // In case no lock action, it will be imprecise but it's fine since this + // isn't expected to happen, and even if it does, it's not a big deal. + Ok(::WeightInfo::migrate_ledger_success()) + } + None => { + // Nothing more to migrate here + Err(::WeightInfo::migrate_ledger_noop()) + } + } + } + + /// Used to remove one entry from the old _dapps_staking_v2_ storage. + /// + /// If there are no more entries to remove, returns `Err(_)` with consumed weight and number of deleted entries. + /// Otherwise returns `Ok(_)` with consumed weight and number of consumed enries. + pub(crate) fn cleanup_old_storage(limit: u32) -> Result<(Weight, u32), (Weight, u32)> { + let hashed_prefix = twox_128(pallet_dapps_staking::Pallet::::name().as_bytes()); + + // Repeated calls in the same block don't work, so we set the limit to `Unlimited` in case of `try-runtime` testing. + let inner_limit = if cfg!(feature = "try-runtime") { + None + } else { + Some(limit) + }; + + let (keys_removed, done) = match clear_prefix(&hashed_prefix, inner_limit) { + KillStorageResult::AllRemoved(value) => (value, true), + KillStorageResult::SomeRemaining(value) => (value, false), + }; + + log::trace!( + target: LOG_TARGET, + "Removed {} keys from storage.", + keys_removed + ); + + if !done { + Ok(( + ::WeightInfo::cleanup_old_storage_success() + .saturating_mul(keys_removed.into()), + keys_removed as u32, + )) + } else { + log::trace!(target: LOG_TARGET, "All keys have been removed.",); + Err(( + ::WeightInfo::cleanup_old_storage_noop(), + keys_removed as u32, + )) + } + } + + /// Max allowed weight that migration should be allowed to consume. + pub(crate) fn max_call_weight() -> Weight { + // 50% of block should be fine + T::BlockWeights::get().max_block / 2 + } + + /// Min allowed weight that migration should be allowed to consume. + /// + /// This serves as a safety marging, to prevent accidental overspending, due to + /// inprecision in implementation or benchmarks, when small weight limit is specified. + pub(crate) fn min_call_weight() -> Weight { + // 5% of block should be fine + T::BlockWeights::get().max_block / 10 + } + + /// Calculate call weight to use. + /// + /// In case of `None`, use the max allowed call weight. + /// Otherwise clamp the specified weight between the allowed min & max values. + fn clamp_call_weight(weight: Option) -> Weight { + weight + .unwrap_or(Self::max_call_weight()) + .min(Self::max_call_weight()) + .max(Self::min_call_weight()) + } + + /// Returns the least amount of weight which should be remaining for migration in order to attempt another step. + /// + /// This is used to ensure we don't go over the limit. + fn migration_weight_margin() -> Weight { + // Consider the weight of all steps + ::WeightInfo::migrate_dapps_success() + .max(::WeightInfo::migrate_ledger_success()) + .max(::WeightInfo::cleanup_old_storage_success()) + // and add the weight of updating migration status + .saturating_add(T::DbWeight::get().writes(1)) + } + } + + #[derive(PartialEq, Eq, Clone, Encode, Decode, Copy, TypeInfo, RuntimeDebug, MaxEncodedLen)] + pub enum MigrationState { + /// No migration in progress + NotInProgress, + /// In the middle of `RegisteredDApps` migration. + RegisteredDApps, + /// In the middle of `Ledgers` migration. + Ledgers, + /// In the middle of old v2 storage cleanup + Cleanup, + /// All migrations have been finished + Finished, + } + + impl Default for MigrationState { + fn default() -> Self { + MigrationState::NotInProgress + } + } + + pub struct DappStakingMigrationHandler(PhantomData); + impl frame_support::traits::OnRuntimeUpgrade for DappStakingMigrationHandler { + fn on_runtime_upgrade() -> Weight { + // When upgrade happens, we need to put dApp staking v3 into maintenance mode immediately. + // For the old pallet, since the storage cleanup is going to happen, maintenance mode must be ensured + // by the runtime config itself. + let mut consumed_weight = T::DbWeight::get().reads_writes(1, 2); + pallet_dapp_staking_v3::ActiveProtocolState::::mutate(|state| { + state.maintenance = true; + }); + + // Set the correct init storage version + pallet_dapp_staking_v3::STORAGE_VERSION.put::>(); + + // In case of try-runtime, we want to execute the whole logic, to ensure it works + // with on-chain data. + if cfg!(feature = "try-runtime") { + let mut steps = 0_u32; + while MigrationStateStorage::::get() != MigrationState::Finished { + match Pallet::::do_migrate(crate::Pallet::::max_call_weight()) { + Ok(weight) => { + consumed_weight.saturating_accrue(weight); + steps.saturating_inc(); + } + Err(_) => { + panic!("Must never happen since we check whether state is `Finished` before calling `do_migrate`."); + } + } + } + + log::trace!( + target: LOG_TARGET, + "dApp Staking migration finished after {} steps with total weight of {}.", + steps, + consumed_weight, + ); + + consumed_weight + } else { + consumed_weight + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + // Get dev accounts with registered dapps and their total reserved balance + let developers: Vec<_> = pallet_dapps_staking::RegisteredDapps::::iter() + .filter_map(|(smart_contract, info)| { + if info.state == pallet_dapps_staking::DAppState::Registered { + let reserved = + ::Currency::reserved_balance( + &info.developer, + ); + Some((info.developer, smart_contract, reserved)) + } else { + None + } + }) + .collect(); + + // Get the stakers and their active locked (staked) amount. + let stakers: Vec<_> = pallet_dapps_staking::Ledger::::iter() + .map(|(staker, ledger)| (staker, ledger.locked)) + .collect(); + + let helper = Helper:: { + developers, + stakers, + }; + + Ok(helper.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { + use sp_runtime::traits::Zero; + + let helper = Helper::::decode(&mut TrailingZeroInput::new(state.as_ref())) + .map_err(|_| "Cannot decode data from pre_upgrade")?; + + // 1. Ensure that all entries have been unregistered/removed and all dev accounts have been refunded. + // Also check that dApps have been registered in the new pallet. + assert!(pallet_dapps_staking::RegisteredDapps::::iter() + .count() + .is_zero()); + assert_eq!( + pallet_dapp_staking_v3::IntegratedDApps::::iter().count(), + helper.developers.len() + ); + + let register_deposit = ::RegisterDeposit::get(); + for (dev_account, smart_contract, old_reserved) in helper.developers { + let new_reserved = + ::Currency::reserved_balance(&dev_account); + assert_eq!(old_reserved, new_reserved + register_deposit); + + let new_smart_contract: ::SmartContract = + Decode::decode(&mut TrailingZeroInput::new( + smart_contract.encode().as_ref(), + )) + .expect("Must succeed since we're using the same underlying type."); + + let dapp_info = + pallet_dapp_staking_v3::IntegratedDApps::::get(&new_smart_contract) + .expect("Must exist!"); + assert_eq!(dapp_info.owner, dev_account); + } + + // 2. Ensure that all ledger entries have been migrated over to the new pallet. + // Total locked amount in the new pallet must equal the sum of all old locked amounts. + assert!(pallet_dapps_staking::Ledger::::iter().count().is_zero()); + assert_eq!( + pallet_dapp_staking_v3::Ledger::::iter().count(), + helper.stakers.len() + ); + + for (staker, old_locked) in &helper.stakers { + let new_locked = pallet_dapp_staking_v3::Ledger::::get(&staker).locked; + assert_eq!(*old_locked, new_locked); + } + + let total_locked = helper + .stakers + .iter() + .map(|(_, locked)| locked) + .sum::(); + assert_eq!( + pallet_dapp_staking_v3::CurrentEraInfo::::get().total_locked, + total_locked + ); + + // 3. Check that rest of the storage has been cleaned up. + assert!(!pallet_dapps_staking::PalletDisabled::::exists()); + assert!(!pallet_dapps_staking::CurrentEra::::exists()); + assert!(!pallet_dapps_staking::BlockRewardAccumulator::::exists()); + assert!(!pallet_dapps_staking::ForceEra::::exists()); + assert!(!pallet_dapps_staking::NextEraStartingBlock::::exists()); + assert!(!pallet_dapps_staking::StorageVersion::::exists()); + + assert!(pallet_dapps_staking::RegisteredDevelopers::::iter() + .count() + .is_zero()); + assert!(pallet_dapps_staking::GeneralEraInfo::::iter() + .count() + .is_zero()); + assert!(pallet_dapps_staking::ContractEraStake::::iter() + .count() + .is_zero()); + assert!(pallet_dapps_staking::GeneralStakerInfo::::iter() + .count() + .is_zero()); + + Ok(()) + } + } +} + +#[cfg(feature = "try-runtime")] +/// Used to help with `try-runtime` testing. +#[derive(Encode, Decode)] +struct Helper { + /// Vec of devs, with their associated smart contract & total reserved balance + developers: Vec<( + T::AccountId, + ::SmartContract, + Balance, + )>, + /// Stakers with their total active locked amount (not undergoing the unbonding process) + stakers: Vec<(T::AccountId, Balance)>, +} diff --git a/pallets/dapp-staking-migration/src/mock.rs b/pallets/dapp-staking-migration/src/mock.rs new file mode 100644 index 0000000000..062de7f1fa --- /dev/null +++ b/pallets/dapp-staking-migration/src/mock.rs @@ -0,0 +1,287 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::{self as pallet_dapp_staking_migration, *}; + +use frame_support::{ + assert_ok, construct_runtime, parameter_types, + traits::{fungible::Mutate as FunMutate, ConstBool, ConstU128, ConstU32, Currency}, + weights::Weight, + PalletId, +}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use sp_arithmetic::fixed_point::FixedU64; +use sp_core::H256; +use sp_io::TestExternalities; +use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; + +use astar_primitives::{ + dapp_staking::{CycleConfiguration, StakingRewardHandler}, + testing::Header, + Balance, BlockNumber, +}; + +pub(crate) type AccountId = u64; + +pub(crate) const EXISTENTIAL_DEPOSIT: Balance = 2; +pub(crate) const MINIMUM_LOCK_AMOUNT: Balance = 10; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub struct Test + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + DappStaking: pallet_dapp_staking_v3, + DappsStaking: pallet_dapps_staking, + DappStakingMigration: pallet_dapp_staking_migration, + } +); + +parameter_types! { + pub const BlockHashCount: BlockNumber = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type MaxLocks = ConstU32<4>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128; + type AccountStore = System; + type HoldIdentifier = (); + type FreezeIdentifier = RuntimeFreezeReason; + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<1>; + type WeightInfo = (); +} + +pub struct DummyPriceProvider; +impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { + fn average_price() -> FixedU64 { + FixedU64::from_rational(1, 10) + } +} + +pub struct DummyStakingRewardHandler; +impl StakingRewardHandler for DummyStakingRewardHandler { + fn staker_and_dapp_reward_pools(_total_staked_value: Balance) -> (Balance, Balance) { + ( + Balance::from(1_000_000_000_000_u128), + Balance::from(1_000_000_000_u128), + ) + } + + fn bonus_reward_pool() -> Balance { + Balance::from(3_000_000_u128) + } + + fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()> { + let _ = Balances::mint_into(beneficiary, reward); + Ok(()) + } +} + +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] +pub enum MockSmartContract { + Wasm(AccountId), + Other(AccountId), +} + +impl Default for MockSmartContract { + fn default() -> Self { + MockSmartContract::Wasm(1) + } +} + +#[cfg(feature = "runtime-benchmarks")] +pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); +#[cfg(feature = "runtime-benchmarks")] +impl pallet_dapp_staking_v3::BenchmarkHelper + for BenchmarkHelper +{ + fn get_smart_contract(id: u32) -> MockSmartContract { + MockSmartContract::Wasm(id as AccountId) + } + + fn set_balance(account: &AccountId, amount: Balance) { + use frame_support::traits::fungible::Unbalanced as FunUnbalanced; + Balances::write_balance(account, amount) + .expect("Must succeed in test/benchmark environment."); + } +} + +pub struct DummyCycleConfiguration; +impl CycleConfiguration for DummyCycleConfiguration { + fn periods_per_cycle() -> u32 { + 4 + } + + fn eras_per_voting_subperiod() -> u32 { + 8 + } + + fn eras_per_build_and_earn_subperiod() -> u32 { + 16 + } + + fn blocks_per_era() -> u32 { + 10 + } +} + +impl pallet_dapp_staking_v3::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type Currency = Balances; + type SmartContract = MockSmartContract; + type ManagerOrigin = frame_system::EnsureRoot; + type NativePriceProvider = DummyPriceProvider; + type StakingRewardHandler = DummyStakingRewardHandler; + type CycleConfiguration = DummyCycleConfiguration; + type EraRewardSpanLength = ConstU32<8>; + type RewardRetentionInPeriods = ConstU32<2>; + type MaxNumberOfContracts = ConstU32<10>; + type MaxUnlockingChunks = ConstU32<5>; + type MinimumLockedAmount = ConstU128; + type UnlockingPeriod = ConstU32<2>; + type MaxNumberOfStakedContracts = ConstU32<5>; + type MinimumStakeAmount = ConstU128<3>; + type NumberOfTiers = ConstU32<4>; + type WeightInfo = pallet_dapp_staking_v3::weights::SubstrateWeight; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = BenchmarkHelper; +} + +parameter_types! { + pub const DappsStakingPalletId: PalletId = PalletId(*b"mokdpstk"); +} + +impl pallet_dapps_staking::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type BlockPerEra = ConstU32<10>; + type RegisterDeposit = ConstU128<100>; + type SmartContract = MockSmartContract; + type WeightInfo = pallet_dapps_staking::weights::SubstrateWeight; + type MaxNumberOfStakersPerContract = ConstU32<10>; + type MinimumStakingAmount = ConstU128; + type PalletId = DappsStakingPalletId; + type MinimumRemainingAmount = ConstU128<1>; + type MaxUnlockingChunks = ConstU32<5>; + type UnbondingPeriod = ConstU32<3>; + type MaxEraStakeValues = ConstU32<10>; + type UnregisteredDappRewardRetention = ConstU32<10>; + type ForcePalletDisabled = ConstBool; +} + +impl pallet_dapp_staking_migration::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = crate::weights::SubstrateWeight; +} + +pub struct ExtBuilder; +impl ExtBuilder { + pub fn build() -> TestExternalities { + // Normal behavior is for reward payout to succeed + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let balances = vec![1000; 9] + .into_iter() + .enumerate() + .map(|(idx, amount)| (idx as u64 + 1, amount)) + .collect(); + + pallet_balances::GenesisConfig:: { balances: balances } + .assimilate_storage(&mut storage) + .ok(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| { + System::set_block_number(1); + }); + + ext + } +} + +/// Initialize old dApps staking storage. +/// +/// This is kept outside of the test ext creation since the same mock is reused +/// in the benchmarks code. +pub fn init() { + let dapps_number = 10_u32; + let staker = dapps_number.into(); + Balances::make_free_balance_be(&staker, 1_000_000_000_000_000_000); + + // Add some dummy dApps to the old pallet & stake on them. + for idx in 0..dapps_number { + let developer = idx.into(); + Balances::make_free_balance_be(&developer, 1_000_000_000_000); + let smart_contract = MockSmartContract::Wasm(idx.into()); + assert_ok!(pallet_dapps_staking::Pallet::::register( + RawOrigin::Root.into(), + developer, + smart_contract.clone(), + )); + assert_ok!(pallet_dapps_staking::Pallet::::bond_and_stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract, + 1_000, + )); + } +} diff --git a/pallets/dapp-staking-migration/src/tests.rs b/pallets/dapp-staking-migration/src/tests.rs new file mode 100644 index 0000000000..7a4776e1c7 --- /dev/null +++ b/pallets/dapp-staking-migration/src/tests.rs @@ -0,0 +1,177 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::mock::*; +use crate::*; + +use frame_support::{assert_ok, assert_storage_noop}; +use sp_runtime::traits::Zero; + +#[test] +fn sanity_check() { + ExtBuilder::build().execute_with(|| { + assert!(DappStakingMigration::max_call_weight() + .all_gte(DappStakingMigration::min_call_weight())); + }); +} + +#[test] +fn migrate_dapps_check() { + ExtBuilder::build().execute_with(|| { + init(); + + // Cleanup single entry, check pre and post states + let init_old_count = pallet_dapps_staking::RegisteredDapps::::iter().count(); + assert!(init_old_count > 0, "Sanity check."); + + let init_new_count = pallet_dapp_staking_v3::IntegratedDApps::::iter().count(); + assert!(init_new_count.is_zero(), "Sanity check."); + + assert_eq!( + DappStakingMigration::migrate_dapps(), + Ok(::WeightInfo::migrate_dapps_success()) + ); + assert_eq!( + init_old_count, + pallet_dapps_staking::RegisteredDapps::::iter().count() + 1, + "One entry should have been cleaned up." + ); + assert_eq!( + pallet_dapp_staking_v3::IntegratedDApps::::iter().count(), + 1, + "Single new entry should have been added." + ); + + // Cleanup the remaining entries. + for _ in 1..init_old_count { + assert_eq!( + DappStakingMigration::migrate_dapps(), + Ok(::WeightInfo::migrate_dapps_success()) + ); + } + + // Further calls should result in Err + assert_eq!( + DappStakingMigration::migrate_dapps(), + Err(::WeightInfo::migrate_dapps_noop()) + ); + }); +} + +#[test] +fn migrate_ledgers_check() { + ExtBuilder::build().execute_with(|| { + init(); + + // Cleanup all enries, check pre and post states. + let init_old_count = pallet_dapps_staking::Ledger::::iter().count(); + assert!(init_old_count > 0, "Sanity check."); + + let init_new_count = pallet_dapp_staking_v3::Ledger::::iter().count(); + assert!(init_new_count.is_zero(), "Sanity check."); + + assert!(pallet_dapp_staking_v3::CurrentEraInfo::::get() + .total_locked + .is_zero()); + + for x in 0..init_old_count { + assert_eq!( + DappStakingMigration::migrate_ledger(), + Ok(::WeightInfo::migrate_ledger_success()) + ); + + assert_eq!( + init_old_count - x - 1, + pallet_dapps_staking::Ledger::::iter().count(), + "One entry should have been cleaned up." + ); + assert_eq!( + x + 1, + pallet_dapp_staking_v3::Ledger::::iter().count(), + "Single new entry should have been added." + ); + assert!(pallet_dapp_staking_v3::CurrentEraInfo::::get().total_locked > 0); + } + + // Further calls should result in Err + assert_eq!( + DappStakingMigration::migrate_ledger(), + Err(::WeightInfo::migrate_ledger_noop()) + ); + }); +} + +// TODO: this doesn't work since clear_prefix doesn't work in tests for some reason. +#[ignore] +#[test] +fn storage_cleanup_check() { + let mut ext = ExtBuilder::build(); + assert_ok!(ext.commit_all()); + + ext.execute_with(|| { + init(); + + let init_count = (pallet_dapps_staking::RegisteredDapps::::iter().count() + + pallet_dapps_staking::Ledger::::iter().count()) as u32; + + for _ in 0..init_count { + assert_ok!(DappStakingMigration::cleanup_old_storage(init_count)); + } + }); +} + +#[test] +fn migrate_call_works() { + ExtBuilder::build().execute_with(|| { + init(); + let account = 1; + + // Call enough times to clean everything up. + while MigrationStateStorage::::get() != MigrationState::Finished { + assert_ok!(DappStakingMigration::migrate( + frame_system::RawOrigin::Signed(account).into(), + Some(Weight::from_parts(1, 1)) + )); + + assert!( + pallet_dapp_staking_v3::ActiveProtocolState::::get().maintenance, + "Maintenance must always be returned after migrate call finishes." + ); + } + + // Check post-state + assert!(pallet_dapps_staking::RegisteredDapps::::iter() + .count() + .is_zero()); + assert!(pallet_dapps_staking::Ledger::::iter() + .count() + .is_zero()); + assert!(pallet_dapps_staking::RegisteredDevelopers::::iter() + .count() + .is_zero()); + assert!(pallet_dapps_staking::GeneralEraInfo::::iter() + .count() + .is_zero()); + + // Migrate call can still be called, but it shouldn't have any effect. + assert_storage_noop!(assert_ok!(DappStakingMigration::migrate( + frame_system::RawOrigin::Signed(account).into(), + None + ))); + }); +} diff --git a/pallets/dapp-staking-migration/src/weights.rs b/pallets/dapp-staking-migration/src/weights.rs new file mode 100644 index 0000000000..158adad022 --- /dev/null +++ b/pallets/dapp-staking-migration/src/weights.rs @@ -0,0 +1,219 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for pallet_dapp_staking_migration +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-12-13, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Dinos-MacBook-Pro.local`, CPU: `` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_dapp_staking_migration +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=weights.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_dapp_staking_migration. +pub trait WeightInfo { + fn migrate_dapps_success() -> Weight; + fn migrate_dapps_noop() -> Weight; + fn migrate_ledger_success() -> Weight; + fn migrate_ledger_noop() -> Weight; + fn cleanup_old_storage_success() -> Weight; + fn cleanup_old_storage_noop() -> Weight; +} + +/// Weights for pallet_dapp_staking_migration using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: DappsStaking RegisteredDapps (r:2 w:1) + /// Proof: DappsStaking RegisteredDapps (max_values: None, max_size: Some(86), added: 2561, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:1) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking NextDAppId (r:1 w:1) + /// Proof: DappStaking NextDAppId (max_values: Some(1), max_size: Some(2), added: 497, mode: MaxEncodedLen) + fn migrate_dapps_success() -> Weight { + // Proof Size summary in bytes: + // Measured: `147` + // Estimated: `6112` + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(45_000_000, 6112) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappsStaking RegisteredDapps (r:1 w:0) + /// Proof: DappsStaking RegisteredDapps (max_values: None, max_size: Some(86), added: 2561, mode: MaxEncodedLen) + fn migrate_dapps_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3551` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(4_000_000, 3551) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: DappsStaking Ledger (r:2 w:1) + /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn migrate_ledger_success() -> Weight { + // Proof Size summary in bytes: + // Measured: `136` + // Estimated: `6472` + // Minimum execution time: 67_000_000 picoseconds. + Weight::from_parts(68_000_000, 6472) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappsStaking Ledger (r:1 w:0) + /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) + fn migrate_ledger_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3731` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(3_000_000, 3731) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: unknown `0xc0d3d54ea9961b06a7139c5a75c15c4f` (r:1 w:1) + /// Proof Skipped: unknown `0xc0d3d54ea9961b06a7139c5a75c15c4f` (r:1 w:1) + fn cleanup_old_storage_success() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3465` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(3_000_000, 3465) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn cleanup_old_storage_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_000_000 picoseconds. + Weight::from_parts(2_000_000, 0) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: DappsStaking RegisteredDapps (r:2 w:1) + /// Proof: DappsStaking RegisteredDapps (max_values: None, max_size: Some(86), added: 2561, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:1) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking NextDAppId (r:1 w:1) + /// Proof: DappStaking NextDAppId (max_values: Some(1), max_size: Some(2), added: 497, mode: MaxEncodedLen) + fn migrate_dapps_success() -> Weight { + // Proof Size summary in bytes: + // Measured: `147` + // Estimated: `6112` + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(45_000_000, 6112) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: DappsStaking RegisteredDapps (r:1 w:0) + /// Proof: DappsStaking RegisteredDapps (max_values: None, max_size: Some(86), added: 2561, mode: MaxEncodedLen) + fn migrate_dapps_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3551` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(4_000_000, 3551) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: DappsStaking Ledger (r:2 w:1) + /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn migrate_ledger_success() -> Weight { + // Proof Size summary in bytes: + // Measured: `136` + // Estimated: `6472` + // Minimum execution time: 67_000_000 picoseconds. + Weight::from_parts(68_000_000, 6472) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: DappsStaking Ledger (r:1 w:0) + /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) + fn migrate_ledger_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3731` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(3_000_000, 3731) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: unknown `0xc0d3d54ea9961b06a7139c5a75c15c4f` (r:1 w:1) + /// Proof Skipped: unknown `0xc0d3d54ea9961b06a7139c5a75c15c4f` (r:1 w:1) + fn cleanup_old_storage_success() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3465` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(3_000_000, 3465) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + fn cleanup_old_storage_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_000_000 picoseconds. + Weight::from_parts(2_000_000, 0) + } +} diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 43ab418e4a..715b4df674 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -86,7 +86,7 @@ pub mod pallet { use super::*; /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] diff --git a/pallets/dapps-staking/src/lib.rs b/pallets/dapps-staking/src/lib.rs index d9da78d259..8ad9ae89c0 100644 --- a/pallets/dapps-staking/src/lib.rs +++ b/pallets/dapps-staking/src/lib.rs @@ -111,7 +111,7 @@ const MAX_ASSUMED_VEC_LEN: u32 = 10; /// DApp State descriptor #[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] -enum DAppState { +pub enum DAppState { /// Contract is registered and active. Registered, /// Contract has been unregistered and is inactive. @@ -122,9 +122,9 @@ enum DAppState { #[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct DAppInfo { /// Developer (owner) account - developer: AccountId, + pub developer: AccountId, /// Current DApp State - state: DAppState, + pub state: DAppState, } impl DAppInfo { @@ -137,7 +137,7 @@ impl DAppInfo { } /// `true` if dApp has been unregistered, `false` otherwise - fn is_unregistered(&self) -> bool { + pub fn is_unregistered(&self) -> bool { matches!(self.state, DAppState::Unregistered(_)) } } @@ -475,7 +475,7 @@ impl UnbondingInfo { } /// Returns sum of all unlocking chunks. - fn sum(&self) -> Balance { + pub fn sum(&self) -> Balance { self.unlocking_chunks .iter() .map(|chunk| chunk.amount) @@ -551,7 +551,7 @@ pub struct AccountLedger { #[codec(compact)] pub locked: Balance, /// Information about unbonding chunks. - unbonding_info: UnbondingInfo, + pub unbonding_info: UnbondingInfo, /// Instruction on how to handle reward payout reward_destination: RewardDestination, } diff --git a/pallets/dapps-staking/src/mock.rs b/pallets/dapps-staking/src/mock.rs index bdc6a0e532..3ecb3e0a3b 100644 --- a/pallets/dapps-staking/src/mock.rs +++ b/pallets/dapps-staking/src/mock.rs @@ -30,7 +30,7 @@ use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use sp_io::TestExternalities; use sp_runtime::{ testing::Header, - traits::{BlakeTwo256, ConstU32, IdentityLookup}, + traits::{BlakeTwo256, ConstBool, ConstU32, IdentityLookup}, }; pub(crate) type AccountId = u64; @@ -166,6 +166,7 @@ impl pallet_dapps_staking::Config for TestRuntime { type UnbondingPeriod = UnbondingPeriod; type MaxEraStakeValues = MaxEraStakeValues; type UnregisteredDappRewardRetention = ConstU32; + type ForcePalletDisabled = ConstBool; } #[derive( diff --git a/pallets/dapps-staking/src/pallet/mod.rs b/pallets/dapps-staking/src/pallet/mod.rs index c2a4ac2d18..c7d46591c1 100644 --- a/pallets/dapps-staking/src/pallet/mod.rs +++ b/pallets/dapps-staking/src/pallet/mod.rs @@ -37,7 +37,7 @@ use sp_runtime::{ }; use sp_std::{convert::From, mem}; -const STAKING_ID: LockIdentifier = *b"dapstake"; +pub const STAKING_ID: LockIdentifier = *b"dapstake"; #[frame_support::pallet] #[allow(clippy::module_inception)] @@ -115,6 +115,10 @@ pub mod pallet { #[pallet::constant] type UnregisteredDappRewardRetention: Get; + /// Can be used to force pallet into permanent maintenance mode. + #[pallet::constant] + type ForcePalletDisabled: Get; + /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; @@ -165,13 +169,13 @@ pub mod pallet { /// Simple map where developer account points to their smart contract #[pallet::storage] #[pallet::getter(fn registered_contract)] - pub(crate) type RegisteredDevelopers = + pub type RegisteredDevelopers = StorageMap<_, Blake2_128Concat, T::AccountId, T::SmartContract>; /// Simple map where smart contract points to basic info about it (e.g. developer address, state) #[pallet::storage] #[pallet::getter(fn dapp_info)] - pub(crate) type RegisteredDapps = + pub type RegisteredDapps = StorageMap<_, Blake2_128Concat, T::SmartContract, DAppInfo>; /// General information about an era like TVL, total staked value, rewards. @@ -207,7 +211,7 @@ pub mod pallet { /// Stores the current pallet storage version. #[pallet::storage] #[pallet::getter(fn storage_version)] - pub(crate) type StorageVersion = StorageValue<_, Version, ValueQuery>; + pub type StorageVersion = StorageValue<_, Version, ValueQuery>; #[pallet::event] #[pallet::generate_deposit(pub(crate) fn deposit_event)] @@ -1135,7 +1139,7 @@ pub mod pallet { /// `Err` if pallet disabled for maintenance, `Ok` otherwise pub fn ensure_pallet_enabled() -> Result<(), Error> { - if PalletDisabled::::get() { + if PalletDisabled::::get() || T::ForcePalletDisabled::get() { Err(Error::::Disabled) } else { Ok(()) diff --git a/precompiles/dapps-staking/src/mock.rs b/precompiles/dapps-staking/src/mock.rs index 976f7fd948..c6fe131511 100644 --- a/precompiles/dapps-staking/src/mock.rs +++ b/precompiles/dapps-staking/src/mock.rs @@ -21,7 +21,7 @@ use super::*; use fp_evm::IsPrecompileResult; use frame_support::{ construct_runtime, parameter_types, - traits::{ConstU64, Currency, OnFinalize, OnInitialize}, + traits::{ConstBool, ConstU64, Currency, OnFinalize, OnInitialize}, weights::{RuntimeDbWeight, Weight}, PalletId, }; @@ -307,6 +307,7 @@ impl pallet_dapps_staking::Config for TestRuntime { type UnbondingPeriod = UnbondingPeriod; type MaxEraStakeValues = MaxEraStakeValues; type UnregisteredDappRewardRetention = ConstU32<2>; + type ForcePalletDisabled = ConstBool; } pub struct ExternalityBuilder { diff --git a/runtime/astar/src/lib.rs b/runtime/astar/src/lib.rs index 7416768745..d18a4ed7e5 100644 --- a/runtime/astar/src/lib.rs +++ b/runtime/astar/src/lib.rs @@ -336,6 +336,7 @@ impl pallet_dapps_staking::Config for Runtime { type MaxEraStakeValues = MaxEraStakeValues; // Not allowed on Astar yet type UnregisteredDappRewardRetention = ConstU32<{ u32::MAX }>; + type ForcePalletDisabled = ConstBool; } /// Multi-VM pointer to smart contract instance. diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index 3a258c29c8..b881475456 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -70,6 +70,7 @@ pallet-block-rewards-hybrid = { workspace = true } pallet-chain-extension-dapps-staking = { workspace = true } pallet-chain-extension-unified-accounts = { workspace = true } pallet-chain-extension-xvm = { workspace = true } +pallet-dapp-staking-migration = { workspace = true } pallet-dapp-staking-v3 = { workspace = true } pallet-dapps-staking = { workspace = true } pallet-dynamic-evm-base-fee = { workspace = true } @@ -124,6 +125,7 @@ std = [ "pallet-chain-extension-unified-accounts/std", "pallet-dapps-staking/std", "pallet-dapp-staking-v3/std", + "pallet-dapp-staking-migration/std", "dapp-staking-v3-runtime-api/std", "pallet-inflation/std", "pallet-dynamic-evm-base-fee/std", @@ -199,6 +201,7 @@ runtime-benchmarks = [ "pallet-dapp-staking-v3/runtime-benchmarks", "pallet-inflation/runtime-benchmarks", "pallet-dynamic-evm-base-fee/runtime-benchmarks", + "pallet-dapp-staking-migration/runtime-benchmarks", ] try-runtime = [ "fp-self-contained/try-runtime", @@ -234,6 +237,7 @@ try-runtime = [ "pallet-dynamic-evm-base-fee/try-runtime", "pallet-evm/try-runtime", "pallet-ethereum-checked/try-runtime", + "pallet-dapp-staking-migration/try-runtime", ] evm-tracing = [ "moonbeam-evm-tracer", diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 288c5cf6da..ac63f7d50e 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -479,6 +479,7 @@ impl pallet_dapps_staking::Config for Runtime { type MinimumRemainingAmount = MinimumRemainingAmount; type MaxEraStakeValues = MaxEraStakeValues; type UnregisteredDappRewardRetention = ConstU32<3>; + type ForcePalletDisabled = ConstBool; // This will be set to `true` when needed } /// Multi-VM pointer to smart contract instance. @@ -589,6 +590,11 @@ impl pallet_inflation::Config for Runtime { type WeightInfo = pallet_inflation::weights::SubstrateWeight; } +impl pallet_dapp_staking_migration::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_dapp_staking_migration::weights::SubstrateWeight; +} + impl pallet_utility::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; @@ -1116,6 +1122,7 @@ construct_runtime!( Vesting: pallet_vesting, DappsStaking: pallet_dapps_staking, DappStaking: pallet_dapp_staking_v3, + DappStakingMigration: pallet_dapp_staking_migration, Inflation: pallet_inflation, BlockReward: pallet_block_rewards_hybrid, TransactionPayment: pallet_transaction_payment, @@ -1169,8 +1176,12 @@ pub type Executive = frame_executive::Executive< frame_system::ChainContext, Runtime, AllPalletsWithSystem, + Migrations, >; +// TODO: remove this prior to the PR merge +pub type Migrations = (pallet_dapp_staking_migration::DappStakingMigrationHandler,); + type EventRecord = frame_system::EventRecord< ::RuntimeEvent, ::Hash, @@ -1250,6 +1261,7 @@ mod benches { [pallet_block_rewards_hybrid, BlockReward] [pallet_ethereum_checked, EthereumChecked] [pallet_dapp_staking_v3, DappStaking] + [pallet_dapp_staking_migration, DappStakingMigration] [pallet_inflation, Inflation] [pallet_dynamic_evm_base_fee, DynamicEvmBaseFee] ); diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index 2f80464e8d..b4f357345b 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -402,6 +402,7 @@ impl pallet_dapps_staking::Config for Runtime { type MinimumRemainingAmount = MinimumRemainingAmount; type MaxEraStakeValues = MaxEraStakeValues; type UnregisteredDappRewardRetention = ConstU32<10>; + type ForcePalletDisabled = ConstBool; } /// Multi-VM pointer to smart contract instance. diff --git a/runtime/shiden/src/lib.rs b/runtime/shiden/src/lib.rs index 52e7134b23..e4616704e9 100644 --- a/runtime/shiden/src/lib.rs +++ b/runtime/shiden/src/lib.rs @@ -339,6 +339,7 @@ impl pallet_dapps_staking::Config for Runtime { type UnbondingPeriod = UnbondingPeriod; type MaxEraStakeValues = MaxEraStakeValues; type UnregisteredDappRewardRetention = ConstU32<7>; + type ForcePalletDisabled = ConstBool; } /// Multi-VM pointer to smart contract instance. diff --git a/tests/xcm-simulator/src/mocks/parachain.rs b/tests/xcm-simulator/src/mocks/parachain.rs index 31e9602b0a..bf67fff877 100644 --- a/tests/xcm-simulator/src/mocks/parachain.rs +++ b/tests/xcm-simulator/src/mocks/parachain.rs @@ -23,8 +23,8 @@ use frame_support::{ dispatch::DispatchClass, match_types, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Contains, Currency, Everything, - Imbalance, InstanceFilter, Nothing, OnUnbalanced, + AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, Contains, Currency, + Everything, Imbalance, InstanceFilter, Nothing, OnUnbalanced, }, weights::{ constants::{BlockExecutionWeight, ExtrinsicBaseWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -37,7 +37,7 @@ use frame_system::{ EnsureRoot, EnsureSigned, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; -use sp_core::{ConstBool, H256}; +use sp_core::H256; use sp_runtime::{ testing::Header, traits::{AccountIdConversion, Convert, Get, IdentityLookup}, @@ -311,6 +311,7 @@ impl pallet_dapps_staking::Config for Runtime { type UnbondingPeriod = ConstU32<2>; type MaxEraStakeValues = ConstU32<4>; type UnregisteredDappRewardRetention = ConstU32<7>; + type ForcePalletDisabled = ConstBool; } /// The type used to represent the kinds of proxying allowed. From 808686f8f7ee6f3f4c81f46971ab9af61127038d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Tue, 19 Dec 2023 14:22:22 +0100 Subject: [PATCH 12/14] Feat/dapp staking v3 precompiles (#1096) * Init commit * All legacy calls covered * v2 interface first definition * Rename * Resolve merge errors * TODO * v2 init implementation * Prepared mock * todo * Migration to v2 utils * More adjustments * Finish adapting implementation to v2 * Mock & fixes * Primitive smart contract * Fix dsv3 test * Prepare impl & mock * Remove redundant code, adjust mock & prepare tests * Tests & utils * Test for legacy getters/view functions * More legacy tests * v1 interface covered with tests * Minor refactor, organization, improvements * v2 tests * Cleanup TODOs * More tests * Updates * docs * Fixes * Address review comments * Adjustments * Audit comments * Fix mock * FMT * Review comments --- Cargo.lock | 31 +- Cargo.toml | 1 + pallets/dapp-staking-migration/src/mock.rs | 15 +- pallets/dapp-staking-v3/README.md | 3 +- pallets/dapp-staking-v3/src/lib.rs | 24 +- pallets/dapp-staking-v3/src/test/mock.rs | 17 +- .../dapp-staking-v3/src/test/testing_utils.rs | 6 +- pallets/dapp-staking-v3/src/test/tests.rs | 100 +- pallets/dapp-staking-v3/src/types.rs | 6 + precompiles/dapp-staking-v3/Cargo.toml | 62 ++ .../dapp-staking-v3/DappsStakingV1.sol | 98 ++ .../dapp-staking-v3/DappsStakingV2.sol | 92 ++ precompiles/dapp-staking-v3/src/lib.rs | 832 +++++++++++++++++ precompiles/dapp-staking-v3/src/test/mock.rs | 461 ++++++++++ precompiles/dapp-staking-v3/src/test/mod.rs | 22 + .../dapp-staking-v3/src/test/tests_v1.rs | 867 ++++++++++++++++++ .../dapp-staking-v3/src/test/tests_v2.rs | 526 +++++++++++ precompiles/dapp-staking-v3/src/test/types.rs | 154 ++++ primitives/src/dapp_staking.rs | 53 ++ runtime/local/Cargo.toml | 4 +- runtime/local/src/chain_extensions.rs | 4 - runtime/local/src/lib.rs | 27 +- runtime/local/src/precompiles.rs | 4 +- runtime/shibuya/Cargo.toml | 2 - runtime/shibuya/src/chain_extensions.rs | 5 - runtime/shibuya/src/lib.rs | 1 - 26 files changed, 3291 insertions(+), 126 deletions(-) create mode 100644 precompiles/dapp-staking-v3/Cargo.toml create mode 100644 precompiles/dapp-staking-v3/DappsStakingV1.sol create mode 100644 precompiles/dapp-staking-v3/DappsStakingV2.sol create mode 100644 precompiles/dapp-staking-v3/src/lib.rs create mode 100644 precompiles/dapp-staking-v3/src/test/mock.rs create mode 100644 precompiles/dapp-staking-v3/src/test/mod.rs create mode 100644 precompiles/dapp-staking-v3/src/test/tests_v1.rs create mode 100644 precompiles/dapp-staking-v3/src/test/tests_v2.rs create mode 100644 precompiles/dapp-staking-v3/src/test/types.rs diff --git a/Cargo.lock b/Cargo.lock index a7674adddf..452564a7c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6057,7 +6057,7 @@ dependencies = [ "pallet-evm-precompile-assets-erc20", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", - "pallet-evm-precompile-dapps-staking", + "pallet-evm-precompile-dapp-staking-v3", "pallet-evm-precompile-dispatch", "pallet-evm-precompile-ed25519", "pallet-evm-precompile-modexp", @@ -7970,6 +7970,34 @@ dependencies = [ "substrate-bn", ] +[[package]] +name = "pallet-evm-precompile-dapp-staking-v3" +version = "0.1.0" +dependencies = [ + "assert_matches", + "astar-primitives", + "derive_more", + "fp-evm", + "frame-support", + "frame-system", + "log", + "num_enum 0.5.11", + "pallet-balances", + "pallet-dapp-staking-v3", + "pallet-evm", + "pallet-timestamp", + "parity-scale-codec", + "precompile-utils", + "scale-info", + "serde", + "sha3", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-evm-precompile-dapps-staking" version = "3.6.3" @@ -13173,7 +13201,6 @@ dependencies = [ "pallet-balances", "pallet-block-rewards-hybrid", "pallet-chain-extension-assets", - "pallet-chain-extension-dapps-staking", "pallet-chain-extension-unified-accounts", "pallet-chain-extension-xvm", "pallet-collator-selection", diff --git a/Cargo.toml b/Cargo.toml index 41a2dead91..ca009583f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -295,6 +295,7 @@ pallet-evm-precompile-substrate-ecdsa = { path = "./precompiles/substrate-ecdsa" pallet-evm-precompile-xcm = { path = "./precompiles/xcm", default-features = false } pallet-evm-precompile-xvm = { path = "./precompiles/xvm", default-features = false } pallet-evm-precompile-dapps-staking = { path = "./precompiles/dapps-staking", default-features = false } +pallet-evm-precompile-dapp-staking-v3 = { path = "./precompiles/dapp-staking-v3", default-features = false } pallet-evm-precompile-unified-accounts = { path = "./precompiles/unified-accounts", default-features = false } pallet-chain-extension-dapps-staking = { path = "./chain-extensions/dapps-staking", default-features = false } diff --git a/pallets/dapp-staking-migration/src/mock.rs b/pallets/dapp-staking-migration/src/mock.rs index 062de7f1fa..c33aa09620 100644 --- a/pallets/dapp-staking-migration/src/mock.rs +++ b/pallets/dapp-staking-migration/src/mock.rs @@ -24,14 +24,13 @@ use frame_support::{ weights::Weight, PalletId, }; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use sp_arithmetic::fixed_point::FixedU64; use sp_core::H256; use sp_io::TestExternalities; use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; use astar_primitives::{ - dapp_staking::{CycleConfiguration, StakingRewardHandler}, + dapp_staking::{CycleConfiguration, SmartContract, StakingRewardHandler}, testing::Header, Balance, BlockNumber, }; @@ -134,17 +133,7 @@ impl StakingRewardHandler for DummyStakingRewardHandler { } } -#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] -pub enum MockSmartContract { - Wasm(AccountId), - Other(AccountId), -} - -impl Default for MockSmartContract { - fn default() -> Self { - MockSmartContract::Wasm(1) - } -} +pub(crate) type MockSmartContract = SmartContract; #[cfg(feature = "runtime-benchmarks")] pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md index c8de9b0036..48a5b531aa 100644 --- a/pallets/dapp-staking-v3/README.md +++ b/pallets/dapp-staking-v3/README.md @@ -227,7 +227,8 @@ be left out of tiers and won't earn **any** reward. In a special and unlikely case that two or more dApps have the exact same score and satisfy tier entry threshold, but there isn't enough leftover tier capacity to accomodate them all, this is considered _undefined_ behavior. Some of the dApps will manage to enter the tier, while others will be left out. There is no strict rule which defines this behavior - instead dApps are encouraged to ensure their tier entry by -having a larger stake than the other dApp(s). +having a larger stake than the other dApp(s). Tehnically, at the moment, the dApp with the lower `dApp Id` will have the advantage over a dApp with +the larger Id. ### Reward Expiry diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 715b4df674..a99b36af95 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -51,7 +51,7 @@ use sp_runtime::{ pub use sp_std::vec::Vec; use astar_primitives::{ - dapp_staking::{CycleConfiguration, StakingRewardHandler}, + dapp_staking::{CycleConfiguration, SmartContractHandle, StakingRewardHandler}, Balance, BlockNumber, }; @@ -118,7 +118,10 @@ pub mod pallet { >; /// Describes smart contract in the context required by dApp staking. - type SmartContract: Parameter + Member + MaxEncodedLen; + type SmartContract: Parameter + + Member + + MaxEncodedLen + + SmartContractHandle; /// Privileged origin for managing dApp staking pallet. type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; @@ -795,7 +798,7 @@ pub mod pallet { ledger.subtract_lock_amount(amount_to_unlock); let current_block = frame_system::Pallet::::block_number(); - let unlock_block = current_block.saturating_add(Self::unlock_period()); + let unlock_block = current_block.saturating_add(Self::unlocking_period()); ledger .add_unlocking_chunk(amount_to_unlock, unlock_block) .map_err(|_| Error::::TooManyUnlockingChunks)?; @@ -1137,8 +1140,9 @@ pub mod pallet { let earliest_staked_era = ledger .earliest_staked_era() .ok_or(Error::::InternalClaimStakerError)?; - let era_rewards = EraRewards::::get(Self::era_reward_index(earliest_staked_era)) - .ok_or(Error::::NoClaimableRewards)?; + let era_rewards = + EraRewards::::get(Self::era_reward_span_index(earliest_staked_era)) + .ok_or(Error::::NoClaimableRewards)?; // The last era for which we can theoretically claim rewards. // And indicator if we know the period's ending era. @@ -1545,7 +1549,7 @@ pub mod pallet { } /// Calculates the `EraRewardSpan` index for the specified era. - pub(crate) fn era_reward_index(era: EraNumber) -> EraNumber { + pub fn era_reward_span_index(era: EraNumber) -> EraNumber { era.saturating_sub(era % T::EraRewardSpanLength::get()) } @@ -1556,7 +1560,7 @@ pub mod pallet { } /// Unlocking period expressed in the number of blocks. - pub(crate) fn unlock_period() -> BlockNumber { + pub fn unlocking_period() -> BlockNumber { T::CycleConfiguration::blocks_per_era().saturating_mul(T::UnlockingPeriod::get().into()) } @@ -1655,7 +1659,9 @@ pub mod pallet { // In case when tier has 1 more free slot, but two dApps with exactly same score satisfy the threshold, // one of them will be assigned to the tier, and the other one will be assigned to the lower tier, if it exists. // - // There is no explicit definition of which dApp gets the advantage - it's decided by dApp IDs hash & the unstable sort algorithm. + // In the current implementation, the dApp with the lower dApp Id has the advantage. + // There is no guarantee this will persist in the future, so it's best for dApps to do their + // best to avoid getting themselves into such situations. // 4. Calculate rewards. let tier_rewards = tier_config @@ -1842,7 +1848,7 @@ pub mod pallet { CurrentEraInfo::::put(era_info); - let era_span_index = Self::era_reward_index(current_era); + let era_span_index = Self::era_reward_span_index(current_era); let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); if let Err(_) = span.push(current_era, era_reward) { // This must never happen but we log the error just in case. diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 8be66cd86a..3f116c6bbc 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -30,7 +30,6 @@ use frame_support::{ }, weights::Weight, }; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use sp_arithmetic::fixed_point::FixedU64; use sp_core::H256; use sp_io::TestExternalities; @@ -40,7 +39,7 @@ use sp_runtime::{ }; use sp_std::cell::RefCell; -use astar_primitives::{testing::Header, Balance, BlockNumber}; +use astar_primitives::{dapp_staking::SmartContract, testing::Header, Balance, BlockNumber}; pub(crate) type AccountId = u64; @@ -146,17 +145,7 @@ impl StakingRewardHandler for DummyStakingRewardHandler { } } -#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] -pub enum MockSmartContract { - Wasm(AccountId), - Other(AccountId), -} - -impl Default for MockSmartContract { - fn default() -> Self { - MockSmartContract::Wasm(1) - } -} +pub(crate) type MockSmartContract = SmartContract; #[cfg(feature = "runtime-benchmarks")] pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); @@ -165,7 +154,7 @@ impl crate::BenchmarkHelper for BenchmarkHelper { fn get_smart_contract(id: u32) -> MockSmartContract { - MockSmartContract::Wasm(id as AccountId) + MockSmartContract::wasm(id as AccountId) } fn set_balance(account: &AccountId, amount: Balance) { diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 29cfd9e40d..a973bba7ac 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -1288,7 +1288,7 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { } // 4. Verify era reward - let era_span_index = DappStaking::era_reward_index(pre_protoc_state.era); + let era_span_index = DappStaking::era_reward_span_index(pre_protoc_state.era); let maybe_pre_era_reward_span = pre_snapshot.era_rewards.get(&era_span_index); let post_era_reward_span = post_snapshot .era_rewards @@ -1467,10 +1467,10 @@ pub(crate) fn required_number_of_reward_claims(account: AccountId) -> u32 { }; let era_span_length: EraNumber = ::EraRewardSpanLength::get(); - let first = DappStaking::era_reward_index(range.0) + let first = DappStaking::era_reward_span_index(range.0) .checked_div(era_span_length) .unwrap(); - let second = DappStaking::era_reward_index(range.1) + let second = DappStaking::era_reward_span_index(range.1) .checked_div(era_span_length) .unwrap(); diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 40dba6ab31..d6154a319a 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -29,7 +29,10 @@ use frame_support::{ }; use sp_runtime::traits::Zero; -use astar_primitives::{dapp_staking::CycleConfiguration, Balance, BlockNumber}; +use astar_primitives::{ + dapp_staking::{CycleConfiguration, SmartContractHandle}, + Balance, BlockNumber, +}; #[test] fn maintenace_mode_works() { @@ -104,11 +107,19 @@ fn maintenace_mode_call_filtering_works() { Error::::Disabled ); assert_noop!( - DappStaking::stake(RuntimeOrigin::signed(1), MockSmartContract::default(), 100), + DappStaking::stake( + RuntimeOrigin::signed(1), + MockSmartContract::wasm(1 as AccountId), + 100 + ), Error::::Disabled ); assert_noop!( - DappStaking::unstake(RuntimeOrigin::signed(1), MockSmartContract::default(), 100), + DappStaking::unstake( + RuntimeOrigin::signed(1), + MockSmartContract::wasm(1 as AccountId), + 100 + ), Error::::Disabled ); assert_noop!( @@ -116,13 +127,16 @@ fn maintenace_mode_call_filtering_works() { Error::::Disabled ); assert_noop!( - DappStaking::claim_bonus_reward(RuntimeOrigin::signed(1), MockSmartContract::default()), + DappStaking::claim_bonus_reward( + RuntimeOrigin::signed(1), + MockSmartContract::wasm(1 as AccountId) + ), Error::::Disabled ); assert_noop!( DappStaking::claim_dapp_reward( RuntimeOrigin::signed(1), - MockSmartContract::default(), + MockSmartContract::wasm(1 as AccountId), 1 ), Error::::Disabled @@ -130,7 +144,7 @@ fn maintenace_mode_call_filtering_works() { assert_noop!( DappStaking::unstake_from_unregistered( RuntimeOrigin::signed(1), - MockSmartContract::default() + MockSmartContract::wasm(1 as AccountId) ), Error::::Disabled ); @@ -671,7 +685,7 @@ fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { #[test] fn claim_unlocked_is_ok() { ExtBuilder::build().execute_with(|| { - let unlocking_blocks = DappStaking::unlock_period(); + let unlocking_blocks = DappStaking::unlocking_period(); // Lock some amount in a few eras let account = 2; @@ -721,7 +735,7 @@ fn claim_unlocked_no_eligible_chunks_fails() { // Cannot claim if unlock period hasn't passed yet let lock_amount = 103; assert_lock(account, lock_amount); - let unlocking_blocks = DappStaking::unlock_period(); + let unlocking_blocks = DappStaking::unlocking_period(); run_for_blocks(unlocking_blocks - 1); assert_noop!( DappStaking::claim_unlocked(RuntimeOrigin::signed(account)), @@ -799,7 +813,7 @@ fn relock_unlocking_insufficient_lock_amount_fails() { }); // Make sure only one chunk is left - let unlocking_blocks = DappStaking::unlock_period(); + let unlocking_blocks = DappStaking::unlocking_period(); run_for_blocks(unlocking_blocks - 1); assert_claim_unlocked(account); @@ -815,7 +829,7 @@ fn stake_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -834,7 +848,7 @@ fn stake_after_expiry_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); // Lock & stake some amount @@ -867,7 +881,7 @@ fn stake_after_expiry_is_ok() { fn stake_with_zero_amount_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; assert_lock(account, 300); @@ -886,7 +900,7 @@ fn stake_on_invalid_dapp_fails() { assert_lock(account, 300); // Try to stake on non-existing contract - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_noop!( DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), Error::::NotOperatedDApp @@ -906,7 +920,7 @@ fn stake_on_invalid_dapp_fails() { fn stake_in_final_era_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); let account = 2; assert_register(1, &smart_contract); assert_lock(account, 300); @@ -929,7 +943,7 @@ fn stake_in_final_era_fails() { fn stake_fails_if_unclaimed_staker_rewards_from_past_remain() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); let account = 2; assert_register(1, &smart_contract); assert_lock(account, 300); @@ -957,7 +971,7 @@ fn stake_fails_if_unclaimed_staker_rewards_from_past_remain() { fn stake_fails_if_claimable_bonus_rewards_from_past_remain() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); let account = 2; assert_register(1, &smart_contract); assert_lock(account, 300); @@ -1101,7 +1115,7 @@ fn unstake_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1123,7 +1137,7 @@ fn unstake_with_leftover_amount_below_minimum_works() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1142,7 +1156,7 @@ fn unstake_with_leftover_amount_below_minimum_works() { fn unstake_with_zero_amount_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; assert_lock(account, 300); @@ -1162,7 +1176,7 @@ fn unstake_on_invalid_dapp_fails() { assert_lock(account, 300); // Try to unstake from non-existing contract - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_noop!( DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract, 100), Error::::NotOperatedDApp @@ -1289,7 +1303,7 @@ fn claim_staker_rewards_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1324,7 +1338,7 @@ fn claim_staker_rewards_double_call_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1351,7 +1365,7 @@ fn claim_staker_rewards_no_claimable_rewards_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1391,7 +1405,7 @@ fn claim_staker_rewards_after_expiry_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1440,7 +1454,7 @@ fn claim_staker_rewards_after_expiry_fails() { fn claim_staker_rewards_fails_due_to_payout_failure() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1469,7 +1483,7 @@ fn claim_bonus_reward_works() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1504,7 +1518,7 @@ fn claim_bonus_reward_double_call_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1529,7 +1543,7 @@ fn claim_bonus_reward_when_nothing_to_claim_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1557,7 +1571,7 @@ fn claim_bonus_reward_with_only_build_and_earn_stake_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1587,7 +1601,7 @@ fn claim_bonus_reward_after_expiry_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1622,7 +1636,7 @@ fn claim_bonus_reward_after_expiry_fails() { fn claim_bonus_reward_fails_due_to_payout_failure() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1651,7 +1665,7 @@ fn claim_dapp_reward_works() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1684,7 +1698,7 @@ fn claim_dapp_reward_works() { #[test] fn claim_dapp_reward_from_non_existing_contract_fails() { ExtBuilder::build().execute_with(|| { - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_noop!( DappStaking::claim_dapp_reward(RuntimeOrigin::signed(1), smart_contract, 1), Error::::ContractNotFound, @@ -1696,7 +1710,7 @@ fn claim_dapp_reward_from_non_existing_contract_fails() { fn claim_dapp_reward_from_invalid_era_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1758,7 +1772,7 @@ fn claim_dapp_reward_if_dapp_not_in_any_tier_fails() { fn claim_dapp_reward_twice_for_same_era_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1791,7 +1805,7 @@ fn claim_dapp_reward_twice_for_same_era_fails() { fn claim_dapp_reward_for_expired_era_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1821,7 +1835,7 @@ fn claim_dapp_reward_for_expired_era_fails() { fn claim_dapp_reward_fails_due_to_payout_failure() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1857,7 +1871,7 @@ fn claim_dapp_reward_fails_due_to_payout_failure() { fn unstake_from_unregistered_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1875,7 +1889,7 @@ fn unstake_from_unregistered_is_ok() { fn unstake_from_unregistered_fails_for_active_contract() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1894,7 +1908,7 @@ fn unstake_from_unregistered_fails_for_active_contract() { fn unstake_from_unregistered_fails_for_not_staked_contract() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); assert_unregister(&smart_contract); @@ -1909,7 +1923,7 @@ fn unstake_from_unregistered_fails_for_not_staked_contract() { fn unstake_from_unregistered_fails_for_past_period() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -2384,7 +2398,7 @@ fn advance_for_some_periods_works() { fn unlock_after_staked_period_ends_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -2445,7 +2459,7 @@ fn stake_and_unstake_after_reward_claim_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index a634e2ad75..73c26974ee 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -84,6 +84,12 @@ pub type AccountLedgerFor = AccountLedger<::MaxUnlockingChunks>; pub type DAppTierRewardsFor = DAppTierRewards<::MaxNumberOfContracts, ::NumberOfTiers>; +// Convenience type for `EraRewardSpan` usage. +pub type EraRewardSpanFor = EraRewardSpan<::EraRewardSpanLength>; + +// Convenience type for `DAppInfo` usage. +pub type DAppInfoFor = DAppInfo<::AccountId>; + /// Era number type pub type EraNumber = u32; /// Period number type diff --git a/precompiles/dapp-staking-v3/Cargo.toml b/precompiles/dapp-staking-v3/Cargo.toml new file mode 100644 index 0000000000..5c40d95a5f --- /dev/null +++ b/precompiles/dapp-staking-v3/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "pallet-evm-precompile-dapp-staking-v3" +version = "0.1.0" +license = "GPL-3.0-or-later" +description = "dApp Staking EVM precompiles" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +log = { workspace = true } +num_enum = { workspace = true } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } + +frame-support = { workspace = true } +frame-system = { workspace = true } + +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Astar +astar-primitives = { workspace = true } +pallet-dapp-staking-v3 = { workspace = true } +precompile-utils = { workspace = true, default-features = false } + +# Frontier +fp-evm = { workspace = true } +pallet-evm = { workspace = true } + +[dev-dependencies] +assert_matches = { workspace = true } +derive_more = { workspace = true } +pallet-balances = { workspace = true, features = ["std"] } +pallet-timestamp = { workspace = true } +precompile-utils = { workspace = true, features = ["testing"] } +serde = { workspace = true } +sha3 = { workspace = true } +sp-arithmetic = { workspace = true } +sp-io = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "scale-info/std", + "astar-primitives/std", + "sp-std/std", + "sp-core/std", + "sp-runtime/std", + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "pallet-dapp-staking-v3/std", + "pallet-evm/std", + "precompile-utils/std", + "pallet-balances/std", + "sp-arithmetic/std", +] +runtime-benchmarks = ["pallet-dapp-staking-v3/runtime-benchmarks"] diff --git a/precompiles/dapp-staking-v3/DappsStakingV1.sol b/precompiles/dapp-staking-v3/DappsStakingV1.sol new file mode 100644 index 0000000000..9b13429413 --- /dev/null +++ b/precompiles/dapp-staking-v3/DappsStakingV1.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: BSD-3-Clause + +pragma solidity >=0.8.0; + +/// Predeployed at the address 0x0000000000000000000000000000000000005001 +/// For better understanding check the source code: +/// repo: https://github.com/AstarNetwork/Astar +/// +/// **NOTE:** This is a soft-deprecated interface used by the old dApps staking v2. +/// It is still supported by the network, but doesn't reflect how dApp staking v3 should be used. +/// Please refer to the `v2` interface for the latest version of the dApp staking contract. +/// +/// It is possible that dApp staking feature will once again evolve in the future so all developers are encouraged +/// to keep their smart contracts which utilize dApp staking precompile interface as upgradable, or implement their logic +/// in such a way it's relatively simple to migrate to the new version of the interface. +interface DappsStaking { + + // Types + + /// Instruction how to handle reward payout for staker. + /// `FreeBalance` - Reward will be paid out to the staker (free balance). + /// `StakeBalance` - Reward will be paid out to the staker and is immediately restaked (locked balance) + enum RewardDestination {FreeBalance, StakeBalance} + + // Storage getters + + /// @notice Read current era. + /// @return era: The current era + function read_current_era() external view returns (uint256); + + /// @notice Read the unbonding period (or unlocking period) in the number of eras. + /// @return period: The unbonding period in eras + function read_unbonding_period() external view returns (uint256); + + /// @notice Read Total network reward for the given era - sum of staker & dApp rewards. + /// @return reward: Total network reward for the given era + function read_era_reward(uint32 era) external view returns (uint128); + + /// @notice Read Total staked amount for the given era + /// @return staked: Total staked amount for the given era + function read_era_staked(uint32 era) external view returns (uint128); + + /// @notice Read Staked amount for the staker + /// @param staker: The staker address in form of 20 or 32 hex bytes + /// @return amount: Staked amount by the staker + function read_staked_amount(bytes calldata staker) external view returns (uint128); + + /// @notice Read Staked amount on a given contract for the staker + /// @param contract_id: The smart contract address used for staking + /// @param staker: The staker address in form of 20 or 32 hex bytes + /// @return amount: Staked amount by the staker + function read_staked_amount_on_contract(address contract_id, bytes calldata staker) external view returns (uint128); + + /// @notice Read the staked amount from the era when the amount was last staked/unstaked + /// @return total: The most recent total staked amount on contract + function read_contract_stake(address contract_id) external view returns (uint128); + + + // Extrinsic calls + + /// @notice Register is root origin only and not allowed via evm precompile. + /// This should always fail. + function register(address) external returns (bool); + + /// @notice Stake provided amount on the contract. + function bond_and_stake(address, uint128) external returns (bool); + + /// @notice Start unbonding process and unstake balance from the contract. + function unbond_and_unstake(address, uint128) external returns (bool); + + /// @notice Withdraw all funds that have completed the unbonding process. + function withdraw_unbonded() external returns (bool); + + /// @notice Claim earned staker rewards for the oldest unclaimed era. + /// In order to claim multiple eras, this call has to be called multiple times. + /// Staker account is derived from the caller address. + /// @param smart_contract: The smart contract address used for staking + function claim_staker(address smart_contract) external returns (bool); + + /// @notice Claim one era of unclaimed dapp rewards for the specified contract and era. + /// @param smart_contract: The smart contract address used for staking + /// @param era: The era to be claimed + function claim_dapp(address smart_contract, uint128 era) external returns (bool); + + /// @notice Set reward destination for staker rewards + /// @param reward_destination: The instruction on how the reward payout should be handled + function set_reward_destination(RewardDestination reward_destination) external returns (bool); + + /// @notice Withdraw staked funds from an unregistered contract. + /// @param smart_contract: The smart contract address used for staking + function withdraw_from_unregistered(address smart_contract) external returns (bool); + + /// @notice Transfer part or entire nomination from origin smart contract to target smart contract + /// @param origin_smart_contract: The origin smart contract address + /// @param amount: The amount to transfer from origin to target + /// @param target_smart_contract: The target smart contract address + function nomination_transfer(address origin_smart_contract, uint128 amount, address target_smart_contract) external returns (bool); +} diff --git a/precompiles/dapp-staking-v3/DappsStakingV2.sol b/precompiles/dapp-staking-v3/DappsStakingV2.sol new file mode 100644 index 0000000000..40c55af6c4 --- /dev/null +++ b/precompiles/dapp-staking-v3/DappsStakingV2.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: BSD-3-Clause + +pragma solidity >=0.8.0; + +/// Predeployed at the address 0x0000000000000000000000000000000000005001 +/// For better understanding check the source code: +/// repo: https://github.com/AstarNetwork/Astar +/// code: pallets/dapp-staking-v3 +interface DAppStaking { + + // Types + + /// Describes the subperiod in which the protocol currently is. + enum Subperiod {Voting, BuildAndEarn} + + /// Describes current smart contract types supported by the network. + enum SmartContractType {EVM, WASM} + + /// @notice Describes protocol state. + /// @param era: Ongoing era number. + /// @param period: Ongoing period number. + /// @param subperiod: Ongoing subperiod type. + struct ProtocolState { + uint256 era; + uint256 period; + Subperiod subperiod; + } + + /// @notice Used to describe smart contract. Astar supports both EVM & WASM smart contracts + /// so it's important to differentiate between the two. This approach also allows + /// easy extensibility in the future. + /// @param contract_type: Type of the smart contract to be used + struct SmartContract { + SmartContractType contract_type; + bytes contract_address; + } + + // Storage getters + + /// @notice Get the current protocol state. + /// @return (current era, current period number, current subperiod type). + function protocol_state() external view returns (ProtocolState memory); + + /// @notice Get the unlocking period expressed in the number of blocks. + /// @return period: The unlocking period expressed in the number of blocks. + function unlocking_period() external view returns (uint256); + + + // Extrinsic calls + + /// @notice Lock the given amount of tokens into dApp staking protocol. + /// @param amount: The amount of tokens to be locked. + function lock(uint128 amount) external returns (bool); + + /// @notice Start the unlocking process for the given amount of tokens. + /// @param amount: The amount of tokens to be unlocked. + function unlock(uint128 amount) external returns (bool); + + /// @notice Claims unlocked tokens, if there are any + function claim_unlocked() external returns (bool); + + /// @notice Stake the given amount of tokens on the specified smart contract. + /// The amount specified must be precise, otherwise the call will fail. + /// @param smart_contract: The smart contract to be staked on. + /// @param amount: The amount of tokens to be staked. + function stake(SmartContract calldata smart_contract, uint128 amount) external returns (bool); + + /// @notice Unstake the given amount of tokens from the specified smart contract. + /// The amount specified must be precise, otherwise the call will fail. + /// @param smart_contract: The smart contract to be unstaked from. + /// @param amount: The amount of tokens to be unstaked. + function unstake(SmartContract calldata smart_contract, uint128 amount) external returns (bool); + + /// @notice Claims one or more pending staker rewards. + function claim_staker_rewards() external returns (bool); + + /// @notice Claim the bonus reward for the specified smart contract. + /// @param smart_contract: The smart contract for which the bonus reward should be claimed. + function claim_bonus_reward(SmartContract calldata smart_contract) external returns (bool); + + /// @notice Claim dApp reward for the specified smart contract & era. + /// @param smart_contract: The smart contract for which the dApp reward should be claimed. + /// @param era: The era for which the dApp reward should be claimed. + function claim_dapp_reward(SmartContract calldata smart_contract, uint256 era) external returns (bool); + + /// @notice Unstake all funds from the unregistered smart contract. + /// @param smart_contract: The smart contract which was unregistered and from which all funds should be unstaked. + function unstake_from_unregistered(SmartContract calldata smart_contract) external returns (bool); + + /// @notice Used to cleanup all expired contract stake entries from the caller. + function cleanup_expired_entries() external returns (bool); +} diff --git a/precompiles/dapp-staking-v3/src/lib.rs b/precompiles/dapp-staking-v3/src/lib.rs new file mode 100644 index 0000000000..5178395be5 --- /dev/null +++ b/precompiles/dapp-staking-v3/src/lib.rs @@ -0,0 +1,832 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Astar dApp staking interface. + +#![cfg_attr(not(feature = "std"), no_std)] + +use fp_evm::PrecompileHandle; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use parity_scale_codec::MaxEncodedLen; + +use frame_support::{ + dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}, + ensure, + traits::ConstU32, +}; + +use pallet_evm::AddressMapping; +use precompile_utils::{ + prelude::*, + solidity::{ + codec::{Reader, Writer}, + Codec, + }, +}; +use sp_core::{Get, H160, U256}; +use sp_runtime::traits::Zero; +use sp_std::{marker::PhantomData, prelude::*}; +extern crate alloc; + +use astar_primitives::{dapp_staking::SmartContractHandle, AccountId, Balance}; +use pallet_dapp_staking_v3::{ + AccountLedgerFor, ActiveProtocolState, ContractStake, ContractStakeAmount, CurrentEraInfo, + DAppInfoFor, EraInfo, EraRewardSpanFor, EraRewards, IntegratedDApps, Ledger, + Pallet as DAppStaking, ProtocolState, SingularStakingInfo, StakerInfo, Subperiod, +}; + +pub const STAKER_BYTES_LIMIT: u32 = 32; +type GetStakerBytesLimit = ConstU32; + +pub type DynamicAddress = BoundedBytes; + +#[cfg(test)] +mod test; + +/// Helper struct used to encode protocol state. +#[derive(Debug, Clone, solidity::Codec)] +pub(crate) struct PrecompileProtocolState { + era: U256, + period: U256, + subperiod: u8, +} + +/// Helper struct used to encode different smart contract types for the v2 interface. +#[derive(Debug, Clone, solidity::Codec)] +pub struct SmartContractV2 { + contract_type: SmartContractTypes, + address: DynamicAddress, +} + +/// Convenience type for smart contract type handling. +#[derive(Clone, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +pub(crate) enum SmartContractTypes { + Evm, + Wasm, +} + +impl Codec for SmartContractTypes { + fn read(reader: &mut Reader) -> MayRevert { + let value256: U256 = reader + .read() + .map_err(|_| RevertReason::read_out_of_bounds(Self::signature()))?; + + let value_as_u8: u8 = value256 + .try_into() + .map_err(|_| RevertReason::value_is_too_large(Self::signature()))?; + + value_as_u8 + .try_into() + .map_err(|_| RevertReason::custom("Unknown smart contract type").into()) + } + + fn write(writer: &mut Writer, value: Self) { + let value_as_u8: u8 = value.into(); + U256::write(writer, value_as_u8.into()); + } + + fn has_static_size() -> bool { + true + } + + fn signature() -> String { + "uint8".into() + } +} + +pub struct DappStakingV3Precompile(PhantomData); +#[precompile_utils::precompile] +impl DappStakingV3Precompile +where + R: pallet_evm::Config + + pallet_dapp_staking_v3::Config + + frame_system::Config, + ::RuntimeOrigin: From>, + R::RuntimeCall: Dispatchable + GetDispatchInfo, + R::RuntimeCall: From>, +{ + // v1 functions + + /// Read the ongoing `era` number. + #[precompile::public("read_current_era()")] + #[precompile::view] + fn read_current_era(handle: &mut impl PrecompileHandle) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + handle.record_db_read::(8 + ProtocolState::max_encoded_len())?; + + let current_era = ActiveProtocolState::::get().era; + + Ok(current_era.into()) + } + + /// Read the `unbonding period` or `unlocking period` expressed in the number of eras. + #[precompile::public("read_unbonding_period()")] + #[precompile::view] + fn read_unbonding_period(_: &mut impl PrecompileHandle) -> EvmResult { + // constant, no DB read + Ok(::UnlockingPeriod::get().into()) + } + + /// Read the total assigned reward pool for the given era. + /// + /// Total amount is sum of staker & dApp rewards. + #[precompile::public("read_era_reward(uint32)")] + #[precompile::view] + fn read_era_reward(handle: &mut impl PrecompileHandle, era: u32) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: EraRewards: + // Twox64Concat(8) + EraIndex(4) + EraRewardSpanFor::max_encoded_len + handle.record_db_read::(12 + EraRewardSpanFor::::max_encoded_len())?; + + // Get the appropriate era reward span + let era_span_index = DAppStaking::::era_reward_span_index(era); + let reward_span = + EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpanFor::::new()); + + // Sum up staker & dApp reward pools for the era + let reward = reward_span.get(era).map_or(Zero::zero(), |r| { + r.staker_reward_pool.saturating_add(r.dapp_reward_pool) + }); + + Ok(reward) + } + + /// Read the total staked amount for the given era. + /// + /// In case era is very far away in history, it's possible that the information is not available. + /// In that case, zero is returned. + /// + /// This is safe to use for current era and the next one. + #[precompile::public("read_era_staked(uint32)")] + #[precompile::view] + fn read_era_staked(handle: &mut impl PrecompileHandle, era: u32) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + handle.record_db_read::(8 + ProtocolState::max_encoded_len())?; + + let current_era = ActiveProtocolState::::get().era; + + // There are few distinct scenenarios: + // 1. Era is in the past so the value might exist. + // 2. Era is current or the next one, in which case we definitely have that information. + // 3. Era is from the future (more than the next era), in which case we don't have that information. + if era < current_era { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: EraRewards: + // Twox64Concat(8) + Twox64Concat(8 + EraIndex(4)) + EraRewardSpanFor::max_encoded_len + handle.record_db_read::(20 + EraRewardSpanFor::::max_encoded_len())?; + + let era_span_index = DAppStaking::::era_reward_span_index(era); + let reward_span = + EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpanFor::::new()); + + let staked = reward_span.get(era).map_or(Zero::zero(), |r| r.staked); + + Ok(staked.into()) + } else if era == current_era || era == current_era.saturating_add(1) { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: CurrentEraInfo: + // Twox64Concat(8) + EraInfo::max_encoded_len + handle.record_db_read::(8 + EraInfo::max_encoded_len())?; + + let current_era_info = CurrentEraInfo::::get(); + + if era == current_era { + Ok(current_era_info.current_stake_amount.total()) + } else { + Ok(current_era_info.next_stake_amount.total()) + } + } else { + Err(RevertReason::custom("Era is in the future").into()) + } + } + + /// Read the total staked amount by the given account. + #[precompile::public("read_staked_amount(bytes)")] + #[precompile::view] + fn read_staked_amount( + handle: &mut impl PrecompileHandle, + staker: DynamicAddress, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + // Storage item: Ledger: + // Blake2_128Concat(16 + SmartContract::max_encoded_len) + Ledger::max_encoded_len + handle.record_db_read::( + 24 + AccountLedgerFor::::max_encoded_len() + + ProtocolState::max_encoded_len() + + ::SmartContract::max_encoded_len(), + )?; + + let staker = Self::parse_input_address(staker.into())?; + + // read the account's ledger + let ledger = Ledger::::get(&staker); + log::trace!(target: "ds-precompile", "read_staked_amount for account: {:?}, ledger: {:?}", staker, ledger); + + // Make sure to check staked amount against the ongoing period (past period stakes are reset to zero). + let current_period_number = ActiveProtocolState::::get().period_number(); + + Ok(ledger.staked_amount(current_period_number)) + } + + /// Read the total staked amount by the given staker on the given contract. + #[precompile::public("read_staked_amount_on_contract(address,bytes)")] + #[precompile::view] + fn read_staked_amount_on_contract( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + staker: DynamicAddress, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + // Storage item: StakerInfo: + // Blake2_128Concat(16 + SmartContract::max_encoded_len) + SingularStakingInfo::max_encoded_len + handle.record_db_read::( + 24 + ProtocolState::max_encoded_len() + + ::SmartContract::max_encoded_len() + + SingularStakingInfo::max_encoded_len(), + )?; + + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + + // parse the staker account + let staker = Self::parse_input_address(staker.into())?; + + // Get staking info for the staker/contract combination + let staking_info = StakerInfo::::get(&staker, &smart_contract).unwrap_or_default(); + log::trace!(target: "ds-precompile", "read_staked_amount_on_contract for account:{:?}, staking_info: {:?}", staker, staking_info); + + // Ensure that the staking info is checked against the current period (stakes from past periods are reset) + let current_period_number = ActiveProtocolState::::get().period_number(); + + if staking_info.period_number() == current_period_number { + Ok(staking_info.total_staked_amount()) + } else { + Ok(0_u128) + } + } + + /// Read the total amount staked on the given contract right now. + #[precompile::public("read_contract_stake(address)")] + #[precompile::view] + fn read_contract_stake( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + // Storage item: IntegratedDApps: + // Blake2_128Concat(16 + SmartContract::max_encoded_len) + DAppInfoFor::max_encoded_len + // Storage item: ContractStake: + // Twox64Concat(8) + EraIndex(4) + ContractStakeAmount::max_encoded_len + handle.record_db_read::( + 36 + ProtocolState::max_encoded_len() + + ::SmartContract::max_encoded_len() + + DAppInfoFor::::max_encoded_len() + + ContractStakeAmount::max_encoded_len(), + )?; + + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + + let current_period_number = ActiveProtocolState::::get().period_number(); + let dapp_info = match IntegratedDApps::::get(&smart_contract) { + Some(dapp_info) => dapp_info, + None => { + // If the contract is not registered, return 0 to keep the legacy behavior. + return Ok(0_u128); + } + }; + + // call pallet-dapps-staking + let contract_stake = ContractStake::::get(&dapp_info.id); + + Ok(contract_stake.total_staked_amount(current_period_number)) + } + + /// Register contract with the dapp-staking pallet + /// Register is root origin only. This should always fail when called via evm precompile. + #[precompile::public("register(address)")] + fn register(_: &mut impl PrecompileHandle, _address: Address) -> EvmResult { + // register is root-origin call. it should always fail when called via evm precompiles. + Err(RevertReason::custom("register via evm precompile is not allowed").into()) + } + + /// Lock & stake some amount on the specified contract. + /// + /// In case existing `stakeable` is sufficient to cover the given `amount`, only the `stake` operation is performed. + /// Otherwise, best effort is done to lock the additional amount so `stakeable` amount can cover the given `amount`. + #[precompile::public("bond_and_stake(address,uint128)")] + fn bond_and_stake( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + amount: u128, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + // Storage item: Ledger: + // Blake2_128Concat(16 + SmartContract::max_encoded_len()) + Ledger::max_encoded_len + handle.record_db_read::( + 24 + AccountLedgerFor::::max_encoded_len() + + ProtocolState::max_encoded_len() + + ::SmartContract::max_encoded_len(), + )?; + + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + log::trace!(target: "ds-precompile", "bond_and_stake {:?}, {:?}", smart_contract, amount); + + // Read total locked & staked amounts + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let protocol_state = ActiveProtocolState::::get(); + let ledger = Ledger::::get(&origin); + + // Check if stakeable amount is enough to cover the given `amount` + let stakeable_amount = ledger.stakeable_amount(protocol_state.period_number()); + + // If it isn't, we need to first lock the additional amount. + if stakeable_amount < amount { + let delta = amount.saturating_sub(stakeable_amount); + + let lock_call = pallet_dapp_staking_v3::Call::::lock { amount: delta }; + RuntimeHelper::::try_dispatch(handle, Some(origin.clone()).into(), lock_call)?; + } + + // Now, with best effort, we can try & stake the given `value`. + let stake_call = pallet_dapp_staking_v3::Call::::stake { + smart_contract, + amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), stake_call)?; + + Ok(true) + } + + /// Start unbonding process and unstake balance from the contract. + #[precompile::public("unbond_and_unstake(address,uint128)")] + fn unbond_and_unstake( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + amount: u128, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + // Storage item: StakerInfo: + // Blake2_128Concat(16 + SmartContract::max_encoded_len) + SingularStakingInfo::max_encoded_len + handle.record_db_read::( + 24 + ProtocolState::max_encoded_len() + + ::SmartContract::max_encoded_len() + + SingularStakingInfo::max_encoded_len(), + )?; + + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + let origin = R::AddressMapping::into_account_id(handle.context().caller); + log::trace!(target: "ds-precompile", "unbond_and_unstake {:?}, {:?}", smart_contract, amount); + + // Find out if there is something staked on the contract + let protocol_state = ActiveProtocolState::::get(); + let staker_info = StakerInfo::::get(&origin, &smart_contract).unwrap_or_default(); + + // If there is, we need to unstake it before calling `unlock` + if staker_info.period_number() == protocol_state.period_number() { + let unstake_call = pallet_dapp_staking_v3::Call::::unstake { + smart_contract, + amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin.clone()).into(), unstake_call)?; + } + + // Now we can try and `unlock` the given `amount` + let unlock_call = pallet_dapp_staking_v3::Call::::unlock { amount }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), unlock_call)?; + + Ok(true) + } + + /// Claim back the unbonded (or unlocked) funds. + #[precompile::public("withdraw_unbonded()")] + fn withdraw_unbonded(handle: &mut impl PrecompileHandle) -> EvmResult { + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let call = pallet_dapp_staking_v3::Call::::claim_unlocked {}; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(true) + } + + /// Claim dApp rewards for the given era + #[precompile::public("claim_dapp(address,uint128)")] + fn claim_dapp( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + era: u128, + ) -> EvmResult { + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + + // parse era + let era = era + .try_into() + .map_err::(|_| RevertReason::value_is_too_large("era type").into()) + .in_field("era")?; + + log::trace!(target: "ds-precompile", "claim_dapp {:?}, era {:?}", smart_contract, era); + + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let call = pallet_dapp_staking_v3::Call::::claim_dapp_reward { + smart_contract, + era, + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(true) + } + + /// Claim staker rewards. + /// + /// Smart contract argument is legacy & is ignored in the new implementation. + #[precompile::public("claim_staker(address)")] + fn claim_staker( + handle: &mut impl PrecompileHandle, + _contract_h160: Address, + ) -> EvmResult { + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let call = pallet_dapp_staking_v3::Call::::claim_staker_rewards {}; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(true) + } + + /// Set claim reward destination for the caller. + /// + /// This call has been deprecated by dApp staking v3. + #[precompile::public("set_reward_destination(uint8)")] + fn set_reward_destination(_: &mut impl PrecompileHandle, _destination: u8) -> EvmResult { + Err(RevertReason::custom("Setting reward destination is no longer supported.").into()) + } + + /// Withdraw staked funds from the unregistered contract + #[precompile::public("withdraw_from_unregistered(address)")] + fn withdraw_from_unregistered( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + ) -> EvmResult { + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + log::trace!(target: "ds-precompile", "withdraw_from_unregistered {:?}", smart_contract); + + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let call = pallet_dapp_staking_v3::Call::::unstake_from_unregistered { smart_contract }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(true) + } + + /// Transfers stake from one contract to another. + /// This is a legacy functionality that is no longer supported via direct call to dApp staking v3. + /// However, it can be achieved by chaining `unstake` and `stake` calls. + #[precompile::public("nomination_transfer(address,uint128,address)")] + fn nomination_transfer( + handle: &mut impl PrecompileHandle, + origin_contract_h160: Address, + amount: u128, + target_contract_h160: Address, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: StakerInfo: + // Blake2_128Concat(16 + SmartContract::max_encoded_len) + SingularStakingInfo::max_encoded_len + handle.record_db_read::( + 16 + ::SmartContract::max_encoded_len() + + SingularStakingInfo::max_encoded_len(), + )?; + + let origin_smart_contract = + ::SmartContract::evm(origin_contract_h160.into()); + let target_smart_contract = + ::SmartContract::evm(target_contract_h160.into()); + log::trace!(target: "ds-precompile", "nomination_transfer {:?} {:?} {:?}", origin_smart_contract, amount, target_smart_contract); + + // Find out how much staker has staked on the origin contract + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let staker_info = StakerInfo::::get(&origin, &origin_smart_contract).unwrap_or_default(); + + // We don't care from which period the staked amount is, the logic takes care of the situation + // if value comes from the past period. + let staked_amount = staker_info.total_staked_amount(); + let minimum_allowed_stake_amount = + ::MinimumStakeAmount::get(); + + // In case the remaining staked amount on the origin contract is less than the minimum allowed stake amount, + // everything will be unstaked. To keep in line with legacy `nomination_transfer` behavior, we should transfer + // the entire amount from the origin to target contract. + // + // In case value comes from the past period, we don't care, since the `unstake` call will fall apart. + let stake_amount = if staked_amount > 0 + && staked_amount.saturating_sub(amount) < minimum_allowed_stake_amount + { + staked_amount + } else { + amount + }; + + // First call unstake from the origin smart contract + let unstake_call = pallet_dapp_staking_v3::Call::::unstake { + smart_contract: origin_smart_contract, + amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin.clone()).into(), unstake_call)?; + + // Then call stake on the target smart contract + let stake_call = pallet_dapp_staking_v3::Call::::stake { + smart_contract: target_smart_contract, + amount: stake_amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), stake_call)?; + + Ok(true) + } + + // v2 functions + + /// Read the current protocol state. + #[precompile::public("protocol_state()")] + #[precompile::view] + fn protocol_state(handle: &mut impl PrecompileHandle) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + handle.record_db_read::(8 + ProtocolState::max_encoded_len())?; + + let protocol_state = ActiveProtocolState::::get(); + + Ok(PrecompileProtocolState { + era: protocol_state.era.into(), + period: protocol_state.period_number().into(), + subperiod: subperiod_id(&protocol_state.subperiod()), + }) + } + + /// Read the `unbonding period` or `unlocking period` expressed in the number of eras. + #[precompile::public("unlocking_period()")] + #[precompile::view] + fn unlocking_period(_: &mut impl PrecompileHandle) -> EvmResult { + // constant, no DB read + Ok(DAppStaking::::unlocking_period().into()) + } + + /// Attempt to lock the given amount into the dApp staking protocol. + #[precompile::public("lock(uint128)")] + fn lock(handle: &mut impl PrecompileHandle, amount: u128) -> EvmResult { + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let lock_call = pallet_dapp_staking_v3::Call::::lock { amount }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), lock_call)?; + + Ok(true) + } + + /// Attempt to unlock the given amount from the dApp staking protocol. + #[precompile::public("unlock(uint128)")] + fn unlock(handle: &mut impl PrecompileHandle, amount: u128) -> EvmResult { + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let unlock_call = pallet_dapp_staking_v3::Call::::unlock { amount }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), unlock_call)?; + + Ok(true) + } + + /// Attempts to claim unlocking chunks which have undergone the entire unlocking period. + #[precompile::public("claim_unlocked()")] + fn claim_unlocked(handle: &mut impl PrecompileHandle) -> EvmResult { + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let claim_unlocked_call = pallet_dapp_staking_v3::Call::::claim_unlocked {}; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), claim_unlocked_call)?; + + Ok(true) + } + + /// Attempts to stake the given amount on the given smart contract. + #[precompile::public("stake((uint8,bytes),uint128)")] + fn stake( + handle: &mut impl PrecompileHandle, + smart_contract: SmartContractV2, + amount: Balance, + ) -> EvmResult { + let smart_contract = Self::decode_smart_contract(smart_contract)?; + + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let stake_call = pallet_dapp_staking_v3::Call::::stake { + smart_contract, + amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), stake_call)?; + + Ok(true) + } + + /// Attempts to unstake the given amount from the given smart contract. + #[precompile::public("unstake((uint8,bytes),uint128)")] + fn unstake( + handle: &mut impl PrecompileHandle, + smart_contract: SmartContractV2, + amount: Balance, + ) -> EvmResult { + let smart_contract = Self::decode_smart_contract(smart_contract)?; + + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let unstake_call = pallet_dapp_staking_v3::Call::::unstake { + smart_contract, + amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), unstake_call)?; + + Ok(true) + } + + /// Attempts to claim one or more pending staker rewards. + #[precompile::public("claim_staker_rewards()")] + fn claim_staker_rewards(handle: &mut impl PrecompileHandle) -> EvmResult { + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let claim_staker_rewards_call = pallet_dapp_staking_v3::Call::::claim_staker_rewards {}; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), claim_staker_rewards_call)?; + + Ok(true) + } + + /// Attempts to claim bonus reward for being a loyal staker of the given dApp. + #[precompile::public("claim_bonus_reward((uint8,bytes))")] + fn claim_bonus_reward( + handle: &mut impl PrecompileHandle, + smart_contract: SmartContractV2, + ) -> EvmResult { + let smart_contract = Self::decode_smart_contract(smart_contract)?; + + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let claim_bonus_reward_call = + pallet_dapp_staking_v3::Call::::claim_bonus_reward { smart_contract }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), claim_bonus_reward_call)?; + + Ok(true) + } + + /// Attempts to claim dApp reward for the given dApp in the given era. + #[precompile::public("claim_bonus_reward((uint8,bytes),uint256)")] + fn claim_dapp_reward( + handle: &mut impl PrecompileHandle, + smart_contract: SmartContractV2, + era: U256, + ) -> EvmResult { + let smart_contract = Self::decode_smart_contract(smart_contract)?; + let era = era + .try_into() + .map_err::(|_| RevertReason::value_is_too_large("Era number.").into()) + .in_field("era")?; + + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let claim_dapp_reward_call = pallet_dapp_staking_v3::Call::::claim_dapp_reward { + smart_contract, + era, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), claim_dapp_reward_call)?; + + Ok(true) + } + + /// Attempts to unstake everything from an unregistered contract. + #[precompile::public("unstake_from_unregistered((uint8,bytes))")] + fn unstake_from_unregistered( + handle: &mut impl PrecompileHandle, + smart_contract: SmartContractV2, + ) -> EvmResult { + let smart_contract = Self::decode_smart_contract(smart_contract)?; + + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let unstake_from_unregistered_call = + pallet_dapp_staking_v3::Call::::unstake_from_unregistered { smart_contract }; + RuntimeHelper::::try_dispatch( + handle, + Some(origin).into(), + unstake_from_unregistered_call, + )?; + + Ok(true) + } + + /// Attempts to cleanup expired entries for the staker. + #[precompile::public("cleanup_expired_entries()")] + fn cleanup_expired_entries(handle: &mut impl PrecompileHandle) -> EvmResult { + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let cleanup_expired_entries_call = + pallet_dapp_staking_v3::Call::::cleanup_expired_entries {}; + RuntimeHelper::::try_dispatch( + handle, + Some(origin).into(), + cleanup_expired_entries_call, + )?; + + Ok(true) + } + + // Utility functions + + /// Helper method to decode smart contract struct for v2 calls + pub(crate) fn decode_smart_contract( + smart_contract: SmartContractV2, + ) -> EvmResult<::SmartContract> { + let smart_contract = match smart_contract.contract_type { + SmartContractTypes::Evm => { + ensure!( + smart_contract.address.as_bytes().len() == 20, + revert("Invalid address length for Astar EVM smart contract.") + ); + let h160_address = H160::from_slice(smart_contract.address.as_bytes()); + ::SmartContract::evm(h160_address) + } + SmartContractTypes::Wasm => { + ensure!( + smart_contract.address.as_bytes().len() == 32, + revert("Invalid address length for Astar WASM smart contract.") + ); + let mut staker_bytes = [0_u8; 32]; + staker_bytes[..].clone_from_slice(&smart_contract.address.as_bytes()); + + ::SmartContract::wasm(staker_bytes.into()) + } + }; + + Ok(smart_contract) + } + + /// Helper method to parse H160 or SS58 address + pub(crate) fn parse_input_address(staker_vec: Vec) -> EvmResult { + let staker: R::AccountId = match staker_vec.len() { + // public address of the ss58 account has 32 bytes + 32 => { + let mut staker_bytes = [0_u8; 32]; + staker_bytes[..].clone_from_slice(&staker_vec[0..32]); + + staker_bytes.into() + } + // public address of the H160 account has 20 bytes + 20 => { + let mut staker_bytes = [0_u8; 20]; + staker_bytes[..].clone_from_slice(&staker_vec[0..20]); + + R::AddressMapping::into_account_id(staker_bytes.into()) + } + _ => { + // Return err if account length is wrong + return Err(revert("Error while parsing staker's address")); + } + }; + + Ok(staker) + } +} + +/// Numeric Id of the subperiod enum value. +pub(crate) fn subperiod_id(subperiod: &Subperiod) -> u8 { + match subperiod { + Subperiod::Voting => 0, + Subperiod::BuildAndEarn => 1, + } +} diff --git a/precompiles/dapp-staking-v3/src/test/mock.rs b/precompiles/dapp-staking-v3/src/test/mock.rs new file mode 100644 index 0000000000..2015ba6736 --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/mock.rs @@ -0,0 +1,461 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::*; + +use fp_evm::{IsPrecompileResult, Precompile}; +use frame_support::{ + assert_ok, construct_runtime, parameter_types, + traits::{ + fungible::{Mutate as FunMutate, Unbalanced as FunUnbalanced}, + ConstU128, ConstU64, GenesisBuild, Hooks, + }, + weights::{RuntimeDbWeight, Weight}, +}; +use frame_system::RawOrigin; +use pallet_evm::{ + AddressMapping, EnsureAddressNever, EnsureAddressRoot, PrecompileResult, PrecompileSet, +}; +use sp_arithmetic::{fixed_point::FixedU64, Permill}; +use sp_core::{H160, H256}; +use sp_io::TestExternalities; +use sp_runtime::traits::{BlakeTwo256, ConstU32, IdentityLookup}; +extern crate alloc; + +use astar_primitives::{ + dapp_staking::{CycleConfiguration, SmartContract, StakingRewardHandler}, + testing::Header, + AccountId, Balance, BlockNumber, +}; +use pallet_dapp_staking_v3::{EraNumber, PeriodNumber, PriceProvider, TierThreshold}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +pub struct AddressMapper; +impl AddressMapping for AddressMapper { + fn into_account_id(account: H160) -> AccountId { + let mut account_id = [0u8; 32]; + account_id[0..20].clone_from_slice(&account.as_bytes()); + + account_id + .try_into() + .expect("H160 is 20 bytes long so it must fit into 32 bytes; QED") + } +} + +pub const READ_WEIGHT: u64 = 3; +pub const WRITE_WEIGHT: u64 = 7; + +parameter_types! { + pub const BlockHashCount: BlockNumber = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); + pub const TestWeights: RuntimeDbWeight = RuntimeDbWeight { + read: READ_WEIGHT, + write: WRITE_WEIGHT, + }; +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type MaxLocks = ConstU32<4>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type HoldIdentifier = (); + type FreezeIdentifier = RuntimeFreezeReason; + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<1>; + type WeightInfo = (); +} + +pub fn precompile_address() -> H160 { + H160::from_low_u64_be(0x5001) +} + +#[derive(Debug, Clone, Copy)] +pub struct DappStakingPrecompile(PhantomData); +impl PrecompileSet for DappStakingPrecompile +where + R: pallet_evm::Config, + DappStakingV3Precompile: Precompile, +{ + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { + match handle.code_address() { + a if a == precompile_address() => Some(DappStakingV3Precompile::::execute(handle)), + _ => None, + } + } + + fn is_precompile(&self, address: sp_core::H160, _gas: u64) -> IsPrecompileResult { + IsPrecompileResult::Answer { + is_precompile: address == precompile_address(), + extra_cost: 0, + } + } +} + +pub type PrecompileCall = DappStakingV3PrecompileCall; + +parameter_types! { + pub PrecompilesValue: DappStakingPrecompile = DappStakingPrecompile(Default::default()); + pub WeightPerGas: Weight = Weight::from_parts(1, 0); +} + +impl pallet_evm::Config for Test { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AddressMapper; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = DappStakingPrecompile; + type PrecompilesValue = PrecompilesValue; + type Timestamp = Timestamp; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = (); + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type WeightInfo = (); + type GasLimitPovSizeRatio = ConstU64<4>; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<5>; + type WeightInfo = (); +} + +type MockSmartContract = SmartContract<::AccountId>; + +pub struct DummyPriceProvider; +impl PriceProvider for DummyPriceProvider { + fn average_price() -> FixedU64 { + FixedU64::from_rational(1, 10) + } +} + +pub struct DummyStakingRewardHandler; +impl StakingRewardHandler for DummyStakingRewardHandler { + fn staker_and_dapp_reward_pools(_total_staked_value: Balance) -> (Balance, Balance) { + ( + Balance::from(1_000_000_000_000_u128), + Balance::from(1_000_000_000_u128), + ) + } + + fn bonus_reward_pool() -> Balance { + Balance::from(3_000_000_u128) + } + + fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()> { + let _ = Balances::mint_into(beneficiary, reward); + Ok(()) + } +} + +pub struct DummyCycleConfiguration; +impl CycleConfiguration for DummyCycleConfiguration { + fn periods_per_cycle() -> u32 { + 4 + } + + fn eras_per_voting_subperiod() -> u32 { + 8 + } + + fn eras_per_build_and_earn_subperiod() -> u32 { + 16 + } + + fn blocks_per_era() -> u32 { + 10 + } +} + +// Just to satsify the trait bound +#[cfg(feature = "runtime-benchmarks")] +pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); +#[cfg(feature = "runtime-benchmarks")] +impl pallet_dapp_staking_v3::BenchmarkHelper + for BenchmarkHelper +{ + fn get_smart_contract(id: u32) -> MockSmartContract { + MockSmartContract::evm(H160::from_low_u64_be(id as u64)) + } + + fn set_balance(_account: &AccountId, _amount: Balance) {} +} + +impl pallet_dapp_staking_v3::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type Currency = Balances; + type SmartContract = MockSmartContract; + type ManagerOrigin = frame_system::EnsureRoot; + type NativePriceProvider = DummyPriceProvider; + type StakingRewardHandler = DummyStakingRewardHandler; + type CycleConfiguration = DummyCycleConfiguration; + type EraRewardSpanLength = ConstU32<8>; + type RewardRetentionInPeriods = ConstU32<2>; + type MaxNumberOfContracts = ConstU32<10>; + type MaxUnlockingChunks = ConstU32<5>; + type MinimumLockedAmount = ConstU128<10>; + type UnlockingPeriod = ConstU32<2>; + type MaxNumberOfStakedContracts = ConstU32<5>; + type MinimumStakeAmount = ConstU128<3>; + type NumberOfTiers = ConstU32<4>; + type WeightInfo = pallet_dapp_staking_v3::weights::SubstrateWeight; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = BenchmarkHelper; +} + +construct_runtime!( + pub struct Test + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + DappStaking: pallet_dapp_staking_v3, + } +); + +pub struct ExternalityBuilder; +impl ExternalityBuilder { + pub fn build() -> TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + >::assimilate_storage( + &pallet_dapp_staking_v3::GenesisConfig { + reward_portion: vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ], + slot_distribution: vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ], + tier_thresholds: vec![ + TierThreshold::DynamicTvlAmount { + amount: 100, + minimum_amount: 80, + }, + TierThreshold::DynamicTvlAmount { + amount: 50, + minimum_amount: 40, + }, + TierThreshold::DynamicTvlAmount { + amount: 20, + minimum_amount: 20, + }, + TierThreshold::FixedTvlAmount { amount: 10 }, + ], + slots_per_tier: vec![10, 20, 30, 40], + }, + &mut storage, + ) + .ok(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| { + System::set_block_number(1); + + let alice_native = AddressMapper::into_account_id(ALICE); + assert_ok!( + ::Currency::write_balance( + &alice_native, + 1000_000_000_000_000_000_000 as Balance, + ) + ); + }); + ext + } +} + +pub fn precompiles() -> DappStakingPrecompile { + PrecompilesValue::get() +} + +// Utility functions + +pub const ALICE: H160 = H160::repeat_byte(0xAA); + +/// Used to register a smart contract, and stake some funds on it. +pub fn register_and_stake( + account: H160, + smart_contract: ::SmartContract, + amount: Balance, +) { + let alice_native = AddressMapper::into_account_id(account); + + // 1. Register smart contract + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + alice_native.clone(), + smart_contract.clone() + )); + + // 2. Lock some amount + assert_ok!(DappStaking::lock( + RawOrigin::Signed(alice_native.clone()).into(), + amount, + )); + + // 3. Stake the locked amount + assert_ok!(DappStaking::stake( + RawOrigin::Signed(alice_native.clone()).into(), + smart_contract.clone(), + amount, + )); +} + +/// Utility function used to create `DynamicAddress` out of the given `H160` address. +/// The first one is simply byte representation of the H160 address. +/// The second one is byte representation of the derived `AccountId` from the H160 address. +pub fn into_dynamic_addresses(address: H160) -> [DynamicAddress; 2] { + [ + address.as_bytes().try_into().unwrap(), + >::as_ref(&AddressMapper::into_account_id(address)) + .try_into() + .unwrap(), + ] +} + +/// Initialize first block. +/// This method should only be called once in a UT otherwise the first block will get initialized multiple times. +pub fn initialize() { + // This assert prevents method misuse + assert_eq!(System::block_number(), 1 as BlockNumber); + DappStaking::on_initialize(System::block_number()); + run_to_block(2); +} + +/// Run to the specified block number. +/// Function assumes first block has been initialized. +pub(crate) fn run_to_block(n: BlockNumber) { + while System::block_number() < n { + DappStaking::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + DappStaking::on_initialize(System::block_number()); + } +} + +/// Run for the specified number of blocks. +/// Function assumes first block has been initialized. +pub(crate) fn run_for_blocks(n: BlockNumber) { + run_to_block(System::block_number() + n); +} + +/// Advance blocks until the specified era has been reached. +/// +/// Function has no effect if era is already passed. +pub(crate) fn advance_to_era(era: EraNumber) { + assert!(era >= ActiveProtocolState::::get().era); + while ActiveProtocolState::::get().era < era { + run_for_blocks(1); + } +} + +/// Advance blocks until next era has been reached. +pub(crate) fn advance_to_next_era() { + advance_to_era(ActiveProtocolState::::get().era + 1); +} + +/// Advance blocks until next period type has been reached. +pub(crate) fn advance_to_next_subperiod() { + let subperiod = ActiveProtocolState::::get().subperiod(); + while ActiveProtocolState::::get().subperiod() == subperiod { + run_for_blocks(1); + } +} + +/// Advance blocks until the specified period has been reached. +/// +/// Function has no effect if period is already passed. +pub(crate) fn advance_to_period(period: PeriodNumber) { + assert!(period >= ActiveProtocolState::::get().period_number()); + while ActiveProtocolState::::get().period_number() < period { + run_for_blocks(1); + } +} + +/// Advance blocks until next period has been reached. +pub(crate) fn advance_to_next_period() { + advance_to_period(ActiveProtocolState::::get().period_number() + 1); +} + +// Return all dApp staking events from the event buffer. +pub fn dapp_staking_events() -> Vec> { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + ::RuntimeEvent::from(e) + .try_into() + .ok() + }) + .collect::>() +} diff --git a/precompiles/dapp-staking-v3/src/test/mod.rs b/precompiles/dapp-staking-v3/src/test/mod.rs new file mode 100644 index 0000000000..a33eb22954 --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/mod.rs @@ -0,0 +1,22 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +mod mock; +mod tests_v1; +mod tests_v2; +mod types; diff --git a/precompiles/dapp-staking-v3/src/test/tests_v1.rs b/precompiles/dapp-staking-v3/src/test/tests_v1.rs new file mode 100644 index 0000000000..8d93b56198 --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/tests_v1.rs @@ -0,0 +1,867 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +extern crate alloc; +use crate::{test::mock::*, *}; +use frame_support::assert_ok; +use frame_system::RawOrigin; +use precompile_utils::testing::*; +use sp_core::H160; +use sp_runtime::traits::Zero; + +use assert_matches::assert_matches; + +use pallet_dapp_staking_v3::{ActiveProtocolState, EraNumber, EraRewards}; + +#[test] +fn read_current_era_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_current_era {}, + ) + .expect_no_logs() + .execute_returns(ActiveProtocolState::::get().era); + + // advance a few eras, check value again + advance_to_era(7); + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_current_era {}, + ) + .expect_no_logs() + .execute_returns(ActiveProtocolState::::get().era); + }); +} + +#[test] +fn read_unbonding_period_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let unlocking_period_in_eras: EraNumber = + ::UnlockingPeriod::get(); + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_unbonding_period {}, + ) + .expect_no_logs() + .execute_returns(unlocking_period_in_eras); + }); +} + +#[test] +fn read_era_reward_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Check historic era for rewards + let era = 3; + advance_to_era(era + 1); + + let span_index = DAppStaking::::era_reward_span_index(era); + + let era_rewards_span = EraRewards::::get(span_index).expect("Entry must exist."); + let expected_reward = era_rewards_span + .get(era) + .map(|r| r.staker_reward_pool + r.dapp_reward_pool) + .expect("It's history era so it must exist."); + assert!(expected_reward > 0, "Sanity check."); + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_era_reward { era }, + ) + .expect_no_logs() + .execute_returns(expected_reward); + + // Check current era for rewards, must be zero + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_era_reward { era: era + 1 }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + }); +} + +#[test] +fn read_era_staked_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + let anchor_era = ActiveProtocolState::::get().era; + + // 1. Current era stake must be zero, since stake is only valid from the next era. + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { era: anchor_era }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { + era: anchor_era + 1, + }, + ) + .expect_no_logs() + .execute_returns(amount); + + // 2. Advance to next era, and check next era after the anchor. + advance_to_era(anchor_era + 5); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { + era: anchor_era + 1, + }, + ) + .expect_no_logs() + .execute_returns(amount); + + // 3. Check era after the next one, must throw an error. + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { + era: ActiveProtocolState::::get().era + 2, + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"Era is in the future"); + }); +} + +#[test] +fn read_staked_amount_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let staker_h160 = ALICE; + let dynamic_addresses = into_dynamic_addresses(staker_h160); + + // 1. Sanity checks - must be zero before anything is staked. + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount { + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + + // 2. Stake some amount and check again + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount { + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(amount); + } + + // 3. Advance into next period, it should be reset back to zero + advance_to_next_period(); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount { + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + }); +} + +#[test] +fn read_staked_amount_on_contract_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let dynamic_addresses = into_dynamic_addresses(staker_h160); + + // 1. Sanity checks - must be zero before anything is staked. + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount_on_contract { + contract_h160: smart_contract_address.into(), + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + + // 2. Stake some amount and check again + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount_on_contract { + contract_h160: smart_contract_address.into(), + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(amount); + } + + // 3. Advance into next period, it should be reset back to zero + advance_to_next_period(); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount_on_contract { + contract_h160: smart_contract_address.into(), + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + }); +} + +#[test] +fn read_contract_stake_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + + // 1. Sanity checks - must be zero before anything is staked. + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_contract_stake { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + + // 2. Stake some amount and check again + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_contract_stake { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(amount); + + // 3. Advance into next period, it should be reset back to zero + advance_to_next_period(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_contract_stake { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + }); +} + +#[test] +fn register_is_unsupported() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::register { + _address: Default::default(), + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"register via evm precompile is not allowed"); + }); +} + +#[test] +fn set_reward_destination_is_unsupported() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::set_reward_destination { _destination: 0 }, + ) + .expect_no_logs() + .execute_reverts(|output| { + output == b"Setting reward destination is no longer supported." + }); + }); +} + +#[test] +fn bond_and_stake_with_two_calls_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() + )); + + // Lock some amount, but not enough to cover the `bond_and_stake` call. + let pre_lock_amount = 500; + let stake_amount = 1_000_000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + pre_lock_amount, + )); + + // Execute legacy call, expect missing funds to be locked. + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::bond_and_stake { + contract_h160: smart_contract_address.into(), + amount: stake_amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 2); + let additional_lock_amount = stake_amount - pre_lock_amount; + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Locked { + amount, + .. + } if amount == additional_lock_amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == stake_amount + ); + }); +} + +#[test] +fn bond_and_stake_with_single_call_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() + )); + + // Lock enough amount to cover `bond_and_stake` call. + let amount = 3000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + amount, + )); + + // Execute legacy call, expect only single stake to be executed. + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::bond_and_stake { + contract_h160: smart_contract_address.into(), + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn unbond_and_unstake_with_two_calls_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Execute legacy call, expect funds to first unstaked, and then unlocked + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::unbond_and_unstake { + contract_h160: smart_contract_address.into(), + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 2); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + }if smart_contract == smart_contract && amount == amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Unlocking { amount, .. } if amount == amount + ); + }); +} + +#[test] +fn unbond_and_unstake_with_single_calls_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Unstake the entire amount, so only unlock call is expected. + assert_ok!(DappStaking::unstake( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + smart_contract.clone(), + amount, + )); + + // Execute legacy call, expect funds to be unlocked + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::unbond_and_unstake { + contract_h160: smart_contract_address.into(), + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unlocking { amount, .. } if amount == amount + ); + }); +} + +#[test] +fn withdraw_unbonded_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let staker_native = AddressMapper::into_account_id(staker_h160); + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Unlock some amount + assert_ok!(DappStaking::unstake( + RawOrigin::Signed(staker_native.clone()).into(), + smart_contract.clone(), + amount, + )); + let unlock_amount = amount / 7; + assert_ok!(DappStaking::unlock( + RawOrigin::Signed(staker_native.clone()).into(), + unlock_amount, + )); + + // Advance enough into time so unlocking chunk can be claimed + let unlock_block = Ledger::::get(&staker_native).unlocking[0].unlock_block; + run_to_block(unlock_block); + + // Execute legacy call, expect unlocked funds to be claimed back + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::withdraw_unbonded {}, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::ClaimedUnlocked { + amount, + .. + } if amount == unlock_amount + ); + }); +} + +#[test] +fn claim_dapp_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance enough eras so we can claim dApp reward + advance_to_era(3); + let claim_era = 2; + + // Execute legacy call, expect dApp rewards to be claimed + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_dapp { + contract_h160: smart_contract_address.into(), + era: claim_era, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::DAppReward { + era, + smart_contract, + .. + } if era as u128 == claim_era && smart_contract == smart_contract + ); + }); +} + +#[test] +fn claim_staker_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance enough eras so we can claim dApp reward + let target_era = 5; + advance_to_era(target_era); + let number_of_claims = (2..target_era).count(); + + // Execute legacy call, expect dApp rewards to be claimed + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_staker { + _contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect multiple reward to be claimed + let events = dapp_staking_events(); + assert_eq!(events.len(), number_of_claims as usize); + for era in 2..target_era { + assert_matches!( + events[era as usize - 2].clone(), + pallet_dapp_staking_v3::Event::Reward { era, .. } if era == era + ); + } + }); +} + +#[test] +fn withdraw_from_unregistered_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Unregister the dApp + assert_ok!(DappStaking::unregister( + RawOrigin::Root.into(), + smart_contract.clone() + )); + + // Execute legacy call, expect funds to be unstaked & withdrawn + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::withdraw_from_unregistered { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::UnstakeFromUnregistered { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn nomination_transfer_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register the first dApp, and stke on it. + let staker_h160 = ALICE; + let staker_native = AddressMapper::into_account_id(staker_h160); + let smart_contract_address_1 = H160::repeat_byte(0xFA); + let smart_contract_1 = + ::SmartContract::evm(smart_contract_address_1); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract_1.clone(), amount); + + // Register the second dApp. + let smart_contract_address_2 = H160::repeat_byte(0xBF); + let smart_contract_2 = + ::SmartContract::evm(smart_contract_address_2); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + staker_native.clone(), + smart_contract_2.clone() + )); + + // 1st scenario - transfer enough amount from the first to second dApp to cover the stake, + // but not enough for full unstake. + let minimum_stake_amount: Balance = + ::MinimumStakeAmount::get(); + + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::nomination_transfer { + origin_contract_h160: smart_contract_address_1.into(), + amount: minimum_stake_amount, + target_contract_h160: smart_contract_address_2.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect the same amount to be staked on the second contract + let events = dapp_staking_events(); + assert_eq!(events.len(), 2); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_1 && amount == minimum_stake_amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_2 && amount == minimum_stake_amount + ); + + // 2nd scenario - transfer almost the entire amount from the first to second dApp. + // The amount is large enough to trigger full unstake of the first contract. + let unstake_amount = amount - minimum_stake_amount - 1; + let expected_stake_unstake_amount = amount - minimum_stake_amount; + + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::nomination_transfer { + origin_contract_h160: smart_contract_address_1.into(), + amount: unstake_amount, + target_contract_h160: smart_contract_address_2.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect the same amount to be staked on the second contract + let events = dapp_staking_events(); + assert_eq!(events.len(), 2); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_1 && amount == expected_stake_unstake_amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_2 && amount == expected_stake_unstake_amount + ); + }); +} diff --git a/precompiles/dapp-staking-v3/src/test/tests_v2.rs b/precompiles/dapp-staking-v3/src/test/tests_v2.rs new file mode 100644 index 0000000000..5977417b5e --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/tests_v2.rs @@ -0,0 +1,526 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +extern crate alloc; +use crate::{test::mock::*, *}; +use frame_support::assert_ok; +use frame_system::RawOrigin; +use precompile_utils::testing::*; +use sp_core::H160; + +use assert_matches::assert_matches; + +use astar_primitives::{dapp_staking::CycleConfiguration, BlockNumber}; +use pallet_dapp_staking_v3::{ActiveProtocolState, EraNumber}; + +#[test] +fn protocol_state_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Prepare some mixed state in the future so not all entries are 'zero' + advance_to_next_period(); + advance_to_next_era(); + + let state = ActiveProtocolState::::get(); + + let expected_outcome = PrecompileProtocolState { + era: state.era.into(), + period: state.period_number().into(), + subperiod: subperiod_id(&state.subperiod()), + }; + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::protocol_state {}, + ) + .expect_no_logs() + .execute_returns(expected_outcome); + }); +} + +#[test] +fn unlocking_period_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let unlocking_period_in_eras: EraNumber = + ::UnlockingPeriod::get(); + let era_length: BlockNumber = + ::CycleConfiguration::blocks_per_era(); + + let expected_outcome = era_length * unlocking_period_in_eras; + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::unlocking_period {}, + ) + .expect_no_logs() + .execute_returns(expected_outcome); + }); +} + +#[test] +fn lock_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Lock some amount and verify event + let amount = 1234; + System::reset_events(); + precompiles() + .prepare_test(ALICE, precompile_address(), PrecompileCall::lock { amount }) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Locked { + amount, + .. + } if amount == amount + ); + }); +} + +#[test] +fn unlock_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let lock_amount = 1234; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), + lock_amount, + )); + + // Unlock some amount and verify event + System::reset_events(); + let unlock_amount = 1234 / 7; + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::unlock { + amount: unlock_amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unlocking { + amount, + .. + } if amount == unlock_amount + ); + }); +} + +#[test] +fn claim_unlocked_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Lock/unlock some amount to create unlocking chunk + let amount = 1234; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), + amount, + )); + assert_ok!(DappStaking::unlock( + RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), + amount, + )); + + // Advance enough into time so unlocking chunk can be claimed + let unlock_block = + Ledger::::get(&AddressMapper::into_account_id(ALICE)).unlocking[0].unlock_block; + run_to_block(unlock_block); + + // Claim unlocked chunk and verify event + System::reset_events(); + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::claim_unlocked {}, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::ClaimedUnlocked { + amount, + .. + } if amount == amount + ); + }); +} + +#[test] +fn stake_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_h160 = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_h160); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() + )); + + // Lock some amount which will be used for staking + let amount = 2000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + amount, + )); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Evm, + address: smart_contract_h160.as_bytes().try_into().unwrap(), + }; + + // Stake some amount and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::stake { + smart_contract: smart_contract_v2, + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn unstake_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() + )); + + // Lock & stake some amount + let amount = 2000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + amount, + )); + assert_ok!(DappStaking::stake( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + smart_contract.clone(), + amount, + )); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Unstake some amount and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::unstake { + smart_contract: smart_contract_v2, + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn claim_staker_rewards_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance a few eras so we can claim a few rewards + let target_era = 7; + advance_to_era(target_era); + let number_of_claims = (2..target_era).count(); + + // Claim staker rewards and verify events + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_staker_rewards {}, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect multiple reward to be claimed + let events = dapp_staking_events(); + assert_eq!(events.len(), number_of_claims as usize); + for era in 2..target_era { + assert_matches!( + events[era as usize - 2].clone(), + pallet_dapp_staking_v3::Event::Reward { era, .. } if era == era + ); + } + }); +} + +#[test] +fn claim_bonus_reward_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it, loyally + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance to the next period + advance_to_next_period(); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Claim bonus reward and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_bonus_reward { + smart_contract: smart_contract_v2, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::BonusReward { smart_contract, .. } if smart_contract == smart_contract + ); + }); +} + +#[test] +fn claim_dapp_reward_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance to 3rd era so we claim rewards for the 2nd era + advance_to_era(3); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Claim dApp reward and verify event + let claim_era: EraNumber = 2; + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_dapp_reward { + smart_contract: smart_contract_v2, + era: claim_era.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::DAppReward { era, smart_contract, .. } if era == claim_era && smart_contract == smart_contract + ); + }); +} + +#[test] +fn unstake_from_unregistered_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Unregister the dApp + assert_ok!(DappStaking::unregister( + RawOrigin::Root.into(), + smart_contract.clone() + )); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Unstake from the unregistered dApp and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::unstake_from_unregistered { + smart_contract: smart_contract_v2, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::UnstakeFromUnregistered { smart_contract, amount, .. } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn cleanup_expired_entries_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Advance over to the Build&Earn subperiod + advance_to_next_subperiod(); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "Sanity check." + ); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance over to the next period so the entry for dApp becomes expired + advance_to_next_period(); + + // Cleanup single expired entry and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::cleanup_expired_entries {}, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::ExpiredEntriesRemoved { count, .. } if count == 1 + ); + }); +} diff --git a/precompiles/dapp-staking-v3/src/test/types.rs b/precompiles/dapp-staking-v3/src/test/types.rs new file mode 100644 index 0000000000..186300e677 --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/types.rs @@ -0,0 +1,154 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +extern crate alloc; +use crate::{test::mock::*, *}; + +use assert_matches::assert_matches; + +#[test] +fn smart_contract_types_are_ok() { + // Verify Astar EVM smart contract type + { + let index: u8 = SmartContractTypes::Evm.into(); + assert_eq!(index, 0); + assert_eq!(Ok(SmartContractTypes::Evm), index.try_into()); + } + + // Verify Astar WASM smart contract type + { + let index: u8 = SmartContractTypes::Wasm.into(); + assert_eq!(index, 1); + assert_eq!(Ok(SmartContractTypes::Wasm), index.try_into()); + } + + // Negative case + { + let index: u8 = 2; + let maybe_smart_contract: Result = index.try_into(); + assert_matches!(maybe_smart_contract, Err(_)); + } +} + +#[test] +fn decode_smart_contract_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // Astar EVM smart contract decoding + { + let address = H160::repeat_byte(0xCA); + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Evm, + address: address.as_bytes().into(), + }; + + assert_eq!( + Ok(::SmartContract::evm(address)), + DappStakingV3Precompile::::decode_smart_contract(smart_contract_v2) + ); + } + + // Astar WASM smart contract decoding + { + let address = [0x6E; 32]; + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: address.into(), + }; + + assert_eq!( + Ok(::SmartContract::wasm(address.into())), + DappStakingV3Precompile::::decode_smart_contract(smart_contract_v2) + ); + } + }); +} + +#[test] +fn decode_smart_contract_fails_when_type_and_address_mismatch() { + ExternalityBuilder::build().execute_with(|| { + // H160 address for Wasm smart contract type + { + let address = H160::repeat_byte(0xCA); + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: address.as_bytes().into(), + }; + + assert_matches!( + DappStakingV3Precompile::::decode_smart_contract(smart_contract_v2), + Err(_) + ); + } + + // Native address for EVM smart contract type + { + let address = [0x6E; 32]; + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Evm, + address: address.into(), + }; + + assert_matches!( + DappStakingV3Precompile::::decode_smart_contract(smart_contract_v2), + Err(_) + ); + } + }); +} + +#[test] +fn parse_input_address_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // H160 address + { + let address_h160 = H160::repeat_byte(0xCA); + let address_native = AddressMapper::into_account_id(address_h160); + + assert_eq!( + DappStakingV3Precompile::::parse_input_address( + address_h160.as_bytes().into() + ), + Ok(address_native) + ); + } + + // Native address + { + let address_native = [0x6E; 32]; + + assert_eq!( + DappStakingV3Precompile::::parse_input_address(address_native.into()), + Ok(address_native.into()) + ); + } + }); +} + +#[test] +fn parse_input_address_fails_with_incorrect_address_length() { + ExternalityBuilder::build().execute_with(|| { + let addresses: Vec<&[u8]> = vec![&[0x6E; 19], &[0xA1; 21], &[0xC3; 31], &[0x99; 33]]; + + for address in addresses { + assert_matches!( + DappStakingV3Precompile::::parse_input_address(address.into()), + Err(_) + ); + } + }); +} diff --git a/primitives/src/dapp_staking.rs b/primitives/src/dapp_staking.rs index 92ead96b15..baf7ff34b5 100644 --- a/primitives/src/dapp_staking.rs +++ b/primitives/src/dapp_staking.rs @@ -18,6 +18,12 @@ use super::{Balance, BlockNumber}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; + +use frame_support::RuntimeDebug; +use sp_core::H160; +use sp_std::hash::Hash; + /// Configuration for cycles, periods, subperiods & eras. /// /// * `cycle` - Time unit similar to 'year' in the real world. Consists of one or more periods. At the beginning of each cycle, inflation is recalculated. @@ -83,3 +89,50 @@ pub trait StakingRewardHandler { /// Attempts to pay out the rewards to the beneficiary. fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()>; } + +/// Trait defining the interface for dApp staking `smart contract types` handler. +/// +/// It can be used to create a representation of the specified smart contract instance type. +pub trait SmartContractHandle { + /// Create a new smart contract representation for the specified EVM address. + fn evm(address: H160) -> Self; + /// Create a new smart contract representation for the specified Wasm address. + fn wasm(address: AccountId) -> Self; +} + +/// Multi-VM pointer to smart contract instance. +#[derive( + PartialEq, + Eq, + Copy, + Clone, + Encode, + Decode, + RuntimeDebug, + MaxEncodedLen, + Hash, + scale_info::TypeInfo, +)] +pub enum SmartContract { + /// EVM smart contract instance. + Evm(H160), + /// Wasm smart contract instance. + Wasm(AccountId), +} + +// TODO: remove this once dApps staking v2 has been removed. +impl Default for SmartContract { + fn default() -> Self { + Self::evm([0x01; 20].into()) + } +} + +impl SmartContractHandle for SmartContract { + fn evm(address: H160) -> Self { + Self::Evm(address) + } + + fn wasm(address: AccountId) -> Self { + Self::Wasm(address) + } +} diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index 0f31ebec72..de1e2ac1a6 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -76,7 +76,7 @@ pallet-dapp-staking-v3 = { workspace = true } pallet-dapps-staking = { workspace = true } pallet-dynamic-evm-base-fee = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } -pallet-evm-precompile-dapps-staking = { workspace = true } +pallet-evm-precompile-dapp-staking-v3 = { workspace = true } pallet-evm-precompile-sr25519 = { workspace = true } pallet-evm-precompile-substrate-ecdsa = { workspace = true } pallet-evm-precompile-unified-accounts = { workspace = true } @@ -142,7 +142,7 @@ std = [ "pallet-evm-precompile-ed25519/std", "pallet-evm-precompile-modexp/std", "pallet-evm-precompile-sha3fips/std", - "pallet-evm-precompile-dapps-staking/std", + "pallet-evm-precompile-dapp-staking-v3/std", "pallet-evm-precompile-sr25519/std", "pallet-evm-precompile-substrate-ecdsa/std", "pallet-evm-precompile-unified-accounts/std", diff --git a/runtime/local/src/chain_extensions.rs b/runtime/local/src/chain_extensions.rs index 516bc8040b..87fc75da1a 100644 --- a/runtime/local/src/chain_extensions.rs +++ b/runtime/local/src/chain_extensions.rs @@ -28,10 +28,6 @@ pub use pallet_chain_extension_xvm::XvmExtension; // Following impls defines chain extension IDs. -impl RegisteredChainExtension for DappsStakingExtension { - const ID: u16 = 00; -} - impl RegisteredChainExtension for XvmExtension { const ID: u16 = 01; } diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 71a002fef6..c5d8abae9a 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -62,7 +62,7 @@ use sp_runtime::{ use sp_std::prelude::*; pub use astar_primitives::{ - dapp_staking::{CycleConfiguration, StakingRewardHandler}, + dapp_staking::{CycleConfiguration, SmartContract, StakingRewardHandler}, evm::{EvmRevertCodeHandler, HashedDefaultMappings}, AccountId, Address, AssetId, Balance, BlockNumber, Hash, Header, Index, Signature, }; @@ -482,29 +482,6 @@ impl pallet_dapps_staking::Config for Runtime { type ForcePalletDisabled = ConstBool; // This will be set to `true` when needed } -/// Multi-VM pointer to smart contract instance. -#[derive( - PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, MaxEncodedLen, scale_info::TypeInfo, -)] -pub enum SmartContract { - /// EVM smart contract instance. - Evm(sp_core::H160), - /// Wasm smart contract instance. - Wasm(AccountId), -} - -impl Default for SmartContract { - fn default() -> Self { - SmartContract::Evm(H160::repeat_byte(0x00)) - } -} - -impl> From<[u8; 32]> for SmartContract { - fn from(input: [u8; 32]) -> Self { - SmartContract::Wasm(input.into()) - } -} - pub struct DummyPriceProvider; impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { fn average_price() -> FixedU64 { @@ -943,7 +920,7 @@ impl pallet_contracts::Config for Runtime { type WeightPrice = pallet_transaction_payment::Pallet; type WeightInfo = pallet_contracts::weights::SubstrateWeight; type ChainExtension = ( - DappsStakingExtension, + // DappsStakingExtension, XvmExtension, AssetsExtension>, UnifiedAccountsExtension, diff --git a/runtime/local/src/precompiles.rs b/runtime/local/src/precompiles.rs index 151c7ca577..d207068f4b 100644 --- a/runtime/local/src/precompiles.rs +++ b/runtime/local/src/precompiles.rs @@ -24,7 +24,7 @@ use frame_support::{parameter_types, traits::Contains}; use pallet_evm_precompile_assets_erc20::Erc20AssetsPrecompileSet; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; -use pallet_evm_precompile_dapps_staking::DappsStakingWrapper; +use pallet_evm_precompile_dapp_staking_v3::DappStakingV3Precompile; use pallet_evm_precompile_dispatch::Dispatch; use pallet_evm_precompile_ed25519::Ed25519Verify; use pallet_evm_precompile_modexp::Modexp; @@ -92,7 +92,7 @@ pub type LocalPrecompilesSetAt = ( // Local specific precompiles: PrecompileAt< AddressU64<20481>, - DappsStakingWrapper, + DappStakingV3Precompile, (CallableByContract, CallableByPrecompile), >, PrecompileAt< diff --git a/runtime/shibuya/Cargo.toml b/runtime/shibuya/Cargo.toml index 49eb2d6c01..73c2b42296 100644 --- a/runtime/shibuya/Cargo.toml +++ b/runtime/shibuya/Cargo.toml @@ -97,7 +97,6 @@ orml-xtokens = { workspace = true } # Astar pallets astar-primitives = { workspace = true } pallet-block-rewards-hybrid = { workspace = true } -pallet-chain-extension-dapps-staking = { workspace = true } pallet-chain-extension-unified-accounts = { workspace = true } pallet-chain-extension-xvm = { workspace = true } pallet-collator-selection = { workspace = true } @@ -165,7 +164,6 @@ std = [ "pallet-block-rewards-hybrid/std", "pallet-contracts/std", "pallet-contracts-primitives/std", - "pallet-chain-extension-dapps-staking/std", "pallet-chain-extension-xvm/std", "pallet-chain-extension-unified-accounts/std", "pallet-dynamic-evm-base-fee/std", diff --git a/runtime/shibuya/src/chain_extensions.rs b/runtime/shibuya/src/chain_extensions.rs index 516bc8040b..a368fa8d18 100644 --- a/runtime/shibuya/src/chain_extensions.rs +++ b/runtime/shibuya/src/chain_extensions.rs @@ -22,16 +22,11 @@ use super::{Runtime, UnifiedAccounts, Xvm}; pub use pallet_chain_extension_assets::AssetsExtension; use pallet_contracts::chain_extension::RegisteredChainExtension; -pub use pallet_chain_extension_dapps_staking::DappsStakingExtension; pub use pallet_chain_extension_unified_accounts::UnifiedAccountsExtension; pub use pallet_chain_extension_xvm::XvmExtension; // Following impls defines chain extension IDs. -impl RegisteredChainExtension for DappsStakingExtension { - const ID: u16 = 00; -} - impl RegisteredChainExtension for XvmExtension { const ID: u16 = 01; } diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index 38a59fe5ed..3a4a0a3119 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -667,7 +667,6 @@ impl pallet_contracts::Config for Runtime { type WeightPrice = pallet_transaction_payment::Pallet; type WeightInfo = pallet_contracts::weights::SubstrateWeight; type ChainExtension = ( - DappsStakingExtension, XvmExtension, AssetsExtension>, UnifiedAccountsExtension, From b7983e99629a2b27a75f2bdc876a918ab072b0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:07:42 +0100 Subject: [PATCH 13/14] Fix for incorrect freeze amount (#1111) --- pallets/dapp-staking-v3/src/lib.rs | 2 +- .../dapp-staking-v3/src/test/testing_utils.rs | 5 +- pallets/dapp-staking-v3/src/test/tests.rs | 61 ++++++++++++++++++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index a99b36af95..a20c921dfc 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1528,7 +1528,7 @@ pub mod pallet { T::Currency::set_freeze( &FreezeReason::DAppStaking.into(), account, - ledger.active_locked_amount(), + ledger.total_locked_amount(), )?; Ledger::::insert(account, ledger); } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index a973bba7ac..8f372a53ab 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -326,8 +326,9 @@ pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { ); assert_eq!( - init_frozen_balance - expected_unlock_amount, - Balances::balance_frozen(&FreezeReason::DAppStaking.into(), &account) + init_frozen_balance , + Balances::balance_frozen(&FreezeReason::DAppStaking.into(), &account), + "Frozen balance must remain the same since the funds are still locked/frozen, only undergoing the unlocking process." ); } diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index d6154a319a..12a06baebc 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -25,7 +25,7 @@ use crate::{ use frame_support::{ assert_noop, assert_ok, assert_storage_noop, error::BadOrigin, - traits::{Currency, Get, OnFinalize, OnInitialize}, + traits::{fungible::Unbalanced as FunUnbalanced, Currency, Get, OnFinalize, OnInitialize}, }; use sp_runtime::traits::Zero; @@ -2520,3 +2520,62 @@ fn stake_after_period_ends_with_max_staked_contracts() { } }) } + +#[test] +fn post_unlock_balance_cannot_be_transfered() { + ExtBuilder::build().execute_with(|| { + let staker = 2; + + // Lock some of the free balance + let init_free_balance = Balances::free_balance(&staker); + let lock_amount = init_free_balance / 3; + assert_lock(staker, lock_amount); + + // Make sure second account is empty + let other_account = 42; + assert_ok!(Balances::write_balance(&other_account, 0)); + + // 1. Ensure we can only transfer what is not locked/frozen. + assert_ok!(Balances::transfer_all( + RuntimeOrigin::signed(staker), + other_account, + true + )); + assert_eq!( + Balances::free_balance(&other_account), + init_free_balance - lock_amount, + "Only what is locked can be transferred." + ); + + // 2. Start the 'unlocking process' for the locked amount, but ensure it still cannot be transferred. + assert_unlock(staker, lock_amount); + + assert_ok!(Balances::write_balance(&other_account, 0)); + assert_ok!(Balances::transfer_all( + RuntimeOrigin::signed(staker), + other_account, + true + )); + assert!( + Balances::free_balance(&other_account).is_zero(), + "Nothing could have been transferred since it's still locked/frozen." + ); + + // 3. Claim the unlocked chunk, and ensure it can be transferred afterwards. + run_to_block(Ledger::::get(&staker).unlocking[0].unlock_block); + assert_claim_unlocked(staker); + + assert_ok!(Balances::write_balance(&other_account, 0)); + assert_ok!(Balances::transfer_all( + RuntimeOrigin::signed(staker), + other_account, + false + )); + assert_eq!( + Balances::free_balance(&other_account), + lock_amount, + "Everything should have been transferred." + ); + assert!(Balances::free_balance(&staker).is_zero()); + }) +} From 9fbbbaadbf8975b151cf380427e42f762d0716b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Wed, 20 Dec 2023 16:36:54 +0100 Subject: [PATCH 14/14] dApp Staking v3 - Shibuya integration (#1109) * dApp staking v3 - Shibuya integration * Init * Fix build * Fix find/replace doc mess * Migration & disable * More integration * Progress * Adjusted integration * Finished integration * Additional modifications & cleanup * Move comment * Fixes * Shibuya integration tests fix & proxy * Renaming * Integration test fixes & legacy support * Adjust for benchmarks * Remove chain-extension, small updates * fixes * Partial weights * Minor changes * Benchmark fixes * dApp staking weights * Weights, deps * Remove redundant storage item * Inflation params, resolve TODOs * Optimize lengthy benchmark * Integration test * Sort out more TODOs * Benchmark optimization * Fix seed * Remove spec version bump * Fix integration test * Weights update --- Cargo.lock | 42 +- Cargo.toml | 3 - bin/collator/Cargo.toml | 1 - bin/collator/src/local/chain_spec.rs | 6 +- .../src/parachain/chain_spec/shibuya.rs | 58 +- chain-extensions/dapps-staking/Cargo.toml | 43 -- chain-extensions/dapps-staking/src/lib.rs | 366 ---------- .../types/dapps-staking/Cargo.toml | 26 - .../types/dapps-staking/src/lib.rs | 154 ---- .../src/benchmarking.rs | 17 +- pallets/dapp-staking-migration/src/lib.rs | 24 +- pallets/dapp-staking-migration/src/weights.rs | 102 +-- .../dapp-staking-v3/src/benchmarking/mod.rs | 138 ++-- .../dapp-staking-v3/src/benchmarking/utils.rs | 87 ++- pallets/dapp-staking-v3/src/lib.rs | 107 ++- pallets/dapp-staking-v3/src/test/mock.rs | 7 +- .../dapp-staking-v3/src/test/testing_utils.rs | 27 +- pallets/dapp-staking-v3/src/types.rs | 6 +- pallets/dapp-staking-v3/src/weights.rs | 670 +++++++++--------- pallets/dapps-staking/src/pallet/mod.rs | 2 +- pallets/inflation/Cargo.toml | 2 + pallets/inflation/src/lib.rs | 49 +- pallets/inflation/src/weights.rs | 88 ++- runtime/local/Cargo.toml | 2 - runtime/local/src/chain_extensions.rs | 1 - runtime/local/src/lib.rs | 12 +- runtime/shibuya/Cargo.toml | 25 +- runtime/shibuya/src/lib.rs | 315 ++++++-- runtime/shibuya/src/precompiles.rs | 6 +- runtime/shibuya/src/weights/mod.rs | 3 + .../weights/pallet_dapp_staking_migration.rs | 133 ++++ .../src/weights/pallet_dapp_staking_v3.rs | 432 +++++++++++ .../shibuya/src/weights/pallet_inflation.rs | 105 +++ tests/integration/Cargo.toml | 1 + .../src/dispatch_precompile_filter_new.rs | 207 ++++++ ...r.rs => dispatch_precompile_filter_old.rs} | 10 +- tests/integration/src/lib.rs | 17 +- tests/integration/src/proxy_new.rs | 222 ++++++ .../src/{proxy.rs => proxy_old.rs} | 2 +- tests/integration/src/setup.rs | 24 +- 40 files changed, 2272 insertions(+), 1270 deletions(-) delete mode 100644 chain-extensions/dapps-staking/Cargo.toml delete mode 100644 chain-extensions/dapps-staking/src/lib.rs delete mode 100644 chain-extensions/types/dapps-staking/Cargo.toml delete mode 100644 chain-extensions/types/dapps-staking/src/lib.rs create mode 100644 runtime/shibuya/src/weights/pallet_dapp_staking_migration.rs create mode 100644 runtime/shibuya/src/weights/pallet_dapp_staking_v3.rs create mode 100644 runtime/shibuya/src/weights/pallet_inflation.rs create mode 100644 tests/integration/src/dispatch_precompile_filter_new.rs rename tests/integration/src/{dispatch_precompile_filter.rs => dispatch_precompile_filter_old.rs} (95%) create mode 100644 tests/integration/src/proxy_new.rs rename tests/integration/src/{proxy.rs => proxy_old.rs} (98%) diff --git a/Cargo.lock b/Cargo.lock index 452564a7c6..1c3e62ad02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -456,7 +456,6 @@ dependencies = [ "moonbeam-rpc-primitives-txpool", "moonbeam-rpc-trace", "moonbeam-rpc-txpool", - "pallet-dapp-staking-v3", "pallet-ethereum", "pallet-evm", "pallet-transaction-payment", @@ -2444,17 +2443,6 @@ dependencies = [ "sp-api", ] -[[package]] -name = "dapps-staking-chain-extension-types" -version = "1.1.0" -dependencies = [ - "frame-support", - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-runtime", -] - [[package]] name = "darling" version = "0.14.4" @@ -4977,6 +4965,7 @@ dependencies = [ "pallet-balances", "pallet-contracts", "pallet-contracts-primitives", + "pallet-dapp-staking-v3", "pallet-dapps-staking", "pallet-ethereum-checked", "pallet-evm", @@ -6040,7 +6029,6 @@ dependencies = [ "pallet-balances", "pallet-block-rewards-hybrid", "pallet-chain-extension-assets", - "pallet-chain-extension-dapps-staking", "pallet-chain-extension-unified-accounts", "pallet-chain-extension-xvm", "pallet-collective", @@ -7496,25 +7484,6 @@ dependencies = [ "sp-std", ] -[[package]] -name = "pallet-chain-extension-dapps-staking" -version = "1.1.0" -dependencies = [ - "dapps-staking-chain-extension-types", - "frame-support", - "frame-system", - "log", - "num-traits", - "pallet-contracts", - "pallet-contracts-primitives", - "pallet-dapps-staking", - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-runtime", - "sp-std", -] - [[package]] name = "pallet-chain-extension-unified-accounts" version = "0.1.0" @@ -8318,6 +8287,7 @@ dependencies = [ "serde", "sp-core", "sp-runtime", + "sp-std", ] [[package]] @@ -13178,6 +13148,7 @@ dependencies = [ "cumulus-primitives-core", "cumulus-primitives-timestamp", "cumulus-primitives-utility", + "dapp-staking-v3-runtime-api", "fp-rpc", "fp-self-contained", "frame-benchmarking", @@ -13199,7 +13170,6 @@ dependencies = [ "pallet-aura", "pallet-authorship", "pallet-balances", - "pallet-block-rewards-hybrid", "pallet-chain-extension-assets", "pallet-chain-extension-unified-accounts", "pallet-chain-extension-xvm", @@ -13207,6 +13177,8 @@ dependencies = [ "pallet-collective", "pallet-contracts", "pallet-contracts-primitives", + "pallet-dapp-staking-migration", + "pallet-dapp-staking-v3", "pallet-dapps-staking", "pallet-democracy", "pallet-dynamic-evm-base-fee", @@ -13217,7 +13189,7 @@ dependencies = [ "pallet-evm-precompile-assets-erc20", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", - "pallet-evm-precompile-dapps-staking", + "pallet-evm-precompile-dapp-staking-v3", "pallet-evm-precompile-dispatch", "pallet-evm-precompile-ed25519", "pallet-evm-precompile-modexp", @@ -13229,6 +13201,7 @@ dependencies = [ "pallet-evm-precompile-xcm", "pallet-evm-precompile-xvm", "pallet-identity", + "pallet-inflation", "pallet-insecure-randomness-collective-flip", "pallet-multisig", "pallet-preimage", @@ -13255,6 +13228,7 @@ dependencies = [ "scale-info", "smallvec 1.11.0", "sp-api", + "sp-arithmetic", "sp-block-builder", "sp-consensus-aura", "sp-core", diff --git a/Cargo.toml b/Cargo.toml index ca009583f4..2e5b80db4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ members = [ "primitives", - "chain-extensions/dapps-staking", "chain-extensions/pallet-assets", "chain-extensions/xvm", "chain-extensions/unified-accounts", @@ -298,12 +297,10 @@ pallet-evm-precompile-dapps-staking = { path = "./precompiles/dapps-staking", de pallet-evm-precompile-dapp-staking-v3 = { path = "./precompiles/dapp-staking-v3", default-features = false } pallet-evm-precompile-unified-accounts = { path = "./precompiles/unified-accounts", default-features = false } -pallet-chain-extension-dapps-staking = { path = "./chain-extensions/dapps-staking", default-features = false } pallet-chain-extension-xvm = { path = "./chain-extensions/xvm", default-features = false } pallet-chain-extension-assets = { path = "./chain-extensions/pallet-assets", default-features = false } pallet-chain-extension-unified-accounts = { path = "./chain-extensions/unified-accounts", default-features = false } -dapps-staking-chain-extension-types = { path = "./chain-extensions/types/dapps-staking", default-features = false } xvm-chain-extension-types = { path = "./chain-extensions/types/xvm", default-features = false } assets-chain-extension-types = { path = "./chain-extensions/types/assets", default-features = false } unified-accounts-chain-extension-types = { path = "./chain-extensions/types/unified-accounts", default-features = false } diff --git a/bin/collator/Cargo.toml b/bin/collator/Cargo.toml index 19c9bdba59..6676a9aa34 100644 --- a/bin/collator/Cargo.toml +++ b/bin/collator/Cargo.toml @@ -94,7 +94,6 @@ shiden-runtime = { workspace = true, features = ["std"] } # astar pallets dependencies astar-primitives = { workspace = true } -pallet-dapp-staking-v3 = { workspace = true } # frame dependencies frame-system = { workspace = true, features = ["std"] } diff --git a/bin/collator/src/local/chain_spec.rs b/bin/collator/src/local/chain_spec.rs index d5977cd3ef..8a510b6f67 100644 --- a/bin/collator/src/local/chain_spec.rs +++ b/bin/collator/src/local/chain_spec.rs @@ -22,8 +22,8 @@ use local_runtime::{ wasm_binary_unwrap, AccountId, AuraConfig, AuraId, BalancesConfig, BlockRewardConfig, CouncilConfig, DappStakingConfig, DemocracyConfig, EVMConfig, GenesisConfig, GrandpaConfig, GrandpaId, InflationConfig, InflationParameters, Precompiles, RewardDistributionConfig, - Signature, SudoConfig, SystemConfig, TechnicalCommitteeConfig, TreasuryConfig, VestingConfig, - AST, + Signature, SudoConfig, SystemConfig, TechnicalCommitteeConfig, TierThreshold, TreasuryConfig, + VestingConfig, AST, }; use sc_service::ChainType; use sp_core::{crypto::Ss58Codec, sr25519, Pair, Public}; @@ -32,8 +32,6 @@ use sp_runtime::{ Perbill, Permill, }; -use pallet_dapp_staking_v3::TierThreshold; - type AccountPublic = ::Signer; /// Specialized `ChainSpec` for Shiden Network. diff --git a/bin/collator/src/parachain/chain_spec/shibuya.rs b/bin/collator/src/parachain/chain_spec/shibuya.rs index ac4478afe1..200d324631 100644 --- a/bin/collator/src/parachain/chain_spec/shibuya.rs +++ b/bin/collator/src/parachain/chain_spec/shibuya.rs @@ -21,17 +21,17 @@ use cumulus_primitives_core::ParaId; use sc_service::ChainType; use shibuya_runtime::{ - wasm_binary_unwrap, AccountId, AuraConfig, AuraId, Balance, BalancesConfig, BlockRewardConfig, - CollatorSelectionConfig, CouncilConfig, DemocracyConfig, EVMChainIdConfig, EVMConfig, - GenesisConfig, ParachainInfoConfig, Precompiles, RewardDistributionConfig, SessionConfig, - SessionKeys, Signature, SudoConfig, SystemConfig, TechnicalCommitteeConfig, TreasuryConfig, - VestingConfig, SBY, + wasm_binary_unwrap, AccountId, AuraConfig, AuraId, Balance, BalancesConfig, + CollatorSelectionConfig, CouncilConfig, DappStakingConfig, DemocracyConfig, EVMChainIdConfig, + EVMConfig, GenesisConfig, InflationConfig, InflationParameters, ParachainInfoConfig, + Precompiles, SessionConfig, SessionKeys, Signature, SudoConfig, SystemConfig, + TechnicalCommitteeConfig, TierThreshold, TreasuryConfig, VestingConfig, SBY, }; use sp_core::{sr25519, Pair, Public}; use sp_runtime::{ traits::{IdentifyAccount, Verify}, - Perbill, + Permill, }; use super::{get_from_seed, Extensions}; @@ -114,17 +114,6 @@ fn make_genesis( }, parachain_info: ParachainInfoConfig { parachain_id }, balances: BalancesConfig { balances }, - block_reward: BlockRewardConfig { - // Make sure sum is 100 - reward_config: RewardDistributionConfig { - treasury_percent: Perbill::from_percent(10), - base_staker_percent: Perbill::from_percent(20), - dapps_percent: Perbill::from_percent(20), - collators_percent: Perbill::from_percent(5), - adjustable_percent: Perbill::from_percent(45), - ideal_dapps_staking_tvl: Perbill::from_percent(40), - }, - }, vesting: VestingConfig { vesting: vec![] }, session: SessionConfig { keys: authorities @@ -174,6 +163,41 @@ fn make_genesis( }, democracy: DemocracyConfig::default(), treasury: TreasuryConfig::default(), + dapp_staking: DappStakingConfig { + reward_portion: vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ], + slot_distribution: vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ], + // TODO: adjust this if needed + tier_thresholds: vec![ + TierThreshold::DynamicTvlAmount { + amount: 100 * SBY, + minimum_amount: 80 * SBY, + }, + TierThreshold::DynamicTvlAmount { + amount: 50 * SBY, + minimum_amount: 40 * SBY, + }, + TierThreshold::DynamicTvlAmount { + amount: 20 * SBY, + minimum_amount: 20 * SBY, + }, + TierThreshold::FixedTvlAmount { amount: 10 * SBY }, + ], + slots_per_tier: vec![10, 20, 30, 40], + }, + // TODO: adjust this if needed + inflation: InflationConfig { + params: InflationParameters::default(), + }, } } diff --git a/chain-extensions/dapps-staking/Cargo.toml b/chain-extensions/dapps-staking/Cargo.toml deleted file mode 100644 index 4169a7bd00..0000000000 --- a/chain-extensions/dapps-staking/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "pallet-chain-extension-dapps-staking" -version = "1.1.0" -license = "Apache-2.0" -description = "dApps Staking chain extension for WASM contracts" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -repository.workspace = true - -[dependencies] -frame-support = { workspace = true } -frame-system = { workspace = true } -log = { workspace = true } -num-traits = { workspace = true } -pallet-contracts = { workspace = true } -pallet-contracts-primitives = { workspace = true } -parity-scale-codec = { workspace = true } -scale-info = { workspace = true } -sp-core = { workspace = true } -sp-runtime = { workspace = true } -sp-std = { workspace = true } - -# Astar -dapps-staking-chain-extension-types = { workspace = true } -pallet-dapps-staking = { workspace = true } - -[features] -default = ["std"] -std = [ - "parity-scale-codec/std", - "dapps-staking-chain-extension-types/std", - "frame-support/std", - "frame-system/std", - "num-traits/std", - "pallet-contracts/std", - "pallet-contracts-primitives/std", - "pallet-dapps-staking/std", - "scale-info/std", - "sp-std/std", - "sp-core/std", - "sp-runtime/std", -] diff --git a/chain-extensions/dapps-staking/src/lib.rs b/chain-extensions/dapps-staking/src/lib.rs deleted file mode 100644 index 44910e3b0d..0000000000 --- a/chain-extensions/dapps-staking/src/lib.rs +++ /dev/null @@ -1,366 +0,0 @@ -// This file is part of Astar. - -// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// Astar is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Astar 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 General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Astar. If not, see . - -#![cfg_attr(not(feature = "std"), no_std)] -use sp_runtime::{traits::Zero, DispatchError}; - -use dapps_staking_chain_extension_types::{ - DSError, DappsStakingAccountInput, DappsStakingEraInput, DappsStakingNominationInput, - DappsStakingValueInput, -}; -use frame_support::traits::{Currency, Get}; -use frame_system::RawOrigin; -use pallet_contracts::chain_extension::{ - ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, -}; -use pallet_dapps_staking::{RewardDestination, WeightInfo}; -use parity_scale_codec::Encode; -use sp_std::marker::PhantomData; - -type BalanceOf = <::Currency as Currency< - ::AccountId, ->>::Balance; - -enum DappsStakingFunc { - CurrentEra, - UnbondingPeriod, - EraRewards, - EraStaked, - StakedAmount, - StakedAmountOnContract, - ReadContractStake, - BondAndStake, - UnbondAndUnstake, - WithdrawUnbonded, - ClaimStaker, - ClaimDapp, - SetRewardDestination, - NominationTransfer, -} - -impl TryFrom for DappsStakingFunc { - type Error = DispatchError; - - fn try_from(value: u16) -> Result { - match value { - 1 => Ok(DappsStakingFunc::CurrentEra), - 2 => Ok(DappsStakingFunc::UnbondingPeriod), - 3 => Ok(DappsStakingFunc::EraRewards), - 4 => Ok(DappsStakingFunc::EraStaked), - 5 => Ok(DappsStakingFunc::StakedAmount), - 6 => Ok(DappsStakingFunc::StakedAmountOnContract), - 7 => Ok(DappsStakingFunc::ReadContractStake), - 8 => Ok(DappsStakingFunc::BondAndStake), - 9 => Ok(DappsStakingFunc::UnbondAndUnstake), - 10 => Ok(DappsStakingFunc::WithdrawUnbonded), - 11 => Ok(DappsStakingFunc::ClaimStaker), - 12 => Ok(DappsStakingFunc::ClaimDapp), - 13 => Ok(DappsStakingFunc::SetRewardDestination), - 14 => Ok(DappsStakingFunc::NominationTransfer), - _ => Err(DispatchError::Other( - "DappsStakingExtension: Unimplemented func_id", - )), - } - } -} - -/// Dapps Staking chain extension. -pub struct DappsStakingExtension(PhantomData); - -impl Default for DappsStakingExtension { - fn default() -> Self { - DappsStakingExtension(PhantomData) - } -} - -impl ChainExtension for DappsStakingExtension -where - T: pallet_dapps_staking::Config + pallet_contracts::Config, - ::SmartContract: From<[u8; 32]>, - ::AccountId: From<[u8; 32]>, -{ - fn call(&mut self, env: Environment) -> Result - where - E: Ext, - { - let func_id = env.func_id().try_into()?; - let mut env = env.buf_in_buf_out(); - - match func_id { - DappsStakingFunc::CurrentEra => { - let base_weight = ::DbWeight::get().reads(1); - env.charge_weight(base_weight)?; - - let era_index = pallet_dapps_staking::CurrentEra::::get(); - env.write(&era_index.encode(), false, None)?; - } - - DappsStakingFunc::UnbondingPeriod => { - let base_weight = ::DbWeight::get().reads(1); - env.charge_weight(base_weight)?; - - let unbonding_period = T::UnbondingPeriod::get(); - env.write(&unbonding_period.encode(), false, None)?; - } - - DappsStakingFunc::EraRewards => { - let arg: u32 = env.read_as()?; - - let base_weight = ::DbWeight::get().reads(1); - env.charge_weight(base_weight)?; - - let era_info = pallet_dapps_staking::GeneralEraInfo::::get(arg); - let reward = era_info.map_or(Zero::zero(), |r| { - r.rewards.stakers.saturating_add(r.rewards.dapps) - }); - env.write(&reward.encode(), false, None)?; - } - - DappsStakingFunc::EraStaked => { - let arg: u32 = env.read_as()?; - - let base_weight = ::DbWeight::get().reads(1); - env.charge_weight(base_weight)?; - - let era_info = pallet_dapps_staking::GeneralEraInfo::::get(arg); - let staked_amount = era_info.map_or(Zero::zero(), |r| r.staked); - env.write(&staked_amount.encode(), false, None)?; - } - - DappsStakingFunc::StakedAmount => { - let staker: T::AccountId = env.read_as()?; - - let base_weight = ::DbWeight::get().reads(1); - env.charge_weight(base_weight)?; - - let ledger = pallet_dapps_staking::Ledger::::get(&staker); - env.write(&ledger.locked.encode(), false, None)?; - } - - DappsStakingFunc::StakedAmountOnContract => { - let args: DappsStakingAccountInput = env.read_as()?; - let staker: T::AccountId = args.staker.into(); - let contract: ::SmartContract = - args.contract.into(); - - let base_weight = ::DbWeight::get().reads(1); - env.charge_weight(base_weight)?; - - let staking_info = - pallet_dapps_staking::GeneralStakerInfo::::get(&staker, &contract); - let staked_amount = staking_info.latest_staked_value(); - env.write(&staked_amount.encode(), false, None)?; - } - - DappsStakingFunc::ReadContractStake => { - let contract_bytes: [u8; 32] = env.read_as()?; - let contract: ::SmartContract = - contract_bytes.into(); - - let base_weight = ::DbWeight::get().reads(1); - env.charge_weight(base_weight.saturating_add(base_weight))?; - - let current_era = pallet_dapps_staking::CurrentEra::::get(); - let staking_info = - pallet_dapps_staking::Pallet::::contract_stake_info(&contract, current_era) - .unwrap_or_default(); - let total = TryInto::::try_into(staking_info.total).unwrap_or(0); - env.write(&total.encode(), false, None)?; - } - - DappsStakingFunc::BondAndStake => { - let args: DappsStakingValueInput> = env.read_as()?; - let contract = args.contract.into(); - let value: BalanceOf = args.value; - - let base_weight = ::WeightInfo::bond_and_stake(); - env.charge_weight(base_weight)?; - - let caller = env.ext().address().clone(); - let call_result = pallet_dapps_staking::Pallet::::bond_and_stake( - RawOrigin::Signed(caller).into(), - contract, - value, - ); - return match call_result { - Err(e) => { - let mapped_error = DSError::try_from(e.error)?; - Ok(RetVal::Converging(mapped_error as u32)) - } - Ok(_) => Ok(RetVal::Converging(DSError::Success as u32)), - }; - } - - DappsStakingFunc::UnbondAndUnstake => { - let args: DappsStakingValueInput> = env.read_as()?; - let contract = args.contract.into(); - let value: BalanceOf = args.value; - - let base_weight = - ::WeightInfo::unbond_and_unstake(); - env.charge_weight(base_weight)?; - - let caller = env.ext().address().clone(); - let call_result = pallet_dapps_staking::Pallet::::unbond_and_unstake( - RawOrigin::Signed(caller).into(), - contract, - value, - ); - return match call_result { - Err(e) => { - let mapped_error = DSError::try_from(e.error)?; - Ok(RetVal::Converging(mapped_error as u32)) - } - Ok(_) => Ok(RetVal::Converging(DSError::Success as u32)), - }; - } - - DappsStakingFunc::WithdrawUnbonded => { - let caller = env.ext().address().clone(); - - let base_weight = - ::WeightInfo::withdraw_unbonded(); - env.charge_weight(base_weight)?; - - let call_result = pallet_dapps_staking::Pallet::::withdraw_unbonded( - RawOrigin::Signed(caller).into(), - ); - return match call_result { - Err(e) => { - let mapped_error = DSError::try_from(e.error)?; - Ok(RetVal::Converging(mapped_error as u32)) - } - Ok(_) => Ok(RetVal::Converging(DSError::Success as u32)), - }; - } - - DappsStakingFunc::ClaimStaker => { - let contract_bytes: [u8; 32] = env.read_as()?; - let contract = contract_bytes.into(); - - let base_weight = ::WeightInfo::claim_staker_with_restake() - .max(::WeightInfo::claim_staker_without_restake()); - let charged_weight = env.charge_weight(base_weight)?; - - let caller = env.ext().address().clone(); - let call_result = pallet_dapps_staking::Pallet::::claim_staker( - RawOrigin::Signed(caller).into(), - contract, - ); - - let actual_weight = match call_result { - Ok(e) => e.actual_weight, - Err(e) => e.post_info.actual_weight, - }; - if let Some(actual_weight) = actual_weight { - env.adjust_weight(charged_weight, actual_weight); - } - - return match call_result { - Err(e) => { - let mapped_error = DSError::try_from(e.error)?; - Ok(RetVal::Converging(mapped_error as u32)) - } - Ok(_) => Ok(RetVal::Converging(DSError::Success as u32)), - }; - } - - DappsStakingFunc::ClaimDapp => { - let args: DappsStakingEraInput = env.read_as()?; - let contract = args.contract.into(); - let era: u32 = args.era; - - let base_weight = ::WeightInfo::claim_dapp(); - env.charge_weight(base_weight)?; - - let caller = env.ext().address().clone(); - let call_result = pallet_dapps_staking::Pallet::::claim_dapp( - RawOrigin::Signed(caller).into(), - contract, - era, - ); - return match call_result { - Err(e) => { - let mapped_error = DSError::try_from(e.error)?; - Ok(RetVal::Converging(mapped_error as u32)) - } - Ok(_) => Ok(RetVal::Converging(DSError::Success as u32)), - }; - } - - DappsStakingFunc::SetRewardDestination => { - let reward_destination_raw: u8 = env.read_as()?; - - let base_weight = - ::WeightInfo::set_reward_destination(); - env.charge_weight(base_weight)?; - - // Transform raw value into dapps staking enum - let reward_destination = if reward_destination_raw == 0 { - RewardDestination::FreeBalance - } else if reward_destination_raw == 1 { - RewardDestination::StakeBalance - } else { - let error = DSError::RewardDestinationValueOutOfBounds; - return Ok(RetVal::Converging(error as u32)); - }; - - let caller = env.ext().address().clone(); - let call_result = pallet_dapps_staking::Pallet::::set_reward_destination( - RawOrigin::Signed(caller).into(), - reward_destination, - ); - return match call_result { - Err(e) => { - let mapped_error = DSError::try_from(e.error)?; - Ok(RetVal::Converging(mapped_error as u32)) - } - Ok(_) => Ok(RetVal::Converging(DSError::Success as u32)), - }; - } - - DappsStakingFunc::NominationTransfer => { - let args: DappsStakingNominationInput> = env.read_as()?; - let origin_smart_contract = args.origin_contract.into(); - let target_smart_contract = args.target_contract.into(); - let value: BalanceOf = args.value; - - let base_weight = - ::WeightInfo::nomination_transfer(); - env.charge_weight(base_weight)?; - - let caller = env.ext().address().clone(); - let call_result = pallet_dapps_staking::Pallet::::nomination_transfer( - RawOrigin::Signed(caller).into(), - origin_smart_contract, - value, - target_smart_contract, - ); - return match call_result { - Err(e) => { - let mapped_error = DSError::try_from(e.error)?; - Ok(RetVal::Converging(mapped_error as u32)) - } - Ok(_) => Ok(RetVal::Converging(DSError::Success as u32)), - }; - } - } - - Ok(RetVal::Converging(DSError::Success as u32)) - } -} diff --git a/chain-extensions/types/dapps-staking/Cargo.toml b/chain-extensions/types/dapps-staking/Cargo.toml deleted file mode 100644 index fcd45a0034..0000000000 --- a/chain-extensions/types/dapps-staking/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "dapps-staking-chain-extension-types" -version = "1.1.0" -license = "Apache-2.0" -description = "Types definitions for dapps-staking chain-extension" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -repository.workspace = true - -[dependencies] -frame-support = { workspace = true } -parity-scale-codec = { workspace = true } -scale-info = { workspace = true } -sp-core = { workspace = true } -sp-runtime = { workspace = true } - -[features] -default = ["std"] -std = [ - "parity-scale-codec/std", - "frame-support/std", - "scale-info/std", - "sp-core/std", - "sp-runtime/std", -] diff --git a/chain-extensions/types/dapps-staking/src/lib.rs b/chain-extensions/types/dapps-staking/src/lib.rs deleted file mode 100644 index 585a15213c..0000000000 --- a/chain-extensions/types/dapps-staking/src/lib.rs +++ /dev/null @@ -1,154 +0,0 @@ -// This file is part of Astar. - -// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// Astar is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Astar 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 General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Astar. If not, see . - -#![cfg_attr(not(feature = "std"), no_std)] -use frame_support::pallet_prelude::MaxEncodedLen; -use parity_scale_codec::{Decode, Encode}; -use sp_runtime::{DispatchError, ModuleError}; - -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] -pub enum DSError { - /// Success - Success = 0, - /// Disabled - Disabled = 1, - /// No change in maintenance mode - NoMaintenanceModeChange = 2, - /// Upgrade is too heavy, reduce the weight parameter. - UpgradeTooHeavy = 3, - /// Can not stake with zero value. - StakingWithNoValue = 4, - /// Can not stake with value less than minimum staking value - InsufficientValue = 5, - /// Number of stakers per contract exceeded. - MaxNumberOfStakersExceeded = 6, - /// Targets must be operated contracts - NotOperatedContract = 7, - /// Contract isn't staked. - NotStakedContract = 8, - /// Contract isn't unregistered. - NotUnregisteredContract = 9, - /// Unclaimed rewards should be claimed before withdrawing stake. - UnclaimedRewardsRemaining = 10, - /// Unstaking a contract with zero value - UnstakingWithNoValue = 11, - /// There are no previously unbonded funds that can be unstaked and withdrawn. - NothingToWithdraw = 12, - /// The contract is already registered by other account - AlreadyRegisteredContract = 13, - /// User attempts to register with address which is not contract - ContractIsNotValid = 14, - /// This account was already used to register contract - AlreadyUsedDeveloperAccount = 15, - /// Smart contract not owned by the account id. - NotOwnedContract = 16, - /// Report issue on github if this is ever emitted - UnknownEraReward = 17, - /// Report issue on github if this is ever emitted - UnexpectedStakeInfoEra = 18, - /// Contract has too many unlocking chunks. Withdraw the existing chunks if possible - /// or wait for current chunks to complete unlocking process to withdraw them. - TooManyUnlockingChunks = 19, - /// Contract already claimed in this era and reward is distributed - AlreadyClaimedInThisEra = 20, - /// Era parameter is out of bounds - EraOutOfBounds = 21, - /// Too many active `EraStake` values for (staker, contract) pairing. - /// Claim existing rewards to fix this problem. - TooManyEraStakeValues = 22, - /// To register a contract, pre-approval is needed for this address - RequiredContractPreApproval = 23, - /// Developer's account is already part of pre-approved list - AlreadyPreApprovedDeveloper = 24, - /// Account is not actively staking - NotActiveStaker = 25, - /// Transfering nomination to the same contract - NominationTransferToSameContract = 26, - /// Unexpected reward destination value - RewardDestinationValueOutOfBounds = 27, - /// Unknown error - UnknownError = 99, -} - -impl TryFrom for DSError { - type Error = DispatchError; - - fn try_from(input: DispatchError) -> Result { - let error_text = match input { - DispatchError::Module(ModuleError { message, .. }) => message, - _ => Some("No module error Info"), - }; - return match error_text { - Some("Disabled") => Ok(DSError::Disabled), - Some("NoMaintenanceModeChange") => Ok(DSError::NoMaintenanceModeChange), - Some("UpgradeTooHeavy") => Ok(DSError::UpgradeTooHeavy), - Some("StakingWithNoValue") => Ok(DSError::StakingWithNoValue), - Some("InsufficientValue") => Ok(DSError::InsufficientValue), - Some("MaxNumberOfStakersExceeded") => Ok(DSError::MaxNumberOfStakersExceeded), - Some("NotOperatedContract") => Ok(DSError::NotOperatedContract), - Some("NotStakedContract") => Ok(DSError::NotStakedContract), - Some("NotUnregisteredContract") => Ok(DSError::NotUnregisteredContract), - Some("UnclaimedRewardsRemaining") => Ok(DSError::UnclaimedRewardsRemaining), - Some("UnstakingWithNoValue") => Ok(DSError::UnstakingWithNoValue), - Some("NothingToWithdraw") => Ok(DSError::NothingToWithdraw), - Some("AlreadyRegisteredContract") => Ok(DSError::AlreadyRegisteredContract), - Some("ContractIsNotValid") => Ok(DSError::ContractIsNotValid), - Some("AlreadyUsedDeveloperAccount") => Ok(DSError::AlreadyUsedDeveloperAccount), - Some("NotOwnedContract") => Ok(DSError::NotOwnedContract), - Some("UnknownEraReward") => Ok(DSError::UnknownEraReward), - Some("UnexpectedStakeInfoEra") => Ok(DSError::UnexpectedStakeInfoEra), - Some("TooManyUnlockingChunks") => Ok(DSError::TooManyUnlockingChunks), - Some("AlreadyClaimedInThisEra") => Ok(DSError::AlreadyClaimedInThisEra), - Some("EraOutOfBounds") => Ok(DSError::EraOutOfBounds), - Some("TooManyEraStakeValues") => Ok(DSError::TooManyEraStakeValues), - Some("RequiredContractPreApproval") => Ok(DSError::RequiredContractPreApproval), - Some("AlreadyPreApprovedDeveloper") => Ok(DSError::AlreadyPreApprovedDeveloper), - Some("NotActiveStaker") => Ok(DSError::NotActiveStaker), - Some("NominationTransferToSameContract") => { - Ok(DSError::NominationTransferToSameContract) - } - _ => Ok(DSError::UnknownError), - }; - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen)] -pub struct DappsStakingValueInput { - pub contract: [u8; 32], - pub value: Balance, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen)] -pub struct DappsStakingAccountInput { - pub contract: [u8; 32], - pub staker: [u8; 32], -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen)] -pub struct DappsStakingEraInput { - pub contract: [u8; 32], - pub era: u32, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen)] -pub struct DappsStakingNominationInput { - pub origin_contract: [u8; 32], - pub target_contract: [u8; 32], - pub value: Balance, -} diff --git a/pallets/dapp-staking-migration/src/benchmarking.rs b/pallets/dapp-staking-migration/src/benchmarking.rs index 6570322068..d26f810241 100644 --- a/pallets/dapp-staking-migration/src/benchmarking.rs +++ b/pallets/dapp-staking-migration/src/benchmarking.rs @@ -19,7 +19,7 @@ use super::{Pallet as Migration, *}; use frame_benchmarking::{account as benchmark_account, v2::*}; -use frame_support::{assert_ok, storage::unhashed::put_raw, traits::Currency}; +use frame_support::{assert_ok, traits::Currency}; /// Generate an unique smart contract using the provided index as a sort-of indetifier fn smart_contract(index: u8) -> T::SmartContract { @@ -107,20 +107,13 @@ mod benchmarks { #[benchmark] fn cleanup_old_storage_success() { - let hashed_prefix = twox_128(pallet_dapps_staking::Pallet::::name().as_bytes()); - let _ = clear_prefix(&hashed_prefix, None); - - put_raw(&hashed_prefix, &[0xFF; 128]); + initial_config::(); #[block] { - if cfg!(test) { - // TODO: for some reason, tests always fail here, nothing gets removed from storage. - // When tested against real runtime, it works just fine. - let _ = Migration::::cleanup_old_storage(1); - } else { - assert!(Migration::::cleanup_old_storage(1).is_ok()); - } + // TODO: for some reason, tests always fail here, nothing gets removed from storage. + // When tested against real runtime, it works just fine. + let _ = Migration::::cleanup_old_storage(1); } } diff --git a/pallets/dapp-staking-migration/src/lib.rs b/pallets/dapp-staking-migration/src/lib.rs index be26dedcd4..d6fdefe803 100644 --- a/pallets/dapp-staking-migration/src/lib.rs +++ b/pallets/dapp-staking-migration/src/lib.rs @@ -581,10 +581,26 @@ pub mod pallet { .collect(); // Get the stakers and their active locked (staked) amount. + + let min_lock_amount: Balance = + ::MinimumLockedAmount::get(); let stakers: Vec<_> = pallet_dapps_staking::Ledger::::iter() - .map(|(staker, ledger)| (staker, ledger.locked)) + .filter_map(|(staker, ledger)| { + if ledger.locked >= min_lock_amount { + Some((staker, ledger.locked)) + } else { + None + } + }) .collect(); + log::info!( + target: LOG_TARGET, + "Out of {} stakers, {} have sufficient amount to lock.", + pallet_dapps_staking::Ledger::::iter().count(), + stakers.len(), + ); + let helper = Helper:: { developers, stakers, @@ -651,6 +667,12 @@ pub mod pallet { total_locked ); + log::info!( + target: LOG_TARGET, + "Total locked amount in the new pallet: {:?}.", + total_locked, + ); + // 3. Check that rest of the storage has been cleaned up. assert!(!pallet_dapps_staking::PalletDisabled::::exists()); assert!(!pallet_dapps_staking::CurrentEra::::exists()); diff --git a/pallets/dapp-staking-migration/src/weights.rs b/pallets/dapp-staking-migration/src/weights.rs index 158adad022..313e4c7070 100644 --- a/pallets/dapp-staking-migration/src/weights.rs +++ b/pallets/dapp-staking-migration/src/weights.rs @@ -20,16 +20,16 @@ //! Autogenerated weights for pallet_dapp_staking_migration //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-12-13, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-12-20, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Dinos-MacBook-Pro.local`, CPU: `` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 +//! HOSTNAME: `devserver-01`, CPU: `Intel(R) Xeon(R) E-2236 CPU @ 3.40GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("shibuya-dev"), DB CACHE: 1024 // Executed Command: // ./target/release/astar-collator // benchmark // pallet -// --chain=dev +// --chain=shibuya-dev // --steps=50 // --repeat=20 // --pallet=pallet_dapp_staking_migration @@ -37,7 +37,7 @@ // --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 -// --output=weights.rs +// --output=./benchmark-results/shibuya-dev/dapp_staking_migration_weights.rs // --template=./scripts/templates/weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -72,10 +72,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: DappStaking NextDAppId (max_values: Some(1), max_size: Some(2), added: 497, mode: MaxEncodedLen) fn migrate_dapps_success() -> Weight { // Proof Size summary in bytes: - // Measured: `147` + // Measured: `558` // Estimated: `6112` - // Minimum execution time: 43_000_000 picoseconds. - Weight::from_parts(45_000_000, 6112) + // Minimum execution time: 46_218_000 picoseconds. + Weight::from_parts(47_610_000, 6112) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -85,8 +85,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3551` - // Minimum execution time: 3_000_000 picoseconds. - Weight::from_parts(4_000_000, 3551) + // Minimum execution time: 3_385_000 picoseconds. + Weight::from_parts(3_552_000, 3551) .saturating_add(T::DbWeight::get().reads(1_u64)) } /// Storage: DappsStaking Ledger (r:2 w:1) @@ -95,18 +95,20 @@ impl WeightInfo for SubstrateWeight { /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) /// Storage: Balances Freezes (r:1 w:1) /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) fn migrate_ledger_success() -> Weight { // Proof Size summary in bytes: - // Measured: `136` + // Measured: `1748` // Estimated: `6472` - // Minimum execution time: 67_000_000 picoseconds. - Weight::from_parts(68_000_000, 6472) - .saturating_add(T::DbWeight::get().reads(6_u64)) - .saturating_add(T::DbWeight::get().writes(5_u64)) + // Minimum execution time: 69_553_000 picoseconds. + Weight::from_parts(70_319_000, 6472) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) } /// Storage: DappsStaking Ledger (r:1 w:0) /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) @@ -114,27 +116,27 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3731` - // Minimum execution time: 3_000_000 picoseconds. - Weight::from_parts(3_000_000, 3731) + // Minimum execution time: 2_918_000 picoseconds. + Weight::from_parts(3_022_000, 3731) .saturating_add(T::DbWeight::get().reads(1_u64)) } - /// Storage: unknown `0xc0d3d54ea9961b06a7139c5a75c15c4f` (r:1 w:1) - /// Proof Skipped: unknown `0xc0d3d54ea9961b06a7139c5a75c15c4f` (r:1 w:1) + /// Storage: DappsStaking Ledger (r:2 w:1) + /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) fn cleanup_old_storage_success() -> Weight { // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `3465` - // Minimum execution time: 3_000_000 picoseconds. - Weight::from_parts(3_000_000, 3465) - .saturating_add(T::DbWeight::get().reads(1_u64)) + // Measured: `739` + // Estimated: `6472` + // Minimum execution time: 7_109_000 picoseconds. + Weight::from_parts(7_383_000, 6472) + .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } fn cleanup_old_storage_noop() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 2_000_000 picoseconds. - Weight::from_parts(2_000_000, 0) + // Minimum execution time: 2_095_000 picoseconds. + Weight::from_parts(2_213_000, 0) } } @@ -152,10 +154,10 @@ impl WeightInfo for () { /// Proof: DappStaking NextDAppId (max_values: Some(1), max_size: Some(2), added: 497, mode: MaxEncodedLen) fn migrate_dapps_success() -> Weight { // Proof Size summary in bytes: - // Measured: `147` + // Measured: `558` // Estimated: `6112` - // Minimum execution time: 43_000_000 picoseconds. - Weight::from_parts(45_000_000, 6112) + // Minimum execution time: 46_218_000 picoseconds. + Weight::from_parts(47_610_000, 6112) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } @@ -165,8 +167,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3551` - // Minimum execution time: 3_000_000 picoseconds. - Weight::from_parts(4_000_000, 3551) + // Minimum execution time: 3_385_000 picoseconds. + Weight::from_parts(3_552_000, 3551) .saturating_add(RocksDbWeight::get().reads(1_u64)) } /// Storage: DappsStaking Ledger (r:2 w:1) @@ -175,18 +177,20 @@ impl WeightInfo for () { /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) /// Storage: Balances Freezes (r:1 w:1) /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) fn migrate_ledger_success() -> Weight { // Proof Size summary in bytes: - // Measured: `136` + // Measured: `1748` // Estimated: `6472` - // Minimum execution time: 67_000_000 picoseconds. - Weight::from_parts(68_000_000, 6472) - .saturating_add(RocksDbWeight::get().reads(6_u64)) - .saturating_add(RocksDbWeight::get().writes(5_u64)) + // Minimum execution time: 69_553_000 picoseconds. + Weight::from_parts(70_319_000, 6472) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) } /// Storage: DappsStaking Ledger (r:1 w:0) /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) @@ -194,26 +198,26 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3731` - // Minimum execution time: 3_000_000 picoseconds. - Weight::from_parts(3_000_000, 3731) + // Minimum execution time: 2_918_000 picoseconds. + Weight::from_parts(3_022_000, 3731) .saturating_add(RocksDbWeight::get().reads(1_u64)) } - /// Storage: unknown `0xc0d3d54ea9961b06a7139c5a75c15c4f` (r:1 w:1) - /// Proof Skipped: unknown `0xc0d3d54ea9961b06a7139c5a75c15c4f` (r:1 w:1) + /// Storage: DappsStaking Ledger (r:2 w:1) + /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) fn cleanup_old_storage_success() -> Weight { // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `3465` - // Minimum execution time: 3_000_000 picoseconds. - Weight::from_parts(3_000_000, 3465) - .saturating_add(RocksDbWeight::get().reads(1_u64)) + // Measured: `739` + // Estimated: `6472` + // Minimum execution time: 7_109_000 picoseconds. + Weight::from_parts(7_383_000, 6472) + .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } fn cleanup_old_storage_noop() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 2_000_000 picoseconds. - Weight::from_parts(2_000_000, 0) + // Minimum execution time: 2_095_000 picoseconds. + Weight::from_parts(2_213_000, 0) } } diff --git a/pallets/dapp-staking-v3/src/benchmarking/mod.rs b/pallets/dapp-staking-v3/src/benchmarking/mod.rs index 3eab3c6f85..11663401d9 100644 --- a/pallets/dapp-staking-v3/src/benchmarking/mod.rs +++ b/pallets/dapp-staking-v3/src/benchmarking/mod.rs @@ -29,6 +29,11 @@ use ::assert_matches::assert_matches; mod utils; use utils::*; +// A lot of benchmarks which require many blocks, eras or periods to pass have been optimized to utilize +// `force` approach, which skips the required amount of blocks that need to be produced in order to advance. +// +// Without this optimization, benchmarks can take hours to execute for production runtimes. + #[benchmarks] mod benchmarks { use super::*; @@ -220,7 +225,7 @@ mod benchmarks { // Move over to the build&earn subperiod to ensure 'non-loyal' staking. // This is needed so we can achieve staker entry cleanup after claiming unlocked tokens. - advance_to_next_subperiod::(); + force_advance_to_next_subperiod::(); assert_eq!( ActiveProtocolState::::get().subperiod(), Subperiod::BuildAndEarn, @@ -261,8 +266,18 @@ mod benchmarks { ); let unlock_amount = unlock_amount * Into::::into(T::MaxUnlockingChunks::get()); + // Hack + // In order to speed up the benchmark, we reduce how long it takes to unlock the chunks + let mut counter = 1; + Ledger::::mutate(&staker, |ledger| { + ledger.unlocking.iter_mut().for_each(|unlocking| { + unlocking.unlock_block = System::::block_number() + counter; + }); + counter += 1; + }); + // Advance to next period to ensure the old stake entries are cleaned up. - advance_to_next_period::(); + force_advance_to_next_period::(); // Additionally, ensure enough blocks have passed so that the unlocking chunk can be claimed. let unlock_block = Ledger::::get(&staker) @@ -424,32 +439,50 @@ mod benchmarks { smart_contract.clone(), )); - // Lock some amount by the staker + // Lock & stake some amount by the staker let amount = T::MinimumLockedAmount::get(); T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, )); - - // Advance to the era just before a new span entry is created. - // This ensures that when rewards are claimed, we'll be claiming from the new span. - // - // This is convenient because it allows us to control how many rewards are claimed. - advance_to_era::(T::EraRewardSpanLength::get() - 1); - - // Now ensure the expected amount of rewards are claimable. - advance_to_era::( - ActiveProtocolState::::get().era + T::EraRewardSpanLength::get() - x, - ); assert_ok!(DappStaking::::stake( RawOrigin::Signed(staker.clone()).into(), smart_contract.clone(), amount )); + // Advance to era just after the last era covered by the first span + force_advance_to_era::(T::EraRewardSpanLength::get()); + + // Hack - modify staker's stake so it seems as if stake was valid from the 'first stake era'/ + // Also fill up the reward span. + // + // This allows us to easily control how many rewards are claimed, without having to advance large amount of blocks/eras/periods + // to find an appropriate scenario. + let first_stake_era = T::EraRewardSpanLength::get() - x; + Ledger::::mutate(&staker, |ledger| { + ledger.staked = ledger.staked_future.unwrap(); + ledger.staked_future = None; + ledger.staked.era = first_stake_era; + }); + + // Just fill them up, the ledger entry will control how much claims we can make + let mut reward_span = EraRewardSpan::<_>::new(); + for era in 0..(T::EraRewardSpanLength::get()) { + assert_ok!(reward_span.push( + era as EraNumber, + EraReward { + staker_reward_pool: 1_000_000_000_000, + staked: amount, + dapp_reward_pool: 1_000_000_000_000, + }, + )); + } + EraRewards::::insert(&0, reward_span); + // This ensures we claim from the past period. - advance_to_next_period::(); + force_advance_to_next_period::(); // For testing purposes System::::reset_events(); @@ -486,25 +519,41 @@ mod benchmarks { RawOrigin::Signed(staker.clone()).into(), amount, )); - - // Advance to the era just before a new span entry is created. - // This ensures that when rewards are claimed, we'll be claiming from the new span. - // - // This is convenient because it allows us to control how many rewards are claimed. - advance_to_era::(T::EraRewardSpanLength::get() - 1); - - // Now ensure the expected amount of rewards are claimable. - advance_to_era::( - ActiveProtocolState::::get().era + T::EraRewardSpanLength::get() - x, - ); assert_ok!(DappStaking::::stake( RawOrigin::Signed(staker.clone()).into(), smart_contract.clone(), amount )); - // This ensures we move over the entire span. - advance_to_era::(T::EraRewardSpanLength::get() * 2); + // Advance to era just after the last era covered by the first span + // This means we'll be able to claim all of the rewards from the previous span. + force_advance_to_era::(T::EraRewardSpanLength::get()); + + // Hack - modify staker's stake so it seems as if stake was valid from the 'first stake era'/ + // Also fill up the reward span. + // + // This allows us to easily control how many rewards are claimed, without having to advance large amount of blocks/eras/periods + // to find an appropriate scenario. + let first_stake_era = T::EraRewardSpanLength::get() - x; + Ledger::::mutate(&staker, |ledger| { + ledger.staked = ledger.staked_future.unwrap(); + ledger.staked_future = None; + ledger.staked.era = first_stake_era; + }); + + // Just fill them up, the ledger entry will control how much claims we can make + let mut reward_span = EraRewardSpan::<_>::new(); + for era in 0..(T::EraRewardSpanLength::get()) { + assert_ok!(reward_span.push( + era as EraNumber, + EraReward { + staker_reward_pool: 1_000_000_000_000, + staked: amount, + dapp_reward_pool: 1_000_000_000_000, + }, + )); + } + EraRewards::::insert(&0, reward_span); // For testing purposes System::::reset_events(); @@ -548,7 +597,7 @@ mod benchmarks { )); // Advance to the next period so we can claim the bonus reward. - advance_to_next_period::(); + force_advance_to_next_period::(); #[extrinsic_call] _(RawOrigin::Signed(staker.clone()), smart_contract.clone()); @@ -574,7 +623,7 @@ mod benchmarks { smart_contract.clone(), )); - let amount = T::MinimumLockedAmount::get() * 1000 * UNIT; + let amount = MIN_TIER_THRESHOLD * 1000; T::BenchmarkHelper::set_balance(&owner, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(owner.clone()).into(), @@ -610,17 +659,18 @@ mod benchmarks { )); } + // Advance enough eras so dApp reward can be claimed. + force_advance_to_next_subperiod::(); + // This is a hacky part to ensure we accomodate max number of contracts. TierConfig::::mutate(|config| { let max_number_of_contracts: u16 = T::MaxNumberOfContracts::get().try_into().unwrap(); config.number_of_slots = max_number_of_contracts; config.slots_per_tier[0] = max_number_of_contracts; config.slots_per_tier[1..].iter_mut().for_each(|x| *x = 0); + config.tier_thresholds[0] = TierThreshold::FixedTvlAmount { amount: 1 }; }); - - // Advance enough eras so dApp reward can be claimed. - advance_to_next_subperiod::(); - advance_to_next_era::(); + force_advance_to_next_era::(); let claim_era = ActiveProtocolState::::get().era - 1; assert_eq!( @@ -695,7 +745,7 @@ mod benchmarks { initial_config::(); // Move over to the build&earn subperiod to ensure 'non-loyal' staking. - advance_to_next_subperiod::(); + force_advance_to_next_subperiod::(); // Prepare staker & lock some amount let staker: T::AccountId = whitelisted_caller(); @@ -725,7 +775,7 @@ mod benchmarks { } // Move over to the next period, marking the entries as expired since they don't have the loyalty flag. - advance_to_next_period::(); + force_advance_to_next_period::(); #[extrinsic_call] _(RawOrigin::Signed(staker.clone())); @@ -791,7 +841,7 @@ mod benchmarks { prepare_contracts_for_tier_assignment::(max_number_of_contracts::()); // Advance to build&earn subperiod - advance_to_next_subperiod::(); + force_advance_to_next_subperiod::(); let snapshot_state = ActiveProtocolState::::get(); // Advance over to the last era of the subperiod, and then again to the last block of that era. @@ -840,11 +890,11 @@ mod benchmarks { prepare_contracts_for_tier_assignment::(max_number_of_contracts::()); // Advance to build&earn subperiod - advance_to_next_subperiod::(); + force_advance_to_next_subperiod::(); let snapshot_state = ActiveProtocolState::::get(); // Advance over to the next era, and then again to the last block of that era. - advance_to_next_era::(); + force_advance_to_next_era::(); run_to_block::(ActiveProtocolState::::get().next_era_start - 1); // Some sanity checks, we should still be in the build&earn subperiod, and in the first period. @@ -888,7 +938,7 @@ mod benchmarks { // Register & stake contracts, to prepare for tier assignment. prepare_contracts_for_tier_assignment::(x); - advance_to_next_era::(); + force_advance_to_next_era::(); let reward_era = ActiveProtocolState::::get().era; let reward_period = ActiveProtocolState::::get().period_number(); @@ -907,9 +957,13 @@ mod benchmarks { // Prepare init config (protocol state, tier params & config, etc.) initial_config::(); - // Advance enough periods to trigger the cleanup + // Advance to era just after the last era covered by the first span. + // This is sufficient to completely fill up the first span with entries for the ongoing era. + force_advance_to_era::(T::EraRewardSpanLength::get()); + + // Advance enough periods to make cleanup feasible. let retention_period = T::RewardRetentionInPeriods::get(); - advance_to_period::( + force_advance_to_period::( ActiveProtocolState::::get().period_number() + retention_period + 2, ); diff --git a/pallets/dapp-staking-v3/src/benchmarking/utils.rs b/pallets/dapp-staking-v3/src/benchmarking/utils.rs index 688f964a76..28dde52267 100644 --- a/pallets/dapp-staking-v3/src/benchmarking/utils.rs +++ b/pallets/dapp-staking-v3/src/benchmarking/utils.rs @@ -49,34 +49,88 @@ pub(super) fn advance_to_era(era: EraNumber) { } } +/// Advance blocks until the specified era has been reached. +/// +/// Relies on the `force` approach to advance one era per block. +pub(super) fn force_advance_to_era(era: EraNumber) { + assert!(era >= ActiveProtocolState::::get().era); + while ActiveProtocolState::::get().era < era { + assert_ok!(DappStaking::::force( + RawOrigin::Root.into(), + ForcingType::Era + )); + run_for_blocks::(One::one()); + } +} + /// Advance blocks until next era has been reached. -pub(super) fn advance_to_next_era() { +pub(super) fn _advance_to_next_era() { advance_to_era::(ActiveProtocolState::::get().era + 1); } +/// Advance to next era, in the next block using the `force` approach. +pub(crate) fn force_advance_to_next_era() { + assert_ok!(DappStaking::::force( + RawOrigin::Root.into(), + ForcingType::Era + )); + run_for_blocks::(One::one()); +} + /// Advance blocks until the specified period has been reached. /// /// Function has no effect if period is already passed. -pub(super) fn advance_to_period(period: PeriodNumber) { +pub(super) fn _advance_to_period(period: PeriodNumber) { assert!(period >= ActiveProtocolState::::get().period_number()); while ActiveProtocolState::::get().period_number() < period { run_for_blocks::(One::one()); } } +/// Advance to the specified period, using the `force` approach. +pub(super) fn force_advance_to_period(period: PeriodNumber) { + assert!(period >= ActiveProtocolState::::get().period_number()); + while ActiveProtocolState::::get().period_number() < period { + force_advance_to_next_subperiod::(); + } +} + /// Advance blocks until next period has been reached. -pub(super) fn advance_to_next_period() { - advance_to_period::(ActiveProtocolState::::get().period_number() + 1); +pub(super) fn _advance_to_next_period() { + _advance_to_period::(ActiveProtocolState::::get().period_number() + 1); +} + +/// Advance blocks until next period has been reached. +/// +/// Relies on the `force` approach to advance one subperiod per block. +pub(super) fn force_advance_to_next_period() { + let init_period_number = ActiveProtocolState::::get().period_number(); + while ActiveProtocolState::::get().period_number() == init_period_number { + assert_ok!(DappStaking::::force( + RawOrigin::Root.into(), + ForcingType::Subperiod + )); + run_for_blocks::(One::one()); + } } /// Advance blocks until next period type has been reached. -pub(super) fn advance_to_next_subperiod() { +pub(super) fn _advance_to_next_subperiod() { let subperiod = ActiveProtocolState::::get().subperiod(); while ActiveProtocolState::::get().subperiod() == subperiod { run_for_blocks::(One::one()); } } +/// Use the `force` approach to advance to the next subperiod immediately in the next block. +pub(super) fn force_advance_to_next_subperiod() { + assert_ok!(DappStaking::::force( + RawOrigin::Root.into(), + ForcingType::Subperiod + )); + run_for_blocks::(One::one()); +} + /// All our networks use 18 decimals for native currency so this should be fine. pub(super) const UNIT: Balance = 1_000_000_000_000_000_000; @@ -172,7 +226,6 @@ pub(super) fn initial_config() { StaticTierParams::::put(tier_params); TierConfig::::put(init_tier_config.clone()); - NextTierConfig::::put(init_tier_config); } /// Maximum number of contracts that 'makes sense' - considers both contract number limit & number of slots. @@ -195,9 +248,14 @@ pub(super) fn prepare_contracts_for_tier_assignment(x: u32) { )); } - // TODO: try to make this more "shuffled" so the generated vector ends up being more random - let mut amount = 1000 * MIN_TIER_THRESHOLD; + let anchor_amount = 1000 * MIN_TIER_THRESHOLD; + let mut amounts: Vec<_> = (0..x) + .map(|i| anchor_amount - UNIT * i as Balance) + .collect(); + trivial_fisher_yates_shuffle(&mut amounts, SEED.into()); + for id in 0..x { + let amount = amounts[id as usize]; let staker = account("staker", id.into(), 1337); T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( @@ -211,8 +269,17 @@ pub(super) fn prepare_contracts_for_tier_assignment(x: u32) { smart_contract, amount, )); + } +} - // Slowly decrease the stake amount - amount.saturating_reduce(UNIT); +/// Reuse from `sassafras` pallet tests. +/// +/// Just a trivial, insecure shuffle for the benchmarks. +fn trivial_fisher_yates_shuffle(vector: &mut Vec, random_seed: u64) { + let mut rng = random_seed as usize; + for i in (1..vector.len()).rev() { + let j = rng % (i + 1); + vector.swap(i, j); + rng = (rng.wrapping_mul(8427637) + 1) as usize; // Some random number generation } } diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index a20c921dfc..f9a10da274 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -39,7 +39,7 @@ use frame_support::{ pallet_prelude::*, traits::{ fungible::{Inspect as FunInspect, MutateFreeze as FunMutateFreeze}, - StorageVersion, + OnRuntimeUpgrade, StorageVersion, }, weights::Weight, }; @@ -431,11 +431,6 @@ pub mod pallet { pub type StaticTierParams = StorageValue<_, TierParameters, ValueQuery>; - /// Tier configuration to be used during the newly started period - #[pallet::storage] - pub type NextTierConfig = - StorageValue<_, TiersConfiguration, ValueQuery>; - /// Tier configuration user for current & preceding eras. #[pallet::storage] pub type TierConfig = @@ -518,7 +513,6 @@ pub mod pallet { ActiveProtocolState::::put(protocol_state); StaticTierParams::::put(tier_params); TierConfig::::put(tier_config.clone()); - NextTierConfig::::put(tier_config); } } @@ -531,6 +525,27 @@ pub mod pallet { fn on_idle(_block: BlockNumberFor, remaining_weight: Weight) -> Weight { Self::expired_entry_cleanup(&remaining_weight) } + + fn integrity_test() { + // dApp staking params + // Sanity checks + assert!(T::EraRewardSpanLength::get() > 0); + assert!(T::RewardRetentionInPeriods::get() > 0); + assert!(T::MaxNumberOfContracts::get() > 0); + assert!(T::MaxUnlockingChunks::get() > 0); + assert!(T::UnlockingPeriod::get() > 0); + assert!(T::MaxNumberOfStakedContracts::get() > 0); + + assert!(T::MinimumLockedAmount::get() > 0); + assert!(T::MinimumStakeAmount::get() > 0); + assert!(T::MinimumLockedAmount::get() >= T::MinimumStakeAmount::get()); + + // Cycle config + assert!(T::CycleConfiguration::periods_per_cycle() > 0); + assert!(T::CycleConfiguration::eras_per_voting_subperiod() > 0); + assert!(T::CycleConfiguration::eras_per_build_and_earn_subperiod() > 0); + assert!(T::CycleConfiguration::blocks_per_era() > 0); + } } /// A reason for freezing funds. @@ -1739,10 +1754,6 @@ pub mod pallet { era_info.migrate_to_next_era(Some(protocol_state.subperiod())); - // Update tier configuration to be used when calculating rewards for the upcoming eras - let next_tier_config = NextTierConfig::::take(); - TierConfig::::put(next_tier_config); - consumed_weight .saturating_accrue(T::WeightInfo::on_initialize_voting_to_build_and_earn()); @@ -1813,7 +1824,7 @@ pub mod pallet { let average_price = T::NativePriceProvider::average_price(); let new_tier_config = TierConfig::::get().calculate_new(average_price, &tier_params); - NextTierConfig::::put(new_tier_config); + TierConfig::::put(new_tier_config); consumed_weight.saturating_accrue( T::WeightInfo::on_initialize_build_and_earn_to_voting(), @@ -1943,3 +1954,75 @@ pub mod pallet { } } } + +/// `OnRuntimeUpgrade` logic used to set & configure init dApp staking v3 storage items. +pub struct DAppStakingV3InitConfig(PhantomData<(T, G)>); +impl< + T: Config, + G: Get<( + EraNumber, + TierParameters, + TiersConfiguration, + )>, + > OnRuntimeUpgrade for DAppStakingV3InitConfig +{ + fn on_runtime_upgrade() -> Weight { + if Pallet::::on_chain_storage_version() >= STORAGE_VERSION { + return T::DbWeight::get().reads(1); + } + + // 0. Unwrap arguments + let (init_era, tier_params, init_tier_config) = G::get(); + + // 1. Prepare active protocol state + let now = frame_system::Pallet::::block_number(); + let voting_period_length = Pallet::::blocks_per_voting_period(); + + let protocol_state = ProtocolState { + era: init_era, + next_era_start: now.saturating_add(voting_period_length), + period_info: PeriodInfo { + number: 1, + subperiod: Subperiod::Voting, + next_subperiod_start_era: init_era.saturating_add(1), + }, + maintenance: true, + }; + + // 2. Write necessary items into storage + ActiveProtocolState::::put(protocol_state); + StaticTierParams::::put(tier_params); + TierConfig::::put(init_tier_config); + STORAGE_VERSION.put::>(); + + // 3. Emit events to make indexers happy + Pallet::::deposit_event(Event::::NewEra { era: init_era }); + Pallet::::deposit_event(Event::::NewSubperiod { + subperiod: Subperiod::Voting, + number: 1, + }); + + log::info!("dApp Staking v3 storage initialized."); + + T::DbWeight::get().reads_writes(1, 4) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { + assert_eq!(Pallet::::on_chain_storage_version(), STORAGE_VERSION); + assert!(ActiveProtocolState::::get().maintenance); + + let number_of_tiers = T::NumberOfTiers::get(); + + let tier_params = StaticTierParams::::get(); + assert_eq!(tier_params.reward_portion.len(), number_of_tiers as usize); + assert!(tier_params.is_valid()); + + let tier_config = TierConfig::::get(); + assert_eq!(tier_config.reward_portion.len(), number_of_tiers as usize); + assert_eq!(tier_config.slots_per_tier.len(), number_of_tiers as usize); + assert_eq!(tier_config.tier_thresholds.len(), number_of_tiers as usize); + + Ok(()) + } +} diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 3f116c6bbc..247605e456 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -24,10 +24,7 @@ use crate::{ use frame_support::{ construct_runtime, parameter_types, - traits::{ - fungible::{Mutate as FunMutate, Unbalanced as FunUnbalanced}, - ConstU128, ConstU32, - }, + traits::{fungible::Mutate as FunMutate, ConstU128, ConstU32}, weights::Weight, }; use sp_arithmetic::fixed_point::FixedU64; @@ -158,6 +155,7 @@ impl crate::BenchmarkHelper } fn set_balance(account: &AccountId, amount: Balance) { + use frame_support::traits::fungible::Unbalanced as FunUnbalanced; Balances::write_balance(account, amount) .expect("Must succeed in test/benchmark environment."); } @@ -305,7 +303,6 @@ impl ExtBuilder { pallet_dapp_staking::StaticTierParams::::put(tier_params); pallet_dapp_staking::TierConfig::::put(init_tier_config.clone()); - pallet_dapp_staking::NextTierConfig::::put(init_tier_config); DappStaking::on_initialize(System::block_number()); }); diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 8f372a53ab..5e2e0af0d5 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -21,7 +21,7 @@ use crate::types::*; use crate::{ pallet::Config, ActiveProtocolState, ContractStake, CurrentEraInfo, DAppId, DAppTiers, EraRewards, Event, FreezeReason, HistoryCleanupMarker, IntegratedDApps, Ledger, NextDAppId, - NextTierConfig, PeriodEnd, PeriodEndInfo, StakerInfo, TierConfig, + PeriodEnd, PeriodEndInfo, StakerInfo, }; use frame_support::{ @@ -57,8 +57,6 @@ pub(crate) struct MemorySnapshot { era_rewards: HashMap::EraRewardSpanLength>>, period_end: HashMap, dapp_tiers: HashMap>, - tier_config: TiersConfiguration<::NumberOfTiers>, - next_tier_config: TiersConfiguration<::NumberOfTiers>, } impl MemorySnapshot { @@ -77,8 +75,6 @@ impl MemorySnapshot { era_rewards: EraRewards::::iter().collect(), period_end: PeriodEnd::::iter().collect(), dapp_tiers: DAppTiers::::iter().collect(), - tier_config: TierConfig::::get(), - next_tier_config: NextTierConfig::::get(), } } @@ -1273,22 +1269,7 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { ); } - // 3. Verify tier config - match pre_protoc_state.subperiod() { - Subperiod::Voting => { - assert!(!NextTierConfig::::exists()); - assert_eq!(post_snapshot.tier_config, pre_snapshot.next_tier_config); - } - Subperiod::BuildAndEarn if is_new_subperiod => { - assert!(NextTierConfig::::exists()); - assert_eq!(post_snapshot.tier_config, pre_snapshot.tier_config); - } - _ => { - assert_eq!(post_snapshot.tier_config, pre_snapshot.tier_config); - } - } - - // 4. Verify era reward + // 3. Verify era reward let era_span_index = DappStaking::era_reward_span_index(pre_protoc_state.era); let maybe_pre_era_reward_span = pre_snapshot.era_rewards.get(&era_span_index); let post_era_reward_span = post_snapshot @@ -1319,7 +1300,7 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { "Total staked amount must be equal to total amount staked at the end of the era." ); - // 5. Verify period end + // 4. Verify period end if is_new_subperiod && pre_protoc_state.subperiod() == Subperiod::BuildAndEarn { let period_end_info = post_snapshot.period_end[&pre_protoc_state.period_number()]; assert_eq!( @@ -1330,7 +1311,7 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { ); } - // 6. Verify event(s) + // 5. Verify event(s) if is_new_subperiod { let events = dapp_staking_events(); assert!( diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 73c26974ee..cf70f0a6aa 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -396,7 +396,6 @@ pub struct AccountLedger> { /// Number of contract stake entries in storage. #[codec(compact)] pub contract_stake_count: u32, - // TODO: rename to staker_info_count? } impl Default for AccountLedger @@ -1525,6 +1524,11 @@ impl> Default for TiersConfiguration { } } +// Some TODOs: +// Some parts regarding tiers should be refactored. +// * There's no need to keep thresholds in two separate storage items since the calculation can always be done compared to the +// anchor value of 5 cents. This still needs to be checked & investigated, but it's worth a try. + impl> TiersConfiguration { /// Check if parameters are valid. pub fn is_valid(&self) -> bool { diff --git a/pallets/dapp-staking-v3/src/weights.rs b/pallets/dapp-staking-v3/src/weights.rs index 3b13aa4622..e98d798707 100644 --- a/pallets/dapp-staking-v3/src/weights.rs +++ b/pallets/dapp-staking-v3/src/weights.rs @@ -20,16 +20,16 @@ //! Autogenerated weights for pallet_dapp_staking_v3 //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-12-01, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-12-20, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Dinos-MacBook-Pro.local`, CPU: `` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 +//! HOSTNAME: `devserver-01`, CPU: `Intel(R) Xeon(R) E-2236 CPU @ 3.40GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("shibuya-dev"), DB CACHE: 1024 // Executed Command: // ./target/release/astar-collator // benchmark // pallet -// --chain=dev +// --chain=shibuya-dev // --steps=50 // --repeat=20 // --pallet=pallet_dapp_staking_v3 @@ -37,7 +37,7 @@ // --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 -// --output=weights.rs +// --output=./benchmark-results/shibuya-dev/dapp_staking_v3_weights.rs // --template=./scripts/templates/weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -81,11 +81,11 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(10_000_000, 0) + // Minimum execution time: 8_474_000 picoseconds. + Weight::from_parts(8_711_000, 0) } /// Storage: DappStaking IntegratedDApps (r:1 w:1) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:1) /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) /// Storage: DappStaking NextDAppId (r:1 w:1) @@ -93,201 +93,203 @@ impl WeightInfo for SubstrateWeight { fn register() -> Weight { // Proof Size summary in bytes: // Measured: `0` - // Estimated: `3093` - // Minimum execution time: 16_000_000 picoseconds. - Weight::from_parts(18_000_000, 3093) + // Estimated: `3091` + // Minimum execution time: 16_360_000 picoseconds. + Weight::from_parts(16_697_000, 3091) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:1) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) fn set_dapp_reward_beneficiary() -> Weight { // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `3093` - // Minimum execution time: 13_000_000 picoseconds. - Weight::from_parts(13_000_000, 3093) + // Measured: `75` + // Estimated: `3091` + // Minimum execution time: 12_927_000 picoseconds. + Weight::from_parts(13_229_000, 3091) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:1) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) fn set_dapp_owner() -> Weight { // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `3093` - // Minimum execution time: 12_000_000 picoseconds. - Weight::from_parts(14_000_000, 3093) + // Measured: `75` + // Estimated: `3091` + // Minimum execution time: 13_610_000 picoseconds. + Weight::from_parts(13_851_000, 3091) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:1) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking ContractStake (r:0 w:1) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) fn unregister() -> Weight { // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `3093` - // Minimum execution time: 17_000_000 picoseconds. - Weight::from_parts(19_000_000, 3093) + // Measured: `75` + // Estimated: `3091` + // Minimum execution time: 16_704_000 picoseconds. + Weight::from_parts(16_952_000, 3091) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) fn lock() -> Weight { // Proof Size summary in bytes: // Measured: `12` // Estimated: `4764` - // Minimum execution time: 40_000_000 picoseconds. - Weight::from_parts(43_000_000, 4764) + // Minimum execution time: 31_680_000 picoseconds. + Weight::from_parts(32_075_000, 4764) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) fn unlock() -> Weight { // Proof Size summary in bytes: - // Measured: `163` + // Measured: `156` // Estimated: `4764` - // Minimum execution time: 37_000_000 picoseconds. - Weight::from_parts(43_000_000, 4764) + // Minimum execution time: 34_576_000 picoseconds. + Weight::from_parts(34_777_000, 4764) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) - /// The range of component `x` is `[0, 3]`. - fn claim_unlocked(_x: u32, ) -> Weight { + /// The range of component `x` is `[0, 8]`. + fn claim_unlocked(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `160 + x * (17 ±0)` + // Measured: `187` // Estimated: `4764` - // Minimum execution time: 34_000_000 picoseconds. - Weight::from_parts(40_955_543, 4764) + // Minimum execution time: 33_562_000 picoseconds. + Weight::from_parts(34_600_552, 4764) + // Standard Error: 5_079 + .saturating_add(Weight::from_parts(193_345, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) fn relock_unlocking() -> Weight { // Proof Size summary in bytes: - // Measured: `174` + // Measured: `182` // Estimated: `4764` - // Minimum execution time: 38_000_000 picoseconds. - Weight::from_parts(40_000_000, 4764) + // Minimum execution time: 36_436_000 picoseconds. + Weight::from_parts(37_262_000, 4764) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking StakerInfo (r:1 w:1) /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) /// Storage: DappStaking ContractStake (r:1 w:1) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) fn stake() -> Weight { // Proof Size summary in bytes: - // Measured: `258` + // Measured: `250` // Estimated: `4764` - // Minimum execution time: 45_000_000 picoseconds. - Weight::from_parts(49_000_000, 4764) + // Minimum execution time: 43_866_000 picoseconds. + Weight::from_parts(44_468_000, 4764) .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking StakerInfo (r:1 w:1) /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) /// Storage: DappStaking ContractStake (r:1 w:1) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) fn unstake() -> Weight { // Proof Size summary in bytes: - // Measured: `437` + // Measured: `427` // Estimated: `4764` - // Minimum execution time: 48_000_000 picoseconds. - Weight::from_parts(54_000_000, 4764) + // Minimum execution time: 47_368_000 picoseconds. + Weight::from_parts(48_049_000, 4764) .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking EraRewards (r:1 w:0) - /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) /// Storage: DappStaking PeriodEnd (r:1 w:0) /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) - /// The range of component `x` is `[1, 8]`. + /// The range of component `x` is `[1, 16]`. fn claim_staker_rewards_past_period(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `485 + x * (8 ±0)` + // Measured: `560` // Estimated: `4764` - // Minimum execution time: 50_000_000 picoseconds. - Weight::from_parts(51_664_058, 4764) - // Standard Error: 15_971 - .saturating_add(Weight::from_parts(4_243_613, 0).saturating_mul(x.into())) + // Minimum execution time: 51_230_000 picoseconds. + Weight::from_parts(48_696_805, 4764) + // Standard Error: 6_139 + .saturating_add(Weight::from_parts(3_374_191, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking EraRewards (r:1 w:0) - /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) - /// The range of component `x` is `[1, 8]`. + /// The range of component `x` is `[1, 16]`. fn claim_staker_rewards_ongoing_period(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `438 + x * (8 ±0)` + // Measured: `501` // Estimated: `4764` - // Minimum execution time: 47_000_000 picoseconds. - Weight::from_parts(48_357_596, 4764) - // Standard Error: 16_408 - .saturating_add(Weight::from_parts(4_302_059, 0).saturating_mul(x.into())) + // Minimum execution time: 45_030_000 picoseconds. + Weight::from_parts(43_179_071, 4764) + // Standard Error: 5_547 + .saturating_add(Weight::from_parts(3_296_864, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -295,66 +297,68 @@ impl WeightInfo for SubstrateWeight { /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) /// Storage: DappStaking PeriodEnd (r:1 w:0) /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) fn claim_bonus_reward() -> Weight { // Proof Size summary in bytes: - // Measured: `158` - // Estimated: `3603` - // Minimum execution time: 34_000_000 picoseconds. - Weight::from_parts(36_000_000, 3603) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) + // Measured: `267` + // Estimated: `3775` + // Minimum execution time: 42_248_000 picoseconds. + Weight::from_parts(42_687_000, 3775) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking DAppTiers (r:1 w:1) - /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) fn claim_dapp_reward() -> Weight { // Proof Size summary in bytes: - // Measured: `1086` - // Estimated: `3948` - // Minimum execution time: 37_000_000 picoseconds. - Weight::from_parts(41_000_000, 3948) + // Measured: `3021` + // Estimated: `5548` + // Minimum execution time: 50_968_000 picoseconds. + Weight::from_parts(51_778_000, 5548) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking StakerInfo (r:1 w:1) /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) fn unstake_from_unregistered() -> Weight { // Proof Size summary in bytes: - // Measured: `397` + // Measured: `389` // Estimated: `4764` - // Minimum execution time: 43_000_000 picoseconds. - Weight::from_parts(47_000_000, 4764) + // Minimum execution time: 42_329_000 picoseconds. + Weight::from_parts(42_737_000, 4764) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } - /// Storage: DappStaking StakerInfo (r:4 w:3) + /// Storage: DappStaking StakerInfo (r:9 w:8) /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) - /// The range of component `x` is `[1, 3]`. + /// The range of component `x` is `[1, 8]`. fn cleanup_expired_entries(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `250 + x * (75 ±0)` + // Measured: `255 + x * (69 ±0)` // Estimated: `4764 + x * (2613 ±0)` - // Minimum execution time: 41_000_000 picoseconds. - Weight::from_parts(38_854_143, 4764) - // Standard Error: 54_134 - .saturating_add(Weight::from_parts(7_359_116, 0).saturating_mul(x.into())) + // Minimum execution time: 42_222_000 picoseconds. + Weight::from_parts(38_945_386, 4764) + // Standard Error: 14_325 + .saturating_add(Weight::from_parts(5_044_310, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(2_u64)) @@ -365,83 +369,91 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(10_000_000, 0) + // Minimum execution time: 9_971_000 picoseconds. + Weight::from_parts(10_190_000, 0) } /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) - /// Storage: DappStaking NextTierConfig (r:1 w:1) - /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// Storage: DappStaking EraRewards (r:1 w:1) - /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) - /// Storage: DappStaking TierConfig (r:0 w:1) - /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) fn on_initialize_voting_to_build_and_earn() -> Weight { // Proof Size summary in bytes: - // Measured: `151` - // Estimated: `3870` - // Minimum execution time: 22_000_000 picoseconds. - Weight::from_parts(24_000_000, 3870) - .saturating_add(T::DbWeight::get().reads(3_u64)) - .saturating_add(T::DbWeight::get().writes(4_u64)) + // Measured: `16` + // Estimated: `4254` + // Minimum execution time: 17_308_000 picoseconds. + Weight::from_parts(17_774_000, 4254) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) /// Storage: DappStaking StaticTierParams (r:1 w:0) /// Proof: DappStaking StaticTierParams (max_values: Some(1), max_size: Some(167), added: 662, mode: MaxEncodedLen) - /// Storage: DappStaking TierConfig (r:1 w:0) + /// Storage: DappStaking TierConfig (r:1 w:1) /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// Storage: DappStaking EraRewards (r:1 w:1) - /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) /// Storage: DappStaking PeriodEnd (r:0 w:1) /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) /// Storage: DappStaking DAppTiers (r:0 w:1) - /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) - /// Storage: DappStaking NextTierConfig (r:0 w:1) - /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) fn on_initialize_build_and_earn_to_voting() -> Weight { // Proof Size summary in bytes: - // Measured: `685` - // Estimated: `3870` - // Minimum execution time: 34_000_000 picoseconds. - Weight::from_parts(37_000_000, 3870) + // Measured: `550` + // Estimated: `4254` + // Minimum execution time: 39_768_000 picoseconds. + Weight::from_parts(40_422_000, 4254) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) /// Storage: DappStaking EraRewards (r:1 w:1) - /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) /// Storage: DappStaking DAppTiers (r:0 w:1) - /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) fn on_initialize_build_and_earn_to_build_and_earn() -> Weight { // Proof Size summary in bytes: - // Measured: `73` - // Estimated: `3870` - // Minimum execution time: 17_000_000 picoseconds. - Weight::from_parts(18_000_000, 3870) + // Measured: `68` + // Estimated: `4254` + // Minimum execution time: 20_976_000 picoseconds. + Weight::from_parts(21_507_000, 4254) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: DappStaking ContractStake (r:101 w:0) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) /// Storage: DappStaking TierConfig (r:1 w:0) /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// The range of component `x` is `[0, 100]`. fn dapp_tier_assignment(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `158 + x * (33 ±0)` - // Estimated: `3063 + x * (2073 ±0)` - // Minimum execution time: 6_000_000 picoseconds. - Weight::from_parts(12_709_998, 3063) - // Standard Error: 8_047 - .saturating_add(Weight::from_parts(2_731_946, 0).saturating_mul(x.into())) + // Measured: `152 + x * (32 ±0)` + // Estimated: `3061 + x * (2071 ±0)` + // Minimum execution time: 7_374_000 picoseconds. + Weight::from_parts(10_826_637, 3061) + // Standard Error: 3_374 + .saturating_add(Weight::from_parts(2_291_643, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 2071).saturating_mul(x.into())) } + /// Storage: DappStaking HistoryCleanupMarker (r:1 w:1) + /// Proof: DappStaking HistoryCleanupMarker (max_values: Some(1), max_size: Some(8), added: 503, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:1) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) fn on_idle_cleanup() -> Weight { - T::DbWeight::get().reads_writes(3, 2) + // Proof Size summary in bytes: + // Measured: `473` + // Estimated: `4254` + // Minimum execution time: 14_500_000 picoseconds. + Weight::from_parts(14_969_000, 4254) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) } } @@ -451,11 +463,11 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(10_000_000, 0) + // Minimum execution time: 8_474_000 picoseconds. + Weight::from_parts(8_711_000, 0) } /// Storage: DappStaking IntegratedDApps (r:1 w:1) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:1) /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) /// Storage: DappStaking NextDAppId (r:1 w:1) @@ -463,201 +475,203 @@ impl WeightInfo for () { fn register() -> Weight { // Proof Size summary in bytes: // Measured: `0` - // Estimated: `3093` - // Minimum execution time: 16_000_000 picoseconds. - Weight::from_parts(18_000_000, 3093) + // Estimated: `3091` + // Minimum execution time: 16_360_000 picoseconds. + Weight::from_parts(16_697_000, 3091) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:1) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) fn set_dapp_reward_beneficiary() -> Weight { // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `3093` - // Minimum execution time: 13_000_000 picoseconds. - Weight::from_parts(13_000_000, 3093) + // Measured: `75` + // Estimated: `3091` + // Minimum execution time: 12_927_000 picoseconds. + Weight::from_parts(13_229_000, 3091) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:1) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) fn set_dapp_owner() -> Weight { // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `3093` - // Minimum execution time: 12_000_000 picoseconds. - Weight::from_parts(14_000_000, 3093) + // Measured: `75` + // Estimated: `3091` + // Minimum execution time: 13_610_000 picoseconds. + Weight::from_parts(13_851_000, 3091) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:1) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking ContractStake (r:0 w:1) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) fn unregister() -> Weight { // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `3093` - // Minimum execution time: 17_000_000 picoseconds. - Weight::from_parts(19_000_000, 3093) + // Measured: `75` + // Estimated: `3091` + // Minimum execution time: 16_704_000 picoseconds. + Weight::from_parts(16_952_000, 3091) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) fn lock() -> Weight { // Proof Size summary in bytes: // Measured: `12` // Estimated: `4764` - // Minimum execution time: 40_000_000 picoseconds. - Weight::from_parts(43_000_000, 4764) + // Minimum execution time: 31_680_000 picoseconds. + Weight::from_parts(32_075_000, 4764) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) fn unlock() -> Weight { // Proof Size summary in bytes: - // Measured: `163` + // Measured: `156` // Estimated: `4764` - // Minimum execution time: 37_000_000 picoseconds. - Weight::from_parts(43_000_000, 4764) + // Minimum execution time: 34_576_000 picoseconds. + Weight::from_parts(34_777_000, 4764) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) - /// The range of component `x` is `[0, 3]`. - fn claim_unlocked(_x: u32, ) -> Weight { + /// The range of component `x` is `[0, 8]`. + fn claim_unlocked(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `160 + x * (17 ±0)` + // Measured: `187` // Estimated: `4764` - // Minimum execution time: 34_000_000 picoseconds. - Weight::from_parts(40_955_543, 4764) + // Minimum execution time: 33_562_000 picoseconds. + Weight::from_parts(34_600_552, 4764) + // Standard Error: 5_079 + .saturating_add(Weight::from_parts(193_345, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) fn relock_unlocking() -> Weight { // Proof Size summary in bytes: - // Measured: `174` + // Measured: `182` // Estimated: `4764` - // Minimum execution time: 38_000_000 picoseconds. - Weight::from_parts(40_000_000, 4764) + // Minimum execution time: 36_436_000 picoseconds. + Weight::from_parts(37_262_000, 4764) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking StakerInfo (r:1 w:1) /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) /// Storage: DappStaking ContractStake (r:1 w:1) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) fn stake() -> Weight { // Proof Size summary in bytes: - // Measured: `258` + // Measured: `250` // Estimated: `4764` - // Minimum execution time: 45_000_000 picoseconds. - Weight::from_parts(49_000_000, 4764) + // Minimum execution time: 43_866_000 picoseconds. + Weight::from_parts(44_468_000, 4764) .saturating_add(RocksDbWeight::get().reads(7_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking StakerInfo (r:1 w:1) /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) /// Storage: DappStaking ContractStake (r:1 w:1) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) fn unstake() -> Weight { // Proof Size summary in bytes: - // Measured: `437` + // Measured: `427` // Estimated: `4764` - // Minimum execution time: 48_000_000 picoseconds. - Weight::from_parts(54_000_000, 4764) + // Minimum execution time: 47_368_000 picoseconds. + Weight::from_parts(48_049_000, 4764) .saturating_add(RocksDbWeight::get().reads(7_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking EraRewards (r:1 w:0) - /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) /// Storage: DappStaking PeriodEnd (r:1 w:0) /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) - /// The range of component `x` is `[1, 8]`. + /// The range of component `x` is `[1, 16]`. fn claim_staker_rewards_past_period(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `485 + x * (8 ±0)` + // Measured: `560` // Estimated: `4764` - // Minimum execution time: 50_000_000 picoseconds. - Weight::from_parts(51_664_058, 4764) - // Standard Error: 15_971 - .saturating_add(Weight::from_parts(4_243_613, 0).saturating_mul(x.into())) + // Minimum execution time: 51_230_000 picoseconds. + Weight::from_parts(48_696_805, 4764) + // Standard Error: 6_139 + .saturating_add(Weight::from_parts(3_374_191, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking EraRewards (r:1 w:0) - /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) - /// The range of component `x` is `[1, 8]`. + /// The range of component `x` is `[1, 16]`. fn claim_staker_rewards_ongoing_period(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `438 + x * (8 ±0)` + // Measured: `501` // Estimated: `4764` - // Minimum execution time: 47_000_000 picoseconds. - Weight::from_parts(48_357_596, 4764) - // Standard Error: 16_408 - .saturating_add(Weight::from_parts(4_302_059, 0).saturating_mul(x.into())) + // Minimum execution time: 45_030_000 picoseconds. + Weight::from_parts(43_179_071, 4764) + // Standard Error: 5_547 + .saturating_add(Weight::from_parts(3_296_864, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -665,66 +679,68 @@ impl WeightInfo for () { /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) /// Storage: DappStaking PeriodEnd (r:1 w:0) /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) fn claim_bonus_reward() -> Weight { // Proof Size summary in bytes: - // Measured: `158` - // Estimated: `3603` - // Minimum execution time: 34_000_000 picoseconds. - Weight::from_parts(36_000_000, 3603) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) + // Measured: `267` + // Estimated: `3775` + // Minimum execution time: 42_248_000 picoseconds. + Weight::from_parts(42_687_000, 3775) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking DAppTiers (r:1 w:1) - /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) fn claim_dapp_reward() -> Weight { // Proof Size summary in bytes: - // Measured: `1086` - // Estimated: `3948` - // Minimum execution time: 37_000_000 picoseconds. - Weight::from_parts(41_000_000, 3948) + // Measured: `3021` + // Estimated: `5548` + // Minimum execution time: 50_968_000 picoseconds. + Weight::from_parts(51_778_000, 5548) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } /// Storage: DappStaking IntegratedDApps (r:1 w:0) - /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) /// Storage: DappStaking StakerInfo (r:1 w:1) /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) fn unstake_from_unregistered() -> Weight { // Proof Size summary in bytes: - // Measured: `397` + // Measured: `389` // Estimated: `4764` - // Minimum execution time: 43_000_000 picoseconds. - Weight::from_parts(47_000_000, 4764) + // Minimum execution time: 42_329_000 picoseconds. + Weight::from_parts(42_737_000, 4764) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } - /// Storage: DappStaking StakerInfo (r:4 w:3) + /// Storage: DappStaking StakerInfo (r:9 w:8) /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) /// Storage: DappStaking Ledger (r:1 w:1) - /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) - /// Storage: Balances Locks (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) - /// Storage: Balances Freezes (r:1 w:0) - /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) - /// The range of component `x` is `[1, 3]`. + /// The range of component `x` is `[1, 8]`. fn cleanup_expired_entries(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `250 + x * (75 ±0)` + // Measured: `255 + x * (69 ±0)` // Estimated: `4764 + x * (2613 ±0)` - // Minimum execution time: 41_000_000 picoseconds. - Weight::from_parts(38_854_143, 4764) - // Standard Error: 54_134 - .saturating_add(Weight::from_parts(7_359_116, 0).saturating_mul(x.into())) + // Minimum execution time: 42_222_000 picoseconds. + Weight::from_parts(38_945_386, 4764) + // Standard Error: 14_325 + .saturating_add(Weight::from_parts(5_044_310, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(2_u64)) @@ -735,82 +751,90 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(10_000_000, 0) + // Minimum execution time: 9_971_000 picoseconds. + Weight::from_parts(10_190_000, 0) } /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) - /// Storage: DappStaking NextTierConfig (r:1 w:1) - /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// Storage: DappStaking EraRewards (r:1 w:1) - /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) - /// Storage: DappStaking TierConfig (r:0 w:1) - /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) fn on_initialize_voting_to_build_and_earn() -> Weight { // Proof Size summary in bytes: - // Measured: `151` - // Estimated: `3870` - // Minimum execution time: 22_000_000 picoseconds. - Weight::from_parts(24_000_000, 3870) - .saturating_add(RocksDbWeight::get().reads(3_u64)) - .saturating_add(RocksDbWeight::get().writes(4_u64)) + // Measured: `16` + // Estimated: `4254` + // Minimum execution time: 17_308_000 picoseconds. + Weight::from_parts(17_774_000, 4254) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) } /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) /// Storage: DappStaking StaticTierParams (r:1 w:0) /// Proof: DappStaking StaticTierParams (max_values: Some(1), max_size: Some(167), added: 662, mode: MaxEncodedLen) - /// Storage: DappStaking TierConfig (r:1 w:0) + /// Storage: DappStaking TierConfig (r:1 w:1) /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// Storage: DappStaking EraRewards (r:1 w:1) - /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) /// Storage: DappStaking PeriodEnd (r:0 w:1) /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) /// Storage: DappStaking DAppTiers (r:0 w:1) - /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) - /// Storage: DappStaking NextTierConfig (r:0 w:1) - /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) fn on_initialize_build_and_earn_to_voting() -> Weight { // Proof Size summary in bytes: - // Measured: `685` - // Estimated: `3870` - // Minimum execution time: 34_000_000 picoseconds. - Weight::from_parts(37_000_000, 3870) + // Measured: `550` + // Estimated: `4254` + // Minimum execution time: 39_768_000 picoseconds. + Weight::from_parts(40_422_000, 4254) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } /// Storage: DappStaking CurrentEraInfo (r:1 w:1) /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) /// Storage: DappStaking EraRewards (r:1 w:1) - /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) /// Storage: DappStaking DAppTiers (r:0 w:1) - /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) fn on_initialize_build_and_earn_to_build_and_earn() -> Weight { // Proof Size summary in bytes: - // Measured: `73` - // Estimated: `3870` - // Minimum execution time: 17_000_000 picoseconds. - Weight::from_parts(18_000_000, 3870) + // Measured: `68` + // Estimated: `4254` + // Minimum execution time: 20_976_000 picoseconds. + Weight::from_parts(21_507_000, 4254) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: DappStaking ContractStake (r:101 w:0) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) /// Storage: DappStaking TierConfig (r:1 w:0) /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) /// The range of component `x` is `[0, 100]`. fn dapp_tier_assignment(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `158 + x * (33 ±0)` - // Estimated: `3063 + x * (2073 ±0)` - // Minimum execution time: 6_000_000 picoseconds. - Weight::from_parts(12_709_998, 3063) - // Standard Error: 8_047 - .saturating_add(Weight::from_parts(2_731_946, 0).saturating_mul(x.into())) + // Measured: `152 + x * (32 ±0)` + // Estimated: `3061 + x * (2071 ±0)` + // Minimum execution time: 7_374_000 picoseconds. + Weight::from_parts(10_826_637, 3061) + // Standard Error: 3_374 + .saturating_add(Weight::from_parts(2_291_643, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 2071).saturating_mul(x.into())) } + /// Storage: DappStaking HistoryCleanupMarker (r:1 w:1) + /// Proof: DappStaking HistoryCleanupMarker (max_values: Some(1), max_size: Some(8), added: 503, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:1) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) fn on_idle_cleanup() -> Weight { - RocksDbWeight::get().reads_writes(3, 2) + // Proof Size summary in bytes: + // Measured: `473` + // Estimated: `4254` + // Minimum execution time: 14_500_000 picoseconds. + Weight::from_parts(14_969_000, 4254) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) } } diff --git a/pallets/dapps-staking/src/pallet/mod.rs b/pallets/dapps-staking/src/pallet/mod.rs index c7d46591c1..7b65ea2d0f 100644 --- a/pallets/dapps-staking/src/pallet/mod.rs +++ b/pallets/dapps-staking/src/pallet/mod.rs @@ -306,7 +306,7 @@ pub mod pallet { // Runtime upgrade should be timed so we ensure that we complete it before // a new era is triggered. This code is just a safety net to ensure nothing is broken // if we fail to do that. - if PalletDisabled::::get() { + if PalletDisabled::::get() || T::ForcePalletDisabled::get() { return T::DbWeight::get().reads(1); } diff --git a/pallets/inflation/Cargo.toml b/pallets/inflation/Cargo.toml index 6b1065d9bb..a82a1480ad 100644 --- a/pallets/inflation/Cargo.toml +++ b/pallets/inflation/Cargo.toml @@ -18,6 +18,7 @@ frame-support = { workspace = true } frame-system = { workspace = true } scale-info = { workspace = true } sp-runtime = { workspace = true } +sp-std = { workspace = true } frame-benchmarking = { workspace = true, optional = true } @@ -33,6 +34,7 @@ std = [ "sp-core/std", "scale-info/std", "serde/std", + "sp-std/std", "frame-support/std", "frame-system/std", "pallet-balances/std", diff --git a/pallets/inflation/src/lib.rs b/pallets/inflation/src/lib.rs index 717aa764f1..b643fe5e32 100644 --- a/pallets/inflation/src/lib.rs +++ b/pallets/inflation/src/lib.rs @@ -102,9 +102,13 @@ use astar_primitives::{ dapp_staking::{CycleConfiguration, StakingRewardHandler}, Balance, BlockNumber, }; -use frame_support::{pallet_prelude::*, traits::Currency}; +use frame_support::{ + pallet_prelude::*, + traits::{Currency, GetStorageVersion, OnRuntimeUpgrade}, +}; use frame_system::{ensure_root, pallet_prelude::*}; use sp_runtime::{traits::CheckedAdd, Perquintill}; +use sp_std::marker::PhantomData; pub mod weights; pub use weights::WeightInfo; @@ -122,7 +126,11 @@ pub mod pallet { use super::*; + /// The current storage version. + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(PhantomData); // Negative imbalance type of this pallet. @@ -589,3 +597,42 @@ pub trait PayoutPerBlock { /// Payout reward to the collator responsible for producing the block. fn collators(reward: Imbalance); } + +/// `OnRuntimeUpgrade` logic for integrating this pallet into the live network. +#[cfg(feature = "try-runtime")] +use sp_std::vec::Vec; +pub struct PalletInflationInitConfig(PhantomData<(T, P)>); +impl> OnRuntimeUpgrade for PalletInflationInitConfig { + fn on_runtime_upgrade() -> Weight { + if Pallet::::on_chain_storage_version() >= STORAGE_VERSION { + return T::DbWeight::get().reads(1); + } + + // 1. Get & set inflation parameters + let inflation_params = P::get(); + InflationParams::::put(inflation_params.clone()); + + // 2. Calculation inflation config, set it & depossit event + let now = frame_system::Pallet::::block_number(); + let config = Pallet::::recalculate_inflation(now); + ActiveInflationConfig::::put(config.clone()); + + Pallet::::deposit_event(Event::::NewInflationConfiguration { config }); + + // 3. Set version + STORAGE_VERSION.put::>(); + + log::info!("Inflation pallet storage initialized."); + + T::WeightInfo::hook_with_recalculation() + .saturating_add(T::DbWeight::get().reads_writes(1, 2)) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { + assert_eq!(Pallet::::on_chain_storage_version(), STORAGE_VERSION); + assert!(InflationParams::::get().is_valid()); + + Ok(()) + } +} diff --git a/pallets/inflation/src/weights.rs b/pallets/inflation/src/weights.rs index 1cee14c199..08a53283c4 100644 --- a/pallets/inflation/src/weights.rs +++ b/pallets/inflation/src/weights.rs @@ -20,16 +20,16 @@ //! Autogenerated weights for pallet_inflation //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-11-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-12-20, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Dinos-MacBook-Pro.local`, CPU: `` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 +//! HOSTNAME: `devserver-01`, CPU: `Intel(R) Xeon(R) E-2236 CPU @ 3.40GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("shibuya-dev"), DB CACHE: 1024 // Executed Command: // ./target/release/astar-collator // benchmark // pallet -// --chain=dev +// --chain=shibuya-dev // --steps=50 // --repeat=20 // --pallet=pallet_inflation @@ -37,7 +37,7 @@ // --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 -// --output=weights.rs +// --output=./benchmark-results/shibuya-dev/inflation_weights.rs // --template=./scripts/templates/weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -65,43 +65,50 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(9_000_000, 0) + // Minimum execution time: 9_036_000 picoseconds. + Weight::from_parts(9_186_000, 0) .saturating_add(T::DbWeight::get().writes(1_u64)) } fn force_set_inflation_config() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(9_000_000, 0) + // Minimum execution time: 9_073_000 picoseconds. + Weight::from_parts(9_411_000, 0) } /// Storage: Inflation InflationParams (r:1 w:0) /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) fn force_inflation_recalculation() -> Weight { // Proof Size summary in bytes: - // Measured: `59` + // Measured: `58` // Estimated: `1549` - // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(16_000_000, 1549) + // Minimum execution time: 14_839_000 picoseconds. + Weight::from_parts(15_198_000, 1549) .saturating_add(T::DbWeight::get().reads(1_u64)) } + /// Storage: System Account (r:2 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) /// Storage: Inflation InflationParams (r:1 w:0) /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) fn hook_with_recalculation() -> Weight { // Proof Size summary in bytes: - // Measured: `59` - // Estimated: `1549` - // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(15_000_000, 1549) - .saturating_add(T::DbWeight::get().reads(1_u64)) + // Measured: `232` + // Estimated: `6196` + // Minimum execution time: 31_965_000 picoseconds. + Weight::from_parts(32_498_000, 6196) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) } + /// Storage: System Account (r:2 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) fn hook_without_recalculation() -> Weight { // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 2_000_000 picoseconds. - Weight::from_parts(3_000_000, 0) + // Measured: `174` + // Estimated: `6196` + // Minimum execution time: 22_235_000 picoseconds. + Weight::from_parts(22_378_000, 6196) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) } } @@ -113,42 +120,49 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(9_000_000, 0) + // Minimum execution time: 9_036_000 picoseconds. + Weight::from_parts(9_186_000, 0) .saturating_add(RocksDbWeight::get().writes(1_u64)) } fn force_set_inflation_config() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(9_000_000, 0) + // Minimum execution time: 9_073_000 picoseconds. + Weight::from_parts(9_411_000, 0) } /// Storage: Inflation InflationParams (r:1 w:0) /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) fn force_inflation_recalculation() -> Weight { // Proof Size summary in bytes: - // Measured: `59` + // Measured: `58` // Estimated: `1549` - // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(16_000_000, 1549) + // Minimum execution time: 14_839_000 picoseconds. + Weight::from_parts(15_198_000, 1549) .saturating_add(RocksDbWeight::get().reads(1_u64)) } + /// Storage: System Account (r:2 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) /// Storage: Inflation InflationParams (r:1 w:0) /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) fn hook_with_recalculation() -> Weight { // Proof Size summary in bytes: - // Measured: `59` - // Estimated: `1549` - // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(15_000_000, 1549) - .saturating_add(RocksDbWeight::get().reads(1_u64)) + // Measured: `232` + // Estimated: `6196` + // Minimum execution time: 31_965_000 picoseconds. + Weight::from_parts(32_498_000, 6196) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) } + /// Storage: System Account (r:2 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) fn hook_without_recalculation() -> Weight { // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 2_000_000 picoseconds. - Weight::from_parts(3_000_000, 0) + // Measured: `174` + // Estimated: `6196` + // Minimum execution time: 22_235_000 picoseconds. + Weight::from_parts(22_378_000, 6196) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) } } diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index de1e2ac1a6..92740a2eae 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -68,7 +68,6 @@ pallet-transaction-payment-rpc-runtime-api = { workspace = true } # Astar pallets astar-primitives = { workspace = true } pallet-block-rewards-hybrid = { workspace = true } -pallet-chain-extension-dapps-staking = { workspace = true } pallet-chain-extension-unified-accounts = { workspace = true } pallet-chain-extension-xvm = { workspace = true } pallet-dapp-staking-migration = { workspace = true } @@ -124,7 +123,6 @@ std = [ "pallet-block-rewards-hybrid/std", "pallet-contracts/std", "pallet-contracts-primitives/std", - "pallet-chain-extension-dapps-staking/std", "pallet-chain-extension-xvm/std", "pallet-chain-extension-unified-accounts/std", "pallet-dapps-staking/std", diff --git a/runtime/local/src/chain_extensions.rs b/runtime/local/src/chain_extensions.rs index 87fc75da1a..a368fa8d18 100644 --- a/runtime/local/src/chain_extensions.rs +++ b/runtime/local/src/chain_extensions.rs @@ -22,7 +22,6 @@ use super::{Runtime, UnifiedAccounts, Xvm}; pub use pallet_chain_extension_assets::AssetsExtension; use pallet_contracts::chain_extension::RegisteredChainExtension; -pub use pallet_chain_extension_dapps_staking::DappsStakingExtension; pub use pallet_chain_extension_unified_accounts::UnifiedAccountsExtension; pub use pallet_chain_extension_xvm::XvmExtension; diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index c5d8abae9a..b1bec5ead9 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -61,12 +61,15 @@ use sp_runtime::{ }; use sp_std::prelude::*; -pub use astar_primitives::{ - dapp_staking::{CycleConfiguration, SmartContract, StakingRewardHandler}, +use astar_primitives::{ + dapp_staking::{CycleConfiguration, SmartContract}, evm::{EvmRevertCodeHandler, HashedDefaultMappings}, - AccountId, Address, AssetId, Balance, BlockNumber, Hash, Header, Index, Signature, + Address, AssetId, Balance, BlockNumber, Hash, Header, Index, }; + +pub use astar_primitives::{AccountId, Signature}; pub use pallet_block_rewards_hybrid::RewardDistributionConfig; +pub use pallet_dapp_staking_v3::TierThreshold; pub use crate::precompiles::WhitelistedCalls; #[cfg(feature = "std")] @@ -1156,8 +1159,7 @@ pub type Executive = frame_executive::Executive< Migrations, >; -// TODO: remove this prior to the PR merge -pub type Migrations = (pallet_dapp_staking_migration::DappStakingMigrationHandler,); +pub type Migrations = (); type EventRecord = frame_system::EventRecord< ::RuntimeEvent, diff --git a/runtime/shibuya/Cargo.toml b/runtime/shibuya/Cargo.toml index 73c2b42296..bd50815120 100644 --- a/runtime/shibuya/Cargo.toml +++ b/runtime/shibuya/Cargo.toml @@ -20,6 +20,7 @@ smallvec = { workspace = true } fp-rpc = { workspace = true } fp-self-contained = { workspace = true } sp-api = { workspace = true } +sp-arithmetic = { workspace = true } sp-block-builder = { workspace = true } sp-consensus-aura = { workspace = true } sp-core = { workspace = true } @@ -96,25 +97,29 @@ orml-xtokens = { workspace = true } # Astar pallets astar-primitives = { workspace = true } -pallet-block-rewards-hybrid = { workspace = true } pallet-chain-extension-unified-accounts = { workspace = true } pallet-chain-extension-xvm = { workspace = true } pallet-collator-selection = { workspace = true } +pallet-dapp-staking-migration = { workspace = true } +pallet-dapp-staking-v3 = { workspace = true } pallet-dapps-staking = { workspace = true } pallet-dynamic-evm-base-fee = { workspace = true } pallet-ethereum-checked = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } -pallet-evm-precompile-dapps-staking = { workspace = true } +pallet-evm-precompile-dapp-staking-v3 = { workspace = true } pallet-evm-precompile-sr25519 = { workspace = true } pallet-evm-precompile-substrate-ecdsa = { workspace = true } pallet-evm-precompile-unified-accounts = { workspace = true } pallet-evm-precompile-xcm = { workspace = true } pallet-evm-precompile-xvm = { workspace = true } +pallet-inflation = { workspace = true } pallet-unified-accounts = { workspace = true } pallet-xc-asset-config = { workspace = true } pallet-xcm = { workspace = true } pallet-xvm = { workspace = true } +dapp-staking-v3-runtime-api = { workspace = true } + precompile-utils = { workspace = true } # Moonbeam tracing @@ -153,6 +158,7 @@ std = [ "sp-block-builder/std", "sp-transaction-pool/std", "sp-inherents/std", + "sp-arithmetic/std", "frame-support/std", "frame-executive/std", "frame-system/std", @@ -161,7 +167,6 @@ std = [ "pallet-aura/std", "pallet-assets/std", "pallet-balances/std", - "pallet-block-rewards-hybrid/std", "pallet-contracts/std", "pallet-contracts-primitives/std", "pallet-chain-extension-xvm/std", @@ -178,7 +183,7 @@ std = [ "pallet-evm-precompile-ed25519/std", "pallet-evm-precompile-modexp/std", "pallet-evm-precompile-sha3fips/std", - "pallet-evm-precompile-dapps-staking/std", + "pallet-evm-precompile-dapp-staking-v3/std", "pallet-evm-precompile-sr25519/std", "pallet-evm-precompile-substrate-ecdsa/std", "pallet-evm-precompile-assets-erc20/std", @@ -186,6 +191,10 @@ std = [ "pallet-evm-precompile-xvm/std", "pallet-evm-precompile-unified-accounts/std", "pallet-dapps-staking/std", + "pallet-dapp-staking-v3/std", + "pallet-dapp-staking-migration/std", + "dapp-staking-v3-runtime-api/std", + "pallet-inflation/std", "pallet-identity/std", "pallet-multisig/std", "pallet-insecure-randomness-collective-flip/std", @@ -242,7 +251,9 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "pallet-dapps-staking/runtime-benchmarks", - "pallet-block-rewards-hybrid/runtime-benchmarks", + "pallet-dapp-staking-v3/runtime-benchmarks", + "pallet-dapp-staking-migration/runtime-benchmarks", + "pallet-inflation/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-collective/runtime-benchmarks", @@ -269,8 +280,10 @@ try-runtime = [ "frame-system/try-runtime", "pallet-aura/try-runtime", "pallet-balances/try-runtime", - "pallet-block-rewards-hybrid/try-runtime", "pallet-dapps-staking/try-runtime", + "pallet-dapp-staking-v3/try-runtime", + "pallet-dapp-staking-migration/try-runtime", + "pallet-inflation/try-runtime", "pallet-sudo/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index 3a4a0a3119..228718d421 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -22,7 +22,6 @@ // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. #![recursion_limit = "256"] -use astar_primitives::evm::HashedDefaultMappings; use cumulus_pallet_parachain_system::AnyRelayNumber; use frame_support::{ construct_runtime, @@ -54,6 +53,7 @@ use pallet_transaction_payment::{ use parity_scale_codec::{Compact, Decode, Encode, MaxEncodedLen}; use polkadot_runtime_common::BlockHashCount; use sp_api::impl_runtime_apis; +use sp_arithmetic::fixed_point::FixedU64; use sp_core::{ConstBool, OpaqueMetadata, H160, H256, U256}; use sp_inherents::{CheckInherentsResult, InherentData}; use sp_runtime::{ @@ -67,12 +67,16 @@ use sp_runtime::{ }; use sp_std::prelude::*; -pub use astar_primitives::{ - ethereum_checked::CheckedEthereumTransact, evm::EvmRevertCodeHandler, - xcm::AssetLocationIdConverter, AccountId, Address, AssetId, Balance, BlockNumber, Hash, Header, - Index, Signature, +use astar_primitives::{ + dapp_staking::{CycleConfiguration, SmartContract}, + evm::{EvmRevertCodeHandler, HashedDefaultMappings}, + xcm::AssetLocationIdConverter, + Address, AssetId, BlockNumber, Hash, Header, Index, }; -pub use pallet_block_rewards_hybrid::RewardDistributionConfig; + +pub use astar_primitives::{AccountId, Balance, Signature}; +pub use pallet_dapp_staking_v3::TierThreshold; +pub use pallet_inflation::InflationParameters; pub use crate::precompiles::WhitelistedCalls; @@ -297,7 +301,7 @@ parameter_types! { impl pallet_timestamp::Config for Runtime { /// A timestamp: milliseconds since the unix epoch. type Moment = u64; - type OnTimestampSet = BlockReward; + type OnTimestampSet = (); type MinimumPeriod = MinimumPeriod; type WeightInfo = pallet_timestamp::weights::SubstrateWeight; } @@ -402,32 +406,109 @@ impl pallet_dapps_staking::Config for Runtime { type MinimumRemainingAmount = MinimumRemainingAmount; type MaxEraStakeValues = MaxEraStakeValues; type UnregisteredDappRewardRetention = ConstU32<10>; + // Needed so benchmark can use the pallets extrinsics + #[cfg(feature = "runtime-benchmarks")] type ForcePalletDisabled = ConstBool; + #[cfg(not(feature = "runtime-benchmarks"))] + type ForcePalletDisabled = ConstBool; } -/// Multi-VM pointer to smart contract instance. -#[derive( - PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, MaxEncodedLen, scale_info::TypeInfo, -)] -pub enum SmartContract { - /// EVM smart contract instance. - Evm(sp_core::H160), - /// Wasm smart contract instance. - Wasm(AccountId), +// Placeholder until we introduce a pallet for this. +// Real solution will be an oracle. +pub struct DummyPriceProvider; +impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { + fn average_price() -> FixedU64 { + FixedU64::from_rational(1, 10) + } } -impl Default for SmartContract { - fn default() -> Self { - SmartContract::Evm(H160::repeat_byte(0x00)) +#[cfg(feature = "runtime-benchmarks")] +pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); +#[cfg(feature = "runtime-benchmarks")] +impl pallet_dapp_staking_v3::BenchmarkHelper, AccountId> + for BenchmarkHelper, AccountId> +{ + fn get_smart_contract(id: u32) -> SmartContract { + let id_bytes = id.to_le_bytes(); + let mut account = [0u8; 32]; + account[..id_bytes.len()].copy_from_slice(&id_bytes); + + SmartContract::Wasm(AccountId::from(account)) + } + + fn set_balance(account: &AccountId, amount: Balance) { + use frame_support::traits::fungible::Unbalanced as FunUnbalanced; + Balances::write_balance(account, amount) + .expect("Must succeed in test/benchmark environment."); + } +} + +impl pallet_dapp_staking_v3::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type Currency = Balances; + type SmartContract = SmartContract; + type ManagerOrigin = frame_system::EnsureRoot; + type NativePriceProvider = DummyPriceProvider; + type StakingRewardHandler = Inflation; + type CycleConfiguration = InflationCycleConfig; + type EraRewardSpanLength = ConstU32<16>; + type RewardRetentionInPeriods = ConstU32<2>; // Low enough value so we can get some expired rewards during testing + type MaxNumberOfContracts = ConstU32<500>; + type MaxUnlockingChunks = ConstU32<8>; + type MinimumLockedAmount = MinimumStakingAmount; // Keep the same as the old pallet + type UnlockingPeriod = ConstU32<4>; // Keep it low so it's easier to test + type MaxNumberOfStakedContracts = ConstU32<8>; + type MinimumStakeAmount = MinimumStakingAmount; + type NumberOfTiers = ConstU32<4>; + type WeightInfo = weights::pallet_dapp_staking_v3::SubstrateWeight; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = BenchmarkHelper, AccountId>; +} + +pub struct InflationPayoutPerBlock; +impl pallet_inflation::PayoutPerBlock for InflationPayoutPerBlock { + fn treasury(reward: NegativeImbalance) { + Balances::resolve_creating(&TreasuryPalletId::get().into_account_truncating(), reward); + } + + fn collators(reward: NegativeImbalance) { + ToStakingPot::on_unbalanced(reward); } } -impl> From<[u8; 32]> for SmartContract { - fn from(input: [u8; 32]) -> Self { - SmartContract::Wasm(input.into()) +pub struct InflationCycleConfig; +impl CycleConfiguration for InflationCycleConfig { + fn periods_per_cycle() -> u32 { + 2 + } + + fn eras_per_voting_subperiod() -> u32 { + 8 + } + + fn eras_per_build_and_earn_subperiod() -> u32 { + 20 + } + + fn blocks_per_era() -> BlockNumber { + 6 * HOURS } } +impl pallet_inflation::Config for Runtime { + type Currency = Balances; + type PayoutPerBlock = InflationPayoutPerBlock; + type CycleConfiguration = InflationCycleConfig; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_inflation::weights::SubstrateWeight; +} + +impl pallet_dapp_staking_migration::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_dapp_staking_migration::weights::SubstrateWeight; +} + impl pallet_utility::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; @@ -531,41 +612,6 @@ impl OnUnbalanced for ToStakingPot { } } -pub struct DappsStakingTvlProvider(); -impl Get for DappsStakingTvlProvider { - fn get() -> Balance { - DappsStaking::tvl() - } -} - -pub struct BeneficiaryPayout(); -impl pallet_block_rewards_hybrid::BeneficiaryPayout for BeneficiaryPayout { - fn treasury(reward: NegativeImbalance) { - Balances::resolve_creating(&TreasuryPalletId::get().into_account_truncating(), reward); - } - - fn collators(reward: NegativeImbalance) { - ToStakingPot::on_unbalanced(reward); - } - - fn dapps_staking(stakers: NegativeImbalance, dapps: NegativeImbalance) { - DappsStaking::rewards(stakers, dapps) - } -} - -parameter_types! { - pub const MaxBlockRewardAmount: Balance = 230_718 * MILLISBY; -} - -impl pallet_block_rewards_hybrid::Config for Runtime { - type Currency = Balances; - type DappsStakingTvlProvider = DappsStakingTvlProvider; - type BeneficiaryPayout = BeneficiaryPayout; - type MaxBlockRewardAmount = MaxBlockRewardAmount; - type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_block_rewards_hybrid::weights::SubstrateWeight; -} - parameter_types! { pub const ExistentialDeposit: Balance = 1_000_000; pub const MaxLocks: u32 = 50; @@ -583,9 +629,9 @@ impl pallet_balances::Config for Runtime { type AccountStore = frame_system::Pallet; type WeightInfo = weights::pallet_balances::SubstrateWeight; type HoldIdentifier = (); - type FreezeIdentifier = (); + type FreezeIdentifier = RuntimeFreezeReason; type MaxHolds = ConstU32<0>; - type MaxFreezes = ConstU32<0>; + type MaxFreezes = ConstU32<1>; } parameter_types! { @@ -1080,7 +1126,7 @@ pub enum ProxyType { /// Only reject_announcement call from pallet proxy allowed for proxy account CancelProxy, /// All runtime calls from pallet DappStaking allowed for proxy account - DappsStaking, + DappStaking, /// Only claim_staker call from pallet DappStaking allowed for proxy account StakerRewardClaim, } @@ -1115,7 +1161,7 @@ impl InstanceFilter for ProxyType { | RuntimeCall::Vesting(pallet_vesting::Call::vest{..}) | RuntimeCall::Vesting(pallet_vesting::Call::vest_other{..}) // Specifically omitting Vesting `vested_transfer`, and `force_vested_transfer` - | RuntimeCall::DappsStaking(..) + | RuntimeCall::DappStaking(..) // Skip entire Assets pallet | RuntimeCall::CollatorSelection(..) | RuntimeCall::Session(..) @@ -1166,13 +1212,15 @@ impl InstanceFilter for ProxyType { ) } // All runtime calls from pallet DappStaking allowed for proxy account - ProxyType::DappsStaking => { - matches!(c, RuntimeCall::DappsStaking(..)) + ProxyType::DappStaking => { + matches!(c, RuntimeCall::DappStaking(..)) } ProxyType::StakerRewardClaim => { matches!( c, - RuntimeCall::DappsStaking(pallet_dapps_staking::Call::claim_staker { .. }) + RuntimeCall::DappStaking( + pallet_dapp_staking_v3::Call::claim_staker_rewards { .. } + ) ) } } @@ -1184,7 +1232,7 @@ impl InstanceFilter for ProxyType { (ProxyType::Any, _) => true, (_, ProxyType::Any) => false, (ProxyType::NonTransfer, _) => true, - (ProxyType::DappsStaking, ProxyType::StakerRewardClaim) => true, + (ProxyType::DappStaking, ProxyType::StakerRewardClaim) => true, _ => false, } } @@ -1252,8 +1300,8 @@ construct_runtime!( TransactionPayment: pallet_transaction_payment = 30, Balances: pallet_balances = 31, Vesting: pallet_vesting = 32, - DappsStaking: pallet_dapps_staking = 34, - BlockReward: pallet_block_rewards_hybrid = 35, + DappStaking: pallet_dapp_staking_v3 = 34, + Inflation: pallet_inflation = 35, Assets: pallet_assets = 36, Authorship: pallet_authorship = 40, @@ -1287,6 +1335,11 @@ construct_runtime!( Xvm: pallet_xvm = 90, Sudo: pallet_sudo = 99, + + // To be removed & cleaned up after migration has been finished + DappStakingMigration: pallet_dapp_staking_migration = 254, + // Legacy dApps staking v2, to be removed after migration has been finished + DappsStaking: pallet_dapps_staking = 255, } ); @@ -1324,10 +1377,118 @@ pub type Executive = frame_executive::Executive< Migrations, >; +parameter_types! { + pub const BlockRewardName: &'static str = "BlockReward"; +} /// All migrations that will run on the next runtime upgrade. /// /// Once done, migrations should be removed from the tuple. -pub type Migrations = (); +pub type Migrations = ( + pallet_inflation::PalletInflationInitConfig, + pallet_dapp_staking_v3::DAppStakingV3InitConfig, + frame_support::migrations::RemovePallet< + BlockRewardName, + ::DbWeight, + >, + // This will handle new pallet storage version setting & it will put the new pallet into maintenance mode. + // But it's most important for testing with try-runtime. + pallet_dapp_staking_migration::DappStakingMigrationHandler, +); + +/// Used to initialize inflation parameters for the runtime. +pub struct InitInflationParams; +impl Get for InitInflationParams { + fn get() -> pallet_inflation::InflationParameters { + pallet_inflation::InflationParameters { + // Recalculation is done every two weeks, hence the small %. + max_inflation_rate: Perquintill::from_percent(1), + treasury_part: Perquintill::from_percent(5), + collators_part: Perquintill::from_percent(3), + dapps_part: Perquintill::from_percent(20), + base_stakers_part: Perquintill::from_percent(25), + adjustable_stakers_part: Perquintill::from_percent(35), + bonus_part: Perquintill::from_percent(12), + ideal_staking_rate: Perquintill::from_percent(20), + } + } +} + +use frame_support::BoundedVec; +use pallet_dapp_staking_v3::{EraNumber, TierParameters, TiersConfiguration}; +type NumberOfTiers = ::NumberOfTiers; +/// Used to initialize dApp staking parameters for the runtime. +pub struct InitDappStakingv3Params; +impl + Get<( + EraNumber, + TierParameters, + TiersConfiguration, + )> for InitDappStakingv3Params +{ + fn get() -> ( + EraNumber, + TierParameters, + TiersConfiguration, + ) { + // 1. Prepare init values + + // Init era of dApp staking v3 should be the next era after dApp staking v2 + let init_era = pallet_dapps_staking::CurrentEra::::get().saturating_add(1); + + // Reward portions according to the Tokenomics 2.0 report + let reward_portion = BoundedVec::try_from(vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ]) + .unwrap_or_default(); + + // Tier thresholds adjusted according to numbers observed on Shibuya + let tier_thresholds = BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { + amount: SBY.saturating_mul(1_000_000), + minimum_amount: SBY.saturating_mul(150_000), + }, + TierThreshold::DynamicTvlAmount { + amount: SBY.saturating_mul(100_000), + minimum_amount: SBY.saturating_mul(60_000), + }, + TierThreshold::DynamicTvlAmount { + amount: SBY.saturating_mul(50_000), + minimum_amount: SBY.saturating_mul(15_000), + }, + TierThreshold::FixedTvlAmount { + amount: SBY.saturating_mul(10_000), + }, + ]) + .unwrap_or_default(); + + // 2. Tier params + let tier_params = + TierParameters::<::NumberOfTiers> { + reward_portion: reward_portion.clone(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ]) + .unwrap_or_default(), + tier_thresholds: tier_thresholds.clone(), + }; + + // 3. Init tier config + let init_tier_config = TiersConfiguration { + number_of_slots: 100, + slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap_or_default(), + reward_portion, + tier_thresholds, + }; + + (init_era, tier_params, init_tier_config) + } +} type EventRecord = frame_system::EventRecord< ::RuntimeEvent, @@ -1405,7 +1566,9 @@ mod benches { [pallet_balances, Balances] [pallet_timestamp, Timestamp] [pallet_dapps_staking, DappsStaking] - [block_rewards_hybrid, BlockReward] + [pallet_dapp_staking_v3, DappStaking] + [pallet_inflation, Inflation] + [pallet_dapp_staking_migration, DappStakingMigration] [pallet_xc_asset_config, XcAssetConfig] [pallet_collator_selection, CollatorSelection] [pallet_xcm, PolkadotXcm] @@ -1870,6 +2033,20 @@ impl_runtime_apis! { } } + impl dapp_staking_v3_runtime_api::DappStakingApi for Runtime { + fn eras_per_voting_subperiod() -> pallet_dapp_staking_v3::EraNumber { + InflationCycleConfig::eras_per_voting_subperiod() + } + + fn eras_per_build_and_earn_subperiod() -> pallet_dapp_staking_v3::EraNumber { + InflationCycleConfig::eras_per_build_and_earn_subperiod() + } + + fn blocks_per_era() -> BlockNumber { + InflationCycleConfig::blocks_per_era() + } + } + #[cfg(feature = "runtime-benchmarks")] impl frame_benchmarking::Benchmark for Runtime { fn benchmark_metadata(extra: bool) -> ( diff --git a/runtime/shibuya/src/precompiles.rs b/runtime/shibuya/src/precompiles.rs index 93f3e4ec16..c9f34a9a9d 100644 --- a/runtime/shibuya/src/precompiles.rs +++ b/runtime/shibuya/src/precompiles.rs @@ -24,7 +24,7 @@ use frame_support::{parameter_types, traits::Contains}; use pallet_evm_precompile_assets_erc20::Erc20AssetsPrecompileSet; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; -use pallet_evm_precompile_dapps_staking::DappsStakingWrapper; +use pallet_evm_precompile_dapp_staking_v3::DappStakingV3Precompile; use pallet_evm_precompile_dispatch::Dispatch; use pallet_evm_precompile_ed25519::Ed25519Verify; use pallet_evm_precompile_modexp::Modexp; @@ -59,7 +59,7 @@ impl Contains for WhitelistedCalls { | RuntimeCall::Utility(pallet_utility::Call::batch_all { calls }) => { calls.iter().all(|call| WhitelistedCalls::contains(call)) } - RuntimeCall::DappsStaking(_) => true, + RuntimeCall::DappStaking(_) => true, RuntimeCall::Assets(pallet_assets::Call::transfer { .. }) => true, _ => false, } @@ -93,7 +93,7 @@ pub type ShibuyaPrecompilesSetAt = ( // Astar specific precompiles: PrecompileAt< AddressU64<20481>, - DappsStakingWrapper, + DappStakingV3Precompile, (CallableByContract, CallableByPrecompile), >, PrecompileAt< diff --git a/runtime/shibuya/src/weights/mod.rs b/runtime/shibuya/src/weights/mod.rs index f1df12d585..b49f1c1a29 100644 --- a/runtime/shibuya/src/weights/mod.rs +++ b/runtime/shibuya/src/weights/mod.rs @@ -18,4 +18,7 @@ pub mod pallet_assets; pub mod pallet_balances; +pub mod pallet_dapp_staking_migration; +pub mod pallet_dapp_staking_v3; +pub mod pallet_inflation; pub mod pallet_xcm; diff --git a/runtime/shibuya/src/weights/pallet_dapp_staking_migration.rs b/runtime/shibuya/src/weights/pallet_dapp_staking_migration.rs new file mode 100644 index 0000000000..cb43de8369 --- /dev/null +++ b/runtime/shibuya/src/weights/pallet_dapp_staking_migration.rs @@ -0,0 +1,133 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for pallet_dapp_staking_migration +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-12-20, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `devserver-01`, CPU: `Intel(R) Xeon(R) E-2236 CPU @ 3.40GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("shibuya-dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=shibuya-dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_dapp_staking_migration +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./benchmark-results/shibuya-dev/dapp_staking_migration_weights.rs +// --template=./scripts/templates/weight-template.hbs + + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; +use pallet_dapp_staking_migration::WeightInfo; + +/// Weights for pallet_dapp_staking_migration using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: DappsStaking RegisteredDapps (r:2 w:1) + /// Proof: DappsStaking RegisteredDapps (max_values: None, max_size: Some(86), added: 2561, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:1) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking NextDAppId (r:1 w:1) + /// Proof: DappStaking NextDAppId (max_values: Some(1), max_size: Some(2), added: 497, mode: MaxEncodedLen) + fn migrate_dapps_success() -> Weight { + // Proof Size summary in bytes: + // Measured: `558` + // Estimated: `6112` + // Minimum execution time: 46_218_000 picoseconds. + Weight::from_parts(47_610_000, 6112) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappsStaking RegisteredDapps (r:1 w:0) + /// Proof: DappsStaking RegisteredDapps (max_values: None, max_size: Some(86), added: 2561, mode: MaxEncodedLen) + fn migrate_dapps_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3551` + // Minimum execution time: 3_385_000 picoseconds. + Weight::from_parts(3_552_000, 3551) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: DappsStaking Ledger (r:2 w:1) + /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn migrate_ledger_success() -> Weight { + // Proof Size summary in bytes: + // Measured: `1748` + // Estimated: `6472` + // Minimum execution time: 69_553_000 picoseconds. + Weight::from_parts(70_319_000, 6472) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } + /// Storage: DappsStaking Ledger (r:1 w:0) + /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) + fn migrate_ledger_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3731` + // Minimum execution time: 2_918_000 picoseconds. + Weight::from_parts(3_022_000, 3731) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: DappsStaking Ledger (r:2 w:1) + /// Proof: DappsStaking Ledger (max_values: None, max_size: Some(266), added: 2741, mode: MaxEncodedLen) + fn cleanup_old_storage_success() -> Weight { + // Proof Size summary in bytes: + // Measured: `739` + // Estimated: `6472` + // Minimum execution time: 7_109_000 picoseconds. + Weight::from_parts(7_383_000, 6472) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn cleanup_old_storage_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_095_000 picoseconds. + Weight::from_parts(2_213_000, 0) + } +} \ No newline at end of file diff --git a/runtime/shibuya/src/weights/pallet_dapp_staking_v3.rs b/runtime/shibuya/src/weights/pallet_dapp_staking_v3.rs new file mode 100644 index 0000000000..0bcfcee538 --- /dev/null +++ b/runtime/shibuya/src/weights/pallet_dapp_staking_v3.rs @@ -0,0 +1,432 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for pallet_dapp_staking_v3 +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-12-20, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `devserver-01`, CPU: `Intel(R) Xeon(R) E-2236 CPU @ 3.40GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("shibuya-dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=shibuya-dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_dapp_staking_v3 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./benchmark-results/shibuya-dev/dapp_staking_v3_weights.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; +use pallet_dapp_staking_v3::WeightInfo; + +/// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn maintenance_mode() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_474_000 picoseconds. + Weight::from_parts(8_711_000, 0) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:1) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking NextDAppId (r:1 w:1) + /// Proof: DappStaking NextDAppId (max_values: Some(1), max_size: Some(2), added: 497, mode: MaxEncodedLen) + fn register() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3091` + // Minimum execution time: 16_360_000 picoseconds. + Weight::from_parts(16_697_000, 3091) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + fn set_dapp_reward_beneficiary() -> Weight { + // Proof Size summary in bytes: + // Measured: `75` + // Estimated: `3091` + // Minimum execution time: 12_927_000 picoseconds. + Weight::from_parts(13_229_000, 3091) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + fn set_dapp_owner() -> Weight { + // Proof Size summary in bytes: + // Measured: `75` + // Estimated: `3091` + // Minimum execution time: 13_610_000 picoseconds. + Weight::from_parts(13_851_000, 3091) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:0 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) + fn unregister() -> Weight { + // Proof Size summary in bytes: + // Measured: `75` + // Estimated: `3091` + // Minimum execution time: 16_704_000 picoseconds. + Weight::from_parts(16_952_000, 3091) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn lock() -> Weight { + // Proof Size summary in bytes: + // Measured: `12` + // Estimated: `4764` + // Minimum execution time: 31_680_000 picoseconds. + Weight::from_parts(32_075_000, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn unlock() -> Weight { + // Proof Size summary in bytes: + // Measured: `156` + // Estimated: `4764` + // Minimum execution time: 34_576_000 picoseconds. + Weight::from_parts(34_777_000, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 8]`. + fn claim_unlocked(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `187` + // Estimated: `4764` + // Minimum execution time: 33_562_000 picoseconds. + Weight::from_parts(34_600_552, 4764) + // Standard Error: 5_079 + .saturating_add(Weight::from_parts(193_345, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn relock_unlocking() -> Weight { + // Proof Size summary in bytes: + // Measured: `182` + // Estimated: `4764` + // Minimum execution time: 36_436_000 picoseconds. + Weight::from_parts(37_262_000, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:1 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + fn stake() -> Weight { + // Proof Size summary in bytes: + // Measured: `250` + // Estimated: `4764` + // Minimum execution time: 43_866_000 picoseconds. + Weight::from_parts(44_468_000, 4764) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:1 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + fn unstake() -> Weight { + // Proof Size summary in bytes: + // Measured: `427` + // Estimated: `4764` + // Minimum execution time: 47_368_000 picoseconds. + Weight::from_parts(48_049_000, 4764) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:0) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:0) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 16]`. + fn claim_staker_rewards_past_period(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `560` + // Estimated: `4764` + // Minimum execution time: 51_230_000 picoseconds. + Weight::from_parts(48_696_805, 4764) + // Standard Error: 6_139 + .saturating_add(Weight::from_parts(3_374_191, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:0) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 16]`. + fn claim_staker_rewards_ongoing_period(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `501` + // Estimated: `4764` + // Minimum execution time: 45_030_000 picoseconds. + Weight::from_parts(43_179_071, 4764) + // Standard Error: 5_547 + .saturating_add(Weight::from_parts(3_296_864, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:0) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + fn claim_bonus_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `267` + // Estimated: `3775` + // Minimum execution time: 42_248_000 picoseconds. + Weight::from_parts(42_687_000, 3775) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:1 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) + fn claim_dapp_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `3021` + // Estimated: `5548` + // Minimum execution time: 50_968_000 picoseconds. + Weight::from_parts(51_778_000, 5548) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(121), added: 2101, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + fn unstake_from_unregistered() -> Weight { + // Proof Size summary in bytes: + // Measured: `389` + // Estimated: `4764` + // Minimum execution time: 42_329_000 picoseconds. + Weight::from_parts(42_737_000, 4764) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: DappStaking StakerInfo (r:9 w:8) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(310), added: 2785, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:1) + /// Proof: Balances Freezes (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:0) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 8]`. + fn cleanup_expired_entries(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `255 + x * (69 ±0)` + // Estimated: `4764 + x * (2613 ±0)` + // Minimum execution time: 42_222_000 picoseconds. + Weight::from_parts(38_945_386, 4764) + // Standard Error: 14_325 + .saturating_add(Weight::from_parts(5_044_310, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2613).saturating_mul(x.into())) + } + fn force() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_971_000 picoseconds. + Weight::from_parts(10_190_000, 0) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) + fn on_initialize_voting_to_build_and_earn() -> Weight { + // Proof Size summary in bytes: + // Measured: `16` + // Estimated: `4254` + // Minimum execution time: 17_308_000 picoseconds. + Weight::from_parts(17_774_000, 4254) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking StaticTierParams (r:1 w:0) + /// Proof: DappStaking StaticTierParams (max_values: Some(1), max_size: Some(167), added: 662, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:1) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:0 w:1) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) + fn on_initialize_build_and_earn_to_voting() -> Weight { + // Proof Size summary in bytes: + // Measured: `550` + // Estimated: `4254` + // Minimum execution time: 39_768_000 picoseconds. + Weight::from_parts(40_422_000, 4254) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) + fn on_initialize_build_and_earn_to_build_and_earn() -> Weight { + // Proof Size summary in bytes: + // Measured: `68` + // Estimated: `4254` + // Minimum execution time: 20_976_000 picoseconds. + Weight::from_parts(21_507_000, 4254) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking ContractStake (r:101 w:0) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(91), added: 2071, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 100]`. + fn dapp_tier_assignment(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `152 + x * (32 ±0)` + // Estimated: `3061 + x * (2071 ±0)` + // Minimum execution time: 7_374_000 picoseconds. + Weight::from_parts(10_826_637, 3061) + // Standard Error: 3_374 + .saturating_add(Weight::from_parts(2_291_643, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2071).saturating_mul(x.into())) + } + /// Storage: DappStaking HistoryCleanupMarker (r:1 w:1) + /// Proof: DappStaking HistoryCleanupMarker (max_values: Some(1), max_size: Some(8), added: 503, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:1) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(789), added: 3264, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(2083), added: 4558, mode: MaxEncodedLen) + fn on_idle_cleanup() -> Weight { + // Proof Size summary in bytes: + // Measured: `473` + // Estimated: `4254` + // Minimum execution time: 14_500_000 picoseconds. + Weight::from_parts(14_969_000, 4254) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } +} \ No newline at end of file diff --git a/runtime/shibuya/src/weights/pallet_inflation.rs b/runtime/shibuya/src/weights/pallet_inflation.rs new file mode 100644 index 0000000000..98616cb747 --- /dev/null +++ b/runtime/shibuya/src/weights/pallet_inflation.rs @@ -0,0 +1,105 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for pallet_inflation +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-12-20, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `devserver-01`, CPU: `Intel(R) Xeon(R) E-2236 CPU @ 3.40GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("shibuya-dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=shibuya-dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_inflation +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./benchmark-results/shibuya-dev/inflation_weights.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; +use pallet_inflation::WeightInfo; + +/// Weights for pallet_inflation using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Inflation InflationParams (r:0 w:1) + /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) + fn force_set_inflation_params() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_036_000 picoseconds. + Weight::from_parts(9_186_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn force_set_inflation_config() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_073_000 picoseconds. + Weight::from_parts(9_411_000, 0) + } + /// Storage: Inflation InflationParams (r:1 w:0) + /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) + fn force_inflation_recalculation() -> Weight { + // Proof Size summary in bytes: + // Measured: `58` + // Estimated: `1549` + // Minimum execution time: 14_839_000 picoseconds. + Weight::from_parts(15_198_000, 1549) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: System Account (r:2 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Inflation InflationParams (r:1 w:0) + /// Proof: Inflation InflationParams (max_values: Some(1), max_size: Some(64), added: 559, mode: MaxEncodedLen) + fn hook_with_recalculation() -> Weight { + // Proof Size summary in bytes: + // Measured: `232` + // Estimated: `6196` + // Minimum execution time: 31_965_000 picoseconds. + Weight::from_parts(32_498_000, 6196) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: System Account (r:2 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn hook_without_recalculation() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `6196` + // Minimum execution time: 22_235_000 picoseconds. + Weight::from_parts(22_378_000, 6196) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} \ No newline at end of file diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index 2295c836a1..5bb0a19714 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -25,6 +25,7 @@ pallet-assets = { workspace = true } pallet-balances = { workspace = true } pallet-contracts = { workspace = true } pallet-contracts-primitives = { workspace = true } +pallet-dapp-staking-v3 = { workspace = true } pallet-dapps-staking = { workspace = true } pallet-proxy = { workspace = true } pallet-utility = { workspace = true } diff --git a/tests/integration/src/dispatch_precompile_filter_new.rs b/tests/integration/src/dispatch_precompile_filter_new.rs new file mode 100644 index 0000000000..b406b43d05 --- /dev/null +++ b/tests/integration/src/dispatch_precompile_filter_new.rs @@ -0,0 +1,207 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . +#![cfg(test)] + +use crate::setup::*; +use astar_primitives::precompiles::DispatchFilterValidate; +use fp_evm::{ExitError, PrecompileFailure}; +use frame_support::{ + dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays}, + traits::Contains, +}; +use pallet_evm_precompile_dispatch::DispatchValidateT; +use parity_scale_codec::Compact; + +/// Whitelisted Calls are defined in the runtime +#[test] +fn filter_accepts_batch_call_with_whitelisted_calls() { + ExtBuilder::default().build().execute_with(|| { + let inner_call = RuntimeCall::DappStaking(DappStakingCall::Call::claim_staker_rewards {}); + let call = RuntimeCall::Utility(UtilityCall::batch { + calls: vec![inner_call], + }); + assert!(WhitelistedCalls::contains(&call)); + }); +} + +#[test] +fn filter_rejects_non_whitelisted_batch_calls() { + ExtBuilder::default().build().execute_with(|| { + // CASE1 - only non whitelisted calls + let transfer_call = RuntimeCall::Balances(BalancesCall::transfer { + dest: MultiAddress::Id(CAT), + value: 100_000_000_000, + }); + let transfer = Box::new(transfer_call); + let call = Box::new(RuntimeCall::Utility(UtilityCall::batch { + calls: vec![*transfer.clone()], + })); + + // Utility call containing Balances Call + assert!(!WhitelistedCalls::contains(&call)); + + // CASE 2 - now whitelisted mixed with whitelisted calls + + let staking_call = RuntimeCall::DappStaking(DappStakingCall::Call::claim_staker_rewards {}); + let staking = Box::new(staking_call); + + let call = Box::new(RuntimeCall::Utility(UtilityCall::batch { + calls: vec![*transfer, *staking.clone()], + })); + + // Utility call containing Balances Call and Dappsstaking Call Fails filter + assert!(!WhitelistedCalls::contains(&call)); + }); +} + +#[test] +fn filter_accepts_whitelisted_calls() { + ExtBuilder::default().build().execute_with(|| { + // Dappstaking call works + let stake_call = RuntimeCall::DappStaking(DappStakingCall::Call::claim_staker_rewards {}); + assert!(WhitelistedCalls::contains(&stake_call)); + + // Pallet::Assets transfer call works + let transfer_call = RuntimeCall::Assets(pallet_assets::Call::transfer { + id: Compact(0), + target: MultiAddress::Address20(H160::repeat_byte(0x01).into()), + amount: 100, + }); + assert!(WhitelistedCalls::contains(&transfer_call)); + }); +} + +#[test] +fn filter_rejects_non_whitelisted_calls() { + ExtBuilder::default().build().execute_with(|| { + // Random call from non whitelisted pallet doesn't work + let transfer_call = RuntimeCall::Balances(BalancesCall::transfer { + dest: MultiAddress::Id(CAT), + value: 100_000_000_000, + }); + assert!(!WhitelistedCalls::contains(&transfer_call)); + + // Only `transfer` call from pallet assets work + // Other random call from Pallet Assets doesn't work + let thaw_asset_call = + RuntimeCall::Assets(pallet_assets::Call::thaw_asset { id: Compact(0) }); + assert!(!WhitelistedCalls::contains(&thaw_asset_call)); + }) +} + +#[test] +fn filter_accepts_whitelisted_batch_all_calls() { + ExtBuilder::default().build().execute_with(|| { + let inner_call1 = RuntimeCall::DappStaking(DappStakingCall::Call::claim_staker_rewards {}); + let inner_call2 = RuntimeCall::DappStaking(DappStakingCall::Call::claim_staker_rewards {}); + let transfer_call = RuntimeCall::Assets(pallet_assets::Call::transfer { + id: Compact(0), + target: MultiAddress::Address20(H160::repeat_byte(0x01).into()), + amount: 100, + }); + let call = RuntimeCall::Utility(UtilityCall::batch_all { + calls: vec![inner_call1, inner_call2, transfer_call], + }); + assert!(WhitelistedCalls::contains(&call)); + }); +} + +#[test] +fn test_correct_dispatch_info_works() { + ExtBuilder::default().build().execute_with(|| { + // Mock implementation + struct Filter; + struct AccountId; + enum RuntimeCall { + System, + DappStaking, + } + impl GetDispatchInfo for RuntimeCall { + fn get_dispatch_info(&self) -> DispatchInfo { + // Default is Pays::Yes and DispatchCall::Normal + DispatchInfo::default() + } + } + impl Contains for Filter { + fn contains(t: &RuntimeCall) -> bool { + match t { + RuntimeCall::DappStaking => true, + _ => false, + } + } + } + // Case 1: Whitelisted Call with correct Dispatch info + assert_eq!( + DispatchFilterValidate::::validate_before_dispatch( + &AccountId, + &RuntimeCall::DappStaking + ), + Option::None + ); + // Case 2: Non-Whitelisted Call with correct Dispatch Info + assert_eq!( + DispatchFilterValidate::::validate_before_dispatch( + &AccountId, + &RuntimeCall::System + ), + Option::Some(PrecompileFailure::Error { + exit_status: ExitError::Other("call filtered out".into()), + }) + ); + }); +} + +#[test] +fn test_incorrect_dispatch_info_fails() { + ExtBuilder::default().build().execute_with(|| { + // Mock implementation + struct Filter; + struct AccountId; + enum RuntimeCall { + DappStaking, + } + impl GetDispatchInfo for RuntimeCall { + fn get_dispatch_info(&self) -> DispatchInfo { + DispatchInfo { + weight: Weight::default(), + class: DispatchClass::Normal, + // Should have been Pays::Yes for call to pass + pays_fee: Pays::No, + } + } + } + impl Contains for Filter { + fn contains(t: &RuntimeCall) -> bool { + match t { + RuntimeCall::DappStaking => true, + } + } + } + + // WhiteListed Call fails because of incorrect DispatchInfo + assert_eq!( + DispatchFilterValidate::::validate_before_dispatch( + &AccountId, + &RuntimeCall::DappStaking + ), + Option::Some(PrecompileFailure::Error { + exit_status: ExitError::Other("invalid call".into()), + }) + ); + }) +} diff --git a/tests/integration/src/dispatch_precompile_filter.rs b/tests/integration/src/dispatch_precompile_filter_old.rs similarity index 95% rename from tests/integration/src/dispatch_precompile_filter.rs rename to tests/integration/src/dispatch_precompile_filter_old.rs index 7992b39c0a..95d04bcfba 100644 --- a/tests/integration/src/dispatch_precompile_filter.rs +++ b/tests/integration/src/dispatch_precompile_filter_old.rs @@ -32,7 +32,7 @@ use parity_scale_codec::Compact; fn filter_accepts_batch_call_with_whitelisted_calls() { ExtBuilder::default().build().execute_with(|| { let contract = SmartContract::Evm(H160::repeat_byte(0x01)); - let inner_call = RuntimeCall::DappsStaking(DappStakingCall::Call::claim_staker { + let inner_call = RuntimeCall::DappsStaking(DappsStakingCall::Call::claim_staker { contract_id: contract.clone(), }); let call = RuntimeCall::Utility(UtilityCall::batch { @@ -61,7 +61,7 @@ fn filter_rejects_non_whitelisted_batch_calls() { // CASE 2 - now whitelisted mixed with whitelisted calls let contract = SmartContract::Evm(H160::repeat_byte(0x01)); - let staking_call = RuntimeCall::DappsStaking(DappStakingCall::Call::claim_staker { + let staking_call = RuntimeCall::DappsStaking(DappsStakingCall::Call::claim_staker { contract_id: contract.clone(), }); let staking = Box::new(staking_call); @@ -80,7 +80,7 @@ fn filter_accepts_whitelisted_calls() { ExtBuilder::default().build().execute_with(|| { // Dappstaking call works let contract = SmartContract::Evm(H160::repeat_byte(0x01)); - let stake_call = RuntimeCall::DappsStaking(DappStakingCall::Call::claim_staker { + let stake_call = RuntimeCall::DappsStaking(DappsStakingCall::Call::claim_staker { contract_id: contract.clone(), }); assert!(WhitelistedCalls::contains(&stake_call)); @@ -117,10 +117,10 @@ fn filter_rejects_non_whitelisted_calls() { fn filter_accepts_whitelisted_batch_all_calls() { ExtBuilder::default().build().execute_with(|| { let contract = SmartContract::Evm(H160::repeat_byte(0x01)); - let inner_call1 = RuntimeCall::DappsStaking(DappStakingCall::Call::claim_staker { + let inner_call1 = RuntimeCall::DappsStaking(DappsStakingCall::Call::claim_staker { contract_id: contract.clone(), }); - let inner_call2 = RuntimeCall::DappsStaking(DappStakingCall::Call::claim_staker { + let inner_call2 = RuntimeCall::DappsStaking(DappsStakingCall::Call::claim_staker { contract_id: contract.clone(), }); let transfer_call = RuntimeCall::Assets(pallet_assets::Call::transfer { diff --git a/tests/integration/src/lib.rs b/tests/integration/src/lib.rs index 012ee39d07..43b2d9843b 100644 --- a/tests/integration/src/lib.rs +++ b/tests/integration/src/lib.rs @@ -23,8 +23,12 @@ #[cfg(any(feature = "shibuya", feature = "shiden", feature = "astar"))] mod setup; -#[cfg(any(feature = "shibuya", feature = "shiden", feature = "astar"))] -mod proxy; +#[cfg(any(feature = "shibuya"))] +mod proxy_new; + +// Remove this once dApp staking v3 is integrated into Shiden & Astar +#[cfg(any(feature = "shiden", feature = "astar"))] +mod proxy_old; #[cfg(any(feature = "shibuya", feature = "shiden", feature = "astar"))] mod assets; @@ -32,7 +36,12 @@ mod assets; #[cfg(feature = "shibuya")] mod xvm; -#[cfg(any(feature = "shibuya", feature = "shiden", feature = "astar"))] -mod dispatch_precompile_filter; +#[cfg(any(feature = "shibuya"))] +mod dispatch_precompile_filter_new; + +// Remove this once dApp staking v3 is integrated into Shiden & Astar +#[cfg(any(feature = "shiden", feature = "astar"))] +mod dispatch_precompile_filter_old; + #[cfg(feature = "shibuya")] mod unified_accounts; diff --git a/tests/integration/src/proxy_new.rs b/tests/integration/src/proxy_new.rs new file mode 100644 index 0000000000..c54bc30ca8 --- /dev/null +++ b/tests/integration/src/proxy_new.rs @@ -0,0 +1,222 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::setup::*; +use astar_primitives::dapp_staking::SmartContractHandle; +use pallet_dapp_staking_v3::ForcingType; + +#[test] +fn test_utility_call_pass_for_any() { + new_test_ext().execute_with(|| { + // Any proxy should be allowed to make balance transfer call + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(ALICE), + MultiAddress::Id(BOB), + ProxyType::Any, + 0 + )); + + // Preparing Utility call + let transfer_call = RuntimeCall::Balances(BalancesCall::transfer { + dest: MultiAddress::Id(CAT), + value: 100_000_000_000, + }); + let inner = Box::new(transfer_call); + let call = Box::new(RuntimeCall::Utility(UtilityCall::batch { + calls: vec![*inner], + })); + + // Utility call passed through filter + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(BOB), + MultiAddress::Id(ALICE), + None, + call.clone() + )); + expect_events(vec![ + UtilityEvent::BatchCompleted.into(), + ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), + ]); + }); +} + +#[test] +fn test_utility_call_pass_for_balances() { + new_test_ext().execute_with(|| { + // Balances proxy should be allowed to make balance transfer call + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(ALICE), + MultiAddress::Id(BOB), + ProxyType::Balances, + 0 + )); + + // Preparing Utility call + let transfer_call = RuntimeCall::Balances(BalancesCall::transfer { + dest: MultiAddress::Id(CAT), + value: 100_000_000_000, + }); + let inner = Box::new(transfer_call); + let call = Box::new(RuntimeCall::Utility(UtilityCall::batch { + calls: vec![*inner], + })); + + // Utility call passed through filter + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(BOB), + MultiAddress::Id(ALICE), + None, + call.clone() + )); + expect_events(vec![ + UtilityEvent::BatchCompleted.into(), + ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), + ]); + }); +} + +#[test] +fn test_utility_call_fail_non_transfer() { + new_test_ext().execute_with(|| { + // NonTransfer proxy shouldn't be allowed to make balance transfer call + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(ALICE), + MultiAddress::Id(BOB), + ProxyType::NonTransfer, + 0 + )); + + // Preparing Utility call + let transfer_call = RuntimeCall::Balances(BalancesCall::transfer { + dest: MultiAddress::Id(CAT), + value: 100_000_000_000, + }); + let inner = Box::new(transfer_call); + let call = Box::new(RuntimeCall::Utility(UtilityCall::batch { + calls: vec![*inner], + })); + + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(BOB), + MultiAddress::Id(ALICE), + None, + call.clone() + )); + + // Utility call filtered out + expect_events(vec![ + UtilityEvent::BatchInterrupted { + index: 0, + error: SystemError::CallFiltered.into(), + } + .into(), + ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), + ]); + }); +} + +#[test] +fn test_utility_call_fail_for_dappstaking() { + new_test_ext().execute_with(|| { + // Dappstaking proxy shouldn't be allowed to make balance transfer call + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(ALICE), + MultiAddress::Id(BOB), + ProxyType::DappStaking, + 0 + )); + + // Preparing Utility call + let transfer_call = RuntimeCall::Balances(BalancesCall::transfer { + dest: MultiAddress::Id(CAT), + value: 100_000_000_000, + }); + let inner = Box::new(transfer_call); + let call = Box::new(RuntimeCall::Utility(UtilityCall::batch { + calls: vec![*inner], + })); + + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(BOB), + MultiAddress::Id(ALICE), + None, + call.clone() + )); + // Utility call filtered out + expect_events(vec![ + UtilityEvent::BatchInterrupted { + index: 0, + error: SystemError::CallFiltered.into(), + } + .into(), + ProxyEvent::ProxyExecuted { result: Ok(()) }.into(), + ]); + }); +} + +#[test] +fn test_staker_reward_claim_proxy_works() { + new_test_ext().execute_with(|| { + // Make CAT delegate for StakerRewardClaim proxy + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(BOB), + MultiAddress::Id(CAT), + ProxyType::StakerRewardClaim, + 0 + )); + + let contract = ::SmartContract::evm( + H160::repeat_byte(0x01), + ); + let staker_reward_claim_call = + RuntimeCall::DappStaking(DappStakingCall::Call::claim_staker_rewards {}); + let call = Box::new(staker_reward_claim_call); + + // contract must be registered + assert_ok!(DappStaking::register( + RuntimeOrigin::root(), + ALICE.clone(), + contract.clone() + )); + + // some amount must be locked&staked + let amount = 600 * UNIT; + assert_ok!(DappStaking::lock(RuntimeOrigin::signed(BOB), amount)); + assert_ok!(DappStaking::stake( + RuntimeOrigin::signed(BOB), + contract.clone(), + amount, + )); + + // Generate some rewards + assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Era)); + run_to_block(System::block_number() + 1); + assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Era)); + run_to_block(System::block_number() + 1); + + // CAT making proxy call on behalf of staker (BOB) + assert_ok!(Proxy::proxy( + RuntimeOrigin::signed(CAT), + MultiAddress::Id(BOB), + None, + call.clone() + )); + + expect_events(vec![ProxyEvent::ProxyExecuted { result: Ok(()) }.into()]); + }) +} diff --git a/tests/integration/src/proxy.rs b/tests/integration/src/proxy_old.rs similarity index 98% rename from tests/integration/src/proxy.rs rename to tests/integration/src/proxy_old.rs index 41fc247372..d6d9479a82 100644 --- a/tests/integration/src/proxy.rs +++ b/tests/integration/src/proxy_old.rs @@ -180,7 +180,7 @@ fn test_staker_reward_claim_proxy_works() { let contract = SmartContract::Evm(H160::repeat_byte(0x01)); let staker_reward_claim_call = - RuntimeCall::DappsStaking(DappStakingCall::Call::claim_staker { + RuntimeCall::DappsStaking(DappsStakingCall::Call::claim_staker { contract_id: contract.clone(), }); let call = Box::new(staker_reward_claim_call); diff --git a/tests/integration/src/setup.rs b/tests/integration/src/setup.rs index bc7dddeaca..ba266bfe3e 100644 --- a/tests/integration/src/setup.rs +++ b/tests/integration/src/setup.rs @@ -159,7 +159,8 @@ pub const INITIAL_AMOUNT: u128 = 100_000 * UNIT; pub type SystemError = frame_system::Error; pub use pallet_balances::Call as BalancesCall; -pub use pallet_dapps_staking as DappStakingCall; +pub use pallet_dapp_staking_v3 as DappStakingCall; +pub use pallet_dapps_staking as DappsStakingCall; pub use pallet_proxy::Event as ProxyEvent; pub use pallet_utility::{Call as UtilityCall, Event as UtilityEvent}; @@ -212,32 +213,37 @@ pub const BLOCK_TIME: u64 = 12_000; pub fn run_to_block(n: u32) { while System::block_number() < n { let block_number = System::block_number(); - Timestamp::set_timestamp(block_number as u64 * BLOCK_TIME); TransactionPayment::on_finalize(block_number); + #[cfg(any(feature = "shibuya"))] + DappStaking::on_finalize(block_number); + #[cfg(any(feature = "astar", feature = "shiden"))] DappsStaking::on_finalize(block_number); Authorship::on_finalize(block_number); Session::on_finalize(block_number); AuraExt::on_finalize(block_number); PolkadotXcm::on_finalize(block_number); Ethereum::on_finalize(block_number); - #[cfg(any(features = "astar"))] - BaseFee::on_finalize(block_number); - #[cfg(any(feature = "shibuya", feature = "shiden"))] DynamicEvmBaseFee::on_finalize(block_number); + #[cfg(any(feature = "shibuya"))] + Inflation::on_finalize(block_number); System::set_block_number(block_number + 1); + let block_number = System::block_number(); + #[cfg(any(feature = "shibuya"))] + Inflation::on_initialize(block_number); + Timestamp::set_timestamp(block_number as u64 * BLOCK_TIME); TransactionPayment::on_initialize(block_number); + #[cfg(any(feature = "shibuya"))] + DappStaking::on_initialize(block_number); + #[cfg(any(feature = "astar", feature = "shiden"))] DappsStaking::on_initialize(block_number); Authorship::on_initialize(block_number); Aura::on_initialize(block_number); AuraExt::on_initialize(block_number); Ethereum::on_initialize(block_number); - #[cfg(any(features = "astar"))] - BaseFee::on_initialize(block_number); - #[cfg(any(feature = "shibuya", feature = "shiden"))] DynamicEvmBaseFee::on_initialize(block_number); - #[cfg(any(feature = "shibuya", feature = "shiden", features = "astar"))] + #[cfg(any(feature = "shibuya", feature = "shiden"))] RandomnessCollectiveFlip::on_initialize(block_number); XcmpQueue::on_idle(block_number, Weight::MAX);