// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.26; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {KeelCoin} from "./KeelCoin.sol"; interface IUniswapV3FactoryMin { function getPool(address a, address b, uint24 fee) external view returns (address); } interface IUniswapV3PoolMin { function liquidity() external view returns (uint128); } /// @title KeelFactory /// @notice Launches KeelCoins. The launcher names the coin, picks the stock its whole market is held in, and the /// starting price (as a number of virtual shares behind the curve). None of it can be changed afterwards, by anyone. /// No owner, no fee to us. /// /// This factory is deployed through the deterministic CREATE2 deployer, so its address is fixed by its code: /// whoever sends that one transaction first deploys exactly this contract, and nobody holds a key to it. contract KeelFactory { /// @notice The picture, description and links together, stored as contract code. uint256 public constant MAX_META = 24_000; /// @notice Bounds on the virtual shares a curve starts with (raw stock units; a share is ~1e18). uint256 public constant MIN_VIRTUAL = 1e15; uint256 public constant MAX_VIRTUAL = 1e24; address public immutable implementation; address public immutable weth; address public immutable usdg; IUniswapV3FactoryMin public immutable v3Factory; address[] public coins; mapping(address => bool) public isCoin; event Launched( address indexed coin, address indexed creator, address indexed stock, uint24 stockPoolFee, uint256 virtualStock, string name, string symbol ); error BadPoolFee(); error NoPool(); error BadName(); error BadMeta(); error BadStock(); error BadVirtual(); constructor(address router, address weth_, address usdg_, address v3Factory_) { weth = weth_; usdg = usdg_; v3Factory = IUniswapV3FactoryMin(v3Factory_); implementation = address(new KeelCoin(router, weth_, address(this))); } /// @param meta ABI-encoded (string image, string description, string website, string x, string telegram) /// @param stock the stock token the coin's market is held in /// @param stockPoolFee the fee tier of a live USDG/stock Uniswap v3 pool (the page's default route) /// @param virtualStock shares behind the curve at the start: the coin's starting value, in the stock /// @param path for the launcher's own first buy (msg.value), a v3 path from WETH to the stock /// @param minCoinsOut for that first buy function launch( string calldata name, string calldata symbol, bytes calldata meta, address stock, uint24 stockPoolFee, uint256 virtualStock, bytes calldata path, uint256 minCoinsOut ) external payable returns (address coin) { if (stockPoolFee != 500 && stockPoolFee != 3000 && stockPoolFee != 10000) revert BadPoolFee(); if (bytes(name).length == 0 || bytes(name).length > 48) revert BadName(); if (bytes(symbol).length == 0 || bytes(symbol).length > 12) revert BadName(); if (meta.length > MAX_META) revert BadMeta(); if (virtualStock < MIN_VIRTUAL || virtualStock > MAX_VIRTUAL) revert BadVirtual(); if (stock == weth || stock == usdg || stock.code.length == 0 || isCoin[stock]) revert BadStock(); // A stock with no live USDG pool cannot be bought into or sold out of with dollars or ETH. address sp = v3Factory.getPool(usdg, stock, stockPoolFee); if (sp == address(0) || IUniswapV3PoolMin(sp).liquidity() == 0) revert NoPool(); address pointer; if (meta.length > 0) pointer = _store(meta); coin = Clones.clone(implementation); KeelCoin(payable(coin)).initialize(name, symbol, pointer, msg.sender, stock, stockPoolFee, virtualStock); coins.push(coin); isCoin[coin] = true; emit Launched(coin, msg.sender, stock, stockPoolFee, virtualStock, name, symbol); if (msg.value > 0) { KeelCoin(payable(coin)).buy{value: msg.value}(path, msg.value, minCoinsOut, msg.sender); } } /// @dev SSTORE2: the bytes become the code of a new contract behind a STOP, so nobody can call it. function _store(bytes calldata data) internal returns (address pointer) { bytes memory init = abi.encodePacked(hex"61", uint16(data.length + 1), hex"80600a3d393df300", data); assembly ("memory-safe") { pointer := create(0, add(init, 32), mload(init)) } if (pointer == address(0)) revert BadMeta(); } function coinCount() external view returns (uint256) { return coins.length; } /// @notice Newest first: `n` coins starting `offset` from the newest. function recent(uint256 offset, uint256 n) external view returns (address[] memory out) { uint256 len = coins.length; if (offset >= len) return out; uint256 m = len - offset < n ? len - offset : n; out = new address[](m); for (uint256 i; i < m; i++) out[i] = coins[len - 1 - offset - i]; } }