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

Commit 17c2025

Browse files
authored
Merge pull request #41 from code-423n4/feature/test-mainnet-fork-gov
add governance proposal simulation tests on polygon mainnet fork
2 parents a7826c6 + 49b5848 commit 17c2025

File tree

8 files changed

+187
-0
lines changed

8 files changed

+187
-0
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
ETHERSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1
22
RINKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<YOUR ALCHEMY KEY>
3+
POLYGON_URL=https://polygon-rpc.com
34
PRIVATE_KEY=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1

deployments/polygonAddresses.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"deployer": "0x3d67109E0200abD4D39Cb38377C9f81573f9F191",
3+
"token": "0x6847D3A4c80a82e1fb26f1fC6F09F3Ad5BEB5222",
4+
"tokenLock": "0xB17828789280C77C17B02fc8E6F20Ddc5721f2C2",
5+
"timelock": "0xdFB26381aFBc37f0Fae4A77D385b91B90347aA12",
6+
"governor": "0xc6eaDcC36aFcf1C430962506ad79145aD5140E58"
7+
}

hardhat.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ const config: HardhatUserConfig = {
2727
},
2828
},
2929
networks: {
30+
hardhat: {
31+
// forking: {
32+
// url: process.env.POLYGON_URL!,
33+
// },
34+
},
3035
develop: {
3136
url: 'http://127.0.0.1:8545/'
3237
},
@@ -52,6 +57,10 @@ const config: HardhatUserConfig = {
5257
etherscan: {
5358
apiKey: process.env.ETHERSCAN_API_KEY,
5459
},
60+
mocha: {
61+
// 1 hour, essentially disabled auto timeout
62+
timeout: 60 * 60 * 1000,
63+
},
5564
};
5665

5766
export default config;

test/GovernanceSim.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import chai, {expect} from 'chai';
2+
import {waffle} from 'hardhat';
3+
import {ZERO} from './shared/Constants';
4+
import {getPolygonContracts} from './shared/Forking';
5+
import {createAndExecuteProposal} from './shared/Governance';
6+
7+
const {solidity} = waffle;
8+
chai.use(solidity);
9+
10+
// can simulate poylgon mainnet governance proposals here, enable fork object in hardhat.config.ts
11+
describe.skip('Governance - Polygon mainnet proposal simulations', async () => {
12+
const [user] = waffle.provider.getWallets();
13+
const deployment = getPolygonContracts(user);
14+
const {arenaToken, timeLock} = deployment;
15+
16+
it('should allow governance to move tokens in timeLock contract', async () => {
17+
const treasuryAmount = await arenaToken.balanceOf(timeLock.address);
18+
expect(treasuryAmount).to.be.gt(ZERO, `Treasury currently does not have any ARENA tokens`);
19+
20+
let targets: string[] = [arenaToken.address];
21+
let values: string[] = [`0`];
22+
let calldatas: string[] = [arenaToken.interface.encodeFunctionData('transfer', [user.address, treasuryAmount])];
23+
await createAndExecuteProposal({targets, values, calldatas, user, ...deployment});
24+
25+
expect(await arenaToken.balanceOf(timeLock.address)).to.eq(ZERO);
26+
});
27+
});

test/shared/Constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +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;

test/shared/Forking.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {Signer} from 'ethers';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
import {
5+
ArenaGovernor,
6+
ArenaGovernor__factory,
7+
ArenaToken,
8+
ArenaToken__factory,
9+
TimelockController,
10+
TimelockController__factory,
11+
TokenLock,
12+
TokenLock__factory,
13+
} from '../../typechain';
14+
15+
export type DeployedContracts = {
16+
governor: ArenaGovernor;
17+
timeLock: TimelockController;
18+
tokenLock: TokenLock;
19+
arenaToken: ArenaToken;
20+
};
21+
export const getPolygonContracts = (signer: Signer): DeployedContracts => {
22+
const deploymentFilePath = path.join(`deployments`, `polygonAddresses.json`);
23+
if (!fs.existsSync(deploymentFilePath)) throw new Error(`File '${path.resolve(deploymentFilePath)}' does not exist.`);
24+
25+
const contents = fs.readFileSync(deploymentFilePath, `utf8`);
26+
let governorAddress;
27+
let arenaAddress;
28+
let timelockAddress;
29+
let tokenLockAddress;
30+
try {
31+
({
32+
governor: governorAddress,
33+
token: arenaAddress,
34+
tokenLock: tokenLockAddress,
35+
timelock: timelockAddress,
36+
} = JSON.parse(contents));
37+
} catch (error) {
38+
throw new Error(`Cannot parse deployment config at '${path.resolve(deploymentFilePath)}'.`);
39+
}
40+
if (!governorAddress) throw new Error(`Deployment file did not include governor address '${deploymentFilePath}'.`);
41+
if (!arenaAddress) throw new Error(`Deployment file did not include arena token address '${deploymentFilePath}'.`);
42+
if (!timelockAddress) throw new Error(`Deployment file did not include timelock address '${deploymentFilePath}'.`);
43+
if (!tokenLockAddress) throw new Error(`Deployment file did not include tokenLock address '${deploymentFilePath}'.`);
44+
45+
return {
46+
governor: ArenaGovernor__factory.connect(governorAddress, signer),
47+
arenaToken: ArenaToken__factory.connect(arenaAddress, signer),
48+
timeLock: TimelockController__factory.connect(timelockAddress, signer),
49+
tokenLock: TokenLock__factory.connect(tokenLockAddress, signer),
50+
};
51+
};

test/shared/Governance.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {Signer} from 'ethers';
2+
import {ethers} from 'hardhat';
3+
import {impersonateAccountWithFunds, stopImpersonateAccount} from '../shared/AccountManipulation';
4+
import {increaseNextBlockTime, setNextBlockNumber} from '../shared/TimeManipulation';
5+
import {POLYGON_AVERAGE_BLOCK_TIME} from './Constants';
6+
import {DeployedContracts} from './Forking';
7+
8+
export const createAndExecuteProposal = async ({
9+
governor,
10+
timeLock,
11+
arenaToken,
12+
user,
13+
targets,
14+
values,
15+
calldatas,
16+
}: {
17+
user: Signer;
18+
targets: string[];
19+
values: string[];
20+
calldatas: string[];
21+
} & DeployedContracts) => {
22+
// 0. set voting delay & duration to 2 blocks, otherwise need to simulate 302,400 blocks
23+
const timeLockSigner = await impersonateAccountWithFunds(timeLock.address);
24+
let originalVotingDelay = await governor.votingDelay();
25+
let originalVotingPeriod = await governor.votingPeriod();
26+
await governor.connect(timeLockSigner).setVotingDelay(`2`);
27+
await governor.connect(timeLockSigner).setVotingPeriod(`2`);
28+
29+
// 1. borrow some treasury tokens to user as we need signer with min. proposalThreshold tokens to propose
30+
const quorumAmount = await governor.quorumVotes();
31+
// careful, this sends ETH to timelock which might break real-world simulation for proposals involving Timelock ETH
32+
await arenaToken.connect(timeLockSigner).transfer(await user.getAddress(), quorumAmount);
33+
await arenaToken.connect(user).delegate(await user.getAddress());
34+
const descriptionHash = ethers.utils.keccak256([]); // keccak(``)
35+
let tx = await governor.connect(user)['propose(address[],uint256[],bytes[],string)'](targets, values, calldatas, ``);
36+
let {events} = await tx.wait();
37+
// get first event (ProposalCreated), then get first arg of that event (proposalId)
38+
const proposalId: string = events![0].args![0].toString();
39+
40+
// 2. advance time past voting delay and vote on proposal
41+
const voteStartBlock = await governor.proposalSnapshot(proposalId);
42+
// simulate elapsed time close to original voting delay
43+
await increaseNextBlockTime(Math.floor(POLYGON_AVERAGE_BLOCK_TIME * originalVotingDelay.toNumber()));
44+
await setNextBlockNumber(voteStartBlock.toNumber() + 1); // is a blocknumber which fits in Number
45+
tx = await governor.connect(user)['castVote'](proposalId, `1`);
46+
47+
// 3. return borrowed tokens
48+
tx = await arenaToken.connect(user).transfer(timeLock.address, quorumAmount);
49+
50+
// 4. advance time past voting period and queue proposal calls to Timelock via GovernorTimelockControl.queue
51+
const voteEndBlock = await governor.proposalDeadline(proposalId);
52+
// simulate elapsed time close to original voting delay
53+
await increaseNextBlockTime(Math.floor(POLYGON_AVERAGE_BLOCK_TIME * originalVotingPeriod.toNumber()));
54+
await setNextBlockNumber(voteEndBlock.toNumber() + 1); // is a blocknumber which fits in Number
55+
tx = await governor
56+
.connect(user)
57+
['queue(address[],uint256[],bytes[],bytes32)'](targets, values, calldatas, descriptionHash);
58+
await tx.wait();
59+
60+
// can revert Governor changes now
61+
await governor.connect(timeLockSigner).setVotingDelay(originalVotingDelay);
62+
await governor.connect(timeLockSigner).setVotingPeriod(originalVotingPeriod);
63+
await stopImpersonateAccount(timeLock.address);
64+
65+
// 5. advance time past timelock delay and then execute
66+
const timeLockMinDelaySeconds = await timeLock.getMinDelay();
67+
await increaseNextBlockTime(timeLockMinDelaySeconds.toNumber());
68+
await governor
69+
.connect(user)
70+
['execute(address[],uint256[],bytes[],bytes32)'](targets, values, calldatas, descriptionHash);
71+
72+
return proposalId;
73+
};

test/shared/TimeManipulation.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
11
import hre from 'hardhat';
2+
import {BigNumber as BN} from 'ethers';
3+
import {POLYGON_AVERAGE_BLOCK_TIME} from './Constants';
4+
5+
export async function getHeadBlockNumber(): Promise<number> {
6+
return BN.from(await hre.network.provider.send('eth_blockNumber', [])).toNumber();
7+
}
28

39
export const setNextBlockTimeStamp = async (timestamp: number) => {
410
return hre.network.provider.send('evm_setNextBlockTimestamp', [timestamp]);
511
};
612

13+
export const increaseNextBlockTime = async (seconds: number) => {
14+
return hre.network.provider.send('evm_increaseTime', [seconds]);
15+
};
16+
17+
export const setNextBlockNumber = async (blockNumber: number) => {
18+
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+
}
23+
};
24+
725
export const mineBlockAt = async (timestamp: number) => {
826
await hre.network.provider.send('evm_setNextBlockTimestamp', [timestamp]);
927
return hre.network.provider.send('evm_mine', []);

0 commit comments

Comments
 (0)