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

Commit a7826c6

Browse files
authored
Merge pull request #38 from code-423n4/feature/mainnetAirdropDeployment
Add final airdrop amount and merkle tree, modded deploy scripts
2 parents 1cc9798 + 2ae9b0f commit a7826c6

File tree

9 files changed

+2525
-127
lines changed

9 files changed

+2525
-127
lines changed

hardhat.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const config: HardhatUserConfig = {
2727
},
2828
},
2929
networks: {
30+
develop: {
31+
url: 'http://127.0.0.1:8545/'
32+
},
3033
rinkeby: {
3134
chainId: 4,
3235
url: process.env.RINKEBY_URL || '',
@@ -39,6 +42,7 @@ const config: HardhatUserConfig = {
3942
},
4043
},
4144
typechain: {
45+
outDir: 'typechain',
4246
target: 'ethers-v5',
4347
},
4448
gasReporter: {

scripts/airdrop/mainnetMerkle.json

Lines changed: 1184 additions & 0 deletions
Large diffs are not rendered by default.

scripts/config.ts renamed to scripts/deploy/config.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {BigNumber as BN, constants} from 'ethers';
2-
import {ONE_18, ONE_DAY, ONE_YEAR} from '../test/shared/Constants';
2+
import {ONE_18, ONE_DAY, ONE_YEAR} from '../../test/shared/Constants';
33

44
type Config = {
55
FREE_SUPPLY: BN;
@@ -9,13 +9,16 @@ type Config = {
99
VEST_DURATION: number;
1010
MERKLE_ROOT: string;
1111
TIMELOCK_DELAY: number;
12+
EXPORT_FILENAME: string;
13+
};
14+
15+
type TokenSaleConfig = {
1216
TOKEN_SALE_START: number;
1317
TOKEN_SALE_DURATION: number;
1418
TOKEN_SALE_USDC: string;
1519
TOKEN_SALE_ARENA_PRICE: BN;
1620
TOKEN_SALE_RECIPIENT: string;
1721
TOKEN_SALE_WHITELIST: typeof TOKEN_SALE_WHITELIST;
18-
EXPORT_FILENAME: string;
1922
};
2023

2124
const TOKEN_SALE_WHITELIST = [
@@ -51,29 +54,29 @@ export const allConfigs: {[key: number]: Config} = {
5154
VEST_DURATION: 4 * ONE_DAY,
5255
MERKLE_ROOT: '0xd97c9a423833d78e0562b8ed2d14752b54e7ef9b52314cafb197e3a339299901',
5356
TIMELOCK_DELAY: 1800, // 30 mins
54-
TOKEN_SALE_START: Math.floor(new Date(`2021-12-27T13:16:00.000Z`).getTime() / 1000),
55-
TOKEN_SALE_DURATION: 14 * ONE_DAY,
56-
TOKEN_SALE_USDC: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC 6 decimals
57-
TOKEN_SALE_ARENA_PRICE: BN.from(30_000).mul(ONE_18).div(ONE_18), // 0.03 USDC * 1e18 / 1.0 ARENA
58-
TOKEN_SALE_RECIPIENT: '0x670f9e8B37d5816c2eB93A1D94841C66652a8E26', // TODO: change this to real recipient
59-
TOKEN_SALE_WHITELIST,
6057
EXPORT_FILENAME: 'rinkebyAddresses.json',
6158
},
6259
// polygon mainnet
6360
137: {
64-
FREE_SUPPLY: BN.from(900).mul(1_000_000).mul(constants.WeiPerEther), // 900M
65-
AIRDROP_SUPPLY: BN.from(100).mul(1_000_000).mul(constants.WeiPerEther), // 100M
61+
FREE_SUPPLY: BN.from(640_826_767).mul(constants.WeiPerEther), // 1B - mainnet markle tokenTotal
62+
AIRDROP_SUPPLY: BN.from(359_173_233).mul(constants.WeiPerEther), // mainnet merkle tokenTotal
6663
CLAIMABLE_PROPORTION: 2000, // 20%
67-
CLAIM_END_DATE: '2022-12-25', // TODO: edit value
64+
CLAIM_END_DATE: '2023-1-11',
6865
VEST_DURATION: 4 * ONE_YEAR,
69-
MERKLE_ROOT: '0x0', // TODO: edit value
66+
MERKLE_ROOT: '0xb86e0dced055310e26ce11e69d47b6e6064be988564fb002d6ba5a29e7eee713',
7067
TIMELOCK_DELAY: 2 * ONE_DAY, // 2 days (same as ENS)
71-
TOKEN_SALE_START: Math.floor(new Date(`2021-12-27T13:50:00.000Z`).getTime() / 1000),
68+
EXPORT_FILENAME: 'polygonAddresses.json',
69+
},
70+
};
71+
72+
export const tokenSaleConfigs: {[key: number]: TokenSaleConfig} = {
73+
// polygon mainnet
74+
137: {
75+
TOKEN_SALE_START: Math.floor(new Date(`2022-01-12T00:00:00.000Z`).getTime() / 1000),
7276
TOKEN_SALE_DURATION: 14 * ONE_DAY,
7377
TOKEN_SALE_USDC: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
7478
TOKEN_SALE_ARENA_PRICE: BN.from(30_000).mul(ONE_18).div(ONE_18), // 0.03 USDC * 1e18 / 1.0 ARENA
75-
TOKEN_SALE_RECIPIENT: '0x670f9e8B37d5816c2eB93A1D94841C66652a8E26',
76-
TOKEN_SALE_WHITELIST,
77-
EXPORT_FILENAME: 'polygonAddresses.json',
79+
TOKEN_SALE_RECIPIENT: '0x670f9e8B37d5816c2eB93A1D94841C66652a8E26', // TODO: change to intended recipient
80+
TOKEN_SALE_WHITELIST, // TODO: update value
7881
},
7982
};

scripts/deploy.ts renamed to scripts/deploy/deployGov.ts

Lines changed: 8 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import {task} from 'hardhat/config';
2-
import {BigNumber as BN} from 'ethers';
31
import {expect} from 'chai';
42
import fs from 'fs';
53

@@ -12,25 +10,23 @@ import {
1210
TimelockController,
1311
ArenaGovernor__factory,
1412
ArenaGovernor,
15-
TokenSale__factory,
16-
TokenSale,
17-
} from '../typechain';
13+
} from '../../typechain';
1814

1915
import {allConfigs} from './config';
16+
import {HardhatRuntimeEnvironment} from 'hardhat/types';
2017

2118
let deployerAddress: string;
2219
let token: ArenaToken;
2320
let revokableTokenLock: RevokableTokenLock;
2421
let timelock: TimelockController;
2522
let governor: ArenaGovernor;
26-
let tokenSale: TokenSale;
2723

2824
// see OZ docs: https://docs.openzeppelin.com/contracts/4.x/api/governance#timelock-roles
2925
const ADMIN_ROLE = '0x5f58e3a2316349923ce3780f8d587db2d72378aed66a8261c916544fa6846ca5';
3026
const PROPOSER_ROLE = '0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1';
3127
const EXECUTOR_ROLE = '0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63';
3228

33-
task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
29+
export async function deployGov(hre: HardhatRuntimeEnvironment) {
3430
const networkId = hre.network.config.chainId as number;
3531
const [deployer] = await hre.ethers.getSigners();
3632
deployerAddress = await deployer.getAddress();
@@ -81,20 +77,8 @@ task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
8177
await governor.deployed();
8278
console.log(`governor address: ${governor.address}`);
8379

84-
console.log(`deploying tokensale...`);
85-
const TokenSaleFactory = (await hre.ethers.getContractFactory('TokenSale')) as TokenSale__factory;
86-
tokenSale = await TokenSaleFactory.deploy(
87-
config.TOKEN_SALE_USDC,
88-
token.address,
89-
config.TOKEN_SALE_START,
90-
config.TOKEN_SALE_DURATION,
91-
config.TOKEN_SALE_ARENA_PRICE,
92-
config.TOKEN_SALE_RECIPIENT,
93-
revokableTokenLock.address,
94-
config.VEST_DURATION
95-
);
96-
await tokenSale.deployed();
97-
console.log(`tokensale address: ${tokenSale.address}`);
80+
console.log(`transfer remaining tokens to timelock`);
81+
await token.transfer(timelock.address, config.FREE_SUPPLY);
9882

9983
// give governor proposer role
10084
// https://docs.openzeppelin.com/contracts/4.x/api/governance#timelock-proposer
@@ -111,26 +95,10 @@ task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
11195

11296
// set revoker role in TokenLock to timelock
11397
await revokableTokenLock.setRevoker(timelock.address);
114-
// set token sale in TokenLock
115-
await revokableTokenLock.setTokenSale(tokenSale.address);
11698

11799
// transfer tokenlock admin role to timelock
118100
await revokableTokenLock.transferOwnership(timelock.address);
119101

120-
// set up token sale whitelist
121-
await tokenSale.changeWhiteList(
122-
config.TOKEN_SALE_WHITELIST.map(({buyer}) => buyer),
123-
config.TOKEN_SALE_WHITELIST.map(({arenaAmount}) => arenaAmount)
124-
);
125-
// transfer token sale admin role to timelock
126-
await tokenSale.transferOwnership(timelock.address);
127-
128-
// transfer all tokens held by deployer to token sale and timelock
129-
const TOKEN_SALE_SUPPLY = config.TOKEN_SALE_WHITELIST.reduce((sum, el) => sum.add(el.arenaAmount), BN.from(`0`));
130-
console.log(`transferring ${TOKEN_SALE_SUPPLY.toString()} ARENA to TokenSale. Remaining back to Timelock`);
131-
await token.transfer(tokenSale.address, TOKEN_SALE_SUPPLY);
132-
await token.transfer(timelock.address, config.FREE_SUPPLY.sub(TOKEN_SALE_SUPPLY));
133-
134102
// transfer token admin role to timelock
135103
await token.transferOwnership(timelock.address);
136104

@@ -141,7 +109,6 @@ task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
141109
tokenLock: revokableTokenLock.address,
142110
timelock: timelock.address,
143111
governor: governor.address,
144-
tokenSale: tokenSale.address,
145112
};
146113
let exportJson = JSON.stringify(addressesToExport, null, 2);
147114
fs.writeFileSync(config.EXPORT_FILENAME, exportJson);
@@ -169,9 +136,6 @@ task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
169136
// TokenLock revoker should be timelock
170137
expect(await revokableTokenLock.revoker()).to.be.eq(timelock.address);
171138

172-
// TokenLock token sale should be set
173-
expect(await revokableTokenLock.tokenSale()).to.be.eq(tokenSale.address);
174-
175139
// TokenLock owner should be timelock
176140
expect(await revokableTokenLock.owner()).to.be.eq(timelock.address);
177141

@@ -181,11 +145,6 @@ task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
181145
// check Token's tokenlock has been set
182146
expect(await token.tokenLock()).to.be.eq(revokableTokenLock.address);
183147

184-
// check TokenSale's tokenlock has been set
185-
expect(await tokenSale.tokenLock()).to.be.eq(revokableTokenLock.address);
186-
// Token's owner should be timelock
187-
expect(await tokenSale.owner()).to.be.eq(timelock.address);
188-
189148
/////////////////////////
190149
// CONFIG VERIFICATION //
191150
/////////////////////////
@@ -194,11 +153,8 @@ task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
194153
// check ArenaToken's token balance == AIRDROP_SUPPLY
195154
expect(await token.balanceOf(token.address)).to.be.eq(config.AIRDROP_SUPPLY);
196155

197-
// check timelock's token balance == TOKEN_SALE_SUPPLY
198-
expect(await token.balanceOf(tokenSale.address)).to.be.eq(TOKEN_SALE_SUPPLY);
199-
200-
// check timelock's token balance == FREE_SUPPLY - TOKEN_SALE_SUPPLY (rest of it)
201-
expect(await token.balanceOf(timelock.address)).to.be.eq(config.FREE_SUPPLY.sub(TOKEN_SALE_SUPPLY));
156+
// check timelock's token balance == FREE_SUPPLY
157+
expect(await token.balanceOf(timelock.address)).to.be.eq(config.FREE_SUPPLY);
202158

203159
// check timelock's minDelay
204160
expect(await timelock.getMinDelay()).to.be.eq(config.TIMELOCK_DELAY);
@@ -214,4 +170,4 @@ task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
214170

215171
console.log('verification complete!');
216172
process.exit(0);
217-
});
173+
}

