├── .gas-snapshot ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── bonding_curve.md ├── bonding_curve.py ├── bonding_curve_equation.py ├── foundry.toml ├── image.png ├── remappings.txt ├── script └── TokenFactory.s.sol ├── slither.config.json ├── soldeer.lock ├── src ├── BondingCurve.sol ├── ConstantBondingCurve.sol ├── Token.sol └── TokenFactory.sol └── test ├── BondingCurve.t.sol ├── ConstantBondingCurve.t.sol ├── TokenFactory.t.sol └── TokenFactoryForked.t.sol /.gas-snapshot: -------------------------------------------------------------------------------- 1 | TokenFactoryTest:test_Buy() (gas: 291365) 2 | TokenFactoryTest:test_BuyForked() (gas: 2939558) 3 | TokenFactoryTest:test_CalculateBuyReturn() (gas: 7122) 4 | TokenFactoryTest:test_CalculateSellReturn() (gas: 6983) 5 | TokenFactoryTest:test_CreateToken() (gas: 224729) 6 | TokenFactoryTest:test_Sell() (gas: 263590) -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/*/11155111/ 9 | /broadcast/**/dry-run/ 10 | 11 | # Docs 12 | docs/ 13 | 14 | # Dotenv file 15 | .env 16 | 17 | dependencies/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## pump eth 2 | 3 | 1. 创建代币 4 | 2. 购买代币 5 | 3. 卖出代币 6 | 7 | ```solidity 8 | contract Token is ERC20 {} 9 | 10 | contract TokenFactory 11 | - createToken(string name, string symbol) 12 | - new Token 13 | - initial_supply 14 | 15 | - buy(address tokenAddress) payable 16 | - calculateBuyReturn() 17 | - uniswap: 18 | - createLiquilityPool() 19 | - addLiquidity() 20 | - burnLiquidityToken() 21 | 22 | - sell(address tokenAddress, uint256 amount) 23 | - calculateSellReturn() 24 | - transfer 25 | ``` 26 | 27 | 1. [x] check token state 28 | 2. [x] Fork mainnet, test uniswap 29 | 3. [x] minimal proxy 30 | 4. [x] Bonding Curve 31 | -------------------------------------------------------------------------------- /bonding_curve.md: -------------------------------------------------------------------------------- 1 | # Bonding Curve 2 | 3 | Bonding Curve 是一个通过预先设定的曲线,来确定 Token 供应量与 Token 价格关系的函数曲线。 4 | 即 Token 价格 = f(Token 供应量)。 5 | 6 | 特点: 价格很难被恶意操纵。 7 | 项目方不能通过保留 99% 的代币实现砸盘。 8 | 9 | 在 Bonding Curve 上,所有的 Token 都是通过将锚定资产打入铸造合约来铸造产生的。 10 | 比如用 ETH 作为锚定资产铸造 Token 代币: 11 | 12 | - 打入 ETH,获得 Token 代币 13 | - 代入 Token 代币,获取 ETH 14 | 15 | ETH 所需的数量和 Token 代币的数量由 Bonding Curve 决定。 16 | 17 | ![image](https://img.gopic.xyz/VanillaCurvePrice.jpg) 18 | 19 | 要得到在 Bonding Curve 下两种代币之间的关系,我们需要对曲线函数进行积分计算。 20 | 比如对于函数曲线 $y = m x^n$(m,n是常数),购买 $dx$ 代币的花费 $dp = y * dx$, 因此如果想在价格为 x 的时候买如 k 个代币,则需要的花费是: 21 | 22 | ![](https://i.sstatic.net/BS1F5.png) 23 | 24 | 因此有 k 和 p 的关系为: 25 | 26 | ![](https://i.sstatic.net/cj20H.png) 27 | 28 | 其中 $Rb = m*x^{(n+1)}/(n+1)$ 表示锚定资产余额, $r = 1/(n+1)$ 表示锚定资产比例。 29 | 30 | 例如 $n=2$,$m=0.003$ 时有 31 | 32 | ![](https://i.sstatic.net/x3X5F.png) 33 | 34 | 以及对应的 solidity 代码: 35 | 36 | ```solidity 37 | function calculatePurchaseReturn( 38 | uint256 _totalSupply, 39 | uint256 _depositAmount 40 | ) public pure returns (uint256) { 41 | uint256 temp = 1000*_depositAmount + _totalSupply*_totalSupply*_totalSupply; 42 | temp = powerOneThird(temp); // temp^(1/3) 43 | 44 | return temp - _totalSupply; 45 | } 46 | 47 | ``` 48 | 49 | 在实际情况中,我们使用[Bancor Bonding Curve 公式](https://drive.google.com/file/d/0B3HPNP-GDn7aRkVaV3dkVl9NS2M/view)来简化计算和避免整数计算溢出。 50 | 51 | 定义 $$ F = \frac{R}{S * P}$$ 52 | 53 | 其中: 54 | 55 | - F: 表示锚定资产比例 56 | - R: 表示锚定资产数量 57 | - S: Token 的供应量 58 | - P: Token 的价格 59 | 60 | 锚定资产比例 = 1 是常数价格曲线。 61 | 锚定资产比例 = 1/2 是线性价格曲线。 62 | 锚定资产比例 < 1/2 是下凹价格曲线。 63 | 锚定资产比例 > 1/2 是上凸价格曲线。 64 | 65 | ![image](./image.png) 66 | 67 | 我们还可以得到两个公式: 68 | 69 | 计算使用锚定资产($E$)买入Token的数量($T$): 70 | 71 | $$ T = S((1+ \frac{E}{R}) ^ F - 1)$$ 72 | 73 | 计算卖出Token数量($T$)所得到的锚定资产($E$): 74 | 75 | $$ E = R(1-(1-\frac{T}{S})^{\frac{1}{F}})$$ 76 | 77 | 参考: 78 | 79 | - https://medium.com/relevant-community/bonding-curves-in-depth-intuition-parametrization-d3905a681e0a 80 | -------------------------------------------------------------------------------- /bonding_curve.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | WAD = 1e18 # 18 decimals for fixed-point precision 4 | 5 | 6 | def mul_wad(x, y): 7 | return (x * y) // WAD 8 | 9 | 10 | def exp_wad(x): 11 | return int(math.exp(x / WAD) * WAD) 12 | 13 | 14 | def ln_wad(x): 15 | return int(math.log(x / WAD) * WAD) 16 | 17 | 18 | def full_mul_div(x, y, z): 19 | return (x * y) // z 20 | 21 | 22 | def price_at_curve(a, b, x): 23 | exp_b_x = exp_wad(mul_wad(b, x)) 24 | return mul_wad(a, exp_b_x) 25 | 26 | 27 | def getFundsNeeded(a, b, x0, deltaX): 28 | # Calculate exp(b * x0) and exp(b * x1) 29 | exp_b_x0 = exp_wad(mul_wad(b, x0)) 30 | exp_b_x1 = exp_wad(mul_wad(b, x0 + deltaX)) 31 | 32 | # Calculate deltaY = (a/b) * (e^(b * x1) - e^(b * x0)) 33 | delta = exp_b_x1 - exp_b_x0 34 | deltaY = full_mul_div(a, delta, b) 35 | 36 | return deltaY 37 | 38 | 39 | def getAmountOut(a, b, x0, deltaY): 40 | # Calculate exp(b * x0) 41 | exp_b_x0 = exp_wad(mul_wad(b, x0)) 42 | 43 | # Calculate exp(b * x0) + (dy*b/a) 44 | exp_b_x1 = exp_b_x0 + full_mul_div(deltaY, b, a) 45 | 46 | # Calculate ln(x1)/b - x0 47 | deltaX = full_mul_div(ln_wad(exp_b_x1), WAD, b) - x0 48 | 49 | return deltaX 50 | 51 | 52 | def get_total_amount(a, b, y): 53 | return getAmountOut(a, b, 0, y) 54 | 55 | 56 | def get_total_funds(a, b, x): 57 | return getFundsNeeded(a, b, 0, x) 58 | 59 | 60 | def calc_a_from_b(b, delta_x, delta_y): 61 | bx = mul_wad(b, delta_x) 62 | print(f"x: {bx/WAD}") 63 | exp_b_x = exp_wad(bx) 64 | print(f"exp_term: {exp_b_x/WAD}") 65 | a = full_mul_div(delta_y, b, exp_b_x - 1 * WAD) 66 | return a 67 | 68 | 69 | if __name__ == "__main__": 70 | b = 74866472 # 10^-9 71 | delta_z = 69_000_000_000 * WAD 72 | delta_x = delta_z * 4 / 5 73 | delta_y = 100_000 * WAD 74 | 75 | # b = 5000000000 # 10^-9 76 | # delta_z = 1_000_000_000 * WAD 77 | # delta_x = delta_z * 4 / 5 78 | # delta_y = 69_000 * WAD 79 | 80 | a = int(calc_a_from_b(b, delta_x, delta_y)) + 1 81 | print(f"b = {b}") 82 | print(f"a = {a}") 83 | 84 | all_y = getFundsNeeded(a, b, 0, delta_x) 85 | print(f"by all token, you need ETH: all_y = {all_y / WAD}") 86 | all_x = getAmountOut(a, b, 0, delta_y) 87 | print(f"use all ETH, you will get token: all_x = {int(all_x)/WAD}") 88 | 89 | # amounts = [] 90 | # step = 10_000_000 * WAD 91 | # for i in range(1, 80): 92 | # amount = getFundsNeeded(a, b, (i - 1) * step, step) / WAD 93 | # print(amount) 94 | # amounts.append(amount) 95 | 96 | # print(sum(amounts)) 97 | print("For each 5000 ETH, you will get token: ") 98 | count = 20 99 | amount = 0 100 | for i in range(1, count + 1): 101 | pre = amount 102 | amount += getAmountOut(a, b, amount, delta_y / count) 103 | price = delta_y / count / (amount - pre) 104 | print(delta_y / count / WAD, f"{round(amount / WAD, 2):.2f}", f"{(amount - pre) / WAD:14.2f}" , f" {price}") 105 | 106 | print("price when add liquidity : ", delta_y / (delta_z - delta_x)) 107 | print("price at curve end: ", price_at_curve(a, b, delta_x) / WAD) 108 | # print("Token needed for add liquidity(actually): ", delta_y / price_at_curve(a, b, delta_x)) 109 | -------------------------------------------------------------------------------- /bonding_curve_equation.py: -------------------------------------------------------------------------------- 1 | import math 2 | from scipy.optimize import fsolve 3 | 4 | # Given values 5 | # total = 69_000_000_000 6 | # delta_x = total * 4 / 5 7 | # delta_y = 100_000 8 | 9 | 10 | total = 1_000_000_000 11 | delta_x = total * 4 / 5 12 | delta_y = 69_000 13 | 14 | delta_C = total - delta_x 15 | C = delta_y / delta_C 16 | print("p for token =", C) 17 | 18 | 19 | # Define the system of equations 20 | def equations(vars): 21 | a, b = vars 22 | eq1 = C - a * math.exp(b * delta_x) # C = a * e^(b * delta_x) 23 | eq2 = delta_y - (a / b) * (math.exp(b * delta_x) - 1) # delta_y = (a/b) * (e^(b * delta_x) - 1) 24 | return [eq1, eq2] 25 | 26 | 27 | # Initial guess 28 | initial_guess = [1e-7, 1e-10] # Start with a small guess for a and b 29 | 30 | # Solve the system of equations 31 | a, b = fsolve(equations, initial_guess) 32 | print(a, b) 33 | print(a * 10**18, b * 10**18) 34 | 35 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 7 | 8 | [dependencies] 9 | "@openzeppelin-contracts" = { version = "5.0.2" } 10 | "@uniswap-v2-core" = { version = "1.0.1" } 11 | "@uniswap-v2-periphery" = { version = "1.1.0-beta.0" } 12 | "@openzeppelin-contracts-upgradeable" = { version = "5.0.2" } 13 | solady = { version = "0.0.233" } 14 | 15 | [rpc_endpoints] 16 | mainnet = "https://ethereum-sepolia-rpc.publicnode.com" 17 | local = "http://localhost:8545" -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiwihui/pumpeth/45329705c2008ea71388462db3119c8ee98f44d5/image.png -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin-contracts-5.0.2=dependencies/@openzeppelin-contracts-5.0.2 2 | @openzeppelin/contracts=dependencies/@openzeppelin-contracts-5.0.2 3 | @uniswap-v2-core-1.0.1=dependencies/@uniswap-v2-core-1.0.1 4 | @uniswap-v2-periphery-1.1.0-beta.0=dependencies/@uniswap-v2-periphery-1.1.0-beta.0 5 | @openzeppelin-contracts-upgradeable-5.0.2=dependencies/@openzeppelin-contracts-upgradeable-5.0.2 6 | @solady-0.0.233=dependencies/solady-0.0.233 -------------------------------------------------------------------------------- /script/TokenFactory.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console} from "forge-std/Script.sol"; 5 | import {TokenFactory} from "../src/TokenFactory.sol"; 6 | import {Token} from "../src/Token.sol"; 7 | import {BondingCurve} from "../src/BondingCurve.sol"; 8 | 9 | contract CounterScript is Script { 10 | function setUp() public {} 11 | 12 | function run() public { 13 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 14 | // sepolia 15 | address UNISWAP_V2_FACTORY = 0x7E0987E5b3a30e3f2828572Bb659A548460a3003; 16 | address UNISWAP_V2_ROUTER = 0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008; 17 | 18 | vm.startBroadcast(deployerPrivateKey); 19 | // BondingCurve bondingCurve = new BondingCurve(16319324419, 1000000000); // 20 eth 20 | 21 | new BondingCurve(505940703, 2000000000); // 1 eth 22 | new BondingCurve(646519142, 1500000000); // 1 eth 23 | BondingCurve bondingCurve = new BondingCurve(815966221, 1000000000); // 1 eth 24 | // deploy token impl 25 | Token tokenImplemetation = new Token(); 26 | new TokenFactory( 27 | address(tokenImplemetation), 28 | UNISWAP_V2_ROUTER, 29 | UNISWAP_V2_FACTORY, 30 | address(bondingCurve), 31 | 100 32 | ); 33 | vm.stopBroadcast(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "filter_paths": "lib,dependencies" 3 | } -------------------------------------------------------------------------------- /soldeer.lock: -------------------------------------------------------------------------------- 1 | 2 | [[dependencies]] 3 | name = "@openzeppelin-contracts" 4 | version = "5.0.2" 5 | source = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_0_2_14-03-2024_06:11:59_contracts.zip" 6 | checksum = "8bc4f0acc7c187771b878d46f7de4bfad1acad2eb5d096d9d05d34035853f5c3" 7 | 8 | [[dependencies]] 9 | name = "@uniswap-v2-core" 10 | version = "1.0.1" 11 | source = "https://soldeer-revisions.s3.amazonaws.com/@uniswap-v2-core/1_0_1_22-01-2024_13:18:30_v2-core.zip" 12 | checksum = "efebb89237048771c19f52d3ce87ec4d0c591279bf1977c49f9e70e5fff530f0" 13 | 14 | [[dependencies]] 15 | name = "@uniswap-v2-periphery" 16 | version = "1.1.0-beta.0" 17 | source = "https://soldeer-revisions.s3.amazonaws.com/@uniswap-v2-periphery/1_1_0-beta_0_22-01-2024_13:18:39_v2-periphery.zip" 18 | checksum = "3bdc6134f1a89bc72052acde4882eb165e27f220982f53b399d7a1121bf2ab06" 19 | 20 | [[dependencies]] 21 | name = "@openzeppelin-contracts-upgradeable" 22 | version = "5.0.2" 23 | source = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts-upgradeable/5_0_2_14-03-2024_06:12:07_contracts-upgradeable.zip" 24 | checksum = "fb3f8db8541fc01636f91b0e7d9dd6f450f1bf7e2b4a17e96caf6e779ace8f5b" 25 | 26 | [[dependencies]] 27 | name = "solady" 28 | version = "0.0.233" 29 | source = "https://soldeer-revisions.s3.amazonaws.com/solady/0_0_233_18-08-2024_08:20:28_solady.zip" 30 | checksum = "dfae622e1c1dc9efcf7f81d1a70b9eba1b914733bef3972d170affab948bfd45" 31 | -------------------------------------------------------------------------------- /src/BondingCurve.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {FixedPointMathLib} from "@solady-0.0.233/src/utils/FixedPointMathLib.sol"; 5 | 6 | contract BondingCurve { 7 | using FixedPointMathLib for uint256; 8 | using FixedPointMathLib for int256; 9 | 10 | uint256 public immutable A; 11 | uint256 public immutable B; 12 | 13 | constructor(uint256 _a, uint256 _b) { 14 | A = _a; 15 | B = _b; 16 | } 17 | 18 | // calculate the funds received for selling deltaX tokens 19 | function getFundsReceived( 20 | uint256 x0, 21 | uint256 deltaX 22 | ) public view returns (uint256 deltaY) { 23 | uint256 a = A; 24 | uint256 b = B; 25 | require(x0 >= deltaX); 26 | // calculate exp(b*x0), exp(b*x1) 27 | int256 exp_b_x0 = (int256(b.mulWad(x0))).expWad(); 28 | int256 exp_b_x1 = (int256(b.mulWad(x0 - deltaX))).expWad(); 29 | 30 | // calculate deltaY = (a/b)*(exp(b*x0) - exp(b*x1)) 31 | uint256 delta = uint256(exp_b_x0 - exp_b_x1); 32 | deltaY = a.fullMulDiv(delta, b); 33 | } 34 | 35 | // calculte the number of tokens that can be purchased for a given amount of funds 36 | function getAmountOut( 37 | uint256 x0, 38 | uint256 deltaY 39 | ) public view returns (uint256 deltaX) { 40 | uint256 a = A; 41 | uint256 b = B; 42 | // calculate exp(b*x0) 43 | uint256 exp_b_x0 = uint256((int256(b.mulWad(x0))).expWad()); 44 | 45 | // calculate exp(b*x0) + (dy*b/a) 46 | uint256 exp_b_x1 = exp_b_x0 + deltaY.fullMulDiv(b, a); 47 | 48 | // calculate ln(x1)/b-x0 49 | deltaX = uint256(int256(exp_b_x1).lnWad()).divWad(b) - x0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ConstantBondingCurve.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | contract ConstantBondingCurve { 5 | uint256 public constant FUNDING_SUPPLY = 800_000_000 ether; 6 | uint256 public constant FUNDING_GOAL = 20 ether; 7 | 8 | function calculateBuyReturn( 9 | uint256 ethAmount 10 | ) public pure returns (uint256) { 11 | return (ethAmount * FUNDING_SUPPLY) / FUNDING_GOAL; 12 | } 13 | 14 | function calculateSellReturn( 15 | uint256 tokenAmount 16 | ) public pure returns (uint256) { 17 | return (tokenAmount * FUNDING_GOAL) / FUNDING_SUPPLY; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | import {ERC20Upgradeable} from "@openzeppelin-contracts-upgradeable-5.0.2/token/ERC20/ERC20Upgradeable.sol"; 4 | import {OwnableUpgradeable} from "@openzeppelin-contracts-upgradeable-5.0.2/access/OwnableUpgradeable.sol"; 5 | 6 | contract Token is ERC20Upgradeable, OwnableUpgradeable { 7 | function initialize( 8 | string memory name, 9 | string memory symbol 10 | ) public initializer { 11 | __ERC20_init(name, symbol); 12 | __Ownable_init(msg.sender); 13 | } 14 | 15 | function mint(address to, uint256 amount) public onlyOwner { 16 | _mint(to, amount); 17 | } 18 | 19 | function burn(address to, uint256 amount) public onlyOwner { 20 | _burn(to, amount); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/TokenFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {IUniswapV2Factory} from "@uniswap-v2-core-1.0.1/contracts/interfaces/IUniswapV2Factory.sol"; 5 | import {IUniswapV2Router01} from "@uniswap-v2-periphery-1.1.0-beta.0/contracts/interfaces/IUniswapV2Router01.sol"; 6 | import {Clones} from "@openzeppelin-contracts-5.0.2/proxy/Clones.sol"; 7 | import {ReentrancyGuard} from "@openzeppelin-contracts-5.0.2/utils/ReentrancyGuard.sol"; 8 | import "@openzeppelin-contracts-5.0.2/token/ERC20/utils/SafeERC20.sol"; 9 | import "@openzeppelin-contracts-5.0.2/access/Ownable.sol"; 10 | 11 | import {BondingCurve} from "./BondingCurve.sol"; 12 | import {Token} from "./Token.sol"; 13 | 14 | contract TokenFactory is ReentrancyGuard, Ownable { 15 | enum TokenState { 16 | NOT_CREATED, 17 | FUNDING, 18 | TRADING 19 | } 20 | uint256 public constant MAX_SUPPLY = 10 ** 9 * 1 ether; // 1 Billion 21 | uint256 public constant INITIAL_SUPPLY = (MAX_SUPPLY * 1) / 5; 22 | uint256 public constant FUNDING_SUPPLY = (MAX_SUPPLY * 4) / 5; 23 | uint256 public constant FUNDING_GOAL = 20 ether; 24 | uint256 public constant FEE_DENOMINATOR = 10000; 25 | 26 | mapping(address => TokenState) public tokens; 27 | mapping(address => uint256) public collateral; 28 | address public immutable tokenImplementation; 29 | address public uniswapV2Router; 30 | address public uniswapV2Factory; 31 | BondingCurve public bondingCurve; 32 | uint256 public feePercent; // bp 33 | uint256 public fee; 34 | 35 | // Events 36 | event TokenCreated(address indexed token, uint256 timestamp); 37 | event TokenLiqudityAdded(address indexed token, uint256 timestamp); 38 | 39 | constructor( 40 | address _tokenImplementation, 41 | address _uniswapV2Router, 42 | address _uniswapV2Factory, 43 | address _bondingCurve, 44 | uint256 _feePercent 45 | ) Ownable(msg.sender) { 46 | tokenImplementation = _tokenImplementation; 47 | uniswapV2Router = _uniswapV2Router; 48 | uniswapV2Factory = _uniswapV2Factory; 49 | bondingCurve = BondingCurve(_bondingCurve); 50 | feePercent = _feePercent; 51 | } 52 | 53 | // Admin functions 54 | 55 | function setBondingCurve(address _bondingCurve) external onlyOwner { 56 | bondingCurve = BondingCurve(_bondingCurve); 57 | } 58 | 59 | function setFeePercent(uint256 _feePercent) external onlyOwner { 60 | feePercent = _feePercent; 61 | } 62 | 63 | function claimFee() external onlyOwner { 64 | (bool success, ) = msg.sender.call{value: fee}(new bytes(0)); 65 | require(success, "ETH send failed"); 66 | fee = 0; 67 | } 68 | 69 | // Token functions 70 | 71 | function createToken( 72 | string memory name, 73 | string memory symbol 74 | ) external returns (address) { 75 | address tokenAddress = Clones.clone(tokenImplementation); 76 | Token token = Token(tokenAddress); 77 | token.initialize(name, symbol); 78 | tokens[tokenAddress] = TokenState.FUNDING; 79 | emit TokenCreated(tokenAddress, block.timestamp); 80 | return tokenAddress; 81 | } 82 | 83 | function buy(address tokenAddress) external payable nonReentrant { 84 | require(tokens[tokenAddress] == TokenState.FUNDING, "Token not found"); 85 | require(msg.value > 0, "ETH not enough"); 86 | // calculate fee 87 | uint256 valueToBuy = msg.value; 88 | uint256 valueToReturn; 89 | uint256 tokenCollateral = collateral[tokenAddress]; 90 | 91 | uint256 remainingEthNeeded = FUNDING_GOAL - tokenCollateral; 92 | uint256 contributionWithoutFee = valueToBuy * FEE_DENOMINATOR / (FEE_DENOMINATOR + feePercent); 93 | if (contributionWithoutFee > remainingEthNeeded) { 94 | contributionWithoutFee = remainingEthNeeded; 95 | } 96 | uint256 _fee = calculateFee(contributionWithoutFee, feePercent); 97 | uint256 totalCharged = contributionWithoutFee + _fee; 98 | valueToReturn = valueToBuy > totalCharged ? valueToBuy - totalCharged : 0; 99 | fee += _fee; 100 | Token token = Token(tokenAddress); 101 | uint256 amount = bondingCurve.getAmountOut( 102 | token.totalSupply(), 103 | contributionWithoutFee 104 | ); 105 | uint256 availableSupply = FUNDING_SUPPLY - token.totalSupply(); 106 | require(amount <= availableSupply, "Token supply not enough"); 107 | tokenCollateral += contributionWithoutFee; 108 | token.mint(msg.sender, amount); 109 | // when reached FUNDING_GOAL 110 | if (tokenCollateral >= FUNDING_GOAL) { 111 | token.mint(address(this), INITIAL_SUPPLY); 112 | address pair = createLiquilityPool(tokenAddress); 113 | uint256 liquidity = addLiquidity( 114 | tokenAddress, 115 | INITIAL_SUPPLY, 116 | tokenCollateral 117 | ); 118 | burnLiquidityToken(pair, liquidity); 119 | tokenCollateral = 0; 120 | tokens[tokenAddress] = TokenState.TRADING; 121 | emit TokenLiqudityAdded(tokenAddress, block.timestamp); 122 | } 123 | collateral[tokenAddress] = tokenCollateral; 124 | // return left 125 | if (valueToReturn > 0) { 126 | (bool success, ) = msg.sender.call{value: msg.value - valueToBuy}( 127 | new bytes(0) 128 | ); 129 | require(success, "ETH send failed"); 130 | } 131 | } 132 | 133 | function sell(address tokenAddress, uint256 amount) external nonReentrant { 134 | require( 135 | tokens[tokenAddress] == TokenState.FUNDING, 136 | "Token is not funding" 137 | ); 138 | require(amount > 0, "Amount should be greater than zero"); 139 | Token token = Token(tokenAddress); 140 | uint256 receivedETH = bondingCurve.getFundsReceived( 141 | token.totalSupply(), 142 | amount 143 | ); 144 | // calculate fee 145 | uint256 _fee = calculateFee(receivedETH, feePercent); 146 | receivedETH -= _fee; 147 | fee += _fee; 148 | token.burn(msg.sender, amount); 149 | collateral[tokenAddress] -= receivedETH; 150 | // send ether 151 | //slither-disable-next-line arbitrary-send-eth 152 | (bool success, ) = msg.sender.call{value: receivedETH}(new bytes(0)); 153 | require(success, "ETH send failed"); 154 | } 155 | 156 | // Internal functions 157 | 158 | function createLiquilityPool( 159 | address tokenAddress 160 | ) internal returns (address) { 161 | IUniswapV2Factory factory = IUniswapV2Factory(uniswapV2Factory); 162 | IUniswapV2Router01 router = IUniswapV2Router01(uniswapV2Router); 163 | 164 | address pair = factory.createPair(tokenAddress, router.WETH()); 165 | return pair; 166 | } 167 | 168 | function addLiquidity( 169 | address tokenAddress, 170 | uint256 tokenAmount, 171 | uint256 ethAmount 172 | ) internal returns (uint256) { 173 | Token token = Token(tokenAddress); 174 | IUniswapV2Router01 router = IUniswapV2Router01(uniswapV2Router); 175 | token.approve(uniswapV2Router, tokenAmount); 176 | //slither-disable-next-line arbitrary-send-eth 177 | (, , uint256 liquidity) = router.addLiquidityETH{value: ethAmount}( 178 | tokenAddress, 179 | tokenAmount, 180 | tokenAmount, 181 | ethAmount, 182 | address(this), 183 | block.timestamp 184 | ); 185 | return liquidity; 186 | } 187 | 188 | function burnLiquidityToken(address pair, uint256 liquidity) internal { 189 | SafeERC20.safeTransfer(IERC20(pair), address(0), liquidity); 190 | } 191 | 192 | function calculateFee( 193 | uint256 _amount, 194 | uint256 _feePercent 195 | ) internal pure returns (uint256) { 196 | return (_amount * _feePercent) / FEE_DENOMINATOR; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /test/BondingCurve.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {BondingCurve} from "../src/BondingCurve.sol"; 6 | 7 | contract BondingCurveTest is Test { 8 | BondingCurve bondingCurve; 9 | 10 | function setUp() public { 11 | bondingCurve = new BondingCurve(16319324419, 1000000000); 12 | } 13 | 14 | function test_getFundsReceived() public { 15 | uint256 x0 = 800_000_000 ether; 16 | uint256 deltaX = 800_000_000 ether; 17 | uint256 factor = 40_000_000 ether; 18 | 19 | uint256 fundsReceived_1 = bondingCurve.getFundsReceived(x0, 1); 20 | console.logUint(fundsReceived_1); 21 | 22 | uint256 expectedFundsNeeded = 20 ether; 23 | uint256 fundsReceived = bondingCurve.getFundsReceived(x0, deltaX); 24 | assertGe( 25 | fundsReceived, 26 | expectedFundsNeeded, 27 | "Funds needed calculation is incorrect" 28 | ); 29 | assertLe( 30 | fundsReceived, 31 | expectedFundsNeeded + 0.01 ether, 32 | "Funds needed calculation is incorrect" 33 | ); 34 | // buy 1 ETH per step 35 | uint256 amount = 0; 36 | uint256 totalAmount = 0; 37 | for (uint256 i = 20; i >= 1; i--) { 38 | amount = bondingCurve.getFundsReceived(i * factor, factor); 39 | totalAmount += amount; 40 | console.logUint(amount); 41 | } 42 | assertLe( 43 | fundsReceived - totalAmount, 44 | 1000, 45 | "Total amount calculation is incorrect" 46 | ); 47 | } 48 | 49 | function test_getAmountOut() public { 50 | uint256 x0 = 0; 51 | uint256 deltaY = 20 ether; 52 | 53 | uint256 amountOut_1 = bondingCurve.getAmountOut(x0, 1); 54 | console.logUint(amountOut_1); 55 | 56 | uint256 expectedAmountOut = 800000000 ether; 57 | uint256 amountOut = bondingCurve.getAmountOut(x0, deltaY); 58 | assertLe( 59 | amountOut, 60 | expectedAmountOut, 61 | "Amount out calculation is incorrect" 62 | ); 63 | assertGe( 64 | amountOut, 65 | expectedAmountOut - 1 ether, 66 | "Amount out calculation is incorrect" 67 | ); 68 | 69 | // buy 1 ETH per step 70 | uint256 amount = 0; 71 | uint256 totalAmount = 0; 72 | for (uint256 i = 1; i <= 20; i++) { 73 | amount = bondingCurve.getAmountOut(totalAmount, 1 ether); 74 | totalAmount += amount; 75 | console.logUint(amount); 76 | } 77 | assertLe( 78 | amountOut - totalAmount, 79 | 0.000001 ether, 80 | "Total amount calculation is incorrect" 81 | ); 82 | } 83 | 84 | function test_getInOut() public { 85 | uint256 x0 = 0; 86 | uint256 amountOut = bondingCurve.getAmountOut(x0, 1 ether); 87 | 88 | uint256 fundsReceived = bondingCurve.getFundsReceived( 89 | amountOut, 90 | amountOut 91 | ); 92 | assertLe(fundsReceived, 1 ether); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/ConstantBondingCurve.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {ConstantBondingCurve} from "../src/ConstantBondingCurve.sol"; 6 | 7 | contract ConstantBondingCurveTest is Test { 8 | ConstantBondingCurve bondingCurve; 9 | 10 | function setUp() public { 11 | bondingCurve = new ConstantBondingCurve(); 12 | } 13 | 14 | function test_CalculateBuyReturn() public { 15 | // 1 ether, 0.8B / 20 = 40M 16 | uint256 tokenAmountBuyed = bondingCurve.calculateBuyReturn(1 ether); 17 | assert(tokenAmountBuyed == 40_000_000 ether); 18 | uint256 tokenAmountAllBuyed = bondingCurve.calculateBuyReturn(20 ether); 19 | assert(tokenAmountAllBuyed == 0.8 * 10 ** 9 * 1 ether); 20 | } 21 | 22 | function test_CalculateSellReturn() public { 23 | // 1 ether, 0.8B / 20 = 40M 24 | uint256 ethAmountReceived = bondingCurve.calculateSellReturn( 25 | 40_000_000 ether 26 | ); 27 | assert(ethAmountReceived == 1 ether); 28 | uint256 ethAmountAllReceived = bondingCurve.calculateSellReturn( 29 | 0.8 * 10 ** 9 * 1 ether 30 | ); 31 | assert(ethAmountAllReceived == 20 ether); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/TokenFactory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {IUniswapV2Factory} from "@uniswap-v2-core-1.0.1/contracts/interfaces/IUniswapV2Factory.sol"; 6 | import {IUniswapV2Pair} from "@uniswap-v2-core-1.0.1/contracts/interfaces/IUniswapV2Pair.sol"; 7 | import {IUniswapV2Router01} from "@uniswap-v2-periphery-1.1.0-beta.0/contracts/interfaces/IUniswapV2Router01.sol"; 8 | import {TokenFactory} from "../src/TokenFactory.sol"; 9 | import {Token} from "../src/Token.sol"; 10 | import {BondingCurve} from "../src/BondingCurve.sol"; 11 | 12 | contract TokenFactoryTest is Test { 13 | TokenFactory public factory; 14 | IUniswapV2Factory uniswapFactory; 15 | IUniswapV2Router01 router; 16 | Token public tokenImplemetation; 17 | BondingCurve public bondingCurve; 18 | uint256 feePercent = 100; 19 | 20 | address public constant UNISWAP_V2_FACTORY = 21 | 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; 22 | address public constant UNISWAP_V2_ROUTER = 23 | 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; 24 | 25 | function setUp() public { 26 | tokenImplemetation = new Token(); 27 | bondingCurve = new BondingCurve(16319324419, 1000000000); 28 | factory = new TokenFactory( 29 | address(tokenImplemetation), 30 | UNISWAP_V2_ROUTER, 31 | UNISWAP_V2_FACTORY, 32 | address(bondingCurve), 33 | feePercent 34 | ); 35 | uniswapFactory = IUniswapV2Factory(factory.uniswapV2Factory()); 36 | router = IUniswapV2Router01(factory.uniswapV2Router()); 37 | } 38 | 39 | function test_CreateToken() public { 40 | vm.expectEmit(false, false, false, false, address(factory)); 41 | emit TokenFactory.TokenCreated(address(1), 1); 42 | address tokenAddress = factory.createToken("MyFirstToken", "MFT"); 43 | assert(tokenAddress != address(0)); 44 | assert(factory.tokens(tokenAddress) == TokenFactory.TokenState.FUNDING); 45 | 46 | Token token = Token(tokenAddress); 47 | assertEq(token.name(), "MyFirstToken"); 48 | assertEq(token.symbol(), "MFT"); 49 | assertEq(token.owner(), address(factory)); 50 | } 51 | 52 | function test_Buy() public { 53 | address tokenAddress = factory.createToken("MyFirstToken", "MFT"); 54 | Token token = Token(tokenAddress); 55 | 56 | address alice = makeAddr("alice"); 57 | vm.deal(alice, 30 ether); 58 | 59 | vm.startPrank(alice); 60 | factory.buy{value: 1 ether}(tokenAddress); 61 | assert(token.balanceOf(alice) > 0); 62 | factory.buy{value: 18 ether}(tokenAddress); 63 | assert( 64 | (19 ether * feePercent) / 65 | (factory.FEE_DENOMINATOR() + feePercent) - 66 | factory.fee() < 67 | 1000 68 | ); 69 | vm.stopPrank(); 70 | } 71 | 72 | function test_Sell() public { 73 | address alice = makeAddr("alice"); 74 | vm.deal(alice, 30 ether); 75 | address tokenAddress = factory.createToken("MyFirstToken", "MFT"); 76 | Token token = Token(tokenAddress); 77 | 78 | vm.startPrank(alice); 79 | factory.buy{value: 1 ether}(tokenAddress); 80 | factory.sell(tokenAddress, 1_000_000); 81 | factory.sell(tokenAddress, 1_000_000); 82 | assert(token.balanceOf(alice) == 58901107293165283998000000); 83 | vm.stopPrank(); 84 | } 85 | 86 | function test_claimFee() public { 87 | address tokenAddress = factory.createToken("MyFirstToken", "MFT"); 88 | 89 | address alice = makeAddr("alice"); 90 | vm.deal(alice, 30 ether); 91 | 92 | vm.startPrank(alice); 93 | factory.buy{value: 1 ether}(tokenAddress); 94 | 95 | vm.expectRevert( 96 | abi.encodeWithSignature( 97 | "OwnableUnauthorizedAccount(address)", 98 | alice 99 | ) 100 | ); 101 | factory.claimFee(); 102 | assert( 103 | (1 ether * feePercent) / 104 | (factory.FEE_DENOMINATOR() + feePercent) - 105 | factory.fee() < 106 | 1000 107 | ); 108 | vm.stopPrank(); 109 | factory.claimFee(); 110 | assert(factory.fee() == 0); 111 | } 112 | 113 | receive() external payable {} 114 | } 115 | -------------------------------------------------------------------------------- /test/TokenFactoryForked.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | 6 | import {IUniswapV2Factory} from "@uniswap-v2-core-1.0.1/contracts/interfaces/IUniswapV2Factory.sol"; 7 | import {IUniswapV2Pair} from "@uniswap-v2-core-1.0.1/contracts/interfaces/IUniswapV2Pair.sol"; 8 | import {IUniswapV2Router01} from "@uniswap-v2-periphery-1.1.0-beta.0/contracts/interfaces/IUniswapV2Router01.sol"; 9 | import {FixedPointMathLib} from "@solady-0.0.233/src/utils/FixedPointMathLib.sol"; 10 | import {TokenFactory} from "../src/TokenFactory.sol"; 11 | import {Token} from "../src/Token.sol"; 12 | import {BondingCurve} from "../src/BondingCurve.sol"; 13 | 14 | contract TokenFactoryForkedTest is Test { 15 | TokenFactory public factory; 16 | uint256 mainnetFork; 17 | IUniswapV2Factory uniswapFactory; 18 | IUniswapV2Router01 router; 19 | Token public tokenImplemetation; 20 | BondingCurve public bondingCurve; 21 | 22 | address public constant UNISWAP_V2_FACTORY = 23 | 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; 24 | address public constant UNISWAP_V2_ROUTER = 25 | 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; 26 | 27 | function setUp() public { 28 | mainnetFork = vm.createSelectFork("mainnet"); 29 | bondingCurve = new BondingCurve(16319324419, 1000000000); 30 | tokenImplemetation = new Token(); 31 | factory = new TokenFactory( 32 | address(tokenImplemetation), 33 | UNISWAP_V2_ROUTER, 34 | UNISWAP_V2_FACTORY, 35 | address(bondingCurve), 36 | 100 37 | ); 38 | uniswapFactory = IUniswapV2Factory(factory.uniswapV2Factory()); 39 | router = IUniswapV2Router01(factory.uniswapV2Router()); 40 | } 41 | 42 | function test_ForkedBuy() public { 43 | vm.selectFork(mainnetFork); 44 | assertEq(vm.activeFork(), mainnetFork); 45 | 46 | address alice = makeAddr("alice"); 47 | vm.deal(alice, 30 ether); 48 | address tokenAddress = factory.createToken("MyFirstToken", "MFT"); 49 | Token token = Token(tokenAddress); 50 | 51 | vm.startPrank(alice); 52 | factory.buy{value: 1 ether}(tokenAddress); 53 | assert(token.balanceOf(alice) == 58895387276865135000000000); 54 | vm.expectEmit(true, false, false, false); 55 | emit TokenFactory.TokenLiqudityAdded(tokenAddress, 1); 56 | factory.buy{value: 25 ether}(tokenAddress); 57 | assert(token.balanceOf(alice) == 799999999977117981000000000); 58 | assert(address(alice).balance > 9 ether); // substract some gas 59 | 60 | assert(factory.tokens(tokenAddress) == TokenFactory.TokenState.TRADING); 61 | assert(factory.collateral(tokenAddress) == 0); 62 | 63 | // check of pair exists 64 | assert( 65 | uniswapFactory.getPair(tokenAddress, router.WETH()) != address(0) 66 | ); 67 | // check liquity 68 | address poolAddress = uniswapFactory.getPair( 69 | tokenAddress, 70 | router.WETH() 71 | ); 72 | IUniswapV2Pair pool = IUniswapV2Pair(poolAddress); 73 | assert( 74 | pool.balanceOf(address(0)) == 75 | FixedPointMathLib.sqrt(20 ether * 200000000 ether) 76 | ); // sqrt(20ether * 200M ether) 77 | assert(pool.balanceOf(address(factory)) == 0); 78 | vm.stopPrank(); 79 | } 80 | 81 | function test_ForkedSell() public { 82 | vm.selectFork(mainnetFork); 83 | assertEq(vm.activeFork(), mainnetFork); 84 | 85 | address alice = makeAddr("alice"); 86 | vm.deal(alice, 30 ether); 87 | address tokenAddress = factory.createToken("MyFirstToken", "MFT"); 88 | 89 | vm.startPrank(alice); 90 | 91 | factory.buy{value: 1 ether}(tokenAddress); 92 | factory.sell(tokenAddress, 58901107293165284000000000); 93 | 94 | factory.buy{value: 25 ether}(tokenAddress); 95 | factory.sell(tokenAddress, 100); 96 | 97 | // all buyed, revert when selling 98 | factory.buy{value: 1 ether}(tokenAddress); 99 | vm.expectRevert(); 100 | factory.sell(tokenAddress, 40_000_000 ether); 101 | vm.stopPrank(); 102 | } 103 | 104 | function test_ForkedSellEmpty() public { 105 | vm.selectFork(mainnetFork); 106 | assertEq(vm.activeFork(), mainnetFork); 107 | 108 | address alice = makeAddr("bob"); 109 | vm.deal(alice, 30 ether); 110 | address tokenAddress = factory.createToken("MyFirstToken", "MFT"); 111 | 112 | vm.startPrank(alice); 113 | 114 | vm.expectRevert(); 115 | factory.sell(tokenAddress, 1); 116 | 117 | vm.stopPrank(); 118 | } 119 | } 120 | --------------------------------------------------------------------------------