diff --git a/chain-extensions/dapps-staking/src/lib.rs b/chain-extensions/dapps-staking/src/lib.rs index 863db5c8..57a5539a 100644 --- a/chain-extensions/dapps-staking/src/lib.rs +++ b/chain-extensions/dapps-staking/src/lib.rs @@ -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; @@ -37,6 +37,7 @@ enum DappsStakingFunc { ClaimDapp, SetRewardDestination, NominationTransfer, + RebondAndStake, } impl TryFrom for DappsStakingFunc { @@ -58,6 +59,7 @@ impl TryFrom 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", )), @@ -146,7 +148,7 @@ impl ChainExtensionExec 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 = ::DbWeight::get().reads(1); @@ -227,7 +229,7 @@ impl ChainExtensionExec 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() @@ -336,6 +338,28 @@ impl ChainExtensionExec 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 = + ::WeightInfo::rebond_and_stake(); + env.charge_weight(base_weight)?; + + let caller = env.ext().address().clone(); + let call_result = pallet_dapps_staking::Pallet::::rebond_and_stake( + RawOrigin::Signed(caller).into(), + contract, + ); + 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)) diff --git a/chain-extensions/types/dapps-staking/src/lib.rs b/chain-extensions/types/dapps-staking/src/lib.rs index 4123e896..b6a7fca7 100644 --- a/chain-extensions/types/dapps-staking/src/lib.rs +++ b/chain-extensions/types/dapps-staking/src/lib.rs @@ -13,25 +13,25 @@ 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, @@ -39,21 +39,21 @@ pub enum DSError { 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, @@ -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, } @@ -106,6 +108,7 @@ impl TryFrom for DSError { Some("NominationTransferToSameContract") => { Ok(DSError::NominationTransferToSameContract) } + Some("NothingToRebond") => Ok(DSError::NothingToRebond), _ => Ok(DSError::UnknownError), }; } @@ -120,27 +123,29 @@ pub enum Contract { Wasm(Account), } +pub type ContractBytes = [u8; 32]; + #[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen)] pub struct DappsStakingValueInput { - 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 { - pub origin_contract: [u8; 32], - pub target_contract: [u8; 32], + pub origin_contract: ContractBytes, + pub target_contract: ContractBytes, pub value: Balance, } diff --git a/frame/dapps-staking/src/benchmarking.rs b/frame/dapps-staking/src/benchmarking.rs index 9340ca29..292e2b1b 100644 --- a/frame/dapps-staking/src/benchmarking.rs +++ b/frame/dapps-staking/src/benchmarking.rs @@ -217,6 +217,27 @@ benchmarks! { assert_last_event::(Event::::NominationTransfer(staker, origin_contract_id, T::MinimumStakingAmount::get(), target_contract_id).into()); } + rebond_and_stake { + initialize::(); + + let (_, contract_id) = register_contract::(1)?; + prepare_bond_and_stake::(T::MaxNumberOfStakersPerContract::get() - 1, &contract_id, SEED)?; + + let staker = whitelisted_caller(); + let _ = T::Currency::make_free_balance_be(&staker, BalanceOf::::max_value()); + let stake_amount = BalanceOf::::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::::bond_and_stake(RawOrigin::Signed(staker.clone()).into(), contract_id.clone(), stake_amount)?; + DappsStaking::::unbond_and_unstake(RawOrigin::Signed(staker.clone()).into(), contract_id.clone(), unstake_amount)?; + }: _(RawOrigin::Signed(staker.clone()), contract_id.clone()) + verify { + assert_last_event::(Event::::RebondAndStake(staker, contract_id, unstake_amount).into()); + } + claim_staker_with_restake { initialize::(); let (_, contract_id) = register_contract::(1)?; diff --git a/frame/dapps-staking/src/pallet/mod.rs b/frame/dapps-staking/src/pallet/mod.rs index c2e8b216..f32237d5 100644 --- a/frame/dapps-staking/src/pallet/mod.rs +++ b/frame/dapps-staking/src/pallet/mod.rs @@ -218,6 +218,8 @@ pub mod pallet { BalanceOf, T::SmartContract, ), + /// Account has rebonded unlocking chunks and staked funds on a smart contract. + RebondAndStake(T::AccountId, T::SmartContract, BalanceOf), } #[pallet::error] @@ -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] @@ -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. + #[pallet::weight(T::WeightInfo::rebond_and_stake())] + pub fn rebond_and_stake( + origin: OriginFor, + 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::::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::::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::>>::default(); + + GeneralEraInfo::::mutate(¤t_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); + } + }); + + Self::update_ledger(&staker, ledger); + Self::update_staker_info(&staker, &contract_id, staker_info); + ContractEraStake::::insert(&contract_id, current_era, staking_info); + + Self::deposit_event(Event::::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, diff --git a/frame/dapps-staking/src/testing_utils.rs b/frame/dapps-staking/src/testing_utils.rs index 953b2698..883ae5c3 100644 --- a/frame/dapps-staking/src/testing_utils.rs +++ b/frame/dapps-staking/src/testing_utils.rs @@ -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, +) { + // 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::::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 + ); + 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(); diff --git a/frame/dapps-staking/src/tests.rs b/frame/dapps-staking/src/tests.rs index cfc5a0b0..f7e134eb 100644 --- a/frame/dapps-staking/src/tests.rs +++ b/frame/dapps-staking/src/tests.rs @@ -1059,6 +1059,71 @@ 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)); + let rebond_contract_id = MockSmartContract::Evm(H160::repeat_byte(0x02)); + assert_register(10, &contract_id); + assert_register(11, &rebond_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); + + // rebond and stake + assert_rebond_and_stake(staker_id, &rebond_contract_id); + }) +} + +#[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), + Error::::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), + Error::::NothingToRebond, + ); + }) +} + #[test] fn withdraw_unbonded_is_ok() { ExternalityBuilder::build().execute_with(|| { diff --git a/frame/dapps-staking/src/weights.rs b/frame/dapps-staking/src/weights.rs index 181d1d3c..fa18eb2a 100644 --- a/frame/dapps-staking/src/weights.rs +++ b/frame/dapps-staking/src/weights.rs @@ -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; @@ -98,6 +99,19 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().writes(5 as u64)) } // Storage: DappsStaking PalletDisabled (r:1 w:0) + // Storage: DappsStaking RegisteredDapps (r:1 w:0) + // Storage: DappsStaking Ledger (r:1 w:1) + // Storage: DappsStaking CurrentEra (r:1 w:0) + // Storage: DappsStaking ContractEraStake (r:1 w:1) + // Storage: DappsStaking GeneralStakerInfo (r:1 w:1) + // Storage: DappsStaking GeneralEraInfo (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn rebond_and_stake() -> Weight { + Weight::from_ref_time(133_638_000 as u64) + .saturating_add(RocksDbWeight::get().reads(8 as u64)) + .saturating_add(RocksDbWeight::get().writes(5 as u64)) + } + // Storage: DappsStaking PalletDisabled (r:1 w:0) // Storage: DappsStaking Ledger (r:1 w:1) // Storage: DappsStaking CurrentEra (r:1 w:0) // Storage: Balances Locks (r:1 w:1) @@ -249,6 +263,19 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes(5 as u64)) } // Storage: DappsStaking PalletDisabled (r:1 w:0) + // Storage: DappsStaking RegisteredDapps (r:1 w:0) + // Storage: DappsStaking Ledger (r:1 w:1) + // Storage: DappsStaking CurrentEra (r:1 w:0) + // Storage: DappsStaking ContractEraStake (r:1 w:1) + // Storage: DappsStaking GeneralStakerInfo (r:1 w:1) + // Storage: DappsStaking GeneralEraInfo (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn rebond_and_stake() -> Weight { + Weight::from_ref_time(133_638_000 as u64) + .saturating_add(RocksDbWeight::get().reads(8 as u64)) + .saturating_add(RocksDbWeight::get().writes(5 as u64)) + } + // Storage: DappsStaking PalletDisabled (r:1 w:0) // Storage: DappsStaking Ledger (r:1 w:1) // Storage: DappsStaking CurrentEra (r:1 w:0) // Storage: Balances Locks (r:1 w:1) diff --git a/precompiles/dapps-staking/DappsStaking.sol b/precompiles/dapps-staking/DappsStaking.sol index 40d32b0a..af7c3aab 100644 --- a/precompiles/dapps-staking/DappsStaking.sol +++ b/precompiles/dapps-staking/DappsStaking.sol @@ -87,4 +87,8 @@ interface DappsStaking { /// @param amount: The amount to transfer from origin to target /// @param target_smart_contract: The target smart contract address function nomination_transfer(address origin_smart_contract, uint128 amount, address target_smart_contract) external; + + /// @notice Rebond and stake all funds that are in the unbonding process. + /// @param smart_contract: The smart contract address to stake + function rebond_and_stake(address smart_contract) external; } diff --git a/precompiles/dapps-staking/src/lib.rs b/precompiles/dapps-staking/src/lib.rs index 49f26939..84ffbebc 100644 --- a/precompiles/dapps-staking/src/lib.rs +++ b/precompiles/dapps-staking/src/lib.rs @@ -357,6 +357,24 @@ where Ok(succeed(EvmDataWriter::new().write(true).build())) } + fn rebond_and_stake(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(1)?; + + // parse contract's address + let contract_h160 = input.read::
()?.0; + let contract_id = Self::decode_smart_contract(contract_h160)?; + log::trace!(target: "ds-precompile", "rebond_and_stake {:?}", contract_id); + + // Build call with origin. + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let call = pallet_dapps_staking::Call::::rebond_and_stake { contract_id }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + /// Helper method to decode type SmartContract enum pub fn decode_smart_contract( contract_h160: H160, @@ -422,6 +440,7 @@ pub enum Action { SetRewardDestination = "set_reward_destination(uint8)", WithdrawFromUnregistered = "withdraw_from_unregistered(address)", NominationTransfer = "nomination_transfer(address,uint128,address)", + RebondAndStake = "rebond_and_stake(address)", } impl Precompile for DappsStakingWrapper @@ -469,6 +488,7 @@ where Action::SetRewardDestination => Self::set_reward_destination(handle), Action::WithdrawFromUnregistered => Self::withdraw_from_unregistered(handle), Action::NominationTransfer => Self::nomination_transfer(handle), + Action::RebondAndStake => Self::rebond_and_stake(handle), } } } diff --git a/precompiles/dapps-staking/src/tests.rs b/precompiles/dapps-staking/src/tests.rs index 24aa67a6..4785e078 100644 --- a/precompiles/dapps-staking/src/tests.rs +++ b/precompiles/dapps-staking/src/tests.rs @@ -375,6 +375,37 @@ fn nomination_transfer() { }); } +#[test] +fn rebond_and_stake_is_ok() { + ExternalityBuilder::default() + .with_balances(vec![ + (TestAccount::Alex.into(), 200 * AST), + (TestAccount::Dino.into(), 200 * AST), + (TestAccount::Bobo.into(), 200 * AST), + ]) + .build() + .execute_with(|| { + initialize_first_block(); + + // register new contract by Alex + let developer = TestAccount::Alex.into(); + register_and_verify(developer, TEST_CONTRACT); + + let amount_staked = 100 * AST; + bond_stake_and_verify(TestAccount::Bobo, TEST_CONTRACT, amount_staked); + + // unstak all + let era = 2; + advance_to_era(era); + unbond_unstake_and_verify(TestAccount::Bobo, TEST_CONTRACT, amount_staked); + + // rebond unbonded funds + let era = 3; + advance_to_era(era); + rebond_and_stake_verify(TestAccount::Bobo, TEST_CONTRACT, amount_staked); + }) +} + // **************************************************************************************************** // Helper functions // **************************************************************************************************** @@ -593,6 +624,33 @@ fn nomination_transfer_verify( ); } +// Since rebond_amount is not accessible (private field), need to provide expected amout to be rebonded. +fn rebond_and_stake_verify(staker: TestAccount, contract: H160, expected_rebond_amount: Balance) { + let smart_contract = + decode_smart_contract_from_array(contract.clone().to_fixed_bytes()).unwrap(); + + let staker_acc_id = AccountId32::from(staker.clone()); + let init_staker_info = DappsStaking::staker_info(&staker_acc_id, &smart_contract); + + precompiles() + .prepare_test( + staker.clone(), + precompile_address(), + EvmDataWriter::new_with_selector(Action::RebondAndStake) + .write(Address(contract.clone())) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + let final_staker_info = DappsStaking::staker_info(&staker_acc_id, &smart_contract); + + assert_eq!( + final_staker_info.latest_staked_value(), + init_staker_info.latest_staked_value() + expected_rebond_amount + ); +} + /// helper function to bond, stake and verify if result is OK fn claim_dapp_and_verify(contract: H160, era: EraIndex) { precompiles()