diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index c703558aa9..4baa8e6b15 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -53,7 +53,7 @@ use sp_runtime::{ use astar_primitives::Balance; use crate::types::*; -pub use pallet::*; +// pub use pallet::*; #[cfg(test)] mod test; @@ -62,1109 +62,1109 @@ 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::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. - /// 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; - - /// Privileged origin for managing dApp staking pallet. - type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; - - /// Length of a standard era in block numbers. - #[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 - /// for the sake of simplicity & consistency. - #[pallet::constant] - 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 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; - - /// 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; - - /// 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; - - /// 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, - 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: 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, - }, - /// 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, - }, - /// Account has claimed some stake rewards. - Reward { - account: T::AccountId, - era: EraNumber, - amount: Balance, - }, - } - - #[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 capacity limit. - TooManyLockedBalanceChunks, - /// Cannot add additional unlocking chunks due to capacity 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, - /// 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, - /// Rewards are no longer claimable since they are too old. - StakerRewardsExpired, - /// There are no claimable rewards for the account. - NoClaimableRewards, - /// An unexpected error occured while trying to claim rewards. - InternalClaimStakerError, - } - - /// 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>; - - /// 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>; - - /// 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>; - - #[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 current_era = protocol_state.era; - let next_era = current_era.saturating_add(1); - let (maybe_period_event, era_reward) = match protocol_state.period_type() { - PeriodType::Voting => { - // For the sake of consistency, we put zero reward into storage - let era_reward = - EraReward::new(Balance::zero(), era_info.total_staked_amount()); - - 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(), - }), - era_reward, - ) - } - PeriodType::BuildAndEarn => { - // TODO: trigger dAPp tier reward calculation here. This will be implemented later. - - let staker_reward_pool = Balance::from(1_000_000_000_000u128); // TODO: calculate this properly, inject it from outside (Tokenomics 2.0 pallet?) - let era_reward = - EraReward::new(staker_reward_pool, era_info.total_staked_amount()); - - // 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 = Balance::from(3_000_000_u32); // TODO: get this from Tokenomics 2.0 pallet - PeriodEnd::::insert( - &protocol_state.period_number(), - PeriodEndInfo { - bonus_reward_pool, - total_vp_stake: era_info.staked_amount(PeriodType::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 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(), - }), - 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()); - // TODO: error must not happen here. Log an error if it does. - // The consequence will be that some rewards will be temporarily lost/unavailable, but nothing protocol breaking. - // Will require a fix from the runtime team 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(3, 3) - } - } - - #[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. - - // 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, - }); - - 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: Balance) -> DispatchResult { - Self::ensure_pallet_enabled()?; - let account = ensure_signed(origin)?; - - let mut ledger = Ledger::::get(&account); - - // Calculate & check amount available for locking - let available_balance = - 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); - - ledger.add_lock_amount(amount_to_lock); - - ensure!( - ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), - Error::::LockedAmountBelowThreshold - ); - - Self::update_ledger(&account, ledger); - CurrentEraInfo::::mutate(|era_info| { - era_info.add_locked(amount_to_lock); - }); - - Self::deposit_event(Event::::Locked { - account, - amount: amount_to_lock, - }); - - 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_info.number); - 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_info.number).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); - - 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?) - - // 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(()) - } - - #[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 mut ledger = Ledger::::get(&account); - - ensure!(!ledger.unlocking.is_empty(), Error::::NoUnlockingChunks); - - let amount = ledger.consume_unlocking_chunks(); - - ledger.add_lock_amount(amount); - 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(()) - } - - /// 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(()) - } - - /// TODO - #[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 protocol_state = ActiveProtocolState::::get(); - let mut ledger = Ledger::::get(&account); - - // TODO: how do we handle expired rewards? Add an additional call to clean them up? - // Putting this logic inside existing calls will add even more complexity. - - // Check if the rewards have expired - let staked_period = ledger.staked_period.ok_or(Error::::NoClaimableRewards)?; - ensure!( - staked_period - >= protocol_state - .period_number() - .saturating_sub(T::RewardRetentionInPeriods::get()), - Error::::StakerRewardsExpired - ); - - // Calculate the reward claim span - let (first_chunk, last_chunk) = ledger - .first_and_last_stake_chunks() - .ok_or(Error::::InternalClaimStakerError)?; - let era_rewards = - EraRewards::::get(Self::era_reward_span_index(first_chunk.get_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. - // Limiting factors are: - // 1. era reward span entries - // 2. last era in period limit - // 3. last era in which staker had non-zero stake - let last_claim_era = if last_chunk.get_amount().is_zero() { - era_rewards - .last_era() - .min(last_period_era) - .min(last_chunk.get_era()) - } else { - era_rewards.last_era().min(last_period_era) - }; - - // Get chunks for reward claiming - let chunks_for_claim = - ledger - .claim_up_to_era(last_claim_era, period_end) - .map_err(|err| match err { - AccountLedgerError::SplitEraInvalid => Error::::NoClaimableRewards, - _ => Error::::InternalClaimStakerError, - })?; - - // Calculate rewards - let mut rewards: Vec<_> = Vec::new(); - let mut reward_sum = Balance::zero(); - for era in first_chunk.get_era()..=last_claim_era { - // TODO: this should be zipped, and values should be fetched only once - let era_reward = era_rewards - .get(era) - .ok_or(Error::::InternalClaimStakerError)?; - - let chunk = chunks_for_claim - .get(era) - .ok_or(Error::::InternalClaimStakerError)?; - - // Optimization, and zero-division protection - if chunk.get_amount().is_zero() || era_reward.staked().is_zero() { - continue; - } - let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) - * era_reward.staker_reward_pool(); - - rewards.push((era, staker_reward)); - reward_sum.saturating_accrue(staker_reward); - } - - 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(()) - } - } - - 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.active_locked_amount(), - WithdrawReasons::all(), - ); - 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) - } - - /// Calculates the `EraRewardSpan` index for the specified era. - pub fn era_reward_span_index(era: EraNumber) -> EraNumber { - era.saturating_sub(era % T::EraRewardSpanLength::get()) - } - } -} +// #[frame_support::pallet] +// pub mod pallet { +// use super::*; + +// /// The current storage version. +// const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); + +// #[pallet::pallet] +// #[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. +// /// 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; + +// /// Privileged origin for managing dApp staking pallet. +// type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; + +// /// Length of a standard era in block numbers. +// #[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 +// /// for the sake of simplicity & consistency. +// #[pallet::constant] +// 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 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; + +// /// 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; + +// /// 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; + +// /// 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, +// 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: 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, +// }, +// /// 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, +// }, +// /// Account has claimed some stake rewards. +// Reward { +// account: T::AccountId, +// era: EraNumber, +// amount: Balance, +// }, +// } + +// #[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 capacity limit. +// TooManyLockedBalanceChunks, +// /// Cannot add additional unlocking chunks due to capacity 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, +// /// 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, +// /// Rewards are no longer claimable since they are too old. +// StakerRewardsExpired, +// /// There are no claimable rewards for the account. +// NoClaimableRewards, +// /// An unexpected error occured while trying to claim rewards. +// InternalClaimStakerError, +// } + +// /// 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>; + +// /// 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>; + +// /// 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>; + +// #[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 current_era = protocol_state.era; +// let next_era = current_era.saturating_add(1); +// let (maybe_period_event, era_reward) = match protocol_state.period_type() { +// PeriodType::Voting => { +// // For the sake of consistency, we put zero reward into storage +// let era_reward = +// EraReward::new(Balance::zero(), era_info.total_staked_amount()); + +// 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(), +// }), +// era_reward, +// ) +// } +// PeriodType::BuildAndEarn => { +// // TODO: trigger dAPp tier reward calculation here. This will be implemented later. + +// let staker_reward_pool = Balance::from(1_000_000_000_000u128); // TODO: calculate this properly, inject it from outside (Tokenomics 2.0 pallet?) +// let era_reward = +// EraReward::new(staker_reward_pool, era_info.total_staked_amount()); + +// // 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 = Balance::from(3_000_000_u32); // TODO: get this from Tokenomics 2.0 pallet +// PeriodEnd::::insert( +// &protocol_state.period_number(), +// PeriodEndInfo { +// bonus_reward_pool, +// total_vp_stake: era_info.staked_amount(PeriodType::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 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(), +// }), +// 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()); +// // TODO: error must not happen here. Log an error if it does. +// // The consequence will be that some rewards will be temporarily lost/unavailable, but nothing protocol breaking. +// // Will require a fix from the runtime team 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(3, 3) +// } +// } + +// #[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. + +// // 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, +// }); + +// 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: Balance) -> DispatchResult { +// Self::ensure_pallet_enabled()?; +// let account = ensure_signed(origin)?; + +// let mut ledger = Ledger::::get(&account); + +// // Calculate & check amount available for locking +// let available_balance = +// 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); + +// ledger.add_lock_amount(amount_to_lock); + +// ensure!( +// ledger.active_locked_amount() >= T::MinimumLockedAmount::get(), +// Error::::LockedAmountBelowThreshold +// ); + +// Self::update_ledger(&account, ledger); +// CurrentEraInfo::::mutate(|era_info| { +// era_info.add_locked(amount_to_lock); +// }); + +// Self::deposit_event(Event::::Locked { +// account, +// amount: amount_to_lock, +// }); + +// 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_info.number); +// 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_info.number).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); + +// 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?) + +// // 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(()) +// } + +// #[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 mut ledger = Ledger::::get(&account); + +// ensure!(!ledger.unlocking.is_empty(), Error::::NoUnlockingChunks); + +// let amount = ledger.consume_unlocking_chunks(); + +// ledger.add_lock_amount(amount); +// 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(()) +// } + +// /// 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(()) +// } + +// /// TODO +// #[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 protocol_state = ActiveProtocolState::::get(); +// let mut ledger = Ledger::::get(&account); + +// // TODO: how do we handle expired rewards? Add an additional call to clean them up? +// // Putting this logic inside existing calls will add even more complexity. + +// // Check if the rewards have expired +// let staked_period = ledger.staked_period.ok_or(Error::::NoClaimableRewards)?; +// ensure!( +// staked_period +// >= protocol_state +// .period_number() +// .saturating_sub(T::RewardRetentionInPeriods::get()), +// Error::::StakerRewardsExpired +// ); + +// // Calculate the reward claim span +// let (first_chunk, last_chunk) = ledger +// .first_and_last_stake_chunks() +// .ok_or(Error::::InternalClaimStakerError)?; +// let era_rewards = +// EraRewards::::get(Self::era_reward_span_index(first_chunk.get_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. +// // Limiting factors are: +// // 1. era reward span entries +// // 2. last era in period limit +// // 3. last era in which staker had non-zero stake +// let last_claim_era = if last_chunk.get_amount().is_zero() { +// era_rewards +// .last_era() +// .min(last_period_era) +// .min(last_chunk.get_era()) +// } else { +// era_rewards.last_era().min(last_period_era) +// }; + +// // Get chunks for reward claiming +// let chunks_for_claim = +// ledger +// .claim_up_to_era(last_claim_era, period_end) +// .map_err(|err| match err { +// AccountLedgerError::SplitEraInvalid => Error::::NoClaimableRewards, +// _ => Error::::InternalClaimStakerError, +// })?; + +// // Calculate rewards +// let mut rewards: Vec<_> = Vec::new(); +// let mut reward_sum = Balance::zero(); +// for era in first_chunk.get_era()..=last_claim_era { +// // TODO: this should be zipped, and values should be fetched only once +// let era_reward = era_rewards +// .get(era) +// .ok_or(Error::::InternalClaimStakerError)?; + +// let chunk = chunks_for_claim +// .get(era) +// .ok_or(Error::::InternalClaimStakerError)?; + +// // Optimization, and zero-division protection +// if chunk.get_amount().is_zero() || era_reward.staked().is_zero() { +// continue; +// } +// let staker_reward = Perbill::from_rational(chunk.get_amount(), era_reward.staked()) +// * era_reward.staker_reward_pool(); + +// rewards.push((era, staker_reward)); +// reward_sum.saturating_accrue(staker_reward); +// } + +// 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(()) +// } +// } + +// 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.active_locked_amount(), +// WithdrawReasons::all(), +// ); +// 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) +// } + +// /// Calculates the `EraRewardSpan` index for the specified era. +// pub fn era_reward_span_index(era: EraNumber) -> EraNumber { +// era.saturating_sub(era % T::EraRewardSpanLength::get()) +// } +// } +// } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 1cd94100b3..c3b15bdc1c 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -39,215 +39,215 @@ 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_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 = (); - type MaxHolds = ConstU32<0>; - type MaxFreezes = ConstU32<0>; - 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 EraRewardSpanLength = ConstU32<8>; - type RewardRetentionInPeriods = ConstU32<2>; - type MaxNumberOfContracts = ConstU16<10>; - 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), - 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); - - // 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: 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, - }); - - // 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) { - 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) { - 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); - } -} - -// 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() -} +// 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_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 = (); +// type MaxHolds = ConstU32<0>; +// type MaxFreezes = ConstU32<0>; +// 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 EraRewardSpanLength = ConstU32<8>; +// type RewardRetentionInPeriods = ConstU32<2>; +// type MaxNumberOfContracts = ConstU16<10>; +// 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), +// 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); + +// // 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: 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, +// }); + +// // 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) { +// 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) { +// 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); +// } +// } + +// // 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..d4f5974862 100644 --- a/pallets/dapp-staking-v3/src/test/mod.rs +++ b/pallets/dapp-staking-v3/src/test/mod.rs @@ -17,6 +17,6 @@ // along with Astar. If not, see . mod mock; -mod testing_utils; -mod tests; +// mod testing_utils; +// mod tests; mod tests_types; diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index dd301b983d..a9e6e336fe 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -34,414 +34,6 @@ 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 new(amount: Balance, era: u32) -> Self { - Self { amount, era } - } - 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 sparse_bounded_amount_era_vec_split_left_works() { - get_u32_type!(MaxLen, 4); - - fn new_era_vec(vec: Vec) -> SparseBoundedAmountEraVec { - let vec: Vec = vec - .into_iter() - .map(|idx| DummyEraAmount::new(idx as Balance, idx)) - .collect(); - SparseBoundedAmountEraVec(BoundedVec::try_from(vec).unwrap()) - } - - // 1st scenario: [1,2,6,7] -- split(4) --> [1,2],[5,6,7] - let mut vec = new_era_vec(vec![1, 2, 6, 7]); - let result = vec.left_split(4).expect("Split should succeed."); - assert_eq!(result.0.len(), 2); - assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); - assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); - - assert_eq!(vec.0.len(), 3); - assert_eq!( - vec.0[0], - DummyEraAmount::new(2, 5), - "Amount must come from last entry in the split." - ); - assert_eq!(vec.0[1], DummyEraAmount::new(6, 6)); - assert_eq!(vec.0[2], DummyEraAmount::new(7, 7)); - - // 2nd scenario: [1,2] -- split(4) --> [1,2],[5] - let mut vec = new_era_vec(vec![1, 2]); - let result = vec.left_split(4).expect("Split should succeed."); - assert_eq!(result.0.len(), 2); - assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); - assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); - - assert_eq!(vec.0.len(), 1); - assert_eq!( - vec.0[0], - DummyEraAmount::new(2, 5), - "Amount must come from last entry in the split." - ); - - // 3rd scenario: [1,2,4,5] -- split(4) --> [1,2,4],[5] - let mut vec = new_era_vec(vec![1, 2, 4, 5]); - let result = vec.left_split(4).expect("Split should succeed."); - assert_eq!(result.0.len(), 3); - assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); - assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); - assert_eq!(result.0[2], DummyEraAmount::new(4, 4)); - - assert_eq!(vec.0.len(), 1); - assert_eq!(vec.0[0], DummyEraAmount::new(5, 5)); - - // 4th scenario: [1,2,4,6] -- split(4) --> [1,2,4],[5,6] - let mut vec = new_era_vec(vec![1, 2, 4, 6]); - let result = vec.left_split(4).expect("Split should succeed."); - assert_eq!(result.0.len(), 3); - assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); - assert_eq!(result.0[1], DummyEraAmount::new(2, 2)); - assert_eq!(result.0[2], DummyEraAmount::new(4, 4)); - - assert_eq!(vec.0.len(), 2); - assert_eq!( - vec.0[0], - DummyEraAmount::new(4, 5), - "Amount must come from last entry in the split." - ); - assert_eq!(vec.0[1], DummyEraAmount::new(6, 6)); - - // 5th scenario: [1,2(0)] -- split(4) --> [1,2],[] - let vec: Vec = vec![(1, 1), (0, 2)] - .into_iter() - .map(|(amount, era)| DummyEraAmount::new(amount as Balance, era)) - .collect(); - let mut vec = SparseBoundedAmountEraVec::<_, MaxLen>(BoundedVec::try_from(vec).unwrap()); - let result = vec.left_split(4).expect("Split should succeed."); - assert_eq!(result.0.len(), 2); - assert_eq!(result.0[0], DummyEraAmount::new(1, 1)); - assert_eq!(result.0[1], DummyEraAmount::new(0, 2)); - - assert!(vec.0.is_empty()); -} - -#[test] -fn sparse_bounded_amount_era_vec_split_left_fails_with_invalid_era() { - get_u32_type!(MaxLen, 4); - let mut vec = SparseBoundedAmountEraVec::::new(); - assert!(vec.add_amount(5, 5).is_ok()); - - assert_eq!(vec.left_split(4), Err(AccountLedgerError::SplitEraInvalid)); -} - -#[test] -fn sparse_bounded_amount_era_vec_get_works() { - get_u32_type!(MaxLen, 4); - let vec: Vec = vec![2, 3, 5] - .into_iter() - .map(|idx| DummyEraAmount::new(idx as Balance, idx)) - .collect(); - let vec = - SparseBoundedAmountEraVec::(BoundedVec::try_from(vec).unwrap()); - - assert_eq!(vec.get(1), None, "Era is not covered by the vector."); - assert_eq!(vec.get(2), Some(DummyEraAmount::new(2, 2))); - assert_eq!(vec.get(3), Some(DummyEraAmount::new(3, 3))); - assert_eq!( - vec.get(4), - Some(DummyEraAmount::new(3, 4)), - "Era is covered by the 3rd era." - ); - assert_eq!(vec.get(5), Some(DummyEraAmount::new(5, 5))); - assert_eq!( - vec.get(6), - Some(DummyEraAmount::new(5, 6)), - "Era is covered by the 5th era." - ); -} - #[test] fn period_type_sanity_check() { assert_eq!(PeriodType::Voting.next(), PeriodType::BuildAndEarn); @@ -536,8 +128,7 @@ fn protocol_state_basic_checks() { #[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()); @@ -546,8 +137,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()); @@ -566,8 +156,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 @@ -608,8 +197,7 @@ 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(); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop @@ -670,35 +258,36 @@ fn account_ledger_add_unlocking_chunk_works() { } #[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_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()); @@ -716,14 +305,7 @@ 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, first_period); assert_eq!( acc_ledger.stakeable_amount(first_period), @@ -739,1221 +321,1226 @@ fn account_ledger_stakeable_amount_works() { ); } -#[test] -fn account_ledger_staked_amount_works() { - get_u32_type!(UnlockingDummy, 5); - get_u32_type!(StakingDummy, 8); - let mut acc_ledger = AccountLedger::::default(); - - // Sanity check for empty ledger - assert!(acc_ledger.staked_amount(1).is_zero()); - - // 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); - - assert_eq!(acc_ledger.staked_amount(first_period), staked_amount); - - // Second scenario - active period doesn't match the ledger - assert!(acc_ledger.staked_amount(first_period + 1).is_zero()); -} - #[test] 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 - 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()); + assert!(acc_ledger + .add_stake_amount(0, 0, PeriodInfo::new(0, PeriodType::Voting, 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; + // First scenario - stake some amount, and ensure values are as expected. + let first_era = 1; let first_period = 1; + let period_info_1 = PeriodInfo::new(first_period, PeriodType::Voting, 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, + first_period ); + 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(first_period), stake_amount); - - // Second scenario - stake some more to the same era, only amount should change + assert_eq!( + acc_ledger.staked_amount_for_type(PeriodType::Voting, first_period), + stake_amount + ); 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); + .staked_amount_for_type(PeriodType::BuildAndEarn, first_period) + .is_zero()); - // 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; + // Second scenario - stake some more to the same era + let snapshot = acc_ledger.staked; assert!(acc_ledger - .add_stake_amount(remaining_not_staked, next_era, first_period) + .add_stake_amount(1, 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(first_period), stake_amount + 1); + assert_eq!(acc_ledger.staked, snapshot); } #[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(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let first_era = 5; let first_period = 2; + let period_info_1 = PeriodInfo::new(first_period, PeriodType::Voting, 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. + println!("{:?}", acc_ledger); 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" ); - // Try to add to the previous era, it should fail + // 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) + 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" ); -} - -#[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 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()); + // Try to add to the next period, it should fail. assert_eq!( - acc_ledger.staked_amount(period_1), - amount_1 - unstake_amount_1 * 3 + acc_ledger.add_stake_amount( + 1, + first_era, + PeriodInfo::new(first_period + 1, PeriodType::Voting, 100) + ), + Err(AccountLedgerError::InvalidPeriod) ); assert_eq!( - acc_ledger.staked.0.len(), - 3, - "New entry must be created to cover the new era stake." + acc_ledger, acc_ledger_snapshot, + "Previous failed action must be a noop" ); -} - -#[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()); + // Try to add to the previous period, it should fail assert_eq!( - acc_ledger.unstake_amount(amount_1, era_1 + 1, period_1 + 1), + acc_ledger.add_stake_amount( + 1, + first_era, + PeriodInfo::new(first_period - 1, PeriodType::Voting, 100) + ), 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) + acc_ledger, acc_ledger_snapshot, + "Previous failed action must be a noop" ); } -#[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 - 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); -} - -#[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()); -} - -#[test] -fn era_reward_span_push_and_get_works() { - get_u32_type!(SpanLength, 8); - let mut era_reward_span = EraRewardSpan::::new(); - - // 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::new(23, 41); - 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::new(37, 53); - 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 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::new(23, 41); - 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!( - era_reward_span.push(era_3, era_reward), - Err(EraRewardSpanError::NoCapacity) - ); -} +// #[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 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 +// 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); +// } + +// #[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()); +// } + +// #[test] +// fn era_reward_span_push_and_get_works() { +// get_u32_type!(SpanLength, 8); +// let mut era_reward_span = EraRewardSpan::::new(); + +// // 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::new(23, 41); +// 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::new(37, 53); +// 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 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::new(23, 41); +// 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!( +// era_reward_span.push(era_3, era_reward), +// Err(EraRewardSpanError::NoCapacity) +// ); +// } diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 267185bd9f..1d15e0e548 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -26,16 +26,16 @@ use sp_runtime::{ use astar_primitives::Balance; -use crate::pallet::Config; +// 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< - BlockNumberFor, - ::MaxUnlockingChunks, - ::MaxStakingChunks, ->; +// pub type AccountLedgerFor = AccountLedger< +// BlockNumberFor, +// ::MaxUnlockingChunks, +// ::MaxStakingChunks, +// >; /// Era number type pub type EraNumber = u32; @@ -44,27 +44,11 @@ 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 { - fn new(amount: Balance, era: EraNumber) -> Self; - /// 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 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. @@ -73,237 +57,8 @@ pub enum AccountLedgerError { UnavailableStakeFunds, /// Unstake amount is to large in respect to what's staked. UnstakeAmountLargerThanStake, - /// Split era is invalid; it's not contained withing the vector's scope. - SplitEraInvalid, -} - -/// 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(()) - } - - /// Splits the vector into two parts, using the provided `era` as the splitting point. - /// - /// All entries which satisfy the condition `entry.era <= era` are removed from the vector. - /// After split is done, the _removed_ part is padded with an entry, IFF the last element doesn't cover the specified era. - /// The same is true for the remaining part of the vector. - /// - /// The `era` argument **must** be contained within the vector's scope. - /// - /// E.g.: - /// a) [1,2,6,7] -- split(4) --> [1,2],[5,6,7] - /// b) [1,2] -- split(4) --> [1,2],[5] - /// c) [1,2,4,5] -- split(4) --> [1,2,4],[5] - /// d) [1,2,4,6] -- split(4) --> [1,2,4],[5,6] - /// e) [1,2(0)] -- split(4) --> [1,2],[] // 2nd entry has 'zero' amount, so no need to keep it - pub fn left_split(&mut self, era: EraNumber) -> Result { - // Split the inner vector into two parts - let (left, mut right): (Vec

, Vec

) = self - .0 - .clone() - .into_iter() - .partition(|chunk| chunk.get_era() <= era); - - if left.is_empty() { - return Err(AccountLedgerError::SplitEraInvalid); - } - - if let Some(&last_l_chunk) = left.last() { - // In case 'right' part is missing an entry covering the specified era, add it. - let maybe_chunk = match right.first() { - Some(first_r_chunk) if first_r_chunk.get_era() > era.saturating_add(1) => { - let mut new_chunk = last_l_chunk.clone(); - new_chunk.set_era(era.saturating_add(1)); - Some(new_chunk) - } - None => { - let mut new_chunk = last_l_chunk.clone(); - new_chunk.set_era(era.saturating_add(1)); - Some(new_chunk) - } - _ => None, - }; - - // Only insert the chunk if it's non-zero - match maybe_chunk { - Some(chunk) if chunk.get_amount() > Balance::zero() => { - right.insert(0, chunk); - } - _ => (), - } - } - - // TODO: I could just use `expect` here since it's impossible for vectors to increase in size. - self.0 = BoundedVec::try_from(right).map_err(|_| AccountLedgerError::NoCapacity)?; - - Ok(Self( - BoundedVec::::try_from(left).map_err(|_| AccountLedgerError::NoCapacity)?, - )) - } - - /// Returns the most appropriate chunk for the specified era, if it exists. - pub fn get(&self, era: EraNumber) -> Option

{ - match self.0.binary_search_by(|chunk| chunk.get_era().cmp(&era)) { - // Entry exists, all good. - Ok(idx) => self.0.get(idx).map(|x| *x), - // Entry doesn't exist, but another one covers it so we return it. - Err(idx) if idx > 0 => self.0.get(idx.saturating_sub(1)).map(|x| { - let mut new_chunk = *x; - new_chunk.set_era(era); - new_chunk - }), - // Era is out of scope. - _ => None, - } - } + /// Nothing to claim. + NothingToClaim, } // TODO: rename to SubperiodType? It would be less ambigious. @@ -433,6 +188,7 @@ where self.next_era_start <= now } + // TODO: rename this into something better? /// 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 { @@ -494,89 +250,56 @@ 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 new(amount: Balance, era: EraNumber) -> Self { - Self { amount, era } - } - 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))] +#[scale_info(skip_type_params(UnlockingLen))] pub struct AccountLedger< BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, UnlockingLen: Get, - StakedLen: Get, > { /// 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 - pub staked: SparseBoundedAmountEraVec, - /// Last period in which account had staked. - pub staked_period: Option, + /// How much user has/had staked in a particular era. + pub staked: StakeAmount, + /// Helper staked amount to keep track of future era stakes. + /// Both `stake` and `staked_future` must ALWAYS refer to the same period. + pub staked_future: Option, + /// TODO + pub staker_rewards_claimed: bool, + /// TODO + pub bonus_reward_claimed: bool, } -impl Default - for AccountLedger +impl Default for AccountLedger where BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, UnlockingLen: Get, - StakedLen: Get, { fn default() -> Self { Self { locked: Balance::zero(), unlocking: BoundedVec::, UnlockingLen>::default(), - staked: SparseBoundedAmountEraVec(BoundedVec::::default()), - staked_period: None, + staked: StakeAmount::default(), + staked_future: None, + staker_rewards_claimed: false, + bonus_reward_claimed: false, } } } -impl AccountLedger +impl AccountLedger where BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, 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. @@ -652,7 +375,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. @@ -681,42 +404,46 @@ 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(), + }, } } + pub fn staked_amount_for_type( + &self, + period_type: PeriodType, + active_period: PeriodNumber, + ) -> Balance { + // 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.for_type(period_type) + } + _ => match self.staked { + stake_amount if stake_amount.period == active_period => { + stake_amount.for_type(period_type) + } + _ => Balance::zero(), + }, + } + } + + // TODO: update this /// Adds the specified amount to total staked amount, if possible. /// /// Staking is only allowed if one of the two following conditions is met: @@ -728,25 +455,39 @@ where &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 => { + // TODO: maybe the check can be nicer? + if self.staked.era > EraNumber::zero() { + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { return Err(AccountLedgerError::InvalidPeriod); } - _ => (), } - 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.period_type); + } + None => { + let mut stake_amount = self.staked; + stake_amount.era = era + 1; + stake_amount.period = current_period_info.number; + stake_amount.add(amount, current_period_info.period_type); + self.staked_future = Some(stake_amount); + } + } Ok(()) } @@ -759,51 +500,31 @@ where &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); - } - _ => (), + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); } // 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) - } - - // TODO: remove this - /// 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) - } - - // TODO: remove this - /// First entry for which a stake entry exists. - /// If no stake entries exist, returns `None`. - pub fn oldest_stake_era(&self) -> Option { - self.staked.0.first().map(|chunk| chunk.era) - } - - // TODO - pub fn first_and_last_stake_chunks(&self) -> Option<(StakeChunk, StakeChunk)> { - let first = self.staked.0.first().map(|chunk| *chunk); - let last = self.staked.0.last().map(|chunk| *chunk); - - match (first, last) { - (Some(first), Some(last)) => Some((first, last)), - _ => None, + self.staked + .subtract(amount, current_period_info.period_type); + if let Some(stake_amount) = self.staked_future.as_mut() { + stake_amount.subtract(amount, current_period_info.period_type); } + + Ok(()) } /// Claim up stake chunks up to the specified `era`. @@ -814,54 +535,68 @@ where &mut self, era: EraNumber, period_end: Option, - ) -> Result, AccountLedgerError> { - let claim_chunks = self.staked.left_split(era)?; - - // Check if all possible chunks have been claimed - match self.staked.0.first() { - Some(chunk) => { - match period_end { - // If first chunk is after the period end, clearly everything has been claimed - Some(period_end) if chunk.get_era() > period_end => { - self.staked_period = None; - self.staked = SparseBoundedAmountEraVec::new(); - } - _ => (), - } - } - // No more chunks remain, meaning everything has been claimed - None => { - self.staked_period = None; - } + // TODO: add a better type later + ) -> Result<(EraNumber, EraNumber, Balance), AccountLedgerError> { + if era <= self.staked.era || self.staked.total().is_zero() { + return Err(AccountLedgerError::NothingToClaim); } - // TODO: this is a bit clunky - introduce variables instead that keep track whether rewards were claimed or not. + let result = (self.staked.era, era, self.staked.total()); + + // Update latest 'staked' era + self.staked.era = era; - Ok(claim_chunks) + // Make sure to clean + match period_end { + Some(ending_era) if era >= ending_era => { + self.staker_rewards_claimed = true; + self.staked = Default::default(); + self.staked_future = None; + } + _ => (), + } + + Ok(result) } } -// 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. +// TODO #[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, } } + pub fn is_empty(&self) -> bool { + self.voting.is_zero() && self.build_and_earn.is_zero() + } + /// Total amount staked in both period types. pub fn total(&self) -> Balance { self.voting.saturating_add(self.build_and_earn) @@ -875,23 +610,21 @@ impl StakeAmount { } } - // TODO: rename to add? /// Stake the specified `amount` for the specified `period_type`. - pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { + pub fn add(&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) { + pub fn subtract(&mut self, amount: Balance, period_type: PeriodType) { match period_type { PeriodType::Voting => self.voting.saturating_reduce(amount), PeriodType::BuildAndEarn => { @@ -949,13 +682,13 @@ impl EraInfo { /// 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); + self.next_stake_amount.add(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); + self.current_stake_amount.subtract(amount, period_type); + self.next_stake_amount.subtract(amount, period_type); } /// Total staked amount in this era. @@ -1002,15 +735,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, } @@ -1024,9 +750,8 @@ impl SingularStakingInfo { /// `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, + // TODO: one drawback here is using the struct which has `era` as the field - it's not needed here. Should I add a special struct just for this? + 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, } @@ -1034,10 +759,7 @@ impl SingularStakingInfo { /// 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), - } + self.staked.add(amount, period_type); } /// Unstakes some of the specified amount from the contract. @@ -1047,43 +769,32 @@ impl SingularStakingInfo { /// /// 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, - ) - } + let snapshot = self.staked; + + self.staked.subtract(amount, period_type); + + self.loyal_staker = self.loyal_staker + && (period_type == PeriodType::Voting + || period_type == PeriodType::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. 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, - } + self.staked.for_type(period_type) } /// If `true` staker has staked during voting period and has never reduced their sta @@ -1093,94 +804,12 @@ 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() - } -} - -/// 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() + self.staked.is_empty() } } @@ -1188,19 +817,17 @@ 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`. +pub struct StakeAmountSeries(BoundedVec>); +impl StakeAmountSeries { + /// Helper function to create a new instance of `StakeAmountSeries`. #[cfg(test)] - pub fn new(inner: Vec) -> Self { + 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. + /// Returns inner `Vec` of `StakeAmount` instances. Useful for testing. #[cfg(test)] - pub fn inner(&self) -> Vec { + pub fn inner(&self) -> Vec { self.0.clone().into_inner() } @@ -1214,9 +841,11 @@ impl ContractStakingInfoSeries { 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)); + /// Returns the `StakeAmount` 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(|stake_amount| stake_amount.era.cmp(&era)); // There are couple of distinct scenarios: // 1. Era exists, so we just return it. @@ -1231,7 +860,7 @@ impl ContractStakingInfoSeries { None } else { match self.0.get(ideal_idx - 1) { - Some(info) if info.period() == period => { + Some(info) if info.period == period => { let mut info = *info; info.era = era; Some(info) @@ -1245,20 +874,18 @@ impl ContractStakingInfoSeries { /// 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()) + 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()) + 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() - } + Some(stake_amount) if stake_amount.period == active_period => stake_amount.total(), _ => Balance::zero(), } } @@ -1266,8 +893,8 @@ impl ContractStakingInfoSeries { /// 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) + Some(stake_amount) if stake_amount.period == period => { + stake_amount.for_type(period_type) } _ => Balance::zero(), } @@ -1281,41 +908,39 @@ impl ContractStakingInfoSeries { 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 { + if let Some(stake_amount) = self.0.last() { + if stake_amount.era > era || stake_amount.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 { + // Get the most relevant `StakeAmount` instance + let mut stake_amount = if let Some(stake_amount) = self.0.last() { + if stake_amount.era == era { // Era matches, so we just update the last element. - let last_element = *last_element; + let stake_amount = *stake_amount; let _ = self.0.pop(); - last_element - } else if last_element.period() == period_info.number { + stake_amount + } else if stake_amount.period == period_info.number { // Periods match so we should 'copy' the last element to get correct staking amount - let mut temp = *last_element; + let mut temp = *stake_amount; temp.era = era; temp } else { // It's a new period, so we need a completely new instance - ContractStakingInfo::new(era, period_info.number) + StakeAmount::new(Balance::zero(), Balance::zero(), era, period_info.number) } } else { // It's a new period, so we need a completely new instance - ContractStakingInfo::new(era, period_info.number) + StakeAmount::new(Balance::zero(), Balance::zero(), era, period_info.number) }; // Update the stake amount - staking_info.stake(amount, period_info.period_type); - - // Prune before pushing the new entry - self.prune(); + stake_amount.add(amount, period_info.period_type); // 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(|_| ()) + self.prune(); + self.0.try_push(stake_amount).map_err(|_| ()) } /// Unstake the specified `amount` from the contract, for the specified `period_type` and `era`. @@ -1325,11 +950,12 @@ impl ContractStakingInfoSeries { period_info: PeriodInfo, era: EraNumber, ) -> Result<(), ()> { + // TODO: look into refactoring/optimizing this - right now it's a bit complex. + // 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 let Some(stake_amount) = 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 + if stake_amount.era.saturating_sub(1) > era || stake_amount.period > period_info.number { return Err(()); } @@ -1341,11 +967,11 @@ impl ContractStakingInfoSeries { // 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); + Some(stake_amount) if stake_amount.era == era.saturating_add(1) => { + let mut stake_amount = *stake_amount; + stake_amount.subtract(amount, period_info.period_type); let _ = self.0.pop(); - Some(last_element) + Some(stake_amount) } _ => None, }; @@ -1354,13 +980,13 @@ impl ContractStakingInfoSeries { // 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); + let second_last_era_info = if let Some(stake_amount) = self.0.last_mut() { + if stake_amount.era == era { + stake_amount.subtract(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); + } else if stake_amount.period == period_info.number { + let mut new_entry = *stake_amount; + new_entry.subtract(amount, period_info.period_type); new_entry.era = era; Some(new_entry) } else {