diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 0134b769f4..ee22b05fce 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -193,6 +193,14 @@ pub mod pallet { NoUnlockedChunksToClaim, /// There are no unlocking chunks available to relock. NoUnlockingChunks, + /// The amount being staked is too large compared to what's available for staking. + UnavailableStakeFunds, + /// There are unclaimed rewards remaining from past periods. They should be claimed before staking again. + UnclaimedRewardsFromPastPeriods, + /// Cannot add additional stake chunks due to size limit. + TooManyStakeChunks, + /// An unexpected error occured while trying to stake. + InternalStakeError, } /// General information about dApp staking protocol state. @@ -219,6 +227,18 @@ pub mod pallet { pub type Ledger = StorageMap<_, Blake2_128Concat, T::AccountId, AccountLedgerFor, ValueQuery>; + /// Information about how much each staker has staked for each smart contract in some period. + #[pallet::storage] + pub type StakerInfo = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + T::SmartContract, + SingularStakingInfo, + OptionQuery, + >; + /// General information about the current era. #[pallet::storage] pub type CurrentEraInfo = StorageValue<_, EraInfo, ValueQuery>; @@ -587,6 +607,21 @@ pub mod pallet { let protocol_state = ActiveProtocolState::::get(); let mut ledger = Ledger::::get(&account); + // Increase stake amount for the current era & period. + ledger + .add_stake_amount(amount, protocol_state.era, protocol_state.period) + .map_err(|err| match err { + AccountLedgerError::InvalidPeriod => { + Error::::UnclaimedRewardsFromPastPeriods + } + AccountLedgerError::UnavailableStakeFunds => Error::::UnavailableStakeFunds, + AccountLedgerError::NoCapacity => Error::::TooManyStakeChunks, + AccountLedgerError::OldEra => Error::::InternalStakeError, + })?; + + // TODO: maybe keep track of pending bonus rewards in the AccountLedger struct? + // That way it's easy to check if stake can even be called - bonus-rewards should be zero & last staked era should be None or current one. + // is it voting or b&e period? // how much does user have available for staking? // has user claimed past rewards? Can we force them to do it before they start staking again? diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 5441be6b22..a434b26fa7 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -157,7 +157,10 @@ impl ExtBuilder { era: 1, next_era_start: BlockNumber::from(101_u32), period: 1, - period_type: PeriodType::Voting(16), + period_type_and_ending_era: PeriodTypeAndEndingEra { + period_type: PeriodType::Voting, + ending_era: 16, + }, maintenance: false, }); }); diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index c02f8f4df3..5f2681c7eb 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -1012,7 +1012,7 @@ fn add_stake_amount_too_large_amount_fails() { // Sanity check assert_eq!( acc_ledger.add_stake_amount(10, 1, 1), - Err(AccountLedgerError::TooLargeStakeAmount) + Err(AccountLedgerError::UnavailableStakeFunds) ); // Lock some amount, and try to stake more than that @@ -1022,7 +1022,7 @@ fn add_stake_amount_too_large_amount_fails() { assert!(acc_ledger.add_lock_amount(lock_amount, first_era).is_ok()); assert_eq!( acc_ledger.add_stake_amount(lock_amount + 1, first_era, first_period), - Err(AccountLedgerError::TooLargeStakeAmount) + Err(AccountLedgerError::UnavailableStakeFunds) ); // Additional check - have some active stake, and then try to overstake @@ -1031,12 +1031,12 @@ fn add_stake_amount_too_large_amount_fails() { .is_ok()); assert_eq!( acc_ledger.add_stake_amount(3, first_era, first_period), - Err(AccountLedgerError::TooLargeStakeAmount) + Err(AccountLedgerError::UnavailableStakeFunds) ); } #[test] -fn add_stake_amount_exceeding_capacity_fails() { +fn add_stake_amount_while_exceeding_capacity_fails() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); get_u32_type!(StakingDummy, 8); diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 23eb0cb64c..1c06d4abcc 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -70,7 +70,7 @@ pub enum AccountLedgerError { /// Invalid period specified. InvalidPeriod, /// Stake amount is to large in respect to what's available. - TooLargeStakeAmount, + UnavailableStakeFunds, } /// Helper struct for easier manipulation of sparse pairs. @@ -231,11 +231,17 @@ where #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] pub enum PeriodType { /// Period during which the focus is on voting. - /// Inner value is the era in which the voting period ends. - Voting(#[codec(compact)] EraNumber), + Voting, /// Period during which dApps and stakers earn rewards. - /// Inner value is the era in which the Build&Eearn period ends. - BuildAndEarn(#[codec(compact)] EraNumber), + BuildAndEarn, +} + +/// Wrapper type around current `PeriodType` and era number when it's expected to end. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub struct PeriodTypeAndEndingEra { + pub period_type: PeriodType, + #[codec(compact)] + pub ending_era: EraNumber, } /// Force types to speed up the next era, and even period. @@ -262,7 +268,7 @@ pub struct ProtocolState { #[codec(compact)] pub period: PeriodNumber, /// Ongoing period type and when is it expected to end. - pub period_type: PeriodType, + pub period_type_and_ending_era: PeriodTypeAndEndingEra, /// `true` if pallet is in maintenance mode (disabled), `false` otherwise. /// TODO: provide some configurable barrier to handle this on the runtime level instead? Make an item for this? pub maintenance: bool, @@ -277,7 +283,10 @@ where era: 0, next_era_start: BlockNumber::from(1_u32), period: 0, - period_type: PeriodType::Voting(0), + period_type_and_ending_era: PeriodTypeAndEndingEra { + period_type: PeriodType::Voting, + ending_era: 2, + }, maintenance: false, } } @@ -546,7 +555,7 @@ where } if self.stakeable_amount(current_period) < amount { - return Err(AccountLedgerError::TooLargeStakeAmount); + return Err(AccountLedgerError::UnavailableStakeFunds); } self.staked.add_amount(amount, era)?; @@ -702,3 +711,105 @@ impl EraInfo { self.unlocking.saturating_reduce(amount); } } + +/// Information about how much a particular staker staked on a particular smart contract. +/// +/// Keeps track of amount staked in the 'voting period', as well as 'build&earn period'. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] +pub struct SingularStakingInfo { + /// Total amount staked during the voting period. + #[codec(compact)] + vp_staked_amount: Balance, + /// Total amount staked during the build&earn period. + #[codec(compact)] + bep_staked_amount: Balance, + /// Period number for which this entry is relevant. + #[codec(compact)] + period: PeriodNumber, + /// Indicates whether a staker is a loyal staker or not. + loyal_staker: bool, + /// Indicates whether staker claimed rewards + reward_claimed: bool, +} + +impl SingularStakingInfo { + /// Creates new instance of the struct. + /// + /// ## Args + /// + /// `period` - period number for which this entry is relevant. + /// `period_type` - period type during which this entry is created. + pub fn new(period: PeriodNumber, period_type: PeriodType) -> Self { + Self { + vp_staked_amount: Balance::zero(), + bep_staked_amount: Balance::zero(), + period, + // Loyalty staking is only possible if stake is first made during the voting period. + loyal_staker: period_type == PeriodType::Voting, + reward_claimed: false, + } + } + + /// Stake the specified amount on the contract, for the specified period type. + pub fn stake(&mut self, amount: Balance, period_type: PeriodType) { + if period_type == PeriodType::Voting { + self.vp_staked_amount.saturating_accrue(amount); + } else { + self.bep_staked_amount.saturating_accrue(amount); + } + } + + /// Unstakes some of the specified amount from the contract. + /// + /// In case the `amount` being unstaked is larger than the amount staked in the `voting period`, + /// and `voting period` has passed, this will remove the _loyalty_ flag from the staker. + /// + /// Returns the amount that was unstaked from the `voting period` stake. + // TODO: Maybe both unstake values should be returned? + pub fn unstake(&mut self, amount: Balance, period_type: PeriodType) -> Balance { + // If B&E period stake can cover the unstaking amount, just reduce it. + if self.bep_staked_amount >= amount { + self.bep_staked_amount.saturating_reduce(amount); + Balance::zero() + } else { + // In case we have to dip into the voting period stake, make sure B&E period stake is reduced first. + // Also make sure to remove loyalty flag from the staker. + let leftover_amount = amount.saturating_sub(self.bep_staked_amount); + self.bep_staked_amount = Balance::zero(); + + let vp_staked_amount_snapshot = self.vp_staked_amount; + self.vp_staked_amount.saturating_reduce(leftover_amount); + self.bep_staked_amount = Balance::zero(); + + // It's ok if staker reduces their stake amount during voting period. + self.loyal_staker = period_type == PeriodType::Voting; + + // Actual amount that was unstaked + vp_staked_amount_snapshot.saturating_sub(self.vp_staked_amount) + } + } + + /// Total staked on the contract by the user. Both period type stakes are included. + pub fn total_staked_amount(&self) -> Balance { + self.vp_staked_amount.saturating_add(self.bep_staked_amount) + } + + /// Returns amount staked in the specified period. + pub fn staked_amount(&self, period_type: PeriodType) -> Balance { + if period_type == PeriodType::Voting { + self.vp_staked_amount + } else { + self.bep_staked_amount + } + } + + /// If `true` staker has staked during voting period and has never reduced their sta + pub fn is_loyal(&self) -> bool { + self.loyal_staker + } + + /// Period for which this entry is relevant. + pub fn period_number(&self) -> PeriodNumber { + self.period + } +}