difftreelog
Merge pull request #867 from UniqueNetwork/feature/appPromoReservationBalance
in: master
Feature/app promo reservation balance
5 files changed
Cargo.lockdiffbeforeafterboth580358035804[[package]]5804[[package]]5805name = "pallet-app-promotion"5805name = "pallet-app-promotion"5806version = "0.1.3"5806version = "0.1.4"5807dependencies = [5807dependencies = [5808 "frame-benchmarking",5808 "frame-benchmarking",5809 "frame-support",5809 "frame-support",5810 "frame-system",5810 "frame-system",5811 "log",5811 "pallet-balances",5812 "pallet-balances",5812 "pallet-common",5813 "pallet-common",5813 "pallet-configuration",5814 "pallet-configuration",pallets/app-promotion/CHANGELOG.mddiffbeforeafterboth445<!-- bureaucrate goes here -->5<!-- bureaucrate goes here -->667## [0.1.4] - 2023-01-3189### Changed1011- Balance reservation when calling the unstake method is removed.12 Now the locked balance (by AppPromo) does not change until the unlock interval expires.137## [0.1.3] - 2022-12-2514## [0.1.3] - 2022-12-258159### Fixed16### Fixedpallets/app-promotion/Cargo.tomldiffbeforeafterboth9license = 'GPLv3'9license = 'GPLv3'10name = 'pallet-app-promotion'10name = 'pallet-app-promotion'11repository = 'https://github.com/UniqueNetwork/unique-chain'11repository = 'https://github.com/UniqueNetwork/unique-chain'12version = '0.1.3'12version = '0.1.4'131314[package.metadata.docs.rs]14[package.metadata.docs.rs]15targets = ['x86_64-unknown-linux-gnu']15targets = ['x86_64-unknown-linux-gnu']67# [dev-dependencies]67# [dev-dependencies]686869################################################################################69################################################################################7070# Other7172log = { version = "0.4.16", default-features = false }pallets/app-promotion/src/lib.rsdiffbeforeafterboth97 use super::*;97 use super::*;98 use frame_support::{98 use frame_support::{99 Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, PalletId,99 Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, PalletId,100 traits::ReservableCurrency,100 traits::ReservableCurrency, weights::Weight,101 };101 };102 use frame_system::pallet_prelude::*;102 use frame_system::pallet_prelude::*;103103234 QueryKind = ValueQuery,234 QueryKind = ValueQuery,235 >;235 >;236236237 /// Stores amount of stakes for an `Account`.237 /// Stores number of stake records for an `Account`.238 ///238 ///239 /// * **Key** - Staker account.239 /// * **Key** - Staker account.240 /// * **Value** - Amount of stakes.240 /// * **Value** - Amount of stakes.241 #[pallet::storage]241 #[pallet::storage]242 pub type StakesPerAccount<T: Config> =242 pub type StakesPerAccount<T: Config> =243 StorageMap<_, Blake2_128Concat, T::AccountId, u8, ValueQuery>;243 StorageMap<_, Blake2_128Concat, T::AccountId, u8, ValueQuery>;244244245 /// Stores amount of stakes for an `Account`.245 /// Pending unstake records for an `Account`.246 ///246 ///247 /// * **Key** - Staker account.247 /// * **Key** - Staker account.248 /// * **Value** - Amount of stakes.248 /// * **Value** - Amount of stakes.262 pub type PreviousCalculatedRecord<T: Config> =262 pub type PreviousCalculatedRecord<T: Config> =263 StorageValue<Value = (T::AccountId, T::BlockNumber), QueryKind = OptionQuery>;263 StorageValue<Value = (T::AccountId, T::BlockNumber), QueryKind = OptionQuery>;264265 #[pallet::storage]266 pub(crate) type UpgradedToReserves<T: Config> =267 StorageValue<Value = bool, QueryKind = ValueQuery>;264268265 #[pallet::hooks]269 #[pallet::hooks]266 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {270 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {276280277 if !block_pending.is_empty() {281 if !block_pending.is_empty() {278 block_pending.into_iter().for_each(|(staker, amount)| {282 block_pending.into_iter().for_each(|(staker, amount)| {279 <<T as Config>::Currency as ReservableCurrency<T::AccountId>>::unreserve(283 Self::get_locked_balance(&staker).map(|b| {284 let new_state = b.amount.checked_sub(&amount).unwrap_or_default();280 &staker, amount,285 Self::set_lock_unchecked(&staker, new_state);281 );286 });282 });287 });283 }288 }284289285 <T as Config>::WeightInfo::on_initialize(counter)290 <T as Config>::WeightInfo::on_initialize(counter)286 }291 }292293 fn on_runtime_upgrade() -> Weight {294 let mut consumed_weight = Weight::zero();295 let mut add_weight = |reads, writes, weight| {296 consumed_weight += T::DbWeight::get().reads_writes(reads, writes);297 consumed_weight += weight;298 };299300 if <UpgradedToReserves<T>>::get() {301 add_weight(1, 0, Weight::zero());302 return consumed_weight;303 } else {304 add_weight(1, 1, Weight::zero());305 <UpgradedToReserves<T>>::set(true);306 }307 <PendingUnstake<T>>::drain().for_each(|(_, v)| {308 add_weight(1, 1, Weight::zero());309 v.into_iter().for_each(|(staker, amount)| {310 <<T as Config>::Currency as ReservableCurrency<T::AccountId>>::unreserve(311 &staker, amount,312 );313 add_weight(1, 1, Weight::zero());314 });315 });316317 consumed_weight318 }319320 #[cfg(feature = "try-runtime")]321 fn pre_upgrade() -> Result<Vec<u8>, &'static str> {322 use sp_std::collections::btree_map::BTreeMap;323 if <UpgradedToReserves<T>>::get() {324 return Ok(Default::default());325 }326 // Staker -> (total amount of reserved balance, reserved by promotion);327 let mut pre_state: BTreeMap<T::AccountId, (BalanceOf<T>, BalanceOf<T>)> =328 BTreeMap::new();329330 <PendingUnstake<T>>::iter().for_each(|(_, v)| {331 v.into_iter().for_each(|(staker, amount)| {332 if let Some((_, reserved_balance)) = pre_state.get_mut(&staker) {333 *reserved_balance += amount;334 } else {335 let total_reserve = <<T as Config>::Currency as ReservableCurrency<336 T::AccountId,337 >>::reserved_balance(&staker);338 pre_state.insert(staker, (total_reserve, amount));339 }340 })341 });342343 Ok(pre_state.encode())344 }345346 #[cfg(feature = "try-runtime")]347 fn post_upgrade(pre_state: Vec<u8>) -> Result<(), &'static str> {348 use sp_std::collections::btree_map::BTreeMap;349350 if <UpgradedToReserves<T>>::get() {351 return Ok(());352 }353354 ensure!(355 <PendingUnstake<T>>::iter().collect::<Vec<_>>().len() == 0,356 "pendingUnstake storage isn't empty"357 );358359 let mut is_ok = true;360361 let pre_state: BTreeMap<T::AccountId, (BalanceOf<T>, BalanceOf<T>)> =362 Decode::decode(&mut &pre_state[..]).map_err(|_| "failed to decode pre_state")?;363 for (staker, (total_reserved, reserved_by_promo)) in pre_state.into_iter() {364 let new_state_reserve = <<T as Config>::Currency as ReservableCurrency<365 T::AccountId,366 >>::reserved_balance(&staker);367 if new_state_reserve != total_reserved - reserved_by_promo {368 is_ok = false;369 log::error!(370 "Incorrect reserved balance for {:?}. New balance: {:?}. Before runtime upgrade: total reserve - {:?}, reserved by promo - {:?}",371 staker, new_state_reserve, total_reserved, reserved_by_promo372 );373 }374 }375376 if is_ok {377 Ok(())378 } else {379 Err("Incorrect balance for some of stakers... See logs")380 }381 }287 }382 }288383289 #[pallet::call]384 #[pallet::call]340 <<T as Config>::Currency as Currency<T::AccountId>>::free_balance(&staker_id);435 <<T as Config>::Currency as Currency<T::AccountId>>::free_balance(&staker_id);341436342 // checks that we can lock `amount` on the `staker` account.437 // checks that we can lock `amount` on the `staker` account.343 <<T as Config>::Currency as Currency<T::AccountId>>::ensure_can_withdraw(438 ensure!(344 &staker_id,345 amount,439 amount346 WithdrawReasons::all(),440 <= match Self::get_locked_balance(&staker_id) {347 balance441 Some(lock) => balance348 .checked_sub(&amount)442 .checked_sub(&lock.amount)349 .ok_or(ArithmeticError::Underflow)?,443 .ok_or(ArithmeticError::Underflow)?,444 None => balance,445 },446 ArithmeticError::Underflow350 )?;447 );351448352 Self::add_lock_balance(&staker_id, amount)?;449 Self::add_lock_balance(&staker_id, amount)?;353450428525429 <PendingUnstake<T>>::insert(block, pendings);526 <PendingUnstake<T>>::insert(block, pendings);430431 Self::unlock_balance(&staker_id, total_staked)?;432433 <<T as Config>::Currency as ReservableCurrency<T::AccountId>>::reserve(434 &staker_id,435 total_staked,436 )?;437527438 TotalStaked::<T>::set(528 TotalStaked::<T>::set(439 TotalStaked::<T>::get()529 TotalStaked::<T>::get()719 T::PalletId::get().into_account_truncating()809 T::PalletId::get().into_account_truncating()720 }810 }721722 /// Unlocks the balance that was locked by the pallet.723 ///724 /// - `staker`: staker account.725 /// - `amount`: amount of unlocked funds.726 fn unlock_balance(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {727 let locked_balance = Self::get_locked_balance(staker)728 .map(|l| l.amount)729 .ok_or(<Error<T>>::IncorrectLockedBalanceOperation)?;730731 // It is understood that we cannot unlock more funds than were locked by staking.732 // Therefore, if implemented correctly, this error should not occur.733 Self::set_lock_unchecked(734 staker,735 locked_balance736 .checked_sub(&amount)737 .ok_or(ArithmeticError::Underflow)?,738 );739 Ok(())740 }741811742 /// Adds the balance to locked by the pallet.812 /// Adds the balance to locked by the pallet.743 ///813 ///tests/src/sub/appPromotion/appPromotion.test.tsdiffbeforeafterboth91 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);91 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);92 });92 });9394 itSub('should allow to stake() if balance is locked with different id', async ({helper}) => {95 const staker = accounts.pop()!;9697 // staker has tokens locked with vesting id:98 await helper.balance.vestedTransfer(donor, staker.address, {start: 0n, period: 1n, periodCount: 1n, perPeriod: 200n * nominal});99 expect(await helper.balance.getSubstrateFull(staker.address))100 .to.deep.contain({free: 1200n * nominal, miscFrozen: 200n * nominal, feeFrozen: 200n * nominal, reserved: 0n});101102 // Locked balance can be staked. staker can stake 1200 tokens (minus fee):103 await helper.staking.stake(staker, 1000n * nominal);104 await helper.staking.stake(staker, 199n * nominal);105 // check balances106 expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}, {id: 'appstake', amount: 1199n * nominal, reasons: 'All'}]);107 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 1199n * nominal, feeFrozen: 1199n * nominal});108 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);109 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(1199n * nominal);110111 // staker can unstake112 await helper.staking.unstake(staker);113 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1199n * nominal);114 const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});115 await helper.wait.forParachainBlockNumber(pendingUnstake.block);116117 // check balances118 expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}]);119 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 200n * nominal, feeFrozen: 200n * nominal});120 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);121 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);122123 // staker can transfer balances now124 await helper.balance.transferToSubstrate(staker, donor.address, 900n * nominal);125 });9312694 itSub('should reject transaction if stake amount is more than total free balance minus frozen', async ({helper}) => {127 itSub('should not allow to stake(), if stake amount is more than total free balance minus locked by staking', async ({helper}) => {95 const staker = accounts.pop()!;128 const staker = accounts.pop()!;9612997 // Can't stake full balance because Alice needs to pay some fee130 // Can't stake full balance because Alice needs to pay some fee98 await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected;131 await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected; // With('Arithmetic')99 await helper.staking.stake(staker, 500n * nominal);132 await helper.staking.stake(staker, 500n * nominal);100133101 // Can't stake 500 tkn because Alice has Less than 500 transferable;134 // Can't stake 500 tkn because Alice has Less than 500 transferable;102 await expect(helper.staking.stake(staker, 500n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions');135 await expect(helper.staking.stake(staker, 500n * nominal)).to.be.rejected; // With('Arithmetic');103 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);136 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);104 });137 });105138115 });148 });116149117 describe('unstake extrinsic', () => {150 describe('unstake extrinsic', () => {118 itSub('should change balance state from "frozen" to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async ({helper}) => {151 itSub('should move tokens to "pendingUnstake" map and subtract it from totalStaked', async ({helper}) => {119 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];152 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];120 const totalStakedBefore = await helper.staking.getTotalStaked();153 const totalStakedBefore = await helper.staking.getTotalStaked();121 await helper.staking.stake(staker, 900n * nominal);154 await helper.staking.stake(staker, 900n * nominal);122 await helper.staking.unstake(staker);155 await helper.staking.unstake(staker);123156124 // Right after unstake balance is reserved157 // Right after unstake tokens are still locked125 // Staker can not transfer158 expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'appstake', amount: 900n * nominal, reasons: 'All'}]);126 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 900n * nominal, miscFrozen: 0n, feeFrozen: 0n});159 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 900n * nominal, feeFrozen: 900n * nominal});160 // Staker can not transfer127 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith('balances.InsufficientBalance');161 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions');128 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(900n * nominal);162 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(900n * nominal);129 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);163 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);130 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);164 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);171 expect(stakes).to.be.deep.equal([]);205 expect(stakes).to.be.deep.equal([]);172 expect(pendingUnstake[0].amount).to.equal(600n * nominal);206 expect(pendingUnstake[0].amount).to.equal(600n * nominal);173207174 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 600n * nominal, feeFrozen: 0n, miscFrozen: 0n});208 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 600n * nominal, miscFrozen: 600n * nominal});209 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);175 await helper.wait.forParachainBlockNumber(pendingUnstake[0].block);210 await helper.wait.forParachainBlockNumber(pendingUnstake[0].block);176 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});211 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});177 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);212 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);209 expect(unstakingPerBlock[1].amount).to.equal(120n * nominal);244 expect(unstakingPerBlock[1].amount).to.equal(120n * nominal);210 });245 });211246212 itSub('should be possible for different accounts in one block', async ({helper}) => {247 itSub('should be possible for 3 accounts in one block', async ({helper}) => {213 const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!];248 const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!];214249215 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));250 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));221 }));256 }));222 });257 });258259 itSub('should not be possible for more than 3 accounts in one block', async ({helper}) => {260 if (!await helper.arrange.isDevNode()) {261 const stakers = await helper.arrange.createAccounts([200n,200n,200n,200n,200n,200n,200n,200n,200n,200n], donor);262263 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));264 const unstakingResults = await Promise.allSettled(stakers.map(staker => helper.staking.unstake(staker)));265266 const successfulUnstakes = unstakingResults.filter(result => result.status === 'fulfilled');267 expect(successfulUnstakes).to.have.length(3);268 }269 });223 });270 });224271225 describe('collection sponsoring', () => {272 describe('collection sponsoring', () => {598 expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});645 expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});599 });646 });600647601 itSub('should not be credited for unstaked (reserved) balance', async ({helper}) => {648 itSub('should not be credited for pending-unstaked tokens', async ({helper}) => {602 // staker unstakes before rewards has been payed649 // staker unstakes before rewards been payed603 const staker = accounts.pop()!;650 const staker = accounts.pop()!;604 await helper.staking.stake(staker, 100n * nominal);651 await helper.staking.stake(staker, 100n * nominal);605 const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});652 const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});