// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; /// @title LaunchBlock v3 — one-transaction launch + bundle for Pons V2 on Robinhood Chain /// @dev Footprint: no events, no counters, no registry. Each launch runs through a fresh EIP-1167 executor that self-destructs in the /// same transaction (EIP-6780), so the Pons factory event names a one-time address that has no code afterwards. /// @notice launchBundle() launches a Pons V2 token and, in the SAME transaction, buys it for up to 31 recipient /// wallets that are declared snipe-tax exempt at launch. No wallet funding, no timing race, no burn risk /// (a curve buy sequenced before its launch is a plain value transfer to a codeless address). /// Optionally routes the token's creator fees through a per-launch FeeVault: anyone can harvest for a /// bounty, the creator keeps the rest minus a protocol split fixed at launch. /// The contract keeps no funds: every wei is forwarded, refunded or paid out inside the call. interface IForwarder { struct Socials { string a; string b; string c; string d; string e; } struct LaunchParams { string name; string symbol; string uri; string description; Socials socials; address deployer; uint16 creatorTaxBps; bool flag; bytes32 salt1; bytes32 salt2; } function launchAndBuy(LaunchParams calldata p, uint256 p1, address p2, uint256 seed, uint256 minOut, address recipient, address[] calldata exempt) external payable returns (address token, address curve, uint256 tokensOut); } contract FeeVault { // ---- immutable-per-clone state (set once by init) ---- address public factory; address public creator; address public token; address public curve; address public escrow; uint16 public protocolBps; uint16 public bountyBps; uint256 public bountyCap; address public treasury; uint256 public creatorOwed; // pull balance if a push to the creator fails bool private inited; error AlreadyInit(); error NothingToHarvest(); error Reentrancy(); uint256 private lock; modifier nonReentrant() { if (lock == 1) revert Reentrancy(); lock = 1; _; lock = 0; } receive() external payable {} function init(address _creator, address _escrow, uint16 _protocolBps, uint16 _bountyBps, uint256 _bountyCap, address _treasury) external { if (inited) revert AlreadyInit(); inited = true; factory = msg.sender; creator = _creator; escrow = _escrow; protocolBps = _protocolBps; bountyBps = _bountyBps; bountyCap = _bountyCap; treasury = _treasury; } function bind(address _token, address _curve) external { require(msg.sender == factory && token == address(0)); token = _token; curve = _curve; } /// @notice pending ETH: escrow credit + anything already sitting here function pending() public view returns (uint256) { (bool ok, bytes memory ret) = escrow.staticcall(abi.encodeWithSignature("balanceOf(address)", address(this))); uint256 credit = (ok && ret.length >= 32) ? abi.decode(ret, (uint256)) : 0; return credit + address(this).balance - creatorOwed; } /// @notice Permissionless. Sweeps curve fees to the escrow (deployer-only on the curve, so the vault must do it), /// claims the escrow credit, pays the caller a bounty, the protocol its split, the creator the rest. function harvest() external nonReentrant returns (uint256 gross, uint256 bounty) { if (curve != address(0)) { curve.call(abi.encodeWithSignature("sweepFees(uint256)", uint256(0))); } // best effort: reverts when nothing to sweep (bool okB, bytes memory ret) = escrow.staticcall(abi.encodeWithSignature("balanceOf(address)", address(this))); if (okB && ret.length >= 32 && abi.decode(ret, (uint256)) > 0) { (bool okC, ) = escrow.call(abi.encodeWithSignature("claim()")); okC; } gross = address(this).balance - creatorOwed; if (gross == 0) revert NothingToHarvest(); bounty = gross * bountyBps / 10_000; if (bounty > bountyCap) bounty = bountyCap; uint256 protocol = gross * protocolBps / 10_000; uint256 toCreator = gross - bounty - protocol; if (bounty > 0) { (bool s1, ) = payable(msg.sender).call{value: bounty}(""); require(s1, "bounty"); } if (protocol > 0) { (bool s2, ) = payable(treasury).call{value: protocol}(""); if (!s2) { toCreator += protocol; protocol = 0; } } (bool s3, ) = payable(creator).call{value: toCreator, gas: 60_000}(""); if (!s3) creatorOwed += toCreator; } /// @notice creator pulls what a failed push left behind function withdrawCreator() external nonReentrant { uint256 a = creatorOwed; creatorOwed = 0; (bool s, ) = payable(creator).call{value: a}(""); require(s, "withdraw"); } /// @notice any ERC20 that lands here goes to the creator (nobody else can ever receive it) function rescueToken(address erc20) external { (bool ok, bytes memory b) = erc20.staticcall(abi.encodeWithSignature("balanceOf(address)", address(this))); uint256 bal = (ok && b.length >= 32) ? abi.decode(b, (uint256)) : 0; if (bal > 0) { (bool s, ) = erc20.call(abi.encodeWithSignature("transfer(address,uint256)", creator, bal)); require(s, "rescue"); } } } contract Executor { bytes4 private constant BUY = 0x59a87bc1; address public immutable factory; // baked into code -> shared by every clone error NotFactory(); error BuyFailed(uint256 i, bytes reason); error Zero(); constructor() { factory = msg.sender; } receive() external payable {} struct Job { uint256 launchFee; uint256 seed; uint256 minSeedOut; address creator; uint256 minOutEach; uint256 gasDrop; } /// @notice Runs the whole launch as msg.sender of the forwarder, then self-destructs (created in this tx => code deleted, EIP-6780). function run(IForwarder fwd, IForwarder.LaunchParams calldata q, address[] calldata recipients, uint256[] calldata amounts, Job calldata j) external payable returns (address token, address curve) { if (msg.sender != factory) revert NotFactory(); (token, curve, ) = fwd.launchAndBuy{value: j.launchFee + j.seed}(q, 0, address(0), j.seed, j.minSeedOut, j.creator, recipients); if (token == address(0) || curve == address(0)) revert Zero(); for (uint256 i; i < recipients.length; ++i) { address r = recipients[i]; uint256 a = amounts[i]; if (r == address(0)) revert Zero(); (bool ok, bytes memory ret) = curve.call{value: a}(abi.encodeWithSelector(BUY, a, j.minOutEach, r)); if (!ok) revert BuyFailed(i, ret); if (j.gasDrop > 0) { (bool g, ) = payable(r).call{value: j.gasDrop}(""); require(g, "gasDrop"); } } } /// @notice Second call from the factory in the same tx: leftover (overpayment + curve refunds) -> creator, then the clone deletes itself /// (SELFDESTRUCT halts execution, so it cannot sit in run() which must return the addresses). function finish(address creator) external { if (msg.sender != factory) revert NotFactory(); selfdestruct(payable(creator)); } } contract LaunchBlock { uint256 public constant MAX_RECIPIENTS = 31; // Pons exempt list limit uint16 public constant MAX_FEE_BPS = 300; // hard cap on the service fee (3% of the bundle) uint16 public constant MAX_PROTOCOL_BPS = 2_000; // hard cap on the fee-vault split (20% of creator fees) IForwarder public immutable forwarder; address public immutable escrow; address public immutable vaultImpl; address public immutable execImpl; address public owner; address public treasury; uint256 public launchFee; // what the forwarder charges on top of the seed (0.0005 ETH at deploy) uint16 public feeBps; // service fee on the bundled ETH uint16 public protocolBps; // split of creator fees when a FeeVault is used uint16 public bountyBps; uint256 public bountyCap; // harvest bounty // v2: no events, no counters, no token->vault registry. The contract writes nothing about a launch anywhere. error NotOwner(); error BadLength(); error TooMany(); error BadValue(); error BuyFailed(uint256 i, bytes reason); error Deployer(); error Zero(); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } constructor(address _forwarder, address _escrow, address _treasury, uint256 _launchFee, uint16 _feeBps, uint16 _protocolBps, uint16 _bountyBps, uint256 _bountyCap) { forwarder = IForwarder(_forwarder); escrow = _escrow; owner = msg.sender; treasury = _treasury; launchFee = _launchFee; require(_feeBps <= MAX_FEE_BPS && _protocolBps <= MAX_PROTOCOL_BPS && _bountyBps <= 1_000); feeBps = _feeBps; protocolBps = _protocolBps; bountyBps = _bountyBps; bountyCap = _bountyCap; vaultImpl = address(new FeeVault()); execImpl = address(new Executor()); } /// @notice How much ETH to send: forwarder fee + seed + bundle + service fee + gas drops. function quote(uint256 seed, uint256[] calldata amounts, uint256 gasDrop) public view returns (uint256 total, uint256 bundled, uint256 fee) { for (uint256 i; i < amounts.length; ++i) bundled += amounts[i]; fee = bundled * feeBps / 10_000; total = launchFee + seed + bundled + fee + gasDrop * amounts.length; } struct Bundle { uint256 seed; uint256 minSeedOut; address[] recipients; uint256[] amounts; uint256 minOutEach; uint256 gasDrop; bool useVault; } /// @notice Launch + bundle in one transaction. /// @param p Pons launch params. p.deployer MUST be msg.sender unless b.useVault (then it is overwritten with the new FeeVault). /// @param b seed = ETH of the launch dev-buy (tokens to msg.sender); recipients/amounts = the bundle (max 31, declared snipe-tax exempt at /// launch); minOutEach = token floor per buy (0 = any fill); gasDrop = ETH forwarded to each recipient so it can sell later /// without a funding trail from you; useVault = route creator fees through a FeeVault (permissionless harvest with bounty). function launchBundle(IForwarder.LaunchParams calldata p, Bundle calldata b) external payable returns (address token, address curve, address vault) { if (b.recipients.length != b.amounts.length) revert BadLength(); if (b.recipients.length > MAX_RECIPIENTS) revert TooMany(); (uint256 total, , uint256 fee) = quote(b.seed, b.amounts, b.gasDrop); if (msg.value < total) revert BadValue(); IForwarder.LaunchParams memory q = p; if (b.useVault) { vault = _clone(vaultImpl); FeeVault(payable(vault)).init(msg.sender, escrow, protocolBps, bountyBps, bountyCap, treasury); q.deployer = vault; } else if (p.deployer != msg.sender) revert Deployer(); address ex = _clone(execImpl); // one-time executor: it is the forwarder's msg.sender, and it is gone after this tx (token, curve) = Executor(payable(ex)).run{value: msg.value - fee}(forwarder, q, b.recipients, b.amounts, Executor.Job(launchFee, b.seed, b.minSeedOut, msg.sender, b.minOutEach, b.gasDrop)); Executor(payable(ex)).finish(msg.sender); // refunds -> creator; executor code gone when this tx ends (EIP-6780) if (b.useVault) { FeeVault(payable(vault)).bind(token, curve); } if (fee > 0) { (bool f, ) = payable(treasury).call{value: fee}(""); require(f, "fee"); } } // ---- admin: fee parameters only (hard-capped); the contract never holds funds and cannot touch a launched token ---- function setParams(uint16 _feeBps, uint16 _protocolBps, uint16 _bountyBps, uint256 _bountyCap, uint256 _launchFee, address _treasury) external onlyOwner { require(_feeBps <= MAX_FEE_BPS && _protocolBps <= MAX_PROTOCOL_BPS && _bountyBps <= 1_000 && _treasury != address(0)); feeBps = _feeBps; protocolBps = _protocolBps; bountyBps = _bountyBps; bountyCap = _bountyCap; launchFee = _launchFee; treasury = _treasury; } function transferOwnership(address n) external onlyOwner { require(n != address(0)); owner = n; } function _clone(address impl) internal returns (address inst) { // EIP-1167 minimal proxy assembly { let ptr := mload(0x40) mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) mstore(add(ptr, 0x14), shl(0x60, impl)) mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) inst := create(0, ptr, 0x37) } require(inst != address(0), "clone"); } }