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

[Pending] Rebond unlocking chunks #97

Draft
wants to merge 17 commits into
base: polkadot-v0.9.29
Choose a base branch
from
32 changes: 28 additions & 4 deletions chain-extensions/dapps-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use sp_runtime::{
use chain_extension_trait::ChainExtensionExec;
use codec::{Decode, Encode};
use dapps_staking_chain_extension_types::{
Contract, DSError, DappsStakingAccountInput, DappsStakingEraInput, DappsStakingNominationInput,
DappsStakingValueInput,
Contract, ContractBytes, DSError, DappsStakingAccountInput, DappsStakingEraInput,
DappsStakingNominationInput, DappsStakingValueInput,
};
use frame_support::traits::{Currency, Get};
use frame_system::RawOrigin;
Expand Down Expand Up @@ -37,6 +37,7 @@ enum DappsStakingFunc {
ClaimDapp,
SetRewardDestination,
NominationTransfer,
RebondAndStake,
}

impl TryFrom<u32> for DappsStakingFunc {
Expand All @@ -58,6 +59,7 @@ impl TryFrom<u32> for DappsStakingFunc {
12 => Ok(DappsStakingFunc::ClaimDapp),
13 => Ok(DappsStakingFunc::SetRewardDestination),
14 => Ok(DappsStakingFunc::NominationTransfer),
15 => Ok(DappsStakingFunc::RebondAndStake),
_ => Err(DispatchError::Other(
"DappsStakingExtension: Unimplemented func_id",
)),
Expand Down Expand Up @@ -146,7 +148,7 @@ impl<T: pallet_dapps_staking::Config> ChainExtensionExec<T> for DappsStakingExte
}

DappsStakingFunc::ReadContractStake => {
let contract_bytes: [u8; 32] = env.read_as()?;
let contract_bytes: ContractBytes = env.read_as()?;
let contract = Self::decode_smart_contract(contract_bytes)?;

let base_weight = <T as frame_system::Config>::DbWeight::get().read;
Expand Down Expand Up @@ -227,7 +229,7 @@ impl<T: pallet_dapps_staking::Config> ChainExtensionExec<T> for DappsStakingExte
}

DappsStakingFunc::ClaimStaker => {
let contract_bytes: [u8; 32] = env.read_as()?;
let contract_bytes: ContractBytes = env.read_as()?;
let contract = Self::decode_smart_contract(contract_bytes)?;

let base_weight = T::WeightInfo::claim_staker_with_restake()
Expand Down Expand Up @@ -336,6 +338,28 @@ impl<T: pallet_dapps_staking::Config> ChainExtensionExec<T> for DappsStakingExte
Ok(_) => Ok(RetVal::Converging(DSError::Success as u32)),
};
}

DappsStakingFunc::RebondAndStake => {
let contract_bytes: ContractBytes = env.read_as()?;
let contract = Self::decode_smart_contract(contract_bytes)?;

let base_weight =
<T as pallet_dapps_staking::Config>::WeightInfo::rebond_and_stake();
env.charge_weight(base_weight)?;

let caller = env.ext().address().clone();
let call_result = pallet_dapps_staking::Pallet::<T>::rebond_and_stake(
RawOrigin::Signed(caller).into(),
contract,
);
Comment on lines +346 to +354
Copy link
Contributor

Choose a reason for hiding this comment

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

Was kinda confused since rebond_and_stake is called twice in a row. I do understand that these are two different calls, but still this is a bit confusing. Maybe we should add couple of comments to reduce overall WTF/minute.

Copy link
Member Author

@shunsukew shunsukew Sep 28, 2022

Choose a reason for hiding this comment

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

This can be said for other functions too. it's a more universal topic.
I thought it's very clear they are different and what they do because they are different methods, one is WeightInfo's and another is Pallet's.

return match call_result {
Err(e) => {
let mapped_error = DSError::try_from(e.error)?;
Ok(RetVal::Converging(mapped_error as u32))
}
Ok(_) => Ok(RetVal::Converging(DSError::Success as u32)),
};
}
}

