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
- Creator
- 0x7B7DE29bc1…C6b0DaB694
- 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