// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.26; import {TickMath} from "./TickMath.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; interface IUniswapV3PoolOracle { function observe(uint32[] calldata secondsAgos) external view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s); } interface IUniswapV3PoolFull { function token0() external view returns (address); function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16, uint16, uint16, uint8, bool); function initialize(uint160 sqrtPriceX96) external; function mint(address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data) external returns (uint256 amount0, uint256 amount1); function swap(address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data) external returns (int256 amount0, int256 amount1); function burn(int24 tickLower, int24 tickUpper, uint128 amount) external returns (uint256, uint256); function collect(address recipient, int24 tickLower, int24 tickUpper, uint128 amount0Requested, uint128 amount1Requested) external returns (uint128 amount0, uint128 amount1); function feeGrowthGlobal0X128() external view returns (uint256); function feeGrowthGlobal1X128() external view returns (uint256); function ticks(int24 tick) external view returns (uint128, int128, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56, uint160, uint32, bool); function positions(bytes32 key) external view returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1); } interface ISwapRouter02 { struct ExactInputParams { bytes path; address recipient; uint256 amountIn; uint256 amountOutMinimum; } function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); } interface IERC20Min { function balanceOf(address) external view returns (uint256); function transfer(address to, uint256 value) external returns (bool); } interface IWETH { function deposit() external payable; function withdraw(uint256) external; } /// @title FledgeCoin /// @notice A coin that starts as its own market, keeps what it trades in stock, and then leaves the nest. /// /// **On the curve.** Every coin is born as a constant-product market against a virtual ETH reserve. A fee /// (fixed at launch, 0.25%–10%) on every buy and sell is swapped on Uniswap v3 — ETH → USDG → one tokenized /// stock — and the stock stays in this contract: the coin's nest. 800M of the 1B coins are for sale on the curve. /// /// **Fledging.** The buy that takes the last curve coin closes the curve, and in the same transaction the ETH /// the curve raised and the 200M coins held back become a full-range position in the coin's 1% Uniswap v3 pool, /// at exactly the price the curve ended on. This contract owns the position and has no function that can remove /// it: the liquidity is locked for good. /// /// **After.** The coin trades on Uniswap like any token. The position's fees keep feeding the nest: fees paid in /// ETH (on buys) are swapped into the stock; fees paid in the coin (on sells) are burned. Anyone may harvest. /// /// **Exit.** Any holder can burn coins for exactly their share (over the whole supply) of the stock and of the /// fee ETH not yet swapped. /// /// Treasury buys never pay a manipulated price: each must return at least what the two pools' own time-weighted /// average prices say, less the pools' fees and 2%; otherwise the ETH waits. No owner, no pause, no upgrade, no /// fee switch. The launcher gets nothing. contract FledgeCoin { // ------------------------------------------------------------------ ERC-20 string public name; string public symbol; uint8 public constant decimals = 18; uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); // ------------------------------------------------------------------ constants uint256 public constant SUPPLY = 1_000_000_000e18; /// @notice Coins sold on the curve; the rest go into the Uniswap position when the curve closes. uint256 public constant CURVE_COINS = 800_000_000e18; uint256 public constant POOL_COINS = SUPPLY - CURVE_COINS; /// @notice Virtual coins behind the curve, chosen so the curve's last price equals (ETH raised / POOL_COINS): /// the pool opens exactly where the curve stopped. With virtual ETH v the curve raises 3v. uint256 public constant VIRTUAL_COINS = CURVE_COINS * POOL_COINS / (CURVE_COINS - POOL_COINS); uint24 public constant POOL_FEE = 10_000; int24 public constant TICK_LOWER = -887_200; // full range at tick spacing 200 int24 public constant TICK_UPPER = 887_200; /// @notice Fee ETH below this waits in `pendingEth` until more arrives (~5 cents). uint256 public constant MIN_CONVERT = 0.00002 ether; /// @notice Gas the treasury swap is given (a two-hop swap into a stock uses ~300k here). uint256 public constant CONVERT_GAS = 600_000; /// @notice Gas kept back for reading the two price oracles before the swap. uint256 public constant ORACLE_GAS = 400_000; /// @notice Gas for one pool's price history read (~80k measured on the busiest pools here). uint256 public constant OBSERVE_GAS = 150_000; /// @notice How far below the average price, after pool fees, a treasury buy may land. uint256 public constant MAX_SLIP_BPS = 200; /// @notice Gas the fledging needs from its start: opening the pool and adding the position (~0.5M). uint256 public constant FLEDGE_GAS = 1_500_000; /// @notice One price step toward the target is only attempted with at least this much gas left. uint256 public constant STEP_GAS = 1_200_000; uint8 public constant ON_CURVE = 0; uint8 public constant FLEDGING = 1; uint8 public constant FLEDGED = 2; ISwapRouter02 public immutable router; address public immutable weth; address public immutable usdg; address public immutable factory; /// @notice The WETH/USDG pool every treasury buy goes through first, and its fee. address public immutable ethPool; uint24 public immutable ethPoolFee; // ------------------------------------------------------------------ set once at launch address public creator; address public stock; /// @notice The USDG/stock pool the treasury buys through, and its fee tier. address public stockPool; uint24 public stockPoolFee; uint16 public feeBps; uint64 public launchedAt; uint256 public virtualEth; /// @notice This coin's own 1% Uniswap v3 pool against WETH (created at launch, empty until the coin fledges). address public pool; bool private coinIs0; /// @notice Picture, description and links, stored as contract code (SSTORE2) by the factory. address public metaPointer; // ------------------------------------------------------------------ state uint8 public phase; /// @notice Real ETH held by the curve (the fee ETH is not in it). uint256 public realEth; /// @notice Fee ETH not yet swapped into the stock. uint256 public pendingEth; /// @notice The pool price the curve ended on (set when the curve closes). uint160 public targetSqrtPriceX96; uint64 public fledgedAt; uint128 public liquidity; /// @notice Lifetime: fee ETH taken, stock bought with it, coins burned (fees and redemptions), trades on the curve. uint256 public totalFeesEth; uint256 public totalStockBought; uint256 public totalRedeemed; uint256 public totalFeeCoinsBurned; uint256 public tradeCount; uint256 private _locked; bool private _inPoolCall; event Trade( address indexed trader, bool isBuy, uint256 ethAmount, uint256 coinAmount, uint256 feeEth, uint256 ethReserve, uint256 coinReserve ); /// @param fairOut what the average prices said `ethIn` was worth, in stock units event TreasuryBuy(uint256 ethIn, uint256 stockOut, uint256 fairOut); /// @param reason 1 = no average price could be read, 2 = the swap would have paid too much or failed event TreasuryBuyDeferred(uint256 ethPending, uint8 reason); event Redeem(address indexed holder, address indexed to, uint256 coinsBurned, uint256 stockOut, uint256 ethOut); event CurveClosed(uint256 ethRaised, uint160 targetSqrtPriceX96); event Fledged(address indexed pool, uint256 ethIn, uint256 coinsIn, uint128 liquidity, uint256 coinsBurned, uint256 ethToNest); event Harvest(uint256 ethFees, uint256 coinsBurned); error AlreadyInitialized(); error OnlyFactory(); error Reentrancy(); error ZeroAmount(); error Slippage(); error EthTransferFailed(); error StockTransferFailed(); error InsufficientBalance(); error InsufficientAllowance(); error NeedsMoreGas(); error CurveClosedAlready(); error NotFledged(); error NotFledging(); error PoolLocked(); error NotPool(); modifier nonReentrant() { if (_locked == 1) revert Reentrancy(); _locked = 1; _; _locked = 0; } constructor(address router_, address weth_, address usdg_, address ethPool_, uint24 ethPoolFee_, address factory_) { router = ISwapRouter02(router_); weth = weth_; usdg = usdg_; ethPool = ethPool_; ethPoolFee = ethPoolFee_; factory = factory_; launchedAt = type(uint64).max; // the implementation itself can never be initialised } function initialize( string calldata name_, string calldata symbol_, address metaPointer_, address creator_, address stock_, address stockPool_, uint24 stockPoolFee_, uint16 feeBps_, uint256 virtualEth_, address pool_ ) external { if (msg.sender != factory) revert OnlyFactory(); if (launchedAt != 0) revert AlreadyInitialized(); name = name_; symbol = symbol_; metaPointer = metaPointer_; creator = creator_; stock = stock_; stockPool = stockPool_; stockPoolFee = stockPoolFee_; feeBps = feeBps_; virtualEth = virtualEth_; pool = pool_; coinIs0 = address(this) < weth; launchedAt = uint64(block.timestamp); totalSupply = SUPPLY; balanceOf[address(this)] = SUPPLY; emit Transfer(address(0), address(this), SUPPLY); } // ------------------------------------------------------------------ ERC-20 logic function transfer(address to, uint256 value) external returns (bool) { _transfer(msg.sender, to, value); return true; } function approve(address spender, uint256 value) external returns (bool) { allowance[msg.sender][spender] = value; emit Approval(msg.sender, spender, value); return true; } function transferFrom(address from, address to, uint256 value) external returns (bool) { uint256 a = allowance[from][msg.sender]; if (a != type(uint256).max) { if (a < value) revert InsufficientAllowance(); allowance[from][msg.sender] = a - value; } _transfer(from, to, value); return true; } /// @dev Until the coin has fledged, nobody but this contract can put coins into its pool. So nobody can /// provide liquidity there first on the coin's side, and a price someone set in the empty pool can be moved /// back to the curve's price without paying anyone. function _transfer(address from, address to, uint256 value) internal { if (to == pool && phase != FLEDGED && from != address(this)) revert PoolLocked(); uint256 b = balanceOf[from]; if (b < value) revert InsufficientBalance(); unchecked { balanceOf[from] = b - value; balanceOf[to] += value; } emit Transfer(from, to, value); } function _burn(address from, uint256 value) internal { uint256 b = balanceOf[from]; if (b < value) revert InsufficientBalance(); unchecked { balanceOf[from] = b - value; totalSupply -= value; } emit Transfer(from, address(0), value); } // ------------------------------------------------------------------ the curve /// @notice The curve's virtual reserves: ETH (virtual + raised) and coins (unsold + virtual). function reserves() public view returns (uint256 ethReserve, uint256 coinReserve) { return (virtualEth + realEth, curveLeft() + VIRTUAL_COINS); } /// @notice Coins still for sale on the curve. function curveLeft() public view returns (uint256) { if (phase != ON_CURVE) return 0; return balanceOf[address(this)] - POOL_COINS; } /// @notice The ETH the curve raises in all: selling every curve coin takes the ETH reserve to /// virtualEth × (CURVE_COINS + VIRTUAL_COINS) / VIRTUAL_COINS. function fledgeEth() public view returns (uint256) { return virtualEth * CURVE_COINS / VIRTUAL_COINS; } /// @notice Coins out for `ethIn` sent to buy, the part that goes to the nest, and the ETH actually used /// (less than `ethIn` only when the buy takes the last curve coin; the rest is refunded). function quoteBuy(uint256 ethIn) public view returns (uint256 coinsOut, uint256 fee, uint256 used) { if (phase != ON_CURVE) return (0, 0, 0); fee = ethIn * feeBps / 10_000; uint256 net = ethIn - fee; (uint256 x, uint256 y) = reserves(); coinsOut = y * net / (x + net); used = ethIn; uint256 left = curveLeft(); if (coinsOut >= left) { coinsOut = left; net = Math.mulDiv(x, left, y - left, Math.Rounding.Ceil); used = Math.mulDiv(net, 10_000, 10_000 - feeBps, Math.Rounding.Ceil); if (used > ethIn) used = ethIn; fee = used - net; } } /// @notice ETH paid out for selling `coinsIn` on the curve, and the part of it that goes to the nest. function quoteSell(uint256 coinsIn) public view returns (uint256 ethOut, uint256 fee) { if (phase != ON_CURVE) return (0, 0); (uint256 x, uint256 y) = reserves(); uint256 gross = x * coinsIn / (y + coinsIn); if (gross > realEth) gross = realEth; fee = gross * feeBps / 10_000; ethOut = gross - fee; } /// @notice Buy on the curve. If this buy takes the last curve coin, the ETH it did not need is refunded to /// the sender and the coin fledges in the same transaction. function buy(uint256 minCoinsOut, address to) public payable nonReentrant returns (uint256 coinsOut) { if (phase != ON_CURVE) revert CurveClosedAlready(); if (msg.value == 0) revert ZeroAmount(); uint256 fee; uint256 used; (coinsOut, fee, used) = quoteBuy(msg.value); if (coinsOut == 0 || coinsOut < minCoinsOut) revert Slippage(); // A buy that fledges the coin needs room for the fee's swap AND the fledging; ask for both up front, so a // wallet's estimate never lands on a gas limit that runs out halfway through. if (coinsOut == curveLeft() && gasleft() < FLEDGE_GAS + CONVERT_GAS + CONVERT_GAS / 63 + ORACLE_GAS + 300_000) { revert NeedsMoreGas(); } realEth += used - fee; _transfer(address(this), to, coinsOut); tradeCount++; (uint256 x, uint256 y) = reserves(); emit Trade(to, true, used, coinsOut, fee, x, y); _accrue(fee); if (curveLeft() == 0) { _closeCurve(); _fledge(); } if (used < msg.value) _sendEth(msg.sender, msg.value - used); } function sell(uint256 coinsIn, uint256 minEthOut, address to) external nonReentrant returns (uint256 ethOut) { if (phase != ON_CURVE) revert CurveClosedAlready(); if (coinsIn == 0) revert ZeroAmount(); uint256 fee; (ethOut, fee) = quoteSell(coinsIn); if (ethOut == 0 || ethOut < minEthOut) revert Slippage(); _transfer(msg.sender, address(this), coinsIn); realEth -= ethOut + fee; tradeCount++; (uint256 x, uint256 y) = reserves(); emit Trade(msg.sender, false, ethOut, coinsIn, fee, x, y); _accrue(fee); _sendEth(to, ethOut); } // ------------------------------------------------------------------ fledging function _closeCurve() internal { phase = FLEDGING; uint256 e = realEth; realEth = 0; IWETH(weth).deposit{value: e}(); // the pool's price is token1 per token0, as sqrt × 2^96 (uint256 a0, uint256 a1) = coinIs0 ? (POOL_COINS, e) : (e, POOL_COINS); uint160 p = uint160(Math.sqrt(Math.mulDiv(a1, 1 << 192, a0))); targetSqrtPriceX96 = p; emit CurveClosed(e, p); } /// @notice Finish fledging, if a first attempt ran out of room. Anyone may call it. function fledge() external nonReentrant { if (phase != FLEDGING) revert NotFledging(); _fledge(); } /// @dev Opens the pool at the curve's last price and adds the position. If somebody has already set a /// different price in the pool, it is moved back first — in steps of at most 2× in price, so a pool strewn /// with tiny positions can only make this take more than one transaction, never make it impossible. The /// only liquidity that can be there holds WETH alone (coins cannot enter the pool yet), so each step sells /// coins above the curve's price: moving the price back never costs the nest anything. function _fledge() internal { if (gasleft() < FLEDGE_GAS) revert NeedsMoreGas(); IUniswapV3PoolFull p = IUniswapV3PoolFull(pool); uint160 target = targetSqrtPriceX96; (uint160 cur,,,,,,) = p.slot0(); if (cur == 0) { p.initialize(target); cur = target; } while (cur != target) { if (gasleft() < STEP_GAS + FLEDGE_GAS) return; // not finished: anyone calls fledge() again bool down = cur > target; // zeroForOne lowers the price uint160 next = down ? uint160(uint256(cur) * 128 / 181) : uint160(Math.min(uint256(cur) * 181 / 128, type(uint160).max)); if (down ? next < target : next > target) next = target; address tokenIn = down ? (coinIs0 ? address(this) : weth) : (coinIs0 ? weth : address(this)); uint256 have = IERC20Min(tokenIn).balanceOf(address(this)); if (have == 0) break; _inPoolCall = true; p.swap(address(this), down, int256(have), next, ""); _inPoolCall = false; (uint160 now_,,,,,,) = p.slot0(); if (now_ == cur) break; // nothing moved: the pool absorbed everything we could offer cur = now_; } _addPosition(p, cur); } function _addPosition(IUniswapV3PoolFull p, uint160 sqrtP) internal { uint256 coins = balanceOf[address(this)]; uint256 wethBal = IERC20Min(weth).balanceOf(address(this)); (uint256 a0, uint256 a1) = coinIs0 ? (coins, wethBal) : (wethBal, coins); uint128 L; if (a0 > 0 && a1 > 0) { uint160 sa = TickMath.getSqrtRatioAtTick(TICK_LOWER); uint160 sb = TickMath.getSqrtRatioAtTick(TICK_UPPER); uint256 l0 = sqrtP >= sb ? 0 : Math.mulDiv(a0, Math.mulDiv(sqrtP, sb, 1 << 96), sb - sqrtP); uint256 l1 = sqrtP <= sa ? 0 : Math.mulDiv(a1, 1 << 96, sqrtP - sa); uint256 l = l0 < l1 ? l0 : l1; l -= l / 1e9 + 1; // the pool rounds what it asks for up; leave it that room if (l > type(uint128).max) l = type(uint128).max; L = uint128(l); } if (L > 0) { _inPoolCall = true; p.mint(address(this), TICK_LOWER, TICK_UPPER, L, ""); _inPoolCall = false; } liquidity = L; uint256 ethIn = wethBal - IERC20Min(weth).balanceOf(address(this)); uint256 coinsIn = coins - balanceOf[address(this)]; // what the position could not take: coins are burned, ETH goes to the nest uint256 leftCoins = balanceOf[address(this)]; if (leftCoins > 0) _burn(address(this), leftCoins); uint256 leftWeth = IERC20Min(weth).balanceOf(address(this)); if (leftWeth > 0) { IWETH(weth).withdraw(leftWeth); pendingEth += leftWeth; } phase = FLEDGED; fledgedAt = uint64(block.timestamp); emit Fledged(address(p), ethIn, coinsIn, L, leftCoins, leftWeth); } function uniswapV3MintCallback(uint256 owed0, uint256 owed1, bytes calldata) external { _payPool(owed0, owed1); } function uniswapV3SwapCallback(int256 d0, int256 d1, bytes calldata) external { _payPool(d0 > 0 ? uint256(d0) : 0, d1 > 0 ? uint256(d1) : 0); } function _payPool(uint256 owed0, uint256 owed1) internal { if (msg.sender != pool || !_inPoolCall) revert NotPool(); (uint256 coins, uint256 w) = coinIs0 ? (owed0, owed1) : (owed1, owed0); if (coins > 0) _transfer(address(this), pool, coins); if (w > 0 && !_callOk(weth, abi.encodeCall(IERC20Min.transfer, (pool, w)))) revert StockTransferFailed(); } // ------------------------------------------------------------------ after fledging: the pool's fees /// @dev Collects the position's fees: the coin side is burned, the ETH side joins the pending fee ETH. function _collectFees() internal returns (uint256 ethFees, uint256 coinFees) { IUniswapV3PoolFull p = IUniswapV3PoolFull(pool); p.burn(TICK_LOWER, TICK_UPPER, 0); // brings the position's fees up to date (uint128 c0, uint128 c1) = p.collect(address(this), TICK_LOWER, TICK_UPPER, type(uint128).max, type(uint128).max); (coinFees, ethFees) = coinIs0 ? (uint256(c0), uint256(c1)) : (uint256(c1), uint256(c0)); if (coinFees > 0) { _burn(address(this), coinFees); totalFeeCoinsBurned += coinFees; } if (ethFees > 0) { IWETH(weth).withdraw(ethFees); pendingEth += ethFees; totalFeesEth += ethFees; } if (coinFees > 0 || ethFees > 0) emit Harvest(ethFees, coinFees); } /// @notice Collect the pool's fees into the nest and swap the fee ETH into the stock. Anyone may call it. function harvest() external nonReentrant { if (phase != FLEDGED) revert NotFledged(); _collectFees(); if (pendingEth >= MIN_CONVERT) _convert(pendingEth); } /// @notice The fees the position has earned and not yet collected: (ETH, coins). function unharvested() public view returns (uint256 ethFees, uint256 coinFees) { if (phase != FLEDGED) return (0, 0); IUniswapV3PoolFull p = IUniswapV3PoolFull(pool); (, int24 tick,,,,,) = p.slot0(); (,, uint256 lo0, uint256 lo1,,,,) = p.ticks(TICK_LOWER); (,, uint256 hi0, uint256 hi1,,,,) = p.ticks(TICK_UPPER); (uint128 liq, uint256 last0, uint256 last1, uint128 owed0, uint128 owed1) = p.positions(keccak256(abi.encodePacked(address(this), TICK_LOWER, TICK_UPPER))); uint256 f0 = _inside(p.feeGrowthGlobal0X128(), lo0, hi0, tick); uint256 f1 = _inside(p.feeGrowthGlobal1X128(), lo1, hi1, tick); uint256 e0; uint256 e1; unchecked { e0 = owed0 + Math.mulDiv(f0 - last0, liq, 1 << 128); e1 = owed1 + Math.mulDiv(f1 - last1, liq, 1 << 128); } (coinFees, ethFees) = coinIs0 ? (e0, e1) : (e1, e0); } /// @dev Uniswap's fee growth inside a range; every subtraction wraps, as in the pool. function _inside(uint256 global, uint256 outLo, uint256 outHi, int24 tick) internal pure returns (uint256 r) { unchecked { uint256 below = tick >= TICK_LOWER ? outLo : global - outLo; uint256 above = tick < TICK_UPPER ? outHi : global - outHi; r = global - below - above; } } // ------------------------------------------------------------------ the nest function _accrue(uint256 fee) internal { totalFeesEth += fee; pendingEth += fee; if (pendingEth >= MIN_CONVERT) _convert(pendingEth); } /// @dev Swaps `amt` of the pending fee ETH into the stock, or leaves it pending. It never makes a /// trade fail — except for too little gas, which it refuses outright: a wallet estimates the /// smallest gas at which a transaction does not revert, and without this refusal that estimate /// would starve the swap inside the try and every fee would be deferred. function _convert(uint256 amt) internal { if (gasleft() < CONVERT_GAS + CONVERT_GAS / 63 + ORACLE_GAS) revert NeedsMoreGas(); uint256 fair = fairStockOut(amt); if (fair == 0) { emit TreasuryBuyDeferred(pendingEth, 1); return; } uint256 minOut = Math.mulDiv(fair, (1e6 - ethPoolFee - stockPoolFee) * (10_000 - MAX_SLIP_BPS), 1e10); if (minOut == 0) minOut = 1; pendingEth -= amt; try router.exactInput{value: amt, gas: CONVERT_GAS}( ISwapRouter02.ExactInputParams({ path: abi.encodePacked(weth, ethPoolFee, usdg, stockPoolFee, stock), recipient: address(this), amountIn: amt, amountOutMinimum: minOut }) ) returns (uint256 out) { totalStockBought += out; emit TreasuryBuy(amt, out, fair); } catch { pendingEth += amt; emit TreasuryBuyDeferred(pendingEth, 2); } } /// @notice Anyone can push pending fee ETH into the stock — all of it, or at most `maxEth` of it /// (a large backlog in a thin pool may only clear in pieces). function convert(uint256 maxEth) external nonReentrant { uint256 amt = pendingEth < maxEth ? pendingEth : maxEth; if (amt == 0) revert ZeroAmount(); _convert(amt); } /// @notice What `ethIn` is worth in stock units at the two pools' time-weighted average prices /// (ETH→USDG, then USDG→stock), before fees. Zero when either average cannot be read. /// Tries a 30-minute window, then 10 minutes, then 2: a very busy pool can have overwritten /// the older observations. Any window excludes a price pushed within the current block. function fairStockOut(uint256 ethIn) public view returns (uint256) { (bool ok1, int24 t1) = _meanTick(ethPool); if (!ok1) return 0; (bool ok2, int24 t2) = _meanTick(stockPool); if (!ok2) return 0; return _quoteAtTick(t2, _quoteAtTick(t1, ethIn, weth, usdg), usdg, stock); } function _meanTick(address p) internal view returns (bool, int24) { uint32[] memory ago = new uint32[](2); for (uint256 i; i < 3; i++) { uint32 w = i == 0 ? 1800 : i == 1 ? 600 : 120; ago[0] = w; try IUniswapV3PoolOracle(p).observe{gas: OBSERVE_GAS}(ago) returns (int56[] memory tc, uint160[] memory) { int56 d = tc[1] - tc[0]; int24 t = int24(d / int56(uint56(w))); if (d < 0 && d % int56(uint56(w)) != 0) t--; // round toward negative infinity, as Uniswap does return (true, t); } catch {} } return (false, 0); } /// @dev Uniswap's OracleLibrary.getQuoteAtTick, with a full-width base amount. function _quoteAtTick(int24 tick, uint256 baseAmount, address baseToken, address quoteToken) internal pure returns (uint256) { uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick); if (sqrtRatioX96 <= type(uint128).max) { uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96; return baseToken < quoteToken ? Math.mulDiv(ratioX192, baseAmount, 1 << 192) : Math.mulDiv(1 << 192, baseAmount, ratioX192); } uint256 ratioX128 = Math.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64); return baseToken < quoteToken ? Math.mulDiv(ratioX128, baseAmount, 1 << 128) : Math.mulDiv(1 << 128, baseAmount, ratioX128); } function nestStock() public view returns (uint256) { return IERC20Min(stock).balanceOf(address(this)); } /// @notice What burning `coins` would pay out right now: that fraction of the stock and of the pending /// fee ETH (after fledging, including the pool fees not yet collected). function quoteRedeem(uint256 coins) public view returns (uint256 stockOut, uint256 ethOut) { (uint256 feeEth, uint256 feeCoins) = unharvested(); uint256 s = totalSupply - feeCoins; if (s == 0) return (0, 0); stockOut = nestStock() * coins / s; ethOut = (pendingEth + feeEth) * coins / s; } /// @notice Burn coins for their share of the nest. The share is over the WHOLE supply, including /// coins in the curve or the pool, so nobody can take more than their fraction. function redeem(uint256 coins, uint256 minStockOut, address to) external nonReentrant returns (uint256 stockOut, uint256 ethOut) { if (coins == 0) revert ZeroAmount(); if (phase == FLEDGED) _collectFees(); uint256 s = totalSupply; stockOut = nestStock() * coins / s; ethOut = pendingEth * coins / s; if (stockOut < minStockOut) revert Slippage(); _burn(msg.sender, coins); totalRedeemed += coins; pendingEth -= ethOut; if (stockOut > 0 && !_callOk(stock, abi.encodeCall(IERC20Min.transfer, (to, stockOut)))) { revert StockTransferFailed(); } if (ethOut > 0) _sendEth(to, ethOut); emit Redeem(msg.sender, to, coins, stockOut, ethOut); } // ------------------------------------------------------------------ for the app /// @notice ABI-encoded (string image, string description, string website, string x, string telegram). function meta() public view returns (bytes memory data) { address p = metaPointer; if (p == address(0)) return data; uint256 size = p.code.length; if (size <= 1) return data; data = new bytes(size - 1); assembly ("memory-safe") { extcodecopy(p, add(data, 32), 1, sub(size, 1)) } } struct Info { string name; string symbol; address creator; address stock; address stockPool; uint24 stockPoolFee; uint16 feeBps; uint64 launchedAt; uint8 phase; address pool; uint256 totalSupply; uint256 ethReserve; uint256 coinReserve; uint256 realEth; uint256 curveLeft; uint256 fledgeEth; uint256 pendingEth; uint256 nestStock; uint256 totalFeesEth; uint256 totalStockBought; uint256 totalRedeemed; uint256 totalFeeCoinsBurned; uint256 tradeCount; uint256 poolSqrtPriceX96; uint256 unharvestedEth; uint256 unharvestedCoins; uint64 fledgedAt; } function info() external view returns (Info memory i) { (uint256 x, uint256 y) = reserves(); (uint256 fe, uint256 fc) = unharvested(); uint160 sp; if (phase == FLEDGED) (sp,,,,,,) = IUniswapV3PoolFull(pool).slot0(); i = Info( name, symbol, creator, stock, stockPool, stockPoolFee, feeBps, launchedAt, phase, pool, totalSupply, x, y, realEth, curveLeft(), fledgeEth(), pendingEth, nestStock(), totalFeesEth, totalStockBought, totalRedeemed, totalFeeCoinsBurned, tradeCount, sp, fe, fc, fledgedAt ); } function _callOk(address target, bytes memory data) internal returns (bool) { (bool ok, bytes memory ret) = target.call(data); return ok && (ret.length == 0 || (ret.length >= 32 && abi.decode(ret, (bool)))); } function _sendEth(address to, uint256 amt) internal { (bool ok,) = to.call{value: amt}(""); if (!ok) revert EthTransferFailed(); } /// @dev Only the router (a refund) and WETH (an unwrap) may send ETH here. A plain transfer is refused, /// so no ETH can end up outside the accounting. receive() external payable { if (msg.sender != address(router) && msg.sender != weth) revert(); } }