// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.26; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {DivvyCoin} from "./DivvyCoin.sol"; interface IUniswapV3FactoryMin { function getPool(address a, address b, uint24 fee) external view returns (address); } interface IUniswapV3PoolMin { function liquidity() external view returns (uint128); function observe(uint32[] calldata secondsAgos) external view returns (int56[] memory, uint160[] memory); } /// @title DivvyFactory /// @notice Launches DivvyCoins. The launcher names the coin, picks the stock it pays its holders in, and /// the trade fee (0.25%–10%); 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 DivvyFactory { 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, uint16 feeBps, string name, string symbol ); error BadFee(); error BadPoolFee(); error NoPool(); error NoPrice(); error BadName(); error BadMeta(); error BadStock(); 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 DivvyCoin(router, weth_, usdg_, p, ethPoolFee_, address(this))); } /// @param meta ABI-encoded (string image, string description, string website, string x, string telegram) /// @param stock the tokenized stock the fees buy /// @param poolFee the Uniswap fee tier of the USDG/stock pool it is bought 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 poolFee, uint16 feeBps, uint256 minCoinsOut ) external payable returns (address coin) { if (feeBps < MIN_FEE_BPS || feeBps > MAX_FEE_BPS) revert BadFee(); 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(); address pool = _checkPool(stock, poolFee); address pointer; if (meta.length > 0) pointer = _store(meta); coin = Clones.clone(implementation); DivvyCoin(payable(coin)).initialize( name, symbol, pointer, msg.sender, stock, pool, poolFee, feeBps, VIRTUAL_ETH ); coins.push(coin); isCoin[coin] = true; emit Launched(coin, msg.sender, stock, feeBps, name, symbol); if (msg.value > 0) DivvyCoin(payable(coin)).buy{value: msg.value}(minCoinsOut, msg.sender); } function _checkPool(address stock, uint24 poolFee) internal view returns (address pool) { if (poolFee != 500 && poolFee != 3000 && poolFee != 10000) revert BadPoolFee(); if (stock == weth || stock == usdg || stock.code.length == 0) revert BadStock(); pool = v3Factory.getPool(usdg, stock, poolFee); if (pool == address(0) || IUniswapV3PoolMin(pool).liquidity() == 0) revert NoPool(); // The coin 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(pool).observe(ago) returns (int56[] memory, uint160[] memory) {} catch { revert NoPrice(); } } /// @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]; } }