Skip to content
This repository was archived by the owner on Apr 4, 2025. It is now read-only.

Commit bfcbdf9

Browse files
authored
Merge pull request #44 from code-423n4/govUp
Deploy new governor contract, bump hardhat + etherscan verification
2 parents 7a0c6f7 + 31cb43d commit bfcbdf9

12 files changed

+406
-89
lines changed

contracts/ArenaGovernor.sol

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,28 @@ pragma solidity 0.8.10;
44
import "@openzeppelin/contracts/governance/Governor.sol";
55
import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
66
import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol";
7+
import "@openzeppelin/contracts/governance/extensions/GovernorPreventLateQuorum.sol";
78
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
89
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
910

1011
contract ArenaGovernor is
1112
Governor,
1213
GovernorSettings,
1314
GovernorCompatibilityBravo,
15+
GovernorPreventLateQuorum,
1416
GovernorVotes,
1517
GovernorTimelockControl
1618
{
17-
constructor(ERC20Votes _token, TimelockController _timelock)
19+
constructor(IVotes _token, TimelockController _timelock)
1820
Governor("ArenaGovernor")
1921
GovernorSettings(
2022
1, /* 1 block */
21-
302400, /* 1 week */
23+
216_000, /* 5 days */
2224
50_000e18 /* minimum proposal threshold of 50_000 tokens */
2325
)
26+
GovernorPreventLateQuorum(
27+
129_600 /* 3 days */
28+
)
2429
GovernorVotes(_token)
2530
GovernorTimelockControl(_timelock)
2631
{}
@@ -57,6 +62,15 @@ contract ArenaGovernor is
5762
return super.state(proposalId);
5863
}
5964

65+
function proposalDeadline(uint256 proposalId)
66+
public
67+
view
68+
override(Governor, IGovernor, GovernorPreventLateQuorum)
69+
returns (uint256)
70+
{
71+
return super.proposalDeadline(proposalId);
72+
}
73+
6074
function propose(
6175
address[] memory targets,
6276
uint256[] memory values,
@@ -94,6 +108,15 @@ contract ArenaGovernor is
94108
return super._cancel(targets, values, calldatas, descriptionHash);
95109
}
96110

111+
function _castVote(
112+
uint256 proposalId,
113+
address account,
114+
uint8 support,
115+
string memory reason
116+
) internal override(Governor, GovernorPreventLateQuorum) returns (uint256) {
117+
return super._castVote(proposalId, account, support, reason);
118+
}
119+
97120
function _executor()
98121
internal
99122
view

