Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dapp staking v3 - part 2 #991

Merged
merged 13 commits into from
Sep 21, 2023
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.
shaunxw marked this conversation as resolved.
Show resolved Hide resolved
#[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
Loading