Skip to content

Commit

Permalink
Feat/dapp staking v3 phase2 (#991)
Browse files Browse the repository at this point in the history
* Phase2 progress

* Adapt for 0.9.43

* Abstract sparse vector type

* Further modifications

* claim unlocked functionality WIP

* claim unlocked tested

* Relock unlocking

* Check era when adding chunk

* Custom error for some types

* Additional type tests - WIP

* More type tests

* Review comments

* Additional changes
  • Loading branch information
Dinonard authored Sep 21, 2023
1 parent a64cd9b commit 6a36db1
Show file tree
Hide file tree
Showing 8 changed files with 2,111 additions and 108 deletions.
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pallets/dapp-staking-v3/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ sp-io = { workspace = true }
sp-runtime = { workspace = true }
sp-std = { workspace = true }

astar-primitives = { workspace = true }

[dev-dependencies]
pallet-balances = { workspace = true }

Expand All @@ -39,4 +41,5 @@ std = [
"frame-support/std",
"frame-system/std",
"pallet-balances/std",
"astar-primitives/std",
]
179 changes: 166 additions & 13 deletions pallets/dapp-staking-v3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ use frame_support::{
use frame_system::pallet_prelude::*;
use sp_runtime::traits::{BadOrigin, Saturating, Zero};

use astar_primitives::Balance;

use crate::types::*;
pub use pallet::*;

Expand All @@ -65,7 +67,6 @@ pub mod pallet {
const STORAGE_VERSION: StorageVersion = StorageVersion::new(5);

#[pallet::pallet]
#[pallet::generate_store(pub(crate) trait Store)]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);

Expand All @@ -75,7 +76,12 @@ pub mod pallet {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

/// Currency used for staking.
type Currency: LockableCurrency<Self::AccountId, Moment = Self::BlockNumber>;
/// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :)
type Currency: LockableCurrency<
Self::AccountId,
Moment = Self::BlockNumber,
Balance = Balance,
>;

/// Describes smart contract in the context required by dApp staking.
type SmartContract: Parameter + Member + MaxEncodedLen;
Expand All @@ -89,6 +95,7 @@ pub mod pallet {
type MaxNumberOfContracts: Get<DAppId>;

/// Maximum number of locked chunks that can exist per account at a time.
// TODO: should this just be hardcoded to 2? Nothing else makes sense really - current era and next era are required.
#[pallet::constant]
type MaxLockedChunks: Get<u32>;

Expand All @@ -98,7 +105,15 @@ pub mod pallet {

/// Minimum amount an account has to lock in dApp staking in order to participate.
#[pallet::constant]
type MinimumLockedAmount: Get<BalanceOf<Self>>;
type MinimumLockedAmount: Get<Balance>;

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

/// Maximum number of staking chunks that can exist per account at a time.
#[pallet::constant]
type MaxStakingChunks: Get<u32>;
}

#[pallet::event]
Expand Down Expand Up @@ -128,7 +143,22 @@ pub mod pallet {
/// Account has locked some amount into dApp staking.
Locked {
account: T::AccountId,
amount: BalanceOf<T>,
amount: Balance,
},
/// Account has started the unlocking process for some amount.
Unlocking {
account: T::AccountId,
amount: Balance,
},
/// Account has claimed unlocked amount, removing the lock from it.
ClaimedUnlocked {
account: T::AccountId,
amount: Balance,
},
/// Account has relocked all of the unlocking chunks.
Relock {
account: T::AccountId,
amount: Balance,
},
}

Expand All @@ -155,6 +185,14 @@ 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,
/// There are no eligible unlocked chunks to claim. This can happen either if no eligible chunks exist, or if user has no chunks at all.
NoUnlockedChunksToClaim,
/// There are no unlocking chunks available to relock.
NoUnlockingChunks,
}

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

/// General information about the current era.
#[pallet::storage]
pub type CurrentEraInfo<T: Config> = StorageValue<_, EraInfo<BalanceOf<T>>, ValueQuery>;
pub type CurrentEraInfo<T: Config> = StorageValue<_, EraInfo, ValueQuery>;

#[pallet::call]
impl<T: Config> Pallet<T> {
Expand Down Expand Up @@ -376,10 +414,7 @@ pub mod pallet {
/// caller should claim pending rewards, before retrying to lock additional funds.
#[pallet::call_index(5)]
#[pallet::weight(Weight::zero())]
pub fn lock(
origin: OriginFor<T>,
#[pallet::compact] amount: BalanceOf<T>,
) -> DispatchResult {
pub fn lock(origin: OriginFor<T>, #[pallet::compact] amount: Balance) -> DispatchResult {
Self::ensure_pallet_enabled()?;
let account = ensure_signed(origin)?;

Expand All @@ -388,7 +423,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 +433,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 +449,124 @@ 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: Balance) -> 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(())
}

/// Claims all of fully unlocked chunks, removing the lock from them.
#[pallet::call_index(7)]
#[pallet::weight(Weight::zero())]
pub fn claim_unlocked(origin: OriginFor<T>) -> DispatchResult {
Self::ensure_pallet_enabled()?;
let account = ensure_signed(origin)?;

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

let current_block = frame_system::Pallet::<T>::block_number();
let amount = ledger.claim_unlocked(current_block);
ensure!(amount > Zero::zero(), Error::<T>::NoUnlockedChunksToClaim);

Self::update_ledger(&account, ledger);
CurrentEraInfo::<T>::mutate(|era_info| {
era_info.unlocking_removed(amount);
});

// TODO: We should ensure user doesn't unlock everything if they still have storage leftovers (e.g. unclaimed rewards?)

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

Ok(())
}

#[pallet::call_index(8)]
#[pallet::weight(Weight::zero())]
pub fn relock_unlocking(origin: OriginFor<T>) -> DispatchResult {
Self::ensure_pallet_enabled()?;
let account = ensure_signed(origin)?;

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

ensure!(!ledger.unlocking.is_empty(), Error::<T>::NoUnlockingChunks);

// Only lock for the next era onwards.
let lock_era = state.era.saturating_add(1);
let amount = ledger.consume_unlocking_chunks();

ledger
.add_lock_amount(amount, lock_era)
.map_err(|_| Error::<T>::TooManyLockedBalanceChunks)?;
ensure!(
ledger.active_locked_amount() >= T::MinimumLockedAmount::get(),
Error::<T>::LockedAmountBelowThreshold
);

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

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

Ok(())
}
}

impl<T: Config> Pallet<T> {
Expand Down Expand Up @@ -450,7 +603,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
29 changes: 26 additions & 3 deletions pallets/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 @@ -58,7 +58,7 @@ construct_runtime!(
parameter_types! {
pub const BlockHashCount: u64 = 250;
pub BlockWeights: frame_system::limits::BlockWeights =
frame_system::limits::BlockWeights::simple_max(Weight::from_ref_time(1024));
frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0));
}

impl frame_system::Config for Test {
Expand Down Expand Up @@ -97,6 +97,10 @@ impl pallet_balances::Config for Test {
type DustRemoval = ();
type ExistentialDeposit = ConstU128<EXISTENTIAL_DEPOSIT>;
type AccountStore = System;
type HoldIdentifier = ();
type FreezeIdentifier = ();
type MaxHolds = ConstU32<0>;
type MaxFreezes = ConstU32<0>;
type WeightInfo = ();
}

Expand All @@ -108,7 +112,9 @@ impl pallet_dapp_staking::Config for Test {
type MaxNumberOfContracts = ConstU16<10>;
type MaxLockedChunks = ConstU32<5>;
type MaxUnlockingChunks = ConstU32<5>;
type MaxStakingChunks = ConstU32<8>;
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 +150,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 +178,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 +189,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);
}
Loading

0 comments on commit 6a36db1

Please sign in to comment.