scripts/deploy/deployTokenSale.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {BigNumber as BN, Signer} from 'ethers';
2+
import {expect} from 'chai';
3+
import fs from 'fs';
4+
import path from 'path';
5+
6+
import {
7+
ArenaToken__factory,
8+
TimelockController__factory,
9+
ArenaGovernor__factory,
10+
TokenSale__factory,
11+
TokenSale,
12+
TokenLock__factory,
13+
} from '../../typechain';
14+
15+
import {allConfigs, tokenSaleConfigs} from './config';
16+
import {HardhatRuntimeEnvironment} from 'hardhat/types';
17+
18+
let proposerAddress: string;
19+
let tokenSale: TokenSale;
20+
21+
const getContracts = (signer: Signer, config: typeof allConfigs[0]) => {
22+
const deploymentFilePath = path.join(`deployments`, config.EXPORT_FILENAME);
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+
contents: contents,
47+
deploymentFilePath: deploymentFilePath,
48+
governor: ArenaGovernor__factory.connect(governorAddress, signer),
49+
arenaToken: ArenaToken__factory.connect(arenaAddress, signer),
50+
timelock: TimelockController__factory.connect(timelockAddress, signer),
51+
tokenLock: TokenLock__factory.connect(tokenLockAddress, signer),
52+
};
53+
};
54+
55+
export async function deployTokenSale(hre: HardhatRuntimeEnvironment) {
56+
const networkId = hre.network.config.chainId as number;
57+
const [proposer] = await hre.ethers.getSigners();
58+
proposerAddress = await proposer.getAddress();
59+
console.log(`Proposer: ${proposerAddress}`);
60+
61+
let config = tokenSaleConfigs[networkId];
62+
let deployConfig = allConfigs[networkId];
63+
if (!config) throw new Error(`No config exists for network ${hre.network.name} (${networkId})`);
64+
const {contents, deploymentFilePath, governor, arenaToken, timelock, tokenLock} = getContracts(
65+
proposer,
66+
deployConfig
67+
);
68+
69+
console.log(`deploying tokensale...`);
70+
const TokenSaleFactory = (await hre.ethers.getContractFactory('TokenSale')) as TokenSale__factory;
71+
72+
tokenSale = await TokenSaleFactory.deploy(
73+
config.TOKEN_SALE_USDC,
74+
arenaToken.address,
75+
config.TOKEN_SALE_START,
76+
config.TOKEN_SALE_DURATION,
77+
config.TOKEN_SALE_ARENA_PRICE,
78+
config.TOKEN_SALE_RECIPIENT,
79+
tokenLock.address,
80+
allConfigs[networkId].VEST_DURATION
81+
);
82+
await tokenSale.deployed();
83+
console.log(`tokenSale address: ${tokenSale.address}`);
84+
85+
// set up token sale whitelist
86+
await tokenSale.changeWhiteList(
87+
config.TOKEN_SALE_WHITELIST.map(({buyer}) => buyer),
88+
config.TOKEN_SALE_WHITELIST.map(({arenaAmount}) => arenaAmount)
89+
);
90+
const TOKEN_SALE_SUPPLY = config.TOKEN_SALE_WHITELIST.reduce((sum, el) => sum.add(el.arenaAmount), BN.from(`0`));
91+
// transfer token sale admin role to timelock
92+
await tokenSale.transferOwnership(timelock.address);
93+
94+
// 1st action: set token sale in TokenLock
95+
// 2nd action: request TOKEN_SALE_SUPPLY tokens from timelock to tokenSale
96+
let targets: string[] = [tokenLock.address, arenaToken.address];
97+
let values: string[] = ['0', '0'];
98+
let calldatas: string[] = [
99+
tokenLock.interface.encodeFunctionData('setTokenSale', [tokenSale.address]),
100+
arenaToken.interface.encodeFunctionData('transfer', [tokenSale.address, TOKEN_SALE_SUPPLY]),
101+
];
102+
103+
const tx = await governor['propose(address[],uint256[],bytes[],string)'](
104+
targets,
105+
values,
106+
calldatas,
107+
`Conduct Arena token sale!`
108+
);
109+
console.log(`proposal submitted: ${tx.hash}`);
110+
console.log(`waiting for block inclusion ...`);
111+
await tx.wait(1);
112+
113+
console.log('exporting addresses...');
114+
let addressesToExport = JSON.parse(contents);
115+
addressesToExport.tokenSale = tokenSale.address;
116+
let exportJson = JSON.stringify(addressesToExport, null, 2);
117+
fs.writeFileSync(deploymentFilePath, exportJson);
118+
119+
/////////////////////////////////
120+
// ACCESS CONTROL VERIFICATION //
121+
/////////////////////////////////
122+
console.log('verifying access control settings...');
123+
// check tokenSale's tokenlock has been set
124+
expect(await tokenSale.tokenLock()).to.be.eq(tokenLock.address);
125+
// tokenSale's owner should be timelock
126+
expect(await tokenSale.owner()).to.be.eq(timelock.address);
127+
128+
console.log('verification complete!');
129+
process.exit(0);
130+
}

