// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {DegenZV3Token} from "./DegenZV3Token.sol"; import { IUniswapV3PoolLaunch, INonfungiblePositionManagerLaunch, IWETHLaunch } from "./interfaces/IV3Launch.sol"; /// @title DegenZV3Launchpad — NOXA-style single-sided Uniswap V3 fair launch. /// @notice On createCoin the full 1B supply is deposited as a single-sided (token-only) /// Uniswap V3 position, so the token is tradeable on a real V3 pool from block zero. /// Liquidity is locked forever (position NFT held here, never removable by anyone). /// There is no bonding curve and no LP migration. "Graduation" is a market-cap /// milestone; on graduation a one-time fee (default 5%) of the position is skimmed /// to the treasury and the rest of the LP stays locked permanently. /// @dev Per-trade fees are NOT charged here — they are collected off-chain by the DegenZ /// fee router (default 0.75%) on trades routed through the DegenZ frontend/bot, so /// the token itself carries no transfer tax and remains a standard ERC-20. contract DegenZV3Launchpad is Ownable, ReentrancyGuard, IERC721Receiver { using SafeERC20 for IERC20; uint256 public constant TOTAL_SUPPLY = 1_000_000_000 ether; // 1B, 18 decimals uint256 private constant Q96 = 0x1000000000000000000000000; // 2**96 uint256 private constant Q192 = 0x1000000000000000000000000000000000000000000000000; // 2**192 int24 private constant MAX_TICK = 887272; uint160 private constant MIN_SQRT_RATIO = 4295128739; uint160 private constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; address public treasury; address public positionManager; // Uniswap V3 NonfungiblePositionManager address public weth; uint24 public poolFee; uint256 public createFee; uint256 public startMarketCapEth; // starting market cap in native wei (initial pool price) uint256 public minGraduationMarketCapEth; uint256 public maxGraduationMarketCapEth; uint256 public graduationFeeBps; // one-time fee skimmed from LP at graduation struct Launch { address token; address pool; uint256 tokenId; uint256 graduationMarketCapEth; address creator; bool graduated; } mapping(address => Launch) public launches; // token => launch address[] public allTokens; address private _activeSwapPool; // guards the dev-buy swap callback (set/cleared within one tx) event CoinCreated( address indexed creator, address indexed token, address indexed pool, string name, string symbol, string metadataUri, uint256 tokenId, uint256 graduationMarketCapEth, uint160 sqrtPriceX96 ); event DevBuy(address indexed token, address indexed creator, uint256 ethIn, uint256 tokensOut); event Graduated(address indexed token, address indexed pool, uint256 marketCapEth, uint256 feeEthOut, uint256 feeTokenOut); event FeesCollected(address indexed token, uint256 ethOut, uint256 tokenOut); event CreateFeeUpdated(uint256 fee); event TreasuryUpdated(address indexed treasury); event StartMarketCapUpdated(uint256 startMcapEth); event GraduationBoundsUpdated(uint256 minMcapEth, uint256 maxMcapEth); event GraduationFeeUpdated(uint256 bps); event V3ConfigUpdated(address positionManager, address weth, uint24 poolFee); error AlreadyGraduated(); error NotReached(); error UnknownToken(); error BadCallback(); error NativeTransferFailed(); constructor( address treasury_, address positionManager_, address weth_, uint24 poolFee_, uint256 createFee_, uint256 startMarketCapEth_, uint256 minGraduationMarketCapEth_, uint256 maxGraduationMarketCapEth_, uint256 graduationFeeBps_ ) Ownable(msg.sender) { require(treasury_ != address(0) && positionManager_ != address(0) && weth_ != address(0), "zero addr"); require(poolFee_ > 0, "fee=0"); require(startMarketCapEth_ > 0, "start mcap=0"); require( minGraduationMarketCapEth_ > 0 && maxGraduationMarketCapEth_ >= minGraduationMarketCapEth_, "grad bounds" ); require(graduationFeeBps_ <= 2000, "grad fee too high"); treasury = treasury_; positionManager = positionManager_; weth = weth_; poolFee = poolFee_; createFee = createFee_; startMarketCapEth = startMarketCapEth_; minGraduationMarketCapEth = minGraduationMarketCapEth_; maxGraduationMarketCapEth = maxGraduationMarketCapEth_; graduationFeeBps = graduationFeeBps_; } // ------------------------------------------------------------------ create function createCoin( string calldata name_, string calldata symbol_, string calldata metadataUri_, uint256 graduationMarketCapEth_ ) external payable nonReentrant returns (address token, address pool) { require(msg.value >= createFee, "fee too low"); require(bytes(name_).length > 0 && bytes(symbol_).length > 0, "name/symbol"); require( graduationMarketCapEth_ >= minGraduationMarketCapEth && graduationMarketCapEth_ <= maxGraduationMarketCapEth, "grad mcap" ); if (createFee > 0) _sendNative(treasury, createFee); uint256 devBuyEth = msg.value - createFee; // Deploy token — full supply minted to this launchpad. token = address(new DegenZV3Token(name_, symbol_, metadataUri_, address(this), TOTAL_SUPPLY)); bool tokenIsToken0 = token < weth; (address token0, address token1) = tokenIsToken0 ? (token, weth) : (weth, token); uint160 sqrtPriceX96 = _startSqrtPriceX96(tokenIsToken0); pool = INonfungiblePositionManagerLaunch(positionManager).createAndInitializePoolIfNecessary( token0, token1, poolFee, sqrtPriceX96 ); uint256 tokenId = _mintSingleSided(pool, token0, token1, tokenIsToken0); launches[token] = Launch({ token: token, pool: pool, tokenId: tokenId, graduationMarketCapEth: graduationMarketCapEth_, creator: msg.sender, graduated: false }); allTokens.push(token); emit CoinCreated( msg.sender, token, pool, name_, symbol_, metadataUri_, tokenId, graduationMarketCapEth_, sqrtPriceX96 ); if (devBuyEth > 0) _devBuy(pool, token, tokenIsToken0, devBuyEth); } function _mintSingleSided(address pool, address token0, address token1, bool tokenIsToken0) internal returns (uint256 tokenId) { int24 spacing = IUniswapV3PoolLaunch(pool).tickSpacing(); (, int24 currentTick,,,,,) = IUniswapV3PoolLaunch(pool).slot0(); int24 maxTick = (MAX_TICK / spacing) * spacing; int24 minTick = -maxTick; int24 tickLower; int24 tickUpper; uint256 amount0Desired; uint256 amount1Desired; if (tokenIsToken0) { // token-only liquidity must sit strictly above spot → range above current tick. tickLower = _ceilTick(currentTick, spacing); if (tickLower >= maxTick) tickLower = maxTick - spacing; tickUpper = maxTick; amount0Desired = TOTAL_SUPPLY; } else { // token is token1 → token-only liquidity sits below spot → range below current tick. tickUpper = _floorTick(currentTick, spacing); if (tickUpper <= minTick) tickUpper = minTick + spacing; tickLower = minTick; amount1Desired = TOTAL_SUPPLY; } IERC20(tokenIsToken0 ? token0 : token1).forceApprove(positionManager, TOTAL_SUPPLY); (tokenId,,,) = INonfungiblePositionManagerLaunch(positionManager).mint( INonfungiblePositionManagerLaunch.MintParams({ token0: token0, token1: token1, fee: poolFee, tickLower: tickLower, tickUpper: tickUpper, amount0Desired: amount0Desired, amount1Desired: amount1Desired, amount0Min: 0, amount1Min: 0, recipient: address(this), deadline: block.timestamp }) ); } // ----------------------------------------------------------------- dev buy function _devBuy(address pool, address token, bool tokenIsToken0, uint256 ethIn) internal { IWETHLaunch(weth).deposit{value: ethIn}(); // buying token with weth: input is weth. zeroForOne = weth is token0. bool zeroForOne = !tokenIsToken0; uint160 limit = zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1; _activeSwapPool = pool; (int256 a0, int256 a1) = IUniswapV3PoolLaunch(pool).swap(msg.sender, zeroForOne, int256(ethIn), limit, ""); _activeSwapPool = address(0); uint256 tokensOut = tokenIsToken0 ? (a0 < 0 ? uint256(-a0) : 0) : (a1 < 0 ? uint256(-a1) : 0); emit DevBuy(token, msg.sender, ethIn, tokensOut); } /// @dev Uniswap V3 swap callback — only reachable during our own guarded dev buy. function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external { if (msg.sender != _activeSwapPool) revert BadCallback(); uint256 owed = amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta); IERC20(weth).safeTransfer(msg.sender, owed); } // -------------------------------------------------------------- graduation /// @notice Permissionless: finalize graduation once market cap reaches the target. /// Skims a one-time fee (default 5%) of the locked LP to treasury. function graduate(address token) external nonReentrant { Launch storage L = launches[token]; if (L.token == address(0)) revert UnknownToken(); if (L.graduated) revert AlreadyGraduated(); uint256 mcap = marketCapEth(token); if (mcap < L.graduationMarketCapEth) revert NotReached(); L.graduated = true; (uint256 ethOut, uint256 tokenOut) = _skimFee(L, graduationFeeBps); emit Graduated(token, L.pool, mcap, ethOut, tokenOut); } /// @notice Collect accrued V3 swap fees from a token's locked position to treasury. function collectFees(address token) external onlyOwner { Launch storage L = launches[token]; if (L.token == address(0)) revert UnknownToken(); (uint256 ethOut, uint256 tokenOut) = _skimFee(L, 0); emit FeesCollected(token, ethOut, tokenOut); } /// @dev Removes `bps` of the position's liquidity (0 = fees only), collects the freed /// principal + accrued swap fees, unwraps WETH and forwards native + tokens to treasury. function _skimFee(Launch storage L, uint256 bps) internal returns (uint256 ethOut, uint256 tokenOut) { if (bps > 0) { (,,,,,,, uint128 liq,,,,) = INonfungiblePositionManagerLaunch(positionManager).positions(L.tokenId); uint128 feeLiq = uint128((uint256(liq) * bps) / 10_000); if (feeLiq > 0) { INonfungiblePositionManagerLaunch(positionManager).decreaseLiquidity( INonfungiblePositionManagerLaunch.DecreaseLiquidityParams({ tokenId: L.tokenId, liquidity: feeLiq, amount0Min: 0, amount1Min: 0, deadline: block.timestamp }) ); } } (uint256 c0, uint256 c1) = INonfungiblePositionManagerLaunch(positionManager).collect( INonfungiblePositionManagerLaunch.CollectParams({ tokenId: L.tokenId, recipient: address(this), amount0Max: type(uint128).max, amount1Max: type(uint128).max }) ); bool tokenIsToken0 = L.token < weth; ethOut = tokenIsToken0 ? c1 : c0; tokenOut = tokenIsToken0 ? c0 : c1; if (ethOut > 0) { IWETHLaunch(weth).withdraw(ethOut); _sendNative(treasury, ethOut); } if (tokenOut > 0) { IERC20(L.token).safeTransfer(treasury, tokenOut); } } // ------------------------------------------------------------------- views /// @notice Fully-diluted market cap in native wei from the live pool price. function marketCapEth(address token) public view returns (uint256) { Launch storage L = launches[token]; if (L.token == address(0)) revert UnknownToken(); (uint160 sqrtP,,,,,,) = IUniswapV3PoolLaunch(L.pool).slot0(); uint256 priceX96 = Math.mulDiv(uint256(sqrtP), uint256(sqrtP), Q96); // price(token1/token0) * 2^96 if (token < weth) { // token is token0: price = weth per token return Math.mulDiv(priceX96, TOTAL_SUPPLY, Q96); } else { // weth is token0: price = token per weth → invert return Math.mulDiv(TOTAL_SUPPLY, Q96, priceX96); } } function graduationProgressBps(address token) external view returns (uint256) { Launch storage L = launches[token]; if (L.token == address(0)) revert UnknownToken(); if (L.graduated) return 10_000; uint256 mcap = marketCapEth(token); if (mcap >= L.graduationMarketCapEth) return 10_000; return (mcap * 10_000) / L.graduationMarketCapEth; } function isGraduated(address token) external view returns (bool) { return launches[token].graduated; } function tokenCount() external view returns (uint256) { return allTokens.length; } // ------------------------------------------------------------------- price function _startSqrtPriceX96(bool tokenIsToken0) internal view returns (uint160) { uint256 ratio; if (tokenIsToken0) { // price(token1/token0) = weth/token = startMcap / SUPPLY ; sqrt * 2^96 = sqrt(ratio*2^192) ratio = Math.mulDiv(startMarketCapEth, Q192, TOTAL_SUPPLY); } else { // price(token1/token0) = token/weth = SUPPLY / startMcap ratio = Math.mulDiv(TOTAL_SUPPLY, Q192, startMarketCapEth); } uint256 sqrtP = Math.sqrt(ratio); require(sqrtP >= MIN_SQRT_RATIO && sqrtP <= MAX_SQRT_RATIO, "sqrtP range"); return uint160(sqrtP); } function _ceilTick(int24 tick, int24 spacing) internal pure returns (int24 r) { r = (tick / spacing) * spacing; if (r < tick) r += spacing; } function _floorTick(int24 tick, int24 spacing) internal pure returns (int24 r) { r = (tick / spacing) * spacing; if (r > tick) r -= spacing; } // ------------------------------------------------------------------- admin function setCreateFee(uint256 fee) external onlyOwner { createFee = fee; emit CreateFeeUpdated(fee); } function setTreasury(address t) external onlyOwner { require(t != address(0), "treasury=0"); treasury = t; emit TreasuryUpdated(t); } function setStartMarketCapEth(uint256 v) external onlyOwner { require(v > 0, "start mcap=0"); startMarketCapEth = v; emit StartMarketCapUpdated(v); } function setGraduationBounds(uint256 minMcap, uint256 maxMcap) external onlyOwner { require(minMcap > 0 && maxMcap >= minMcap, "grad bounds"); minGraduationMarketCapEth = minMcap; maxGraduationMarketCapEth = maxMcap; emit GraduationBoundsUpdated(minMcap, maxMcap); } function setGraduationFeeBps(uint256 bps) external onlyOwner { require(bps <= 2000, "too high"); graduationFeeBps = bps; emit GraduationFeeUpdated(bps); } function setV3Config(address positionManager_, address weth_, uint24 poolFee_) external onlyOwner { require(positionManager_ != address(0) && weth_ != address(0), "zero addr"); require(poolFee_ > 0, "fee=0"); positionManager = positionManager_; weth = weth_; poolFee = poolFee_; emit V3ConfigUpdated(positionManager_, weth_, poolFee_); } function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { return IERC721Receiver.onERC721Received.selector; } function _sendNative(address to, uint256 amount) internal { (bool ok,) = to.call{value: amount}(""); if (!ok) revert NativeTransferFailed(); } receive() external payable {} }