// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.26; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {FledgeCoin} from "./FledgeCoin.sol"; interface IUniswapV3FactoryMin { function getPool(address a, address b, uint24 fee) external view returns (address); function createPool(address a, address b, uint24 fee) external returns (address); } interface IUniswapV3PoolMin { function liquidity() external view returns (uint128); function observe(uint32[] calldata secondsAgos) external view returns (int56[] memory, uint160[] memory); } /// @title FledgeFactory /// @notice Launches FledgeCoins. The launcher names the coin, picks the stock its nest buys and the curve's /// trade fee (0.25%–10%); none of it can be changed afterwards, by anyone. No owner, no fee to us. /// /// Each launch also creates the coin's own 1% Uniswap v3 pool against WETH, empty: the coin moves into it when /// its curve sells out. /// /// 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 FledgeFactory { uint256 public constant VIRTUAL_ETH = 1 ether; uint16 public constant MIN_FEE_BPS = 25; uint16 public constant MAX_FEE_BPS = 1000; /// @notice The picture, description and links together, stored as contract code. uint256 public constant MAX_META = 24_000; address public immutable implementation; address public immutable weth; address public immutable usdg; IUniswapV3FactoryMin public immutable v3Factory; address public immutable ethPool; uint24 public immutable ethPoolFee; address[] public coins; mapping(address => bool) public isCoin; event Launched( address indexed coin, address indexed creator, address indexed stock, uint24 stockPoolFee, uint16 feeBps, string name, string symbol, address pool ); error BadFee(); error BadPoolFee(); error NoPool(); error NoPrice(); error BadName(); error BadMeta(); error BadStock(); error EthTransferFailed(); constructor(address router, address weth_, address usdg_, address v3Factory_, uint24 ethPoolFee_) { weth = weth_; usdg = usdg_; v3Factory = IUniswapV3FactoryMin(v3Factory_); ethPoolFee = ethPoolFee_; address p = IUniswapV3FactoryMin(v3Factory_).getPool(weth_, usdg_, ethPoolFee_); if (p == address(0)) revert NoPool(); ethPool = p; implementation = address(new FledgeCoin(router, weth_, usdg_, p, ethPoolFee_, address(this))); } /// @param meta ABI-encoded (string image, string description, string website, string x, string telegram) /// @param stockPoolFee the Uniswap fee tier of the USDG/stock pool the nest buys through /// @param minCoinsOut for the launcher's own first buy (msg.value), if any function launch( string calldata name, string calldata symbol, bytes calldata meta, address stock, uint24 stockPoolFee, uint16 feeBps, uint256 minCoinsOut ) external payable returns (address coin) { if (feeBps < MIN_FEE_BPS || feeBps > MAX_FEE_BPS) revert BadFee(); 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 (stock == weth || stock == usdg || stock.code.length == 0) revert BadStock(); address sp = v3Factory.getPool(usdg, stock, stockPoolFee); if (sp == address(0) || IUniswapV3PoolMin(sp).liquidity() == 0) revert NoPool(); // The nest only buys at an average price; a pool that cannot give one would never buy. uint32[] memory ago = new uint32[](2); ago[0] = 120; try IUniswapV3PoolMin(sp).observe(ago) returns (int56[] memory, uint160[] memory) {} catch { revert NoPrice(); } address pointer; if (meta.length > 0) pointer = _store(meta); coin = Clones.clone(implementation); // A clone's address is predictable, so somebody may have created its pool already: use it if so. address pool = v3Factory.getPool(coin, weth, 10_000); if (pool == address(0)) pool = v3Factory.createPool(coin, weth, 10_000); FledgeCoin(payable(coin)).initialize( name, symbol, pointer, msg.sender, stock, sp, stockPoolFee, feeBps, VIRTUAL_ETH, pool ); coins.push(coin); isCoin[coin] = true; emit Launched(coin, msg.sender, stock, stockPoolFee, feeBps, name, symbol, pool); if (msg.value > 0) { FledgeCoin(payable(coin)).buy{value: msg.value}(minCoinsOut, msg.sender); // a first buy that takes the whole curve is refunded what it did not need — to us; pass it on uint256 back = address(this).balance; if (back > 0) { (bool ok,) = msg.sender.call{value: back}(""); if (!ok) revert EthTransferFailed(); } } } /// @dev Only a coin refunding a launcher's first buy sends ETH here. receive() external payable { if (!isCoin[msg.sender]) revert(); } /// @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]; } }