Skip to content

Commit 03bc60e

Browse files
authored
Investigate fungible trait Holds and Freezes overlap (#1873)
# 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>
1 parent d4d450d commit 03bc60e

File tree

3 files changed

+92
-2
lines changed

3 files changed

+92
-2
lines changed

.github/workflows/common/codecov/action.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ runs:
99
steps:
1010
- name: Install grcov
1111
shell: bash
12-
run: cargo +nightly-2023-07-13 install grcov
12+
run: cargo +nightly-2023-07-13 install --locked grcov
1313
- name: Build
1414
shell: bash # Limited to 10 threads max
1515
run: cargo +nightly-2023-07-13 build -j 10 --features frequency-lint-check

e2e/scaffolding/extrinsicHelpers.ts

+16
Original file line numberDiff line numberDiff line change
@@ -795,4 +795,20 @@ export class ExtrinsicHelper {
795795
currentBlock = await getBlockNumber();
796796
}
797797
}
798+
799+
public static submitProposal(keys: KeyringPair, spendAmount: AnyNumber | Compact<u128>) {
800+
return new Extrinsic(
801+
() => ExtrinsicHelper.api.tx.treasury.proposeSpend(spendAmount, keys.address),
802+
keys,
803+
ExtrinsicHelper.api.events.treasury.Proposed
804+
);
805+
}
806+
807+
public static rejectProposal(keys: KeyringPair, proposalId: any) {
808+
return new Extrinsic(
809+
() => ExtrinsicHelper.api.tx.treasury.rejectProposal(proposalId),
810+
keys,
811+
ExtrinsicHelper.api.events.treasury.Rejected
812+
);
813+
}
798814
}

e2e/sudo/sudo.test.ts

+75-1
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ import { Extrinsic, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
88
import { isTestnet } from '../scaffolding/env';
99
import { getSudo, getFundingSource } from '../scaffolding/funding';
1010
import { AVRO_GRAPH_CHANGE } from '../schemas/fixtures/avroGraphChangeSchemaType';
11-
import { Bytes, u16 } from '@polkadot/types';
11+
import { Bytes, u16, u64 } from '@polkadot/types';
1212
import {
1313
DOLLARS,
1414
createDelegatorAndDelegation,
1515
createProviderKeysAndId,
1616
getCurrentItemizedHash,
1717
generateSchemaPartialName,
18+
createKeys,
19+
createMsaAndProvider,
1820
} from '../scaffolding/helpers';
1921
import { AVRO_CHAT_MESSAGE } from '../stateful-pallet-storage/fixtures/itemizedSchemaType';
22+
import { stakeToProvider } from '../scaffolding/helpers';
2023

2124
describe('Sudo required', function () {
2225
let sudoKey: KeyringPair;
@@ -179,6 +182,77 @@ describe('Sudo required', function () {
179182
});
180183
});
181184
});
185+
186+
describe('Capacity should not be affected by a hold being slashed', function () {
187+
it('stake should fail when overlapping tokens are on hold', async function () {
188+
const accountBalance: bigint = 122n * DOLLARS;
189+
const stakeBalance: bigint = 100n * DOLLARS;
190+
const spendBalance: bigint = 20n * DOLLARS;
191+
const proposalBond: bigint = 100n * DOLLARS;
192+
193+
// Setup some keys and a provider for capacity staking
194+
const stakeKeys: KeyringPair = createKeys('StakeKeys');
195+
const stakeProviderId: u64 = await createMsaAndProvider(
196+
fundingSource,
197+
stakeKeys,
198+
'StakeProvider',
199+
accountBalance
200+
);
201+
202+
// Create a treasury proposal which will result in a hold with minimum bond = 100 DOLLARS
203+
const proposalExt = ExtrinsicHelper.submitProposal(stakeKeys, spendBalance);
204+
const { target: proposalEvent } = await proposalExt.signAndSend();
205+
assert.notEqual(proposalEvent, undefined, 'should return a Proposal event');
206+
207+
// Confirm that the tokens were reserved/hold in the stakeKeys account using the query API
208+
let stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys.address);
209+
assert.equal(
210+
stakedAcctInfo.data.reserved,
211+
proposalBond,
212+
`expected ${proposalBond} reserved balance, got ${stakedAcctInfo.data.reserved}`
213+
);
214+
215+
// Create a stake that will result in overlapping tokens being frozen
216+
// stake will allow only the balance not on hold to be staked
217+
await assert.rejects(stakeToProvider(fundingSource, stakeKeys, stakeProviderId, stakeBalance));
218+
219+
// Slash the provider
220+
const slashExt = ExtrinsicHelper.rejectProposal(sudoKey, proposalEvent?.data.proposalIndex);
221+
const { target: slashEvent } = await slashExt.sudoSignAndSend();
222+
assert.notEqual(slashEvent, undefined, 'should return a Treasury event');
223+
224+
// Confirm that the tokens were slashed from the stakeKeys account using the query API
225+
stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys.address);
226+
assert.equal(
227+
stakedAcctInfo.data.reserved,
228+
0n,
229+
`expected 0 reserved balance, got ${stakedAcctInfo.data.reserved}`
230+
);
231+
});
232+
233+
it('proposal should fail when overlapping tokens are on hold', async function () {
234+
const accountBalance: bigint = 122n * DOLLARS;
235+
const stakeBalance: bigint = 100n * DOLLARS;
236+
const spendBalance: bigint = 20n * DOLLARS;
237+
238+
// Setup some keys and a provider for capacity staking
239+
const stakeKeys: KeyringPair = createKeys('StakeKeys');
240+
const stakeProviderId: u64 = await createMsaAndProvider(
241+
fundingSource,
242+
stakeKeys,
243+
'StakeProvider',
244+
accountBalance
245+
);
246+
247+
// Create a stake that will result in overlapping tokens being frozen
248+
await assert.doesNotReject(stakeToProvider(fundingSource, stakeKeys, stakeProviderId, stakeBalance));
249+
250+
// Create a treasury proposal which will result in a hold with minimum bond = 100 DOLLARS
251+
// The proposal should fail because the stakeKeys account has overlapping tokens frozen
252+
const proposalExt = ExtrinsicHelper.submitProposal(stakeKeys, spendBalance);
253+
await assert.rejects(proposalExt.signAndSend());
254+
});
255+
});
182256
});
183257
});
184258
});

0 commit comments

Comments
 (0)