From 5cab4c6478cc309826518b8bdf633b9c0ff4ef8c Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 29 Sep 2023 16:58:56 +0200 Subject: [PATCH] dApp staking v3 - PR3 --- pallets/dapp-staking-v3/src/lib.rs | 44 +++ pallets/dapp-staking-v3/src/test/mock.rs | 1 + pallets/dapp-staking-v3/src/test/tests.rs | 16 ++ .../dapp-staking-v3/src/test/tests_types.rs | 257 +++++++++++++++++- pallets/dapp-staking-v3/src/types.rs | 83 +++++- 5 files changed, 385 insertions(+), 16 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index fc5a24d8a3..0134b769f4 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -567,6 +567,44 @@ pub mod pallet { Ok(()) } + + /// TODO + #[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!( + Self::is_active(&smart_contract), + Error::::NotOperatedDApp + ); + + let protocol_state = ActiveProtocolState::::get(); + let mut ledger = Ledger::::get(&account); + + // 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? + + Ok(()) + } + + /// TODO + #[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()?; + Ok(()) + } } impl Pallet { @@ -609,5 +647,11 @@ pub mod pallet { Ledger::::insert(account, ledger); } } + + /// `true` if smart contract is active, `false` if it has been unregistered. + fn is_active(smart_contract: &T::SmartContract) -> bool { + IntegratedDApps::::get(smart_contract) + .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) + } } } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index b8ef08163a..5441be6b22 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -117,6 +117,7 @@ impl pallet_dapp_staking::Config for Test { type UnlockingPeriod = ConstU64<20>; } +// 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), diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 4a6dcdc901..1eb2514aa3 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -83,6 +83,22 @@ fn maintenace_mode_call_filtering_works() { DappStaking::unlock(RuntimeOrigin::signed(1), 100), Error::::Disabled ); + assert_noop!( + DappStaking::claim_unlocked(RuntimeOrigin::signed(1)), + Error::::Disabled + ); + assert_noop!( + DappStaking::relock_unlocking(RuntimeOrigin::signed(1)), + Error::::Disabled + ); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(1), MockSmartContract::default(), 100), + Error::::Disabled + ); + assert_noop!( + DappStaking::unstake(RuntimeOrigin::signed(1), MockSmartContract::default(), 100), + Error::::Disabled + ); }) } diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 87623d8e82..c02f8f4df3 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -94,7 +94,7 @@ fn sparse_bounded_amount_era_vec_add_amount_works() { // 4th scenario - add to the previous era, should fail and be a noop assert_eq!( vec.add_amount(init_amount, first_era), - Err(SparseBoundedError::OldEra) + Err(AccountLedgerError::OldEra) ); assert_eq!(vec.0.len(), 2); assert_eq!(vec.0[0], DummyEraAmount::new(init_amount * 2, first_era)); @@ -106,7 +106,7 @@ fn sparse_bounded_amount_era_vec_add_amount_works() { } assert_eq!( vec.add_amount(init_amount, 100), - Err(SparseBoundedError::NoCapacity) + Err(AccountLedgerError::NoCapacity) ); } @@ -369,7 +369,7 @@ fn account_ledger_add_lock_amount_works() { // Adding to previous era should fail assert_eq!( acc_ledger.add_lock_amount(addition, first_era - 1), - Err(SparseBoundedError::OldEra) + Err(AccountLedgerError::OldEra) ); // Add up to storage limit @@ -387,7 +387,7 @@ fn account_ledger_add_lock_amount_works() { let acc_ledger_clone = acc_ledger.clone(); assert_eq!( acc_ledger.add_lock_amount(addition, acc_ledger.lock_era() + 1), - Err(SparseBoundedError::NoCapacity) + Err(AccountLedgerError::NoCapacity) ); assert_eq!(acc_ledger, acc_ledger_clone); } @@ -522,7 +522,7 @@ fn account_ledger_subtract_lock_amount_overflow_fails() { let acc_ledger_clone = acc_ledger.clone(); assert_eq!( acc_ledger.subtract_lock_amount(unlock_amount, LockedDummy::get() + 1), - Err(SparseBoundedError::NoCapacity) + Err(AccountLedgerError::NoCapacity) ); assert_eq!(acc_ledger, acc_ledger_clone); } @@ -799,7 +799,7 @@ fn account_ledger_add_unlocking_chunk_works() { let acc_ledger_snapshot = acc_ledger.clone(); assert_eq!( acc_ledger.add_unlocking_chunk(1, block_number + UnlockingDummy::get() as u64 + 1), - Err(SparseBoundedError::NoCapacity) + Err(AccountLedgerError::NoCapacity) ); assert_eq!(acc_ledger, acc_ledger_snapshot); } @@ -831,6 +831,251 @@ fn active_stake_works() { assert!(acc_ledger.active_stake(period + 1).is_zero()); } +#[test] +fn stakeable_amount_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Sanity check for empty ledger + assert!(acc_ledger.stakeable_amount(1).is_zero()); + + // First scenario - some locked amount, no staking chunks + let first_era = 1; + let first_period = 1; + let locked_amount = 19; + assert!(acc_ledger.add_lock_amount(locked_amount, first_era).is_ok()); + assert_eq!( + acc_ledger.stakeable_amount(first_period), + locked_amount, + "Stakeable amount has to be equal to the locked amount" + ); + + // Second scenario - some staked amount is introduced, period is still valid + let staked_amount = 7; + acc_ledger.staked = SparseBoundedAmountEraVec( + BoundedVec::try_from(vec![StakeChunk { + amount: staked_amount, + era: first_era, + }]) + .expect("Only one chunk so creation should succeed."), + ); + acc_ledger.staked_period = Some(first_period); + + assert_eq!( + acc_ledger.stakeable_amount(first_period), + locked_amount - staked_amount, + "Total stakeable amount should be equal to the locked amount minus what is already staked." + ); + + // Third scenario - continuation of the previous, but we move to the next period. + assert_eq!( + acc_ledger.stakeable_amount(first_period + 1), + locked_amount, + "Stakeable amount has to be equal to the locked amount since old period staking isn't valid anymore" + ); +} + +#[test] +fn staked_amount_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Sanity check for empty ledger + assert!(acc_ledger.staked_amount(1).is_zero()); + + // First scenario - active period matches the ledger + let first_era = 1; + let first_period = 1; + let locked_amount = 19; + let staked_amount = 13; + assert!(acc_ledger.add_lock_amount(locked_amount, first_era).is_ok()); + acc_ledger.staked = SparseBoundedAmountEraVec( + BoundedVec::try_from(vec![StakeChunk { + amount: staked_amount, + era: first_era, + }]) + .expect("Only one chunk so creation should succeed."), + ); + acc_ledger.staked_period = Some(first_period); + + assert_eq!(acc_ledger.staked_amount(first_period), staked_amount); + + // Second scenario - active period doesn't match the ledger + assert!(acc_ledger.staked_amount(first_period + 1).is_zero()); +} + +#[test] +fn add_stake_amount_works() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Sanity check + assert!(acc_ledger.add_stake_amount(0, 0, 0).is_ok()); + assert!(acc_ledger.staked_period.is_none()); + assert!(acc_ledger.staked.0.is_empty()); + + // First scenario - stake some amount, and ensure values are as expected + let first_era = 2; + let first_period = 1; + let lock_amount = 17; + let stake_amount = 11; + assert!(acc_ledger.add_lock_amount(lock_amount, first_era).is_ok()); + + assert!(acc_ledger + .add_stake_amount(stake_amount, first_era, first_period) + .is_ok()); + assert_eq!(acc_ledger.staked_period, Some(first_period)); + assert_eq!(acc_ledger.staked.0.len(), 1); + assert_eq!( + acc_ledger.staked.0[0], + StakeChunk { + amount: stake_amount, + era: first_era, + } + ); + assert_eq!(acc_ledger.staked_amount(first_period), stake_amount); + + // Second scenario - stake some more to the same era, only amount should change + assert!(acc_ledger + .add_stake_amount(1, first_era, first_period) + .is_ok()); + assert_eq!(acc_ledger.staked.0.len(), 1); + assert_eq!(acc_ledger.staked_amount(first_period), stake_amount + 1); + + // Third scenario - stake to the next era, new chunk should be added + let next_era = first_era + 3; + let remaining_not_staked = lock_amount - stake_amount - 1; + assert!(acc_ledger + .add_stake_amount(remaining_not_staked, next_era, first_period) + .is_ok()); + assert_eq!(acc_ledger.staked.0.len(), 2); + assert_eq!(acc_ledger.staked_amount(first_period), lock_amount); +} + +#[test] +fn add_stake_amount_invalid_era_fails() { + get_u32_type!(LockedDummy, 5); + get_u32_type!(UnlockingDummy, 5); + get_u32_type!(StakingDummy, 8); + let mut acc_ledger = + AccountLedger::::default(); + + // Prep actions + let first_era = 5; + let first_period = 2; + let lock_amount = 13; + let stake_amount = 7; + assert!(acc_ledger.add_lock_amount(lock_amount, first_era).is_ok()); + assert!(acc_ledger + .add_stake_amount(stake_amount, first_era, first_period) + .is_ok()); + let acc_ledger_snapshot = acc_ledger.clone(); + + // Try to add to the next era, it should fail + assert_eq!( + acc_ledger.add_stake_amount(1, first_era, first_period + 1), + Err(AccountLedgerError::InvalidPeriod) + ); + assert_eq!( + acc_ledger, acc_ledger_snapshot, + "Previous failed action must be a noop" + ); + + // Try to add to the previous era, it should fail + assert_eq!( + acc_ledger.add_stake_amount(1, first_era, first_period - 1), + Err(AccountLedgerError::InvalidPeriod) + ); + assert_eq!( + acc_ledger, acc_ledger_snapshot, + "Previous failed action must be a noop" + ); +} + +#[test] +fn add_stake_amount_too_large_amount_fails() { + get_u32_type!(LockedDummy, 5); + 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::TooLargeStakeAmount) + ); + + // Lock some amount, and try to stake more than that + let first_era = 5; + let first_period = 2; + let lock_amount = 13; + 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) + ); + + // 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::TooLargeStakeAmount) + ); +} + +#[test] +fn add_stake_amount_exceeding_capacity_fails() { + get_u32_type!(LockedDummy, 5); + 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; + assert!(acc_ledger.add_lock_amount(lock_amount, first_era).is_ok()); + 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 unlockable_amount_works() { get_u32_type!(LockedDummy, 5); diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 59321fe311..23eb0cb64c 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -62,11 +62,15 @@ pub trait AmountEraPair: MaxEncodedLen + Default + Copy { /// Simple enum representing errors possible when using sparse bounded vector. #[derive(Debug, PartialEq, Eq)] -pub enum SparseBoundedError { +pub enum AccountLedgerError { /// Old era values cannot be added. OldEra, /// Bounded storage capacity exceeded. NoCapacity, + /// Invalid period specified. + InvalidPeriod, + /// Stake amount is to large in respect to what's available. + TooLargeStakeAmount, } /// Helper struct for easier manipulation of sparse pairs. @@ -106,13 +110,13 @@ where &mut self, amount: Balance, era: EraNumber, - ) -> Result<(), SparseBoundedError> { + ) -> Result<(), AccountLedgerError> { if amount.is_zero() { return Ok(()); } let mut chunk = if let Some(&chunk) = self.0.last() { - ensure!(chunk.get_era() <= era, SparseBoundedError::OldEra); + ensure!(chunk.get_era() <= era, AccountLedgerError::OldEra); chunk } else { P::default() @@ -128,7 +132,7 @@ where chunk.set_era(era); self.0 .try_push(chunk) - .map_err(|_| SparseBoundedError::NoCapacity)?; + .map_err(|_| AccountLedgerError::NoCapacity)?; } Ok(()) @@ -155,7 +159,7 @@ where &mut self, amount: Balance, era: EraNumber, - ) -> Result<(), SparseBoundedError> { + ) -> Result<(), AccountLedgerError> { if amount.is_zero() || self.0.is_empty() { return Ok(()); } @@ -217,7 +221,7 @@ where } // Update `locked` to the new vector - self.0 = BoundedVec::try_from(inner).map_err(|_| SparseBoundedError::NoCapacity)?; + self.0 = BoundedVec::try_from(inner).map_err(|_| AccountLedgerError::NoCapacity)?; Ok(()) } @@ -452,6 +456,9 @@ where /// Returns active locked amount. /// If `zero`, means that associated account hasn't got any active locked funds. + /// + /// This value will always refer to the latest known locked amount. + /// It might be relevant for the current period, or for the next one. pub fn active_locked_amount(&self) -> Balance { self.latest_locked_chunk() .map_or(Balance::zero(), |locked| locked.amount) @@ -492,6 +499,62 @@ where } } + /// Amount that is available for staking. + /// + /// This is equal to the total active locked amount, minus the staked amount already active. + pub fn stakeable_amount(&self, active_period: PeriodNumber) -> Balance { + self.active_locked_amount() + .saturating_sub(self.active_stake(active_period)) + } + + /// Amount that is staked, in respect to currently active period. + pub fn staked_amount(&self, active_period: PeriodNumber) -> Balance { + match self.staked_period { + Some(last_staked_period) if last_staked_period == active_period => self + .staked + .0 + .last() + // We should never fallback to the default value since that would mean ledger is in invalid state. + // TODO: perhaps this can be implemented in a better way to have some error handling? Returning 0 might not be the most secure way to handle it. + .map_or(Balance::zero(), |chunk| chunk.amount), + _ => Balance::zero(), + } + } + + /// Adds the specified amount to total staked amount, if possible. + /// + /// Staking is allowed only allowed if one of the two following conditions is met: + /// 1. Staker is staking again in the period in which they already staked. + /// 2. Staker is staking for the first time in this period, and there are no staking chunks from the previous eras. + /// + /// Additonally, the staked amount must not exceed what's available for staking. + pub fn add_stake_amount( + &mut self, + amount: Balance, + era: EraNumber, + current_period: PeriodNumber, + ) -> Result<(), AccountLedgerError> { + if amount.is_zero() { + return Ok(()); + } + + match self.staked_period { + Some(last_staked_period) if last_staked_period != current_period => { + return Err(AccountLedgerError::InvalidPeriod); + } + _ => (), + } + + if self.stakeable_amount(current_period) < amount { + return Err(AccountLedgerError::TooLargeStakeAmount); + } + + self.staked.add_amount(amount, era)?; + self.staked_period = Some(current_period); + + Ok(()) + } + /// Adds the specified amount to the total locked amount, if possible. /// Caller must ensure that the era matches the next one, not the current one. /// @@ -503,7 +566,7 @@ where &mut self, amount: Balance, era: EraNumber, - ) -> Result<(), SparseBoundedError> { + ) -> Result<(), AccountLedgerError> { self.locked.add_amount(amount, era) } @@ -517,7 +580,7 @@ where &mut self, amount: Balance, era: EraNumber, - ) -> Result<(), SparseBoundedError> { + ) -> Result<(), AccountLedgerError> { self.locked.subtract_amount(amount, era) } @@ -531,7 +594,7 @@ where &mut self, amount: Balance, unlock_block: BlockNumber, - ) -> Result<(), SparseBoundedError> { + ) -> Result<(), AccountLedgerError> { if amount.is_zero() { return Ok(()); } @@ -551,7 +614,7 @@ where }; self.unlocking .try_insert(idx, new_unlocking_chunk) - .map_err(|_| SparseBoundedError::NoCapacity)?; + .map_err(|_| AccountLedgerError::NoCapacity)?; } }