├── package.json ├── contracts ├── interfaces │ ├── IUniswapV2Factory.sol │ └── IUniswapV2Router.sol └── Token.sol ├── tsconfig.json ├── .gitignore ├── hardhat.config.ts ├── ignition └── modules │ └── Lock.ts ├── scripts └── deploy.ts ├── README.md └── test └── Token.ts /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-project", 3 | "devDependencies": { 4 | "@nomicfoundation/hardhat-toolbox": "^5.0.0", 5 | "hardhat": "^2.22.3" 6 | }, 7 | "dependencies": { 8 | "@openzeppelin/contracts": "^5.0.2" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | interface IUniswapV2Factory { 5 | function createPair( 6 | address tokenA, 7 | address tokenB 8 | ) external returns (address pair); 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | # Hardhat files 5 | /cache 6 | /artifacts 7 | 8 | # TypeChain files 9 | /typechain 10 | /typechain-types 11 | 12 | # solidity-coverage files 13 | /coverage 14 | /coverage.json 15 | 16 | # Hardhat Ignition default folder for deployments against a local node 17 | ignition/deployments/chain-31337 18 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | 4 | const config: HardhatUserConfig = { 5 | solidity: "0.8.24", 6 | mocha: { 7 | timeout: 6000000000000 8 | }, 9 | networks: { 10 | hardhat: { 11 | forking: { 12 | url: 'https://eth-mainnet.g.alchemy.com/v2/AcptWBmH9mRjOCvY0LdPiTvOK9vZVXFe' 13 | } 14 | } 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /ignition/modules/Lock.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | 3 | const JAN_1ST_2030 = 1893456000; 4 | const ONE_GWEI: bigint = 1_000_000_000n; 5 | 6 | const LockModule = buildModule("LockModule", (m) => { 7 | const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030); 8 | const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI); 9 | 10 | const lock = m.contract("Lock", [unlockTime], { 11 | value: lockedAmount, 12 | }); 13 | 14 | return { lock }; 15 | }); 16 | 17 | export default LockModule; 18 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Router.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | interface IUniswapV2Router { 5 | function factory() external pure returns (address); 6 | 7 | function WETH() external pure returns (address); 8 | 9 | function addLiquidity( 10 | address tokenA, 11 | address tokenB, 12 | uint amountADesired, 13 | uint amountBDesired, 14 | uint amountAMin, 15 | uint amountBMin, 16 | address to, 17 | uint deadline 18 | ) external returns (uint amountA, uint amountB, uint liquidity); 19 | } 20 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | async function main() { 4 | const startDate = BigInt(Math.floor(Date.now() / 1000)) 5 | const endDate = startDate + 600n 6 | const token = await ethers.deployContract("Token", [1000000000000000000000000n, startDate, endDate, 6000n, 10n]); 7 | 8 | await token.waitForDeployment(); 9 | 10 | console.log("Token:", token.target); 11 | } 12 | 13 | // We recommend this pattern to be able to use async/await everywhere 14 | // and properly handle errors. 15 | main().catch((error) => { 16 | console.error(error); 17 | process.exitCode = 1; 18 | }); 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fair Launch Token 2 | 3 | ### Constructor 4 | 5 | #### `constructor(uint256 amount, uint256 startDate, uint256 endDate, uint256 lpPercent, uint256 ticks)` 6 | 7 | - amount: The amount of token to be minted 8 | - startDate: The start date of the sale 9 | - endDate: The end date of the sale 10 | - lpPercent: The percent of the token to be used for LP 11 | - ticks: The total ticks of the sale 12 | 13 | ### Read Functions 14 | 15 | #### `getCurrentTokenForUser(address user)` 16 | 17 | Returns the minted token amount for the user in the current tick 18 | 19 | #### `getTokenPerTick()` 20 | 21 | Returns the assigned token account per tick 22 | 23 | #### `getCurrentTickIndex()` 24 | 25 | Returns the current tick 26 | 27 | ### Write Functions 28 | 29 | #### `setTicks(uint256 ticks) onlyOwner` 30 | 31 | Updates the total ticks 32 | 33 | - ticks: The total ticks of the sale 34 | 35 | #### `enter(uint256 _usdtAmount)` 36 | 37 | Adds the user with USDT deposit in the current tick 38 | 39 | - \_usdtAmount: The USDT deposit amount 40 | 41 | #### `exit()` 42 | 43 | Removes the user from the sale with returning the USDT deposit 44 | 45 | #### `claim()` 46 | 47 | Sends the mined token amount to the user after the sale finished 48 | 49 | #### `createPair()` 50 | 51 | Creates a LP with the token and USDT after the sale finished 52 | 53 | ### Gas Usage (Estimation) 54 | 55 | - Enter: 174975 = 0,0013998 (4$) 56 | - Exit: 141069 = 0,001128552 (3$) 57 | - Claim: 114821 = 0,000918568 (2.70$) 58 | - Create: 2750300 = 0,0220024 (66.24$) 59 | -------------------------------------------------------------------------------- /test/Token.ts: -------------------------------------------------------------------------------- 1 | import { 2 | time, 3 | loadFixture, 4 | } from "@nomicfoundation/hardhat-toolbox/network-helpers"; 5 | import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; 6 | import { expect } from "chai"; 7 | import hre, { ethers } from "hardhat"; 8 | 9 | describe("Lock", function () { 10 | // We define a fixture to reuse the same setup in every test. 11 | // We use loadFixture to run this setup once, snapshot that state, 12 | // and reset Hardhat Network to that snapshot in every test. 13 | async function deployToken() { 14 | const [owner] = await hre.ethers.getSigners() 15 | 16 | const users = await Promise.all(Array(400).fill('').map(async () => { 17 | const wallet = ethers.Wallet.createRandom() 18 | await ethers.provider.send("hardhat_impersonateAccount", [wallet.address]); 19 | return ethers.provider.getSigner(wallet.address); 20 | })) 21 | const USDT = await hre.ethers.getContractAt('IERC20', '0xdAC17F958D2ee523a2206206994597C13D831ec7') 22 | 23 | 24 | 25 | const Token = await hre.ethers.getContractFactory("Token"); 26 | const startDate = BigInt(Math.floor(Date.now() / 1000) + 1200) 27 | const endDate = startDate + 60000n 28 | const token = await Token.deploy(1000000000000000000000000n, startDate, endDate, 6000n, 10n); 29 | 30 | const usdtVaults = await hre.ethers.getImpersonatedSigner('0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503') 31 | 32 | 33 | for (let i = 0; i < 400; i++) { 34 | await owner.sendTransaction({ from: owner.address, to: users[i].address, value: ethers.parseEther('1') }) 35 | await (await USDT.connect(usdtVaults).transfer(users[i].address, ethers.parseUnits('1000', 6))).wait() 36 | } 37 | 38 | return { token, users, owner, USDT, startDate, endDate }; 39 | } 40 | 41 | describe("Token", function () { 42 | it("Enter ticks", async function () { 43 | const { token, owner, users, USDT, startDate, endDate } = await loadFixture(deployToken); 44 | 45 | for (let i = 0; i < 40; i++) { 46 | await (await USDT.connect(users[i]).approve(token.target, ethers.parseUnits('1000', 6))).wait() 47 | } 48 | 49 | await time.increaseTo(Number(startDate) + 100); 50 | console.log(await token.getCurrentTickIndex()) 51 | for (let i = 1; i < 100; i++) { 52 | await (await token.connect(users[i]).enter(ethers.parseUnits(Math.floor(Math.random() * 999 + 1).toString(), 6))).wait() 53 | } 54 | 55 | await time.increaseTo(Number(startDate) + 6100); 56 | console.log(await token.getCurrentTickIndex()) 57 | for (let i = 100; i < 200; i++) { 58 | await (await token.connect(users[i]).enter(ethers.parseUnits(Math.floor(Math.random() * 999 + 1).toString(), 6))).wait() 59 | } 60 | 61 | await time.increaseTo(Number(startDate) + 18100); 62 | console.log(await token.getCurrentTickIndex()) 63 | for (let i = 200; i < 300; i++) { 64 | await (await token.connect(users[i]).enter(ethers.parseUnits(Math.floor(Math.random() * 999 + 1).toString(), 6))).wait() 65 | } 66 | 67 | await time.increaseTo(Number(startDate) + 30100); 68 | console.log(await token.getCurrentTickIndex()) 69 | for (let i = 300; i < 400; i++) { 70 | await (await token.connect(users[i]).enter(ethers.parseUnits(Math.floor(Math.random() * 999 + 1).toString(), 6))).wait() 71 | } 72 | 73 | const enterTx = await (await token.connect(users[0]).enter(ethers.parseUnits(Math.floor(Math.random() * 999 + 1).toString(), 6))).wait() 74 | 75 | console.log('Enter: ', enterTx?.gasPrice, enterTx?.gasUsed) 76 | 77 | 78 | await time.increaseTo(Number(startDate) + 48100); 79 | 80 | const exitTx = await (await token.connect(users[300]).exit()).wait() 81 | 82 | console.log('Exit: ', exitTx?.gasPrice, exitTx?.gasUsed) 83 | 84 | await time.increaseTo(Number(endDate) + 100) 85 | 86 | const claimTx = await (await token.connect(users[200]).claim()).wait() 87 | 88 | console.log('Claim: ', claimTx?.gasPrice, claimTx?.gasUsed) 89 | 90 | const createTx = await (await token.createPair()).wait() 91 | 92 | console.log('Create: ', createTx?.gasPrice, createTx?.gasUsed) 93 | 94 | 95 | }); 96 | }); 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /contracts/Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 7 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 8 | import {IUniswapV2Factory} from "./interfaces/IUniswapV2Factory.sol"; 9 | import {IUniswapV2Router} from "./interfaces/IUniswapV2Router.sol"; 10 | import "hardhat/console.sol"; 11 | 12 | contract Token is ERC20, Ownable { 13 | using SafeERC20 for IERC20; 14 | 15 | struct TickAccount { 16 | uint256 enter; 17 | uint256 exit; 18 | uint256 deposit; 19 | bool isClaimed; 20 | } 21 | 22 | struct TickState { 23 | uint256 deposit; 24 | uint256 users; 25 | } 26 | 27 | address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; 28 | address public constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; 29 | address public pair; 30 | 31 | uint256 public _totalSupply; 32 | uint256 public _startDate; 33 | uint256 public _endDate; 34 | uint256 public _ticks; 35 | uint256 public _lpPercent; 36 | 37 | uint256 public _totalDeposit; 38 | uint256 public _totalUsers; 39 | 40 | mapping(address => TickAccount) public tickAccounts; 41 | mapping(uint256 => TickState) public tickStates; 42 | 43 | event Entered(address indexed user, uint256 amount, uint256 tick); 44 | event Exited( 45 | address indexed user, 46 | uint256 amount, 47 | uint256 entered, 48 | uint256 exited 49 | ); 50 | event Claimed(address indexed user, uint256 amount); 51 | 52 | constructor( 53 | uint256 amount, 54 | uint256 startDate, 55 | uint256 endDate, 56 | uint256 lpPercent, 57 | uint256 ticks 58 | ) ERC20("", "") Ownable(msg.sender) { 59 | _totalSupply = amount; 60 | _startDate = startDate; 61 | _endDate = endDate; 62 | _lpPercent = lpPercent; 63 | _ticks = ticks; 64 | 65 | _mint(address(this), _totalSupply); 66 | } 67 | 68 | function setTicks(uint256 ticks) external onlyOwner { 69 | require(block.timestamp < _startDate, "Sale started"); 70 | 71 | _ticks = ticks; 72 | } 73 | 74 | function enter(uint256 _usdtAmount) external { 75 | TickAccount storage tickAccount = tickAccounts[msg.sender]; 76 | require(tickAccount.enter == 0, "Already entered"); 77 | require(_usdtAmount > 0, "Invalid usdt amount"); 78 | 79 | IERC20(USDT).safeTransferFrom(msg.sender, address(this), _usdtAmount); 80 | 81 | uint256 curTick = getCurrentTickIndex(); 82 | 83 | tickAccount.enter = curTick + 1; 84 | tickAccount.deposit = _usdtAmount; 85 | 86 | _totalDeposit += _usdtAmount; 87 | _totalUsers += 1; 88 | 89 | for (uint i = curTick + 1; i <= _ticks; i++) { 90 | TickState storage tickState = tickStates[i]; 91 | tickState.deposit += _usdtAmount; 92 | tickState.users += 1; 93 | } 94 | 95 | emit Entered(msg.sender, _usdtAmount, curTick + 1); 96 | } 97 | 98 | function exit() external { 99 | TickAccount storage tickAccount = tickAccounts[msg.sender]; 100 | require(block.timestamp <= _endDate, "Sale finished"); 101 | require(tickAccount.enter > 0, "Not entered"); 102 | require(tickAccount.exit == 0, "Already exited"); 103 | 104 | uint256 curTick = getCurrentTickIndex(); 105 | 106 | tickAccount.exit = curTick; 107 | 108 | uint256 usdtAmount = tickAccount.deposit; 109 | 110 | IERC20(USDT).safeTransfer(msg.sender, usdtAmount); 111 | 112 | for (uint i = tickAccount.enter; i <= _ticks; i++) { 113 | TickState storage tickState = tickStates[i]; 114 | tickState.deposit -= usdtAmount; 115 | tickState.users -= 1; 116 | } 117 | 118 | emit Exited( 119 | msg.sender, 120 | tickAccount.deposit, 121 | tickAccount.enter, 122 | tickAccount.exit 123 | ); 124 | } 125 | 126 | function claim() external { 127 | TickAccount storage tickAccount = tickAccounts[msg.sender]; 128 | require(block.timestamp > _endDate, "Sale not finished"); 129 | require(tickAccount.enter > 0, "Not entered"); 130 | require(tickAccount.exit == 0, "Exited"); 131 | require(!tickAccount.isClaimed, "Already claimed"); 132 | 133 | tickAccount.isClaimed = true; 134 | 135 | uint256 userAmount = getCurrentTokenForUser(msg.sender); 136 | 137 | _transfer(address(this), msg.sender, userAmount); 138 | 139 | emit Claimed(msg.sender, userAmount); 140 | } 141 | 142 | function createPair() external { 143 | require(block.timestamp > _endDate, "Sale not finished"); 144 | 145 | address _pair = IUniswapV2Factory(IUniswapV2Router(ROUTER).factory()) 146 | .createPair(address(this), USDT); 147 | 148 | pair = _pair; 149 | 150 | uint256 tokenAmountDesired = (_totalSupply * _lpPercent) / 10000; 151 | uint256 usdtAmountDesired = IERC20(USDT).balanceOf(address(this)); 152 | 153 | _approve(address(this), ROUTER, tokenAmountDesired); 154 | IERC20(USDT).forceApprove(ROUTER, usdtAmountDesired); 155 | 156 | IUniswapV2Router(ROUTER).addLiquidity( 157 | USDT, 158 | address(this), 159 | usdtAmountDesired, 160 | tokenAmountDesired, 161 | 0, 162 | 0, 163 | owner(), 164 | block.timestamp + 1000 165 | ); 166 | } 167 | 168 | function getCurrentTokenForUser( 169 | address user 170 | ) public view returns (uint256) { 171 | TickAccount memory tickAccount = tickAccounts[user]; 172 | 173 | if (tickAccount.enter == 0 || tickAccount.exit > 0) { 174 | return 0; 175 | } 176 | 177 | uint256 curTick = getCurrentTickIndex(); 178 | 179 | uint256 amountPerTick = getTokenPerTick(); 180 | 181 | uint256 userAmount = 0; 182 | 183 | for (uint i = tickAccount.enter; i <= curTick; i++) { 184 | uint256 userAmountInTick = (amountPerTick * tickAccount.deposit) / 185 | tickStates[i].deposit; 186 | 187 | userAmount += userAmountInTick; 188 | } 189 | 190 | return userAmount; 191 | } 192 | 193 | function getTokenPerTick() public view returns (uint256) { 194 | return (_totalSupply * (10000 - _lpPercent)) / 10000 / _ticks; 195 | } 196 | 197 | function getCurrentTickIndex() public view returns (uint256) { 198 | uint256 curTime = block.timestamp; 199 | if (curTime < _startDate) { 200 | return 0; 201 | } else if (curTime >= _endDate) { 202 | return _ticks; 203 | } else { 204 | uint256 duration = curTime - _startDate; 205 | uint256 totalDuration = _endDate - _startDate; 206 | return (_ticks * duration) / totalDuration; 207 | } 208 | } 209 | } 210 | --------------------------------------------------------------------------------