Skip to content

Commit b5bcb22

Browse files
committed
test: proposal queueing and execution
1 parent 22e929b commit b5bcb22

File tree

3 files changed

+96
-28
lines changed

3 files changed

+96
-28
lines changed

scripts/deploy-governor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export const deployGovernor = async (
2020
await rootDAOGovernor.waitForDeployment()
2121
const governorAddr = await rootDAOGovernor.getAddress()
2222

23+
/*
24+
From the GovernorTimelockControl docs:
25+
The {Governor} needs the proposer (and ideally the executor) roles for the {Governor} to work properly.
26+
*/
27+
2328
// grant Proposer role to the Governor
2429
const proposerRole = await timelock.PROPOSER_ROLE()
2530
const grantProposerTx = await timelock.grantRole(proposerRole, governorAddr)

test/Governor.test.ts

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { expect } from 'chai'
22
import { ethers } from 'hardhat'
3-
import { loadFixture, mine } from '@nomicfoundation/hardhat-toolbox/network-helpers'
3+
import { loadFixture, mine, time } from '@nomicfoundation/hardhat-toolbox/network-helpers'
44
import { deployRif } from '../scripts/deploy-rif'
55
import { deployGovernor } from '../scripts/deploy-governor'
66
import { deployTimelock } from '../scripts/deploy-timelock'
77
import { RIFToken, RootDao, StRIFToken, TokenFaucet, DaoTimelockUpgradable } from '../typechain-types'
88
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
99
import { deployStRIF } from '../scripts/deploy-stRIF'
10-
import { parseEther, solidityPackedKeccak256, toBeHex } from 'ethers'
10+
import { parseEther, solidityPackedKeccak256 } from 'ethers'
1111
import { Proposal, ProposalState, OperationState } from '../types'
1212

1313
describe('RootDAO Contact', () => {
@@ -51,6 +51,10 @@ describe('RootDAO Contact', () => {
5151
expect(await timelock.getMinDelay())
5252
})
5353

54+
it('Timelock should be set on the Governor', async () => {
55+
expect(await governor.timelock()).to.equal(await timelock.getAddress())
56+
})
57+
5458
it('voting delay should be initialized', async () => {
5559
expect(await governor.votingDelay()).to.equal(initialVotingDelay)
5660
})
@@ -78,22 +82,19 @@ describe('RootDAO Contact', () => {
7882
const generateDescriptionHash = (proposalDesc?: string) =>
7983
solidityPackedKeccak256(['string'], [proposalDesc ?? defaultDescription])
8084

81-
const createProposal = async (proposalDesc?: string) => {
85+
const createProposal = async (proposalDesc = defaultDescription) => {
8286
const blockHeight = await ethers.provider.getBlockNumber()
8387
const votingDelay = await governor.votingDelay()
8488

85-
proposal = [
86-
[await holders[1].getAddress()],
87-
[parseEther(sendAmount)],
88-
['0x00'],
89-
proposalDesc ?? defaultDescription,
90-
]
89+
// proposal = [[await holders[1].getAddress()], [parseEther(sendAmount)], ['0x00']]
90+
const calldata = stRIF.interface.encodeFunctionData('symbol')
91+
proposal = [[await stRIF.getAddress()], [0n], [calldata]]
9192

9293
proposalId = await governor
9394
.connect(holders[0])
9495
.hashProposal(proposal[0], proposal[1], proposal[2], generateDescriptionHash(proposalDesc))
9596

96-
const proposalTx = await governor.connect(holders[0]).propose(...proposal)
97+
const proposalTx = await governor.connect(holders[0]).propose(...proposal, proposalDesc)
9798
await proposalTx.wait()
9899
proposalSnapshot = votingDelay + BigInt(blockHeight) + 1n
99100
return proposalTx
@@ -172,7 +173,7 @@ describe('RootDAO Contact', () => {
172173
it('the rest of the holders should not be able to create proposal', async () => {
173174
await Promise.all(
174175
holders.slice(1).map(async holder => {
175-
const tx = governor.connect(holder).propose(...proposal)
176+
const tx = governor.connect(holder).propose(...proposal, defaultDescription)
176177
await expect(tx).to.be.revertedWithCustomError(
177178
{ interface: governor.interface },
178179
'GovernorInsufficientProposerVotes',
@@ -244,7 +245,7 @@ describe('RootDAO Contact', () => {
244245
expect(state).to.equal(ProposalState.Defeated)
245246
})
246247

247-
it('when proposal reach quorum and votingPeriod is reached proposal state should become ProposalState.Succeeded', async () => {
248+
it('when proposal reaches quorum and votingPeriod is reached proposal state should become ProposalState.Succeeded', async () => {
248249
await createProposal(otherDesc)
249250

250251
await mine((await governor.votingDelay()) + 1n)
@@ -262,28 +263,90 @@ describe('RootDAO Contact', () => {
262263

263264
expect(await getState()).to.be.equal(ProposalState.Succeeded)
264265
})
266+
})
267+
268+
describe('Queueing the Proposal', () => {
269+
let eta: bigint = 0n
270+
let timelockPropId: string
271+
/*
272+
https://docs.openzeppelin.com/contracts/5.x/api/governance#IGovernor-queue-address---uint256---bytes---bytes32-
273+
Queue a proposal. Some governors require this step to be performed before execution
274+
can happen. If queuing is not necessary, this function may revert. Queuing a proposal
275+
requires the quorum to be reached, the vote to be successful, and the deadline to be reached.
276+
*/
277+
it('proposal should need queueing before execution', async () => {
278+
expect(await governor.proposalNeedsQueuing(proposalId)).to.be.true
279+
})
280+
it('proposer should put the proposal to the execution queue', async () => {
281+
const minDelay = await timelock.getMinDelay()
282+
const lastBlockTimestamp = await time.latest()
283+
// Estimated Time of Arrival
284+
eta = BigInt(lastBlockTimestamp) + minDelay + 1n
285+
const from = await time.latestBlock()
286+
const tx = await governor['queue(uint256)'](proposalId)
287+
//event ProposalQueued(uint256 proposalId, uint256 etaSeconds)
288+
await expect(tx).to.emit(governor, 'ProposalQueued').withArgs(proposalId, eta)
289+
await tx.wait()
265290

266-
it('Proposal should be registered as an operation on the Timelock', async () => {
267-
expect(await timelock.isOperation(toBeHex(proposalId)))
291+
/*
292+
There is a second event emitted by the same tx: it is Timelock's `CallScheduled`.
293+
294+
event CallScheduled(bytes32 indexed id, uint256 indexed index, address target, uint256 value, bytes data, bytes32 predecessor, uint256 delay)
295+
296+
Let's take a look at its arguments. We are going to extract Timelock proposal ID
297+
which differs from Governor's proposal ID. Knowing this ID we can query some info
298+
from the Timelock directly
299+
*/
300+
301+
const to = await time.latestBlock()
302+
const filter =
303+
timelock.filters['CallScheduled(bytes32,uint256,address,uint256,bytes,bytes32,uint256)']
304+
const [event] = await timelock.queryFilter(filter, from, to)
305+
const [id, , target, value, data, , delay] = event.args
306+
timelockPropId = id // save timelock proposal ID
307+
expect(target).to.equal(proposal[0][0])
308+
expect(value).to.equal(proposal[1][0])
309+
expect(data).to.equal(proposal[2][0])
310+
expect(delay).to.equal(minDelay) // 86400
268311
})
269312

270-
it('Operation should be in Unset stage', async () => {
271-
const state = Number(await timelock.getOperationState(toBeHex(proposalId)))
272-
expect(state).to.equal(OperationState.Unset)
313+
it('proposal should be in the Timelock`s OperationState.Waiting state after queueing', async () => {
314+
const timelockState = await timelock.getOperationState(timelockPropId)
315+
expect(timelockState).to.equal(OperationState.Waiting)
316+
})
317+
it('proposal should be in the Governor`s ProposalState.Queued state after queueing', async () => {
318+
expect(await governor.state(proposalId)).to.equal(ProposalState.Queued)
273319
})
274320

275-
it('should return operation timestamp as 0 (because it is unset)', async () => {
276-
expect(await timelock.getTimestamp(toBeHex(proposalId))).to.equal(0)
321+
it('proposal ETA (Estimated Time of Arrival) should be recorded on the governor', async () => {
322+
expect(await governor.proposalEta(proposalId)).to.equal(eta)
277323
})
278324

279-
/* it('after a proposal succeeded it should be queued for execution', async () => {
280-
const tx = await governor
281-
.connect(deployer)
282-
[
283-
'execute(address[],uint256[],bytes[],bytes32)'
284-
](proposal[0], proposal[1], proposal[2], generateDescriptionHash(otherDesc))
285-
await tx.wait()
286-
}) */
325+
it('should increase blockchain node time to proposal ETA', async () => {
326+
await time.increaseTo(eta)
327+
const block = await ethers.provider.getBlock('latest')
328+
expect(block?.timestamp).to.equal(eta)
329+
})
330+
331+
it('proposal should move to the OperationState.Ready state on the Timelock', async () => {
332+
const timelockState = await timelock.getOperationState(timelockPropId)
333+
expect(timelockState).to.equal(OperationState.Ready)
334+
// the same info
335+
expect(await timelock.isOperationReady(timelockPropId)).to.be.true
336+
})
337+
338+
it('should execute proposal on the Governor after the ETA', async () => {
339+
const tx = await governor['execute(address[],uint256[],bytes[],bytes32)'](
340+
...proposal,
341+
generateDescriptionHash(otherDesc),
342+
)
343+
await expect(tx).to.emit(governor, 'ProposalExecuted').withArgs(proposalId)
344+
})
345+
346+
it('proposal should move to Executed state after execution', async () => {
347+
const state = await governor.state(proposalId)
348+
expect(state).to.equal(ProposalState.Executed)
349+
})
287350
})
288351
})
289352
})

types/governor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export enum VoteType {
1717
Abstain,
1818
}
1919

20-
export type Proposal = [string[], BigNumberish[], BytesLike[], string]
20+
export type Proposal = [string[], BigNumberish[], BytesLike[]]
2121

2222
export enum OperationState {
2323
Unset,

0 commit comments

Comments
 (0)