Skip to content

Commit 22e929b

Browse files
committed
fix: assign roles to governor on timelock
1 parent 16fc30d commit 22e929b

File tree

6 files changed

+113
-48
lines changed

6 files changed

+113
-48
lines changed

eslint.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ export default [
99
pluginJs.configs.recommended,
1010
...tseslint.configs.recommended,
1111
eslintPluginPrettier,
12+
{
13+
rules: {
14+
'prettier/prettier': ['warn'],
15+
},
16+
},
1217
]

scripts/deploy-governor.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
11
import hre, { ethers, upgrades } from 'hardhat'
2-
import { GovernorTimelockControlUpgradeable, RootDao } from '../typechain-types'
2+
import { RootDao } from '../typechain-types'
33

4-
export const deployGovernor = async (tokenAddress: string, deployerAddress: string) => {
4+
export const deployGovernor = async (
5+
tokenAddress: string,
6+
deployerAddress: string,
7+
timelockAddress: string,
8+
) => {
9+
const timelock = await hre.ethers.getContractAt('DaoTimelockUpgradable', timelockAddress)
510
const RootDAOFactory = await ethers.getContractFactory('RootDao')
6-
const TimelockFactory = await ethers.getContractFactory('DaoTimelockUpgradable')
7-
// TODO: figure out why it allows to put only a single argument
8-
const timelock = (await upgrades.deployProxy(TimelockFactory, [1, [], [], ethers.ZeroAddress], {
9-
initializer: 'initialize(uint256,address[],address[],address)',
10-
kind: 'uups',
11-
timeout: 0,
12-
})) as unknown as GovernorTimelockControlUpgradeable
13-
1411
const rootDAOGovernor = (await upgrades.deployProxy(
1512
RootDAOFactory,
16-
[tokenAddress, await timelock.getAddress(), deployerAddress],
13+
[tokenAddress, timelockAddress, deployerAddress],
1714
{
1815
initializer: 'initialize',
1916
kind: 'uups',
2017
timeout: 0,
2118
},
2219
)) as unknown as RootDao
20+
await rootDAOGovernor.waitForDeployment()
21+
const governorAddr = await rootDAOGovernor.getAddress()
22+
23+
// grant Proposer role to the Governor
24+
const proposerRole = await timelock.PROPOSER_ROLE()
25+
const grantProposerTx = await timelock.grantRole(proposerRole, governorAddr)
26+
await grantProposerTx.wait()
27+
28+
// grant Executor role to the Governor
29+
const executorRole = await timelock.EXECUTOR_ROLE()
30+
const grantExecutorRoleTx = await timelock.grantRole(executorRole, governorAddr)
31+
await grantExecutorRoleTx.wait()
2332

24-
const rootDAOContact = await rootDAOGovernor.waitForDeployment()
33+
// renounce Admin role - this operation should be finally done by the DAO deployer
34+
const adminRole = await timelock.DEFAULT_ADMIN_ROLE()
35+
const [deployer] = await hre.ethers.getSigners()
36+
const renounceTx = await timelock.renounceRole(adminRole, deployer.address)
37+
await renounceTx.wait()
2538

26-
console.log(`Deployed Governor on ${hre.network.name} with address ${await rootDAOContact.getAddress()}`)
39+
console.log(`Deployed Governor on ${hre.network.name} with address ${await rootDAOGovernor.getAddress()}`)
2740

28-
return rootDAOContact
41+
return rootDAOGovernor
2942
}

scripts/deploy-timelock.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import hre, { ethers, upgrades } from 'hardhat'
2+
import { DaoTimelockUpgradable } from '../typechain-types'
3+
4+
export const deployTimelock = async () => {
5+
const [owner] = await hre.ethers.getSigners()
6+
const TimelockFactory = await ethers.getContractFactory('DaoTimelockUpgradable')
7+
/*
8+
https://docs.openzeppelin.com/contracts/5.x/api/governance#TimelockController
9+
Initialize the TImelock contract with the following parameters:
10+
minDelay: initial minimum delay in seconds for operations
11+
proposers: accounts to be granted proposer and canceller roles
12+
executors: accounts to be granted executor role
13+
admin: optional account to be granted admin role; disable with zero address
14+
*/
15+
const minDelay = 60 * 60 * 24 // 24 hours in seconds
16+
const proposers: string[] = []
17+
const executors = [] as string[]
18+
const admin = owner.address
19+
const timelock = await upgrades.deployProxy(TimelockFactory, [minDelay, proposers, executors, admin], {
20+
initializer: 'initialize',
21+
kind: 'uups',
22+
timeout: 0,
23+
})
24+
await timelock.waitForDeployment()
25+
26+
console.log(`Deployed Timelock on ${hre.network.name} with address ${await timelock.getAddress()}`)
27+
28+
return timelock as unknown as DaoTimelockUpgradable
29+
}

scripts/deploy.ts renamed to scripts/deploy-tokens.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ethers } from 'hardhat'
22
import { deployStRIF } from './deploy-stRIF'
33
import { deployRif } from './deploy-rif'
44