Ok(RetVal::Converging(DSError::Success as u32))
Expand Down
35 changes: 20 additions & 15 deletions chain-extensions/types/dapps-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,47 @@ pub enum DSError {
Disabled = 1,
/// No change in maintenance mode
NoMaintenanceModeChange = 2,
/// Upgrade is too heavy, reduce the weight parameter.
/// Upgrade is too heavy, reduce the weight parameter
UpgradeTooHeavy = 3,
/// Can not stake with zero value.
/// Can not stake with zero value
StakingWithNoValue = 4,
/// Can not stake with value less than minimum staking value
InsufficientValue = 5,
/// Number of stakers per contract exceeded.
/// Number of stakers per contract exceeded
MaxNumberOfStakersExceeded = 6,
/// Targets must be operated contracts
NotOperatedContract = 7,
/// Contract isn't staked.
/// Contract isn't staked
NotStakedContract = 8,
/// Contract isn't unregistered.
NotUnregisteredContract = 9,
/// Unclaimed rewards should be claimed before withdrawing stake.
/// Unclaimed rewards should be claimed before withdrawing stake
UnclaimedRewardsRemaining = 10,
/// Unstaking a contract with zero value
UnstakingWithNoValue = 11,
/// There are no previously unbonded funds that can be unstaked and withdrawn.
/// There are no previously unbonded funds that can be unstaked and withdrawn
NothingToWithdraw = 12,
/// The contract is already registered by other account
AlreadyRegisteredContract = 13,
/// User attempts to register with address which is not contract
ContractIsNotValid = 14,
/// This account was already used to register contract
AlreadyUsedDeveloperAccount = 15,
/// Smart contract not owned by the account id.
/// Smart contract not owned by the account id
NotOwnedContract = 16,
/// Report issue on github if this is ever emitted
UnknownEraReward = 17,
/// Report issue on github if this is ever emitted
UnexpectedStakeInfoEra = 18,
/// Contract has too many unlocking chunks. Withdraw the existing chunks if possible
/// or wait for current chunks to complete unlocking process to withdraw them.
/// or wait for current chunks to complete unlocking process to withdraw them
TooManyUnlockingChunks = 19,
/// Contract already claimed in this era and reward is distributed
AlreadyClaimedInThisEra = 20,
/// Era parameter is out of bounds
EraOutOfBounds = 21,
/// Too many active `EraStake` values for (staker, contract) pairing.
/// Claim existing rewards to fix this problem.
/// Too many active `EraStake` values for (staker, contract) pairing
/// Claim existing rewards to fix this problem
TooManyEraStakeValues = 22,
/// To register a contract, pre-approval is needed for this address
RequiredContractPreApproval = 23,
Expand All @@ -65,6 +65,8 @@ pub enum DSError {
NominationTransferToSameContract = 26,
/// Unexpected reward destination value
RewardDestinationValueOutOfBounds = 27,
/// There are no previously unbonded funds that can be reboneded and staked
NothingToRebond = 28,
/// Unknown error
UnknownError = 99,
}
Expand Down Expand Up @@ -106,6 +108,7 @@ impl TryFrom<DispatchError> for DSError {
Some("NominationTransferToSameContract") => {
Ok(DSError::NominationTransferToSameContract)
}
Some("NothingToRebond") => Ok(DSError::NothingToRebond),
_ => Ok(DSError::UnknownError),
};
}
Expand All @@ -120,27 +123,29 @@ pub enum Contract<Account> {
Wasm(Account),
}

pub type ContractBytes = [u8; 32];
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this type is in public it would be a good idea to add some explanatory comment. What is this type, why it 32 bytes long, what is the purpose of that type, etc.


#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen)]
pub struct DappsStakingValueInput<Balance> {
pub contract: [u8; 32],
pub contract: ContractBytes,
pub value: Balance,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen)]
pub struct DappsStakingAccountInput {
pub contract: [u8; 32],
pub contract: ContractBytes,
pub staker: [u8; 32],
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen)]
pub struct DappsStakingEraInput {
pub contract: [u8; 32],
pub contract: ContractBytes,
pub era: u32,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen)]
pub struct DappsStakingNominationInput<Balance> {
pub origin_contract: [u8; 32],
pub target_contract: [u8; 32],
pub origin_contract: ContractBytes,
pub target_contract: ContractBytes,
pub value: Balance,
}
21 changes: 21 additions & 0 deletions frame/dapps-staking/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,27 @@ benchmarks! {
assert_last_event::<T>(Event::<T>::NominationTransfer(staker, origin_contract_id, T::MinimumStakingAmount::get(), target_contract_id).into());
}

rebond_and_stake {
initialize::<T>();

let (_, contract_id) = register_contract::<T>(1)?;
prepare_bond_and_stake::<T>(T::MaxNumberOfStakersPerContract::get() - 1, &contract_id, SEED)?;

let staker = whitelisted_caller();
let _ = T::Currency::make_free_balance_be(&staker, BalanceOf::<T>::max_value());
let stake_amount = BalanceOf::<T>::max_value() / 2u32.into();
let unstake_amount = stake_amount / 2u32.into();

// rebond_and_stake works only when user has unlocking_chunks.
// before executing it, need to prepare the valid state by bond and unbond.
// unbonded funds remain as unlocking_chunks unless it is withdrawn.
DappsStaking::<T>::bond_and_stake(RawOrigin::Signed(staker.clone()).into(), contract_id.clone(), stake_amount)?;
DappsStaking::<T>::unbond_and_unstake(RawOrigin::Signed(staker.clone()).into(), contract_id.clone(), unstake_amount)?;
}: _(RawOrigin::Signed(staker.clone()), contract_id.clone())
verify {
assert_last_event::<T>(Event::<T>::RebondAndStake(staker, contract_id, unstake_amount).into());
}

claim_staker_with_restake {
initialize::<T>();
let (_, contract_id) = register_contract::<T>(1)?;
Expand Down
62 changes: 62 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,64 @@ pub mod pallet {
Ok(().into())
}

/// Lock up and stake unbonded chunks of origin account.
///
/// All unbonding chunks will be used and staked to the specified contract.
///
/// The dispatch origin for this call must be _Signed_ by the staker's account.
Copy link
Member

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,
) -> 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);
let value_to_stake = ledger.unbonding_info.sum();
ensure!(value_to_stake > Zero::zero(), Error::<T>::NothingToRebond);

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,
)?;

ledger.unbonding_info.unlocking_chunks = Vec::<UnlockingChunk<BalanceOf<T>>>::default();

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
Member

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
61 changes: 61 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,67 @@ 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>,
) {
// Get latest staking info
let current_era = DappsStaking::current_era();
let init_state = MemorySnapshot::all(current_era, &contract_id, staker);

// Define expected stake amount
let expected_stake_amount = init_state.ledger.unbonding_info.sum();

// Ensure op is successful and event is emitted
assert_ok!(DappsStaking::rebond_and_stake(
Origin::signed(staker),
contract_id.clone(),
));
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);
assert!(final_state.ledger.unbonding_info.is_empty());

// locked amount before and after the operation should be the same.
// unlocking chunks are still locked unless it is withdrawn.
assert_eq!(final_state.ledger.locked, init_state.ledger.locked);

// 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
Member

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