1
1
import { expect } from 'chai'
2
2
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'
4
4
import { deployRif } from '../scripts/deploy-rif'
5
5
import { deployGovernor } from '../scripts/deploy-governor'
6
6
import { deployTimelock } from '../scripts/deploy-timelock'
7
7
import { RIFToken , RootDao , StRIFToken , TokenFaucet , DaoTimelockUpgradable } from '../typechain-types'
8
8
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
9
9
import { deployStRIF } from '../scripts/deploy-stRIF'
10
- import { parseEther , solidityPackedKeccak256 , toBeHex } from 'ethers'
10
+ import { parseEther , solidityPackedKeccak256 } from 'ethers'
11
11
import { Proposal , ProposalState , OperationState } from '../types'
12
12
13
13
describe ( 'RootDAO Contact' , ( ) => {
@@ -51,6 +51,10 @@ describe('RootDAO Contact', () => {
51
51
expect ( await timelock . getMinDelay ( ) )
52
52
} )
53
53
54
+ it ( 'Timelock should be set on the Governor' , async ( ) => {
55
+ expect ( await governor . timelock ( ) ) . to . equal ( await timelock . getAddress ( ) )
56
+ } )
57
+
54
58
it ( 'voting delay should be initialized' , async ( ) => {
55
59
expect ( await governor . votingDelay ( ) ) . to . equal ( initialVotingDelay )
56
60
} )
@@ -78,22 +82,19 @@ describe('RootDAO Contact', () => {
78
82
const generateDescriptionHash = ( proposalDesc ?: string ) =>
79
83
solidityPackedKeccak256 ( [ 'string' ] , [ proposalDesc ?? defaultDescription ] )
80
84
81
- const createProposal = async ( proposalDesc ?: string ) => {
85
+ const createProposal = async ( proposalDesc = defaultDescription ) => {
82
86
const blockHeight = await ethers . provider . getBlockNumber ( )
83
87
const votingDelay = await governor . votingDelay ( )
84
88
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 ] ]
91
92
92
93
proposalId = await governor
93
94
. connect ( holders [ 0 ] )
94
95
. hashProposal ( proposal [ 0 ] , proposal [ 1 ] , proposal [ 2 ] , generateDescriptionHash ( proposalDesc ) )
95
96
96
- const proposalTx = await governor . connect ( holders [ 0 ] ) . propose ( ...proposal )
97
+ const proposalTx = await governor . connect ( holders [ 0 ] ) . propose ( ...proposal , proposalDesc )
97
98
await proposalTx . wait ( )
98
99
proposalSnapshot = votingDelay + BigInt ( blockHeight ) + 1n
99
100
return proposalTx
@@ -172,7 +173,7 @@ describe('RootDAO Contact', () => {
172
173
it ( 'the rest of the holders should not be able to create proposal' , async ( ) => {
173
174
await Promise . all (
174
175
holders . slice ( 1 ) . map ( async holder => {
175
- const tx = governor . connect ( holder ) . propose ( ...proposal )
176
+ const tx = governor . connect ( holder ) . propose ( ...proposal , defaultDescription )
176
177
await expect ( tx ) . to . be . revertedWithCustomError (
177
178
{ interface : governor . interface } ,
178
179
'GovernorInsufficientProposerVotes' ,
@@ -244,7 +245,7 @@ describe('RootDAO Contact', () => {
244
245
expect ( state ) . to . equal ( ProposalState . Defeated )
245
246
} )
246
247
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 ( ) => {
248
249
await createProposal ( otherDesc )
249
250
250
251
await mine ( ( await governor . votingDelay ( ) ) + 1n )
@@ -262,28 +263,90 @@ describe('RootDAO Contact', () => {
262
263
263
264
expect ( await getState ( ) ) . to . be . equal ( ProposalState . Succeeded )
264
265
} )
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 ( )
265
290
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
268
311
} )
269
312
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 )
273
319
} )
274
320
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 )
277
323
} )
278
324
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
+ } )
287
350
} )
288
351
} )
289
352
} )
0 commit comments