Skip to content
This repository was archived by the owner on Sep 28, 2023. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
45 changes: 45 additions & 0 deletions frame/dapps-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ use sp_runtime::{
RuntimeDebug,
};
use sp_std::{ops::Add, prelude::*};
use core::cmp::Ordering;

pub mod pallet;
pub mod weights;
Expand Down Expand Up @@ -408,6 +409,14 @@ pub struct UnbondingInfo<Balance: AtLeast32BitUnsigned + Default + Copy> {
unlocking_chunks: Vec<UnlockingChunk<Balance>>,
}

pub fn unlock_era_asc<Balance>(a: &UnlockingChunk<Balance>, b: &UnlockingChunk<Balance>) -> Ordering {
a.unlock_era.cmp(&b.unlock_era)
}

pub fn unlock_era_desc<Balance>(a: &UnlockingChunk<Balance>, b: &UnlockingChunk<Balance>) -> Ordering {
b.unlock_era.cmp(&a.unlock_era)
}

impl<Balance> UnbondingInfo<Balance>
where
Balance: AtLeast32BitUnsigned + Default + Copy,
Expand Down Expand Up @@ -445,6 +454,15 @@ where
}
}

fn sort<F>(&mut self, compare: F)
where
F: FnMut(&UnlockingChunk<Balance>, &UnlockingChunk<Balance>) -> Ordering
{
self
.unlocking_chunks
.sort_by(compare);
}

/// Partitions the unlocking chunks into two groups:
///
/// First group includes all chunks which have unlock era lesser or equal to the specified era.
Expand All @@ -470,6 +488,33 @@ where
)
}

fn collect_amount(self, amount: Balance) -> (Balance, Self) {
let mut remaining_chunks: Vec<UnlockingChunk<Balance>> = Default::default();
let collected_amount = self
.unlocking_chunks
.iter()
.fold(Balance::zero(), |accum, item| {
let next_accum = accum + item.amount;
if next_accum <= amount {
return next_accum;
}

if accum < amount && amount < next_accum {
let excessive_amount = next_accum - amount;
remaining_chunks.push(UnlockingChunk{
amount: excessive_amount,
unlock_era: item.unlock_era,
});
return amount;
}

remaining_chunks.push(*item);
accum
});

(collected_amount, Self { unlocking_chunks: remaining_chunks })
}

#[cfg(test)]
/// Return clone of the internal vector. Should only be used for testing.
fn vec(&self) -> Vec<UnlockingChunk<Balance>> {
Expand Down
76 changes: 76 additions & 0 deletions frame/dapps-staking/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ pub mod pallet {
BalanceOf<T>,
T::SmartContract,
),
/// Account has rebonded unlocking chunks and staked funds on a smart contract.
RebondAndStake(T::AccountId, T::SmartContract, BalanceOf<T>)
}

#[pallet::error]
Expand Down Expand Up @@ -270,6 +272,8 @@ pub mod pallet {
NotActiveStaker,
/// Transfering nomination to the same contract
NominationTransferToSameContract,
/// There are no previously unbonded funds that can be rebonded and re-staked.
NothingToRebond,
}

#[pallet::hooks]
Expand Down Expand Up @@ -570,6 +574,78 @@ pub mod pallet {
Ok(().into())
}

/// Lock up and stake unbonded chunks of origin account.
///
/// `value` must be more than the `minimum_balance` specified by `MinimumStakingAmount`
/// unless account already has bonded value equal or more than 'minimum_balance'.
///
/// The dispatch origin for this call must be _Signed_ by the staker's account.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also check top of lib.rs file and update comments there if needed.

