{"language":"Solidity","sources":{"src/LazyDirect.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {LazyToken} from \"./LazyToken.sol\";\nimport {TickMath} from \"./TickMath.sol\";\n\ninterface IERC20Min {\n    function approve(address spender, uint256 amount) external returns (bool);\n    function balanceOf(address a) external view returns (uint256);\n    function transfer(address to, uint256 amount) external returns (bool);\n}\n\n\ninterface IFeeVaultRoute {\n    function splits(address token) external view returns (uint16 creatorBps, uint16 holdersBps, uint16 burnBps);\n    function deposit(address token) external payable;\n    function depositTokens(address token, uint256 amount) external;\n}\n\ninterface IPermit2Min {\n    function approve(address token, address spender, uint160 amount, uint48 expiration) external;\n}\n\nstruct PoolKey {\n    address currency0;\n    address currency1;\n    uint24 fee;\n    int24 tickSpacing;\n    address hooks;\n}\n\nstruct ExactInputSingleParams {\n    PoolKey poolKey;\n    bool zeroForOne;\n    uint128 amountIn;\n    uint128 amountOutMinimum;\n    bytes hookData;\n}\n\ninterface IUniversalRouter {\n    function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable;\n}\n\ninterface IPositionManager {\n    function initializePool(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24);\n    function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable;\n    function nextTokenId() external view returns (uint256);\n}\n\n\n/// @notice Direct-to-market launcher: mints a fixed-supply LazyToken straight into a\n///         Uniswap v4 USDC pool on Arc as single-sided token liquidity. No bonding curve,\n///         no quote capital required. The LP position is held here permanently (locked);\n///         collected LP fees split 80/20 creator/platform.\ncontract LazyDirect {\n    uint8 constant MINT_POSITION = 0x02;\n    uint8 constant SETTLE_PAIR = 0x0d;\n    uint8 constant DECREASE_LIQUIDITY = 0x01;\n    uint8 constant TAKE_PAIR = 0x11;\n\n    uint24 public constant POOL_FEE = 10_000; // 1%\n    int24 public constant TICK_SPACING = 200;\n    uint256 public constant CREATOR_BPS = 8_000;\n    uint256 public constant SUPPLY = 1_000_000_000e18;\n\n    address public platform;\n    address public factory;\n    IPositionManager public immutable posm;\n    IPermit2Min public immutable permit2;\n    address public immutable feeVault;\n    /// @notice Uniswap v4 Universal Router, used for the creator's optional first buy.\n    IUniversalRouter public immutable router;\n\n\n    struct Launch {\n        uint256 tokenId;\n        address token;\n        address creator;\n        int24 tickLower;\n        int24 tickUpper;\n    }\n\n    mapping(address => Launch) public launchOfToken;\n    address[] public tokens;\n\n    /// Payouts that could not be pushed (recipient reverted); withdrawable any time.\n    mapping(address => uint256) public owed;\n\n    uint256 private _lock = 1;\n\n    modifier nonReentrant() {\n        require(_lock == 1, \"REENTRANT\");\n        _lock = 2;\n        _;\n        _lock = 1;\n    }\n\n    event DirectLaunch(address indexed token, address indexed creator, uint256 tokenId, uint160 sqrtPriceX96, string metadataURI);\n    event DevBuy(address indexed token, address indexed creator, uint256 spent, uint256 received);\n    event FeesCollected(address indexed token, uint256 quoteFees, uint256 tokenFees);\n    event FactoryUpdated(address factory);\n    event PlatformUpdated(address platform);\n    event PayoutDeferred(address indexed to, uint256 amount);\n\n    constructor(address _platform, address _posm, address _permit2, address _feeVault, address _router) {\n        require(_platform != address(0), \"ZERO_PLATFORM\");\n        platform = _platform;\n        posm = IPositionManager(_posm);\n        permit2 = IPermit2Min(_permit2);\n        feeVault = _feeVault;\n        router = IUniversalRouter(_router);\n    }\n\n    function tokenCount() external view returns (uint256) {\n        return tokens.length;\n    }\n\n    struct LaunchParams {\n        string name;\n        string symbol;\n        string metadataURI;\n        uint160 sqrtPriceX96;\n        int24 tickLower;\n        int24 tickUpper;\n        uint160 sqrtLowerX96;\n        uint160 sqrtUpperX96;\n        uint16 creatorSplitBps;\n        uint16 holdersSplitBps;\n        uint16 burnSplitBps;\n        /// @notice Optional first buy for the creator, executed in this same transaction.\n        uint256 devBuy;\n        /// @notice Minimum tokens the creator accepts for `devBuy` (slippage guard).\n        uint256 devMinOut;\n    }\n\n\n    function setPlatform(address _platform) external {\n        require(msg.sender == platform, \"NOT_PLATFORM\");\n        require(_platform != address(0), \"ZERO\");\n        platform = _platform;\n        emit PlatformUpdated(_platform);\n    }\n\n    function setFactory(address _factory) external {\n        require(msg.sender == platform, \"NOT_PLATFORM\");\n        factory = _factory;\n        emit FactoryUpdated(_factory);\n    }\n\n    /// @notice Called by LazyFactory so every LazyMemes launch is attributable to the factory.\n    ///         The factory deploys the vanity token and transfers the full supply here first.\n    function launchFor(address creator, address token_, LaunchParams calldata p)\n        external\n        payable\n        returns (address token, uint256 tokenId)\n    {\n        require(msg.sender == factory && factory != address(0), \"NOT_FACTORY\");\n        return _launch(creator, token_, p);\n    }\n\n    function _launch(address creator, address token_, LaunchParams memory p)\n        internal\n        returns (address token, uint256 tokenId)\n    {\n        require(p.tickLower < p.tickUpper, \"TICKS\");\n        require(p.tickLower % TICK_SPACING == 0 && p.tickUpper % TICK_SPACING == 0, \"TICK_SPACING\");\n        // The sqrt boundaries are derived from the ticks the position is actually minted\n        // at, so caller-supplied sqrt values can never disagree with the real range.\n        uint160 sqrtLower = TickMath.getSqrtRatioAtTick(p.tickLower);\n        uint160 sqrtUpper = TickMath.getSqrtRatioAtTick(p.tickUpper);\n        require(sqrtUpper > sqrtLower, \"RANGE\");\n        // The token is currency1, so token-only liquidity is only correct when the\n        // starting price sits at or above the range; below it the mint would\n        // silently require quote (currency0) capital this contract does not hold.\n        require(p.sqrtPriceX96 >= sqrtUpper, \"START_PRICE\");\n        require(IERC20Min(token_).balanceOf(address(this)) >= SUPPLY, \"NO_SUPPLY\");\n        token = token_;\n\n        PoolKey memory key = PoolKey({\n            currency0: address(0),\n            currency1: token,\n            fee: POOL_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: address(0)\n        });\n\n        posm.initializePool(key, p.sqrtPriceX96);\n\n        IERC20Min(token).approve(address(permit2), type(uint256).max);\n        permit2.approve(token, address(posm), type(uint160).max, type(uint48).max);\n\n        // token-only liquidity: L = amount1 * 2^96 / (sqrtUpper - sqrtLower)\n        uint256 amount1 = SUPPLY;\n        uint256 liquidity = (amount1 * (1 << 96)) / (uint256(sqrtUpper) - uint256(sqrtLower));\n        liquidity = (liquidity * 999) / 1000;\n        require(liquidity > 0, \"LIQ\");\n\n        tokenId = posm.nextTokenId();\n\n        bytes memory actions = abi.encodePacked(MINT_POSITION, SETTLE_PAIR);\n        bytes[] memory params = new bytes[](2);\n        params[0] = abi.encode(key, p.tickLower, p.tickUpper, liquidity, uint128(0), uint128(amount1), address(this), bytes(\"\"));\n        params[1] = abi.encode(key.currency0, key.currency1);\n\n        posm.modifyLiquidities(abi.encode(actions, params), block.timestamp + 600);\n\n        launchOfToken[token] = Launch({tokenId: tokenId, token: token, creator: creator, tickLower: p.tickLower, tickUpper: p.tickUpper});\n        tokens.push(token);\n\n        // Optional creator first buy — same transaction, same block as the pool\n        // creation, so nobody can front-run the launch.\n        uint256 devBuy = p.devBuy;\n        require(msg.value >= devBuy, \"DEV_BUY_VALUE\");\n        if (devBuy > 0) {\n            require(address(router) != address(0), \"NO_ROUTER\");\n            uint256 balBefore = IERC20Min(token).balanceOf(address(this));\n\n            bytes memory sActions = abi.encodePacked(uint8(0x06), uint8(0x0c), uint8(0x0f)); // SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL\n            bytes[] memory sParams = new bytes[](3);\n            sParams[0] = abi.encode(\n                ExactInputSingleParams({\n                    poolKey: key,\n                    zeroForOne: true,\n                    amountIn: uint128(devBuy),\n                    amountOutMinimum: uint128(p.devMinOut),\n                    hookData: bytes(\"\")\n                })\n            );\n            sParams[1] = abi.encode(key.currency0, devBuy);\n            sParams[2] = abi.encode(key.currency1, p.devMinOut);\n\n            bytes memory commands = abi.encodePacked(uint8(0x10)); // V4_SWAP\n            bytes[] memory inputs = new bytes[](1);\n            inputs[0] = abi.encode(sActions, sParams);\n            router.execute{value: devBuy}(commands, inputs, block.timestamp + 600);\n\n            uint256 bought = IERC20Min(token).balanceOf(address(this)) - balBefore;\n            require(bought >= p.devMinOut && bought > 0, \"DEV_SLIPPAGE\");\n            require(IERC20Min(token).transfer(creator, bought), \"DEV_PAY\");\n            emit DevBuy(token, creator, devBuy, bought);\n        }\n\n        uint256 fee = msg.value - devBuy;\n        if (fee > 0) {\n            (bool ok, ) = platform.call{value: fee}(\"\");\n            require(ok, \"FEE\");\n        }\n\n        emit DirectLaunch(token, creator, tokenId, p.sqrtPriceX96, p.metadataURI);\n    }\n\n\n    function collectFees(address token) external nonReentrant {\n        Launch memory l = launchOfToken[token];\n        require(l.tokenId != 0, \"UNKNOWN\");\n\n        PoolKey memory key = PoolKey({\n            currency0: address(0),\n            currency1: token,\n            fee: POOL_FEE,\n            tickSpacing: TICK_SPACING,\n            hooks: address(0)\n        });\n\n        bytes memory actions = abi.encodePacked(DECREASE_LIQUIDITY, TAKE_PAIR);\n        bytes[] memory params = new bytes[](2);\n        params[0] = abi.encode(l.tokenId, uint256(0), uint128(0), uint128(0), bytes(\"\"));\n        params[1] = abi.encode(key.currency0, key.currency1, address(this));\n\n        uint256 quoteBefore = address(this).balance;\n        uint256 tokenBefore = IERC20Min(token).balanceOf(address(this));\n\n        posm.modifyLiquidities(abi.encode(actions, params), block.timestamp + 600);\n\n        uint256 quoteFees = address(this).balance - quoteBefore;\n        uint256 tokenFees = IERC20Min(token).balanceOf(address(this)) - tokenBefore;\n\n        (, uint16 hBps, uint16 bBps) = IFeeVaultRoute(feeVault).splits(token);\n        uint256 vaultBps = uint256(hBps) + uint256(bBps);\n\n        if (quoteFees > 0) {\n            uint256 creatorCut = (quoteFees * CREATOR_BPS) / 10_000;\n            uint256 toVault = (creatorCut * vaultBps) / 10_000;\n            uint256 toCreator = creatorCut - toVault;\n            if (toVault > 0) IFeeVaultRoute(feeVault).deposit{value: toVault}(token);\n            // A recipient that rejects payment can never block fee collection.\n            _send(l.creator, toCreator);\n            _send(platform, quoteFees - creatorCut);\n        }\n        if (tokenFees > 0) {\n            uint256 creatorCut = (tokenFees * CREATOR_BPS) / 10_000;\n            uint256 toVault = (creatorCut * vaultBps) / 10_000;\n            require(IERC20Min(token).transfer(l.creator, creatorCut - toVault), \"PAY_T\");\n            require(IERC20Min(token).transfer(platform, tokenFees - creatorCut), \"PAY_T\");\n            if (toVault > 0) {\n                require(IERC20Min(token).transfer(feeVault, toVault), \"PAY_T\");\n                IFeeVaultRoute(feeVault).depositTokens(token, toVault);\n            }\n        }\n\n        emit FeesCollected(token, quoteFees, tokenFees);\n    }\n\n    /// @dev Push payment with a gas cap; credited for later withdrawal if it fails.\n    function _send(address to, uint256 amount) internal {\n        if (amount == 0) return;\n        (bool ok, ) = to.call{value: amount, gas: 30_000}(\"\");\n        if (!ok) {\n            owed[to] += amount;\n            emit PayoutDeferred(to, amount);\n        }\n    }\n\n    /// @notice Withdraw payouts that could not be pushed at collection time.\n    function withdrawOwed() external nonReentrant {\n        uint256 amount = owed[msg.sender];\n        require(amount > 0, \"NOTHING\");\n        owed[msg.sender] = 0;\n        (bool ok, ) = msg.sender.call{value: amount}(\"\");\n        require(ok, \"PAY\");\n    }\n\n    receive() external payable {}\n}\n"},"src/LazyToken.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\ninterface IFeeVaultHook {\n    function onTransfer(address from, address to, uint256 value) external;\n}\n\n/// @notice Fixed-supply ERC20 template deployed once per meme-coin launch.\n///         Entire supply is minted to the bonding curve at construction.\ncontract LazyToken {\n    string public name;\n    string public symbol;\n    uint8 public constant decimals = 18;\n    uint256 public immutable totalSupply;\n\n    address public immutable creator;\n    /// @notice Reward vault notified on every transfer so holder rewards accrue pro-rata.\n    address public immutable feeVault;\n    string public metadataURI;\n\n    uint256 private _locked = 1;\n\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    event Transfer(address indexed from, address indexed to, uint256 value);\n    event Approval(address indexed owner, address indexed spender, uint256 value);\n\n    constructor(\n        string memory _name,\n        string memory _symbol,\n        string memory _metadataURI,\n        address _creator,\n        address _mintTo,\n        uint256 _supply,\n        address _feeVault\n    ) {\n        name = _name;\n        symbol = _symbol;\n        metadataURI = _metadataURI;\n        creator = _creator;\n        feeVault = _feeVault;\n        totalSupply = _supply;\n        balanceOf[_mintTo] = _supply;\n        emit Transfer(address(0), _mintTo, _supply);\n    }\n\n    /// @notice Alias so explorers and indexers can read the off-chain metadata JSON.\n    function tokenURI() external view returns (string memory) {\n        return metadataURI;\n    }\n\n    /// @notice Metadata is permanently frozen at deployment: nobody, including the\n    ///         creator or the platform, can ever change it.\n    function metadataFrozen() external pure returns (bool) {\n        return true;\n    }\n\n    function transfer(address to, uint256 value) external returns (bool) {\n        _transfer(msg.sender, to, value);\n        return true;\n    }\n\n    function approve(address spender, uint256 value) external returns (bool) {\n        allowance[msg.sender][spender] = value;\n        emit Approval(msg.sender, spender, value);\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 value) external returns (bool) {\n        uint256 allowed = allowance[from][msg.sender];\n        if (allowed != type(uint256).max) {\n            require(allowed >= value, \"ALLOWANCE\");\n            allowance[from][msg.sender] = allowed - value;\n        }\n        _transfer(from, to, value);\n        return true;\n    }\n\n    function _transfer(address from, address to, uint256 value) internal {\n        require(to != address(0), \"TO_ZERO\");\n        // The reward hook runs before balances change (it snapshots pre-transfer\n        // balances), so a non-reentrancy lock guarantees no callback can re-enter\n        // transfer logic while this contract is in an intermediate state.\n        require(_locked == 1, \"REENTRANCY\");\n        _locked = 2;\n        uint256 bal = balanceOf[from];\n        require(bal >= value, \"BALANCE\");\n        // Reward accounting must never be able to block a transfer: the hook is\n        // called with a bounded gas stipend and any failure is ignored.\n        if (feeVault != address(0)) {\n            try IFeeVaultHook(feeVault).onTransfer{gas: 250_000}(from, to, value) {} catch {}\n        }\n        unchecked {\n            balanceOf[from] = bal - value;\n            balanceOf[to] += value;\n        }\n        _locked = 1;\n        emit Transfer(from, to, value);\n    }\n}\n"},"src/TickMath.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @notice Uniswap's tick -> sqrt price conversion (unchanged math, 0.8 safe wrapper).\nlibrary TickMath {\n    int24 internal constant MIN_TICK = -887272;\n    int24 internal constant MAX_TICK = 887272;\n\n    function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) {\n        unchecked {\n            uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));\n            require(absTick <= uint256(int256(MAX_TICK)), \"T\");\n\n            uint256 ratio = absTick & 0x1 != 0\n                ? 0xfffcb933bd6fad37aa2d162d1a594001\n                : 0x100000000000000000000000000000000;\n            if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128;\n            if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;\n            if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;\n            if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128;\n            if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128;\n            if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128;\n            if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;\n            if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;\n            if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128;\n            if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;\n            if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;\n            if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;\n            if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;\n            if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;\n            if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128;\n            if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;\n            if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128;\n            if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128;\n            if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128;\n\n            if (tick > 0) ratio = type(uint256).max / ratio;\n\n            sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1));\n        }\n    }\n}\n"}},"settings":{"remappings":["forge-std/=lib/forge-std/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode.object","evm.bytecode.sourceMap","evm.bytecode.linkReferences","evm.deployedBytecode.object","evm.deployedBytecode.sourceMap","evm.deployedBytecode.linkReferences","evm.deployedBytecode.immutableReferences","evm.methodIdentifiers","metadata"]}},"evmVersion":"cancun","viaIR":true,"libraries":{}}}
