git.delta.rocks / unique-network / refs/commits / f1a5ba762b0b

difftreelog

Merge pull request #867 from UniqueNetwork/feature/appPromoReservationBalance

Yaroslav Bolyukin2023-02-02parents: #d6786e1 #2380650.patch.diff
in: master
Feature/app promo reservation balance

5 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5803,11 +5803,12 @@
 
 [[package]]
 name = "pallet-app-promotion"
-version = "0.1.3"
+version = "0.1.4"
 dependencies = [
  "frame-benchmarking",
  "frame-support",
  "frame-system",
+ "log",
  "pallet-balances",
  "pallet-common",
  "pallet-configuration",
modifiedpallets/app-promotion/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/app-promotion/CHANGELOG.md
+++ b/pallets/app-promotion/CHANGELOG.md
@@ -4,6 +4,13 @@
 
 <!-- bureaucrate goes here -->
 
+## [0.1.4] - 2023-01-31
+
+### Changed
+
+- Balance reservation when calling the unstake method is removed.
+  Now the locked balance (by AppPromo) does not change until the unlock interval expires.
+
 ## [0.1.3] - 2022-12-25
 
 ### Fixed
modifiedpallets/app-promotion/Cargo.tomldiffbeforeafterboth
--- a/pallets/app-promotion/Cargo.toml
+++ b/pallets/app-promotion/Cargo.toml
@@ -9,7 +9,7 @@
 license = 'GPLv3'
 name = 'pallet-app-promotion'
 repository = 'https://github.com/UniqueNetwork/unique-chain'
-version = '0.1.3'
+version = '0.1.4'
 
 [package.metadata.docs.rs]
 targets = ['x86_64-unknown-linux-gnu']
@@ -67,3 +67,6 @@
 # [dev-dependencies]
 
 ################################################################################
+# Other
+
+log = { version = "0.4.16", default-features = false }
\ No newline at end of file
modifiedpallets/app-promotion/src/lib.rsdiffbeforeafterboth
97 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::*;
103103
234 QueryKind = ValueQuery,234 QueryKind = ValueQuery,
235 >;235 >;
236236
237 /// 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>;
244244
245 /// 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>;
264
265 #[pallet::storage]
266 pub(crate) type UpgradedToReserves<T: Config> =
267 StorageValue<Value = bool, QueryKind = ValueQuery>;
264268
265 #[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> {
276280
277 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 }
284289
285 <T as Config>::WeightInfo::on_initialize(counter)290 <T as Config>::WeightInfo::on_initialize(counter)
286 }291 }
292
293 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 };
299
300 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 });
316
317 consumed_weight
318 }
319
320 #[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();
329
330 <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 });
342
343 Ok(pre_state.encode())
344 }
345
346 #[cfg(feature = "try-runtime")]
347 fn post_upgrade(pre_state: Vec<u8>) -> Result<(), &'static str> {
348 use sp_std::collections::btree_map::BTreeMap;
349
350 if <UpgradedToReserves<T>>::get() {
351 return Ok(());
352 }
353
354 ensure!(
355 <PendingUnstake<T>>::iter().collect::<Vec<_>>().len() == 0,
356 "pendingUnstake storage isn't empty"
357 );
358
359 let mut is_ok = true;
360
361 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_promo
372 );
373 }
374 }
375
376 if is_ok {
377 Ok(())
378 } else {
379 Err("Incorrect balance for some of stakers... See logs")
380 }
381 }
287 }382 }
288383
289 #[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);
341436
342 // 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 amount
346 WithdrawReasons::all(),440 <= match Self::get_locked_balance(&staker_id) {
347 balance441 Some(lock) => balance
348 .checked_sub(&amount)442 .checked_sub(&lock.amount)
349 .ok_or(ArithmeticError::Underflow)?,443 .ok_or(ArithmeticError::Underflow)?,
444 None => balance,
445 },
446 ArithmeticError::Underflow
350 )?;447 );
351448
352 Self::add_lock_balance(&staker_id, amount)?;449 Self::add_lock_balance(&staker_id, amount)?;
353450
428525
429 <PendingUnstake<T>>::insert(block, pendings);526 <PendingUnstake<T>>::insert(block, pendings);
430
431 Self::unlock_balance(&staker_id, total_staked)?;
432
433 <<T as Config>::Currency as ReservableCurrency<T::AccountId>>::reserve(
434 &staker_id,
435 total_staked,
436 )?;
437527
438 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 }
721
722 /// 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)?;
730
731 // 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_balance
736 .checked_sub(&amount)
737 .ok_or(ArithmeticError::Underflow)?,
738 );
739 Ok(())
740 }
741811
742 /// Adds the balance to locked by the pallet.812 /// Adds the balance to locked by the pallet.
743 ///813 ///
modifiedtests/src/sub/appPromotion/appPromotion.test.tsdiffbeforeafterboth
--- a/tests/src/sub/appPromotion/appPromotion.test.ts
+++ b/tests/src/sub/appPromotion/appPromotion.test.ts
@@ -91,15 +91,48 @@
       expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);
     });
 
