Skip to content

Commit

Permalink
Investigate fungible trait Holds and Freezes overlap (#1873)
Browse files Browse the repository at this point in the history
# Goal
The goal of this PR is to investigate the overlap of `hold` and `frozen`
in the frame system account, used by the balances pallet and the
fungible trait (which replaced the Currency trait).

Closes #1819

# Discussion
Investigation indicates that it is not currently possible to create a
situation where `frozen` overlaps with `hold` [currently indicated by
`reserved` in the frame system account] and that `hold` can be slashed.

- Use the `Democracy` pallet to create a proposal that would result in
some tokens being placed on `hold`. The tokens on `hold` would always be
returned to the user at some point in the future, they are not able to
be slashed, as near as I can tell.
- Use the `treasury` pallet to create a spend proposal that would result
in a minimum of 100 tokens being used as a bond and placed on `hold`.
These tokens will be slashed if the proposal is rejected. We can
simulate a rejection using `sudo` and see that the tokens are slashed.
- Validators and their Nominators can have their stake slashed. This is
done using `hold`.

However, experimentation with `hold` shows that conditions where `hold`
and `frozen` overlap cause the transactions to fail.

Tests were added to make sure this behavior does not change in the
future.

# Changes
Two tests were added: 
1. Create a treasury spend proposal and then attempt to stake with an
amount that would overlap the `hold`. This transaction fails.
2. Create a stake and then attempt to create a treasury spend proposal
that would require an amount of tokens that would overlap with the
existing stake. This transaction fails.

# How to Test

- Confirm that the e2e tests pass.


# Checklist
- [ ] Chain spec updated
- [ ] Custom RPC OR Runtime API added/changed? Updated js/api-augment.
- [ ] Design doc(s) updated
- [x] Tests added
- [ ] Benchmarks added
- [ ] Weights updated

---------

Co-authored-by: Matthew Orris <--help>
  • Loading branch information
mattheworris authored Feb 14, 2024
1 parent d4d450d commit 03bc60e
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/common/codecov/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ runs:
steps:
- name: Install grcov
shell: bash
run: cargo +nightly-2023-07-13 install grcov
run: cargo +nightly-2023-07-13 install --locked grcov
- name: Build
shell: bash # Limited to 10 threads max
run: cargo +nightly-2023-07-13 build -j 10 --features frequency-lint-check
Expand Down
16 changes: 16 additions & 0 deletions e2e/scaffolding/extrinsicHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -795,4 +795,20 @@ export class ExtrinsicHelper {
currentBlock = await getBlockNumber();
}
}

public static submitProposal(keys: KeyringPair, spendAmount: AnyNumber | Compact<u128>) {
return new Extrinsic(
() => ExtrinsicHelper.api.tx.treasury.proposeSpend(spendAmount, keys.address),
keys,
ExtrinsicHelper.api.events.treasury.Proposed
);
}

public static rejectProposal(keys: KeyringPair, proposalId: any) {
return new Extrinsic(
() => ExtrinsicHelper.api.tx.treasury.rejectProposal(proposalId),
keys,
ExtrinsicHelper.api.events.treasury.Rejected
);
}
}
76 changes: 75 additions & 1 deletion e2e/sudo/sudo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import { Extrinsic, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
import { isTestnet } from '../scaffolding/env';
import { getSudo, getFundingSource } from '../scaffolding/funding';
import { AVRO_GRAPH_CHANGE } from '../schemas/fixtures/avroGraphChangeSchemaType';
import { Bytes, u16 } from '@polkadot/types';
import { Bytes, u16, u64 } from '@polkadot/types';
import {
DOLLARS,
createDelegatorAndDelegation,
createProviderKeysAndId,
getCurrentItemizedHash,
generateSchemaPartialName,
createKeys,
createMsaAndProvider,
} from '../scaffolding/helpers';
import { AVRO_CHAT_MESSAGE } from '../stateful-pallet-storage/fixtures/itemizedSchemaType';
import { stakeToProvider } from '../scaffolding/helpers';

describe('Sudo required', function () {
let sudoKey: KeyringPair;
Expand Down Expand Up @@ -179,6 +182,77 @@ describe('Sudo required', function () {
});
});
});

describe('Capacity should not be affected by a hold being slashed', function () {
it('stake should fail when overlapping tokens are on hold', async function () {
const accountBalance: bigint = 122n * DOLLARS;
const stakeBalance: bigint = 100n * DOLLARS;
const spendBalance: bigint = 20n * DOLLARS;
const proposalBond: bigint = 100n * DOLLARS;

// Setup some keys and a provider for capacity staking
const stakeKeys: KeyringPair = createKeys('StakeKeys');
const stakeProviderId: u64 = await createMsaAndProvider(
fundingSource,
stakeKeys,
'StakeProvider',
accountBalance
);

// Create a treasury proposal which will result in a hold with minimum bond = 100 DOLLARS
const proposalExt = ExtrinsicHelper.submitProposal(stakeKeys, spendBalance);
const { target: proposalEvent } = await proposalExt.signAndSend();
assert.notEqual(proposalEvent, undefined, 'should return a Proposal event');

// Confirm that the tokens were reserved/hold in the stakeKeys account using the query API
let stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys.address);
assert.equal(
stakedAcctInfo.data.reserved,
proposalBond,
`expected ${proposalBond} reserved balance, got ${stakedAcctInfo.data.reserved}`
);

// Create a stake that will result in overlapping tokens being frozen
// stake will allow only the balance not on hold to be staked
await assert.rejects(stakeToProvider(fundingSource, stakeKeys, stakeProviderId, stakeBalance));

// Slash the provider
const slashExt = ExtrinsicHelper.rejectProposal(sudoKey, proposalEvent?.data.proposalIndex);
const { target: slashEvent } = await slashExt.sudoSignAndSend();
assert.notEqual(slashEvent, undefined, 'should return a Treasury event');

// Confirm that the tokens were slashed from the stakeKeys account using the query API
stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys.address);
assert.equal(
stakedAcctInfo.data.reserved,
0n,
`expected 0 reserved balance, got ${stakedAcctInfo.data.reserved}`
);
});

it('proposal should fail when overlapping tokens are on hold', async function () {
const accountBalance: bigint = 122n * DOLLARS;
const stakeBalance: bigint = 100n * DOLLARS;
const spendBalance: bigint = 20n * DOLLARS;

// Setup some keys and a provider for capacity staking
const stakeKeys: KeyringPair = createKeys('StakeKeys');
const stakeProviderId: u64 = await createMsaAndProvider(
fundingSource,
stakeKeys,
'StakeProvider',
accountBalance
);

// Create a stake that will result in overlapping tokens being frozen
await assert.doesNotReject(stakeToProvider(fundingSource, stakeKeys, stakeProviderId, stakeBalance));

// Create a treasury proposal which will result in a hold with minimum bond = 100 DOLLARS
// The proposal should fail because the stakeKeys account has overlapping tokens frozen
const proposalExt = ExtrinsicHelper.submitProposal(stakeKeys, spendBalance);
await assert.rejects(proposalExt.signAndSend());
});
});
});
});
});

0 comments on commit 03bc60e

Please sign in to comment.