Skip to content

Commit

Permalink
Allow for new token sale, check for buyer vest schedule, cap sale rec…
Browse files Browse the repository at this point in the history
…ipient amount
  • Loading branch information
HickupHH3 committed Mar 4, 2022
1 parent bae376b commit fe09cfb
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 60 deletions.
85 changes: 68 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,15 +15,19 @@ 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;
/// DAO receives the remaining proceeds of the sale
address internal immutable timelockController;
/// 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;
Expand All @@ -40,9 +44,11 @@ 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 _timelockController The address receiving the remaining proceeds of the sale. Should be C4 timelock controller
* @param _vestDuration Token vesting duration
* @param _remainingSaleRecipientAmount Amount receivable by sale recipient
*/
constructor(
ERC20 _tokenIn,
Expand All @@ -52,7 +58,9 @@ contract TokenSale is Ownable {
uint256 _tokenOutPrice,
address _saleRecipient,
address _tokenLock,
uint256 _vestDuration
address _timelockController,
uint256 _vestDuration,
uint256 _remainingSaleRecipientAmount
) {
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 @@ -63,16 +71,18 @@ contract TokenSale is Ownable {
"TokenSale: sale recipient should not be zero or this"
);
require(_tokenLock != address(0), "Address cannot be 0x");
require(_timelockController != address(0), "Address cannot be 0x");

tokenIn = _tokenIn;
tokenOut = _tokenOut;
saleStart = _saleStart;
saleDuration = _saleDuration;
tokenOutPrice = _tokenOutPrice;
saleRecipient = _saleRecipient;

tokenLock = IRevokableTokenLock(_tokenLock);
tokenLock = ITokenLockVestReader(_tokenLock);
timelockController = _timelockController;
vestDuration = _vestDuration;
remainingSaleRecipientAmount = _remainingSaleRecipientAmount;
}

/**
Expand All @@ -86,10 +96,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 timelock
uint256 timelockControllerAmount = tokenInAmount_ - remainingSaleRecipientAmount;
require(
tokenIn.transferFrom(msg.sender, timelockController, timelockControllerAmount),
"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 +134,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 +162,38 @@ contract TokenSale is Ownable {
_buyers.length == _newTokenOutAmounts.length,
"TokenSale: parameter length mismatch"
);
require(block.timestamp < saleStart, "TokenSale: sale already started");
require(block.timestamp < saleStart, "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];
}
}

/**
* @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
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);
}
42 changes: 18 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;
};

// TODO: update values as they are in USDC when they should be in ARENA tokens
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(1_000_000).mul(ONE_18)},
{buyer: '0x3a5c572aE7a806c661970058450dC90D9eF0f353', arenaAmount: BN.from(400_000).mul(ONE_18)},
{buyer: '0xcfc50541c3dEaf725ce738EF87Ace2Ad778Ba0C5', arenaAmount: BN.from(305_000).mul(ONE_18)},
{buyer: '0xC02ad7b9a9121fc849196E844DC869D2250DF3A6', arenaAmount: BN.from(250_000).mul(ONE_18)},
{buyer: '0xCfCA53C4b6d3f763969c9A9C36DBCAd61F11F36D', arenaAmount: BN.from(200_000).mul(ONE_18)},
{buyer: '0x636EDa86F6EC324347Bd560c1045192586b9DEE8', arenaAmount: BN.from(200_000).mul(ONE_18)},
{buyer: '0xDbBB1bD4cbDA95dd2f1477be139C3D6cb9d2B349', arenaAmount: BN.from(100_000).mul(ONE_18)},
{buyer: '0x4dA94e682326BD14997D1E1c62350654D8e44c5d', arenaAmount: BN.from(75_000).mul(ONE_18)},
{buyer: '0x20392b9607dc8cC49BEa5B7B90E65d6251617538', arenaAmount: BN.from(35_000).mul(ONE_18)},
// {buyer: '0x45d28aA363fF215B4c6b6a212DC610f004272bb5', arenaAmount: BN.from(25_000).mul(ONE_18)}, note: this address is in airdrop
{buyer: '0x7fCAf93cc92d51c490FFF701fb2C6197497a80db', arenaAmount: BN.from(25_000).mul(ONE_18)},
];

export const allConfigs: {[key: number]: Config} = {
Expand Down Expand Up @@ -72,11 +65,12 @@ 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: Math.floor(new Date(`2022-01-12T00:00:00.000Z`).getTime() / 1000), // TODO: modify to new date
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(ONE.pow(6)), // 1.75M USDC, rest to treasury
},
};
6 changes: 3 additions & 3 deletions scripts/deploy/deployTokenSale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ export async function deployTokenSale(hre: HardhatRuntimeEnvironment) {
config.TOKEN_SALE_ARENA_PRICE,
config.TOKEN_SALE_RECIPIENT,
tokenLock.address,
allConfigs[networkId].VEST_DURATION
timelock.address,
allConfigs[networkId].VEST_DURATION,
config.RECIPIENT_AMOUNT
);
await tokenSale.deployed();
console.log(`tokenSale address: ${tokenSale.address}`);
Expand Down Expand Up @@ -120,8 +122,6 @@ export async function deployTokenSale(hre: HardhatRuntimeEnvironment) {
// 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
Loading

0 comments on commit fe09cfb

Please sign in to comment.