Skip to content
This repository has been archived by the owner on Sep 28, 2023. It is now read-only.

dApp staking v3 - phase2 #164

Draft
wants to merge 4 commits into
base: feat/dapp-staking-v3
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 80 additions & 4 deletions frame/dapp-staking-v3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ pub mod pallet {
/// Minimum amount an account has to lock in dApp staking in order to participate.
#[pallet::constant]
type MinimumLockedAmount: Get<BalanceOf<Self>>;

/// Amount of blocks that need to pass before unlocking chunks can be claimed by the owner.
#[pallet::constant]
type UnlockingPeriod: Get<BlockNumberFor<Self>>;
}

#[pallet::event]
Expand Down Expand Up @@ -130,6 +134,12 @@ pub mod pallet {
account: T::AccountId,
amount: BalanceOf<T>,
},
// TODO: do we also add unlocking block info to the event?
/// Account has started the unlocking process for some amount.
Unlocking {
account: T::AccountId,
amount: BalanceOf<T>,
},
}

#[pallet::error]
Expand All @@ -155,6 +165,10 @@ pub mod pallet {
LockedAmountBelowThreshold,
/// Cannot add additional locked balance chunks due to size limit.
TooManyLockedBalanceChunks,
/// Cannot add additional unlocking chunks due to size limit
TooManyUnlockingChunks,
/// Remaining stake prevents entire balance of starting the unlocking process.
RemainingStakePreventsFullUnlock,
}

/// General information about dApp staking protocol state.
Expand Down Expand Up @@ -388,7 +402,7 @@ pub mod pallet {

// Calculate & check amount available for locking
let available_balance =
T::Currency::free_balance(&account).saturating_sub(ledger.locked_amount());
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::<T>::ZeroAmount);

Expand All @@ -398,13 +412,13 @@ pub mod pallet {
.add_lock_amount(amount_to_lock, lock_era)
.map_err(|_| Error::<T>::TooManyLockedBalanceChunks)?;
ensure!(
ledger.locked_amount() >= T::MinimumLockedAmount::get(),
ledger.active_locked_amount() >= T::MinimumLockedAmount::get(),
Error::<T>::LockedAmountBelowThreshold
);

Self::update_ledger(&account, ledger);
CurrentEraInfo::<T>::mutate(|era_info| {
era_info.total_locked.saturating_accrue(amount_to_lock);
era_info.add_locked(amount_to_lock);
});

Self::deposit_event(Event::<T>::Locked {
Expand All @@ -414,6 +428,68 @@ pub mod pallet {

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<T>,
#[pallet::compact] amount: BalanceOf<T>,
) -> DispatchResult {
Self::ensure_pallet_enabled()?;
let account = ensure_signed(origin)?;

let state = ActiveProtocolState::<T>::get();
let mut ledger = Ledger::<T>::get(&account);

let available_for_unlocking = ledger.unlockable_amount(state.period);
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).is_zero(),
Error::<T>::RemainingStakePreventsFullUnlock
);
ledger.active_locked_amount()
} else {
amount_to_unlock
};

// Sanity check
ensure!(!amount_to_unlock.is_zero(), Error::<T>::ZeroAmount);

// Update ledger with new lock and unlocking amounts
ledger
.subtract_lock_amount(amount_to_unlock, state.era)
.map_err(|_| Error::<T>::TooManyLockedBalanceChunks)?;

let current_block = frame_system::Pallet::<T>::block_number();
let unlock_block = current_block.saturating_add(T::UnlockingPeriod::get());
ledger
.add_unlocking_chunk(amount_to_unlock, unlock_block)
.map_err(|_| Error::<T>::TooManyUnlockingChunks)?;

// Update storage
Self::update_ledger(&account, ledger);
CurrentEraInfo::<T>::mutate(|era_info| {
era_info.unlocking_started(amount_to_unlock);
});

Self::deposit_event(Event::<T>::Unlocking {
account,
amount: amount_to_unlock,
});

Ok(())
}
}

