SushiV3Factory

0xac707d8b6b72322aae38a797dbed1c11a3e62e27

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
16,922 bytes
ABI entries
45
22 read · 9 write
License
none

Contract information

Address
0xac707d8b6b72322aae38a797dbed1c11a3e62e27
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x315b43ea66…83c83193e0

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (22)

BPS()uint256
MAX_LIMIT_INTERVAL()uint64
MAX_LIMIT_VALUE()uint256
MAX_TARGET_MCAP()uint256
MIN_LIMIT_INTERVAL()uint64
MIN_LIMIT_START()uint256
MIN_TARGET_MCAP()uint256
POOL_FEE()uint24
TICK_SPACING()int24
TOTAL_SUPPLY()uint256
allTokens(uint256)address
antiPvpRegistry()address
config()address
feeManager()address
getLaunchInfo(bytes32)tuple
getPositionTokenId(bytes32, uint256)uint256
owner()address
poolIdByToken(address)bytes32
positionManager()address
totalLaunches()uint256
v3Factory()address
weth()address

Events (4)

AntiPVPRegistrySetFeeManagerSetOwnershipTransferredTokenLaunched

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract SushiConfig",
        "name": "_config",
        "type": "address"
      },
      {
        "internalType": "contract IUniswapV3Factory",
        "name": "_v3Factory",
        "type": "address"
      },
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "_positionManager",
        "type": "address"
      },
      {
        "internalType": "contract IWETH9",
        "name": "_weth",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_owner",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AntiPVPDuplicateCombo",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeManagerAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeManagerNotSet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidLimits",
    "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": [],
    "name": "VanitySaltBadOrientation",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "registry",
        "type": "address"
      }
    ],
    "name": "AntiPVPRegistrySet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "feeManager",
        "type": "address"
      }
    ],
    "name": "FeeManagerSet",
    "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"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "targetMcapETH",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      }
    ],
    "name": "TokenLaunched",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_LIMIT_INTERVAL",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_LIMIT_VALUE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_TARGET_MCAP",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_LIMIT_INTERVAL",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_LIMIT_START",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_TARGET_MCAP",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "POOL_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "antiPvpRegistry",
    "outputs": [
      {
        "internalType": "contract IAntiPVPRegistry",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "config",
    "outputs": [
      {
        "internalType": "contract SushiConfig",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "address",
            "name": "beneficiary",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "targetMcapETH",
            "type": "uint256"
          },
          {
            "internalType": "enum SushiV3Factory.FeeClaimMode",
            "name": "feeClaimMode",
            "type": "uint8"
          },
          {
            "components": [
              {
                "internalType": "address[5]",
                "name": "wallets",
                "type": "address[5]"
              },
              {
                "internalType": "uint16[5]",
                "name": "percentages",
                "type": "uint16[5]"
              },
              {
                "internalType": "uint8",
                "name": "count",
                "type": "uint8"
              }
            ],
            "internalType": "struct SushiV3Factory.FeeSplit",
            "name": "feeSplit",
            "type": "tuple"
          },
          {
            "internalType": "bool",
            "name": "isBranded",
            "type": "bool"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          },
          {
            "internalType": "uint256",
            "name": "devBuyEth",
            "type": "uint256"
          },
          {
            "internalType": "bool",
            "name": "isLimited",
            "type": "bool"
          },
          {
            "components": [
              {
                "internalType": "uint64",
                "name": "maxTxInterval",
                "type": "uint64"
              },
              {
                "internalType": "uint64",
                "name": "maxWalletInterval",
                "type": "uint64"
              },
              {
                "internalType": "uint256",
                "name": "maxTxStart",
                "type": "uint256"
              },
              {
                "internalType": "uint256",
                "name": "maxTxStep",
                "type": "uint256"
              },
              {
                "internalType": "uint256",
                "name": "maxWalletStart",
                "type": "uint256"
              },
              {
                "internalType": "uint256",
                "name": "maxWalletStep",
                "type": "uint256"
              }
            ],
            "internalType": "struct TimeLimits.TimeConfig",
            "name": "limits",
            "type": "tuple"
          }
        ],
        "internalType": "struct SushiV3Factory.CreateParams",
        "name": "params",
        "type": "tuple"
      }
    ],
    "name": "createLaunch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "address",
            "name": "beneficiary",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "targetMcapETH",
            "type": "uint256"
          },
          {
            "internalType": "enum SushiV3Factory.FeeClaimMode",
            "name": "feeClaimMode",
            "type": "uint8"
          },
          {
            "components": [
              {
                "internalType": "address[5]",
                "name": "wallets",
                "type": "address[5]"
              },
              {
                "internalType": "uint16[5]",
                "name": "percentages",
                "type": "uint16[5]"
              },
              {
                "internalType": "uint8",
                "name": "count",
                "type": "uint8"
              }
            ],
            "internalType": "struct SushiV3Factory.FeeSplit",
            "name": "feeSplit",
            "type": "tuple"
          },
          {
            "internalType": "bool",
            "name": "isBranded",
            "type": "bool"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          },
          {
            "internalType": "uint256",
            "name": "devBuyEth",
            "type": "uint256"
          },
          {
            "internalType": "bool",
            "name": "isLimited",
            "type": "bool"
          },
          {
            "components": [
              {
                "internalType": "uint64",
                "name": "maxTxInterval",
                "type": "uint64"
              },
              {
                "internalType": "uint64",
                "name": "maxWalletInterval",
                "type": "uint64"
              },
              {
                "internalType": "uint256",
                "name": "maxTxStart",
                "type": "uint256"
              },
              {
                "internalType": "uint256",
                "name": "maxTxStep",
                "type": "uint256"
              },
              {
                "internalType": "uint256",
                "name": "maxWalletStart",
                "type": "uint256"
              },
              {
                "internalType": "uint256",
                "name": "maxWalletStep",
                "type": "uint256"
              }
            ],
            "internalType": "struct TimeLimits.TimeConfig",
            "name": "limits",
            "type": "tuple"
          }
        ],
        "internalType": "struct SushiV3Factory.CreateParams",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "createLaunchFor",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeManager",
    "outputs": [
      {
        "internalType": "contract SushiV3FeeManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "getLaunchInfo",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "token",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "pool",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "beneficiary",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "bytes32",
            "name": "poolId",
            "type": "bytes32"
          },
          {
            "internalType": "uint256",
            "name": "targetMcapETH",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "createdAt",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "createdBlock",
            "type": "uint256"
          },
          {
            "internalType": "enum SushiV3Factory.FeeClaimMode",
            "name": "feeClaimMode",
            "type": "uint8"
          },
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "components": [
              {
                "internalType": "address[5]",
                "name": "wallets",
                "type": "address[5]"
              },
              {
                "internalType": "uint16[5]",
                "name": "percentages",
                "type": "uint16[5]"
              },
              {
                "internalType": "uint8",
                "name": "count",
                "type": "uint8"
              }
            ],
            "internalType": "struct SushiV3Factory.FeeSplit",
            "name": "feeSplit",
            "type": "tuple"
          },
          {
            "internalType": "uint256[5]",
            "name": "positionTokenIds",
            "type": "uint256[5]"
          },
          {
            "internalType": "bool[5]",
            "name": "positionMinted",
            "type": "bool[5]"
          }
        ],
        "internalType": "struct SushiV3Factory.LaunchInfo",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "getPositionTokenId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "lockTokenLimits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "poolIdByToken",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rescueETH",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_registry",
        "type": "address"
      }
    ],
    "name": "setAntiPVPRegistry",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract SushiV3FeeManager",
        "name": "_feeManager",
        "type": "address"
      }
    ],
    "name": "setFeeManager",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "exempt",
        "type": "bool"
      }
    ],
    "name": "setTokenLimitExempt",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalLaunches",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "v3Factory",
    "outputs": [
      {
        "internalType": "contract IUniswapV3Factory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "contract IWETH9",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import {IUniswapV3Factory, IUniswapV3Pool, INonfungiblePositionManager, IWETH9} from "../interfaces/ISushiV3.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {LimitedToken} from "./LimitedToken.sol";
import {SushiConfig} from "./SushiConfig.sol";
import {SushiV3FeeManager} from "./SushiV3FeeManager.sol";
import {PriceMath} from "../libraries/PriceMath.sol";
import {TimeLimits} from "../libraries/TimeLimits.sol";
import {TokenDeployer} from "../libraries/TokenDeployer.sol";

interface IAntiPVPRegistry {
    function isComboAvailable(string calldata name, string calldata symbol) external view returns (bool);
    function registerCombo(string calldata name, string calldata symbol) external;
}

/**
 * @title SushiV3Factory
 * @notice Launch orchestration + registry half of the Sushi V3 (clAMM) launcher.
 *         Split out of the original single-contract design to stay under the 24KB
 *         EIP-170 limit: this contract deploys tokens, creates/initializes pools,
 *         and keeps the launch registry; its sibling SushiV3FeeManager holds the
 *         NFT positions, runs swaps, and collects/splits LP fees.
 *
 * Per-launch flow (all atomic in the launch tx):
 *   1. Deploy the launch token (CREATE2, deterministic salt). Full supply -> factory.
 *   2. Create + initialize the Sushi V3 pool (WETH / token, 1% tier).
 *   3. Transfer the full supply to the FeeManager and (ADVANCED) exempt the pool,
 *      NPM and FeeManager from token limits.
 *   4. FeeManager.registerAndMint: wrap the bootstrap ETH and mint the 5 positions.
 *   5. Optional atomic dev-buy via FeeManager.devBuy: WETH -> token to the creator.
 *
 * The ProofOfSteak fee-source interface (collectFeesFromPosition / claim /
 * claimable) lives on the FeeManager now, so ProofOfSteak's positionHelper must
 * point at the FeeManager (KitchenSushi reads factory.feeManager()).
 */
contract SushiV3Factory is Ownable, ReentrancyGuard {
    uint256 public constant TOTAL_SUPPLY = 1_000_000_000 * 1e18;
    uint256 public constant MIN_TARGET_MCAP = 1 ether;
    uint256 public constant MAX_TARGET_MCAP = 10 ether;
    uint256 public constant BPS = 10000;

    /// @notice Sushi V3 1% fee tier (tickSpacing 200 — matches DIRECT's spacing).
    uint24 public constant POOL_FEE = 10000;
    int24 public constant TICK_SPACING = 200;

    // ---- Time-limit bounds (validated at launch) ----------------------------
    /// @notice Floor for the STARTING maxTx / maxWallet, so a launch cannot brick
    ///         buys with a dust cap. 0.1% of supply = 1,000,000 tokens.
    uint256 public constant MIN_LIMIT_START = TOTAL_SUPPLY / 1000;
    /// @notice Any limit start/step above supply is meaningless (effectively no
    ///         limit) — reject it so intent stays honest (use all-zeros for none).
    uint256 public constant MAX_LIMIT_VALUE = TOTAL_SUPPLY;
    /// @notice Bounds on a ramp interval when its step is non-zero (seconds).
    uint64 public constant MIN_LIMIT_INTERVAL = 1;
    uint64 public constant MAX_LIMIT_INTERVAL = 60;

    enum FeeClaimMode { BOTH, ETH_ONLY, TOKEN_ONLY }

    struct FeeSplit {
        address[5] wallets;
        uint16[5] percentages;
        uint8 count;
    }

    struct CreateParams {
        string name;
        string symbol;
        address beneficiary;
        uint256 targetMcapETH;
        FeeClaimMode feeClaimMode;
        FeeSplit feeSplit;
        bool isBranded;
        bytes32 salt;
        uint256 devBuyEth;
        // Route selector:
        //   isLimited = false => DIRECT: deploy a plain DirectToken with NO limit
        //     code; `limits` is ignored (and must be all-zero).
        //   isLimited = true  => ADVANCED: deploy a LimitedToken and apply `limits`.
        bool isLimited;
        // Time-limit curve (ADVANCED only). Must be all-zero when isLimited == false.
        TimeLimits.TimeConfig limits;
    }

    struct LaunchInfo {
        address token;
        address pool;
        address beneficiary;
        address creator;
        bytes32 poolId;
        uint256 targetMcapETH;
        uint256 createdAt;
        uint256 createdBlock;
        FeeClaimMode feeClaimMode;
        string name;
        string symbol;
        FeeSplit feeSplit;
        uint256[5] positionTokenIds;
        bool[5] positionMinted;
    }

    SushiConfig public immutable config;
    IUniswapV3Factory public immutable v3Factory;
    INonfungiblePositionManager public immutable positionManager;
    IWETH9 public immutable weth;

    /// @notice Position + swap + fee sibling. Holds the NFTs, runs swaps, collects
    ///         fees. Set once after deploy (owner-gated). ProofOfSteak's
    ///         positionHelper points here.
    SushiV3FeeManager public feeManager;

    uint256 public totalLaunches;
    mapping(bytes32 => LaunchInfo) internal _launchByPoolId;
    mapping(address => bytes32) public poolIdByToken;
    address[] public allTokens;

    IAntiPVPRegistry public antiPvpRegistry;

    event FeeManagerSet(address indexed feeManager);
    event TokenLaunched(
        address indexed token,
        address indexed pool,
        address indexed creator,
        bytes32 poolId,
        uint256 targetMcapETH,
        uint160 sqrtPriceX96,
        string name,
        string symbol
    );
    event AntiPVPRegistrySet(address indexed registry);

    error AntiPVPDuplicateCombo();
    error InvalidLimits(); // dev-supplied time-limit config out of bounds
    error VanitySaltBadOrientation(); // paid vanity salt produced token <= WETH; re-mine it
    error FeeManagerNotSet();
    error FeeManagerAlreadySet();

    constructor(
        SushiConfig _config,
        IUniswapV3Factory _v3Factory,
        INonfungiblePositionManager _positionManager,
        IWETH9 _weth,
        address _owner
    ) Ownable(_owner) {
        config = _config;
        v3Factory = _v3Factory;
        positionManager = _positionManager;
        weth = _weth;
    }

    /// @notice One-time wiring of the FeeManager sibling. Owner-gated; can only be
    ///         set once (immutable after deploy wiring).
    function setFeeManager(SushiV3FeeManager _feeManager) external onlyOwner {
        require(address(_feeManager) != address(0), "Zero fee manager");
        if (address(feeManager) != address(0)) revert FeeManagerAlreadySet();
        feeManager = _feeManager;
        emit FeeManagerSet(address(_feeManager));
    }

    function setAntiPVPRegistry(address _registry) external onlyOwner {
        antiPvpRegistry = IAntiPVPRegistry(_registry);
        emit AntiPVPRegistrySet(_registry);
    }

    // ------------------------------------------------------------------
    // LAUNCH
    // ------------------------------------------------------------------

    function createLaunch(CreateParams calldata params)
        external
        payable
        returns (address token, bytes32 poolId)
    {
        return _createLaunch(params, msg.sender);
    }

    /// @notice Launch on behalf of `creator` (used by KitchenSushi so the real dev
    ///         EOA is recorded as creator and receives any dev-buy tokens).
    function createLaunchFor(CreateParams calldata params, address creator)
        external
        payable
        returns (address token, bytes32 poolId)
    {
        require(msg.sender == config.kitchen(), "Only kitchen");
        require(creator != address(0), "Zero creator");
        return _createLaunch(params, creator);
    }

    function _createLaunch(CreateParams calldata params, address creator)
        internal
        nonReentrant
        returns (address token, bytes32 poolId)
    {
        if (address(feeManager) == address(0)) revert FeeManagerNotSet();
        require(!config.paused(), "Paused");

        require(bytes(params.name).length > 0 && bytes(params.name).length <= 32, "Invalid name");
        require(bytes(params.symbol).length > 0 && bytes(params.symbol).length <= 10, "Invalid symbol");
        require(params.beneficiary != address(0), "Zero beneficiary");
        require(
            params.targetMcapETH >= MIN_TARGET_MCAP && params.targetMcapETH <= MAX_TARGET_MCAP, "Invalid MCAP"
        );
        require(uint8(params.feeClaimMode) <= uint8(FeeClaimMode.TOKEN_ONLY), "Invalid claim mode");
        _validateFeeSplit(params.feeSplit);
        _validateLimits(params.isLimited, params.limits);

        if (address(antiPvpRegistry) != address(0)) {
            if (!antiPvpRegistry.isComboAvailable(params.name, params.symbol)) {
                revert AntiPVPDuplicateCombo();
            }
        }

        // Exact payment: bootstrap + service fees + dev-buy, reconciled exactly.
        uint256 minBootstrap = config.MIN_BOOTSTRAP_ETH();
        uint256 feesForTreasury;
        if (!params.isBranded) feesForTreasury += 0.001 ether;
        if (params.salt != bytes32(0)) feesForTreasury += 0.0005 ether;
        require(msg.value == minBootstrap + feesForTreasury + params.devBuyEth, "Exact payment required");

        if (feesForTreasury > 0) {
            (bool ok,) = config.treasury().call{value: feesForTreasury}("");
            require(ok, "Treasury transfer failed");
        }

        // 1. Deploy token with auto-orientation: the factory internally finds a
        //    CREATE2 salt whose token address sorts ABOVE WETH (so WETH is token0,
        //    which the pricing math requires) — for FREE and WITHOUT altering the
        //    user's name/symbol. The exact ticker the user chose is always preserved.
        (token,) = _deployTokenOriented(params, params.salt);

        // 2. Create + initialize the pool.
        address pool = v3Factory.getPool(address(weth), token, POOL_FEE);
        if (pool == address(0)) {
            pool = v3Factory.createPool(address(weth), token, POOL_FEE);
        }
        uint160 sqrtPriceX96 = PriceMath.calculateSqrtPrice(params.targetMcapETH, TOTAL_SUPPLY, TICK_SPACING);
        // Only initialize once (createPool leaves it uninitialized).
        (uint160 existing,,,,,,) = IUniswapV3Pool(pool).slot0();
        if (existing == 0) {
            IUniswapV3Pool(pool).initialize(sqrtPriceX96);
        }

        poolId = keccak256(abi.encode(address(weth), token, POOL_FEE));

        // ADVANCED only: exempt the LAUNCH ACTORS (NPM + FeeManager; factory and
        // controller are already exempt in the token constructor) so the supply
        // move, position minting and dev-buy bypass limits. The POOL is
        // deliberately NOT exempted — buys (`pool -> buyer`) and sells
        // (`seller -> pool`) are exactly what maxTx/maxWallet must cap. The pool is
        // registered via setPool() so the token skips maxWallet only when the pool
        // is the recipient (a sell can't be blocked by the pool's own balance).
        // DIRECT tokens have no limit code, so nothing to configure.
        if (params.isLimited) {
            address[] memory exempts = new address[](2);
            exempts[0] = address(positionManager);
            exempts[1] = address(feeManager);
            LimitedToken(token).setLimitExemptBatch(exempts, true);
            LimitedToken(token).setPool(pool);
        }

        // Register launch (positions live on the FeeManager; getters read them there).
        LaunchInfo storage info = _launchByPoolId[poolId];
        info.token = token;
        info.pool = pool;
        info.beneficiary = params.beneficiary;
        info.creator = creator;
        info.poolId = poolId;
        info.targetMcapETH = params.targetMcapETH;
        info.createdAt = block.timestamp;
        info.createdBlock = block.number;
        info.feeClaimMode = params.feeClaimMode;
        info.name = params.name;
        info.symbol = params.symbol;
        info.feeSplit = params.feeSplit;

        poolIdByToken[token] = poolId;
        allTokens.push(token);
        totalLaunches++;

        emit TokenLaunched(token, pool, creator, poolId, params.targetMcapETH, sqrtPriceX96, params.name, params.symbol);

        if (address(antiPvpRegistry) != address(0)) {
            antiPvpRegistry.registerCombo(params.name, params.symbol);
        }

        // 3. Move the full supply to the FeeManager, then bootstrap liquidity:
        //    wrap ETH -> WETH, mint 5 positions (done inside registerAndMint).
        IERC20(token).transfer(address(feeManager), TOTAL_SUPPLY);

        feeManager.registerAndMint{value: minBootstrap}(
            SushiV3FeeManager.RegisterParams({
                poolId: poolId,
                token: token,
                pool: pool,
                beneficiary: params.beneficiary,
                targetMcapETH: params.targetMcapETH,
                feeClaimMode: SushiV3FeeManager.FeeClaimMode(uint8(params.feeClaimMode)),
                feeSplit: _toFmSplit(params.feeSplit),
                bootstrapETH: minBootstrap
            })
        );

        // 4. Atomic dev-buy (guaranteed first swap), delivered to the creator.
        if (params.devBuyEth > 0) {
            feeManager.devBuy{value: params.devBuyEth}(poolId, creator, params.devBuyEth);
        }

        return (token, poolId);
    }

    /// @dev Translate the factory's FeeSplit into the FeeManager's identical struct.
    function _toFmSplit(FeeSplit calldata fs) internal pure returns (SushiV3FeeManager.FeeSplit memory out) {
        out.wallets = fs.wallets;
        out.percentages = fs.percentages;
        out.count = fs.count;
    }

    function _deployToken(CreateParams calldata params, bytes32 salt) internal returns (address token) {
        // Delegatecalled: the factory is still the CREATE2 deployer + limit admin,
        // so token addresses are unchanged (deterministic mining preserved).
        return TokenDeployer.deploy(
            params.isLimited,
            params.isBranded,
            salt,
            params.name,
            params.symbol,
            address(this),
            params.beneficiary,
            params.limits
        );
    }

    // ------------------------------------------------------------------
    // TOKEN ADMIN PASS-THROUGH (factory is the token limitAdmin)
    // ------------------------------------------------------------------

    /// @notice Set an extra limit exemption on a launched token (owner-only).
    function setTokenLimitExempt(address token, address account, bool exempt) external onlyOwner {
        LimitedToken(token).setLimitExempt(account, exempt);
    }

    /// @notice Renounce limit-admin on a token so exemptions are frozen.
    function lockTokenLimits(address token) external onlyOwner {
        LimitedToken(token).lockLimits();
    }

    // ------------------------------------------------------------------
    // VIEWS
    // ------------------------------------------------------------------

    /// @notice Full launch record. positionTokenIds / positionMinted are read from
    ///         the FeeManager (which holds the NFTs) so the struct stays complete.
    function getLaunchInfo(bytes32 poolId) external view returns (LaunchInfo memory) {
        LaunchInfo memory info = _launchByPoolId[poolId];
        if (address(feeManager) != address(0) && info.token != address(0)) {
            info.positionTokenIds = feeManager.getPositionTokenIds(poolId);
            info.positionMinted = feeManager.getPositionsMinted(poolId);
        }
        return info;
    }

    function getPositionTokenId(bytes32 poolId, uint256 index) external view returns (uint256) {
        return feeManager.getPositionTokenId(poolId, index);
    }

    // ------------------------------------------------------------------
    // INTERNAL HELPERS
    // ------------------------------------------------------------------

    function _validateFeeSplit(FeeSplit calldata fs) internal pure {
        if (fs.count == 0) return;
        require(fs.count <= 5, "Too many split wallets");
        uint256 total;
        for (uint256 i = 0; i < fs.count; i++) {
            require(fs.wallets[i] != address(0), "Zero split wallet");
            total += fs.percentages[i];
        }
        require(total == BPS, "Split must sum to 100%");
    }

    /// @dev Bound the dev-supplied time-limit config.
    ///      - DIRECT (isLimited == false): the config MUST be all-zero (DIRECT
    ///        tokens have no limit code, so a non-zero config would be a silent
    ///        no-op — reject it to keep intent honest).
    ///      - ADVANCED (isLimited == true): all-zero == "no limits", allowed;
    ///        otherwise maxTxStart >= 0.1% supply, maxWalletStart >= maxTxStart,
    ///        no start/step above total supply, and any non-zero step needs a
    ///        1..60s interval (a zero step == flat cap, interval ignored).
    ///        Limits auto-lift at 1800s (30 min) regardless of step/interval.
    function _validateLimits(bool isLimited, TimeLimits.TimeConfig calldata c) internal pure {
        bool allZero = c.maxTxStart == 0 && c.maxTxStep == 0 && c.maxWalletStart == 0 && c.maxWalletStep == 0
            && c.maxTxInterval == 0 && c.maxWalletInterval == 0;

        if (!isLimited) {
            // DIRECT: limits config must be empty.
            if (!allZero) revert InvalidLimits();
            return;
        }

        if (allZero) return; // ADVANCED with no limits

        // Starting caps must be usable and sane.
        if (c.maxTxStart < MIN_LIMIT_START || c.maxTxStart > MAX_LIMIT_VALUE) revert InvalidLimits();
        if (c.maxWalletStart < c.maxTxStart || c.maxWalletStart > MAX_LIMIT_VALUE) revert InvalidLimits();

        // Steps can't exceed supply.
        if (c.maxTxStep > MAX_LIMIT_VALUE || c.maxWalletStep > MAX_LIMIT_VALUE) revert InvalidLimits();

        // A non-zero ramp step needs a bounded interval (also avoids div-by-zero).
        if (c.maxTxStep != 0) {
            if (c.maxTxInterval < MIN_LIMIT_INTERVAL || c.maxTxInterval > MAX_LIMIT_INTERVAL) revert InvalidLimits();
        }
        if (c.maxWalletStep != 0) {
            if (c.maxWalletInterval < MIN_LIMIT_INTERVAL || c.maxWalletInterval > MAX_LIMIT_INTERVAL) {
                revert InvalidLimits();
            }
        }
    }

    /// @dev Deterministic salt for vanity mining: chainid + factory + name + symbol
    ///      + user salt. Independent of creator/totalLaunches so an off-chain miner
    ///      can compute the exact CREATE2 address.
    /// @dev Salt now includes an auto-orientation `nonce` (see _deployTokenOriented).
    ///      The nonce is bumped internally, for free, until the predicted token
    ///      address sorts ABOVE WETH (so WETH is always token0). name/symbol/userSalt
    ///      are UNCHANGED — the user's exact ticker is always preserved.
    function _tokenSalt(bytes32 userSalt, string calldata name, string calldata symbol, uint256 nonce)
        internal
        view
        returns (bytes32)
    {
        return keccak256(abi.encode("SUSHI_V3_TOKEN_V1", block.chainid, address(this), name, symbol, userSalt, nonce));
    }

    /// @dev Predict the CREATE2 address for a given salt + init code hash (the
    ///      factory is the deployer). Standard EIP-1014 formula.
    function _predictCreate2(bytes32 salt, bytes32 initCodeHash) internal view returns (address) {
        return address(
            uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash))))
        );
    }

    /// @dev Deploy the launch token with a WETH-token0 orientation.
    ///
    ///      TWO paths, so free auto-orientation and paid vanity don't collide:
    ///
    ///      - FREE (userSalt == 0): the factory internally bumps a hidden nonce until
    ///        the predicted CREATE2 address sorts ABOVE WETH. Any name/symbol lands
    ///        first try, the exact ticker is preserved, and no vanity fee is charged.
    ///        Predicting the address per attempt is cheap (pure hashing); only ONE
    ///        real deploy happens.
    ///
    ///      - VANITY (userSalt != 0): the caller mined this salt OFF-CHAIN to produce
    ///        both their desired vanity address AND a valid orientation (token > WETH).
    ///        We honor their EXACT salt (nonce fixed at 0, no internal bump) so the
    ///        address they mined is the address they get. If it does not sort above
    ///        WETH, we revert (VanitySaltBadOrientation) BEFORE deploying so the fee
    ///        isn't wasted — they re-mine a valid vanity salt. The vanity fee is
    ///        charged in _createLaunch based on userSalt != 0.
    function _deployTokenOriented(CreateParams calldata params, bytes32 userSalt)
        internal
        returns (address token, bytes32 salt)
    {
        bytes32 initHash = TokenDeployer.initCodeHash(
            params.isLimited, params.isBranded, params.name, params.symbol, address(this), params.beneficiary, params.limits
        );

        if (userSalt != bytes32(0)) {
            // Paid vanity: use the caller's exact salt; require a valid orientation.
            salt = _tokenSalt(userSalt, params.name, params.symbol, 0);
            address predicted = _predictCreate2(salt, initHash);
            if (predicted <= address(weth)) revert VanitySaltBadOrientation();
            token = _deployToken(params, salt);
            require(token == predicted, "predict mismatch");
            return (token, salt);
        }

        // Free auto-orientation: bump the hidden nonce until token > WETH.
        for (uint256 nonce = 0; nonce < 256; nonce++) {
            salt = _tokenSalt(userSalt, params.name, params.symbol, nonce);
            address predicted = _predictCreate2(salt, initHash);
            if (predicted > address(weth)) {
                token = _deployToken(params, salt);
                require(token == predicted, "predict mismatch");
                return (token, salt);
            }
        }
        revert("orientation not found"); // ~256 tries: probability of failing is ~2^-106
    }

    /// @notice Rescue parked ETH to treasury.
    function rescueETH() external onlyOwner {
        uint256 bal = address(this).balance;
        (bool ok,) = config.treasury().call{value: bal}("");
        require(ok, "rescue failed");
    }

    receive() external payable {}
}
Chain explorer7459msChain node89ms