5-
const deploy = async () => {
5+
const deployTokens = async () => {
66
const [deployer] = await ethers.getSigners()
77
const { rifAddress } = await deployRif(deployer)
88
const deployerAddress = await deployer.getAddress()
@@ -12,7 +12,7 @@ const deploy = async () => {
1212
console.log(`stRIFToken deployed at: ${await stRIFToken.getAddress()}`)
1313
}
1414

15-
deploy().catch(err => {
15+
deployTokens().catch(err => {
1616
console.log('deployment error: ', err)
1717
process.exit(1)
1818
})

test/Governor.test.ts

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { ethers } from 'hardhat'
33
import { loadFixture, mine } from '@nomicfoundation/hardhat-toolbox/network-helpers'
44
import { deployRif } from '../scripts/deploy-rif'
55
import { deployGovernor } from '../scripts/deploy-governor'
6-
import { RIFToken, RootDao, StRIFToken, TokenFaucet } from '../typechain-types'
6+
import { deployTimelock } from '../scripts/deploy-timelock'
7+
import { RIFToken, RootDao, StRIFToken, TokenFaucet, DaoTimelockUpgradable } from '../typechain-types'
78
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
89
import { deployStRIF } from '../scripts/deploy-stRIF'
9-
import { parseEther, solidityPackedKeccak256 } from 'ethers'
10-
import { Proposal, ProposalState } from '../types'
10+
import { parseEther, solidityPackedKeccak256, toBeHex } from 'ethers'
11+
import { Proposal, ProposalState, OperationState } from '../types'
1112

1213
describe('RootDAO Contact', () => {
1314
const initialVotingDelay = 1n // secs 1 day
@@ -16,6 +17,7 @@ describe('RootDAO Contact', () => {
1617

1718
let rif: { rifToken: RIFToken; rifAddress: string; tokenFaucet: TokenFaucet }
1819
let stRIF: StRIFToken
20+
let timelock: DaoTimelockUpgradable
1921
let governor: RootDao
2022
let holders: SignerWithAddress[]
2123
let deployer: SignerWithAddress
@@ -30,19 +32,25 @@ describe('RootDAO Contact', () => {
3032
rif = await loadFixture(deployRIF)
3133
const deployGovToken = async () => deployStRIF(rif.rifAddress, deployer.address)
3234
stRIF = await loadFixture(deployGovToken)
33-
34-
const deployDAO = async () => deployGovernor(await stRIF.getAddress(), deployer.address)
35+
timelock = await loadFixture(deployTimelock)
36+
const deployDAO = async () =>
37+
deployGovernor(await stRIF.getAddress(), deployer.address, await timelock.getAddress())
3538
governor = await loadFixture(deployDAO)
3639
})
3740

3841
describe('Upon deployment', () => {
3942
it('should deploy all contracts', async () => {
4043
expect(rif.rifAddress).to.be.properAddress
4144
expect(await stRIF.getAddress()).to.be.properAddress
45+
expect(await timelock.getAddress()).to.be.properAddress
4246
expect(await governor.getAddress()).to.be.properAddress
4347
expect(await rif.tokenFaucet.getAddress()).to.be.properAddress
4448
})
4549

50+
it('min delay should be set on the Timelock', async () => {
51+
expect(await timelock.getMinDelay())
52+
})
53+
4654
it('voting delay should be initialized', async () => {
4755
expect(await governor.votingDelay()).to.equal(initialVotingDelay)
4856
})
@@ -60,8 +68,8 @@ describe('RootDAO Contact', () => {
6068
const sendAmount = '2'
6169
let proposal: Proposal
6270
let proposalId: bigint
63-
// let proposalCalldata: string
6471
let proposalSnapshot: bigint
72+
// let proposalCalldata: string
6573

6674
const getState = async () => await governor.state(proposalId)
6775

@@ -72,7 +80,6 @@ describe('RootDAO Contact', () => {
7280

7381
const createProposal = async (proposalDesc?: string) => {
7482
const blockHeight = await ethers.provider.getBlockNumber()
75-
const votingPeriod = await governor.votingPeriod()
7683
const votingDelay = await governor.votingDelay()
7784

7885
proposal = [
@@ -86,16 +93,10 @@ describe('RootDAO Contact', () => {
8693
.connect(holders[0])
8794
.hashProposal(proposal[0], proposal[1], proposal[2], generateDescriptionHash(proposalDesc))
8895

89-
const tx = await governor.connect(holders[0]).propose(...proposal)
90-
91-
const snapshot = votingDelay + BigInt(blockHeight) + 1n
92-
const snapshotPlusDuration = votingPeriod + votingDelay + BigInt(blockHeight) + 1n
93-
94-
return {
95-
snapshot,
96-
snapshotPlusDuration,
97-
tx,
98-
}
96+
const proposalTx = await governor.connect(holders[0]).propose(...proposal)
97+
await proposalTx.wait()
98+
proposalSnapshot = votingDelay + BigInt(blockHeight) + 1n
99+
return proposalTx
99100
}
100101

101102
const checkVotes = async () => {
@@ -145,25 +146,29 @@ describe('RootDAO Contact', () => {
145146
})
146147

147148
it('holder[0] should be able to create proposal', async () => {
148-
const { snapshot, snapshotPlusDuration, tx } = await createProposal()
149-
149+
const proposalTx = await createProposal()
150+
const votingPeriod = await governor.votingPeriod()
150151
const ProposalCreatedEvent = [
151152
proposalId,
152153
holders[0].address, // proposer
153154
proposal[0], // targets
154155
proposal[1], // values
155156
[''], // ?
156157
proposal[2], // calldatas
157-
snapshot,
158-
snapshotPlusDuration,
158+
proposalSnapshot,
159+
proposalSnapshot + votingPeriod,
159160
defaultDescription,
160161
]
161162

162-
await expect(tx)
163+
await expect(proposalTx)
163164
.to.emit(governor, 'ProposalCreated')
164165
.withArgs(...ProposalCreatedEvent)
165166
})
166167

168+
it('proposal creation should initiate a Proposal Snapshot creation', async () => {
169+
expect(await governor.proposalSnapshot(proposalId)).equal(proposalSnapshot)
170+
})
171+
167172
it('the rest of the holders should not be able to create proposal', async () => {
168173
await Promise.all(
169174
holders.slice(1).map(async holder => {
@@ -176,13 +181,6 @@ describe('RootDAO Contact', () => {
176181
)
177182
})
178183

179-
it('proposal creation should initiate a Proposal Snapshot creation', async () => {
180-
const votingDelay = await governor.votingDelay()
181-
const block = await ethers.provider.getBlockNumber()
182-
proposalSnapshot = await governor.proposalSnapshot(proposalId)
183-
expect(votingDelay + BigInt(block)).equal(proposalSnapshot)
184-
})
185-
186184
it('should calculate the quorum correctly', async () => {
187185
await mine((await governor.votingDelay()) + 1n)
188186

@@ -254,7 +252,7 @@ describe('RootDAO Contact', () => {
254252
proposalSnapshot = await governor.proposalSnapshot(proposalId)
255253
const quorum = await governor.quorum(proposalSnapshot)
256254

257-
for (let holder of holders.slice(2, holders.length)) {
255+
for (const holder of holders.slice(2, holders.length)) {
258256
if ((await checkVotes()) <= quorum) {
259257
await governor.connect(holder).castVote(proposalId, 1)
260258
}
@@ -265,14 +263,27 @@ describe('RootDAO Contact', () => {
265263
expect(await getState()).to.be.equal(ProposalState.Succeeded)
266264
})
267265

268-
it('after a proposal succeded it should be queued for execution', async () => {
266+
it('Proposal should be registered as an operation on the Timelock', async () => {
267+
expect(await timelock.isOperation(toBeHex(proposalId)))
268+
})
269+
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)
273+
})
274+
275+
it('should return operation timestamp as 0 (because it is unset)', async () => {
276+
expect(await timelock.getTimestamp(toBeHex(proposalId))).to.equal(0)
277+
})
278+
279+
/* it('after a proposal succeeded it should be queued for execution', async () => {
269280
const tx = await governor
270281
.connect(deployer)
271282
[
272283
'execute(address[],uint256[],bytes[],bytes32)'
273284
](proposal[0], proposal[1], proposal[2], generateDescriptionHash(otherDesc))
274285
await tx.wait()
275-
})
286+
}) */
276287
})
277288
})
278289
})

types/governor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ export enum VoteType {
1818
}
1919

2020
export type Proposal = [string[], BigNumberish[], BytesLike[], string]
21+
22+
export enum OperationState {
23+
Unset,
24+
Waiting,
25+
Ready,
26+
Done,
27+
}

0 commit comments

Comments
 (0)