From 802327680436aae83abb9d173288d81e0240dd06 Mon Sep 17 00:00:00 2001 From: Shannon Wells Date: Fri, 7 Jul 2023 15:30:03 -0700 Subject: [PATCH] change staking target extrinsic, closes #1570 (#1623) --- e2e/capacity/change_staking_target.test.ts | 54 +++ .../src/tests/change_staking_target_tests.rs | 317 ++++++++++++++++++ pallets/capacity/src/tests/mock.rs | 5 +- pallets/capacity/src/tests/other_tests.rs | 2 +- .../src/tests/rewards_provider_tests.rs | 3 +- .../src/tests/staking_target_details_tests.rs | 29 +- pallets/capacity/src/tests/unstaking_tests.rs | 4 +- .../capacity/src/tests/withdrawal_tests.rs | 2 +- pallets/capacity/src/weights.rs | 15 + .../frequency-tx-payment/src/tests/mock.rs | 1 + runtime/common/src/constants.rs | 2 + runtime/frequency/src/lib.rs | 1 + 12 files changed, 416 insertions(+), 19 deletions(-) create mode 100644 e2e/capacity/change_staking_target.test.ts create mode 100644 pallets/capacity/src/tests/change_staking_target_tests.rs diff --git a/e2e/capacity/change_staking_target.test.ts b/e2e/capacity/change_staking_target.test.ts new file mode 100644 index 0000000000..fcaeeccedd --- /dev/null +++ b/e2e/capacity/change_staking_target.test.ts @@ -0,0 +1,54 @@ +import "@frequency-chain/api-augment"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { u64, } from "@polkadot/types"; +import assert from "assert"; +import { ExtrinsicHelper, } from "../scaffolding/extrinsicHelpers"; +import { + devAccounts, createKeys, createMsaAndProvider, + stakeToProvider, CHAIN_ENVIRONMENT, + TEST_EPOCH_LENGTH, setEpochLength, + CENTS, DOLLARS, createAndFundKeypair, createProviderKeysAndId +} + from "../scaffolding/helpers"; +import { firstValueFrom } from "rxjs"; +import { MessageSourceId} from "@frequency-chain/api-augment/interfaces"; + +describe.only("change_staking_target tests", () => { + const tokenMinStake: bigint = 1n * CENTS; + const capacityMin: bigint = tokenMinStake / 50n; + + const unusedMsaId = async () => { + const maxMsaId = (await ExtrinsicHelper.getCurrentMsaIdentifierMaximum()).toNumber(); + return maxMsaId + 99; + } + + before(async () => { + if (process.env.CHAIN_ENVIRONMENT === CHAIN_ENVIRONMENT.DEVELOPMENT) { + await setEpochLength(devAccounts[0].keys, TEST_EPOCH_LENGTH); + } + }); + + it("happy path succeeds", async () => { + const providerBalance = 2n * DOLLARS; + const stakeKeys = createKeys("staker"); + const oldProvider = await createMsaAndProvider(stakeKeys, "Provider1", providerBalance); + const [_unused, newProvider] = await createProviderKeysAndId(); + + await assert.doesNotReject(stakeToProvider(stakeKeys, oldProvider, tokenMinStake*3n)); + + const call = ExtrinsicHelper.changeStakingTarget(stakeKeys, oldProvider, newProvider, tokenMinStake); + const [events] = await call.signAndSend(); + assert.notEqual(events, undefined); + }); + + // not intended to be exhaustive, just check one error case + it("fails if 'to' is not a Provider", async () => { + const providerBalance = 2n * DOLLARS; + const stakeKeys = createKeys("staker"); + const notAProvider = await unusedMsaId(); + const oldProvider = await createMsaAndProvider(stakeKeys, "Provider1", providerBalance); + await assert.doesNotReject(stakeToProvider(stakeKeys, oldProvider, tokenMinStake*3n)); + const call = ExtrinsicHelper.changeStakingTarget(stakeKeys, oldProvider, notAProvider, tokenMinStake); + await assert.rejects(call.signAndSend(), {name: "InvalidTarget"}) + }); +}); diff --git a/pallets/capacity/src/tests/change_staking_target_tests.rs b/pallets/capacity/src/tests/change_staking_target_tests.rs new file mode 100644 index 0000000000..0d5d660abc --- /dev/null +++ b/pallets/capacity/src/tests/change_staking_target_tests.rs @@ -0,0 +1,317 @@ +use super::{mock::*, testing_utils::*}; +use crate::{ + BalanceOf, CapacityDetails, Config, CurrentEraInfo, Error, Event, RewardEraInfo, + StakingAccountDetails, StakingAccountLedger, StakingTargetDetails, +}; +use common_primitives::{ + capacity::StakingType::{MaximumCapacity, ProviderBoost}, + msa::MessageSourceId, +}; +use frame_support::{assert_noop, assert_ok, traits::Get}; + +// staker is unused unless amount > 0 +fn setup_provider(staker: u64, target: MessageSourceId, amount: u64) { + let provider_name = String::from("Cst-") + target.to_string().as_str(); + register_provider(target, provider_name); + if amount > 0 { + assert_ok!(Capacity::stake(RuntimeOrigin::signed(staker), target, amount, ProviderBoost)); + } +} + +type TestCapacityDetails = CapacityDetails, u32>; +type TestTargetDetails = StakingTargetDetails; + +#[test] +fn do_retarget_happy_path() { + new_test_ext().execute_with(|| { + let staker = 200u64; + let from_msa: MessageSourceId = 1; + let from_amount = 20u64; + let to_amount = from_amount / 2; + let to_msa: MessageSourceId = 2; + setup_provider(staker, from_msa, from_amount); + setup_provider(staker, to_msa, to_amount); + + // retarget half the stake to to_msa + assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &to_amount)); + + // expect from stake amounts to be halved + let expected_from_details: TestCapacityDetails = CapacityDetails { + remaining_capacity: 1, + total_tokens_staked: 10, + total_capacity_issued: 1, + last_replenished_epoch: 0, + }; + let from_capacity_details: TestCapacityDetails = + Capacity::get_capacity_for(from_msa).unwrap(); + assert_eq!(from_capacity_details, expected_from_details); + + // expect to stake amounts to be increased by the retarget amount + let expected_to_details: TestCapacityDetails = CapacityDetails { + remaining_capacity: 2, + total_tokens_staked: 20, + total_capacity_issued: 2, + last_replenished_epoch: 0, + }; + let to_capacity_details = Capacity::get_capacity_for(to_msa).unwrap(); + assert_eq!(to_capacity_details, expected_to_details); + + let expected_from_target_details: TestTargetDetails = + StakingTargetDetails { amount: 10, capacity: 1 }; + let from_target_details = Capacity::get_target_for(staker, from_msa).unwrap(); + assert_eq!(from_target_details, expected_from_target_details); + + let expected_to_target_details: TestTargetDetails = + StakingTargetDetails { amount: 20, capacity: 2 }; + let to_target_details = Capacity::get_target_for(staker, to_msa).unwrap(); + assert_eq!(to_target_details, expected_to_target_details); + }) +} + +#[test] +fn do_retarget_deletes_staking_target_details_if_zero_balance() { + new_test_ext().execute_with(|| { + let staker = 200u64; + let from_msa: MessageSourceId = 1; + let to_msa: MessageSourceId = 2; + let amount = 10u64; + setup_provider(staker, from_msa, amount); + setup_provider(staker, to_msa, amount); + + // stake additional to provider from another Msa, doesn't matter which type. + // total staked to from_msa is now 22u64. + assert_ok!(Capacity::stake( + RuntimeOrigin::signed(300u64), + from_msa, + 12u64, + MaximumCapacity + )); + + assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &amount)); + + let expected_from_details: TestCapacityDetails = CapacityDetails { + remaining_capacity: 1, + total_tokens_staked: 12, + total_capacity_issued: 1, + last_replenished_epoch: 0, + }; + let from_capacity_details: TestCapacityDetails = + Capacity::get_capacity_for(from_msa).unwrap(); + assert_eq!(from_capacity_details, expected_from_details); + + let expected_to_details: TestCapacityDetails = CapacityDetails { + remaining_capacity: 2, + total_tokens_staked: 2 * amount, + total_capacity_issued: 2, + last_replenished_epoch: 0, + }; + + let to_capacity_details = Capacity::get_capacity_for(to_msa).unwrap(); + assert_eq!(to_capacity_details, expected_to_details); + + assert!(Capacity::get_target_for(staker, from_msa).is_none()); + + let expected_to_target_details: TestTargetDetails = + StakingTargetDetails { amount: 2 * amount, capacity: 2 }; + let to_target_details = Capacity::get_target_for(staker, to_msa).unwrap(); + assert_eq!(to_target_details, expected_to_target_details); + + assert!(Capacity::get_target_for(staker, from_msa).is_none()); + }) +} + +#[test] +fn change_staking_starget_emits_event_on_success() { + new_test_ext().execute_with(|| { + let staker = 200u64; + let from_msa: MessageSourceId = 1; + let from_amount = 20u64; + let to_amount = from_amount / 2; + let to_msa: MessageSourceId = 2; + setup_provider(staker, from_msa, from_amount); + setup_provider(staker, to_msa, to_amount); + + assert_ok!(Capacity::change_staking_target( + RuntimeOrigin::signed(staker), + from_msa, + to_msa, + to_amount + )); + let events = staking_events(); + + assert_eq!( + events.last().unwrap(), + &Event::StakingTargetChanged { account: staker, from_msa, to_msa, amount: to_amount } + ); + }) +} + +#[test] +fn change_staking_target_errors_if_too_many_changes_before_thaw() { + new_test_ext().execute_with(|| { + let staker = 200u64; + let from_msa: MessageSourceId = 1; + let to_msa: MessageSourceId = 2; + + let max_chunks: u32 = ::MaxUnlockingChunks::get(); + let staking_amount = ((max_chunks + 2u32) * 10u32) as u64; + setup_provider(staker, from_msa, staking_amount); + setup_provider(staker, to_msa, 10); + + let retarget_amount = 10u64; + for _i in 0..(max_chunks) { + assert_ok!(Capacity::change_staking_target( + RuntimeOrigin::signed(staker), + from_msa, + to_msa, + retarget_amount + )); + } + + assert_noop!( + Capacity::change_staking_target( + RuntimeOrigin::signed(staker), + from_msa, + to_msa, + retarget_amount + ), + Error::::MaxUnlockingChunksExceeded + ); + }); +} + +#[test] +fn change_staking_target_garbage_collects_thawed_chunks() { + new_test_ext().execute_with(|| { + let staked_amount = 50u64; + let staking_account = 200u64; + let from_target: MessageSourceId = 3; + let to_target: MessageSourceId = 4; + setup_provider(staking_account, from_target, staked_amount); + setup_provider(staking_account, to_target, staked_amount); + + CurrentEraInfo::::set(RewardEraInfo { era_index: 20, started_at: 100 }); + let max_chunks = ::MaxUnlockingChunks::get(); + for i in 0..max_chunks { + println!("{:?}", i); + assert_ok!(Capacity::change_staking_target( + RuntimeOrigin::signed(staking_account), + from_target, + to_target, + 10u64, + )); + } + CurrentEraInfo::::set(RewardEraInfo { era_index: 25, started_at: 100 }); + assert_ok!(Capacity::change_staking_target( + RuntimeOrigin::signed(staking_account), + from_target, + to_target, + 10u64, + )); + }) +} + +#[test] +fn change_staking_target_test_parametric_validity() { + new_test_ext().execute_with(|| { + let staked_amount = 10u64; + let from_account = 200u64; + + StakingAccountLedger::::insert( + from_account, + StakingAccountDetails { + active: 20, + total: 20, + unlocking: Default::default(), + staking_type: ProviderBoost, + last_rewards_claimed_at: None, + stake_change_unlocking: Default::default(), + }, + ); + let from_account_not_staking = 100u64; + let from_target_not_staked: MessageSourceId = 1; + let to_target_not_provider: MessageSourceId = 2; + let from_target: MessageSourceId = 3; + let to_target: MessageSourceId = 4; + setup_provider(from_account, from_target_not_staked, 0); + setup_provider(from_account, from_target, staked_amount); + setup_provider(from_account, to_target, staked_amount); + + assert_ok!(Capacity::stake( + RuntimeOrigin::signed(from_account), + from_target, + staked_amount, + ProviderBoost + )); + + struct TestCase { + from_account: u64, + from_target: MessageSourceId, + to_target: MessageSourceId, + retarget_amount: u64, + expected_err: Error, + } + let test_cases: Vec = vec![ + // from is a provider but account is not staking to it + TestCase { + from_account, + from_target: from_target_not_staked, + to_target, + retarget_amount: staked_amount, + expected_err: Error::::StakerTargetRelationshipNotFound, + }, + // from_account is not staking at all. + TestCase { + from_account: from_account_not_staking, + from_target, + to_target, + retarget_amount: staked_amount, + expected_err: Error::::NotAStakingAccount, + }, + // // from and to providers are valid, but zero amount too small + TestCase { + from_account, + from_target, + to_target, + retarget_amount: 0, + expected_err: Error::::StakingAmountBelowMinimum, + }, + // // nonzero amount below minimum is still too small + TestCase { + from_account, + from_target, + to_target, + retarget_amount: 9, + expected_err: Error::::StakingAmountBelowMinimum, + }, + // // account is staked with from-target, but to-target is not a provider + TestCase { + from_account, + from_target, + to_target: to_target_not_provider, + retarget_amount: staked_amount, + expected_err: Error::::InvalidTarget, + }, + // account doesn't have enough staked to make the transfer + TestCase { + from_account, + from_target, + to_target, + retarget_amount: 999, + expected_err: Error::::InsufficientStakingBalance, + }, + ]; + + for tc in test_cases { + assert_noop!( + Capacity::change_staking_target( + RuntimeOrigin::signed(tc.from_account), + tc.from_target, + tc.to_target, + tc.retarget_amount, + ), + tc.expected_err + ); + } + }); +} diff --git a/pallets/capacity/src/tests/mock.rs b/pallets/capacity/src/tests/mock.rs index de40a9eef1..1611c0be60 100644 --- a/pallets/capacity/src/tests/mock.rs +++ b/pallets/capacity/src/tests/mock.rs @@ -1,8 +1,8 @@ use crate as pallet_capacity; -use crate::{BalanceOf, Config, StakingRewardClaim, StakingRewardsProvider}; +use crate::{BalanceOf, StakingRewardClaim, StakingRewardsProvider}; use common_primitives::{ - node::{AccountId, Balance, Hash, Header, ProposalProvider}, + node::{AccountId, Hash, Header, ProposalProvider}, schema::{SchemaId, SchemaValidator}, }; use frame_support::{ @@ -189,6 +189,7 @@ impl pallet_capacity::Config for Test { type EraLength = ConstU32<10>; type StakingRewardsPastErasMax = ConstU32<5>; type RewardsProvider = TestStakingRewardsProvider; + type ChangeStakingTargetThawEras = ConstU32<5>; } pub fn new_test_ext() -> sp_io::TestExternalities { diff --git a/pallets/capacity/src/tests/other_tests.rs b/pallets/capacity/src/tests/other_tests.rs index 9c559f8aef..a6c5c70ea5 100644 --- a/pallets/capacity/src/tests/other_tests.rs +++ b/pallets/capacity/src/tests/other_tests.rs @@ -87,7 +87,7 @@ fn set_target_details_is_successful() { assert_eq!(Capacity::get_target_for(&staker, target), None); - let mut target_details = StakingTargetDetails::>::default(); + let mut target_details = StakingTargetDetails::::default(); target_details.amount = 10; target_details.capacity = 10; diff --git a/pallets/capacity/src/tests/rewards_provider_tests.rs b/pallets/capacity/src/tests/rewards_provider_tests.rs index 17d7fd86db..536b6e4dff 100644 --- a/pallets/capacity/src/tests/rewards_provider_tests.rs +++ b/pallets/capacity/src/tests/rewards_provider_tests.rs @@ -1,7 +1,6 @@ use super::mock::*; use crate::{ - tests::testing_utils::{run_to_block, system_run_to_block}, - Config, CurrentEraInfo, Error, Event, RewardEraInfo, RewardPoolInfo, StakingAccountDetails, + CurrentEraInfo, Error, RewardEraInfo, RewardPoolInfo, StakingAccountDetails, StakingRewardClaim, StakingRewardPool, StakingRewardsProvider, }; use frame_support::assert_err; diff --git a/pallets/capacity/src/tests/staking_target_details_tests.rs b/pallets/capacity/src/tests/staking_target_details_tests.rs index dbe9ea0d4a..da6f3360d1 100644 --- a/pallets/capacity/src/tests/staking_target_details_tests.rs +++ b/pallets/capacity/src/tests/staking_target_details_tests.rs @@ -4,35 +4,42 @@ use frame_support::{assert_err, assert_ok}; #[test] fn impl_staking_target_details_increase_by() { - let mut staking_target = StakingTargetDetails::>::default(); + let mut staking_target = StakingTargetDetails::::default(); assert_eq!(staking_target.deposit(10, 10), Some(())); assert_eq!( staking_target, - StakingTargetDetails::> { - amount: BalanceOf::::from(10u64), - capacity: 10 - } + StakingTargetDetails:: { amount: BalanceOf::::from(10u64), capacity: 10 } ) } #[test] fn staking_target_details_withdraw_reduces_staking_and_capacity_amounts() { - let mut staking_target_details = StakingTargetDetails::> { - amount: BalanceOf::::from(15u64), - capacity: BalanceOf::::from(20u64), + let mut staking_target_details = StakingTargetDetails:: { + amount: BalanceOf::::from(25u64), + capacity: BalanceOf::::from(30u64), }; staking_target_details.withdraw(10, 10); assert_eq!( staking_target_details, - StakingTargetDetails::> { - amount: BalanceOf::::from(5u64), - capacity: BalanceOf::::from(10u64), + StakingTargetDetails:: { + amount: BalanceOf::::from(15u64), + capacity: BalanceOf::::from(20u64), } ) } +#[test] +fn staking_target_details_withdraw_reduces_to_zero_if_balance_is_below_minimum() { + let mut staking_target_details = StakingTargetDetails:: { + amount: BalanceOf::::from(10u64), + capacity: BalanceOf::::from(20u64), + }; + staking_target_details.withdraw(8, 16); + assert_eq!(staking_target_details, StakingTargetDetails::::default()); +} + #[test] fn staking_target_details_withdraw_reduces_total_tokens_staked_and_total_tokens_available() { let mut capacity_details = CapacityDetails::, ::EpochNumber> { diff --git a/pallets/capacity/src/tests/unstaking_tests.rs b/pallets/capacity/src/tests/unstaking_tests.rs index 8e930cc141..3b0b98075e 100644 --- a/pallets/capacity/src/tests/unstaking_tests.rs +++ b/pallets/capacity/src/tests/unstaking_tests.rs @@ -48,7 +48,7 @@ fn unstake_happy_path() { assert_eq!( staking_target_details, - StakingTargetDetails::> { + StakingTargetDetails:: { amount: BalanceOf::::from(60u64), capacity: BalanceOf::::from(6u64), } @@ -138,7 +138,7 @@ fn unstake_errors_amount_to_unstake_exceeds_amount_staked() { assert_ok!(Capacity::stake(RuntimeOrigin::signed(token_account), target, staking_amount)); assert_noop!( Capacity::unstake(RuntimeOrigin::signed(token_account), target, unstaking_amount), - Error::::AmountToUnstakeExceedsAmountStaked + Error::::InsufficientStakingBalance ); }); } diff --git a/pallets/capacity/src/tests/withdrawal_tests.rs b/pallets/capacity/src/tests/withdrawal_tests.rs index f8b0038b08..46f6fe443a 100644 --- a/pallets/capacity/src/tests/withdrawal_tests.rs +++ b/pallets/capacity/src/tests/withdrawal_tests.rs @@ -63,7 +63,7 @@ fn impl_withdraw_errors_insufficient_balance() { assert_noop!( Capacity::deduct(target_msa_id, 20u32.into()), - Error::::InsufficientBalance + Error::::InsufficientCapacityBalance ); let mut capacity_details = diff --git a/pallets/capacity/src/weights.rs b/pallets/capacity/src/weights.rs index c5d7ed84e5..78e9f47ca3 100644 --- a/pallets/capacity/src/weights.rs +++ b/pallets/capacity/src/weights.rs @@ -54,6 +54,7 @@ pub trait WeightInfo { fn on_initialize() -> Weight; fn unstake() -> Weight; fn set_epoch_length() -> Weight; + fn change_staking_target() -> Weight; } /// Weights for pallet_capacity using the Substrate node and recommended hardware. @@ -139,6 +140,13 @@ impl WeightInfo for SubstrateWeight { Weight::from_parts(6_662_000, 0) .saturating_add(T::DbWeight::get().writes(1_u64)) } + + /// Storage: + /// Proof: + fn change_staking_target() -> Weight { + Weight::from_parts(1_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } // For backwards compatibility and tests @@ -223,4 +231,11 @@ impl WeightInfo for () { Weight::from_parts(6_662_000, 0) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + + /// Storage: + /// Proof: + fn change_staking_target() -> Weight { + Weight::from_parts(1_000_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } } diff --git a/pallets/frequency-tx-payment/src/tests/mock.rs b/pallets/frequency-tx-payment/src/tests/mock.rs index 5d5e892df3..2644189a72 100644 --- a/pallets/frequency-tx-payment/src/tests/mock.rs +++ b/pallets/frequency-tx-payment/src/tests/mock.rs @@ -220,6 +220,7 @@ impl pallet_capacity::Config for Test { type EraLength = ConstU32<5>; type StakingRewardsPastErasMax = ConstU32<2>; type RewardsProvider = Capacity; + type ChangeStakingTargetThawEras = ConstU32<1>; } use pallet_balances::Call as BalancesCall; diff --git a/runtime/common/src/constants.rs b/runtime/common/src/constants.rs index c039b939fb..eb15120105 100644 --- a/runtime/common/src/constants.rs +++ b/runtime/common/src/constants.rs @@ -381,4 +381,6 @@ parameter_types! { pub const CapacityPerToken: Perbill = Perbill::from_percent(2); } +pub type CapacityChangeStakingTargetThawEras = ConstU32<5>; + // -end- Capacity Pallet --- diff --git a/runtime/frequency/src/lib.rs b/runtime/frequency/src/lib.rs index 04a7be575d..89d7306241 100644 --- a/runtime/frequency/src/lib.rs +++ b/runtime/frequency/src/lib.rs @@ -418,6 +418,7 @@ impl pallet_capacity::Config for Runtime { type EraLength = ConstU32<{ 14 * DAYS }>; type StakingRewardsPastErasMax = ConstU32<26u32>; // 1 year type RewardsProvider = Capacity; + type ChangeStakingTargetThawEras = CapacityChangeStakingTargetThawEras; } impl pallet_schemas::Config for Runtime {