Skip to content

Commit

Permalink
Merge pull request #38 from code-423n4/feature/mainnetAirdropDeployment
Browse files Browse the repository at this point in the history
Add final airdrop amount and merkle tree, modded deploy scripts
  • Loading branch information
HickupHH3 authored Jan 12, 2022
2 parents 1cc9798 + 2ae9b0f commit a7826c6
Show file tree
Hide file tree
Showing 9 changed files with 2,525 additions and 127 deletions.
4 changes: 4 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const config: HardhatUserConfig = {
},
},
networks: {
develop: {
url: 'http://127.0.0.1:8545/'
},
rinkeby: {
chainId: 4,
url: process.env.RINKEBY_URL || '',
Expand All @@ -39,6 +42,7 @@ const config: HardhatUserConfig = {
},
},
typechain: {
outDir: 'typechain',
target: 'ethers-v5',
},
gasReporter: {
Expand Down
1,184 changes: 1,184 additions & 0 deletions scripts/airdrop/mainnetMerkle.json

Large diffs are not rendered by default.

35 changes: 19 additions & 16 deletions scripts/config.ts → scripts/deploy/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {BigNumber as BN, constants} from 'ethers';
import {ONE_18, ONE_DAY, ONE_YEAR} from '../test/shared/Constants';
import {ONE_18, ONE_DAY, ONE_YEAR} from '../../test/shared/Constants';

type Config = {
FREE_SUPPLY: BN;
Expand All @@ -9,13 +9,16 @@ type Config = {
VEST_DURATION: number;
MERKLE_ROOT: string;
TIMELOCK_DELAY: number;
EXPORT_FILENAME: string;
};

type TokenSaleConfig = {
TOKEN_SALE_START: number;
TOKEN_SALE_DURATION: number;
TOKEN_SALE_USDC: string;
TOKEN_SALE_ARENA_PRICE: BN;
TOKEN_SALE_RECIPIENT: string;
TOKEN_SALE_WHITELIST: typeof TOKEN_SALE_WHITELIST;
EXPORT_FILENAME: string;
};

const TOKEN_SALE_WHITELIST = [
Expand Down Expand Up @@ -51,29 +54,29 @@ export const allConfigs: {[key: number]: Config} = {
VEST_DURATION: 4 * ONE_DAY,
MERKLE_ROOT: '0xd97c9a423833d78e0562b8ed2d14752b54e7ef9b52314cafb197e3a339299901',
TIMELOCK_DELAY: 1800, // 30 mins
TOKEN_SALE_START: Math.floor(new Date(`2021-12-27T13:16:00.000Z`).getTime() / 1000),
TOKEN_SALE_DURATION: 14 * ONE_DAY,
TOKEN_SALE_USDC: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC 6 decimals
TOKEN_SALE_ARENA_PRICE: BN.from(30_000).mul(ONE_18).div(ONE_18), // 0.03 USDC * 1e18 / 1.0 ARENA
TOKEN_SALE_RECIPIENT: '0x670f9e8B37d5816c2eB93A1D94841C66652a8E26', // TODO: change this to real recipient
TOKEN_SALE_WHITELIST,
EXPORT_FILENAME: 'rinkebyAddresses.json',
},
// polygon mainnet
137: {
FREE_SUPPLY: BN.from(900).mul(1_000_000).mul(constants.WeiPerEther), // 900M
AIRDROP_SUPPLY: BN.from(100).mul(1_000_000).mul(constants.WeiPerEther), // 100M
FREE_SUPPLY: BN.from(640_826_767).mul(constants.WeiPerEther), // 1B - mainnet markle tokenTotal
AIRDROP_SUPPLY: BN.from(359_173_233).mul(constants.WeiPerEther), // mainnet merkle tokenTotal
CLAIMABLE_PROPORTION: 2000, // 20%
CLAIM_END_DATE: '2022-12-25', // TODO: edit value
CLAIM_END_DATE: '2023-1-11',
VEST_DURATION: 4 * ONE_YEAR,
MERKLE_ROOT: '0x0', // TODO: edit value
MERKLE_ROOT: '0xb86e0dced055310e26ce11e69d47b6e6064be988564fb002d6ba5a29e7eee713',
TIMELOCK_DELAY: 2 * ONE_DAY, // 2 days (same as ENS)
TOKEN_SALE_START: Math.floor(new Date(`2021-12-27T13:50:00.000Z`).getTime() / 1000),
EXPORT_FILENAME: 'polygonAddresses.json',
},
};

export const tokenSaleConfigs: {[key: number]: TokenSaleConfig} = {
// polygon mainnet
137: {
TOKEN_SALE_START: Math.floor(new Date(`2022-01-12T00:00:00.000Z`).getTime() / 1000),
TOKEN_SALE_DURATION: 14 * ONE_DAY,
TOKEN_SALE_USDC: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
TOKEN_SALE_ARENA_PRICE: BN.from(30_000).mul(ONE_18).div(ONE_18), // 0.03 USDC * 1e18 / 1.0 ARENA
TOKEN_SALE_RECIPIENT: '0x670f9e8B37d5816c2eB93A1D94841C66652a8E26',
TOKEN_SALE_WHITELIST,
EXPORT_FILENAME: 'polygonAddresses.json',
TOKEN_SALE_RECIPIENT: '0x670f9e8B37d5816c2eB93A1D94841C66652a8E26', // TODO: change to intended recipient
TOKEN_SALE_WHITELIST, // TODO: update value
},
};
60 changes: 8 additions & 52 deletions scripts/deploy.ts → scripts/deploy/deployGov.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import {task} from 'hardhat/config';
import {BigNumber as BN} from 'ethers';
import {expect} from 'chai';
import fs from 'fs';

Expand All @@ -12,25 +10,23 @@ import {
TimelockController,
ArenaGovernor__factory,
ArenaGovernor,
TokenSale__factory,
TokenSale,
} from '../typechain';
} from '../../typechain';

import {allConfigs} from './config';
import {HardhatRuntimeEnvironment} from 'hardhat/types';

let deployerAddress: string;
let token: ArenaToken;
let revokableTokenLock: RevokableTokenLock;
let timelock: TimelockController;
let governor: ArenaGovernor;
let tokenSale: TokenSale;

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

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

console.log(`deploying tokensale...`);
const TokenSaleFactory = (await hre.ethers.getContractFactory('TokenSale')) as TokenSale__factory;
tokenSale = await TokenSaleFactory.deploy(
config.TOKEN_SALE_USDC,
token.address,
config.TOKEN_SALE_START,
config.TOKEN_SALE_DURATION,
config.TOKEN_SALE_ARENA_PRICE,
config.TOKEN_SALE_RECIPIENT,
revokableTokenLock.address,
config.VEST_DURATION
);
await tokenSale.deployed();
console.log(`tokensale address: ${tokenSale.address}`);
console.log(`transfer remaining tokens to timelock`);
await token.transfer(timelock.address, config.FREE_SUPPLY);

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

// set revoker role in TokenLock to timelock
await revokableTokenLock.setRevoker(timelock.address);
// set token sale in TokenLock
await revokableTokenLock.setTokenSale(tokenSale.address);

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

// set up token sale whitelist
await tokenSale.changeWhiteList(
config.TOKEN_SALE_WHITELIST.map(({buyer}) => buyer),
config.TOKEN_SALE_WHITELIST.map(({arenaAmount}) => arenaAmount)
);
// transfer token sale admin role to timelock
await tokenSale.transferOwnership(timelock.address);

// transfer all tokens held by deployer to token sale and timelock
const TOKEN_SALE_SUPPLY = config.TOKEN_SALE_WHITELIST.reduce((sum, el) => sum.add(el.arenaAmount), BN.from(`0`));
console.log(`transferring ${TOKEN_SALE_SUPPLY.toString()} ARENA to TokenSale. Remaining back to Timelock`);
await token.transfer(tokenSale.address, TOKEN_SALE_SUPPLY);
await token.transfer(timelock.address, config.FREE_SUPPLY.sub(TOKEN_SALE_SUPPLY));

// transfer token admin role to timelock
await token.transferOwnership(timelock.address);

Expand All @@ -141,7 +109,6 @@ task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
tokenLock: revokableTokenLock.address,
timelock: timelock.address,
governor: governor.address,
tokenSale: tokenSale.address,
};
let exportJson = JSON.stringify(addressesToExport, null, 2);
fs.writeFileSync(config.EXPORT_FILENAME, exportJson);
Expand Down Expand Up @@ -169,9 +136,6 @@ task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
// TokenLock revoker should be timelock
expect(await revokableTokenLock.revoker()).to.be.eq(timelock.address);

// TokenLock token sale should be set
expect(await revokableTokenLock.tokenSale()).to.be.eq(tokenSale.address);

// TokenLock owner should be timelock
expect(await revokableTokenLock.owner()).to.be.eq(timelock.address);

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

// check TokenSale's tokenlock has been set
expect(await tokenSale.tokenLock()).to.be.eq(revokableTokenLock.address);
// Token's owner should be timelock
expect(await tokenSale.owner()).to.be.eq(timelock.address);

/////////////////////////
// CONFIG VERIFICATION //
/////////////////////////
Expand All @@ -194,11 +153,8 @@ task('deploy', 'deploy contracts').setAction(async (taskArgs, hre) => {
// check ArenaToken's token balance == AIRDROP_SUPPLY
expect(await token.balanceOf(token.address)).to.be.eq(config.AIRDROP_SUPPLY);

// check timelock's token balance == TOKEN_SALE_SUPPLY
expect(await token.balanceOf(tokenSale.address)).to.be.eq(TOKEN_SALE_SUPPLY);

// check timelock's token balance == FREE_SUPPLY - TOKEN_SALE_SUPPLY (rest of it)
expect(await token.balanceOf(timelock.address)).to.be.eq(config.FREE_SUPPLY.sub(TOKEN_SALE_SUPPLY));
// check timelock's token balance == FREE_SUPPLY
expect(await token.balanceOf(timelock.address)).to.be.eq(config.FREE_SUPPLY);

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

console.log('verification complete!');
process.exit(0);
});
}
130 changes: 130 additions & 0 deletions scripts/deploy/deployTokenSale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {BigNumber as BN, Signer} from 'ethers';
import {expect} from 'chai';
import fs from 'fs';
import path from 'path';