-    itSub('should reject transaction if stake amount is more than total free balance minus frozen', async ({helper}) => {
+    itSub('should allow to stake() if balance is locked with different id', async ({helper}) => {
+      const staker = accounts.pop()!;
+
+      // staker has tokens locked with vesting id:
+      await helper.balance.vestedTransfer(donor, staker.address, {start: 0n, period: 1n, periodCount: 1n, perPeriod: 200n * nominal});
+      expect(await helper.balance.getSubstrateFull(staker.address))
+        .to.deep.contain({free: 1200n * nominal, miscFrozen: 200n * nominal, feeFrozen: 200n * nominal, reserved: 0n});
+
+      // Locked balance can be staked. staker can stake 1200 tokens (minus fee):
+      await helper.staking.stake(staker, 1000n * nominal);
+      await helper.staking.stake(staker, 199n * nominal);
+      // check balances
+      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}, {id: 'appstake', amount: 1199n * nominal, reasons: 'All'}]);
+      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 1199n * nominal, feeFrozen: 1199n * nominal});
+      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);
+      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(1199n * nominal);
+
+      // staker can unstake
+      await helper.staking.unstake(staker);
+      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1199n * nominal);
+      const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});
+      await helper.wait.forParachainBlockNumber(pendingUnstake.block);
+
+      // check balances
+      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}]);
+      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 200n * nominal, feeFrozen: 200n * nominal});
+      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);
+      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);
+
+      // staker can transfer balances now
+      await helper.balance.transferToSubstrate(staker, donor.address, 900n * nominal);
+    });
+
+    itSub('should not allow to stake(), if stake amount is more than total free balance minus locked by staking', async ({helper}) => {
       const staker = accounts.pop()!;
 
       // Can't stake full balance because Alice needs to pay some fee
-      await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected;
+      await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected; // With('Arithmetic')
       await helper.staking.stake(staker, 500n * nominal);
 
       // Can't stake 500 tkn because Alice has Less than 500 transferable;
-      await expect(helper.staking.stake(staker, 500n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions');
+      await expect(helper.staking.stake(staker, 500n * nominal)).to.be.rejected; // With('Arithmetic');
       expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);
     });
 
@@ -115,16 +148,17 @@
   });
 
   describe('unstake extrinsic', () => {
-    itSub('should change balance state from "frozen" to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async ({helper}) => {
+    itSub('should move tokens to "pendingUnstake" map and subtract it from totalStaked', async ({helper}) => {
       const [staker, recepient] = [accounts.pop()!, accounts.pop()!];
       const totalStakedBefore = await helper.staking.getTotalStaked();
       await helper.staking.stake(staker, 900n * nominal);
       await helper.staking.unstake(staker);
 
-      // Right after unstake balance is reserved
+      // Right after unstake tokens are still locked
+      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'appstake', amount: 900n * nominal, reasons: 'All'}]);
+      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 900n * nominal, feeFrozen: 900n * nominal});
       // Staker can not transfer
-      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 900n * nominal, miscFrozen: 0n, feeFrozen: 0n});
-      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith('balances.InsufficientBalance');
+      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions');
       expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(900n * nominal);
       expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);
       expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);
@@ -171,7 +205,8 @@
       expect(stakes).to.be.deep.equal([]);
       expect(pendingUnstake[0].amount).to.equal(600n * nominal);
 
-      expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 600n * nominal, feeFrozen: 0n, miscFrozen: 0n});
+      expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 600n * nominal, miscFrozen: 600n * nominal});
+      expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);
       await helper.wait.forParachainBlockNumber(pendingUnstake[0].block);
       expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});
       expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);
@@ -209,7 +244,7 @@
       expect(unstakingPerBlock[1].amount).to.equal(120n * nominal);
     });
 
-    itSub('should be possible for different accounts in one block', async ({helper}) => {
+    itSub('should be possible for 3 accounts in one block', async ({helper}) => {
       const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!];
 
       await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));
@@ -220,6 +255,18 @@
         expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);
       }));
     });
+
+    itSub('should not be possible for more than 3 accounts in one block', async ({helper}) => {
+      if (!await helper.arrange.isDevNode()) {
+        const stakers = await helper.arrange.createAccounts([200n,200n,200n,200n,200n,200n,200n,200n,200n,200n], donor);
+
+        await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));
+        const unstakingResults = await Promise.allSettled(stakers.map(staker => helper.staking.unstake(staker)));
+
+        const successfulUnstakes = unstakingResults.filter(result => result.status === 'fulfilled');
+        expect(successfulUnstakes).to.have.length(3);
+      }
+    });
   });
 
   describe('collection sponsoring', () => {
@@ -598,8 +645,8 @@
       expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});
     });
 
-    itSub('should not be credited for unstaked (reserved) balance', async ({helper}) => {
-      // staker unstakes before rewards has been payed
+    itSub('should not be credited for pending-unstaked tokens', async ({helper}) => {
+      // staker unstakes before rewards been payed
       const staker = accounts.pop()!;
       await helper.staking.stake(staker, 100n * nominal);
       const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});