deployments/polygonAddresses.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"token": "0x6847D3A4c80a82e1fb26f1fC6F09F3Ad5BEB5222",
44
"tokenLock": "0xB17828789280C77C17B02fc8E6F20Ddc5721f2C2",
55
"timelock": "0xdFB26381aFBc37f0Fae4A77D385b91B90347aA12",
6-
"governor": "0xc6eaDcC36aFcf1C430962506ad79145aD5140E58",
6+
"governorV1": "0xc6eaDcC36aFcf1C430962506ad79145aD5140E58",
7+
"governor": "0x4Db7E521942BDA8b1fB1B310280135ba4B9c2bee",
78
"tokenSale": "0xD0e7d5a2220e32914540D97A6D0548658050180b"
89
}

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424
},
2525
"homepage": "https://github.com/code-423n4/genesis#readme",
2626
"dependencies": {
27-
"hardhat": "2.8.3",
27+
"hardhat": "2.9.0",
2828
"lodash": "^4.17.21"
2929
},
3030
"devDependencies": {
3131
"@nomiclabs/hardhat-ethers": "^2.0.4",
32-
"@nomiclabs/hardhat-etherscan": "3.0.0",
32+
"@nomiclabs/hardhat-etherscan": "3.0.3",
3333
"@nomiclabs/hardhat-waffle": "^2.0.0",
34-
"@openzeppelin/contracts": "^4.4.0",
34+
"@openzeppelin/contracts": "^4.5.0",
3535
"@typechain/ethers-v5": "^9.0.0",
3636
"@typechain/hardhat": "^4.0.0",
3737
"@types/chai": "^4.2.21",

shared/Constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ export const MAX_UINT = constants.MaxUint256;
99
export const ONE_HOUR = 60 * 60;
1010
export const ONE_DAY = 24 * ONE_HOUR;
1111
export const ONE_YEAR = 365 * ONE_DAY;
12-
export const POLYGON_AVERAGE_BLOCK_TIME = 2.2;
12+
export const POLYGON_AVERAGE_BLOCK_TIME = '0x2';

shared/Forking.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '../typechain';
1414

1515
export type DeployedContracts = {
16+
governorV1: ArenaGovernor,
1617
governor: ArenaGovernor;
1718
timeLock: TimelockController;
1819
tokenLock: TokenLock;
@@ -23,12 +24,14 @@ export const getPolygonContracts = (signer: Signer): DeployedContracts => {
2324
if (!fs.existsSync(deploymentFilePath)) throw new Error(`File '${path.resolve(deploymentFilePath)}' does not exist.`);
2425

2526
const contents = fs.readFileSync(deploymentFilePath, `utf8`);
27+
let governorV1Address;
2628
let governorAddress;
2729
let arenaAddress;
2830
let timelockAddress;
2931
let tokenLockAddress;
3032
try {
3133
({
34+
governorV1: governorV1Address,
3235
governor: governorAddress,
3336
token: arenaAddress,
3437
tokenLock: tokenLockAddress,
@@ -43,6 +46,7 @@ export const getPolygonContracts = (signer: Signer): DeployedContracts => {
4346
if (!tokenLockAddress) throw new Error(`Deployment file did not include tokenLock address '${deploymentFilePath}'.`);
4447

4548
return {
49+
governorV1: ArenaGovernor__factory.connect(governorV1Address, signer),
4650
governor: ArenaGovernor__factory.connect(governorAddress, signer),
4751
arenaToken: ArenaToken__factory.connect(arenaAddress, signer),
4852
timeLock: TimelockController__factory.connect(timelockAddress, signer),

shared/Governance.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {Signer} from 'ethers';
22
import {ethers} from 'hardhat';
33
import {impersonateAccountWithFunds, stopImpersonateAccount} from './AccountManipulation';
44
import {increaseNextBlockTime, setNextBlockNumber} from './TimeManipulation';
5-
import {POLYGON_AVERAGE_BLOCK_TIME} from './Constants';
65
import {DeployedContracts} from './Forking';
76
import {getABIFromPolygonscan} from './Polygonscan';
87

@@ -20,21 +19,14 @@ export const createAndExecuteProposal = async ({
2019
values: string[];
2120
calldatas: string[];
2221
} & DeployedContracts) => {
23-
// 0. set voting delay & duration to 2 blocks, otherwise need to simulate 302,400 blocks
2422
const timeLockSigner = await impersonateAccountWithFunds(timeLock.address);
25-
let originalVotingDelay = await governor.votingDelay();
26-
let originalVotingPeriod = await governor.votingPeriod();
27-
console.log('setting voting delay and duration to 2 blocks...');
28-
await governor.connect(timeLockSigner).setVotingDelay(`2`);
29-
await governor.connect(timeLockSigner).setVotingPeriod(`2`);
3023

3124
// 1. borrow some treasury tokens to user as we need signer with min. proposalThreshold tokens to propose
3225
const quorumAmount = await governor.quorumVotes();
3326
// careful, this sends ETH to timelock which might break real-world simulation for proposals involving Timelock ETH
3427
console.log('transferring tokens to user for proposal creation...');
3528
await arenaToken.connect(timeLockSigner).transfer(await user.getAddress(), quorumAmount);
3629
await arenaToken.connect(user).delegate(await user.getAddress());
37-
const descriptionHash = ethers.utils.keccak256([]); // keccak(``)
3830
console.log('creating proposal...');
3931
let tx = await governor.connect(user)['propose(address[],uint256[],bytes[],string)'](targets, values, calldatas, ``);
4032
let {events} = await tx.wait();
@@ -43,9 +35,8 @@ export const createAndExecuteProposal = async ({
4335

4436
// 2. advance time past voting delay and vote on proposal
4537
const voteStartBlock = await governor.proposalSnapshot(proposalId);
46-
// simulate elapsed time close to original voting delay
47-
await increaseNextBlockTime(Math.floor(POLYGON_AVERAGE_BLOCK_TIME * originalVotingDelay.toNumber()));
48-
await setNextBlockNumber(voteStartBlock.toNumber() + 1); // is a blocknumber which fits in Number
38+
// simulate to voteStartBlock
39+
await setNextBlockNumber(voteStartBlock.toNumber());
4940
console.log('casting vote...');
5041
tx = await governor.connect(user)['castVote'](proposalId, `1`);
5142

@@ -54,17 +45,12 @@ export const createAndExecuteProposal = async ({
5445

5546
// 4. advance time past voting period and queue proposal calls to Timelock via GovernorTimelockControl.queue
5647
const voteEndBlock = await governor.proposalDeadline(proposalId);
57-
// simulate elapsed time close to original voting delay
58-
await increaseNextBlockTime(Math.floor(POLYGON_AVERAGE_BLOCK_TIME * originalVotingPeriod.toNumber()));
59-
await setNextBlockNumber(voteEndBlock.toNumber() + 1); // is a blocknumber which fits in Number
48+
// simulate to voteEndBlock + 1
49+
await setNextBlockNumber(voteEndBlock.toNumber() + 1);
6050
console.log('queueing proposal...');
6151
tx = await governor.connect(user)['queue(uint256)'](proposalId);
6252
await tx.wait();
6353

64-
// can revert Governor changes now
65-
console.log('resetting voting delay and period...');
66-
await governor.connect(timeLockSigner).setVotingDelay(originalVotingDelay);
67-
await governor.connect(timeLockSigner).setVotingPeriod(originalVotingPeriod);
6854
await stopImpersonateAccount(timeLock.address);
6955

7056
// 5. advance time past timelock delay and then execute

shared/TimeManipulation.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@ export const increaseNextBlockTime = async (seconds: number) => {
1616

1717
export const setNextBlockNumber = async (blockNumber: number) => {
1818
let currentBlock = await getHeadBlockNumber();
19-
for (; currentBlock < blockNumber; currentBlock++) {
20-
await hre.network.provider.send('evm_increaseTime', [Math.round(POLYGON_AVERAGE_BLOCK_TIME)]);
21-
await hre.network.provider.send('evm_mine', []);
22-
}
19+
await hre.network.provider.send("hardhat_mine", ['0x' + (blockNumber - currentBlock).toString(16), POLYGON_AVERAGE_BLOCK_TIME]);
2320
};
2421

2522
export const mineBlockAt = async (timestamp: number) => {
2623
await hre.network.provider.send('evm_setNextBlockTimestamp', [timestamp]);
2724
return hre.network.provider.send('evm_mine', []);
2825
};
2926

27+
// default 2s per block
28+
export const mineBlocks = async (numBlocks: string, interval: string = POLYGON_AVERAGE_BLOCK_TIME) => {
29+
await hre.network.provider.send("hardhat_mine", [numBlocks, interval]);
30+
}
31+
3032
export const resetNetwork = async () => {
3133
return hre.network.provider.request({
3234
method: 'hardhat_reset',

test/GovernanceSim.spec.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ chai.use(solidity);
1010
// can simulate poylgon mainnet governance proposals here, enable fork object in hardhat.config.ts
1111
describe.skip('Governance - Polygon mainnet proposal simulations', async () => {
1212
const [user] = waffle.provider.getWallets();
13-
const deployment = getPolygonContracts(user);
14-
const {arenaToken, timeLock} = deployment;
13+
let deployment = getPolygonContracts(user);
14+
const {governorV1, governor, arenaToken, timeLock} = deployment;
1515

1616
it('should allow governance to move tokens in timeLock contract', async () => {
1717
const treasuryAmount = await arenaToken.balanceOf(timeLock.address);
@@ -24,4 +24,34 @@ describe.skip('Governance - Polygon mainnet proposal simulations', async () => {
2424

2525
expect(await arenaToken.balanceOf(timeLock.address)).to.eq(ZERO);
2626
});
27+
28+
it('should migrate to new governor', async () => {
29+
// set to current existing governor for proposal creation
30+
deployment.governor = governorV1;
31+
const PROPOSER_ROLE = '0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1';
32+
let targets: string[] = [timeLock.address, timeLock.address];
33+
let values: string[] = [`0`, `0`];
34+
let calldatas: string[] = [
35+
timeLock.interface.encodeFunctionData('grantRole', [PROPOSER_ROLE, governor.address]),
36+
timeLock.interface.encodeFunctionData('revokeRole', [PROPOSER_ROLE, governorV1.address]),
37+
];
38+
await createAndExecuteProposal({targets, values, calldatas, user, ...deployment});
39+
40+
const treasuryAmount = await arenaToken.balanceOf(timeLock.address);
41+
targets = [arenaToken.address];
42+
values = [`0`];
43+
calldatas = [arenaToken.interface.encodeFunctionData('transfer', [user.address, treasuryAmount])];
44+
45+
// attempt to move funds through old governor, will fail
46+
try {
47+
await createAndExecuteProposal({targets, values, calldatas, user, ...deployment});
48+
} catch (e) {
49+
console.log(e);
50+
}
51+
52+
// attempt to move funds through new governor, should be successful
53+
deployment.governor = governor;
54+
await createAndExecuteProposal({targets, values, calldatas, user, ...deployment});
55+
expect(await arenaToken.balanceOf(timeLock.address)).to.eq(ZERO);
56+
});
2757
});

test/RevokableTokenLock.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {expect} from 'chai';
22
import {BigNumber} from 'ethers';
33
import {ethers, waffle} from 'hardhat';
4-
import {IERC20, RevokableTokenLock} from '../typechain';
4+
import {TestERC20, RevokableTokenLock} from '../typechain';
55
import {ZERO_ADDRESS, ONE_HOUR} from '../shared/Constants';
66
import {setNextBlockTimeStamp, mineBlockAt} from '../shared/TimeManipulation';
77

test/TokenLock.spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {expect} from 'chai';
22
import chai from 'chai';
3-
import hre, {ethers, waffle} from 'hardhat';
4-
import {IERC20, TokenLock} from '../typechain';
3+
import {ethers, waffle} from 'hardhat';
4+
import {TestERC20, TokenLock} from '../typechain';
55
import {MAX_UINT, ONE, ONE_DAY, ONE_YEAR, ONE_18, ZERO, ZERO_ADDRESS} from '../shared/Constants';
66
import {mineBlockAt, resetNetwork, setNextBlockTimeStamp} from '../shared/TimeManipulation';
77

@@ -122,12 +122,13 @@ describe('TokenLock', async () => {
122122
});
123123

124124
it('should revert if lock() is called without sufficient balance or allowance', async () => {
125+
await expect(tokenLock.connect(admin).lock(user.address, ONE)).to.be.revertedWith(
126+
'ERC20: insufficient allowance'
127+
);
128+
await token.connect(user).approve(tokenLock.address, ONE);
125129
await expect(tokenLock.connect(user).lock(user.address, ONE)).to.be.revertedWith(
126130
'ERC20: transfer amount exceeds balance'
127131
);
128-
await expect(tokenLock.connect(admin).lock(user.address, ONE)).to.be.revertedWith(
129-
'ERC20: transfer amount exceeds allowance'
130-
);
131132
});
132133

133134
describe('successful lock', async () => {

test/TokenSale.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {expect} from 'chai';
22
import chai from 'chai';
33
import {ethers, waffle} from 'hardhat';
44
import {BigNumber as BN} from 'ethers';
5-
import {IERC20, RevokableTokenLock, TokenSale} from '../typechain';
5+
import {TestERC20, RevokableTokenLock, TokenSale} from '../typechain';
66
import {ONE_DAY, ONE_18, MAX_UINT, ONE_YEAR} from '../shared/Constants';
77
import {setNextBlockTimeStamp, resetNetwork} from '../shared/TimeManipulation';
88

0 commit comments

Comments
 (0)