MemeFolioAtomicLauncher

0x7861a13054ca22424bc18df742355df58333520b

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
16,732 bytes
ABI entries
31
7 read · 4 write
License
none

Contract information

Address
0x7861a13054ca22424bc18df742355df58333520b
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x4ad871a4e3…ec85877b06

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (7)

launchManifestHash(tuple)bytes32
launched()bool
manifestHash()bytes32
owner()address
permit2()address
poolManager()address
positionManager()address

Events (3)

LaunchCommittedLaunchCompletedOwnershipTransferred

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      },
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "contract IPositionManager",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "contract IAllowanceTransfer",
        "name": "permit2_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "Create2EmptyBytecode",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedDeployment",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAddress",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "expected",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "actual",
        "type": "address"
      }
    ],
    "name": "InvalidDeployment",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidLaunchConfiguration",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidManifest",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LaunchInvariantFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ManifestAlreadyCommitted",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "expected",
        "type": "bytes32"
      },
      {
        "internalType": "bytes32",
        "name": "actual",
        "type": "bytes32"
      }
    ],
    "name": "ManifestMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoManifestCommitted",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "manifestHash",
        "type": "bytes32"
      }
    ],
    "name": "LaunchCommitted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "manifestHash",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "hook",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "locker",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "distributor",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "deploymentBlock",
        "type": "uint256"
      }
    ],
    "name": "LaunchCompleted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "manifestHash_",
        "type": "bytes32"
      }
    ],
    "name": "commitLaunch",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "components": [
              {
                "internalType": "bytes32",
                "name": "salt",
                "type": "bytes32"
              },
              {
                "internalType": "bytes",
                "name": "initCode",
                "type": "bytes"
              },
              {
                "internalType": "address",
                "name": "expectedAddress",
                "type": "address"
              }
            ],
            "internalType": "struct MemeFolioAtomicLauncher.Deployment",
            "name": "token",
            "type": "tuple"
          },
          {
            "components": [
              {
                "internalType": "bytes32",
                "name": "salt",
                "type": "bytes32"
              },
              {
                "internalType": "bytes",
                "name": "initCode",
                "type": "bytes"
              },
              {
                "internalType": "address",
                "name": "expectedAddress",
                "type": "address"
              }
            ],
            "internalType": "struct MemeFolioAtomicLauncher.Deployment",
            "name": "hook",
            "type": "tuple"
          },
          {
            "components": [
              {
                "internalType": "bytes32",
                "name": "salt",
                "type": "bytes32"
              },
              {
                "internalType": "bytes",
                "name": "initCode",
                "type": "bytes"
              },
              {
                "internalType": "address",
                "name": "expectedAddress",
                "type": "address"
              }
            ],
            "internalType": "struct MemeFolioAtomicLauncher.Deployment",
            "name": "locker",
            "type": "tuple"
          },
          {
            "components": [
              {
                "internalType": "bytes32",
                "name": "salt",
                "type": "bytes32"
              },
              {
                "internalType": "bytes",
                "name": "initCode",
                "type": "bytes"
              },
              {
                "internalType": "address",
                "name": "expectedAddress",
                "type": "address"
              }
            ],
            "internalType": "struct MemeFolioAtomicLauncher.Deployment",
            "name": "distributor",
            "type": "tuple"
          },
          {
            "internalType": "string",
            "name": "tokenName",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "tokenSymbol",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "tokenSupply",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "minHolderBalance",
            "type": "uint256"
          },
          {
            "internalType": "address payable",
            "name": "executor",
            "type": "address"
          },
          {
            "internalType": "address payable",
            "name": "treasury",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "admin",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "operator",
            "type": "address"
          },
          {
            "internalType": "address[5]",
            "name": "rewardTokens",
            "type": "address[5]"
          },
          {
            "internalType": "address[]",
            "name": "excludedAccounts",
            "type": "address[]"
          },
          {
            "internalType": "uint256",
            "name": "maxDistributionBatchSize",
            "type": "uint256"
          },
          {
            "internalType": "uint24",
            "name": "poolFee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "uint160",
            "name": "initialSqrtPriceX96",
            "type": "uint160"
          }
        ],
        "internalType": "struct MemeFolioAtomicLauncher.LaunchData",
        "name": "data",
        "type": "tuple"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "token",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "hook",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "locker",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "distributor",
            "type": "address"
          },
          {
            "internalType": "bytes32",
            "name": "poolId",
            "type": "bytes32"
          },
          {
            "internalType": "uint256",
            "name": "positionTokenId",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "deploymentBlock",
            "type": "uint256"
          }
        ],
        "internalType": "struct MemeFolioAtomicLauncher.LaunchResult",
        "name": "result",
        "type": "tuple"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "components": [
              {
                "internalType": "bytes32",
                "name": "salt",
                "type": "bytes32"
              },
              {
                "internalType": "bytes",
                "name": "initCode",
                "type": "bytes"
              },
              {
                "internalType": "address",
                "name": "expectedAddress",
                "type": "address"
              }
            ],
            "internalType": "struct MemeFolioAtomicLauncher.Deployment",
            "name": "token",
            "type": "tuple"
          },
          {
            "components": [
              {
                "internalType": "bytes32",
                "name": "salt",
                "type": "bytes32"
              },
              {
                "internalType": "bytes",
                "name": "initCode",
                "type": "bytes"
              },
              {
                "internalType": "address",
                "name": "expectedAddress",
                "type": "address"
              }
            ],
            "internalType": "struct MemeFolioAtomicLauncher.Deployment",
            "name": "hook",
            "type": "tuple"
          },
          {
            "components": [
              {
                "internalType": "bytes32",
                "name": "salt",
                "type": "bytes32"
              },
              {
                "internalType": "bytes",
                "name": "initCode",
                "type": "bytes"
              },
              {
                "internalType": "address",
                "name": "expectedAddress",
                "type": "address"
              }
            ],
            "internalType": "struct MemeFolioAtomicLauncher.Deployment",
            "name": "locker",
            "type": "tuple"
          },
          {
            "components": [
              {
                "internalType": "bytes32",
                "name": "salt",
                "type": "bytes32"
              },
              {
                "internalType": "bytes",
                "name": "initCode",
                "type": "bytes"
              },
              {
                "internalType": "address",
                "name": "expectedAddress",
                "type": "address"
              }
            ],
            "internalType": "struct MemeFolioAtomicLauncher.Deployment",
            "name": "distributor",
            "type": "tuple"
          },
          {
            "internalType": "string",
            "name": "tokenName",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "tokenSymbol",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "tokenSupply",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "minHolderBalance",
            "type": "uint256"
          },
          {
            "internalType": "address payable",
            "name": "executor",
            "type": "address"
          },
          {
            "internalType": "address payable",
            "name": "treasury",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "admin",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "operator",
            "type": "address"
          },
          {
            "internalType": "address[5]",
            "name": "rewardTokens",
            "type": "address[5]"
          },
          {
            "internalType": "address[]",
            "name": "excludedAccounts",
            "type": "address[]"
          },
          {
            "internalType": "uint256",
            "name": "maxDistributionBatchSize",
            "type": "uint256"
          },
          {
            "internalType": "uint24",
            "name": "poolFee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "uint160",
            "name": "initialSqrtPriceX96",
            "type": "uint160"
          }
        ],
        "internalType": "struct MemeFolioAtomicLauncher.LaunchData",
        "name": "data",
        "type": "tuple"
      }
    ],
    "name": "launchManifestHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launched",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "manifestHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "permit2",
    "outputs": [
      {
        "internalType": "contract IAllowanceTransfer",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "contract IPositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IHooks } from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import { IPoolManager } from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol";
import { Currency, CurrencyLibrary } from "@uniswap/v4-core/src/types/Currency.sol";
import { PoolId, PoolIdLibrary } from "@uniswap/v4-core/src/types/PoolId.sol";
import { PoolKey } from "@uniswap/v4-core/src/types/PoolKey.sol";
import { IPositionManager } from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import { Actions } from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import { LiquidityAmounts } from "@uniswap/v4-periphery/src/libraries/LiquidityAmounts.sol";
import {
    PositionInfo,
    PositionInfoLibrary
} from "@uniswap/v4-periphery/src/libraries/PositionInfoLibrary.sol";
import { IAllowanceTransfer } from "permit2/src/interfaces/IAllowanceTransfer.sol";
import { MemeFolioDistributor } from "./MemeFolioDistributor.sol";
import { MemeFolioTaxHook } from "./MemeFolioTaxHook.sol";
import { MemeFolioToken } from "./MemeFolioToken.sol";
import { PermanentV4LiquidityLocker } from "./PermanentV4LiquidityLocker.sol";

/// @title MemeFolioAtomicLauncher
/// @notice One-shot launch coordinator committed to one complete Meme ETF launch manifest.
contract MemeFolioAtomicLauncher is Ownable, ReentrancyGuard {
    using Address for address payable;
    using PoolIdLibrary for PoolKey;
    using PositionInfoLibrary for PositionInfo;
    using SafeERC20 for IERC20;

    struct Deployment {
        bytes32 salt;
        bytes initCode;
        address expectedAddress;
    }

    struct LaunchData {
        Deployment token;
        Deployment hook;
        Deployment locker;
        Deployment distributor;
        string tokenName;
        string tokenSymbol;
        uint256 tokenSupply;
        uint256 minHolderBalance;
        address payable executor;
        address payable treasury;
        address admin;
        address operator;
        address[5] rewardTokens;
        address[] excludedAccounts;
        uint256 maxDistributionBatchSize;
        uint24 poolFee;
        int24 tickSpacing;
        int24 tickLower;
        int24 tickUpper;
        uint160 initialSqrtPriceX96;
    }

    struct LaunchResult {
        address token;
        address hook;
        address locker;
        address distributor;
        bytes32 poolId;
        uint256 positionTokenId;
        uint256 deploymentBlock;
    }

    address internal constant DEAD = 0x000000000000000000000000000000000000dEaD;

    IPoolManager public immutable poolManager;
    IPositionManager public immutable positionManager;
    IAllowanceTransfer public immutable permit2;

    bytes32 public manifestHash;
    bool public launched;

    error InvalidAddress();
    error InvalidDeployment(address expected, address actual);
    error InvalidLaunchConfiguration();
    error InvalidManifest();
    error LaunchInvariantFailed();
    error ManifestMismatch(bytes32 expected, bytes32 actual);
    error ManifestAlreadyCommitted();
    error NoManifestCommitted();

    event LaunchCommitted(bytes32 indexed manifestHash);
    event LaunchCompleted(
        bytes32 indexed manifestHash,
        address indexed token,
        address indexed hook,
        address locker,
        address distributor,
        bytes32 poolId,
        uint256 positionTokenId,
        uint256 deploymentBlock
    );

    constructor(
        address owner_,
        IPoolManager poolManager_,
        IPositionManager positionManager_,
        IAllowanceTransfer permit2_
    ) Ownable(owner_) {
        if (
            owner_ == address(0) || address(poolManager_) == address(0)
                || address(positionManager_) == address(0) || address(permit2_) == address(0)
        ) revert InvalidAddress();
        poolManager = poolManager_;
        positionManager = positionManager_;
        permit2 = permit2_;
    }

    function commitLaunch(bytes32 manifestHash_) external onlyOwner {
        if (manifestHash_ == bytes32(0)) revert InvalidManifest();
        if (manifestHash != bytes32(0)) revert ManifestAlreadyCommitted();
        manifestHash = manifestHash_;
        emit LaunchCommitted(manifestHash_);
    }

    /// @notice Hashes every launch input together with this chain and the immutable dependencies.
    function launchManifestHash(LaunchData calldata data) public view returns (bytes32) {
        bytes32 launchDataHash = keccak256(abi.encode(data));
        return keccak256(
            abi.encode(
                block.chainid,
                address(this),
                address(poolManager),
                address(positionManager),
                address(permit2),
                launchDataHash
            )
        );
    }

    /// @notice Creates and verifies the complete first-tradable Meme ETF state atomically.
    function launch(LaunchData calldata data)
        external
        onlyOwner
        nonReentrant
        returns (LaunchResult memory result)
    {
        bytes32 committed = manifestHash;
        if (committed == bytes32(0)) revert NoManifestCommitted();
        bytes32 actualManifest = launchManifestHash(data);
        if (actualManifest != committed) revert ManifestMismatch(committed, actualManifest);
        if (launched) revert LaunchInvariantFailed();

        _validateLaunchData(data);
        // Effects precede every external launch call. Any later failure reverts this flag together
        // with all CREATE2 deployments, while successful execution permanently blocks reentry.
        launched = true;
        _transferOwnership(address(0));
        result.token = _deploy(data.token);
        result.hook = _deploy(data.hook);
        result.locker = _deploy(data.locker);
        result.distributor = _deploy(data.distributor);
        _verifyComponents(data, result);

        PoolKey memory key = _poolKey(data, result);
        result.poolId = PoolId.unwrap(key.toId());
        {
            int24 initializedTick = poolManager.initialize(key, data.initialSqrtPriceX96);
            if (initializedTick != data.tickUpper) revert LaunchInvariantFailed();
        }
        if (MemeFolioTaxHook(payable(result.hook)).launchAuthority() != address(0)) {
            revert LaunchInvariantFailed();
        }

        uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1(
            TickMath.getSqrtPriceAtTick(data.tickLower), data.initialSqrtPriceX96, data.tokenSupply
        );
        if (liquidity == 0) revert InvalidLaunchConfiguration();
        result.positionTokenId = _mintPosition(data, result, key, liquidity);
        PermanentV4LiquidityLocker(result.locker).finalizeLock(result.positionTokenId);

        IERC20 token = IERC20(result.token);
        uint256 dust = token.balanceOf(address(this));
        if (dust != 0) token.safeTransfer(DEAD, dust);
        permit2.approve(result.token, address(positionManager), 0, 0);
        token.forceApprove(address(permit2), 0);
        uint256 forcedNative = address(this).balance;
        if (forcedNative != 0) data.treasury.sendValue(forcedNative);

        _verifyFinalState(data, result, token);
        result.deploymentBlock = block.number;
        emit LaunchCompleted(
            committed,
            result.token,
            result.hook,
            result.locker,
            result.distributor,
            result.poolId,
            result.positionTokenId,
            result.deploymentBlock
        );
    }

    function _validateLaunchData(LaunchData calldata data) private view {
        if (
            bytes(data.tokenName).length == 0 || bytes(data.tokenSymbol).length == 0
                || data.tokenSupply == 0 || data.minHolderBalance == 0
                || data.minHolderBalance > data.tokenSupply || data.executor == address(0)
                || data.treasury == address(0) || data.admin == address(0)
                || data.operator == address(0) || data.executor == data.treasury
                || data.poolFee != 0 || data.tickSpacing <= 0 || data.tickLower >= data.tickUpper
                || data.tickLower % data.tickSpacing != 0 || data.tickUpper % data.tickSpacing != 0
                || data.initialSqrtPriceX96 != TickMath.getSqrtPriceAtTick(data.tickUpper)
                || data.maxDistributionBatchSize == 0 || data.maxDistributionBatchSize > 100
        ) revert InvalidLaunchConfiguration();
        _validateDeployment(data.token);
        _validateDeployment(data.hook);
        _validateDeployment(data.locker);
        _validateDeployment(data.distributor);
        if (
            data.token.expectedAddress == data.hook.expectedAddress
                || data.token.expectedAddress == data.locker.expectedAddress
                || data.token.expectedAddress == data.distributor.expectedAddress
                || data.hook.expectedAddress == data.locker.expectedAddress
                || data.hook.expectedAddress == data.distributor.expectedAddress
                || data.locker.expectedAddress == data.distributor.expectedAddress
        ) revert InvalidLaunchConfiguration();
        for (uint256 i; i < data.rewardTokens.length; ++i) {
            address reward = data.rewardTokens[i];
            if (
                reward == address(0) || reward == data.token.expectedAddress
                    || reward.code.length == 0
            ) {
                revert InvalidLaunchConfiguration();
            }
            for (uint256 j; j < i; ++j) {
                if (data.rewardTokens[j] == reward) revert InvalidLaunchConfiguration();
            }
        }
    }

    function _validateDeployment(Deployment calldata deployment) private view {
        if (deployment.initCode.length == 0 || deployment.expectedAddress == address(0)) {
            revert InvalidLaunchConfiguration();
        }
        address computed = Create2.computeAddress(
            deployment.salt, keccak256(deployment.initCode), address(this)
        );
        if (computed != deployment.expectedAddress) {
            revert InvalidDeployment(deployment.expectedAddress, computed);
        }
        if (computed.code.length != 0) revert InvalidLaunchConfiguration();
    }

    function _deploy(Deployment calldata deployment) private returns (address deployed) {
        deployed = Create2.deploy(0, deployment.salt, deployment.initCode);
        if (deployed != deployment.expectedAddress) {
            revert InvalidDeployment(deployment.expectedAddress, deployed);
        }
    }

    function _verifyComponents(LaunchData calldata data, LaunchResult memory result) private view {
        MemeFolioToken token = MemeFolioToken(result.token);
        MemeFolioTaxHook hook = MemeFolioTaxHook(payable(result.hook));
        PermanentV4LiquidityLocker locker = PermanentV4LiquidityLocker(result.locker);
        MemeFolioDistributor distributor = MemeFolioDistributor(payable(result.distributor));
        if (
            keccak256(bytes(token.name())) != keccak256(bytes(data.tokenName))
                || keccak256(bytes(token.symbol())) != keccak256(bytes(data.tokenSymbol))
                || token.initialSupply() != data.tokenSupply
                || token.totalSupply() != data.tokenSupply
                || token.balanceOf(address(this)) != data.tokenSupply
                || token.minHolderBalance() != data.minHolderBalance
                || address(hook.poolManager()) != address(poolManager)
                || hook.projectToken() != result.token || hook.buybackWallet() != data.executor
                || hook.treasuryWallet() != data.treasury
                || hook.poolTickSpacing() != data.tickSpacing
                || hook.initialSqrtPriceX96() != data.initialSqrtPriceX96
                || hook.launchAuthority() != address(this)
                || address(locker.positionManager()) != address(positionManager)
                || locker.launchAuthority() != address(this)
                || locker.projectToken() != result.token || locker.hook() != result.hook
                || locker.poolFee() != data.poolFee || locker.tickSpacing() != data.tickSpacing
                || locker.tickLower() != data.tickLower || locker.tickUpper() != data.tickUpper
                || address(distributor.sourceToken()) != result.token
                || distributor.treasury() != data.treasury
                || distributor.minHolderBalance() != data.minHolderBalance
                || distributor.maxDistributionBatchSize() != data.maxDistributionBatchSize
                || !distributor.hasRole(distributor.DEFAULT_ADMIN_ROLE(), data.admin)
                || !distributor.hasRole(distributor.OPERATOR_ROLE(), data.operator)
                || distributor.rewardTokenCount() != data.rewardTokens.length
        ) revert LaunchInvariantFailed();
        for (uint256 i; i < data.rewardTokens.length; ++i) {
            if (distributor.rewardTokenAt(i) != data.rewardTokens[i]) {
                revert LaunchInvariantFailed();
            }
        }
        for (uint256 i; i < data.excludedAccounts.length; ++i) {
            if (!distributor.excludedFromRewards(data.excludedAccounts[i])) {
                revert LaunchInvariantFailed();
            }
        }
    }

    function _poolKey(LaunchData calldata data, LaunchResult memory result)
        private
        pure
        returns (PoolKey memory)
    {
        return PoolKey({
            currency0: CurrencyLibrary.ADDRESS_ZERO,
            currency1: Currency.wrap(result.token),
            fee: data.poolFee,
            tickSpacing: data.tickSpacing,
            hooks: IHooks(result.hook)
        });
    }

    function _mintPosition(
        LaunchData calldata data,
        LaunchResult memory result,
        PoolKey memory key,
        uint128 liquidity
    ) private returns (uint256 positionTokenId) {
        IERC20(result.token).forceApprove(address(permit2), type(uint256).max);
        permit2.approve(result.token, address(positionManager), type(uint160).max, type(uint48).max);
        uint256 firstCandidateTokenId = positionManager.nextTokenId();
        bytes memory actions = abi.encodePacked(
            uint8(Actions.MINT_POSITION),
            uint8(Actions.CLOSE_CURRENCY),
            uint8(Actions.CLOSE_CURRENCY)
        );
        bytes[] memory params = new bytes[](3);
        params[0] = abi.encode(
            key,
            data.tickLower,
            data.tickUpper,
            uint256(liquidity),
            uint128(0),
            uint128(data.tokenSupply),
            result.locker,
            bytes("")
        );
        params[1] = abi.encode(key.currency0);
        params[2] = abi.encode(key.currency1);
        positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp + 30 minutes);
        positionTokenId = _findCanonicalLockerPosition(
            firstCandidateTokenId,
            positionManager.nextTokenId(),
            result.locker,
            key,
            data.tickLower,
            data.tickUpper
        );
    }

    function _findCanonicalLockerPosition(
        uint256 firstTokenId,
        uint256 nextTokenId,
        address locker,
        PoolKey memory expectedKey,
        int24 expectedTickLower,
        int24 expectedTickUpper
    ) private view returns (uint256 bestTokenId) {
        uint128 bestLiquidity = 0;
        for (uint256 tokenId = firstTokenId; tokenId < nextTokenId; ++tokenId) {
            if (IERC721(address(positionManager)).ownerOf(tokenId) != locker) continue;
            (PoolKey memory key, PositionInfo info) =
                positionManager.getPoolAndPositionInfo(tokenId);
            if (
                Currency.unwrap(key.currency0) != Currency.unwrap(expectedKey.currency0)
                    || Currency.unwrap(key.currency1) != Currency.unwrap(expectedKey.currency1)
                    || key.fee != expectedKey.fee || key.tickSpacing != expectedKey.tickSpacing
                    || address(key.hooks) != address(expectedKey.hooks)
                    || info.tickLower() != expectedTickLower
                    || info.tickUpper() != expectedTickUpper
            ) continue;
            uint128 positionLiquidity = positionManager.getPositionLiquidity(tokenId);
            if (positionLiquidity > bestLiquidity) {
                bestLiquidity = positionLiquidity;
                bestTokenId = tokenId;
            }
        }
        if (bestTokenId == 0 || bestLiquidity == 0) revert LaunchInvariantFailed();
    }

    function _verifyFinalState(LaunchData calldata data, LaunchResult memory result, IERC20 token)
        private
        view
    {
        PermanentV4LiquidityLocker locker = PermanentV4LiquidityLocker(result.locker);
        (uint160 permitAmount,,) =
            permit2.allowance(result.token, address(this), address(positionManager));
        if (
            token.balanceOf(address(this)) != 0 || address(this).balance != 0
                || token.allowance(address(this), address(permit2)) != 0 || permitAmount != 0
                || MemeFolioTaxHook(payable(result.hook)).launchAuthority() != address(0)
                || locker.launchAuthority() != address(0)
                || locker.positionTokenId() != result.positionTokenId
                || IERC721(address(positionManager)).ownerOf(result.positionTokenId)
                    != result.locker || data.token.expectedAddress.code.length == 0
                || data.hook.expectedAddress.code.length == 0
                || data.locker.expectedAddress.code.length == 0
                || data.distributor.expectedAddress.code.length == 0
        ) revert LaunchInvariantFailed();
    }
}
Chain explorer2971msChain node91ms