impl<T: Config> Pallet<T> {
Expand Down Expand Up @@ -450,7 +526,7 @@ pub mod pallet {
T::Currency::set_lock(
STAKING_ID,
account,
ledger.locked_amount(),
ledger.active_locked_amount(),
WithdrawReasons::all(),
);
Ledger::<T>::insert(account, ledger);
Expand Down
22 changes: 20 additions & 2 deletions frame/dapp-staking-v3/src/test/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::{self as pallet_dapp_staking, *};

use frame_support::{
construct_runtime, parameter_types,
traits::{ConstU128, ConstU16, ConstU32},
traits::{ConstU128, ConstU16, ConstU32, ConstU64},
weights::Weight,
};
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
Expand Down Expand Up @@ -109,6 +109,7 @@ impl pallet_dapp_staking::Config for Test {
type MaxLockedChunks = ConstU32<5>;
type MaxUnlockingChunks = ConstU32<5>;
type MinimumLockedAmount = ConstU128<MINIMUM_LOCK_AMOUNT>;
type UnlockingPeriod = ConstU64<20>;
}

#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)]
Expand Down Expand Up @@ -144,6 +145,15 @@ impl ExtBuilder {
ext.execute_with(|| {
System::set_block_number(1);
DappStaking::on_initialize(System::block_number());

// TODO: remove this after proper on_init handling is implemented
pallet_dapp_staking::ActiveProtocolState::<Test>::put(ProtocolState {
era: 1,
next_era_start: BlockNumber::from(101_u32),
period: 1,
period_type: PeriodType::Voting(16),
maintenance: false,
});
});

ext
Expand All @@ -163,7 +173,7 @@ pub(crate) fn _run_to_block(n: u64) {

/// Run for the specified number of blocks.
/// Function assumes first block has been initialized.
pub(crate) fn _run_for_blocks(n: u64) {
pub(crate) fn run_for_blocks(n: u64) {
_run_to_block(System::block_number() + n);
}

Expand All @@ -174,3 +184,11 @@ pub(crate) fn advance_to_era(era: EraNumber) {
// TODO: Properly implement this later when additional logic has been implemented
ActiveProtocolState::<Test>::mutate(|state| state.era = era);
}

/// 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) {
// TODO: Properly implement this later when additional logic has been implemented
ActiveProtocolState::<Test>::mutate(|state| state.period = period);
}
102 changes: 97 additions & 5 deletions frame/dapp-staking-v3/src/test/testing_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@
// along with Astar. If not, see <http://www.gnu.org/licenses/>.

use crate::test::mock::*;
use crate::*;
use crate::types::*;
use crate::{
pallet as pallet_dapp_staking, ActiveProtocolState, BlockNumberFor, CurrentEraInfo, DAppId,
Event, IntegratedDApps, Ledger, NextDAppId,
};

use frame_support::assert_ok;
use frame_support::{assert_ok, traits::Get};
use sp_runtime::traits::Zero;
use std::collections::HashMap;

/// Helper struct used to store the entire pallet state snapshot.
Expand All @@ -29,7 +34,7 @@ pub(crate) struct MemorySnapshot {
next_dapp_id: DAppId,
current_era_info: EraInfo<BalanceOf<Test>>,
integrated_dapps: HashMap<
<Test as pallet::Config>::SmartContract,
<Test as pallet_dapp_staking::Config>::SmartContract,
DAppInfo<<Test as frame_system::Config>::AccountId>,
>,
ledger: HashMap<<Test as frame_system::Config>::AccountId, AccountLedgerFor<Test>>,
Expand All @@ -52,7 +57,7 @@ impl MemorySnapshot {
pub fn locked_balance(&self, account: &AccountId) -> Balance {
self.ledger
.get(&account)
.map_or(Balance::zero(), |ledger| ledger.locked_amount())
.map_or(Balance::zero(), |ledger| ledger.active_locked_amount())
}
}

Expand Down Expand Up @@ -196,7 +201,7 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) {
.ledger
.get(&account)
.expect("Ledger entry has to exist after succcessful lock call")
.era(),
.lock_era(),
post_snapshot.active_protocol_state.era + 1
);

Expand All @@ -211,3 +216,90 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) {
"Active era locked amount should remain exactly the same."
);
}

/// Lock funds into dApp staking and assert success.
pub(crate) fn assert_unlock(account: AccountId, amount: Balance) {
let pre_snapshot = MemorySnapshot::new();

assert!(
pre_snapshot.ledger.contains_key(&account),
"Cannot unlock for non-existing ledger."
);

// Calculate expected unlock amount
let pre_ledger = &pre_snapshot.ledger[&account];
let expected_unlock_amount = {
// Cannot unlock more than is available
let possible_unlock_amount = pre_ledger
.unlockable_amount(pre_snapshot.active_protocol_state.period)
.min(amount);

// When unlocking would take accounn below the minimum lock threshold, unlock everything
let locked_amount = pre_ledger.active_locked_amount();
let min_locked_amount = <Test as pallet_dapp_staking::Config>::MinimumLockedAmount::get();
if locked_amount.saturating_sub(possible_unlock_amount) < min_locked_amount {
locked_amount
} else {
possible_unlock_amount
}
};

// Unlock funds
assert_ok!(DappStaking::unlock(RuntimeOrigin::signed(account), amount,));
System::assert_last_event(RuntimeEvent::DappStaking(Event::Unlocking {
account,
amount: expected_unlock_amount,
}));

// Verify post-state
let post_snapshot = MemorySnapshot::new();

// Verify ledger is as expected
let period_number = pre_snapshot.active_protocol_state.period;
let post_ledger = &post_snapshot.ledger[&account];
assert_eq!(
pre_ledger.active_locked_amount(),
post_ledger.active_locked_amount() + expected_unlock_amount,
"Active locked amount should be decreased by the amount unlocked."
);
assert_eq!(
pre_ledger.unlocking_amount() + expected_unlock_amount,
post_ledger.unlocking_amount(),
"Total unlocking amount should be increased by the amount unlocked."
);
assert_eq!(
pre_ledger.total_locked_amount(),
post_ledger.total_locked_amount(),
"Total locked amount should remain exactly the same since the unlocking chunks are still locked."
);
assert_eq!(
pre_ledger.unlockable_amount(period_number),
post_ledger.unlockable_amount(period_number) + expected_unlock_amount,
"Unlockable amount should be decreased by the amount unlocked."
);

// In case ledger is empty, it should have been removed from the storage
if post_ledger.is_empty() {
assert!(!Ledger::<Test>::contains_key(&account));
}

// Verify era info post-state
let pre_era_info = &pre_snapshot.current_era_info;
let post_era_info = &post_snapshot.current_era_info;
assert_eq!(
pre_era_info.unlocking + expected_unlock_amount,
post_era_info.unlocking
);
assert_eq!(
pre_era_info
.total_locked
.saturating_sub(expected_unlock_amount),
post_era_info.total_locked
);
assert_eq!(
pre_era_info
.active_era_locked
.saturating_sub(expected_unlock_amount),
post_era_info.active_era_locked
);
}
Loading