{"language":"Solidity","sources":{"src/LazyCurve.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {LazyToken} from \"./LazyToken.sol\";\n\ninterface IFeeVault {\n    function deposit(address token) external payable;\n}\n\ninterface IGraduatorMin {\n    function graduate(address curve) external returns (uint256 tokenId);\n}\n\n/// @notice Constant-product bonding curve priced in the chain's native currency (USDC on Arc).\n///         Virtual quote reserve gives a $4,500 starting market cap on a 1B supply.\n///         Trading fee: 1% total, split 0.8% creator / 0.2% platform.\n///         Graduation once `raiseTarget` of net quote has entered the curve.\ncontract LazyCurve {\n    uint256 public constant FEE_BPS = 100; // 1%\n    uint256 public constant CREATOR_BPS = 80; // 0.8%\n    uint256 public constant GRADUATION_FEE_BPS = 100; // 1% of pool at graduation\n    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;\n\n    /// Split of the creator's 0.8% share, in bps of that share (sums to 10_000).\n    uint16 public immutable creatorSplitBps;\n    uint16 public immutable holdersSplitBps;\n    uint16 public immutable burnSplitBps;\n\n    /// Reward vault holding the holders + burn slices until settlement.\n    address public immutable feeVault;\n\n    address public immutable factory;\n    address public immutable platform;\n    address public immutable creator;\n    LazyToken public immutable token;\n\n    uint256 public immutable virtualQuote; // starting virtual quote reserve\n    uint256 public immutable raiseTarget; // net quote to raise before graduation\n    /// @notice Virtual token reserve the curve prices against. It is larger than the real\n    ///         supply (pump.fun style): the extra tokens never exist, which is what lets the\n    ///         Uniswap pool open at exactly the last curve price instead of below it.\n    uint256 public immutable virtualToken;\n    /// @notice Real tokens sellable on the curve. Everything above this stays for the pool.\n    uint256 public immutable sellCap;\n\n    uint256 public quoteReserve; // virtual + real\n    uint256 public tokenReserve; // virtual token reserve\n    uint256 public tokensSold; // real tokens handed to buyers\n    uint256 public quoteRaised; // real quote collected into the curve\n\n\n    bool public graduated;\n    bool public migrated;\n\n    /// Payouts that could not be pushed (recipient reverted); withdrawable any time.\n    mapping(address => uint256) public owed;\n    uint256 private _totalOwed;\n\n    /// @notice The only address graduated liquidity can ever be migrated to,\n    ///         fixed at launch so a compromised platform key cannot redirect it.\n    address public immutable migrateTarget;\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    function totalOwed() public view returns (uint256) {\n        return _totalOwed;\n    }\n\n    event Buy(address indexed buyer, uint256 quoteIn, uint256 tokensOut, uint256 fee);\n    event Sell(address indexed seller, uint256 tokensIn, uint256 quoteOut, uint256 fee);\n    event Graduated(uint256 quoteForLiquidity, uint256 tokensForLiquidity, uint256 fee);\n    event Migrated(address indexed target, uint256 quoteAmount, uint256 tokenAmount);\n    event FeeSplit(uint256 toCreator, uint256 toHolders, uint256 burned, uint256 toPlatform);\n    event PayoutDeferred(address indexed to, uint256 amount);\n    event AutoMigrateAttempted(address indexed target);\n    event AutoMigrated(address indexed target);\n    event AutoMigrateFailed(address indexed target);\n\n\n    /// @param _token pre-deployed vanity LazyToken; the factory transfers the full supply here.\n    constructor(\n        address _platform,\n        address _creator,\n        uint256 _virtualQuote,\n        uint256 _virtualToken,\n        uint256 _raiseTarget,\n        address _token,\n        uint256 _supply,\n        uint16 _creatorSplitBps,\n        uint16 _holdersSplitBps,\n        uint16 _burnSplitBps,\n        address _feeVault,\n        address _migrateTarget\n    ) {\n        require(_platform != address(0), \"ZERO_PLATFORM\");\n        require(\n            uint256(_creatorSplitBps) + uint256(_holdersSplitBps) + uint256(_burnSplitBps) == 10_000,\n            \"SPLIT\"\n        );\n        creatorSplitBps = _creatorSplitBps;\n        holdersSplitBps = _holdersSplitBps;\n        burnSplitBps = _burnSplitBps;\n        feeVault = _feeVault;\n        migrateTarget = _migrateTarget;\n        factory = msg.sender;\n        platform = _platform;\n        creator = _creator;\n        virtualQuote = _virtualQuote;\n        raiseTarget = _raiseTarget;\n        virtualToken = _virtualToken;\n        token = LazyToken(_token);\n        quoteReserve = _virtualQuote;\n        tokenReserve = _virtualToken;\n\n        // Tokens that will have been sold once the raise target is met, from the\n        // constant-product curve itself. The rest of the real supply is the pool float.\n        uint256 endReserve = (_virtualQuote * _virtualToken) / (_virtualQuote + _raiseTarget);\n        uint256 cap = _virtualToken - endReserve;\n        require(cap > 0 && cap <= _supply, \"CURVE_PARAMS\");\n        sellCap = cap;\n    }\n\n\n    function marketCap() external view returns (uint256) {\n        return (quoteReserve * token.totalSupply()) / tokenReserve;\n    }\n\n    function quoteBuy(uint256 quoteIn) public view returns (uint256 tokensOut, uint256 fee) {\n        fee = (quoteIn * FEE_BPS) / 10_000;\n        uint256 net = quoteIn - fee;\n        tokensOut = (tokenReserve * net) / (quoteReserve + net);\n    }\n\n    function quoteSell(uint256 tokensIn) public view returns (uint256 quoteOut, uint256 fee) {\n        uint256 gross = (quoteReserve * tokensIn) / (tokenReserve + tokensIn);\n        fee = (gross * FEE_BPS) / 10_000;\n        quoteOut = gross - fee;\n    }\n\n    function _ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {\n        return a == 0 ? 0 : (a - 1) / b + 1;\n    }\n\n    function buy(uint256 minTokensOut) external payable nonReentrant {\n        require(!graduated, \"GRADUATED\");\n        require(msg.value > 0, \"NO_VALUE\");\n        uint256 spend = msg.value;\n        (uint256 tokensOut, uint256 fee) = quoteBuy(spend);\n\n        // Never sell past the curve's share of the supply: the remainder is the\n        // pool float. An oversized buy is filled up to the cap and the rest refunded.\n        uint256 remaining = sellCap - tokensSold;\n        uint256 refund;\n        if (tokensOut > remaining) {\n            tokensOut = remaining;\n            uint256 netNeeded = _ceilDiv(quoteReserve * remaining, tokenReserve - remaining);\n            uint256 gross = _ceilDiv(netNeeded * 10_000, 10_000 - FEE_BPS);\n            if (gross > spend) gross = spend;\n            refund = spend - gross;\n            spend = gross;\n            fee = (spend * FEE_BPS) / 10_000;\n        }\n        require(tokensOut >= minTokensOut && tokensOut > 0, \"SLIPPAGE\");\n\n        uint256 net = spend - fee;\n        quoteReserve += net;\n        tokenReserve -= tokensOut;\n        tokensSold += tokensOut;\n        quoteRaised += net;\n\n        _payFees(fee);\n        require(token.transfer(msg.sender, tokensOut), \"TRANSFER_OUT\");\n        emit Buy(msg.sender, spend, tokensOut, fee);\n\n        if (refund > 0) {\n            (bool r, ) = msg.sender.call{value: refund}(\"\");\n            require(r, \"REFUND\");\n        }\n\n        if (!graduated && quoteRaised >= raiseTarget) _graduate();\n    }\n\n    function sell(uint256 tokensIn, uint256 minQuoteOut) external nonReentrant {\n        require(!graduated, \"GRADUATED\");\n        require(tokensIn > 0, \"NO_TOKENS\");\n        (uint256 quoteOut, uint256 fee) = quoteSell(tokensIn);\n        require(quoteOut >= minQuoteOut && quoteOut > 0, \"SLIPPAGE\");\n\n        require(token.transferFrom(msg.sender, address(this), tokensIn), \"TRANSFER_IN\");\n        quoteReserve -= (quoteOut + fee);\n        tokenReserve += tokensIn;\n        tokensSold = tokensSold > tokensIn ? tokensSold - tokensIn : 0;\n        quoteRaised = quoteRaised > (quoteOut + fee) ? quoteRaised - (quoteOut + fee) : 0;\n\n        _payFees(fee);\n        (bool ok, ) = msg.sender.call{value: quoteOut}(\"\");\n        require(ok, \"PAY_SELLER\");\n        emit Sell(msg.sender, tokensIn, quoteOut, fee);\n    }\n\n\n    /// @dev Fees are never pushed during a trade. They are credited to the\n    ///      recipient and pulled with `withdrawOwed`, so no recipient (creator,\n    ///      platform or a hostile contract) can ever make a buy or sell revert,\n    ///      and no trade pays for someone else's fallback gas.\n    function _send(address to, uint256 amount) internal {\n        if (amount == 0) return;\n        owed[to] += amount;\n        _totalOwed += amount;\n        emit PayoutDeferred(to, amount);\n    }\n\n    /// @notice Fees this address can withdraw right now.\n    function claimable(address who) external view returns (uint256) {\n        return owed[who];\n    }\n\n    /// @notice Withdraw payouts that could not be pushed at trade time.\n    function withdrawOwed() external nonReentrant {\n        uint256 amount = owed[msg.sender];\n        require(amount > 0, \"NOTHING\");\n        owed[msg.sender] = 0;\n        _totalOwed -= amount;\n        (bool ok, ) = msg.sender.call{value: amount}(\"\");\n        require(ok, \"PAY\");\n    }\n\n    function _payFees(uint256 fee) internal {\n        if (fee == 0) return;\n        uint256 creatorCut = (fee * CREATOR_BPS) / FEE_BPS;\n\n        // Platform's fixed 0.2%.\n        _send(platform, fee - creatorCut);\n\n        // Creator's 0.8%, routed exactly as chosen on the launch page.\n        uint256 toHolders = (creatorCut * holdersSplitBps) / 10_000;\n        uint256 toBurn = (creatorCut * burnSplitBps) / 10_000;\n        uint256 toCreator = creatorCut - toHolders - toBurn;\n\n        // Holder rewards and burns pool in the vault and are swapped into the\n        // token at settlement, so holders are paid in tokens and burns really\n        // reduce supply.\n        uint256 toVault = toHolders + toBurn;\n        if (toVault > 0) IFeeVault(feeVault).deposit{value: toVault}(address(token));\n        _send(creator, toCreator);\n        emit FeeSplit(toCreator, toHolders, toBurn, fee - creatorCut);\n    }\n\n    /// @dev Gas forwarded to the graduator for the in-transaction Uniswap v4 migration.\n    uint256 internal constant MIGRATE_GAS = 4_000_000;\n\n    function _graduate() internal {\n        require(!graduated, \"GRADUATED\");\n        graduated = true;\n        // Fee is charged on the quote actually raised through trades, so an\n        // outside donation to this contract cannot inflate the platform cut.\n        uint256 bal = address(this).balance;\n        uint256 pool = quoteRaised < bal ? quoteRaised : bal;\n        uint256 fee = (pool * GRADUATION_FEE_BPS) / 10_000;\n        _send(platform, fee);\n\n        // The same 1% is taken from the token side, so removing the fee cannot\n        // move the pool's opening price away from the last curve price.\n        uint256 tokenBal = token.balanceOf(address(this));\n        uint256 tokenFee = (tokenBal * GRADUATION_FEE_BPS) / 10_000;\n        if (tokenFee > 0) token.transfer(platform, tokenFee);\n\n        emit Graduated(pool - fee, tokenBal - tokenFee, fee);\n\n\n        // Migrate to Uniswap v4 in the very same transaction that crossed the\n        // target, so liquidity is live the moment the curve closes and nothing\n        // depends on an off-chain job. Wrapped in try/catch (and gas-capped) so\n        // the buyer's trade can never fail because of the migration step; the\n        // platform keeper can still finish it later if this call runs out of gas.\n        emit AutoMigrateAttempted(migrateTarget);\n        uint256 codeSize;\n        address target = migrateTarget;\n        assembly { codeSize := extcodesize(target) }\n        if (codeSize > 0) {\n            // Demand real headroom before trying. Without this, wallet gas\n            // estimation settles on the cheap \"migration failed\" branch and the\n            // real transaction then never has enough gas to migrate.\n            require(gasleft() >= MIGRATE_GAS + (MIGRATE_GAS / 32) + 150_000, \"GRAD_GAS\");\n            (bool ok, ) = target.call{gas: MIGRATE_GAS}(\n                abi.encodeWithSelector(IGraduatorMin.graduate.selector, address(this))\n            );\n            if (ok && migrated) emit AutoMigrated(target);\n            else emit AutoMigrateFailed(target);\n        } else {\n            emit AutoMigrateFailed(target);\n        }\n    }\n\n    /// @notice Moves graduated liquidity (quote + remaining tokens) to the DEX router/helper.\n    /// @dev Callable by the platform or by the migrate target itself, so the graduator can\n    ///      pull liquidity inside the same transaction that seeds the pool and measure the\n    ///      exact amounts received instead of trusting its own balance.\n    /// @dev Deliberately not `nonReentrant`: it is called back by the graduator from\n    ///      inside the graduating buy. Safety comes from the one-shot `migrated` flag\n    ///      (set before any external call) and the fixed caller/target checks.\n    function migrate(address target) external {\n        require(msg.sender == platform || msg.sender == migrateTarget, \"NOT_PLATFORM\");\n        require(graduated && !migrated, \"NOT_READY\");\n        require(target != address(0) && target == migrateTarget, \"TARGET\");\n        migrated = true;\n        uint256 bal = address(this).balance;\n        uint256 reserved = totalOwed();\n        uint256 quoteAmount = bal > reserved ? bal - reserved : 0;\n        // Real tokens held here (supply minus what buyers took, minus the graduation\n        // fee slice) — not the virtual reserve, which includes phantom tokens.\n        uint256 tokenAmount = token.balanceOf(address(this));\n        tokenReserve = 0;\n        require(token.transfer(target, tokenAmount), \"TOKEN_SEND\");\n\n        (bool ok, ) = target.call{value: quoteAmount}(\"\");\n        require(ok, \"SEND\");\n        emit Migrated(target, quoteAmount, tokenAmount);\n    }\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"}},"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":{}}}
