Skip to content

Commit

Permalink
add governance proposal simulation tests on polygon mainnet fork
Browse files Browse the repository at this point in the history
  • Loading branch information
MrToph committed Jan 14, 2022
1 parent a7826c6 commit 01b2aed
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 0 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
ETHERSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1
RINKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<YOUR ALCHEMY KEY>
POLYGON_URL=https://polygon-rpc.com
PRIVATE_KEY=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1
7 changes: 7 additions & 0 deletions deployments/polygonAddresses.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"deployer": "0x3d67109E0200abD4D39Cb38377C9f81573f9F191",
"token": "0x6847D3A4c80a82e1fb26f1fC6F09F3Ad5BEB5222",
"tokenLock": "0xB17828789280C77C17B02fc8E6F20Ddc5721f2C2",
"timelock": "0xdFB26381aFBc37f0Fae4A77D385b91B90347aA12",
"governor": "0xc6eaDcC36aFcf1C430962506ad79145aD5140E58"
}
9 changes: 9 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ const config: HardhatUserConfig = {
},
},
networks: {
hardhat: {
// forking: {
// url: process.env.POLYGON_URL!,
// },
},
develop: {
url: 'http://127.0.0.1:8545/'
},
Expand All @@ -52,6 +57,10 @@ const config: HardhatUserConfig = {
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
mocha: {
// 1 hour, essentially disabled auto timeout
timeout: 60 * 60 * 1000,
},
};

export default config;
27 changes: 27 additions & 0 deletions test/GovernanceSim.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import chai, {expect} from 'chai';
import {waffle} from 'hardhat';
import {ZERO} from './shared/Constants';
import {getPolygonContracts} from './shared/Forking';
import {createAndExecuteProposal} from './shared/Governance';

const {solidity} = waffle;
chai.use(solidity);

// can simulate poylgon mainnet governance proposals here, enable fork object in hardhat.config.ts
describe.skip('Governance - Polygon mainnet proposal simulations', async () => {
const [user] = waffle.provider.getWallets();
const deployment = getPolygonContracts(user);
const {arenaToken, timeLock} = deployment;

it('should allow governance to move tokens in timeLock contract', async () => {
const treasuryAmount = await arenaToken.balanceOf(timeLock.address);
expect(treasuryAmount).to.be.gt(ZERO, `Treasury currently does not have any ARENA tokens`);

let targets: string[] = [arenaToken.address];
let values: string[] = [`0`];
let calldatas: string[] = [arenaToken.interface.encodeFunctionData('transfer', [user.address, treasuryAmount])];
await createAndExecuteProposal({targets, values, calldatas, user, ...deployment});

expect(await arenaToken.balanceOf(timeLock.address)).to.eq(ZERO);
});
});
51 changes: 51 additions & 0 deletions test/shared/Forking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {Signer} from 'ethers';
import * as fs from 'fs';
import * as path from 'path';
import {
ArenaGovernor,
ArenaGovernor__factory,
ArenaToken,
ArenaToken__factory,
TimelockController,
TimelockController__factory,
TokenLock,
TokenLock__factory,
} from '../../typechain';

export type DeployedContracts = {
governor: ArenaGovernor;
timeLock: TimelockController;
tokenLock: TokenLock;
arenaToken: ArenaToken;
};
export const getPolygonContracts = (signer: Signer): DeployedContracts => {
const deploymentFilePath = path.join(`deployments`, `polygonAddresses.json`);
if (!fs.existsSync(deploymentFilePath)) throw new Error(`File '${path.resolve(deploymentFilePath)}' does not exist.`);

const contents = fs.readFileSync(deploymentFilePath, `utf8`);
let governorAddress;
let arenaAddress;
let timelockAddress;
let tokenLockAddress;
try {
({
governor: governorAddress,
token: arenaAddress,
tokenLock: tokenLockAddress,
timelock: timelockAddress,
} = JSON.parse(contents));
} catch (error) {
throw new Error(`Cannot parse deployment config at '${path.resolve(deploymentFilePath)}'.`);
}
if (!governorAddress) throw new Error(`Deployment file did not include governor address '${deploymentFilePath}'.`);
if (!arenaAddress) throw new Error(`Deployment file did not include arena token address '${deploymentFilePath}'.`);
if (!timelockAddress) throw new Error(`Deployment file did not include timelock address '${deploymentFilePath}'.`);
if (!tokenLockAddress) throw new Error(`Deployment file did not include tokenLock address '${deploymentFilePath}'.`);

return {
governor: ArenaGovernor__factory.connect(governorAddress, signer),
arenaToken: ArenaToken__factory.connect(arenaAddress, signer),
timeLock: TimelockController__factory.connect(timelockAddress, signer),
tokenLock: TokenLock__factory.connect(tokenLockAddress, signer),
};
};
63 changes: 63 additions & 0 deletions test/shared/Governance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {Signer} from 'ethers';
import {ethers} from 'hardhat';
import {impersonateAccountWithFunds, stopImpersonateAccount} from '../shared/AccountManipulation';
import {increaseNextBlockTime, setNextBlockNumber} from '../shared/TimeManipulation';
import {DeployedContracts} from './Forking';

export const createAndExecuteProposal = async ({
governor,
timeLock,
arenaToken,
user,
targets,
values,
calldatas,
}: {
user: Signer;
targets: string[];
values: string[];
calldatas: string[];
} & DeployedContracts) => {
// we need address with min. proposalThreshold tokens to propose
// 1. borrow some treasury tokens to user
const quorumAmount = await governor.quorumVotes();
// careful, this sends ETH to timelock which might break real-world simulation for proposals involving Timelock ETH
const timeLockSigner = await impersonateAccountWithFunds(timeLock.address);
await arenaToken.connect(timeLockSigner).transfer(await user.getAddress(), quorumAmount);
// set voting delay & duration to 2 blocks, otherwise need to simulate 302,400 blocks
await governor.connect(timeLockSigner).setVotingDelay(`2`);
await governor.connect(timeLockSigner).setVotingPeriod(`2`);
await stopImpersonateAccount(timeLock.address);

await arenaToken.connect(user).delegate(await user.getAddress());
const descriptionHash = ethers.utils.keccak256([]); // keccak(``)
let tx = await governor.connect(user)['propose(address[],uint256[],bytes[],string)'](targets, values, calldatas, ``);
let {events} = await tx.wait();
// get first event (ProposalCreated), then get first arg of that event (proposalId)
const proposalId: string = events![0].args![0].toString();

// 2. vote on proposal
const voteStartBlock = await governor.proposalSnapshot(proposalId);
await setNextBlockNumber(voteStartBlock.toNumber() + 1); // is a blocknumber which fits in Number
tx = await governor.connect(user)['castVote'](proposalId, `1`);

// 3. return borrowed tokens
tx = await arenaToken.connect(user).transfer(timeLock.address, quorumAmount);

// 4. advance time past voting period and queue proposal calls to Timelock via GovernorTimelockControl.queue
const voteEndBlock = await governor.proposalDeadline(proposalId);
await setNextBlockNumber(voteEndBlock.toNumber() + 1); // is a blocknumber which fits in Number
tx = await governor
.connect(user)
['queue(address[],uint256[],bytes[],bytes32)'](targets, values, calldatas, descriptionHash);
await tx.wait();

// 5. advance time past timelock delay and then execute
const timeLockMinDelaySeconds = await timeLock.getMinDelay();
await increaseNextBlockTime(timeLockMinDelaySeconds.toNumber());
await governor
.connect(user)
['execute(address[],uint256[],bytes[],bytes32)'](targets, values, calldatas, descriptionHash);

return proposalId;
};
18 changes: 18 additions & 0 deletions test/shared/TimeManipulation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import hre from 'hardhat';
import {BigNumber as BN} from 'ethers';

export async function getHeadBlockNumber(): Promise<number> {
return BN.from(await hre.network.provider.send('eth_blockNumber', [])).toNumber();
}

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

export const increaseNextBlockTime = async (seconds: number) => {
return hre.network.provider.send('evm_increaseTime', [seconds]);
};

export const setNextBlockNumber = async (blockNumber: number) => {
let currentBlock = await getHeadBlockNumber();
for (; currentBlock < blockNumber; currentBlock++) {
// TODO: can this be made more efficient?
await hre.network.provider.send('evm_increaseTime', [2]); // polygon has a block time of 2.2 seconds
await hre.network.provider.send('evm_mine', []);
}
};

export const mineBlockAt = async (timestamp: number) => {
await hre.network.provider.send('evm_setNextBlockTimestamp', [timestamp]);
return hre.network.provider.send('evm_mine', []);
Expand Down

0 comments on commit 01b2aed

Please sign in to comment.