From 7232705bc81234063696a284d6a75086b53e948b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Wed, 4 Mar 2026 16:12:10 +0000 Subject: [PATCH 01/13] Begin refactor of governance e2e tests --- packages/shared/src/governance.ts | 235 ++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) diff --git a/packages/shared/src/governance.ts b/packages/shared/src/governance.ts index 802d77fba..e9d879f5e 100644 --- a/packages/shared/src/governance.ts +++ b/packages/shared/src/governance.ts @@ -86,6 +86,236 @@ function referendumCmp( /// ------- /// ------- +/** + * Test the process of submitting a referendum for a treasury spend + */ +export async function submitReferendumTest< + TCustom extends Record | undefined, + TInitStorages extends Record> | undefined, +>(chain: Chain) { + const [client] = await setupNetworks(chain) + + // Fund test accounts not already provisioned in the test chain spec. + await client.dev.setStorage({ + System: { + account: [ + [[devAccounts.alice.address], { providers: 1, data: { free: 10e10 } }], + [[devAccounts.bob.address], { providers: 1, data: { free: 10e10 } }], + ], + }, + }) + + // Get the referendum's intended track data + + const referendaTracks = client.api.consts.referenda.tracks + const smallTipper = referendaTracks.find((track) => track[1].name.eq('small_tipper'))! + + /** + * Get current referendum count i.e. the next referendum's index + */ + const referendumIndex = await client.api.query.referenda.referendumCount() + + /** + * Submit a new referendum + */ + const submissionTx = client.api.tx.referenda.submit( + { + Origins: 'SmallTipper', + } as any, + { + Inline: client.api.tx.system.remark('hello').method.toHex(), + }, + { + After: 1, + }, + ) + const submissionEvents = await sendTransaction(submissionTx.signAsync(devAccounts.alice)) + + await client.dev.newBlock() + + // Fields to be removed, check comment below. + const unwantedFields = /index/ + await checkEvents(submissionEvents, 'referenda') + .redact({ removeKeys: unwantedFields }) + .toMatchSnapshot('referendum submission events') + + /** + * Check the created referendum's data + */ + let referendumDataOpt: Option = + await client.api.query.referenda.referendumInfoFor(referendumIndex) + assert(referendumDataOpt.isSome, "submitted referendum's data cannot be `None`") + let referendumData: PalletReferendaReferendumInfoConvictionVotingTally = referendumDataOpt.unwrap() + + // These fields must be excised from the queried referendum data before being put in the test + // snapshot. + // These fields contain epoch-sensitive data, which will cause spurious test failures + // periodically. + const unwantedFields2 = /alarm|submitted/ + await check(referendumData) + .redact({ removeKeys: unwantedFields2 }) + .toMatchSnapshot('referendum info after submission') + + expect(referendumData.isOngoing).toBe(true) + // Ongoing referendum data, prior to the decision deposit. + const ongoingReferendum: PalletReferendaReferendumStatusConvictionVotingTally = referendumData.asOngoing + + // Check the entirety of the stored referendum's data + + expect(ongoingReferendum.track.toNumber()).toBe(smallTipper[0].toNumber()) + expect(ongoingReferendum.origin.toJSON()).toMatchObject({ origins: 'SmallTipper' }) + + expect(ongoingReferendum.proposal.asInline.toHex()).toBe(client.api.tx.system.remark('hello').method.toHex()) + + // The referendum was above set to be enacted 1 block after its passing. + expect(ongoingReferendum.enactment.isAfter).toBe(true) + expect(ongoingReferendum.enactment.asAfter.toNumber()).toBe(1) + + // Check submission block + const currentBlock = (await client.api.rpc.chain.getHeader()).number.toNumber() + expect(ongoingReferendum.submitted.toNumber()).toBe(currentBlock) + + expect(ongoingReferendum.submissionDeposit.who.toString()).toBe( + encodeAddress(devAccounts.alice.address, chain.properties.addressEncoding), + ) + expect(ongoingReferendum.submissionDeposit.amount.toNumber()).toBe( + client.api.consts.referenda.submissionDeposit.toNumber(), + ) + + // Immediately after a referendum's submission, it will not have a decision deposit, + // which it will need to begin the decision period. + expect(ongoingReferendum.decisionDeposit.isNone).toBe(true) + expect(ongoingReferendum.deciding.isNone).toBe(true) + + // Current voting state of the referendum. + const votes = { + ayes: 0, + nays: 0, + support: 0, + } + + // Check that voting data is empty + await check(ongoingReferendum.tally).toMatchObject(votes) + + // The referendum should not have been put in a queue - this test assumes there's room in the referendum's + // track. + expect(ongoingReferendum.inQueue.isFalse).toBe(true) + + // Check the alarm + expect(ongoingReferendum.alarm.isSome).toBe(true) + const undecidingTimeoutAlarm = ongoingReferendum.alarm.unwrap()[0] + const blocksUntilAlarm = undecidingTimeoutAlarm.sub(ongoingReferendum.submitted) + // Check that the referendum's alarm is set to ring after the (globally predetermined) timeout + // of 14 days, or 201600 blocks. + expect(blocksUntilAlarm.toNumber()).toBe(client.api.consts.referenda.undecidingTimeout.toNumber()) + const alarm = [undecidingTimeoutAlarm, [undecidingTimeoutAlarm, 0]] + expect(ongoingReferendum.alarm.unwrap().eq(alarm)).toBe(true) + + // Modify the referendum to simulate a timeout caused by an unplaced decision deposit. + + const undecidedTimeout = undecidingTimeoutAlarm.sub(ongoingReferendum.submitted) + const newSubmitted = 1 + (currentBlock - undecidedTimeout.toNumber()) + + await client.dev.setStorage({ + Referenda: { + ReferendumInfoFor: [ + [ + [referendumIndex], + { + Ongoing: { + track: ongoingReferendum.track, + origin: ongoingReferendum.origin, + proposal: ongoingReferendum.proposal, + enactment: ongoingReferendum.enactment, + submitted: newSubmitted, + submissionDeposit: ongoingReferendum.submissionDeposit, + decisionDeposit: ongoingReferendum.decisionDeposit, + deciding: ongoingReferendum.deciding, + tally: ongoingReferendum.tally, + inQueue: ongoingReferendum.inQueue, + alarm: [newSubmitted + undecidedTimeout.toNumber(), [newSubmitted + undecidedTimeout.toNumber(), 0]], + }, + }, + ], + ], + }, + // An accompanying nudge call must also be scheduled, otherwise the above referendum will not be serviced. + Scheduler: { + Agenda: [ + [ + [currentBlock + 1], + [ + { + call: { Inline: client.api.tx.referenda.nudgeReferendum(referendumIndex).method.toHex() }, + origin: { system: 'Root' }, + }, + ], + ], + ], + }, + }) + + await client.dev.newBlock() + + // Check event for the timed-out referendum + let events = await client.api.query.system.events() + + const referendaEvents = events.filter((record) => { + const { event } = record + return event.section === 'referenda' + }) + + expect(referendaEvents.length, 'cancelling a referendum should emit 1 event').toBe(1) + + const timedOutEvent = referendaEvents[0] + expect(client.api.events.referenda.TimedOut.is(timedOutEvent.event)).toBe(true) + + await check(timedOutEvent).toMatchSnapshot('timed-out referendum event') + + // Check the timed-out referendum's data + + referendumDataOpt = await client.api.query.referenda.referendumInfoFor(referendumIndex) + assert(referendumDataOpt.isSome, "submitted referendum's data cannot be `None`") + referendumData = referendumDataOpt.unwrap() + expect(referendumData.isTimedOut).toBe(true) + + const timedOutRef: ITuple<[u32, Option, Option]> = + referendumData.asTimedOut + + expect( + timedOutRef.eq([ + newSubmitted + undecidedTimeout.toNumber(), + { + who: encodeAddress(defaultAccountsSr25519.alice.address, chain.properties.addressEncoding), + amount: client.api.consts.referenda.submissionDeposit, + }, + null, + ]), + ).toBe(true) + + // Attempt to refund the submission deposit + + const refundTx = client.api.tx.referenda.refundSubmissionDeposit(referendumIndex) + await sendTransaction(refundTx.signAsync(devAccounts.alice)) + + await client.dev.newBlock() + + events = await client.api.query.system.events() + + const refundEvents = events.filter((record) => { + const { event } = record + return event.section === 'system' && event.method === 'ExtrinsicFailed' + }) + + // Timed out referenda cannot have their submission deposit refunded. + const refundEvent = refundEvents[0] + assert(client.api.events.system.ExtrinsicFailed.is(refundEvent.event)) + const dispatchError = refundEvent.event.data.dispatchError + assert(dispatchError.isModule) + + expect(client.api.errors.referenda.BadStatus.is(dispatchError.asModule)).toBe(true) +} + /** * Test the process of * 1. submitting a referendum for a treasury spend @@ -896,6 +1126,11 @@ export function baseGovernanceE2ETests< kind: 'describe', label: 'referenda tests', children: [ + { + kind: 'test', + label: 'referendum submission and timeout', + testFn: async () => await submitReferendumTest(chain), + }, { kind: 'test', label: 'referendum lifecycle test - submission, decision deposit, various voting should all work', From 4f63b81482137604515cb5d0826cd23292819283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Thu, 19 Mar 2026 00:58:22 +0000 Subject: [PATCH 02/13] Test low approval/support ref failures --- packages/shared/src/governance.ts | 548 +++++++++++++++++++++++++++--- 1 file changed, 495 insertions(+), 53 deletions(-) diff --git a/packages/shared/src/governance.ts b/packages/shared/src/governance.ts index e9d879f5e..9954a6180 100644 --- a/packages/shared/src/governance.ts +++ b/packages/shared/src/governance.ts @@ -1,7 +1,7 @@ import { sendTransaction } from '@acala-network/chopsticks-testing' import { type Chain, defaultAccountsSr25519 } from '@e2e-test/networks' -import { type RootTestTree, setupNetworks } from '@e2e-test/shared' +import { type DescribeNode, type RootTestTree, setupNetworks, type TestNode } from '@e2e-test/shared' import type { Option, u32 } from '@polkadot/types' import type { @@ -25,9 +25,24 @@ import { getBlockNumber, objectCmp, scheduleInlineCallWithOrigin, - type TestConfig, } from './helpers/index.js' +/// ------- +/// Types +/// ------- + +export interface GovernanceTrackConfig { + trackId: number + trackName: string + /** Must match the runtime's `Origins` enum variant (e.g. `'SmallTipper'`). */ + originName: string +} + +export interface GovernanceTestConfig { + testSuiteName: string + tracks: GovernanceTrackConfig[] +} + /// ------- /// Helpers /// ------- @@ -95,11 +110,12 @@ export async function submitReferendumTest< >(chain: Chain) { const [client] = await setupNetworks(chain) - // Fund test accounts not already provisioned in the test chain spec. + // Fund test accounts with enough for submission deposit + fees. + const submissionDeposit = client.api.consts.referenda.submissionDeposit.toBigInt() await client.dev.setStorage({ System: { account: [ - [[devAccounts.alice.address], { providers: 1, data: { free: 10e10 } }], + [[devAccounts.alice.address], { providers: 1, data: { free: (submissionDeposit * 10n).toString() } }], [[devAccounts.bob.address], { providers: 1, data: { free: 10e10 } }], ], }, @@ -108,12 +124,13 @@ export async function submitReferendumTest< // Get the referendum's intended track data const referendaTracks = client.api.consts.referenda.tracks - const smallTipper = referendaTracks.find((track) => track[1].name.eq('small_tipper'))! + const smallTipper = referendaTracks.find((track) => track[1].name.toString().startsWith('small_tipper'))! - /** - * Get current referendum count i.e. the next referendum's index - */ - const referendumIndex = await client.api.query.referenda.referendumCount() + // Flush any pre-existing scheduled calls from the fork block so they don't + // interfere with our referendum index. + await client.dev.newBlock() + + const referendumIndex = (await client.api.query.referenda.referendumCount()).toNumber() /** * Submit a new referendum @@ -142,8 +159,9 @@ export async function submitReferendumTest< /** * Check the created referendum's data */ - let referendumDataOpt: Option = - await client.api.query.referenda.referendumInfoFor(referendumIndex) + let referendumDataOpt = (await client.api.query.referenda.referendumInfoFor( + referendumIndex, + )) as unknown as Option assert(referendumDataOpt.isSome, "submitted referendum's data cannot be `None`") let referendumData: PalletReferendaReferendumInfoConvictionVotingTally = referendumDataOpt.unwrap() @@ -171,10 +189,6 @@ export async function submitReferendumTest< expect(ongoingReferendum.enactment.isAfter).toBe(true) expect(ongoingReferendum.enactment.asAfter.toNumber()).toBe(1) - // Check submission block - const currentBlock = (await client.api.rpc.chain.getHeader()).number.toNumber() - expect(ongoingReferendum.submitted.toNumber()).toBe(currentBlock) - expect(ongoingReferendum.submissionDeposit.who.toString()).toBe( encodeAddress(devAccounts.alice.address, chain.properties.addressEncoding), ) @@ -211,10 +225,21 @@ export async function submitReferendumTest< const alarm = [undecidingTimeoutAlarm, [undecidingTimeoutAlarm, 0]] expect(ongoingReferendum.alarm.unwrap().eq(alarm)).toBe(true) - // Modify the referendum to simulate a timeout caused by an unplaced decision deposit. + /** + * Simulate a timeout caused by an unplaced decision deposit. + * + * 1. backdate `submitted` so that `submitted + undecidingTimeout` falls on the next local block + * 2. set the alarm to that same block + * 3. schedule a `nudgeReferendum` call so the runtime services the alarm + * + * The referendum's timing fields (`submitted`, alarm) use local block numbers. + * The scheduler may use a different provider, so the nudge is scheduled via the helper. + */ - const undecidedTimeout = undecidingTimeoutAlarm.sub(ongoingReferendum.submitted) - const newSubmitted = 1 + (currentBlock - undecidedTimeout.toNumber()) + const undecidedTimeout = client.api.consts.referenda.undecidingTimeout.toNumber() + const localBlock = (await client.api.rpc.chain.getHeader()).number.toNumber() + const alarmBlock = localBlock + 1 + const newSubmitted = alarmBlock - undecidedTimeout await client.dev.setStorage({ Referenda: { @@ -233,48 +258,42 @@ export async function submitReferendumTest< deciding: ongoingReferendum.deciding, tally: ongoingReferendum.tally, inQueue: ongoingReferendum.inQueue, - alarm: [newSubmitted + undecidedTimeout.toNumber(), [newSubmitted + undecidedTimeout.toNumber(), 0]], + alarm: [alarmBlock, [alarmBlock, 0]], }, }, ], ], }, - // An accompanying nudge call must also be scheduled, otherwise the above referendum will not be serviced. - Scheduler: { - Agenda: [ - [ - [currentBlock + 1], - [ - { - call: { Inline: client.api.tx.referenda.nudgeReferendum(referendumIndex).method.toHex() }, - origin: { system: 'Root' }, - }, - ], - ], - ], - }, }) + // Schedule the nudge via the helper — it handles non-local block number providers correctly. + await scheduleInlineCallWithOrigin( + client, + client.api.tx.referenda.nudgeReferendum(referendumIndex).method.toHex(), + { system: 'Root' }, + chain.properties.schedulerBlockProvider, + ) + await client.dev.newBlock() // Check event for the timed-out referendum let events = await client.api.query.system.events() - const referendaEvents = events.filter((record) => { - const { event } = record - return event.section === 'referenda' - }) + const timedOutEvents = events.filter( + ({ event }) => client.api.events.referenda.TimedOut.is(event) && event.data[0].toNumber() === referendumIndex, + ) - expect(referendaEvents.length, 'cancelling a referendum should emit 1 event').toBe(1) + expect(timedOutEvents.length, 'timing out a referendum should emit 1 TimedOut event').toBe(1) - const timedOutEvent = referendaEvents[0] - expect(client.api.events.referenda.TimedOut.is(timedOutEvent.event)).toBe(true) + const timedOutEvent = timedOutEvents[0] await check(timedOutEvent).toMatchSnapshot('timed-out referendum event') // Check the timed-out referendum's data - referendumDataOpt = await client.api.query.referenda.referendumInfoFor(referendumIndex) + referendumDataOpt = (await client.api.query.referenda.referendumInfoFor( + referendumIndex, + )) as unknown as Option assert(referendumDataOpt.isSome, "submitted referendum's data cannot be `None`") referendumData = referendumDataOpt.unwrap() expect(referendumData.isTimedOut).toBe(true) @@ -282,16 +301,13 @@ export async function submitReferendumTest< const timedOutRef: ITuple<[u32, Option, Option]> = referendumData.asTimedOut - expect( - timedOutRef.eq([ - newSubmitted + undecidedTimeout.toNumber(), - { - who: encodeAddress(defaultAccountsSr25519.alice.address, chain.properties.addressEncoding), - amount: client.api.consts.referenda.submissionDeposit, - }, - null, - ]), - ).toBe(true) + // [end_block, submission_deposit, decision_deposit] + expect(timedOutRef[1].isSome, 'submission deposit should be present after timeout').toBe(true) + expect(timedOutRef[1].unwrap().who.toString()).toBe( + encodeAddress(defaultAccountsSr25519.alice.address, chain.properties.addressEncoding), + ) + expect(timedOutRef[1].unwrap().amount.toBigInt()).toBe(client.api.consts.referenda.submissionDeposit.toBigInt()) + expect(timedOutRef[2].isNone, 'decision deposit should be absent (never placed)').toBe(true) // Attempt to refund the submission deposit @@ -1114,13 +1130,437 @@ export async function referendumLifecycleKillTest< }) } +/** + * Shared preamble for negative-outcome tests: + * 1. submitting a referendum on the given track + * 2. placing its decision deposit + * 3. reading back the created referendum's data + */ +async function submitAndDeposit( + client: Awaited>[0], + trackConfig: GovernanceTrackConfig, +) { + const referendaTracks = client.api.consts.referenda.tracks + const track = referendaTracks.find((t) => t[0].toNumber() === trackConfig.trackId) + assert(track, `Track '${trackConfig.trackName}' (ID ${trackConfig.trackId}) not found in runtime`) + + /** + * 1. Submit a new referendum on the given track + */ + + const referendumIndex = (await client.api.query.referenda.referendumCount()).toNumber() + + const submissionTx = client.api.tx.referenda.submit( + { Origins: trackConfig.originName } as any, + { Inline: client.api.tx.system.remark('hello').method.toHex() }, + { After: 1 }, + ) + await sendTransaction(submissionTx.signAsync(devAccounts.alice)) + await client.dev.newBlock() + + /** + * 2. Place decision deposit + */ + + const decisionDepTx = client.api.tx.referenda.placeDecisionDeposit(referendumIndex) + await sendTransaction(decisionDepTx.signAsync(devAccounts.bob)) + await client.dev.newBlock() + + /** + * 3. Check the created referendum's data + */ + + const referendumDataOpt: Option = + await client.api.query.referenda.referendumInfoFor(referendumIndex) + assert(referendumDataOpt.isSome, "referendum's data cannot be `None` after submission + deposit") + const referendumData = referendumDataOpt.unwrap() + assert(referendumData.isOngoing) + + return { referendumIndex, ongoing: referendumData.asOngoing, track } +} + +/** + * Fast-forward a referendum to the end of its decision period via storage injection: + * 1. backdating `submitted` and `deciding.since` so the decision period has elapsed by the next block + * 2. scheduling a `nudgeReferendum` call via the scheduler + */ +async function injectDecisionPeriodEnd( + client: Awaited>[0], + chain: Chain, + referendumIndex: number, + ongoing: PalletReferendaReferendumStatusConvictionVotingTally, + track: (typeof client.api.consts.referenda.tracks)[number], +) { + const currentBlock = (await client.api.rpc.chain.getHeader()).number.toNumber() + const decisionPeriod = track[1].decisionPeriod.toNumber() + const prepPeriod = track[1].preparePeriod.toNumber() + + /** + * 1. Backdate the referendum so the decision period has elapsed by the next block + * + * Runtime rejects when: now >= deciding.since + decisionPeriod + * Next block's local number: currentBlock + 1 + */ + + const decidingSince = currentBlock + 1 - decisionPeriod + const newSubmitted = decidingSince - prepPeriod + + await client.dev.setStorage({ + Referenda: { + ReferendumInfoFor: [ + [ + [referendumIndex], + { + Ongoing: { + track: ongoing.track, + origin: ongoing.origin, + proposal: ongoing.proposal, + enactment: ongoing.enactment, + submitted: newSubmitted, + submissionDeposit: ongoing.submissionDeposit, + decisionDeposit: ongoing.decisionDeposit, + deciding: { since: decidingSince, confirming: null }, + tally: ongoing.tally, + inQueue: ongoing.inQueue, + alarm: [currentBlock + 1, [currentBlock + 1, 0]], + }, + }, + ], + ], + }, + }) + + /** + * 2. Schedule a nudge for the next block so the runtime evaluates the referendum + */ + + await scheduleInlineCallWithOrigin( + client, + client.api.tx.referenda.nudgeReferendum(referendumIndex).method.toHex(), + { system: 'Root' }, + chain.properties.schedulerBlockProvider, + ) +} + +/** + * Post-rejection verification: + * 1. checking that the `Rejected` event was emitted + * 2. checking the rejected referendum's data + * 3. refunding the decision deposit (should succeed) + * 4. attempting to refund the submission deposit (should fail with `BadStatus`) + */ +async function verifyRejection( + client: Awaited>[0], + referendumIndex: number, + trackLabel: string, + scenarioLabel: string, +) { + /** + * 1. Check the `Rejected` event + */ + + const events = await client.api.query.system.events() + const referendaEvents = events.filter(({ event }) => event.section === 'referenda') + + expect(referendaEvents.length, 'rejecting a referendum should emit 1 referenda event').toBe(1) + const rejectedEvent = referendaEvents[0] + expect(client.api.events.referenda.Rejected.is(rejectedEvent.event)).toBe(true) + + await check(rejectedEvent) + .redact({ removeKeys: /index|pollIndex/ }) + .toMatchSnapshot(`rejected referendum event (${scenarioLabel}) - ${trackLabel}`) + + /** + * 2. Check the rejected referendum's data + */ + + const referendumDataOpt: Option = + await client.api.query.referenda.referendumInfoFor(referendumIndex) + assert(referendumDataOpt.isSome) + const referendumData = referendumDataOpt.unwrap() + expect(referendumData.isRejected).toBe(true) + + const rejectedRef: ITuple<[u32, Option, Option]> = + referendumData.asRejected + expect(rejectedRef[1].isSome, 'submission deposit should be present after rejection').toBe(true) + expect(rejectedRef[2].isSome, 'decision deposit should be present after rejection').toBe(true) + + /** + * 3. Refund the decision deposit — this should succeed for rejected referenda + */ + + const refundDecisionTx = client.api.tx.referenda.refundDecisionDeposit(referendumIndex) + await sendTransaction(refundDecisionTx.signAsync(devAccounts.bob)) + await client.dev.newBlock() + + await checkSystemEvents(client, { section: 'referenda', method: 'DecisionDepositRefunded' }) + .redact({ removeKeys: /index/ }) + .toMatchSnapshot(`decision deposit refund after rejection (${scenarioLabel}) - ${trackLabel}`) + + /** + * 4. Attempt to refund the submission deposit — this should fail with `BadStatus` + */ + + const refundSubmissionTx = client.api.tx.referenda.refundSubmissionDeposit(referendumIndex) + await sendTransaction(refundSubmissionTx.signAsync(devAccounts.alice)) + await client.dev.newBlock() + + const postRefundEvents = await client.api.query.system.events() + const failedRefundEvent = postRefundEvents.find( + ({ event }) => event.section === 'system' && event.method === 'ExtrinsicFailed', + ) + assert(failedRefundEvent, 'submission deposit refund should have failed for rejected referendum') + assert(client.api.events.system.ExtrinsicFailed.is(failedRefundEvent.event)) + const dispatchError = failedRefundEvent.event.data.dispatchError + assert(dispatchError.isModule) + expect(client.api.errors.referenda.BadStatus.is(dispatchError.asModule)).toBe(true) +} + +/** + * Test the rejection of a referendum due to insufficient support: + * 1. submitting a referendum and placing its decision deposit + * 2. casting a single nay vote (approval = 0 %, support = 0 %) + * 3. fast-forwarding the decision period via storage injection + * 4. verifying the runtime organically rejects the referendum + * + * 4.1 checking the `Rejected` event and referendum storage state + * + * 4.2 checking that the decision deposit can be refunded + * + * 4.3 checking that the submission deposit cannot be refunded (`BadStatus`) + */ +export async function insufficientSupportTest< + TCustom extends Record | undefined, + TInitStorages extends Record> | undefined, +>(chain: Chain, trackConfig: GovernanceTrackConfig) { + const [client] = await setupNetworks(chain) + + const referendaTracks = client.api.consts.referenda.tracks + const track = referendaTracks.find((t) => t[0].toNumber() === trackConfig.trackId)! + const decisionDeposit = track[1].decisionDeposit.toBigInt() + const submissionDeposit = client.api.consts.referenda.submissionDeposit.toBigInt() + + await client.dev.setStorage({ + System: { + account: [ + [[devAccounts.alice.address], { providers: 1, data: { free: (submissionDeposit * 10n).toString() } }], + [[devAccounts.bob.address], { providers: 1, data: { free: (decisionDeposit * 10n).toString() } }], + [[devAccounts.charlie.address], { providers: 1, data: { free: 10e10 } }], + ], + }, + }) + + /** + * 1. Submit referendum and place decision deposit + */ + + const { referendumIndex } = await submitAndDeposit(client, trackConfig) + + /** + * 2. Cast a single nay vote + * + * A nay vote is required: an empty tally (0/0/0) passes approval because + * `Perbill::from_rational(0, 0)` returns 100 % in Substrate. + * + * With only a nay: approval = 0 %, support = 0 %. + */ + + const nayBalance = 1e10 + const nayVoteTx = client.api.tx.convictionVoting.vote(referendumIndex, { + Standard: { + vote: { aye: false, conviction: 'None' }, + balance: nayBalance, + }, + }) + await sendTransaction(nayVoteTx.signAsync(devAccounts.charlie)) + await client.dev.newBlock() + + const postVoteOpt: Option = + await client.api.query.referenda.referendumInfoFor(referendumIndex) + assert(postVoteOpt.isSome) + const postVote = postVoteOpt.unwrap() + assert(postVote.isOngoing) + const ongoingPostVote = postVote.asOngoing + + expect(ongoingPostVote.tally.ayes.toBigInt()).toBe(0n) + expect(ongoingPostVote.tally.support.toBigInt()).toBe(0n) + expect(ongoingPostVote.tally.nays.toBigInt() > 0n).toBe(true) + + /** + * 3. Fast-forward past the decision period and nudge + */ + + await injectDecisionPeriodEnd(client, chain, referendumIndex, ongoingPostVote, track) + await client.dev.newBlock() + + /** + * 4. Verify the referendum was rejected + * + * 4.1 checking the `Rejected` event and referendum storage state + * + * 4.2 checking that the decision deposit can be refunded + * + * 4.3 checking that the submission deposit cannot be refunded (`BadStatus`) + */ + + await verifyRejection(client, referendumIndex, trackConfig.trackName, 'insufficient support') +} + +/** + * Test the rejection of a referendum due to insufficient approval: + * 1. submitting a referendum and placing its decision deposit + * 2. casting an aye vote large enough to clear support (10 % of total issuance) + * 3. casting a nay vote that sinks approval below the 50 % floor (3× the aye) + * 4. checking the tally + * 5. fast-forwarding the decision period via storage injection + * 6. verifying the runtime organically rejects the referendum + * + * 6.1 checking the `Rejected` event and referendum storage state + * + * 6.2 checking that the decision deposit can be refunded + * + * 6.3 checking that the submission deposit cannot be refunded (`BadStatus`) + */ +export async function insufficientApprovalTest< + TCustom extends Record | undefined, + TInitStorages extends Record> | undefined, +>(chain: Chain, trackConfig: GovernanceTrackConfig) { + const [client] = await setupNetworks(chain) + + const referendaTracks = client.api.consts.referenda.tracks + const track = referendaTracks.find((t) => t[0].toNumber() === trackConfig.trackId)! + const decisionDeposit = track[1].decisionDeposit.toBigInt() + const submissionDeposit = client.api.consts.referenda.submissionDeposit.toBigInt() + + const totalIssuance = (await client.api.query.balances.totalIssuance()).toBigInt() + + // aye = 10 % of total issuance (clears support), nay = 3× aye (approval = 25 % < 50 % floor) + const ayeBalance = totalIssuance / 10n + const nayBalance = ayeBalance * 3n + + await client.dev.setStorage({ + System: { + account: [ + [[devAccounts.alice.address], { providers: 1, data: { free: (submissionDeposit * 10n).toString() } }], + [[devAccounts.bob.address], { providers: 1, data: { free: (decisionDeposit * 10n).toString() } }], + [[devAccounts.charlie.address], { providers: 1, data: { free: (ayeBalance * 2n).toString() } }], + [[devAccounts.dave.address], { providers: 1, data: { free: (nayBalance * 2n).toString() } }], + ], + }, + }) + + /** + * 1. Submit referendum and place decision deposit + */ + + const { referendumIndex } = await submitAndDeposit(client, trackConfig) + + /** + * 2. Cast an aye vote large enough to clear support + */ + + const ayeVoteTx = client.api.tx.convictionVoting.vote(referendumIndex, { + Standard: { + vote: { aye: true, conviction: 'Locked1x' }, + balance: ayeBalance.toString(), + }, + }) + await sendTransaction(ayeVoteTx.signAsync(devAccounts.charlie)) + await client.dev.newBlock() + + /** + * 3. Cast a nay vote that sinks approval below the 50 % floor + */ + + const nayVoteTx = client.api.tx.convictionVoting.vote(referendumIndex, { + Standard: { + vote: { aye: false, conviction: 'Locked1x' }, + balance: nayBalance.toString(), + }, + }) + await sendTransaction(nayVoteTx.signAsync(devAccounts.dave)) + await client.dev.newBlock() + + /** + * 4. Check the tally + */ + + const postVoteOpt: Option = + await client.api.query.referenda.referendumInfoFor(referendumIndex) + assert(postVoteOpt.isSome) + const postVote = postVoteOpt.unwrap() + assert(postVote.isOngoing) + const ongoingPostVote = postVote.asOngoing + + expect(ongoingPostVote.tally.ayes.toBigInt()).toBe(ayeBalance) + expect(ongoingPostVote.tally.nays.toBigInt()).toBe(nayBalance) + expect(ongoingPostVote.tally.support.toBigInt()).toBe(ayeBalance) + + /** + * 5. Fast-forward past the decision period and nudge + */ + + await injectDecisionPeriodEnd(client, chain, referendumIndex, ongoingPostVote, track) + await client.dev.newBlock() + + /** + * 6. Verify the referendum was rejected + * + * 6.1 checking the `Rejected` event and referendum storage state + * + * 6.2 checking that the decision deposit can be refunded + * + * 6.3 checking that the submission deposit cannot be refunded (`BadStatus`) + */ + + await verifyRejection(client, referendumIndex, trackConfig.trackName, 'insufficient approval') +} + +/// ------- +/// Test trees +/// ------- + +function insufficientSupportTestTree< + TCustom extends Record | undefined, + TInitStorages extends Record> | undefined, +>(chain: Chain, tracks: GovernanceTrackConfig[]): DescribeNode { + const children: TestNode[] = tracks.map((trackConfig) => ({ + kind: 'test' as const, + label: `insufficient support rejection for ${trackConfig.trackName}`, + testFn: async () => await insufficientSupportTest(chain, trackConfig), + })) + + return { + kind: 'describe', + label: 'insufficient support rejection tests', + children, + } +} + +function insufficientApprovalTestTree< + TCustom extends Record | undefined, + TInitStorages extends Record> | undefined, +>(chain: Chain, tracks: GovernanceTrackConfig[]): DescribeNode { + const children: TestNode[] = tracks.map((trackConfig) => ({ + kind: 'test' as const, + label: `insufficient approval rejection for ${trackConfig.trackName}`, + testFn: async () => await insufficientApprovalTest(chain, trackConfig), + })) + + return { + kind: 'describe', + label: 'insufficient approval rejection tests', + children, + } +} + export function baseGovernanceE2ETests< TCustom extends Record | undefined, TInitStorages extends Record> | undefined, ->(chain: Chain, testConfig: TestConfig): RootTestTree { +>(chain: Chain, govConfig: GovernanceTestConfig): RootTestTree { return { kind: 'describe', - label: testConfig.testSuiteName, + label: govConfig.testSuiteName, children: [ { kind: 'describe', @@ -1143,6 +1583,8 @@ export function baseGovernanceE2ETests< }, ], }, + insufficientSupportTestTree(chain, govConfig.tracks), + insufficientApprovalTestTree(chain, govConfig.tracks), ], } } From 650a79f5e3bd76838fe8af71d99a56924ffb6006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Thu, 19 Mar 2026 00:59:34 +0000 Subject: [PATCH 03/13] Update PAH/KAH governance snaps --- ...assetHubKusama.governance.e2e.test.ts.snap | 128 ++++++++++++++++++ .../src/assetHubKusama.governance.e2e.test.ts | 13 +- ...setHubPolkadot.governance.e2e.test.ts.snap | 128 ++++++++++++++++++ .../assetHubPolkadot.governance.e2e.test.ts | 13 +- 4 files changed, 270 insertions(+), 12 deletions(-) diff --git a/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap b/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap index 7be267fd2..749e730a7 100644 --- a/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap +++ b/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap @@ -1,5 +1,69 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Kusama Asset Hub Governance > insufficient approval rejection tests > insufficient approval rejection for small_tipper > decision deposit refund after rejection (insufficient approval) - small_tipper 1`] = ` +[ + { + "data": { + "amount": "(rounded 33000000000)", + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Kusama Asset Hub Governance > insufficient approval rejection tests > insufficient approval rejection for small_tipper > rejected referendum event (insufficient approval) - small_tipper 1`] = ` +{ + "event": { + "data": [ + 637, + { + "ayes": "0x000000000000000018c59bfc3dbf6d18", + "nays": "0x00000000000000004a50d3f4b93e4748", + "support": "0x000000000000000018c59bfc3dbf6d18", + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Kusama Asset Hub Governance > insufficient support rejection tests > insufficient support rejection for small_tipper > decision deposit refund after rejection (insufficient support) - small_tipper 1`] = ` +[ + { + "data": { + "amount": "(rounded 33000000000)", + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Kusama Asset Hub Governance > insufficient support rejection tests > insufficient support rejection for small_tipper > rejected referendum event (insufficient support) - small_tipper 1`] = ` +{ + "event": { + "data": [ + 637, + { + "ayes": 0, + "nays": 1000000000, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + exports[`Kusama Asset Hub Governance > referenda tests > referendum lifecycle test - submission, decision deposit, various voting should all work > cancelling referendum with signed origin 1`] = ` [ { @@ -532,3 +596,67 @@ exports[`Kusama Asset Hub Governance > referenda tests > referendum lifecycle te }, ] `; + +exports[`Kusama Asset Hub Governance > referenda tests > referendum submission and timeout > referendum info after submission 1`] = ` +{ + "ongoing": { + "deciding": null, + "decisionDeposit": null, + "enactment": { + "after": 1, + }, + "inQueue": false, + "origin": { + "origins": "SmallTipper", + }, + "proposal": { + "inline": "0x00001468656c6c6f", + }, + "submissionDeposit": { + "amount": 33333333333, + "who": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 30, + }, +} +`; + +exports[`Kusama Asset Hub Governance > referenda tests > referendum submission and timeout > referendum submission events 1`] = ` +[ + { + "data": { + "proposal": { + "Inline": "0x00001468656c6c6f", + }, + "track": 30, + }, + "method": "Submitted", + "section": "referenda", + }, +] +`; + +exports[`Kusama Asset Hub Governance > referenda tests > referendum submission and timeout > timed-out referendum event 1`] = ` +{ + "event": { + "data": [ + 637, + { + "ayes": 0, + "nays": 0, + "support": 0, + }, + ], + "index": "0x5c0a", + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; diff --git a/packages/kusama/src/assetHubKusama.governance.e2e.test.ts b/packages/kusama/src/assetHubKusama.governance.e2e.test.ts index 0ddcd84d6..a2c104229 100644 --- a/packages/kusama/src/assetHubKusama.governance.e2e.test.ts +++ b/packages/kusama/src/assetHubKusama.governance.e2e.test.ts @@ -1,8 +1,9 @@ import { assetHubKusama } from '@e2e-test/networks/chains' -import { baseGovernanceE2ETests, registerTestTree } from '@e2e-test/shared' +import { baseGovernanceE2ETests, type GovernanceTestConfig, registerTestTree } from '@e2e-test/shared' -registerTestTree( - baseGovernanceE2ETests(assetHubKusama, { - testSuiteName: 'Kusama Asset Hub Governance', - }), -) +const governanceConfig: GovernanceTestConfig = { + testSuiteName: 'Kusama Asset Hub Governance', + tracks: [{ trackId: 1, trackName: 'small_tipper', originName: 'SmallTipper' }], +} + +registerTestTree(baseGovernanceE2ETests(assetHubKusama, governanceConfig)) diff --git a/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap b/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap index b08e611b2..1d1ae25e5 100644 --- a/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap +++ b/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap @@ -1,5 +1,69 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Polkadot Asset Hub Governance > insufficient approval rejection tests > insufficient approval rejection for small_tipper > decision deposit refund after rejection (insufficient approval) - small_tipper 1`] = ` +[ + { + "data": { + "amount": 10000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Polkadot Asset Hub Governance > insufficient approval rejection tests > insufficient approval rejection for small_tipper > rejected referendum event (insufficient approval) - small_tipper 1`] = ` +{ + "event": { + "data": [ + 1850, + { + "ayes": "0x000000000000000017304fe24dda2a9c", + "nays": "0x00000000000000004590efa6e98e7fd4", + "support": "0x000000000000000017304fe24dda2a9c", + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Polkadot Asset Hub Governance > insufficient support rejection tests > insufficient support rejection for small_tipper > decision deposit refund after rejection (insufficient support) - small_tipper 1`] = ` +[ + { + "data": { + "amount": 10000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Polkadot Asset Hub Governance > insufficient support rejection tests > insufficient support rejection for small_tipper > rejected referendum event (insufficient support) - small_tipper 1`] = ` +{ + "event": { + "data": [ + 1850, + { + "ayes": 0, + "nays": 1000000000, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + exports[`Polkadot Asset Hub Governance > referenda tests > referendum lifecycle test - submission, decision deposit, various voting should all work > cancelling referendum with signed origin 1`] = ` [ { @@ -532,3 +596,67 @@ exports[`Polkadot Asset Hub Governance > referenda tests > referendum lifecycle }, ] `; + +exports[`Polkadot Asset Hub Governance > referenda tests > referendum submission and timeout > referendum info after submission 1`] = ` +{ + "ongoing": { + "deciding": null, + "decisionDeposit": null, + "enactment": { + "after": 1, + }, + "inQueue": false, + "origin": { + "origins": "SmallTipper", + }, + "proposal": { + "inline": "0x00001468656c6c6f", + }, + "submissionDeposit": { + "amount": 100000000000, + "who": "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 30, + }, +} +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > referendum submission and timeout > referendum submission events 1`] = ` +[ + { + "data": { + "proposal": { + "Inline": "0x00001468656c6c6f", + }, + "track": 30, + }, + "method": "Submitted", + "section": "referenda", + }, +] +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > referendum submission and timeout > timed-out referendum event 1`] = ` +{ + "event": { + "data": [ + 1850, + { + "ayes": 0, + "nays": 0, + "support": 0, + }, + ], + "index": "0x3e0a", + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; diff --git a/packages/polkadot/src/assetHubPolkadot.governance.e2e.test.ts b/packages/polkadot/src/assetHubPolkadot.governance.e2e.test.ts index ea27fe80d..ff6200559 100644 --- a/packages/polkadot/src/assetHubPolkadot.governance.e2e.test.ts +++ b/packages/polkadot/src/assetHubPolkadot.governance.e2e.test.ts @@ -1,8 +1,9 @@ import { assetHubPolkadot } from '@e2e-test/networks/chains' -import { baseGovernanceE2ETests, registerTestTree } from '@e2e-test/shared' +import { baseGovernanceE2ETests, type GovernanceTestConfig, registerTestTree } from '@e2e-test/shared' -registerTestTree( - baseGovernanceE2ETests(assetHubPolkadot, { - testSuiteName: 'Polkadot Asset Hub Governance', - }), -) +const governanceConfig: GovernanceTestConfig = { + testSuiteName: 'Polkadot Asset Hub Governance', + tracks: [{ trackId: 1, trackName: 'small_tipper', originName: 'SmallTipper' }], +} + +registerTestTree(baseGovernanceE2ETests(assetHubPolkadot, governanceConfig)) From a10937c7c9d979b534b7fd88eb3aeb2a38cfa474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Thu, 19 Mar 2026 14:05:57 +0000 Subject: [PATCH 04/13] Create track capacity overflow test Also, refactor tests to referenda negative execution into own tree. --- packages/shared/src/governance.ts | 438 ++++++++++++++++++------------ 1 file changed, 257 insertions(+), 181 deletions(-) diff --git a/packages/shared/src/governance.ts b/packages/shared/src/governance.ts index 9954a6180..d2ede8d57 100644 --- a/packages/shared/src/governance.ts +++ b/packages/shared/src/governance.ts @@ -1,7 +1,7 @@ import { sendTransaction } from '@acala-network/chopsticks-testing' import { type Chain, defaultAccountsSr25519 } from '@e2e-test/networks' -import { type DescribeNode, type RootTestTree, setupNetworks, type TestNode } from '@e2e-test/shared' +import { type DescribeNode, type RootTestTree, setupNetworks } from '@e2e-test/shared' import type { Option, u32 } from '@polkadot/types' import type { @@ -34,8 +34,10 @@ import { export interface GovernanceTrackConfig { trackId: number trackName: string - /** Must match the runtime's `Origins` enum variant (e.g. `'SmallTipper'`). */ + /** Must match the runtime's `Origins` enum variant (e.g. `'SmallTipper'`), or a system origin (e.g. `'Root'`). */ originName: string + /** When true, the proposal origin is `{ system: originName }` instead of `{ Origins: originName }`. */ + systemOrigin?: boolean } export interface GovernanceTestConfig { @@ -102,135 +104,89 @@ function referendumCmp( /// ------- /** - * Test the process of submitting a referendum for a treasury spend + * Test the rejection of a referendum due to a missing decision deposit (undeciding timeout): + * 1. submitting a referendum (without placing a decision deposit) + * 2. checking the created referendum's data + * 3. fast-forwarding past the undeciding timeout via storage injection + * 4. verifying the runtime times out the referendum + * + * 4.1 checking the `TimedOut` event and referendum storage state + * + * 4.2 checking that the submission deposit cannot be refunded (`BadStatus`) */ -export async function submitReferendumTest< +export async function missingDecisionDepositTest< TCustom extends Record | undefined, TInitStorages extends Record> | undefined, ->(chain: Chain) { +>(chain: Chain, trackConfig: GovernanceTrackConfig) { const [client] = await setupNetworks(chain) - // Fund test accounts with enough for submission deposit + fees. + const referendaTracks = client.api.consts.referenda.tracks + const track = referendaTracks.find((t) => t[0].toNumber() === trackConfig.trackId)! const submissionDeposit = client.api.consts.referenda.submissionDeposit.toBigInt() + + const decisionDeposit = track[1].decisionDeposit.toBigInt() await client.dev.setStorage({ System: { account: [ [[devAccounts.alice.address], { providers: 1, data: { free: (submissionDeposit * 10n).toString() } }], - [[devAccounts.bob.address], { providers: 1, data: { free: 10e10 } }], + [[devAccounts.bob.address], { providers: 1, data: { free: (decisionDeposit * 10n).toString() } }], ], }, }) - // Get the referendum's intended track data - - const referendaTracks = client.api.consts.referenda.tracks - const smallTipper = referendaTracks.find((track) => track[1].name.toString().startsWith('small_tipper'))! - - // Flush any pre-existing scheduled calls from the fork block so they don't - // interfere with our referendum index. - await client.dev.newBlock() - - const referendumIndex = (await client.api.query.referenda.referendumCount()).toNumber() - /** - * Submit a new referendum + * 1. Submit a referendum and place its decision deposit, then immediately refund it + * + * We go through submitAndDeposit to reliably get a referendum index (the fork block + * may create other referenda in the same block), then undo the deposit via storage + * to simulate the "no decision deposit" scenario. */ - const submissionTx = client.api.tx.referenda.submit( - { - Origins: 'SmallTipper', - } as any, - { - Inline: client.api.tx.system.remark('hello').method.toHex(), - }, - { - After: 1, - }, - ) - const submissionEvents = await sendTransaction(submissionTx.signAsync(devAccounts.alice)) - await client.dev.newBlock() + const { referendumIndex, ongoing: ongoingWithDeposit } = await submitAndDeposit(client, trackConfig) - // Fields to be removed, check comment below. - const unwantedFields = /index/ - await checkEvents(submissionEvents, 'referenda') - .redact({ removeKeys: unwantedFields }) - .toMatchSnapshot('referendum submission events') + await client.dev.setStorage({ + Referenda: { + ReferendumInfoFor: [ + [ + [referendumIndex], + { + Ongoing: { + track: ongoingWithDeposit.track, + origin: ongoingWithDeposit.origin, + proposal: ongoingWithDeposit.proposal, + enactment: ongoingWithDeposit.enactment, + submitted: ongoingWithDeposit.submitted, + submissionDeposit: ongoingWithDeposit.submissionDeposit, + decisionDeposit: null, + deciding: null, + tally: ongoingWithDeposit.tally, + inQueue: false, + alarm: ongoingWithDeposit.alarm, + }, + }, + ], + ], + }, + }) /** - * Check the created referendum's data + * 2. Check the referendum's data — decision deposit should be absent */ + let referendumDataOpt = (await client.api.query.referenda.referendumInfoFor( referendumIndex, )) as unknown as Option - assert(referendumDataOpt.isSome, "submitted referendum's data cannot be `None`") + assert(referendumDataOpt.isSome) let referendumData: PalletReferendaReferendumInfoConvictionVotingTally = referendumDataOpt.unwrap() + assert(referendumData.isOngoing) + const ongoingReferendum = referendumData.asOngoing - // These fields must be excised from the queried referendum data before being put in the test - // snapshot. - // These fields contain epoch-sensitive data, which will cause spurious test failures - // periodically. - const unwantedFields2 = /alarm|submitted/ - await check(referendumData) - .redact({ removeKeys: unwantedFields2 }) - .toMatchSnapshot('referendum info after submission') - - expect(referendumData.isOngoing).toBe(true) - // Ongoing referendum data, prior to the decision deposit. - const ongoingReferendum: PalletReferendaReferendumStatusConvictionVotingTally = referendumData.asOngoing - - // Check the entirety of the stored referendum's data - - expect(ongoingReferendum.track.toNumber()).toBe(smallTipper[0].toNumber()) - expect(ongoingReferendum.origin.toJSON()).toMatchObject({ origins: 'SmallTipper' }) - - expect(ongoingReferendum.proposal.asInline.toHex()).toBe(client.api.tx.system.remark('hello').method.toHex()) - - // The referendum was above set to be enacted 1 block after its passing. - expect(ongoingReferendum.enactment.isAfter).toBe(true) - expect(ongoingReferendum.enactment.asAfter.toNumber()).toBe(1) - - expect(ongoingReferendum.submissionDeposit.who.toString()).toBe( - encodeAddress(devAccounts.alice.address, chain.properties.addressEncoding), - ) - expect(ongoingReferendum.submissionDeposit.amount.toNumber()).toBe( - client.api.consts.referenda.submissionDeposit.toNumber(), - ) - - // Immediately after a referendum's submission, it will not have a decision deposit, - // which it will need to begin the decision period. + expect(ongoingReferendum.track.toNumber()).toBe(track[0].toNumber()) expect(ongoingReferendum.decisionDeposit.isNone).toBe(true) expect(ongoingReferendum.deciding.isNone).toBe(true) - // Current voting state of the referendum. - const votes = { - ayes: 0, - nays: 0, - support: 0, - } - - // Check that voting data is empty - await check(ongoingReferendum.tally).toMatchObject(votes) - - // The referendum should not have been put in a queue - this test assumes there's room in the referendum's - // track. - expect(ongoingReferendum.inQueue.isFalse).toBe(true) - - // Check the alarm - expect(ongoingReferendum.alarm.isSome).toBe(true) - const undecidingTimeoutAlarm = ongoingReferendum.alarm.unwrap()[0] - const blocksUntilAlarm = undecidingTimeoutAlarm.sub(ongoingReferendum.submitted) - // Check that the referendum's alarm is set to ring after the (globally predetermined) timeout - // of 14 days, or 201600 blocks. - expect(blocksUntilAlarm.toNumber()).toBe(client.api.consts.referenda.undecidingTimeout.toNumber()) - const alarm = [undecidingTimeoutAlarm, [undecidingTimeoutAlarm, 0]] - expect(ongoingReferendum.alarm.unwrap().eq(alarm)).toBe(true) - /** - * Simulate a timeout caused by an unplaced decision deposit. - * - * 1. backdate `submitted` so that `submitted + undecidingTimeout` falls on the next local block - * 2. set the alarm to that same block - * 3. schedule a `nudgeReferendum` call so the runtime services the alarm + * 3. Fast-forward past the undeciding timeout via storage injection * * The referendum's timing fields (`submitted`, alarm) use local block numbers. * The scheduler may use a different provider, so the nudge is scheduled via the helper. @@ -266,7 +222,6 @@ export async function submitReferendumTest< }, }) - // Schedule the nudge via the helper — it handles non-local block number providers correctly. await scheduleInlineCallWithOrigin( client, client.api.tx.referenda.nudgeReferendum(referendumIndex).method.toHex(), @@ -276,59 +231,51 @@ export async function submitReferendumTest< await client.dev.newBlock() - // Check event for the timed-out referendum - let events = await client.api.query.system.events() + /** + * 4. Verify the referendum timed out + * + * 4.1 checking the `TimedOut` event and referendum storage state + */ + const events = await client.api.query.system.events() const timedOutEvents = events.filter( ({ event }) => client.api.events.referenda.TimedOut.is(event) && event.data[0].toNumber() === referendumIndex, ) - expect(timedOutEvents.length, 'timing out a referendum should emit 1 TimedOut event').toBe(1) - const timedOutEvent = timedOutEvents[0] - - await check(timedOutEvent).toMatchSnapshot('timed-out referendum event') - - // Check the timed-out referendum's data + await check(timedOutEvents[0]) + .redact({ removeKeys: /index|pollIndex/ }) + .toMatchSnapshot(`timed-out referendum event (missing decision deposit) - ${trackConfig.trackName}`) referendumDataOpt = (await client.api.query.referenda.referendumInfoFor( referendumIndex, )) as unknown as Option - assert(referendumDataOpt.isSome, "submitted referendum's data cannot be `None`") + assert(referendumDataOpt.isSome) referendumData = referendumDataOpt.unwrap() expect(referendumData.isTimedOut).toBe(true) + // [end_block, submission_deposit, decision_deposit] const timedOutRef: ITuple<[u32, Option, Option]> = referendumData.asTimedOut - - // [end_block, submission_deposit, decision_deposit] expect(timedOutRef[1].isSome, 'submission deposit should be present after timeout').toBe(true) - expect(timedOutRef[1].unwrap().who.toString()).toBe( - encodeAddress(defaultAccountsSr25519.alice.address, chain.properties.addressEncoding), - ) - expect(timedOutRef[1].unwrap().amount.toBigInt()).toBe(client.api.consts.referenda.submissionDeposit.toBigInt()) expect(timedOutRef[2].isNone, 'decision deposit should be absent (never placed)').toBe(true) - // Attempt to refund the submission deposit + /** + * 4.2 checking that the submission deposit cannot be refunded (`BadStatus`) + */ const refundTx = client.api.tx.referenda.refundSubmissionDeposit(referendumIndex) await sendTransaction(refundTx.signAsync(devAccounts.alice)) - await client.dev.newBlock() - events = await client.api.query.system.events() - - const refundEvents = events.filter((record) => { - const { event } = record - return event.section === 'system' && event.method === 'ExtrinsicFailed' - }) - - // Timed out referenda cannot have their submission deposit refunded. - const refundEvent = refundEvents[0] - assert(client.api.events.system.ExtrinsicFailed.is(refundEvent.event)) - const dispatchError = refundEvent.event.data.dispatchError + const postRefundEvents = await client.api.query.system.events() + const failedRefundEvent = postRefundEvents.find( + ({ event }) => event.section === 'system' && event.method === 'ExtrinsicFailed', + ) + assert(failedRefundEvent, 'submission deposit refund should have failed for timed-out referendum') + assert(client.api.events.system.ExtrinsicFailed.is(failedRefundEvent.event)) + const dispatchError = failedRefundEvent.event.data.dispatchError assert(dispatchError.isModule) - expect(client.api.errors.referenda.BadStatus.is(dispatchError.asModule)).toBe(true) } @@ -1014,10 +961,8 @@ export async function referendumLifecycleKillTest< }, }) - /** - * Get current referendum count i.e. the next referendum's index - */ - const referendumIndex = (await client.api.query.referenda.referendumCount()).toNumber() + const referendaTracks = client.api.consts.referenda.tracks + const smallTipper = referendaTracks.find((track) => track[1].name.toString().startsWith('small_tipper'))! /** * Submit a new referendum @@ -1038,12 +983,17 @@ export async function referendumLifecycleKillTest< await client.dev.newBlock() - /** - * Check the created referendum's data - */ - - const referendaTracks = client.api.consts.referenda.tracks - const smallTipper = referendaTracks.find((track) => track[1].name.toString().startsWith('small_tipper'))! + // Find referendum index from the block's Submitted events, matching our track. + const submitEvents = await client.api.query.system.events() + const submittedOnTrack = submitEvents.filter( + ({ event }) => + client.api.events.referenda.Submitted.is(event) && event.data[1].toNumber() === smallTipper[0].toNumber(), + ) + assert( + submittedOnTrack.length === 1, + `expected 1 Submitted event on small_tipper track, got ${submittedOnTrack.length}`, + ) + const referendumIndex = submittedOnTrack[0].event.data[0].toNumber() /** * Place decision deposit @@ -1133,8 +1083,13 @@ export async function referendumLifecycleKillTest< /** * Shared preamble for negative-outcome tests: * 1. submitting a referendum on the given track - * 2. placing its decision deposit - * 3. reading back the created referendum's data + * 2. finding the created referendum's index from the block events + * 3. placing its decision deposit + * 4. reading back the created referendum's data + * + * The referendum index is obtained by scanning the `Submitted` event rather than + * pre-reading `referendumCount`, because scheduled calls from the fork block can + * create other referenda in the same `newBlock()` call. */ async function submitAndDeposit( client: Awaited>[0], @@ -1148,10 +1103,12 @@ async function submitAndDeposit( * 1. Submit a new referendum on the given track */ - const referendumIndex = (await client.api.query.referenda.referendumCount()).toNumber() + const proposalOrigin = trackConfig.systemOrigin + ? { system: trackConfig.originName } + : { Origins: trackConfig.originName } const submissionTx = client.api.tx.referenda.submit( - { Origins: trackConfig.originName } as any, + proposalOrigin as any, { Inline: client.api.tx.system.remark('hello').method.toHex() }, { After: 1 }, ) @@ -1159,7 +1116,24 @@ async function submitAndDeposit( await client.dev.newBlock() /** - * 2. Place decision deposit + * 2. Find the referendum index from the block events + * + * The fork block may schedule calls that create other referenda in the same block as + * our submission. We identify ours by matching the track ID in the `Submitted` event. + */ + + const submitBlockEvents = await client.api.query.system.events() + const submittedOnTrack = submitBlockEvents.filter( + ({ event }) => client.api.events.referenda.Submitted.is(event) && event.data[1].toNumber() === trackConfig.trackId, + ) + assert( + submittedOnTrack.length === 1, + `expected exactly 1 Submitted event on track ${trackConfig.trackName} (ID ${trackConfig.trackId}), got ${submittedOnTrack.length}`, + ) + const referendumIndex = submittedOnTrack[0].event.data[0].toNumber() + + /** + * 3. Place decision deposit */ const decisionDepTx = client.api.tx.referenda.placeDecisionDeposit(referendumIndex) @@ -1167,7 +1141,7 @@ async function submitAndDeposit( await client.dev.newBlock() /** - * 3. Check the created referendum's data + * 4. Read back the created referendum's data */ const referendumDataOpt: Option = @@ -1516,41 +1490,145 @@ export async function insufficientApprovalTest< await verifyRejection(client, referendumIndex, trackConfig.trackName, 'insufficient approval') } -/// ------- -/// Test trees -/// ------- - -function insufficientSupportTestTree< +/** + * Test that a referendum is queued when the track's deciding capacity is saturated: + * 1. submitting a referendum and placing its decision deposit + * 2. saturating the track's deciding capacity via storage injection + * 3. fast-forwarding past the preparation period via storage injection + * 4. verifying the referendum is queued (not deciding) because the track is full + */ +export async function trackCapacityOverflowTest< TCustom extends Record | undefined, TInitStorages extends Record> | undefined, ->(chain: Chain, tracks: GovernanceTrackConfig[]): DescribeNode { - const children: TestNode[] = tracks.map((trackConfig) => ({ - kind: 'test' as const, - label: `insufficient support rejection for ${trackConfig.trackName}`, - testFn: async () => await insufficientSupportTest(chain, trackConfig), - })) +>(chain: Chain, trackConfig: GovernanceTrackConfig) { + const [client] = await setupNetworks(chain) - return { - kind: 'describe', - label: 'insufficient support rejection tests', - children, - } + const referendaTracks = client.api.consts.referenda.tracks + const track = referendaTracks.find((t) => t[0].toNumber() === trackConfig.trackId)! + const maxDeciding = track[1].maxDeciding.toNumber() + const decisionDeposit = track[1].decisionDeposit.toBigInt() + const submissionDeposit = client.api.consts.referenda.submissionDeposit.toBigInt() + + await client.dev.setStorage({ + System: { + account: [ + [[devAccounts.alice.address], { providers: 1, data: { free: (submissionDeposit * 10n).toString() } }], + [[devAccounts.bob.address], { providers: 1, data: { free: (decisionDeposit * 10n).toString() } }], + ], + }, + }) + + /** + * 1. Submit referendum and place decision deposit + */ + + const { referendumIndex, ongoing } = await submitAndDeposit(client, trackConfig) + expect(ongoing.deciding.isNone, 'referendum should not yet be deciding (prep period not elapsed)').toBe(true) + + /** + * 2. Saturate the track and fast-forward past the preparation period + * + * Setting `DecidingCount` to `maxDeciding` makes the runtime believe the track is full. + * Backdating `submitted` so that `submitted + preparePeriod ≤ nextBlock` ensures the + * preparation period has elapsed. The nudge call triggers the runtime to evaluate the + * referendum: because the track is full, it queues the referendum instead of deciding. + */ + + const currentBlock = (await client.api.rpc.chain.getHeader()).number.toNumber() + const prepPeriod = track[1].preparePeriod.toNumber() + const newSubmitted = currentBlock + 1 - prepPeriod + + await client.dev.setStorage({ + Referenda: { + DecidingCount: [[[trackConfig.trackId], maxDeciding]], + ReferendumInfoFor: [ + [ + [referendumIndex], + { + Ongoing: { + track: ongoing.track, + origin: ongoing.origin, + proposal: ongoing.proposal, + enactment: ongoing.enactment, + submitted: newSubmitted, + submissionDeposit: ongoing.submissionDeposit, + decisionDeposit: ongoing.decisionDeposit, + deciding: null, + tally: ongoing.tally, + inQueue: false, + alarm: [currentBlock + 1, [currentBlock + 1, 0]], + }, + }, + ], + ], + }, + }) + + /** + * 3. Schedule a nudge so the runtime evaluates the referendum + */ + + await scheduleInlineCallWithOrigin( + client, + client.api.tx.referenda.nudgeReferendum(referendumIndex).method.toHex(), + { system: 'Root' }, + chain.properties.schedulerBlockProvider, + ) + + await client.dev.newBlock() + + /** + * 4. Verify the referendum is queued, not deciding + */ + + const referendumDataOpt: Option = + (await client.api.query.referenda.referendumInfoFor(referendumIndex)) as any + assert(referendumDataOpt.isSome) + const referendumData = referendumDataOpt.unwrap() + assert(referendumData.isOngoing, 'referendum should still be ongoing (queued, not terminal)') + const queuedRef = referendumData.asOngoing + + expect(queuedRef.deciding.isNone, 'queued referendum should not be in decision phase').toBe(true) + expect( + queuedRef.inQueue.isTrue, + `referendum should be queued when track is at capacity (maxDeciding=${maxDeciding})`, + ).toBe(true) + + await check(queuedRef) + .redact({ removeKeys: /alarm|submitted/ }) + .toMatchSnapshot(`queued referendum (track at capacity) - ${trackConfig.trackName}`) } -function insufficientApprovalTestTree< - TCustom extends Record | undefined, - TInitStorages extends Record> | undefined, ->(chain: Chain, tracks: GovernanceTrackConfig[]): DescribeNode { - const children: TestNode[] = tracks.map((trackConfig) => ({ - kind: 'test' as const, - label: `insufficient approval rejection for ${trackConfig.trackName}`, - testFn: async () => await insufficientApprovalTest(chain, trackConfig), - })) +/// ------- +/// Test trees +/// ------- +function negativeFlowsForTrack(chain: Chain, trackConfig: GovernanceTrackConfig): DescribeNode { return { kind: 'describe', - label: 'insufficient approval rejection tests', - children, + label: trackConfig.trackName, + children: [ + { + kind: 'test' as const, + label: 'missing decision deposit timeout', + testFn: async () => await missingDecisionDepositTest(chain, trackConfig), + }, + { + kind: 'test' as const, + label: 'insufficient support rejection', + testFn: async () => await insufficientSupportTest(chain, trackConfig), + }, + { + kind: 'test' as const, + label: 'insufficient approval rejection', + testFn: async () => await insufficientApprovalTest(chain, trackConfig), + }, + { + kind: 'test' as const, + label: 'track capacity overflow', + testFn: async () => await trackCapacityOverflowTest(chain, trackConfig), + }, + ], } } @@ -1566,11 +1644,6 @@ export function baseGovernanceE2ETests< kind: 'describe', label: 'referenda tests', children: [ - { - kind: 'test', - label: 'referendum submission and timeout', - testFn: async () => await submitReferendumTest(chain), - }, { kind: 'test', label: 'referendum lifecycle test - submission, decision deposit, various voting should all work', @@ -1581,10 +1654,13 @@ export function baseGovernanceE2ETests< label: 'referendum lifecycle test 2 - submission, decision deposit, and killing should work', testFn: async () => await referendumLifecycleKillTest(chain), }, + { + kind: 'describe', + label: 'negative execution flows', + children: govConfig.tracks.map((trackConfig) => negativeFlowsForTrack(chain, trackConfig)), + }, ], }, - insufficientSupportTestTree(chain, govConfig.tracks), - insufficientApprovalTestTree(chain, govConfig.tracks), ], } } From e746449c8a9a3251876786b28e060817eb6e4095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Thu, 19 Mar 2026 14:08:23 +0000 Subject: [PATCH 05/13] Reduce number of tracks being tested, upd. snaps --- ...assetHubKusama.governance.e2e.test.ts.snap | 353 ++++++++++++++---- .../src/assetHubKusama.governance.e2e.test.ts | 17 +- ...setHubPolkadot.governance.e2e.test.ts.snap | 353 ++++++++++++++---- .../assetHubPolkadot.governance.e2e.test.ts | 17 +- 4 files changed, 598 insertions(+), 142 deletions(-) diff --git a/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap b/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap index 749e730a7..25eaec4e8 100644 --- a/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap +++ b/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap @@ -1,10 +1,10 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Kusama Asset Hub Governance > insufficient approval rejection tests > insufficient approval rejection for small_tipper > decision deposit refund after rejection (insufficient approval) - small_tipper 1`] = ` +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient approval rejection > decision deposit refund after rejection (insufficient approval) - big_spender 1`] = ` [ { "data": { - "amount": "(rounded 33000000000)", + "amount": "(rounded 13000000000000)", "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", }, "method": "DecisionDepositRefunded", @@ -13,7 +13,7 @@ exports[`Kusama Asset Hub Governance > insufficient approval rejection tests > i ] `; -exports[`Kusama Asset Hub Governance > insufficient approval rejection tests > insufficient approval rejection for small_tipper > rejected referendum event (insufficient approval) - small_tipper 1`] = ` +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient approval rejection > rejected referendum event (insufficient approval) - big_spender 1`] = ` { "event": { "data": [ @@ -32,11 +32,205 @@ exports[`Kusama Asset Hub Governance > insufficient approval rejection tests > i } `; -exports[`Kusama Asset Hub Governance > insufficient support rejection tests > insufficient support rejection for small_tipper > decision deposit refund after rejection (insufficient support) - small_tipper 1`] = ` +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient support rejection > decision deposit refund after rejection (insufficient support) - big_spender 1`] = ` [ { "data": { - "amount": "(rounded 33000000000)", + "amount": "(rounded 13000000000000)", + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient support rejection > rejected referendum event (insufficient support) - big_spender 1`] = ` +{ + "event": { + "data": [ + 637, + { + "ayes": 0, + "nays": 1000000000, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - big_spender 1`] = ` +{ + "event": { + "data": [ + 637, + { + "ayes": 0, + "nays": 0, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > queued referendum (track at capacity) - big_spender 1`] = ` +{ + "deciding": null, + "decisionDeposit": { + "amount": 13333333333200, + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "enactment": { + "after": 1, + }, + "inQueue": true, + "origin": { + "origins": "BigSpender", + }, + "proposal": { + "inline": "0x00001468656c6c6f", + }, + "submissionDeposit": { + "amount": 33333333333, + "who": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 34, +} +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient approval rejection > decision deposit refund after rejection (insufficient approval) - referendum_killer 1`] = ` +[ + { + "data": { + "amount": "(rounded 1700000000000000)", + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient approval rejection > rejected referendum event (insufficient approval) - referendum_killer 1`] = ` +{ + "event": { + "data": [ + 637, + { + "ayes": "0x000000000000000018c59bfc3dbf6d18", + "nays": "0x00000000000000004a50d3f4b93e4748", + "support": "0x000000000000000018c59bfc3dbf6d18", + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient support rejection > decision deposit refund after rejection (insufficient support) - referendum_killer 1`] = ` +[ + { + "data": { + "amount": "(rounded 1700000000000000)", + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient support rejection > rejected referendum event (insufficient support) - referendum_killer 1`] = ` +{ + "event": { + "data": [ + 637, + { + "ayes": 0, + "nays": 1000000000, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - referendum_killer 1`] = ` +{ + "event": { + "data": [ + 637, + { + "ayes": 0, + "nays": 0, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > queued referendum (track at capacity) - referendum_killer 1`] = ` +{ + "deciding": null, + "decisionDeposit": { + "amount": 1666666666650000, + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "enactment": { + "after": 1, + }, + "inQueue": true, + "origin": { + "origins": "ReferendumKiller", + }, + "proposal": { + "inline": "0x00001468656c6c6f", + }, + "submissionDeposit": { + "amount": 33333333333, + "who": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 21, +} +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > insufficient approval rejection > decision deposit refund after rejection (insufficient approval) - root 1`] = ` +[ + { + "data": { + "amount": "(rounded 3300000000000000)", "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", }, "method": "DecisionDepositRefunded", @@ -45,7 +239,39 @@ exports[`Kusama Asset Hub Governance > insufficient support rejection tests > in ] `; -exports[`Kusama Asset Hub Governance > insufficient support rejection tests > insufficient support rejection for small_tipper > rejected referendum event (insufficient support) - small_tipper 1`] = ` +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > insufficient approval rejection > rejected referendum event (insufficient approval) - root 1`] = ` +{ + "event": { + "data": [ + 637, + { + "ayes": "0x000000000000000018c59bfc3dbf6d18", + "nays": "0x00000000000000004a50d3f4b93e4748", + "support": "0x000000000000000018c59bfc3dbf6d18", + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > insufficient support rejection > decision deposit refund after rejection (insufficient support) - root 1`] = ` +[ + { + "data": { + "amount": "(rounded 3300000000000000)", + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > insufficient support rejection > rejected referendum event (insufficient support) - root 1`] = ` { "event": { "data": [ @@ -64,6 +290,57 @@ exports[`Kusama Asset Hub Governance > insufficient support rejection tests > in } `; +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - root 1`] = ` +{ + "event": { + "data": [ + 637, + { + "ayes": 0, + "nays": 0, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > queued referendum (track at capacity) - root 1`] = ` +{ + "deciding": null, + "decisionDeposit": { + "amount": 3333333333300000, + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "enactment": { + "after": 1, + }, + "inQueue": true, + "origin": { + "system": { + "root": null, + }, + }, + "proposal": { + "inline": "0x00001468656c6c6f", + }, + "submissionDeposit": { + "amount": 33333333333, + "who": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 0, +} +`; + exports[`Kusama Asset Hub Governance > referenda tests > referendum lifecycle test - submission, decision deposit, various voting should all work > cancelling referendum with signed origin 1`] = ` [ { @@ -596,67 +873,3 @@ exports[`Kusama Asset Hub Governance > referenda tests > referendum lifecycle te }, ] `; - -exports[`Kusama Asset Hub Governance > referenda tests > referendum submission and timeout > referendum info after submission 1`] = ` -{ - "ongoing": { - "deciding": null, - "decisionDeposit": null, - "enactment": { - "after": 1, - }, - "inQueue": false, - "origin": { - "origins": "SmallTipper", - }, - "proposal": { - "inline": "0x00001468656c6c6f", - }, - "submissionDeposit": { - "amount": 33333333333, - "who": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 30, - }, -} -`; - -exports[`Kusama Asset Hub Governance > referenda tests > referendum submission and timeout > referendum submission events 1`] = ` -[ - { - "data": { - "proposal": { - "Inline": "0x00001468656c6c6f", - }, - "track": 30, - }, - "method": "Submitted", - "section": "referenda", - }, -] -`; - -exports[`Kusama Asset Hub Governance > referenda tests > referendum submission and timeout > timed-out referendum event 1`] = ` -{ - "event": { - "data": [ - 637, - { - "ayes": 0, - "nays": 0, - "support": 0, - }, - ], - "index": "0x5c0a", - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; diff --git a/packages/kusama/src/assetHubKusama.governance.e2e.test.ts b/packages/kusama/src/assetHubKusama.governance.e2e.test.ts index a2c104229..14d34c80e 100644 --- a/packages/kusama/src/assetHubKusama.governance.e2e.test.ts +++ b/packages/kusama/src/assetHubKusama.governance.e2e.test.ts @@ -1,9 +1,24 @@ import { assetHubKusama } from '@e2e-test/networks/chains' import { baseGovernanceE2ETests, type GovernanceTestConfig, registerTestTree } from '@e2e-test/shared' +// Tracks selected by referendum volume (Polkassembly, all-time) + criticality. +// Only one track is active: the per-track tests are identical in logic and each takes ~15 s, +// so running every track is repetitive. Uncomment additional tracks as needed. const governanceConfig: GovernanceTestConfig = { testSuiteName: 'Kusama Asset Hub Governance', - tracks: [{ trackId: 1, trackName: 'small_tipper', originName: 'SmallTipper' }], + tracks: [ + // --- high-volume tracks (by referendum count) --- + //{ trackId: 33, trackName: 'medium_spender', originName: 'MediumSpender' }, // 123 refs (19.2 %) + { trackId: 34, trackName: 'big_spender', originName: 'BigSpender' }, // 90 refs (14.0 %) + //{ trackId: 32, trackName: 'small_spender', originName: 'SmallSpender' }, // 61 refs (9.5 %) + //{ trackId: 30, trackName: 'small_tipper', originName: 'SmallTipper' }, // 46 refs (7.2 %) + //{ trackId: 31, trackName: 'big_tipper', originName: 'BigTipper' }, // 19 refs (3.0 %) + // --- critical tracks (low volume, high impact) --- + { trackId: 0, trackName: 'root', originName: 'Root', systemOrigin: true }, // 41 refs (6.4 %) + //{ trackId: 1, trackName: 'whitelisted_caller', originName: 'WhitelistedCaller' }, // 124 refs (19.3 %) + //{ trackId: 20, trackName: 'referendum_canceller', originName: 'ReferendumCanceller' }, // 12 refs (1.9 %) + { trackId: 21, trackName: 'referendum_killer', originName: 'ReferendumKiller' }, // 5 refs (0.8 %) + ], } registerTestTree(baseGovernanceE2ETests(assetHubKusama, governanceConfig)) diff --git a/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap b/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap index 1d1ae25e5..252aa8949 100644 --- a/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap +++ b/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap @@ -1,10 +1,10 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Polkadot Asset Hub Governance > insufficient approval rejection tests > insufficient approval rejection for small_tipper > decision deposit refund after rejection (insufficient approval) - small_tipper 1`] = ` +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient approval rejection > decision deposit refund after rejection (insufficient approval) - big_spender 1`] = ` [ { "data": { - "amount": 10000000000, + "amount": 50000000000000, "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", }, "method": "DecisionDepositRefunded", @@ -13,7 +13,7 @@ exports[`Polkadot Asset Hub Governance > insufficient approval rejection tests > ] `; -exports[`Polkadot Asset Hub Governance > insufficient approval rejection tests > insufficient approval rejection for small_tipper > rejected referendum event (insufficient approval) - small_tipper 1`] = ` +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient approval rejection > rejected referendum event (insufficient approval) - big_spender 1`] = ` { "event": { "data": [ @@ -32,11 +32,205 @@ exports[`Polkadot Asset Hub Governance > insufficient approval rejection tests > } `; -exports[`Polkadot Asset Hub Governance > insufficient support rejection tests > insufficient support rejection for small_tipper > decision deposit refund after rejection (insufficient support) - small_tipper 1`] = ` +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient support rejection > decision deposit refund after rejection (insufficient support) - big_spender 1`] = ` [ { "data": { - "amount": 10000000000, + "amount": 50000000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient support rejection > rejected referendum event (insufficient support) - big_spender 1`] = ` +{ + "event": { + "data": [ + 1850, + { + "ayes": 0, + "nays": 1000000000, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - big_spender 1`] = ` +{ + "event": { + "data": [ + 1850, + { + "ayes": 0, + "nays": 0, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > queued referendum (track at capacity) - big_spender 1`] = ` +{ + "deciding": null, + "decisionDeposit": { + "amount": 50000000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "enactment": { + "after": 1, + }, + "inQueue": true, + "origin": { + "origins": "BigSpender", + }, + "proposal": { + "inline": "0x00001468656c6c6f", + }, + "submissionDeposit": { + "amount": 100000000000, + "who": "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 34, +} +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient approval rejection > decision deposit refund after rejection (insufficient approval) - referendum_killer 1`] = ` +[ + { + "data": { + "amount": 500000000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient approval rejection > rejected referendum event (insufficient approval) - referendum_killer 1`] = ` +{ + "event": { + "data": [ + 1850, + { + "ayes": "0x000000000000000017304fe24dda2a9c", + "nays": "0x00000000000000004590efa6e98e7fd4", + "support": "0x000000000000000017304fe24dda2a9c", + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient support rejection > decision deposit refund after rejection (insufficient support) - referendum_killer 1`] = ` +[ + { + "data": { + "amount": 500000000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient support rejection > rejected referendum event (insufficient support) - referendum_killer 1`] = ` +{ + "event": { + "data": [ + 1850, + { + "ayes": 0, + "nays": 1000000000, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - referendum_killer 1`] = ` +{ + "event": { + "data": [ + 1850, + { + "ayes": 0, + "nays": 0, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > queued referendum (track at capacity) - referendum_killer 1`] = ` +{ + "deciding": null, + "decisionDeposit": { + "amount": 500000000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "enactment": { + "after": 1, + }, + "inQueue": true, + "origin": { + "origins": "ReferendumKiller", + }, + "proposal": { + "inline": "0x00001468656c6c6f", + }, + "submissionDeposit": { + "amount": 100000000000, + "who": "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 21, +} +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > insufficient approval rejection > decision deposit refund after rejection (insufficient approval) - root 1`] = ` +[ + { + "data": { + "amount": 1000000000000000, "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", }, "method": "DecisionDepositRefunded", @@ -45,7 +239,39 @@ exports[`Polkadot Asset Hub Governance > insufficient support rejection tests > ] `; -exports[`Polkadot Asset Hub Governance > insufficient support rejection tests > insufficient support rejection for small_tipper > rejected referendum event (insufficient support) - small_tipper 1`] = ` +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > insufficient approval rejection > rejected referendum event (insufficient approval) - root 1`] = ` +{ + "event": { + "data": [ + 1850, + { + "ayes": "0x000000000000000017304fe24dda2a9c", + "nays": "0x00000000000000004590efa6e98e7fd4", + "support": "0x000000000000000017304fe24dda2a9c", + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > insufficient support rejection > decision deposit refund after rejection (insufficient support) - root 1`] = ` +[ + { + "data": { + "amount": 1000000000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "method": "DecisionDepositRefunded", + "section": "referenda", + }, +] +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > insufficient support rejection > rejected referendum event (insufficient support) - root 1`] = ` { "event": { "data": [ @@ -64,6 +290,57 @@ exports[`Polkadot Asset Hub Governance > insufficient support rejection tests > } `; +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - root 1`] = ` +{ + "event": { + "data": [ + 1850, + { + "ayes": 0, + "nays": 0, + "support": 0, + }, + ], + }, + "phase": { + "initialization": null, + }, + "topics": [], +} +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > queued referendum (track at capacity) - root 1`] = ` +{ + "deciding": null, + "decisionDeposit": { + "amount": 1000000000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "enactment": { + "after": 1, + }, + "inQueue": true, + "origin": { + "system": { + "root": null, + }, + }, + "proposal": { + "inline": "0x00001468656c6c6f", + }, + "submissionDeposit": { + "amount": 100000000000, + "who": "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 0, +} +`; + exports[`Polkadot Asset Hub Governance > referenda tests > referendum lifecycle test - submission, decision deposit, various voting should all work > cancelling referendum with signed origin 1`] = ` [ { @@ -596,67 +873,3 @@ exports[`Polkadot Asset Hub Governance > referenda tests > referendum lifecycle }, ] `; - -exports[`Polkadot Asset Hub Governance > referenda tests > referendum submission and timeout > referendum info after submission 1`] = ` -{ - "ongoing": { - "deciding": null, - "decisionDeposit": null, - "enactment": { - "after": 1, - }, - "inQueue": false, - "origin": { - "origins": "SmallTipper", - }, - "proposal": { - "inline": "0x00001468656c6c6f", - }, - "submissionDeposit": { - "amount": 100000000000, - "who": "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 30, - }, -} -`; - -exports[`Polkadot Asset Hub Governance > referenda tests > referendum submission and timeout > referendum submission events 1`] = ` -[ - { - "data": { - "proposal": { - "Inline": "0x00001468656c6c6f", - }, - "track": 30, - }, - "method": "Submitted", - "section": "referenda", - }, -] -`; - -exports[`Polkadot Asset Hub Governance > referenda tests > referendum submission and timeout > timed-out referendum event 1`] = ` -{ - "event": { - "data": [ - 1850, - { - "ayes": 0, - "nays": 0, - "support": 0, - }, - ], - "index": "0x3e0a", - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; diff --git a/packages/polkadot/src/assetHubPolkadot.governance.e2e.test.ts b/packages/polkadot/src/assetHubPolkadot.governance.e2e.test.ts index ff6200559..860241120 100644 --- a/packages/polkadot/src/assetHubPolkadot.governance.e2e.test.ts +++ b/packages/polkadot/src/assetHubPolkadot.governance.e2e.test.ts @@ -1,9 +1,24 @@ import { assetHubPolkadot } from '@e2e-test/networks/chains' import { baseGovernanceE2ETests, type GovernanceTestConfig, registerTestTree } from '@e2e-test/shared' +// Tracks selected by referendum volume (Polkassembly, all-time) + criticality. +// Only one track is active: the per-track tests are identical in logic and each takes ~15 s, +// so running every track is repetitive. Uncomment additional tracks as needed. const governanceConfig: GovernanceTestConfig = { testSuiteName: 'Polkadot Asset Hub Governance', - tracks: [{ trackId: 1, trackName: 'small_tipper', originName: 'SmallTipper' }], + tracks: [ + // --- high-volume tracks (by referendum count) --- + //{ trackId: 33, trackName: 'medium_spender', originName: 'MediumSpender' }, // 609 refs (32.7 %) + //{ trackId: 32, trackName: 'small_spender', originName: 'SmallSpender' }, // 275 refs (14.8 %) + //{ trackId: 30, trackName: 'small_tipper', originName: 'SmallTipper' }, // 235 refs (12.6 %) + { trackId: 34, trackName: 'big_spender', originName: 'BigSpender' }, // 173 refs (9.3 %) + //{ trackId: 31, trackName: 'big_tipper', originName: 'BigTipper' }, // 153 refs (8.2 %) + // --- critical tracks (low volume, high impact) --- + { trackId: 0, trackName: 'root', originName: 'Root', systemOrigin: true }, // 59 refs (3.2 %) + //{ trackId: 1, trackName: 'whitelisted_caller', originName: 'WhitelistedCaller' }, // 106 refs (5.7 %) + //{ trackId: 20, trackName: 'referendum_canceller', originName: 'ReferendumCanceller' }, // 27 refs (1.4 %) + { trackId: 21, trackName: 'referendum_killer', originName: 'ReferendumKiller' }, // 6 refs (0.3 %) + ], } registerTestTree(baseGovernanceE2ETests(assetHubPolkadot, governanceConfig)) From 63e6c41f0ecf3292ba0440604bae402f15bffb6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Sat, 21 Mar 2026 21:10:37 +0000 Subject: [PATCH 06/13] Test track overflow mechanism --- packages/shared/src/governance.ts | 202 ++++++++++++++++++++++-------- 1 file changed, 153 insertions(+), 49 deletions(-) diff --git a/packages/shared/src/governance.ts b/packages/shared/src/governance.ts index d2ede8d57..22810ad3a 100644 --- a/packages/shared/src/governance.ts +++ b/packages/shared/src/governance.ts @@ -1491,11 +1491,14 @@ export async function insufficientApprovalTest< } /** - * Test that a referendum is queued when the track's deciding capacity is saturated: - * 1. submitting a referendum and placing its decision deposit - * 2. saturating the track's deciding capacity via storage injection - * 3. fast-forwarding past the preparation period via storage injection - * 4. verifying the referendum is queued (not deciding) because the track is full + * Test that a referendum is queued when the track's deciding capacity is saturated, + * then promoted to deciding once a slot opens: + * 1. submitting a blocker referendum and placing its decision deposit + * 2. submitting the overflow referendum and placing its decision deposit + * 3. backdating the overflow past its preparation period and nudging it into the queue + * 4. verifying the overflow is queued + * 5. advancing the blocker to the last block of its confirmation period + * 6. verifying the blocker passes and the overflow is promoted to deciding */ export async function trackCapacityOverflowTest< TCustom extends Record | undefined, @@ -1505,9 +1508,11 @@ export async function trackCapacityOverflowTest< const referendaTracks = client.api.consts.referenda.tracks const track = referendaTracks.find((t) => t[0].toNumber() === trackConfig.trackId)! - const maxDeciding = track[1].maxDeciding.toNumber() const decisionDeposit = track[1].decisionDeposit.toBigInt() const submissionDeposit = client.api.consts.referenda.submissionDeposit.toBigInt() + const maxDeciding = track[1].maxDeciding.toNumber() + const prepPeriod = track[1].preparePeriod.toNumber() + const confirmPeriod = track[1].confirmPeriod.toNumber() await client.dev.setStorage({ System: { @@ -1519,44 +1524,57 @@ export async function trackCapacityOverflowTest< }) /** - * 1. Submit referendum and place decision deposit + * 1. Submit a blocker referendum on the root track and place its decision deposit + */ + + const { referendumIndex: blockerIndex, ongoing: blockerOngoing } = await submitAndDeposit(client, trackConfig) + + /** + * 2. Submit the overflow referendum and place its decision deposit */ - const { referendumIndex, ongoing } = await submitAndDeposit(client, trackConfig) - expect(ongoing.deciding.isNone, 'referendum should not yet be deciding (prep period not elapsed)').toBe(true) + const { referendumIndex: overflowIndex, ongoing: overflowOngoing } = await submitAndDeposit(client, trackConfig) + + const depositEvents = await client.api.query.system.events() + const depositPlaced = depositEvents.find( + ({ event }) => + client.api.events.referenda.DecisionDepositPlaced.is(event) && event.data[0].toNumber() === overflowIndex, + ) + assert(depositPlaced, 'DecisionDepositPlaced event should be emitted for the overflow referendum') + assert(client.api.events.referenda.DecisionDepositPlaced.is(depositPlaced.event)) + expect(depositPlaced.event.data[0].toNumber()).toBe(overflowIndex) + expect(depositPlaced.event.data[2].toBigInt()).toBe(decisionDeposit) /** - * 2. Saturate the track and fast-forward past the preparation period + * 3. Backdate the overflow past its preparation period and nudge it into the queue * - * Setting `DecidingCount` to `maxDeciding` makes the runtime believe the track is full. - * Backdating `submitted` so that `submitted + preparePeriod ≤ nextBlock` ensures the - * preparation period has elapsed. The nudge call triggers the runtime to evaluate the - * referendum: because the track is full, it queues the referendum instead of deciding. + * Setting `DecidingCount` to 1 makes the runtime believe the track is full (the blocker + * occupies the only slot). Backdating `submitted` past the preparation period and nudging + * causes the runtime to evaluate the overflow: because the track is full, it queues it. */ - const currentBlock = (await client.api.rpc.chain.getHeader()).number.toNumber() - const prepPeriod = track[1].preparePeriod.toNumber() - const newSubmitted = currentBlock + 1 - prepPeriod + const block1 = (await client.api.rpc.chain.getHeader()).number.toNumber() + const overflowSubmitted = block1 + 1 - prepPeriod await client.dev.setStorage({ Referenda: { DecidingCount: [[[trackConfig.trackId], maxDeciding]], ReferendumInfoFor: [ [ - [referendumIndex], + [overflowIndex], { Ongoing: { - track: ongoing.track, - origin: ongoing.origin, - proposal: ongoing.proposal, - enactment: ongoing.enactment, - submitted: newSubmitted, - submissionDeposit: ongoing.submissionDeposit, - decisionDeposit: ongoing.decisionDeposit, + track: overflowOngoing.track, + origin: overflowOngoing.origin, + proposal: overflowOngoing.proposal, + enactment: overflowOngoing.enactment, + submitted: overflowSubmitted, + submissionDeposit: overflowOngoing.submissionDeposit, + decisionDeposit: overflowOngoing.decisionDeposit, deciding: null, - tally: ongoing.tally, + tally: overflowOngoing.tally, inQueue: false, - alarm: [currentBlock + 1, [currentBlock + 1, 0]], + alarm: [block1 + 1, [block1 + 1, 0]], }, }, ], @@ -1564,13 +1582,9 @@ export async function trackCapacityOverflowTest< }, }) - /** - * 3. Schedule a nudge so the runtime evaluates the referendum - */ - await scheduleInlineCallWithOrigin( client, - client.api.tx.referenda.nudgeReferendum(referendumIndex).method.toHex(), + client.api.tx.referenda.nudgeReferendum(overflowIndex).method.toHex(), { system: 'Root' }, chain.properties.schedulerBlockProvider, ) @@ -1578,25 +1592,115 @@ export async function trackCapacityOverflowTest< await client.dev.newBlock() /** - * 4. Verify the referendum is queued, not deciding + * 4. Verify the overflow is queued, not deciding */ - const referendumDataOpt: Option = - (await client.api.query.referenda.referendumInfoFor(referendumIndex)) as any - assert(referendumDataOpt.isSome) - const referendumData = referendumDataOpt.unwrap() - assert(referendumData.isOngoing, 'referendum should still be ongoing (queued, not terminal)') - const queuedRef = referendumData.asOngoing - - expect(queuedRef.deciding.isNone, 'queued referendum should not be in decision phase').toBe(true) - expect( - queuedRef.inQueue.isTrue, - `referendum should be queued when track is at capacity (maxDeciding=${maxDeciding})`, - ).toBe(true) - - await check(queuedRef) - .redact({ removeKeys: /alarm|submitted/ }) - .toMatchSnapshot(`queued referendum (track at capacity) - ${trackConfig.trackName}`) + const queuedOpt: Option = + (await client.api.query.referenda.referendumInfoFor(overflowIndex)) as any + assert(queuedOpt.isSome) + const queued = queuedOpt.unwrap() + assert(queued.isOngoing, 'overflow referendum should still be ongoing (queued, not terminal)') + const queuedRef = queued.asOngoing + + expect(queuedRef.inQueue.isTrue, 'overflow should be queued when track is at capacity').toBe(true) + expect(queuedRef.deciding.isNone, 'queued overflow should not be in decision phase').toBe(true) + + /** + * 5. Advance the blocker to the last block of its confirmation period + * + * Setting `confirming` to `block2 + 1` places the blocker at the exact block where + * confirmation ends. The injected tally has full approval and support so `is_passing` + * holds. The blocker's nudge is written directly into the scheduler agenda alongside + * the referendum state to avoid a second `scheduleInlineCallWithOrigin` call. + */ + + const block2 = (await client.api.rpc.chain.getHeader()).number.toNumber() + const totalIssuance = (await client.api.query.balances.totalIssuance()).toBigInt() + + const confirmDeadline = block2 + 1 + const decidingSince = confirmDeadline - confirmPeriod + const blockerSubmitted = decidingSince - prepPeriod + + const schedulerBlock = + chain.properties.schedulerBlockProvider === 'NonLocal' + ? ((await client.api.query.parachainSystem.lastRelayChainBlockNumber()) as any).toNumber() + : block2 + 1 + + await client.dev.setStorage({ + Referenda: { + ReferendumInfoFor: [ + [ + [blockerIndex], + { + Ongoing: { + track: blockerOngoing.track, + origin: blockerOngoing.origin, + proposal: blockerOngoing.proposal, + enactment: blockerOngoing.enactment, + submitted: blockerSubmitted, + submissionDeposit: blockerOngoing.submissionDeposit, + decisionDeposit: blockerOngoing.decisionDeposit, + deciding: { since: decidingSince, confirming: confirmDeadline }, + tally: { ayes: totalIssuance.toString(), nays: 0, support: totalIssuance.toString() }, + inQueue: false, + alarm: [block2 + 1, [block2 + 1, 0]], + }, + }, + ], + ], + }, + Scheduler: { + agenda: [ + [ + [schedulerBlock], + [ + { + call: { Inline: client.api.tx.referenda.nudgeReferendum(blockerIndex).method.toHex() }, + origin: { system: 'Root' }, + }, + ], + ], + ], + incompleteSince: schedulerBlock, + }, + }) + + await client.dev.newBlock() + + /** + * 6. Verify the blocker passed and the overflow was promoted + * + * The blocker's approval triggers `note_one_fewer_deciding`, which schedules + * `one_fewer_deciding` for the next block. That call pops the overflow from + * the `TrackQueue` and begins its decision phase. + */ + + const blockerResultOpt: Option = + (await client.api.query.referenda.referendumInfoFor(blockerIndex)) as any + assert(blockerResultOpt.isSome) + expect(blockerResultOpt.unwrap().isApproved, 'blocker should have been approved').toBe(true) + + await client.dev.newBlock() + + const promotedOpt: Option = + (await client.api.query.referenda.referendumInfoFor(overflowIndex)) as any + assert(promotedOpt.isSome) + const promoted = promotedOpt.unwrap() + assert(promoted.isOngoing, 'overflow should still be ongoing after promotion') + expect(promoted.asOngoing.deciding.isSome, 'overflow should now be in decision phase').toBe(true) + expect(promoted.asOngoing.inQueue.isFalse, 'overflow should no longer be queued').toBe(true) + + const decidingCount = ((await client.api.query.referenda.decidingCount(trackConfig.trackId)) as any).toNumber() + expect(decidingCount, 'DecidingCount should reflect the promoted overflow').toBe(maxDeciding) + + const promotionEvents = await client.api.query.system.events() + const decisionStarted = promotionEvents.find( + ({ event }) => client.api.events.referenda.DecisionStarted.is(event) && event.data[0].toNumber() === overflowIndex, + ) + assert(decisionStarted, 'DecisionStarted event should be emitted when the overflow is promoted') + assert(client.api.events.referenda.DecisionStarted.is(decisionStarted.event)) + expect(decisionStarted.event.data[0].toNumber()).toBe(overflowIndex) + expect(decisionStarted.event.data[1].toNumber()).toBe(trackConfig.trackId) } /// ------- From e38905ca66c9ff768b0ed9ff3c7056c6b754b77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Sat, 21 Mar 2026 21:10:51 +0000 Subject: [PATCH 07/13] Update AH governance snapshots --- ...assetHubKusama.governance.e2e.test.ts.snap | 99 +++++++++++++++++++ ...setHubPolkadot.governance.e2e.test.ts.snap | 99 +++++++++++++++++++ 2 files changed, 198 insertions(+) diff --git a/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap b/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap index 25eaec4e8..45e23c145 100644 --- a/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap +++ b/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap @@ -83,6 +83,39 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow } `; +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > overflow decision deposit placed - big_spender 1`] = ` +[ + { + "data": { + "amount": "(rounded 13000000000000)", + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "method": "DecisionDepositPlaced", + "section": "referenda", + }, +] +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > overflow promoted to deciding - big_spender 1`] = ` +[ + { + "data": { + "proposal": { + "Inline": "0x00001468656c6c6f", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 34, + }, + "method": "DecisionStarted", + "section": "referenda", + }, +] +`; + exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > queued referendum (track at capacity) - big_spender 1`] = ` { "deciding": null, @@ -196,6 +229,39 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow } `; +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > overflow decision deposit placed - referendum_killer 1`] = ` +[ + { + "data": { + "amount": "(rounded 1700000000000000)", + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "method": "DecisionDepositPlaced", + "section": "referenda", + }, +] +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > overflow promoted to deciding - referendum_killer 1`] = ` +[ + { + "data": { + "proposal": { + "Inline": "0x00001468656c6c6f", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 21, + }, + "method": "DecisionStarted", + "section": "referenda", + }, +] +`; + exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > queued referendum (track at capacity) - referendum_killer 1`] = ` { "deciding": null, @@ -309,6 +375,39 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow } `; +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > overflow decision deposit placed - root 1`] = ` +[ + { + "data": { + "amount": "(rounded 3300000000000000)", + "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", + }, + "method": "DecisionDepositPlaced", + "section": "referenda", + }, +] +`; + +exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > overflow promoted to deciding - root 1`] = ` +[ + { + "data": { + "proposal": { + "Inline": "0x00001468656c6c6f", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 0, + }, + "method": "DecisionStarted", + "section": "referenda", + }, +] +`; + exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > queued referendum (track at capacity) - root 1`] = ` { "deciding": null, diff --git a/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap b/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap index 252aa8949..7414b2db8 100644 --- a/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap +++ b/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap @@ -83,6 +83,39 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl } `; +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > overflow decision deposit placed - big_spender 1`] = ` +[ + { + "data": { + "amount": 50000000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "method": "DecisionDepositPlaced", + "section": "referenda", + }, +] +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > overflow promoted to deciding - big_spender 1`] = ` +[ + { + "data": { + "proposal": { + "Inline": "0x00001468656c6c6f", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 34, + }, + "method": "DecisionStarted", + "section": "referenda", + }, +] +`; + exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > queued referendum (track at capacity) - big_spender 1`] = ` { "deciding": null, @@ -196,6 +229,39 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl } `; +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > overflow decision deposit placed - referendum_killer 1`] = ` +[ + { + "data": { + "amount": 500000000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "method": "DecisionDepositPlaced", + "section": "referenda", + }, +] +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > overflow promoted to deciding - referendum_killer 1`] = ` +[ + { + "data": { + "proposal": { + "Inline": "0x00001468656c6c6f", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 21, + }, + "method": "DecisionStarted", + "section": "referenda", + }, +] +`; + exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > queued referendum (track at capacity) - referendum_killer 1`] = ` { "deciding": null, @@ -309,6 +375,39 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl } `; +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > overflow decision deposit placed - root 1`] = ` +[ + { + "data": { + "amount": 1000000000000000, + "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", + }, + "method": "DecisionDepositPlaced", + "section": "referenda", + }, +] +`; + +exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > overflow promoted to deciding - root 1`] = ` +[ + { + "data": { + "proposal": { + "Inline": "0x00001468656c6c6f", + }, + "tally": { + "ayes": 0, + "nays": 0, + "support": 0, + }, + "track": 0, + }, + "method": "DecisionStarted", + "section": "referenda", + }, +] +`; + exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > queued referendum (track at capacity) - root 1`] = ` { "deciding": null, From 350bd0cbc8ae8820897a2f4b24e97a15bb54e549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Fri, 27 Mar 2026 17:00:08 +0000 Subject: [PATCH 08/13] Test track overflow handling in referenda submission --- packages/shared/src/governance.ts | 256 +++++++++++++++++++++++++----- 1 file changed, 212 insertions(+), 44 deletions(-) diff --git a/packages/shared/src/governance.ts b/packages/shared/src/governance.ts index 22810ad3a..5769ba995 100644 --- a/packages/shared/src/governance.ts +++ b/packages/shared/src/governance.ts @@ -1491,19 +1491,15 @@ export async function insufficientApprovalTest< } /** - * Test that a referendum is queued when the track's deciding capacity is saturated, - * then promoted to deciding once a slot opens: + * Shared preamble for track capacity overflow tests: * 1. submitting a blocker referendum and placing its decision deposit * 2. submitting the overflow referendum and placing its decision deposit * 3. backdating the overflow past its preparation period and nudging it into the queue * 4. verifying the overflow is queued - * 5. advancing the blocker to the last block of its confirmation period - * 6. verifying the blocker passes and the overflow is promoted to deciding + * + * Returns everything needed for the caller to free the blocker's slot and verify promotion. */ -export async function trackCapacityOverflowTest< - TCustom extends Record | undefined, - TInitStorages extends Record> | undefined, ->(chain: Chain, trackConfig: GovernanceTrackConfig) { +async function setupOverflow(chain: Chain, trackConfig: GovernanceTrackConfig) { const [client] = await setupNetworks(chain) const referendaTracks = client.api.consts.referenda.tracks @@ -1524,7 +1520,7 @@ export async function trackCapacityOverflowTest< }) /** - * 1. Submit a blocker referendum on the root track and place its decision deposit + * 1. Submit a blocker referendum and place its decision deposit */ const { referendumIndex: blockerIndex, ongoing: blockerOngoing } = await submitAndDeposit(client, trackConfig) @@ -1548,9 +1544,9 @@ export async function trackCapacityOverflowTest< /** * 3. Backdate the overflow past its preparation period and nudge it into the queue * - * Setting `DecidingCount` to 1 makes the runtime believe the track is full (the blocker - * occupies the only slot). Backdating `submitted` past the preparation period and nudging - * causes the runtime to evaluate the overflow: because the track is full, it queues it. + * Setting `DecidingCount` to `maxDeciding` makes the runtime believe the track is full. + * Backdating `submitted` past the preparation period and nudging causes the runtime to + * evaluate the overflow: because the track is full, it queues it. */ const block1 = (await client.api.rpc.chain.getHeader()).number.toNumber() @@ -1600,31 +1596,87 @@ export async function trackCapacityOverflowTest< assert(queuedOpt.isSome) const queued = queuedOpt.unwrap() assert(queued.isOngoing, 'overflow referendum should still be ongoing (queued, not terminal)') - const queuedRef = queued.asOngoing + expect(queued.asOngoing.inQueue.isTrue, 'overflow should be queued when track is at capacity').toBe(true) + expect(queued.asOngoing.deciding.isNone, 'queued overflow should not be in decision phase').toBe(true) + + return { + client, + chain, + track, + trackConfig, + blockerIndex, + blockerOngoing, + overflowIndex, + maxDeciding, + prepPeriod, + confirmPeriod, + } +} + +/** + * Post-promotion verification shared across overflow test variants: + * checks the overflow left the queue, entered deciding, and emitted `DecisionStarted`. + */ +async function verifyPromotion( + client: Awaited>[0], + overflowIndex: number, + trackConfig: GovernanceTrackConfig, + maxDeciding: number, +) { + const promotedOpt: Option = + (await client.api.query.referenda.referendumInfoFor(overflowIndex)) as any + assert(promotedOpt.isSome) + const promoted = promotedOpt.unwrap() + assert(promoted.isOngoing, 'overflow should still be ongoing after promotion') + expect(promoted.asOngoing.deciding.isSome, 'overflow should now be in decision phase').toBe(true) + expect(promoted.asOngoing.inQueue.isFalse, 'overflow should no longer be queued').toBe(true) + + const decidingCount = ((await client.api.query.referenda.decidingCount(trackConfig.trackId)) as any).toNumber() + expect(decidingCount, 'DecidingCount should reflect the promoted overflow').toBe(maxDeciding) - expect(queuedRef.inQueue.isTrue, 'overflow should be queued when track is at capacity').toBe(true) - expect(queuedRef.deciding.isNone, 'queued overflow should not be in decision phase').toBe(true) + const promotionEvents = await client.api.query.system.events() + const decisionStarted = promotionEvents.find( + ({ event }) => client.api.events.referenda.DecisionStarted.is(event) && event.data[0].toNumber() === overflowIndex, + ) + assert(decisionStarted, 'DecisionStarted event should be emitted when the overflow is promoted') + assert(client.api.events.referenda.DecisionStarted.is(decisionStarted.event)) + expect(decisionStarted.event.data[0].toNumber()).toBe(overflowIndex) + expect(decisionStarted.event.data[1].toNumber()).toBe(trackConfig.trackId) +} + +/** + * Test that the overflow referendum is promoted after the blocker is approved: + * 1–4. shared setup (submit both refs, queue the overflow) + * 5. advancing the blocker to the last block of its confirmation period + * 6. verifying the blocker passes and the overflow is promoted to deciding + */ +export async function overflowPromotionViaApprovalTest< + TCustom extends Record | undefined, + TInitStorages extends Record> | undefined, +>(chain: Chain, trackConfig: GovernanceTrackConfig) { + const ctx = await setupOverflow(chain, trackConfig) + const { client, blockerIndex, blockerOngoing, overflowIndex, maxDeciding, prepPeriod, confirmPeriod } = ctx /** * 5. Advance the blocker to the last block of its confirmation period * - * Setting `confirming` to `block2 + 1` places the blocker at the exact block where + * Setting `confirming` to `block + 1` places the blocker at the exact block where * confirmation ends. The injected tally has full approval and support so `is_passing` * holds. The blocker's nudge is written directly into the scheduler agenda alongside * the referendum state to avoid a second `scheduleInlineCallWithOrigin` call. */ - const block2 = (await client.api.rpc.chain.getHeader()).number.toNumber() + const block = (await client.api.rpc.chain.getHeader()).number.toNumber() const totalIssuance = (await client.api.query.balances.totalIssuance()).toBigInt() - const confirmDeadline = block2 + 1 + const confirmDeadline = block + 1 const decidingSince = confirmDeadline - confirmPeriod const blockerSubmitted = decidingSince - prepPeriod const schedulerBlock = chain.properties.schedulerBlockProvider === 'NonLocal' ? ((await client.api.query.parachainSystem.lastRelayChainBlockNumber()) as any).toNumber() - : block2 + 1 + : block + 1 await client.dev.setStorage({ Referenda: { @@ -1643,7 +1695,7 @@ export async function trackCapacityOverflowTest< deciding: { since: decidingSince, confirming: confirmDeadline }, tally: { ayes: totalIssuance.toString(), nays: 0, support: totalIssuance.toString() }, inQueue: false, - alarm: [block2 + 1, [block2 + 1, 0]], + alarm: [block + 1, [block + 1, 0]], }, }, ], @@ -1667,40 +1719,140 @@ export async function trackCapacityOverflowTest< await client.dev.newBlock() + const blockerResultOpt: Option = + (await client.api.query.referenda.referendumInfoFor(blockerIndex)) as any + assert(blockerResultOpt.isSome) + expect(blockerResultOpt.unwrap().isApproved, 'blocker should have been approved').toBe(true) + /** - * 6. Verify the blocker passed and the overflow was promoted + * 6. Verify the overflow was promoted + */ + + await client.dev.newBlock() + await verifyPromotion(client, overflowIndex, trackConfig, maxDeciding) +} + +/** + * Test that the overflow referendum is promoted after the blocker is rejected: + * 1–4. shared setup (submit both refs, queue the overflow) + * 5. fast-forwarding the blocker past its decision period with a failing tally + * 6. verifying the blocker is rejected and the overflow is promoted to deciding + */ +export async function overflowPromotionViaRejectionTest< + TCustom extends Record | undefined, + TInitStorages extends Record> | undefined, +>(chain: Chain, trackConfig: GovernanceTrackConfig) { + const ctx = await setupOverflow(chain, trackConfig) + const { client, blockerIndex, blockerOngoing, overflowIndex, maxDeciding } = ctx + + /** + * 5. Fast-forward the blocker past its decision period with a failing tally * - * The blocker's approval triggers `note_one_fewer_deciding`, which schedules - * `one_fewer_deciding` for the next block. That call pops the overflow from - * the `TrackQueue` and begins its decision phase. + * Backdating `deciding.since` so the decision period has elapsed by the next block. + * A nay-only tally is injected because `Perbill::from_rational(0, 0)` returns 100 % + * in Substrate — an empty tally would pass approval and enter confirmation instead. */ + const block = (await client.api.rpc.chain.getHeader()).number.toNumber() + const decisionPeriod = ctx.track[1].decisionPeriod.toNumber() + const prepPeriod = ctx.track[1].preparePeriod.toNumber() + const decidingSince = block + 1 - decisionPeriod + const blockerSubmitted = decidingSince - prepPeriod + + await client.dev.setStorage({ + Referenda: { + ReferendumInfoFor: [ + [ + [blockerIndex], + { + Ongoing: { + track: blockerOngoing.track, + origin: blockerOngoing.origin, + proposal: blockerOngoing.proposal, + enactment: blockerOngoing.enactment, + submitted: blockerSubmitted, + submissionDeposit: blockerOngoing.submissionDeposit, + decisionDeposit: blockerOngoing.decisionDeposit, + deciding: { since: decidingSince, confirming: null }, + tally: { ayes: 0, nays: 1, support: 0 }, + inQueue: false, + alarm: [block + 1, [block + 1, 0]], + }, + }, + ], + ], + }, + }) + + await scheduleInlineCallWithOrigin( + client, + client.api.tx.referenda.nudgeReferendum(blockerIndex).method.toHex(), + { system: 'Root' }, + chain.properties.schedulerBlockProvider, + ) + + await client.dev.newBlock() + const blockerResultOpt: Option = (await client.api.query.referenda.referendumInfoFor(blockerIndex)) as any assert(blockerResultOpt.isSome) - expect(blockerResultOpt.unwrap().isApproved, 'blocker should have been approved').toBe(true) + expect(blockerResultOpt.unwrap().isRejected, 'blocker should have been rejected').toBe(true) + + /** + * 6. Verify the overflow was promoted + */ await client.dev.newBlock() + await verifyPromotion(client, overflowIndex, trackConfig, maxDeciding) +} - const promotedOpt: Option = - (await client.api.query.referenda.referendumInfoFor(overflowIndex)) as any - assert(promotedOpt.isSome) - const promoted = promotedOpt.unwrap() - assert(promoted.isOngoing, 'overflow should still be ongoing after promotion') - expect(promoted.asOngoing.deciding.isSome, 'overflow should now be in decision phase').toBe(true) - expect(promoted.asOngoing.inQueue.isFalse, 'overflow should no longer be queued').toBe(true) +/** + * Test that the overflow referendum is promoted after the blocker is killed: + * 1–4. shared setup (submit both refs, queue the overflow) + * 5. killing the blocker with a Root-origin call via the scheduler + * 6. verifying the blocker is killed and the overflow is promoted to deciding + */ +export async function overflowPromotionViaKillTest< + TCustom extends Record | undefined, + TInitStorages extends Record> | undefined, +>(chain: Chain, trackConfig: GovernanceTrackConfig) { + const ctx = await setupOverflow(chain, trackConfig) + const { client, blockerIndex, overflowIndex, maxDeciding } = ctx - const decidingCount = ((await client.api.query.referenda.decidingCount(trackConfig.trackId)) as any).toNumber() - expect(decidingCount, 'DecidingCount should reflect the promoted overflow').toBe(maxDeciding) + /** + * 5. Kill the blocker with a Root-origin call via the scheduler + */ - const promotionEvents = await client.api.query.system.events() - const decisionStarted = promotionEvents.find( - ({ event }) => client.api.events.referenda.DecisionStarted.is(event) && event.data[0].toNumber() === overflowIndex, - ) - assert(decisionStarted, 'DecisionStarted event should be emitted when the overflow is promoted') - assert(client.api.events.referenda.DecisionStarted.is(decisionStarted.event)) - expect(decisionStarted.event.data[0].toNumber()).toBe(overflowIndex) - expect(decisionStarted.event.data[1].toNumber()).toBe(trackConfig.trackId) + const schedulerBlock = + chain.properties.schedulerBlockProvider === 'NonLocal' + ? ((await client.api.query.parachainSystem.lastRelayChainBlockNumber()) as any).toNumber() + : (await client.api.rpc.chain.getHeader()).number.toNumber() + 1 + + await client.dev.setStorage({ + Scheduler: { + agenda: [ + [ + [schedulerBlock], + [{ call: { Inline: client.api.tx.referenda.kill(blockerIndex).method.toHex() }, origin: { system: 'Root' } }], + ], + ], + incompleteSince: schedulerBlock, + }, + }) + + await client.dev.newBlock() + + const blockerResultOpt: Option = + (await client.api.query.referenda.referendumInfoFor(blockerIndex)) as any + assert(blockerResultOpt.isSome) + expect(blockerResultOpt.unwrap().isKilled, 'blocker should have been killed').toBe(true) + + /** + * 6. Verify the overflow was promoted + */ + + await client.dev.newBlock() + await verifyPromotion(client, overflowIndex, trackConfig, maxDeciding) } /// ------- @@ -1728,9 +1880,25 @@ function negativeFlowsForTrack(chain: Chain, trackConfig: GovernanceTr testFn: async () => await insufficientApprovalTest(chain, trackConfig), }, { - kind: 'test' as const, + kind: 'describe' as const, label: 'track capacity overflow', - testFn: async () => await trackCapacityOverflowTest(chain, trackConfig), + children: [ + { + kind: 'test' as const, + label: 'promotion via approval', + testFn: async () => await overflowPromotionViaApprovalTest(chain, trackConfig), + }, + { + kind: 'test' as const, + label: 'promotion via rejection', + testFn: async () => await overflowPromotionViaRejectionTest(chain, trackConfig), + }, + { + kind: 'test' as const, + label: 'promotion via kill', + testFn: async () => await overflowPromotionViaKillTest(chain, trackConfig), + }, + ], }, ], } From d6bd802b4e6c566aa566a5711ed8ef6d0cae39d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Fri, 27 Mar 2026 17:00:27 +0000 Subject: [PATCH 09/13] Update governance snapshots --- ...assetHubKusama.governance.e2e.test.ts.snap | 191 ------------------ ...setHubPolkadot.governance.e2e.test.ts.snap | 191 ------------------ 2 files changed, 382 deletions(-) diff --git a/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap b/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap index 45e23c145..8f858bf61 100644 --- a/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap +++ b/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap @@ -83,69 +83,6 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow } `; -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > overflow decision deposit placed - big_spender 1`] = ` -[ - { - "data": { - "amount": "(rounded 13000000000000)", - "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", - }, - "method": "DecisionDepositPlaced", - "section": "referenda", - }, -] -`; - -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > overflow promoted to deciding - big_spender 1`] = ` -[ - { - "data": { - "proposal": { - "Inline": "0x00001468656c6c6f", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 34, - }, - "method": "DecisionStarted", - "section": "referenda", - }, -] -`; - -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > queued referendum (track at capacity) - big_spender 1`] = ` -{ - "deciding": null, - "decisionDeposit": { - "amount": 13333333333200, - "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", - }, - "enactment": { - "after": 1, - }, - "inQueue": true, - "origin": { - "origins": "BigSpender", - }, - "proposal": { - "inline": "0x00001468656c6c6f", - }, - "submissionDeposit": { - "amount": 33333333333, - "who": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 34, -} -`; - exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient approval rejection > decision deposit refund after rejection (insufficient approval) - referendum_killer 1`] = ` [ { @@ -229,69 +166,6 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow } `; -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > overflow decision deposit placed - referendum_killer 1`] = ` -[ - { - "data": { - "amount": "(rounded 1700000000000000)", - "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", - }, - "method": "DecisionDepositPlaced", - "section": "referenda", - }, -] -`; - -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > overflow promoted to deciding - referendum_killer 1`] = ` -[ - { - "data": { - "proposal": { - "Inline": "0x00001468656c6c6f", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 21, - }, - "method": "DecisionStarted", - "section": "referenda", - }, -] -`; - -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > queued referendum (track at capacity) - referendum_killer 1`] = ` -{ - "deciding": null, - "decisionDeposit": { - "amount": 1666666666650000, - "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", - }, - "enactment": { - "after": 1, - }, - "inQueue": true, - "origin": { - "origins": "ReferendumKiller", - }, - "proposal": { - "inline": "0x00001468656c6c6f", - }, - "submissionDeposit": { - "amount": 33333333333, - "who": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 21, -} -`; - exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > insufficient approval rejection > decision deposit refund after rejection (insufficient approval) - root 1`] = ` [ { @@ -375,71 +249,6 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow } `; -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > overflow decision deposit placed - root 1`] = ` -[ - { - "data": { - "amount": "(rounded 3300000000000000)", - "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", - }, - "method": "DecisionDepositPlaced", - "section": "referenda", - }, -] -`; - -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > overflow promoted to deciding - root 1`] = ` -[ - { - "data": { - "proposal": { - "Inline": "0x00001468656c6c6f", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 0, - }, - "method": "DecisionStarted", - "section": "referenda", - }, -] -`; - -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > queued referendum (track at capacity) - root 1`] = ` -{ - "deciding": null, - "decisionDeposit": { - "amount": 3333333333300000, - "who": "FoQJpPyadYccjavVdTWxpxU7rUEaYhfLCPwXgkfD6Zat9QP", - }, - "enactment": { - "after": 1, - }, - "inQueue": true, - "origin": { - "system": { - "root": null, - }, - }, - "proposal": { - "inline": "0x00001468656c6c6f", - }, - "submissionDeposit": { - "amount": 33333333333, - "who": "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 0, -} -`; - exports[`Kusama Asset Hub Governance > referenda tests > referendum lifecycle test - submission, decision deposit, various voting should all work > cancelling referendum with signed origin 1`] = ` [ { diff --git a/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap b/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap index 7414b2db8..3a315e40a 100644 --- a/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap +++ b/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap @@ -83,69 +83,6 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl } `; -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > overflow decision deposit placed - big_spender 1`] = ` -[ - { - "data": { - "amount": 50000000000000, - "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", - }, - "method": "DecisionDepositPlaced", - "section": "referenda", - }, -] -`; - -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > overflow promoted to deciding - big_spender 1`] = ` -[ - { - "data": { - "proposal": { - "Inline": "0x00001468656c6c6f", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 34, - }, - "method": "DecisionStarted", - "section": "referenda", - }, -] -`; - -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > track capacity overflow > queued referendum (track at capacity) - big_spender 1`] = ` -{ - "deciding": null, - "decisionDeposit": { - "amount": 50000000000000, - "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", - }, - "enactment": { - "after": 1, - }, - "inQueue": true, - "origin": { - "origins": "BigSpender", - }, - "proposal": { - "inline": "0x00001468656c6c6f", - }, - "submissionDeposit": { - "amount": 100000000000, - "who": "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 34, -} -`; - exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient approval rejection > decision deposit refund after rejection (insufficient approval) - referendum_killer 1`] = ` [ { @@ -229,69 +166,6 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl } `; -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > overflow decision deposit placed - referendum_killer 1`] = ` -[ - { - "data": { - "amount": 500000000000000, - "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", - }, - "method": "DecisionDepositPlaced", - "section": "referenda", - }, -] -`; - -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > overflow promoted to deciding - referendum_killer 1`] = ` -[ - { - "data": { - "proposal": { - "Inline": "0x00001468656c6c6f", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 21, - }, - "method": "DecisionStarted", - "section": "referenda", - }, -] -`; - -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > track capacity overflow > queued referendum (track at capacity) - referendum_killer 1`] = ` -{ - "deciding": null, - "decisionDeposit": { - "amount": 500000000000000, - "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", - }, - "enactment": { - "after": 1, - }, - "inQueue": true, - "origin": { - "origins": "ReferendumKiller", - }, - "proposal": { - "inline": "0x00001468656c6c6f", - }, - "submissionDeposit": { - "amount": 100000000000, - "who": "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 21, -} -`; - exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > insufficient approval rejection > decision deposit refund after rejection (insufficient approval) - root 1`] = ` [ { @@ -375,71 +249,6 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl } `; -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > overflow decision deposit placed - root 1`] = ` -[ - { - "data": { - "amount": 1000000000000000, - "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", - }, - "method": "DecisionDepositPlaced", - "section": "referenda", - }, -] -`; - -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > overflow promoted to deciding - root 1`] = ` -[ - { - "data": { - "proposal": { - "Inline": "0x00001468656c6c6f", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 0, - }, - "method": "DecisionStarted", - "section": "referenda", - }, -] -`; - -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > track capacity overflow > queued referendum (track at capacity) - root 1`] = ` -{ - "deciding": null, - "decisionDeposit": { - "amount": 1000000000000000, - "who": "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3", - }, - "enactment": { - "after": 1, - }, - "inQueue": true, - "origin": { - "system": { - "root": null, - }, - }, - "proposal": { - "inline": "0x00001468656c6c6f", - }, - "submissionDeposit": { - "amount": 100000000000, - "who": "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5", - }, - "tally": { - "ayes": 0, - "nays": 0, - "support": 0, - }, - "track": 0, -} -`; - exports[`Polkadot Asset Hub Governance > referenda tests > referendum lifecycle test - submission, decision deposit, various voting should all work > cancelling referendum with signed origin 1`] = ` [ { From 04602bc8944a498d24368107833bbf7c95812bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Fri, 27 Mar 2026 18:02:39 +0000 Subject: [PATCH 10/13] Bump biome --- biome.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biome.json b/biome.json index be5315d60..fdcd7f1a0 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.5/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "ignoreUnknown": false, From 4bc8de5f8904d46d75a7b8e8a5a6e55bfd97d841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Fri, 27 Mar 2026 18:02:47 +0000 Subject: [PATCH 11/13] Fix lint in governance E2E tests --- packages/shared/src/governance.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/governance.ts b/packages/shared/src/governance.ts index 21c7fbd8a..ebee25909 100644 --- a/packages/shared/src/governance.ts +++ b/packages/shared/src/governance.ts @@ -999,6 +999,7 @@ export async function referendumLifecycleKillTest< submittedOnTrack.length === 1, `expected 1 Submitted event on small_tipper track, got ${submittedOnTrack.length}`, ) + assert(client.api.events.referenda.Submitted.is(submittedOnTrack[0].event)) const referendumIndex = submittedOnTrack[0].event.data[0].toNumber() /** @@ -1069,7 +1070,7 @@ export async function referendumLifecycleKillTest< } }) - const referendumDataOpt = await client.api.query.referenda.referendumInfoFor(referendumIndex) + const referendumDataOpt = (await client.api.query.referenda.referendumInfoFor(referendumIndex)) as any // killing a referendum does not remove it from storage, though it does prune most of its data. assert(referendumDataOpt.isSome, "referendum's data cannot be `None`") expect(referendumDataOpt.unwrap().isKilled, 'referendum should be killed!').toBeTruthy() @@ -1136,6 +1137,7 @@ async function submitAndDeposit( submittedOnTrack.length === 1, `expected exactly 1 Submitted event on track ${trackConfig.trackName} (ID ${trackConfig.trackId}), got ${submittedOnTrack.length}`, ) + assert(client.api.events.referenda.Submitted.is(submittedOnTrack[0].event)) const referendumIndex = submittedOnTrack[0].event.data[0].toNumber() /** @@ -1151,7 +1153,7 @@ async function submitAndDeposit( */ const referendumDataOpt: Option = - await client.api.query.referenda.referendumInfoFor(referendumIndex) + (await client.api.query.referenda.referendumInfoFor(referendumIndex)) as any assert(referendumDataOpt.isSome, "referendum's data cannot be `None` after submission + deposit") const referendumData = referendumDataOpt.unwrap() assert(referendumData.isOngoing) @@ -1356,7 +1358,7 @@ export async function insufficientSupportTest< await client.dev.newBlock() const postVoteOpt: Option = - await client.api.query.referenda.referendumInfoFor(referendumIndex) + (await client.api.query.referenda.referendumInfoFor(referendumIndex)) as any assert(postVoteOpt.isSome) const postVote = postVoteOpt.unwrap() assert(postVote.isOngoing) @@ -1466,7 +1468,7 @@ export async function insufficientApprovalTest< */ const postVoteOpt: Option = - await client.api.query.referenda.referendumInfoFor(referendumIndex) + (await client.api.query.referenda.referendumInfoFor(referendumIndex)) as any assert(postVoteOpt.isSome) const postVote = postVoteOpt.unwrap() assert(postVote.isOngoing) From 757847b3a486a36d892b8e3d872da3b59ea6cc18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Fri, 27 Mar 2026 18:03:26 +0000 Subject: [PATCH 12/13] Bump block numbers --- KNOWN_GOOD_BLOCK_NUMBERS_KUSAMA.env | 22 +++++++++++----------- KNOWN_GOOD_BLOCK_NUMBERS_POLKADOT.env | 22 +++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/KNOWN_GOOD_BLOCK_NUMBERS_KUSAMA.env b/KNOWN_GOOD_BLOCK_NUMBERS_KUSAMA.env index 46503925b..9fd34d67b 100644 --- a/KNOWN_GOOD_BLOCK_NUMBERS_KUSAMA.env +++ b/KNOWN_GOOD_BLOCK_NUMBERS_KUSAMA.env @@ -1,11 +1,11 @@ -ASSETHUBKUSAMA_BLOCK_NUMBER=14881946 -BASILISK_BLOCK_NUMBER=13689002 -BIFROSTKUSAMA_BLOCK_NUMBER=13170842 -BRIDGEHUBKUSAMA_BLOCK_NUMBER=7871865 -CORETIMEKUSAMA_BLOCK_NUMBER=4807258 -ENCOINTERKUSAMA_BLOCK_NUMBER=13048553 -KARURA_BLOCK_NUMBER=11238051 -KUSAMA_BLOCK_NUMBER=32842141 -MOONRIVER_BLOCK_NUMBER=15656188 -PEOPLEKUSAMA_BLOCK_NUMBER=8395093 -SHIDEN_BLOCK_NUMBER=14229257 +ASSETHUBKUSAMA_BLOCK_NUMBER=14890706 +BASILISK_BLOCK_NUMBER=13691369 +BIFROSTKUSAMA_BLOCK_NUMBER=13173486 +BRIDGEHUBKUSAMA_BLOCK_NUMBER=7873431 +CORETIMEKUSAMA_BLOCK_NUMBER=4808826 +ENCOINTERKUSAMA_BLOCK_NUMBER=13051335 +KARURA_BLOCK_NUMBER=11239620 +KUSAMA_BLOCK_NUMBER=32845285 +MOONRIVER_BLOCK_NUMBER=15659045 +PEOPLEKUSAMA_BLOCK_NUMBER=8398191 +SHIDEN_BLOCK_NUMBER=14231850 diff --git a/KNOWN_GOOD_BLOCK_NUMBERS_POLKADOT.env b/KNOWN_GOOD_BLOCK_NUMBERS_POLKADOT.env index edd142641..776225775 100644 --- a/KNOWN_GOOD_BLOCK_NUMBERS_POLKADOT.env +++ b/KNOWN_GOOD_BLOCK_NUMBERS_POLKADOT.env @@ -1,11 +1,11 @@ -ACALA_BLOCK_NUMBER=10777530 -ASSETHUBPOLKADOT_BLOCK_NUMBER=13879437 -ASTAR_BLOCK_NUMBER=12782304 -BIFROSTPOLKADOT_BLOCK_NUMBER=11636352 -BRIDGEHUBPOLKADOT_BLOCK_NUMBER=7318159 -COLLECTIVESPOLKADOT_BLOCK_NUMBER=8523027 -CORETIMEPOLKADOT_BLOCK_NUMBER=3909554 -HYDRATION_BLOCK_NUMBER=11880416 -MOONBEAM_BLOCK_NUMBER=14982728 -PEOPLEPOLKADOT_BLOCK_NUMBER=4248728 -POLKADOT_BLOCK_NUMBER=30541895 +ACALA_BLOCK_NUMBER=10779116 +ASSETHUBPOLKADOT_BLOCK_NUMBER=13887516 +ASTAR_BLOCK_NUMBER=12784990 +BIFROSTPOLKADOT_BLOCK_NUMBER=11639122 +BRIDGEHUBPOLKADOT_BLOCK_NUMBER=7319703 +COLLECTIVESPOLKADOT_BLOCK_NUMBER=8524618 +CORETIMEPOLKADOT_BLOCK_NUMBER=3911120 +HYDRATION_BLOCK_NUMBER=11883028 +MOONBEAM_BLOCK_NUMBER=14985655 +PEOPLEPOLKADOT_BLOCK_NUMBER=4250218 +POLKADOT_BLOCK_NUMBER=30545099 From 89986cb92775f7e674b496a9bbd9c6d1b7d4fe15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Bald=C3=A9?= Date: Mon, 30 Mar 2026 18:10:58 +0000 Subject: [PATCH 13/13] Remove unstable voting snapshots --- ...assetHubKusama.governance.e2e.test.ts.snap | 114 ------------------ ...setHubPolkadot.governance.e2e.test.ts.snap | 114 ------------------ packages/shared/src/governance.ts | 24 ++-- 3 files changed, 17 insertions(+), 235 deletions(-) diff --git a/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap b/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap index db65a65cc..299917eeb 100644 --- a/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap +++ b/packages/kusama/src/__snapshots__/assetHubKusama.governance.e2e.test.ts.snap @@ -13,25 +13,6 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow ] `; -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient approval rejection > rejected referendum event (insufficient approval) - big_spender 1`] = ` -{ - "event": { - "data": [ - 644, - { - "ayes": "0x000000000000000018efae0ec4898c07", - "nays": "0x00000000000000004acf0a2c4d9ca415", - "support": "0x000000000000000018efae0ec4898c07", - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient support rejection > decision deposit refund after rejection (insufficient support) - big_spender 1`] = ` [ { @@ -45,25 +26,6 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow ] `; -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient support rejection > rejected referendum event (insufficient support) - big_spender 1`] = ` -{ - "event": { - "data": [ - 644, - { - "ayes": 0, - "nays": 1000000000, - "support": 0, - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > big_spender > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - big_spender 1`] = ` { "event": { @@ -96,25 +58,6 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow ] `; -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient approval rejection > rejected referendum event (insufficient approval) - referendum_killer 1`] = ` -{ - "event": { - "data": [ - 644, - { - "ayes": "0x000000000000000018efae0ec4898c07", - "nays": "0x00000000000000004acf0a2c4d9ca415", - "support": "0x000000000000000018efae0ec4898c07", - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient support rejection > decision deposit refund after rejection (insufficient support) - referendum_killer 1`] = ` [ { @@ -128,25 +71,6 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow ] `; -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient support rejection > rejected referendum event (insufficient support) - referendum_killer 1`] = ` -{ - "event": { - "data": [ - 644, - { - "ayes": 0, - "nays": 1000000000, - "support": 0, - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - referendum_killer 1`] = ` { "event": { @@ -179,25 +103,6 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow ] `; -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > insufficient approval rejection > rejected referendum event (insufficient approval) - root 1`] = ` -{ - "event": { - "data": [ - 644, - { - "ayes": "0x000000000000000018efae0ec4898c07", - "nays": "0x00000000000000004acf0a2c4d9ca415", - "support": "0x000000000000000018efae0ec4898c07", - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > insufficient support rejection > decision deposit refund after rejection (insufficient support) - root 1`] = ` [ { @@ -211,25 +116,6 @@ exports[`Kusama Asset Hub Governance > referenda tests > negative execution flow ] `; -exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > insufficient support rejection > rejected referendum event (insufficient support) - root 1`] = ` -{ - "event": { - "data": [ - 644, - { - "ayes": 0, - "nays": 1000000000, - "support": 0, - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Kusama Asset Hub Governance > referenda tests > negative execution flows > root > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - root 1`] = ` { "event": { diff --git a/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap b/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap index 8e16432f5..7721d63e9 100644 --- a/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap +++ b/packages/polkadot/src/__snapshots__/assetHubPolkadot.governance.e2e.test.ts.snap @@ -13,25 +13,6 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl ] `; -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient approval rejection > rejected referendum event (insufficient approval) - big_spender 1`] = ` -{ - "event": { - "data": [ - 1873, - { - "ayes": "0x00000000000000001743cbf495d5d359", - "nays": "0x000000000000000045cb63ddc1817a0b", - "support": "0x00000000000000001743cbf495d5d359", - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient support rejection > decision deposit refund after rejection (insufficient support) - big_spender 1`] = ` [ { @@ -45,25 +26,6 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl ] `; -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > insufficient support rejection > rejected referendum event (insufficient support) - big_spender 1`] = ` -{ - "event": { - "data": [ - 1873, - { - "ayes": 0, - "nays": 1000000000, - "support": 0, - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > big_spender > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - big_spender 1`] = ` { "event": { @@ -96,25 +58,6 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl ] `; -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient approval rejection > rejected referendum event (insufficient approval) - referendum_killer 1`] = ` -{ - "event": { - "data": [ - 1873, - { - "ayes": "0x00000000000000001743cbf495d5d359", - "nays": "0x000000000000000045cb63ddc1817a0b", - "support": "0x00000000000000001743cbf495d5d359", - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient support rejection > decision deposit refund after rejection (insufficient support) - referendum_killer 1`] = ` [ { @@ -128,25 +71,6 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl ] `; -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > insufficient support rejection > rejected referendum event (insufficient support) - referendum_killer 1`] = ` -{ - "event": { - "data": [ - 1873, - { - "ayes": 0, - "nays": 1000000000, - "support": 0, - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > referendum_killer > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - referendum_killer 1`] = ` { "event": { @@ -179,25 +103,6 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl ] `; -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > insufficient approval rejection > rejected referendum event (insufficient approval) - root 1`] = ` -{ - "event": { - "data": [ - 1873, - { - "ayes": "0x00000000000000001743cbf495d5d359", - "nays": "0x000000000000000045cb63ddc1817a0b", - "support": "0x00000000000000001743cbf495d5d359", - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > insufficient support rejection > decision deposit refund after rejection (insufficient support) - root 1`] = ` [ { @@ -211,25 +116,6 @@ exports[`Polkadot Asset Hub Governance > referenda tests > negative execution fl ] `; -exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > insufficient support rejection > rejected referendum event (insufficient support) - root 1`] = ` -{ - "event": { - "data": [ - 1873, - { - "ayes": 0, - "nays": 1000000000, - "support": 0, - }, - ], - }, - "phase": { - "initialization": null, - }, - "topics": [], -} -`; - exports[`Polkadot Asset Hub Governance > referenda tests > negative execution flows > root > missing decision deposit timeout > timed-out referendum event (missing decision deposit) - root 1`] = ` { "event": { diff --git a/packages/shared/src/governance.ts b/packages/shared/src/governance.ts index ebee25909..1d9259ab0 100644 --- a/packages/shared/src/governance.ts +++ b/packages/shared/src/governance.ts @@ -1236,6 +1236,7 @@ async function verifyRejection( referendumIndex: number, trackLabel: string, scenarioLabel: string, + expectedTally: { ayes: bigint; nays: bigint; support: bigint }, ) { /** * 1. Check the `Rejected` event @@ -1246,11 +1247,12 @@ async function verifyRejection( expect(referendaEvents.length, 'rejecting a referendum should emit 1 referenda event').toBe(1) const rejectedEvent = referendaEvents[0] - expect(client.api.events.referenda.Rejected.is(rejectedEvent.event)).toBe(true) - - await check(rejectedEvent) - .redact({ removeKeys: /index|pollIndex/ }) - .toMatchSnapshot(`rejected referendum event (${scenarioLabel}) - ${trackLabel}`) + assert(client.api.events.referenda.Rejected.is(rejectedEvent.event)) + const [index, tally] = rejectedEvent.event.data + expect(index.toNumber(), 'Rejected event should reference the correct referendum').toBe(referendumIndex) + expect(tally.ayes.toBigInt()).toBe(expectedTally.ayes) + expect(tally.nays.toBigInt()).toBe(expectedTally.nays) + expect(tally.support.toBigInt()).toBe(expectedTally.support) /** * 2. Check the rejected referendum's data @@ -1385,7 +1387,11 @@ export async function insufficientSupportTest< * 4.3 checking that the submission deposit cannot be refunded (`BadStatus`) */ - await verifyRejection(client, referendumIndex, trackConfig.trackName, 'insufficient support') + await verifyRejection(client, referendumIndex, trackConfig.trackName, 'insufficient support', { + ayes: ongoingPostVote.tally.ayes.toBigInt(), + nays: ongoingPostVote.tally.nays.toBigInt(), + support: ongoingPostVote.tally.support.toBigInt(), + }) } /** @@ -1495,7 +1501,11 @@ export async function insufficientApprovalTest< * 6.3 checking that the submission deposit cannot be refunded (`BadStatus`) */ - await verifyRejection(client, referendumIndex, trackConfig.trackName, 'insufficient approval') + await verifyRejection(client, referendumIndex, trackConfig.trackName, 'insufficient approval', { + ayes: ayeBalance, + nays: nayBalance, + support: ayeBalance, + }) } /**