{"language":"Solidity","sources":{"src/v2/PoolLauncher.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\ninterface ISale {\n    function sold() external view returns (uint256);\n}\n\ninterface IWool20 {\n    function approve(address, uint256) external returns (bool);\n    function transfer(address, uint256) external returns (bool);\n    function balanceOf(address) external view returns (uint256);\n}\n\ninterface IPancakeFactory {\n    function getPair(address, address) external view returns (address);\n    function createPair(address, address) external returns (address);\n}\n\ninterface IPancakeRouter {\n    function addLiquidityETH(\n        address token,\n        uint256 amountTokenDesired,\n        uint256 amountTokenMin,\n        uint256 amountETHMin,\n        address to,\n        uint256 deadline\n    ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity);\n}\n\n/// @title PoolLauncher — 一次性开池合约，参数写死，谁都能调\n/// @notice 目的：把\"到时候我们手动建池\"变成\"条件满足后任何人都能开池，且参数不可改\"。\n///         - 触发条件写死在合约里（2,666 只 / 14 天后 1,000 只 / 60 天后任意）\n///         - WOOL 与 BNB 必须打进来才能开，开完 LP 直接送黑洞\n///         - 如果有人抢先建了 WOOL/WBNB 交易对，合约**拒绝开池**（防 1 wei 占坑）\ncontract PoolLauncher {\n    ISale public immutable sale;\n    address public immutable wool;\n    IPancakeFactory public immutable factory;\n    IPancakeRouter public immutable router;\n    address public immutable wbnb;\n\n    uint256 public constant SOLD_TRIGGER = 2666;      // 30% of 8,888\n    uint256 public constant STALL_MIN_SOLD = 1000;    // 兜底：14 天后至少要卖这么多\n    uint256 public constant STALL_DAYS = 14 days;\n    uint256 public constant LONGSTOP_DAYS = 60 days;  // 兜底：60 天后不论卖了多少\n    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;\n\n    address public immutable owner;   // 仅用于把多余的 BNB/WOOL 退回，不能改任何参数\n    uint256 public armedAt;\n    bool public opened;\n    address public pair;\n\n    event Armed(uint256 at, uint256 woolFunded);\n    event Opened(address indexed pair, uint256 woolUsed, uint256 bnbUsed, uint256 lpBurned);\n    event Refunded(address indexed to, uint256 bnb, uint256 wool);\n\n    error NotOwner();\n    error AlreadyOpened();\n    error NotYet();\n    error PairSquatted(address pair);\n    error NothingToOpen();\n\n    constructor(\n        ISale sale_,\n        address wool_,\n        IPancakeFactory factory_,\n        IPancakeRouter router_,\n        address wbnb_\n    ) {\n        sale = sale_;\n        wool = wool_;\n        factory = factory_;\n        router = router_;\n        wbnb = wbnb_;\n        owner = msg.sender;\n        armedAt = block.timestamp;\n    }\n\n    receive() external payable {}\n\n    /// @notice 条件满足了吗（前端/keeper 直接读这个）\n    function ready() public view returns (bool) {\n        uint256 sold = sale.sold();\n        if (opened) return false;\n        if (sold >= SOLD_TRIGGER) return true;\n        if (block.timestamp >= armedAt + STALL_DAYS && sold >= STALL_MIN_SOLD) return true;\n        if (block.timestamp >= armedAt + LONGSTOP_DAYS) return true;\n        return false;\n    }\n\n    /// @notice 开池：创建交易对 → 加流动性 → LP 打进黑洞。任何人都能调。\n    function open() external {\n        if (opened) revert AlreadyOpened();\n        if (!ready()) revert NotYet();\n\n        uint256 bnb = address(this).balance;\n        uint256 woolAmt = IWool20(wool).balanceOf(address(this));\n        if (bnb == 0 || woolAmt == 0) revert NothingToOpen();\n\n        // 防抢注：已经有人建过这个交易对就不开，交给人来看\n        address existing = factory.getPair(wool, wbnb);\n        if (existing != address(0)) revert PairSquatted(existing);\n\n        address p = factory.createPair(wool, wbnb);\n        pair = p;\n        opened = true;\n\n        IWool20(wool).approve(address(router), woolAmt);\n        (uint256 usedToken, uint256 usedEth, uint256 lp) = router.addLiquidityETH{value: bnb}(\n            wool, woolAmt, 0, 0, address(this), block.timestamp + 600\n        );\n\n        // LP 直接送黑洞：任何人都撤不了池\n        if (lp > 0) {\n            (bool ok, ) = p.call(abi.encodeWithSignature(\"transfer(address,uint256)\", DEAD, lp));\n            require(ok, \"lp burn\");\n        }\n\n        emit Opened(p, usedToken, usedEth, lp);\n\n        // 剩下的（一般不会有）退回\n        uint256 leftBnb = address(this).balance;\n        uint256 leftWool = IWool20(wool).balanceOf(address(this));\n        if (leftBnb > 0 || leftWool > 0) {\n            if (leftBnb > 0) { (bool a, ) = owner.call{value: leftBnb}(\"\"); require(a, \"bnb\"); }\n            if (leftWool > 0) IWool20(wool).transfer(owner, leftWool);\n            emit Refunded(owner, leftBnb, leftWool);\n        }\n    }\n\n    /// @notice 开池前可以撤资（只给 owner，开池后失效）\n    function withdraw() external {\n        if (msg.sender != owner) revert NotOwner();\n        if (opened) revert AlreadyOpened();\n        uint256 bnb = address(this).balance;\n        uint256 w = IWool20(wool).balanceOf(address(this));\n        if (bnb > 0) { (bool a, ) = owner.call{value: bnb}(\"\"); require(a, \"bnb\"); }\n        if (w > 0) IWool20(wool).transfer(owner, w);\n        emit Refunded(owner, bnb, w);\n    }\n}\n"}},"settings":{"remappings":["lib/openzeppelin-contracts/:forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","lib/openzeppelin-contracts/:forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/"],"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":{}}}
