Skip to content

Commit

Permalink
change staking target extrinsic, closes #1570 (#1623)
Browse files Browse the repository at this point in the history
The goal of this PR is to implement the `change_staking_target`
extrinsic as specified in the Staking Rewards design document. The
design document is updated as part of this PR to account for needed
changes discovered during this implementation phase.

Closes #1570
  • Loading branch information
shannonwells committed Oct 11, 2023
1 parent bbd773a commit d6d92b3
Show file tree
Hide file tree
Showing 24 changed files with 783 additions and 83 deletions.
23 changes: 11 additions & 12 deletions designdocs/capacity_staking_rewards_implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,28 +231,27 @@ pub enum Error<T> {
to_era: Option<T::RewardEra>
);
```
Both emit events `StakingRewardClaimed` with the parameters of the extrinsic.

2. **change_staking_target(origin, from, to, amount)**
Changes a staking account detail's target MSA Id to a new one by `amount`
Rules for this are similar to unstaking; if `amount` would leave less than the minimum staking amount for the `from` target, the entire amount is retargeted.
No more than T::MaxUnlockingChunks staking amounts may be retargeted within this Thawing Period.
Each call creates one chunk. Emits a `StakingTargetChanged` event with the parameters of the extrinsic.

```rust
/// Change a staking account detail's target MSA Id to a new one.
/// If Some(amount) is specified, that amount up to the total staking amount is retargeted.
/// Rules for this are similar to unstaking; if `amount` would leave less than the minimum staking
/// amount for the `from` target, the entire amount is retargeted.
/// If amount is None, ALL of the total staking amount for 'from' is changed to the new target MSA Id.
/// No more than T::MaxUnlockingChunks staking amounts may be retargeted within this Thawing Period.
/// Each call creates one chunk.
/// Errors:
/// - NotAStakingAccount if origin has no StakingAccount associated with the target
/// - pallet_msa::Error::ProviderNotRegistered if 'to' MSA Id does not exist or is not a Provider
/// - MaxUnlockingChunksExceeded if 'from' target staking amount is still thawing in the staking unlock chunks (either type)
/// - ZeroAmountNotAllowed if `amount` is zero
/// - InsufficientStakingAmount if `amount` to transfer to the new target is below the minimum staking amount.
/// - StakerTargetRelationshipNotFound` if `from` is not a staking target for Origin. This also covers when account's MSA is not staking anything at all or account has no MSA
/// - StakingAmountBelowMinimum if amount to retarget is below the minimum staking amount.
/// - InsufficientStakingBalance if amount to retarget exceeds what the staker has targeted to the `from` MSA Id.
/// - InvalidTarget if `to` is not a Registered Provider.
#[pallet:call_index(n+1)] // n = current call index in the pallet
pub fn change_staking_target(
origin: OriginFor<T>,
from: MessageSourceId,
to: MessageSourceId,
amount: Option<BalanceOf<T>>
amount: BalanceOf<T>
);
```

Expand Down
54 changes: 54 additions & 0 deletions integration-tests/capacity/change_staking_target.test.ts
Original file line number Diff line number Diff line change
@@ -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"})
});
});
4 changes: 2 additions & 2 deletions integration-tests/capacity/replenishment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe("Capacity Replenishment Testing: ", function () {
// new user/msa stakes to provider
const userKeys = createKeys("userKeys");
await fundKeypair(devAccounts[0].keys, userKeys, 5n * DOLLARS);
let [_, events] = await ExtrinsicHelper.stake(userKeys, stakeProviderId, userStakeAmt).fundAndSend();
let [_, events] = await ExtrinsicHelper.stake(userKeys, stakeProviderId, userStakeAmt, 'MaximumCapacity').fundAndSend();
assertEvent(events, 'system.ExtrinsicSuccess');

const payload = JSON.stringify({ changeType: 1, fromId: 1, objectId: 2 })
Expand All @@ -125,7 +125,7 @@ describe("Capacity Replenishment Testing: ", function () {
assert(remainingCapacity < callCapacityCost);

// user stakes tiny additional amount
[_, events] = await ExtrinsicHelper.stake(userKeys, stakeProviderId, userIncrementAmt).fundAndSend();
[_, events] = await ExtrinsicHelper.stake(userKeys, stakeProviderId, userIncrementAmt, 'MaximumCapacity').fundAndSend();
assertEvent(events, 'capacity.Staked');

// provider can now send a message
Expand Down
10 changes: 5 additions & 5 deletions integration-tests/capacity/staking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ describe("Capacity Staking Tests", function () {
const stakeAmount = 10n * CENTS;
const stakeKeys = await createAndFundKeypair(stakeAmount, "StakeKeys");

const failStakeObj = ExtrinsicHelper.stake(stakeKeys, maxMsaId + 1, stakeAmount);
const failStakeObj = ExtrinsicHelper.stake(stakeKeys, maxMsaId + 1, stakeAmount, 'MaximumCapacity');
await assert.rejects(failStakeObj.fundAndSend(), { name: "InvalidTarget" });
});
});
Expand All @@ -219,7 +219,7 @@ describe("Capacity Staking Tests", function () {
let providerId = await createMsaAndProvider(stakingKeys, "stakingKeys", 150n * CENTS);
let stakeAmount = 1500n;

const failStakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, stakeAmount);
const failStakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, stakeAmount, 'MaximumCapacity');
await assert.rejects(failStakeObj.fundAndSend(), { name: "InsufficientStakingAmount" });
});
});
Expand All @@ -229,7 +229,7 @@ describe("Capacity Staking Tests", function () {
let stakingKeys = createKeys("stakingKeys");
let providerId = await createMsaAndProvider(stakingKeys, "stakingKeys", );

const failStakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, 0);
const failStakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, 0, 'MaximumCapacity');
await assert.rejects(failStakeObj.fundAndSend(), { name: "ZeroAmountNotAllowed" });
});
});
Expand All @@ -240,7 +240,7 @@ describe("Capacity Staking Tests", function () {
let providerId = await createMsaAndProvider(stakingKeys, "stakingKeys");
let stakingAmount = 1n * DOLLARS;

const failStakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, stakingAmount);
const failStakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, stakingAmount, 'MaximumCapacity');
await assert.rejects(failStakeObj.fundAndSend(), { name: "BalanceTooLowtoStake" });
});
});
Expand Down Expand Up @@ -282,7 +282,7 @@ describe("Capacity Staking Tests", function () {
let stakingKeys: KeyringPair = createKeys("stakingKeys");
let providerId: u64 = await createMsaAndProvider(stakingKeys, "stakingKeys", accountBalance);

const stakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, tokenMinStake);
const stakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, tokenMinStake, 'MaximumCapacity');
const [stakeEvent] = await stakeObj.fundAndSend();
assert.notEqual(stakeEvent, undefined, "should return a Stake event");

Expand Down
2 changes: 1 addition & 1 deletion integration-tests/capacity/transactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ describe("Capacity Transactions", function () {
it("fails to pay with Capacity for a non-capacity transaction", async function () {
const capacityKeys = createKeys("CapacityKeys");
const capacityProvider = await createMsaAndProvider(capacityKeys, "CapacityProvider", FUNDS_AMOUNT);
const nonCapacityTxn = ExtrinsicHelper.stake(capacityKeys, capacityProvider, 1n * CENTS);
const nonCapacityTxn = ExtrinsicHelper.stake(capacityKeys, capacityProvider, 1n * CENTS, 'MaximumCapacity');
await assert.rejects(nonCapacityTxn.payWithCapacity(), {
name: "RpcError", message:
"1010: Invalid Transaction: Custom error: 0"
Expand Down
8 changes: 6 additions & 2 deletions integration-tests/scaffolding/extrinsicHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,8 @@ export class ExtrinsicHelper {
public static setEpochLength(keys: KeyringPair, epoch_length: any): Extrinsic {
return new Extrinsic(() => ExtrinsicHelper.api.tx.capacity.setEpochLength(epoch_length), keys, ExtrinsicHelper.api.events.capacity.EpochLengthUpdated);
}
public static stake(keys: KeyringPair, target: any, amount: any): Extrinsic {
return new Extrinsic(() => ExtrinsicHelper.api.tx.capacity.stake(target, amount), keys, ExtrinsicHelper.api.events.capacity.Staked);
public static stake(keys: KeyringPair, target: any, amount: any, stakingType: 'MaximumCapacity' | 'ProviderBoost'): Extrinsic {
return new Extrinsic(() => ExtrinsicHelper.api.tx.capacity.stake(target, amount, stakingType), keys, ExtrinsicHelper.api.events.capacity.Staked);
}

public static unstake(keys: KeyringPair, target: any, amount: any): Extrinsic {
Expand All @@ -396,6 +396,10 @@ export class ExtrinsicHelper {
return new Extrinsic(() => ExtrinsicHelper.api.tx.capacity.withdrawUnstaked(), keys, ExtrinsicHelper.api.events.capacity.StakeWithdrawn);
}

public static changeStakingTarget(keys: KeyringPair, fromMsa: any, toMsa: any, amount: any): Extrinsic {
return new Extrinsic(() => ExtrinsicHelper.api.tx.capacity.changeStakingTarget(fromMsa, toMsa, amount), keys, ExtrinsicHelper.api.events.capacity.StakingTargetChanged);
}

public static payWithCapacityBatchAll(keys: KeyringPair, calls: any): Extrinsic {
return new Extrinsic(() => ExtrinsicHelper.api.tx.frequencyTxPayment.payWithCapacityBatchAll(calls), keys, ExtrinsicHelper.api.events.utility.BatchCompleted);
}
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/scaffolding/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ export async function createMsaAndProvider(keys: KeyringPair, providerName: stri

// Stakes the given amount of tokens from the given keys to the given provider
export async function stakeToProvider(keys: KeyringPair, providerId: u64, tokensToStake: bigint): Promise<void> {
const stakeOp = ExtrinsicHelper.stake(keys, providerId, tokensToStake);
const stakeOp = ExtrinsicHelper.stake(keys, providerId, tokensToStake, 'MaximumCapacity');
const [stakeEvent] = await stakeOp.fundAndSend();
assert.notEqual(stakeEvent, undefined, 'stakeToProvider: should have returned Stake event');

Expand Down
53 changes: 52 additions & 1 deletion pallets/capacity/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,33 @@ pub fn register_provider<T: Config>(target_id: MessageSourceId, name: &'static s
let name = Vec::from(name).try_into().expect("error");
assert_ok!(T::BenchmarkHelper::create(target_id, name));
}

#[allow(clippy::expect_used)]
pub fn setup_provider_stake<T: Config>(
staker: &T::AccountId,
target_id: &MessageSourceId,
amount: u32,
) {
let staking_amount: BalanceOf<T> = T::MinimumStakingAmount::get().saturating_add(amount.into());
let capacity_amount: BalanceOf<T> = Capacity::<T>::capacity_generated(staking_amount);

// will add the amount to the account if it exists.
let mut staking_account: StakingAccountDetails<T> =
Capacity::get_staking_account_for(staker).unwrap_or_default();

let mut target_details = StakingTargetDetails::<T>::default();
let mut capacity_details =
CapacityDetails::<BalanceOf<T>, <T as Config>::EpochNumber>::default();

staking_account.deposit(staking_amount);
target_details.deposit(staking_amount, capacity_amount);
capacity_details.deposit(&staking_amount, &capacity_amount);

Capacity::<T>::set_staking_account(staker, &staking_account);
Capacity::<T>::set_target_details_for(staker, *target_id, target_details);
Capacity::<T>::set_capacity_for(*target_id, capacity_details);
}

pub fn create_funded_account<T: Config>(
string: &'static str,
n: u32,
Expand Down Expand Up @@ -92,7 +119,7 @@ benchmarks! {
let block_number = 4u32;

let mut staking_account = StakingAccountDetails::<T>::default();
let mut target_details = StakingTargetDetails::<BalanceOf<T>>::default();
let mut target_details = StakingTargetDetails::<T>::default();
let mut capacity_details = CapacityDetails::<BalanceOf<T>, <T as Config>::EpochNumber>::default();

staking_account.deposit(staking_amount);
Expand All @@ -116,6 +143,30 @@ benchmarks! {
assert_last_event::<T>(Event::<T>::EpochLengthUpdated {blocks: epoch_length}.into());
}

change_staking_target {
let caller: T::AccountId = create_funded_account::<T>("account", SEED, 5u32);
let from_msa = 33;
let to_msa = 34;
// amount in addition to minimum
let from_msa_amount = 32u32;
let to_msa_amount = 1u32;

register_provider::<T>(from_msa, "frommsa");
register_provider::<T>(to_msa, "tomsa");
setup_provider_stake::<T>(&caller, &from_msa, from_msa_amount);
setup_provider_stake::<T>(&caller, &to_msa, to_msa_amount);
let restake_amount = 11u32;

}: _ (RawOrigin::Signed(caller.clone(), ), from_msa, to_msa, restake_amount.into())
verify {
assert_last_event::<T>(Event::<T>::StakingTargetChanged {
account: caller,
from_msa,
to_msa,
amount: restake_amount.into()
}.into());
}

impl_benchmark_test_suite!(Capacity,
crate::tests::mock::new_test_ext(),
crate::tests::mock::Test);
Expand Down
Loading

0 comments on commit d6d92b3

Please sign in to comment.