Skip to content

Commit

Permalink
Merge pull request #43 from code-423n4/saleMod
Browse files Browse the repository at this point in the history
Allow for new token sale, check for buyer vest schedule, cap sale recipient amount
  • Loading branch information
HickupHH3 authored Mar 4, 2022
2 parents bae376b + 3615c7c commit 7a0c6f7
Show file tree
Hide file tree
Showing 12 changed files with 472 additions and 141 deletions.
84 changes: 67 additions & 17 deletions contracts/ArenaTokenSale.sol → contracts/TokenSale.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity 0.8.10;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import "../interfaces/IRevokableTokenLock.sol";
import "../interfaces/ITokenLockVestReader.sol";

/**
* @dev Sells a token at a predetermined price to whitelisted buyers. The number of tokens each address can buy can be regulated.
Expand All @@ -15,22 +15,25 @@ contract TokenSale is Ownable {
/// token to give out (ARENA)
ERC20 public immutable tokenOut;
/// time when tokens can be first purchased
uint64 public immutable saleStart;
uint64 public saleStart;
/// duration of the token sale, cannot purchase afterwards
uint64 public immutable saleDuration;
/// address receiving the proceeds of the sale
address internal saleRecipient;
/// address receiving a defined portion proceeds of the sale
address internal immutable saleRecipient;
/// amount receivable by sale recipient
uint256 public remainingSaleRecipientAmount;
/// vesting contract
IRevokableTokenLock public tokenLock;
ITokenLockVestReader public immutable tokenLock;
/// vesting duration
uint256 public vestDuration;
uint256 public immutable vestDuration;

/// how many `tokenOut`s each address may buy
mapping(address => uint256) public whitelistedBuyersAmount;
/// tokenIn per tokenOut price. precision is in tokenInDecimals - tokenOutDecimals + 18
/// i.e., it should be provided as tokenInAmount * 1e18 / tokenOutAmount
uint256 public immutable tokenOutPrice;

event BuyerWhitelisted(address indexed buyer, uint256 amount);
event Sale(address indexed buyer, uint256 amountIn, uint256 amountOut);

/**
Expand All @@ -40,9 +43,10 @@ contract TokenSale is Ownable {
* @param _saleStart The time when tokens can be first purchased
* @param _saleDuration The duration of the token sale
* @param _tokenOutPrice The tokenIn per tokenOut price. precision should be in tokenInDecimals - tokenOutDecimals + 18
* @param _saleRecipient The address receiving the proceeds of the sale
* @param _saleRecipient The address receiving a portion proceeds of the sale
* @param _tokenLock The contract in which _tokenOut will be vested in
* @param _vestDuration Token vesting duration
* @param _saleRecipientAmount Amount receivable by sale recipient
*/
constructor(
ERC20 _tokenIn,
Expand All @@ -52,7 +56,8 @@ contract TokenSale is Ownable {
uint256 _tokenOutPrice,
address _saleRecipient,
address _tokenLock,
uint256 _vestDuration
uint256 _vestDuration,
uint256 _saleRecipientAmount
) {
require(block.timestamp <= _saleStart, "TokenSale: start date may not be in the past");
require(_saleDuration > 0, "TokenSale: the sale duration must not be zero");
Expand All @@ -70,9 +75,9 @@ contract TokenSale is Ownable {
saleDuration = _saleDuration;
tokenOutPrice = _tokenOutPrice;
saleRecipient = _saleRecipient;

tokenLock = IRevokableTokenLock(_tokenLock);
tokenLock = ITokenLockVestReader(_tokenLock);
vestDuration = _vestDuration;
remainingSaleRecipientAmount = _saleRecipientAmount;
}

/**
Expand All @@ -86,10 +91,31 @@ contract TokenSale is Ownable {
require(_tokenOutAmount > 0, "TokenSale: non-whitelisted purchaser or have already bought");
whitelistedBuyersAmount[msg.sender] = 0;
tokenInAmount_ = (_tokenOutAmount * tokenOutPrice) / 1e18;
require(
tokenIn.transferFrom(msg.sender, saleRecipient, tokenInAmount_),
"TokenSale: tokenIn transfer failed"
);

// saleRecipient will receive proceeds first, until fully allocated
if (tokenInAmount_ <= remainingSaleRecipientAmount) {
remainingSaleRecipientAmount -= tokenInAmount_;
require(
tokenIn.transferFrom(msg.sender, saleRecipient, tokenInAmount_),
"TokenSale: tokenIn transfer failed"
);
} else {
// saleRecipient will either be receiving or have received full allocation
// portion will go to owner
uint256 ownerAmount = tokenInAmount_ - remainingSaleRecipientAmount;
require(
tokenIn.transferFrom(msg.sender, owner(), ownerAmount),
"TokenSale: tokenIn transfer failed"
);
if (remainingSaleRecipientAmount > 0) {
uint256 saleRecipientAmount = remainingSaleRecipientAmount;
remainingSaleRecipientAmount = 0;
require(
tokenIn.transferFrom(msg.sender, saleRecipient, saleRecipientAmount),
"TokenSale: tokenIn transfer failed"
);
}
}

uint256 claimableAmount = (_tokenOutAmount * 2_000) / 10_000;
uint256 remainingAmount;
Expand All @@ -103,7 +129,7 @@ contract TokenSale is Ownable {
"TokenSale: tokenOut transfer failed"
);

// if we use same tokenLock instance as airdrop, we make sure that
// we use same tokenLock instance as airdrop, we make sure that
// the claimers and buyers are distinct to not reinitialize vesting
tokenLock.setupVesting(
msg.sender,
Expand Down Expand Up @@ -131,18 +157,42 @@ contract TokenSale is Ownable {
_buyers.length == _newTokenOutAmounts.length,
"TokenSale: parameter length mismatch"
);
require(block.timestamp < saleStart, "TokenSale: sale already started");
require(
block.timestamp < saleStart || block.timestamp > saleStart + saleDuration,
"TokenSale: ongoing sale"
);

for (uint256 i = 0; i < _buyers.length; i++) {
// Does not cover the case that the buyer has not claimed his airdrop
// So it will have to be somewhat manually checked
ITokenLockVestReader.VestingParams memory vestParams = tokenLock.vesting(_buyers[i]);
require(vestParams.unlockBegin == 0, "TokenSale: buyer has existing vest schedule");
whitelistedBuyersAmount[_buyers[i]] = _newTokenOutAmounts[i];
emit BuyerWhitelisted(_buyers[i], _newTokenOutAmounts[i]);
}
}

/**
* @dev Modifies the start time of the sale. Enables a new sale to be created assuming one is not ongoing
* @dev A new list of buyers and tokenAmounts can be done by calling changeWhiteList()
* @param _newSaleStart The new start time of the token sale
*/
function setNewSaleStart(uint64 _newSaleStart) external {
require(msg.sender == owner() || msg.sender == saleRecipient, "TokenSale: not authorized");
// can only change if there is no ongoing sale
require(
block.timestamp < saleStart || block.timestamp > saleStart + saleDuration,
"TokenSale: ongoing sale"
);
require(block.timestamp < _newSaleStart, "TokenSale: new sale too early");
saleStart = _newSaleStart;
}

/**
* @dev Transfers out any remaining `tokenOut` after the sale to owner
*/
function sweepTokenOut() external {
require(saleStart + saleDuration < block.timestamp, "TokenSale: sale did not end yet");
require(saleStart + saleDuration < block.timestamp, "TokenSale: ongoing sale");

uint256 tokenOutBalance = tokenOut.balanceOf(address(this));
require(tokenOut.transfer(owner(), tokenOutBalance), "TokenSale: transfer failed");
Expand Down
3 changes: 2 additions & 1 deletion deployments/polygonAddresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"token": "0x6847D3A4c80a82e1fb26f1fC6F09F3Ad5BEB5222",
"tokenLock": "0xB17828789280C77C17B02fc8E6F20Ddc5721f2C2",
"timelock": "0xdFB26381aFBc37f0Fae4A77D385b91B90347aA12",
"governor": "0xc6eaDcC36aFcf1C430962506ad79145aD5140E58"
"governor": "0xc6eaDcC36aFcf1C430962506ad79145aD5140E58",
"tokenSale": "0xD0e7d5a2220e32914540D97A6D0548658050180b"
}
8 changes: 5 additions & 3 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ const config: HardhatUserConfig = {
accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
},
etherscan: {
apiKey: {
polygon: process.env.POLYGONSCAN_API_KEY == undefined ? '' : process.env.POLYGONSCAN_API_KEY,
}
},
typechain: {
outDir: 'typechain',
target: 'ethers-v5',
Expand All @@ -54,9 +59,6 @@ const config: HardhatUserConfig = {
enabled: process.env.REPORT_GAS !== undefined,
currency: 'USD',
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
mocha: {
// 1 hour, essentially disabled auto timeout
timeout: 60 * 60 * 1000,
Expand Down
16 changes: 16 additions & 0 deletions interfaces/ITokenLockVestReader.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

import "./IRevokableTokenLock.sol";

interface ITokenLockVestReader is IRevokableTokenLock {
struct VestingParams {
uint256 unlockBegin;
uint256 unlockCliff;
uint256 unlockEnd;
uint256 lockedAmounts;
uint256 claimedAmounts;
}

function vesting(address) external view returns (VestingParams memory);
}
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@
},
"homepage": "https://github.com/code-423n4/genesis#readme",
"dependencies": {
"hardhat": "2.7.0",
"hardhat": "2.8.3",
"lodash": "^4.17.21"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.3",
"@nomiclabs/hardhat-etherscan": "2.1.7",
"@nomiclabs/hardhat-ethers": "^2.0.4",
"@nomiclabs/hardhat-etherscan": "3.0.0",
"@nomiclabs/hardhat-waffle": "^2.0.0",
"@openzeppelin/contracts": "^4.4.0",
"@typechain/ethers-v5": "^7.0.1",
"@typechain/hardhat": "^2.3.0",
"@typechain/ethers-v5": "^9.0.0",
"@typechain/hardhat": "^4.0.0",
"@types/chai": "^4.2.21",
"@types/lodash": "^4.14.178",
"@types/mocha": "^9.0.0",
Expand All @@ -60,7 +60,7 @@
"solidity-coverage": "^0.7.16",
"ts-generator": "^0.1.1",
"ts-node": "^10.4.0",
"typechain": "^5.1.2",
"typescript": "^4.3.5"
"typechain": "^7.0.0",
"typescript": "^4.5.5"
}
}
43 changes: 19 additions & 24 deletions scripts/deploy/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,22 @@ type TokenSaleConfig = {
TOKEN_SALE_ARENA_PRICE: BN;
TOKEN_SALE_RECIPIENT: string;
TOKEN_SALE_WHITELIST: typeof TOKEN_SALE_WHITELIST;
RECIPIENT_AMOUNT: BN;
TOKEN_SALE_SUPPLY: BN;
};

const TOKEN_SALE_WHITELIST = [
{buyer: '0x0f4Aeb1847B7F1a735f4a5Af7E8C299b793c1a9A', arenaAmount: BN.from(`10000`).mul(ONE_18)},
{buyer: '0x3Ab0029e1C4515134464b267557cB80A39902699', arenaAmount: BN.from(`10001`).mul(ONE_18)},
{buyer: '0x4F3F7ca7E91D869180EBbA55e4322845a8Dc6862', arenaAmount: BN.from(`10002`).mul(ONE_18)},
{buyer: '0x5dcEb6f4dc5b64Af6271A5Ab3297DbE3C01dd57B', arenaAmount: BN.from(`10003`).mul(ONE_18)},
{buyer: '0x62641eAE546835813B56EC7b544756A532275Dd3', arenaAmount: BN.from(`10004`).mul(ONE_18)},
{buyer: '0x670f9e8B37d5816c2eB93A1D94841C66652a8E26', arenaAmount: BN.from(`10005`).mul(ONE_18)},
{buyer: '0x691Cbab55CC1806d29994784Ba9d9e679c03f164', arenaAmount: BN.from(`10006`).mul(ONE_18)},
{buyer: '0x697ccd97C8419EBba7347CEF03a0CD02804EbF54', arenaAmount: BN.from(`10007`).mul(ONE_18)},
{buyer: '0x6c422839E7EceDb6d2A86F3F2bFd03aDd154Fc27', arenaAmount: BN.from(`10008`).mul(ONE_18)},
{buyer: '0x7C0fb88c87c30eBF70340E25fe47763e53b907cF', arenaAmount: BN.from(`10009`).mul(ONE_18)},
{buyer: '0x8498EAb53e03E3143d77B2303eDBdAC6C9041D33', arenaAmount: BN.from(`10010`).mul(ONE_18)},
{buyer: '0x8D31BAC0870e323354eAF6F98277860772FFB2d4', arenaAmount: BN.from(`10011`).mul(ONE_18)},
{buyer: '0xA432F83d8054F5F859cAcb86574baC5e07DD6529', arenaAmount: BN.from(`10012`).mul(ONE_18)},
{buyer: '0xD3488b8C87416946D82CC957178B0863A1F089b2', arenaAmount: BN.from(`10013`).mul(ONE_18)},
{buyer: '0xD5388291EAbe96b56069440C97046791E2F72573', arenaAmount: BN.from(`10014`).mul(ONE_18)},
{buyer: '0xF20eb7eAf52712EA0Aa80467741f34E6b0dB18F8', arenaAmount: BN.from(`10015`).mul(ONE_18)},
{buyer: '0xa1fA3C686C9c4E5e8407b32B67191B079a65ffD2', arenaAmount: BN.from(`10016`).mul(ONE_18)},
{buyer: '0xbB79597641483Ed96BCE9fc24b4D63F720898b8A', arenaAmount: BN.from(`10017`).mul(ONE_18)},
{buyer: '0xe552C6A88E71B2A5069Dec480507F54321Dc65F3', arenaAmount: BN.from(`10018`).mul(ONE_18)},
{buyer: '0xf4290941dBc8b31c277E30deFF3fC59979FC6757', arenaAmount: BN.from(`10019`).mul(ONE_18)},
{buyer: '0x1aa1F9f80f4c5dCe34d0f4faB4F66AAF562330bd', arenaAmount: BN.from(33_333_333).mul(ONE_18)},
{buyer: '0x3a5c572aE7a806c661970058450dC90D9eF0f353', arenaAmount: BN.from(13_333_333).mul(ONE_18)},
{buyer: '0xcfc50541c3dEaf725ce738EF87Ace2Ad778Ba0C5', arenaAmount: BN.from(10_166_666).mul(ONE_18)},
{buyer: '0xC02ad7b9a9121fc849196E844DC869D2250DF3A6', arenaAmount: BN.from(8_333_333).mul(ONE_18)},
{buyer: '0xCfCA53C4b6d3f763969c9A9C36DBCAd61F11F36D', arenaAmount: BN.from(6_666_666).mul(ONE_18)},
{buyer: '0x636EDa86F6EC324347Bd560c1045192586b9DEE8', arenaAmount: BN.from(6_666_666).mul(ONE_18)},
{buyer: '0xDbBB1bD4cbDA95dd2f1477be139C3D6cb9d2B349', arenaAmount: BN.from(3_333_333).mul(ONE_18)},
{buyer: '0x4dA94e682326BD14997D1E1c62350654D8e44c5d', arenaAmount: BN.from(2_500_000).mul(ONE_18)},
{buyer: '0x20392b9607dc8cC49BEa5B7B90E65d6251617538', arenaAmount: BN.from(1_166_666).mul(ONE_18)},
{buyer: '0x83b23E8e5da74fD3f3E5471865FC778d9c843df0', arenaAmount: BN.from(833_333).mul(ONE_18)},
{buyer: '0x7fCAf93cc92d51c490FFF701fb2C6197497a80db', arenaAmount: BN.from(833_333).mul(ONE_18)},
];

export const allConfigs: {[key: number]: Config} = {
Expand Down Expand Up @@ -72,11 +65,13 @@ export const allConfigs: {[key: number]: Config} = {
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_START: 1644451200, // Thursday, February 10, 2022 12:00:00 AM UTC
TOKEN_SALE_DURATION: 10 * 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', // TODO: change to intended recipient
TOKEN_SALE_WHITELIST, // TODO: update value
TOKEN_SALE_RECIPIENT: '0x7f0049597056E37B4B1f887196E44CAc050D4863', // C4 Polygon multisig
TOKEN_SALE_WHITELIST,
RECIPIENT_AMOUNT: BN.from(1_750_000).mul(BN.from(10).pow(6)), // 1.75M USDC, rest to treasury
TOKEN_SALE_SUPPLY: BN.from(100_000_000).mul(ONE_18), // 100M ARENA tokens
},
};
24 changes: 18 additions & 6 deletions scripts/deploy/deployTokenSale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {

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

let proposerAddress: string;
let tokenSale: TokenSale;
Expand Down Expand Up @@ -77,7 +78,8 @@ export async function deployTokenSale(hre: HardhatRuntimeEnvironment) {
config.TOKEN_SALE_ARENA_PRICE,
config.TOKEN_SALE_RECIPIENT,
tokenLock.address,
allConfigs[networkId].VEST_DURATION
allConfigs[networkId].VEST_DURATION,
config.RECIPIENT_AMOUNT
);
await tokenSale.deployed();
console.log(`tokenSale address: ${tokenSale.address}`);
Expand All @@ -87,7 +89,6 @@ export async function deployTokenSale(hre: HardhatRuntimeEnvironment) {
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);

Expand All @@ -97,14 +98,14 @@ export async function deployTokenSale(hre: HardhatRuntimeEnvironment) {
let values: string[] = ['0', '0'];
let calldatas: string[] = [
tokenLock.interface.encodeFunctionData('setTokenSale', [tokenSale.address]),
arenaToken.interface.encodeFunctionData('transfer', [tokenSale.address, TOKEN_SALE_SUPPLY]),
arenaToken.interface.encodeFunctionData('transfer', [tokenSale.address, config.TOKEN_SALE_SUPPLY]),
];

const tx = await governor['propose(address[],uint256[],bytes[],string)'](
targets,
values,
calldatas,
`Conduct Arena token sale!`
`# C4IP-6: Transfer ARENA tokens for token sale\nThis proposal takes action on the token sale approved by [C4IP-1](<https://www.withtally.com/governance/eip155:137:0xc6eaDcC36aFcf1C430962506ad79145aD5140E58/proposal/61969381053746686972699442694032986733206504062025717191093241526145462208038>) and the hiring of Code4 Corporation approved by [C4IP-3](<https://www.withtally.com/governance/eip155:137:0xc6eaDcC36aFcf1C430962506ad79145aD5140E58/proposal/46190911081008287731655546929165163023822387405966829437304548060152876868278>) both of which are discussed in detail in [this forum post](<https://forum.code4rena.com/t/c4ip-1-constitution-dao-bootstrapping-reimbursements-token-sale/93>)<br>\n\n- 100,000,000 $ARENA tokens transferred to the [token sale contract](<https://polygonscan.com/address/0xD0e7d5a2220e32914540D97A6D0548658050180b>)\n\n- Tokens are sold at price of 1 ARENA = .03 USDC\n\n- Token sale details to be administered by Code4 Corporation\n\n- $1.75M of the initial sale will immediately be used to fund Code4 Corporation operations\n\n- Remaining $1.25M proceeds will be transferred to the Code4rena treasury\n\n\n<!-- -->\n\n`
);
console.log(`proposal submitted: ${tx.hash}`);
console.log(`waiting for block inclusion ...`);
Expand All @@ -116,12 +117,23 @@ export async function deployTokenSale(hre: HardhatRuntimeEnvironment) {
let exportJson = JSON.stringify(addressesToExport, null, 2);
fs.writeFileSync(deploymentFilePath, exportJson);

console.log(`Verifying tokenSale contract...`);
await verifyContract(hre, tokenSale.address, [
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,
config.RECIPIENT_AMOUNT,
]);

/////////////////////////////////
// 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);

Expand Down
8 changes: 8 additions & 0 deletions scripts/deploy/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {HardhatRuntimeEnvironment} from 'hardhat/types';

export async function verifyContract(hre: HardhatRuntimeEnvironment, contractAddress: string, ctorArgs: any[]) {
await hre.run('verify:verify', {
address: contractAddress,
constructorArguments: ctorArgs,
});
}
Loading

0 comments on commit 7a0c6f7

Please sign in to comment.