scripts/deploy/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {task} from 'hardhat/config';
2+
3+
task('deployGov', 'deploy governance and token contracts').setAction(async (taskArgs, hre) => {
4+
// only load this file when task is run because it depends on typechain built artifacts
5+
// which will create a circular dependency when required by hardhat.config.ts for first compilation
6+
const {deployGov} = await import('./deployGov');
7+
await deployGov(hre);
8+
});
9+
10+
task('deployTokenSale', 'deploy token sale and make proposal for relevant actions').setAction(async (taskArgs, hre) => {
11+
// only load this file when task is run because it depends on typechain built artifacts
12+
// which will create a circular dependency when required by hardhat.config.ts for first compilation
13+
const {deployTokenSale} = await import('./deployTokenSale');
14+
await deployTokenSale(hre);
15+
});

scripts/proposals/transfer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fs from 'fs';
33
import path from 'path';
44
import _ from 'lodash';
55
import {ArenaGovernor__factory, ArenaToken__factory} from '../../typechain';
6-
import {allConfigs} from '../config';
6+
import {allConfigs} from '../deploy/config';
77
import {HardhatRuntimeEnvironment} from 'hardhat/types';
88

99
let transferInterface = new ethers.utils.Interface([`function transfer(address to, uint256 amount)`]);

0 commit comments

Comments
 (0)