import {
ArenaToken__factory,
TimelockController__factory,
ArenaGovernor__factory,
TokenSale__factory,
TokenSale,
TokenLock__factory,
} from '../../typechain';

import {allConfigs, tokenSaleConfigs} from './config';
import {HardhatRuntimeEnvironment} from 'hardhat/types';

let proposerAddress: string;
let tokenSale: TokenSale;

const getContracts = (signer: Signer, config: typeof allConfigs[0]) => {
const deploymentFilePath = path.join(`deployments`, config.EXPORT_FILENAME);
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 {
contents: contents,
deploymentFilePath: deploymentFilePath,
governor: ArenaGovernor__factory.connect(governorAddress, signer),
arenaToken: ArenaToken__factory.connect(arenaAddress, signer),
timelock: TimelockController__factory.connect(timelockAddress, signer),
tokenLock: TokenLock__factory.connect(tokenLockAddress, signer),
};
};

export async function deployTokenSale(hre: HardhatRuntimeEnvironment) {
const networkId = hre.network.config.chainId as number;
const [proposer] = await hre.ethers.getSigners();
proposerAddress = await proposer.getAddress();
console.log(`Proposer: ${proposerAddress}`);

let config = tokenSaleConfigs[networkId];
let deployConfig = allConfigs[networkId];
if (!config) throw new Error(`No config exists for network ${hre.network.name} (${networkId})`);
const {contents, deploymentFilePath, governor, arenaToken, timelock, tokenLock} = getContracts(
proposer,
deployConfig
);

console.log(`deploying tokensale...`);
const TokenSaleFactory = (await hre.ethers.getContractFactory('TokenSale')) as TokenSale__factory;

tokenSale = await TokenSaleFactory.deploy(
config.TOKEN_SALE_USDC,
arenaToken.address,
config.TOKEN_SALE_START,
config.TOKEN_SALE_DURATION,
config.TOKEN_SALE_ARENA_PRICE,
config.TOKEN_SALE_RECIPIENT,
tokenLock.address,
allConfigs[networkId].VEST_DURATION
);
await tokenSale.deployed();
console.log(`tokenSale address: ${tokenSale.address}`);

// set up token sale whitelist
await tokenSale.changeWhiteList(
config.TOKEN_SALE_WHITELIST.map(({buyer}) => buyer),
config.TOKEN_SALE_WHITELIST.map(({arenaAmount}) => arenaAmount)
);
const TOKEN_SALE_SUPPLY = config.TOKEN_SALE_WHITELIST.reduce((sum, el) => sum.add(el.arenaAmount), BN.from(`0`));
// transfer token sale admin role to timelock
await tokenSale.transferOwnership(timelock.address);

// 1st action: set token sale in TokenLock
// 2nd action: request TOKEN_SALE_SUPPLY tokens from timelock to tokenSale
let targets: string[] = [tokenLock.address, arenaToken.address];
let values: string[] = ['0', '0'];
let calldatas: string[] = [
tokenLock.interface.encodeFunctionData('setTokenSale', [tokenSale.address]),
arenaToken.interface.encodeFunctionData('transfer', [tokenSale.address, TOKEN_SALE_SUPPLY]),
];

const tx = await governor['propose(address[],uint256[],bytes[],string)'](
targets,
values,
calldatas,
`Conduct Arena token sale!`
);
console.log(`proposal submitted: ${tx.hash}`);
console.log(`waiting for block inclusion ...`);
await tx.wait(1);

console.log('exporting addresses...');
let addressesToExport = JSON.parse(contents);
addressesToExport.tokenSale = tokenSale.address;
let exportJson = JSON.stringify(addressesToExport, null, 2);
fs.writeFileSync(deploymentFilePath, exportJson);

/////////////////////////////////
// ACCESS CONTROL VERIFICATION //
/////////////////////////////////
console.log('verifying access control settings...');
// check tokenSale's tokenlock has been set
expect(await tokenSale.tokenLock()).to.be.eq(tokenLock.address);
// tokenSale's owner should be timelock
expect(await tokenSale.owner()).to.be.eq(timelock.address);

console.log('verification complete!');
process.exit(0);
}
15 changes: 15 additions & 0 deletions scripts/deploy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {task} from 'hardhat/config';

task('deployGov', 'deploy governance and token contracts').setAction(async (taskArgs, hre) => {
// only load this file when task is run because it depends on typechain built artifacts
// which will create a circular dependency when required by hardhat.config.ts for first compilation
const {deployGov} = await import('./deployGov');
await deployGov(hre);
});

task('deployTokenSale', 'deploy token sale and make proposal for relevant actions').setAction(async (taskArgs, hre) => {
// only load this file when task is run because it depends on typechain built artifacts
// which will create a circular dependency when required by hardhat.config.ts for first compilation
const {deployTokenSale} = await import('./deployTokenSale');
await deployTokenSale(hre);
});
2 changes: 1 addition & 1 deletion scripts/proposals/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fs from 'fs';
import path from 'path';
import _ from 'lodash';
import {ArenaGovernor__factory, ArenaToken__factory} from '../../typechain';
import {allConfigs} from '../config';
import {allConfigs} from '../deploy/config';
import {HardhatRuntimeEnvironment} from 'hardhat/types';

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

0 comments on commit a7826c6

Please sign in to comment.