#[pallet::weight(T::WeightInfo::rebond_and_stake())]
pub fn rebond_and_stake(
origin: OriginFor<T>,
contract_id: T::SmartContract,
#[pallet::compact] value: BalanceOf<T>,
Comment thread
shunsukew marked this conversation as resolved.
Outdated
) -> DispatchResultWithPostInfo {
Self::ensure_pallet_enabled()?;
let staker = ensure_signed(origin)?;

// Check that contract is ready for staking.
ensure!(
Self::is_active(&contract_id),
Error::<T>::NotOperatedContract
);

// Get the staking ledger or create an entry if it doesn't exist.
let mut ledger = Self::ledger(&staker);
ensure!(
!ledger.unbonding_info.is_empty(),
Error::<T>::NothingToRebond
);

// Sort chunks descending order by unlock_era, so that chunks with bigger era_index are collected with priority.
ledger.unbonding_info.sort(unlock_era_desc);
let (value_to_stake, mut remaining_chunks) = ledger.unbonding_info.collect_amount(value);
ensure!(
value_to_stake > Zero::zero(),
Error::<T>::StakingWithNoValue
);

let current_era = Self::current_era();
let mut staking_info =
Self::contract_stake_info(&contract_id, current_era).unwrap_or_default();
let mut staker_info = Self::staker_info(&staker, &contract_id);

Self::stake_on_contract(
&mut staker_info,
&mut staking_info,
value_to_stake,
current_era,
)?;

remaining_chunks.sort(unlock_era_asc);
ledger.unbonding_info = remaining_chunks;
ledger.locked = ledger.locked.saturating_add(value_to_stake);

GeneralEraInfo::<T>::mutate(&current_era, |value| {
if let Some(x) = value {
x.staked = x.staked.saturating_add(value_to_stake);
x.locked = x.locked.saturating_add(value_to_stake);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect - TVL doesn't increase after this since unbonding chunks are still considered to be locked.

UT should be updated to catch this.

}
});

Self::update_ledger(&staker, ledger);
Self::update_staker_info(&staker, &contract_id, staker_info);
ContractEraStake::<T>::insert(&contract_id, current_era, staking_info);

Self::deposit_event(Event::<T>::RebondAndStake(
staker,
contract_id,
value_to_stake,
));

Ok(().into())
}

/// Withdraw all funds that have completed the unbonding process.
///
/// If there are unbonding chunks which will be fully unbonded in future eras,
Expand Down
66 changes: 66 additions & 0 deletions frame/dapps-staking/src/testing_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,72 @@ pub(crate) fn assert_unbond_and_unstake(
assert_eq!(init_state.era_info.locked, final_state.era_info.locked);
}

pub(crate) fn assert_rebond_and_stake(
staker: AccountId,
contract_id: &MockSmartContract<AccountId>,
value: Balance,
) {
// Get latest staking info
let current_era = DappsStaking::current_era();
let init_state = MemorySnapshot::all(current_era, &contract_id, staker);
let mut init_ledger = init_state.ledger.clone();

// Define expected state after extrinsic
init_ledger.unbonding_info.sort(unlock_era_desc);
let (expected_stake_amount, mut expected_remaining_info) = init_ledger.unbonding_info.collect_amount(value);
expected_remaining_info.sort(unlock_era_asc);

// Ensure op is successful and event is emitted
assert_ok!(DappsStaking::rebond_and_stake(Origin::signed(staker), contract_id.clone(), value));
System::assert_last_event(mock::Event::DappsStaking(Event::RebondAndStake(
staker,
contract_id.clone(),
expected_stake_amount,
)));

// Fetch the latest unbonding info so we can compare it to initial unbonding info
let final_state = MemorySnapshot::all(current_era, &contract_id, staker);
let final_ledger = final_state.ledger.clone();
assert_eq!(
final_ledger.unbonding_info,
expected_remaining_info,
);
assert_eq!(
final_ledger.locked,
init_ledger.locked + expected_stake_amount
);

// In case staker hasn't been staking this contract until now
if init_state.staker_info.latest_staked_value() == 0 {
assert!(GeneralStakerInfo::<TestRuntime>::contains_key(
&staker,
contract_id
));
assert_eq!(
final_state.contract_info.number_of_stakers,
init_state.contract_info.number_of_stakers + 1
);
}

// Verify the remaining states
assert_eq!(
final_state.era_info.staked,
init_state.era_info.staked + expected_stake_amount
);
assert_eq!(
final_state.era_info.locked,
init_state.era_info.locked + expected_stake_amount
);
Comment on lines +403 to +406

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect, see my comment above.

assert_eq!(
final_state.contract_info.total,
init_state.contract_info.total + expected_stake_amount
);
assert_eq!(
final_state.staker_info.latest_staked_value(),
init_state.staker_info.latest_staked_value() + expected_stake_amount
);
}

/// Used to perform start_unbonding with success and storage assertions.
pub(crate) fn assert_withdraw_unbonded(staker: AccountId) {
let current_era = DappsStaking::current_era();
Expand Down
72 changes: 72 additions & 0 deletions frame/dapps-staking/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,78 @@ fn unbond_and_unstake_with_no_chunks_allowed() {
})
}

