diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 4baa8e6b15..8df02a4f90 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,1099 @@ 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>; + + /// 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 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, + /// 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, ContractStakeAmountSeries, 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.staked_amount(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_info) + .map_err(|err| match err { + AccountLedgerError::InvalidPeriod => { + Error::::UnclaimedRewardsFromPastPeriods + } + AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, + // 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_info) + .map_err(|err| match err { + AccountLedgerError::InvalidPeriod => Error::::UnstakeFromPastPeriod, + AccountLedgerError::UnstakeAmountLargerThanStake => { + Error::::UnstakeAmountTooLarge + } + _ => 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 c3b15bdc1c..ab88c11773 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -39,215 +39,214 @@ 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 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/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index a9e6e336fe..5ad99cb1a2 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -292,12 +292,12 @@ fn account_ledger_stakeable_amount_works() { // Sanity check for empty ledger assert!(acc_ledger.stakeable_amount(1).is_zero()); - // First scenario - some locked amount, no staking chunks - let first_period = 1; + // 1st scenario - some locked amount, no staking chunks + let period_1 = 1; let locked_amount = 19; acc_ledger.add_lock_amount(locked_amount); assert_eq!( - acc_ledger.stakeable_amount(first_period), + acc_ledger.stakeable_amount(period_1), locked_amount, "Stakeable amount has to be equal to the locked amount" ); @@ -305,38 +305,42 @@ 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 = StakeAmount::new(0, staked_amount, first_era, first_period); + acc_ledger.staked = StakeAmount::new(0, staked_amount, first_era, period_1); assert_eq!( - acc_ledger.stakeable_amount(first_period), + acc_ledger.stakeable_amount(period_1), locked_amount - staked_amount, "Total stakeable amount should be equal to the locked amount minus what is already staked." ); // Third scenario - continuation of the previous, but we move to the next period. assert_eq!( - acc_ledger.stakeable_amount(first_period + 1), + acc_ledger.stakeable_amount(period_1 + 1), locked_amount, "Stakeable amount has to be equal to the locked amount since old period staking isn't valid anymore" ); } #[test] -fn account_ledger_add_stake_amount_works() { +fn account_ledger_add_stake_amount_basic_example_works() { get_u32_type!(UnlockingDummy, 5); let mut acc_ledger = AccountLedger::::default(); // Sanity check + let period_number = 2; assert!(acc_ledger - .add_stake_amount(0, 0, PeriodInfo::new(0, PeriodType::Voting, 0)) + .add_stake_amount(0, 0, PeriodInfo::new(period_number, PeriodType::Voting, 0)) .is_ok()); assert!(acc_ledger.staked.is_empty()); assert!(acc_ledger.staked_future.is_none()); + assert!(acc_ledger + .staked_amount_for_type(PeriodType::Voting, period_number) + .is_zero()); - // First scenario - stake some amount, and ensure values are as expected. + // 1st 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 period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); let lock_amount = 17; let stake_amount = 11; acc_ledger.add_lock_amount(lock_amount); @@ -354,17 +358,17 @@ fn account_ledger_add_stake_amount_works() { .staked_future .expect("Must exist after stake.") .period, - first_period + period_1 ); 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); + assert_eq!(acc_ledger.staked_amount(period_1), stake_amount); assert_eq!( - acc_ledger.staked_amount_for_type(PeriodType::Voting, first_period), + acc_ledger.staked_amount_for_type(PeriodType::Voting, period_1), stake_amount ); assert!(acc_ledger - .staked_amount_for_type(PeriodType::BuildAndEarn, first_period) + .staked_amount_for_type(PeriodType::BuildAndEarn, period_1) .is_zero()); // Second scenario - stake some more to the same era @@ -372,19 +376,62 @@ fn account_ledger_add_stake_amount_works() { assert!(acc_ledger .add_stake_amount(1, first_era, period_info_1) .is_ok()); - assert_eq!(acc_ledger.staked_amount(first_period), stake_amount + 1); + assert_eq!(acc_ledger.staked_amount(period_1), stake_amount + 1); assert_eq!(acc_ledger.staked, snapshot); } #[test] -fn account_ledger_add_stake_amount_invalid_era_fails() { +fn account_ledger_add_stake_amount_advanced_example_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // 1st scenario - stake some amount, and ensure values are as expected. + let first_era = 1; + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); + let lock_amount = 17; + let stake_amount_1 = 11; + acc_ledger.add_lock_amount(lock_amount); + + // We only have entry for the current era + acc_ledger.staked = StakeAmount::new(stake_amount_1, 0, first_era, period_1); + + let stake_amount_2 = 2; + let acc_ledger_snapshot = acc_ledger.clone(); + assert!(acc_ledger + .add_stake_amount(stake_amount_2, first_era, period_info_1) + .is_ok()); + assert_eq!( + acc_ledger.staked_amount(period_1), + stake_amount_1 + stake_amount_2 + ); + assert_eq!( + acc_ledger.staked, acc_ledger_snapshot.staked, + "This entry must remain unchanged." + ); + assert_eq!( + acc_ledger.staked_amount_for_type(PeriodType::Voting, period_1), + stake_amount_1 + stake_amount_2 + ); + assert_eq!( + acc_ledger + .staked_future + .unwrap() + .for_type(PeriodType::Voting), + stake_amount_1 + stake_amount_2 + ); + assert_eq!(acc_ledger.staked_future.unwrap().era, first_era + 1); +} + +#[test] +fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { get_u32_type!(UnlockingDummy, 5); 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 period_1 = 2; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); let lock_amount = 13; let stake_amount = 7; acc_ledger.add_lock_amount(lock_amount); @@ -394,1153 +441,1064 @@ fn account_ledger_add_stake_amount_invalid_era_fails() { let acc_ledger_snapshot = acc_ledger.clone(); // Try to add to the next era, it should fail. - println!("{:?}", acc_ledger); assert_eq!( acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), Err(AccountLedgerError::InvalidEra) ); + + // Try to add to the next period, it should fail. assert_eq!( - acc_ledger, acc_ledger_snapshot, - "Previous failed action must be a noop" + acc_ledger.add_stake_amount( + 1, + first_era, + PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) + ), + Err(AccountLedgerError::InvalidPeriod) ); - // Try to add to the previous era, it should fail. + // Alternative situation - no future entry, only current era + acc_ledger.staked = StakeAmount::new(0, stake_amount, first_era, period_1); + acc_ledger.staked_future = None; + assert_eq!( - acc_ledger.add_stake_amount(1, first_era - 1, period_info_1), + acc_ledger.add_stake_amount(1, first_era + 1, period_info_1), Err(AccountLedgerError::InvalidEra) ); assert_eq!( - acc_ledger, acc_ledger_snapshot, - "Previous failed action must be a noop" + acc_ledger.add_stake_amount( + 1, + first_era, + PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) + ), + Err(AccountLedgerError::InvalidPeriod) + ); +} + +#[test] +fn account_ledger_add_stake_amount_too_large_amount_fails() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Sanity check + assert_eq!( + acc_ledger.add_stake_amount(10, 1, PeriodInfo::new(1, PeriodType::Voting, 100)), + Err(AccountLedgerError::UnavailableStakeFunds) + ); + + // Lock some amount, and try to stake more than that + let first_era = 5; + let period_1 = 2; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::Voting, 100); + let lock_amount = 13; + acc_ledger.add_lock_amount(lock_amount); + assert_eq!( + acc_ledger.add_stake_amount(lock_amount + 1, first_era, period_info_1), + Err(AccountLedgerError::UnavailableStakeFunds) + ); + + // Additional check - have some active stake, and then try to overstake + assert!(acc_ledger + .add_stake_amount(lock_amount - 2, first_era, period_info_1) + .is_ok()); + assert_eq!( + acc_ledger.add_stake_amount(3, first_era, period_info_1), + Err(AccountLedgerError::UnavailableStakeFunds) + ); +} + +#[test] +fn account_ledger_unstake_amount_basic_scenario_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 19; + let era_1 = 2; + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + acc_ledger.add_lock_amount(amount_1); + + let mut acc_ledger_2 = acc_ledger.clone(); + + // 'Current' staked entry will remain empty. + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_info_1) + .is_ok()); + + // Only 'current' entry has some values, future is set to None. + acc_ledger_2.staked = StakeAmount::new(0, amount_1, era_1, period_1); + acc_ledger_2.staked_future = None; + + for mut acc_ledger in vec![acc_ledger, acc_ledger_2] { + // Sanity check + assert!(acc_ledger.unstake_amount(0, era_1, period_info_1).is_ok()); + + // 1st scenario - unstake some amount from the current era. + let unstake_amount_1 = 3; + assert!(acc_ledger + .unstake_amount(unstake_amount_1, era_1, period_info_1) + .is_ok()); + assert_eq!( + acc_ledger.staked_amount(period_1), + amount_1 - unstake_amount_1 + ); + + // 2nd scenario - perform full unstake + assert!(acc_ledger + .unstake_amount(amount_1 - unstake_amount_1, era_1, period_info_1) + .is_ok()); + assert!(acc_ledger.staked_amount(period_1).is_zero()); + assert!(acc_ledger.staked.is_empty()); + assert_eq!(acc_ledger.staked, StakeAmount::default()); + assert!(acc_ledger.staked_future.is_none()); + } +} +#[test] +fn account_ledger_unstake_amount_advanced_scenario_works() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 19; + let era_1 = 2; + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + acc_ledger.add_lock_amount(amount_1); + + // We have two entries at once + acc_ledger.staked = StakeAmount::new(amount_1 - 1, 0, era_1, period_1); + acc_ledger.staked_future = Some(StakeAmount::new(amount_1 - 1, 1, era_1 + 1, period_1)); + + // 1st scenario - unstake some amount from the current era, both entries should be affected. + let unstake_amount_1 = 3; + assert!(acc_ledger + .unstake_amount(unstake_amount_1, era_1, period_info_1) + .is_ok()); + assert_eq!( + acc_ledger.staked_amount(period_1), + amount_1 - unstake_amount_1 + ); + + assert_eq!( + acc_ledger.staked.for_type(PeriodType::Voting), + amount_1 - 1 - 3 + ); + assert_eq!( + acc_ledger + .staked_future + .unwrap() + .for_type(PeriodType::Voting), + amount_1 - 3 + ); + assert!(acc_ledger + .staked_future + .unwrap() + .for_type(PeriodType::BuildAndEarn) + .is_zero()); + + // 2nd scenario - perform full unstake + assert!(acc_ledger + .unstake_amount(amount_1 - unstake_amount_1, era_1, period_info_1) + .is_ok()); + assert!(acc_ledger.staked_amount(period_1).is_zero()); + assert_eq!(acc_ledger.staked, StakeAmount::default()); + assert!(acc_ledger.staked_future.is_none()); + + // 3rd scenario - try to stake again, ensure it works + let era_2 = era_1 + 7; + let amount_2 = amount_1 - 5; + assert!(acc_ledger + .add_stake_amount(amount_2, era_2, period_info_1) + .is_ok()); + assert_eq!(acc_ledger.staked_amount(period_1), amount_2); + assert_eq!(acc_ledger.staked, StakeAmount::default()); + assert_eq!( + acc_ledger + .staked_future + .unwrap() + .for_type(PeriodType::BuildAndEarn), + amount_2 + ); +} + +#[test] +fn account_ledger_unstake_from_invalid_era_fails() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 13; + let era_1 = 2; + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + acc_ledger.add_lock_amount(amount_1); + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_info_1) + .is_ok()); + + // Try to add to the next era, it should fail. + assert_eq!( + acc_ledger.add_stake_amount(1, era_1 + 1, period_info_1), + Err(AccountLedgerError::InvalidEra) ); // Try to add to the next period, it should fail. assert_eq!( acc_ledger.add_stake_amount( 1, - first_era, - PeriodInfo::new(first_period + 1, PeriodType::Voting, 100) + era_1, + PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) ), Err(AccountLedgerError::InvalidPeriod) ); + + // Alternative situation - no future entry, only current era + acc_ledger.staked = StakeAmount::new(0, 1, era_1, period_1); + acc_ledger.staked_future = None; + assert_eq!( - acc_ledger, acc_ledger_snapshot, - "Previous failed action must be a noop" + acc_ledger.add_stake_amount(1, era_1 + 1, period_info_1), + Err(AccountLedgerError::InvalidEra) ); - - // Try to add to the previous period, it should fail assert_eq!( acc_ledger.add_stake_amount( 1, - first_era, - PeriodInfo::new(first_period - 1, PeriodType::Voting, 100) + era_1, + PeriodInfo::new(period_1 + 1, PeriodType::Voting, 100) ), Err(AccountLedgerError::InvalidPeriod) ); +} + +#[test] +fn account_ledger_unstake_too_much_fails() { + get_u32_type!(UnlockingDummy, 5); + let mut acc_ledger = AccountLedger::::default(); + + // Prep actions + let amount_1 = 23; + let era_1 = 2; + let period_1 = 1; + let period_info_1 = PeriodInfo::new(period_1, PeriodType::BuildAndEarn, 100); + acc_ledger.add_lock_amount(amount_1); + assert!(acc_ledger + .add_stake_amount(amount_1, era_1, period_info_1) + .is_ok()); + + assert_eq!( + acc_ledger.unstake_amount(amount_1 + 1, era_1, period_info_1), + Err(AccountLedgerError::UnstakeAmountLargerThanStake) + ); +} + +#[test] +fn account_ledger_unlockable_amount_works() { + get_u32_type!(UnlockingDummy, 5); + 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; + let period_info = PeriodInfo::new(stake_period, PeriodType::Voting, 100); + assert!(acc_ledger + .add_stake_amount(stake_amount, lock_era, period_info) + .is_ok()); + assert_eq!( + acc_ledger.unlockable_amount(stake_period), + lock_amount - stake_amount + ); + + // 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); + 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); + 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 account_ledger_claim_up_to_era_works() { + // TODO!!! +} + +#[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; + let period_number = 1; + let era = 2; + era_info.current_stake_amount = + StakeAmount::new(vp_stake_amount, bep_stake_amount_1, era, period_number); + era_info.next_stake_amount = + StakeAmount::new(vp_stake_amount, bep_stake_amount_2, era + 1, period_number); + let total_staked = era_info.total_staked_amount(); + let total_staked_next_era = era_info.total_staked_amount_next_era(); + + // 1st scenario - unstake some amount, no overflow + let unstake_amount_1 = bep_stake_amount_1; + era_info.unstake_amount(unstake_amount_1, PeriodType::BuildAndEarn); + + // 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.add(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.add(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.subtract(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.subtract(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.subtract(bep_unstake_2, PeriodType::BuildAndEarn); + assert_eq!(stake_amount.total(), total_stake - bep_unstake_2); assert_eq!( - acc_ledger, acc_ledger_snapshot, - "Previous failed action must be a noop" + stake_amount.for_type(PeriodType::Voting), + vp_stake_1 - vp_unstake_1 - 1 ); + assert!(stake_amount.for_type(PeriodType::BuildAndEarn).is_zero()); } -// #[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) -// ); -// } +#[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_amount_info_series_get_works() { + let info_1 = StakeAmount::new(0, 0, 4, 2); + let info_2 = StakeAmount::new(11, 0, 7, 3); + let info_3 = StakeAmount::new(0, 13, 9, 3); + + let series = ContractStakeAmountSeries::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().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(), 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_stake_amount_info_series_stake_is_ok() { + let mut series = ContractStakeAmountSeries::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(), 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(), 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(), + entry_2_1.total() + amount_2, + "Since it's the same period, stake amount must carry over from the previous entry." + ); + + // 4th scenario - stake some more to the next era, but this time also bump the period. + let era_3 = era_2 + 3; + let 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(), + 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(), amount_3 + amount_4); +} + +#[test] +fn contract_stake_amount_info_series_stake_with_inconsistent_data_fails() { + let mut series = ContractStakeAmountSeries::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_stake_amount_info_series_unstake_is_ok() { + let mut series = ContractStakeAmountSeries::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(), + stake_amount - amount_1, + "Oldest entry must remain unchanged." + ); + assert_eq!( + series.get(era_2, period).unwrap().total(), + 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(), + 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_stake_amount_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 (stake_amount_2, stake_amount_3) = (11, 13); + let info_1 = StakeAmount::new(11, 0, era_1, period_1); + let info_2 = StakeAmount::new(stake_amount_2, 0, era_2, period_2); + let info_3 = StakeAmount::new(0, stake_amount_3, era_3, period_2); + + // 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 = ContractStakeAmountSeries::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(), + stake_amount_2 + ); + assert_eq!( + series + .get(era_2 + 1, period_2) + .expect("Entry must exist.") + .total(), + stake_amount_2 - unstake_amount + ); + assert_eq!( + series + .get(era_3, period_2) + .expect("Entry must exist.") + .total(), + stake_amount_3 - unstake_amount + ); +} + +#[test] +fn contract_stake_amount_info_series_unstake_with_inconsistent_data_fails() { + let mut series = ContractStakeAmountSeries::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 1d15e0e548..8c418e60ba 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -26,16 +26,10 @@ 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, -// >; +// Convenience type for `AccountLedger` usage. +pub type AccountLedgerFor = AccountLedger, ::MaxUnlockingChunks>; /// Era number type pub type EraNumber = u32; @@ -462,13 +456,23 @@ where } // TODO: maybe the check can be nicer? - if self.staked.era > EraNumber::zero() { + if !self.staked.is_empty() { + // In case entry for the current era exists, it must match the era exactly. if self.staked.era != era { return Err(AccountLedgerError::InvalidEra); } if self.staked.period != current_period_info.number { return Err(AccountLedgerError::InvalidPeriod); } + // In case it doesn't (i.e. first time staking), then the future era must match exactly + // one era after the one provided via argument. + } else if let Some(stake_amount) = self.staked_future { + if stake_amount.era != era + 1 { + return Err(AccountLedgerError::InvalidEra); + } + if stake_amount.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } } if self.stakeable_amount(current_period_info.number) < amount { @@ -506,11 +510,24 @@ where return Ok(()); } - if self.staked.era != era { - return Err(AccountLedgerError::InvalidEra); - } - if self.staked.period != current_period_info.number { - return Err(AccountLedgerError::InvalidPeriod); + // TODO: maybe the check can be nicer? (and not duplicated?) + if !self.staked.is_empty() { + // In case entry for the current era exists, it must match the era exactly. + if self.staked.era != era { + return Err(AccountLedgerError::InvalidEra); + } + if self.staked.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } + // In case it doesn't (i.e. first time staking), then the future era must match exactly + // one era after the one provided via argument. + } else if let Some(stake_amount) = self.staked_future { + if stake_amount.era != era + 1 { + return Err(AccountLedgerError::InvalidEra); + } + if stake_amount.period != current_period_info.number { + return Err(AccountLedgerError::InvalidPeriod); + } } // User must be precise with their unstake amount. @@ -520,8 +537,18 @@ where self.staked .subtract(amount, current_period_info.period_type); - if let Some(stake_amount) = self.staked_future.as_mut() { + // Convenience cleanup + if self.staked.is_empty() { + self.staked = Default::default(); + } + if let Some(mut stake_amount) = self.staked_future { stake_amount.subtract(amount, current_period_info.period_type); + + self.staked_future = if stake_amount.is_empty() { + None + } else { + Some(stake_amount) + }; } Ok(()) @@ -535,8 +562,9 @@ where &mut self, era: EraNumber, period_end: Option, - // TODO: add a better type later ) -> Result<(EraNumber, EraNumber, Balance), AccountLedgerError> { + // TODO: the check also needs to ensure that future entry is covered!!! + // TODO2: the return type won't work since we can have 2 distinct values - one from staked, one from staked_future if era <= self.staked.era || self.staked.total().is_zero() { return Err(AccountLedgerError::NothingToClaim); } @@ -817,9 +845,9 @@ 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 StakeAmountSeries(BoundedVec>); -impl StakeAmountSeries { - /// Helper function to create a new instance of `StakeAmountSeries`. +pub struct ContractStakeAmountSeries(BoundedVec>); +impl ContractStakeAmountSeries { + /// Helper function to create a new instance of `ContractStakeAmountSeries`. #[cfg(test)] pub fn new(inner: Vec) -> Self { Self(BoundedVec::try_from(inner).expect("Test should ensure this is always valid")) @@ -872,16 +900,6 @@ impl StakeAmountSeries { } } - /// Last era for which a stake entry exists, `None` if no entries exist. - pub fn last_stake_era(&self) -> Option { - self.0.last().map(|info| info.era) - } - - /// Last period for which a stake entry exists, `None` if no entries exist. - pub fn last_stake_period(&self) -> Option { - self.0.last().map(|info| info.period) - } - /// Total staked amount on the contract, in the active period. pub fn total_staked_amount(&self, active_period: PeriodNumber) -> Balance { match self.0.last() {