#[test]
fn rebond_and_stake_is_ok() {
ExternalityBuilder::build().execute_with(|| {
initialize_first_block();

let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01));
assert_register(10, &contract_id);

let staker_id = 1;
assert_bond_and_stake(staker_id, &contract_id, 1000);

let first_unbond_value = 100;
let second_unbond_value = 250;
let initial_era = DappsStaking::current_era();

// Unbond some amount in the initial era
assert_unbond_and_unstake(staker_id, &contract_id, first_unbond_value);

// Advance one era and then unbond some more
advance_to_era(initial_era + 1);
assert_unbond_and_unstake(staker_id, &contract_id, second_unbond_value);

// unbond and stake
assert_rebond_and_stake(staker_id, &contract_id, 300);

// unbond and stake again.
// this time value exceeds the total amount of unbonding chunks, but it succeeds by consuming the total amount.
assert_rebond_and_stake(staker_id, &contract_id, 200);

assert!(Ledger::<TestRuntime>::get(&staker_id)
.unbonding_info
.is_empty()
);
})
}

#[test]
fn rebond_and_stake_unexist_contract_fails() {
ExternalityBuilder::build().execute_with(|| {
initialize_first_block();

let staker_id = 1;
let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01));
assert_register(10, &contract_id);

assert_bond_and_stake(staker_id, &contract_id, 1000);
assert_unbond_and_unstake(staker_id, &contract_id, 100);

let non_exist_contract_id = MockSmartContract::Evm(H160::repeat_byte(0x02));
assert_noop!(
DappsStaking::rebond_and_stake(Origin::signed(staker_id), non_exist_contract_id, 200),
Error::<TestRuntime>::NotOperatedContract,
);
})
}

#[test]
fn rebond_and_stake_no_unbonding_chunks_fails() {
ExternalityBuilder::build().execute_with(|| {
initialize_first_block();

let staker_id = 1;
let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01));
assert_register(10, &contract_id);

assert_noop!(
DappsStaking::rebond_and_stake(Origin::signed(staker_id), contract_id, 200),
Error::<TestRuntime>::NothingToRebond,
);
})
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest also covering the error cases with InsufficientValue or TooManyEraStakeValues.

Not mandatory, just a suggestion.

#[test]
fn withdraw_unbonded_is_ok() {
ExternalityBuilder::build().execute_with(|| {
Expand Down
18 changes: 18 additions & 0 deletions frame/dapps-staking/src/tests_lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use alloc::vec;
use frame_support::assert_ok;
use mock::Balance;

Expand Down Expand Up @@ -57,6 +58,23 @@ fn unbonding_info_test() {
assert_eq!(unbonding_info.sum(), first_info.sum() + second_info.sum());
}

// #[test]
// fn unbonding_info_sort_test() {
// let mut unbonding_info = UnbondingInfo::<Balance>::default();

// // Prepare unlocking chunks.
// let count = 5;
// let base_amount: Balance = 100;
// let base_unlock_era = 4 * count;
// let mut chunks = vec![];
// for x in 1_u32..=count as u32 {
// chunks.push(UnlockingChunk {
// amount: base_amount * x as Balance,
// unlock_era: base_unlock_era - 3 * x,
// });
// }
// }

#[test]
fn staker_info_basic() {
let staker_info = StakerInfo::<Balance>::default();
Expand Down
9 changes: 9 additions & 0 deletions frame/dapps-staking/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub trait WeightInfo {
fn developer_pre_approval() -> Weight;
fn bond_and_stake() -> Weight;
fn unbond_and_unstake() -> Weight;
fn rebond_and_stake() -> Weight;
fn withdraw_unbonded() -> Weight;
fn claim_staker_without_restake() -> Weight;
fn claim_staker_with_restake() -> Weight;
Expand Down Expand Up @@ -97,6 +98,10 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
.saturating_add(T::DbWeight::get().reads(8 as Weight))
.saturating_add(T::DbWeight::get().writes(5 as Weight))
}
// TODO benchmarking
fn rebond_and_stake() -> Weight {
todo!()
}
// Storage: DappsStaking PalletDisabled (r:1 w:0)
// Storage: DappsStaking Ledger (r:1 w:1)
// Storage: DappsStaking CurrentEra (r:1 w:0)
Expand Down Expand Up @@ -248,6 +253,10 @@ impl WeightInfo for () {
.saturating_add(RocksDbWeight::get().reads(8 as Weight))
.saturating_add(RocksDbWeight::get().writes(5 as Weight))
}
// TODO benchmarking
fn rebond_and_stake() -> Weight {
todo!()
}
// Storage: DappsStaking PalletDisabled (r:1 w:0)
// Storage: DappsStaking Ledger (r:1 w:1)
// Storage: DappsStaking CurrentEra (r:1 w:0)
Expand Down