SushiLaunchpad
0x30dd6230ead9312d5d00ad58ef6ef6a0093b0554
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
16,413 bytes
ABI entries
78
12 read · 11 write
License
none
Contract information
- Address
- 0x30dd6230ead9312d5d00ad58ef6ef6a0093b0554
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x104Cf4b063…437708C223
- Creation tx
- 0x8cb9c05029…6e7dcd2964
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (12)
defaultSushiFeeBps() → uint16
launchFee() → uint256
launchInfo(address) → tuple
onERC721Received(address, address, uint256, bytes) → bytes4
owner() → address
pendingOwner() → address
positionManager() → address
protocolRecipient() → address
protocolReserveBps() → uint16
renounceOwnership()
sushiFeeBps(address) → uint16
v3Factory() → address
Events (12)
DefaultSushiFeeBpsUpdatedFeesDistributedLaunchFeeUpdatedLaunchFeesWithdrawnOwnershipTransferStartedOwnershipTransferredPositionCreatedProtocolRecipientUpdatedProtocolReserveBpsUpdatedProtocolReserveWithdrawnSushiFeeBpsUpdatedTokenLaunched
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
},
{
"internalType": "address",
"name": "initialProtocolRecipient",
"type": "address"
},
{
"internalType": "address",
"name": "factory_",
"type": "address"
},
{
"internalType": "address",
"name": "positionManager_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "uint64",
"name": "deadline",
"type": "uint64"
}
],
"name": "DeadlineExpired",
"type": "error"
},
{
"inputs": [],
"name": "EmptyName",
"type": "error"
},
{
"inputs": [],
"name": "EmptyRanges",
"type": "error"
},
{
"inputs": [],
"name": "EmptySymbol",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "used",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "desired",
"type": "uint256"
}
],
"name": "ExcessTokenConsumption",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "required",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "supplied",
"type": "uint256"
}
],
"name": "InsufficientLaunchFee",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"name": "InvalidFeeBps",
"type": "error"
},
{
"inputs": [],
"name": "InvalidPositionManager",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "quoteToken",
"type": "address"
}
],
"name": "InvalidQuoteToken",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"internalType": "int24",
"name": "startTick",
"type": "int24"
},
{
"internalType": "int24",
"name": "endTick",
"type": "int24"
}
],
"name": "InvalidRangeOrder",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"name": "InvalidReserveBps",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"internalType": "int24",
"name": "tick",
"type": "int24"
}
],
"name": "InvalidTickBounds",
"type": "error"
},
{
"inputs": [
{
"internalType": "int24",
"name": "actualTickSpacing",
"type": "int24"
}
],
"name": "InvalidTickSpacing",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"internalType": "int24",
"name": "tick",
"type": "int24"
}
],
"name": "InvalidTickSpacingAt",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "length",
"type": "uint256"
}
],
"name": "NameTooLong",
"type": "error"
},
{
"inputs": [],
"name": "NativeTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "NoCleanTokenAddress",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"internalType": "int24",
"name": "previousEndTick",
"type": "int24"
},
{
"internalType": "int24",
"name": "startTick",
"type": "int24"
}
],
"name": "NonContiguousRanges",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "NonzeroQuoteTokenUsed",
"type": "error"
},
{
"inputs": [],
"name": "NothingToWithdraw",
"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": "OwnershipRenunciationDisabled",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"name": "PoolConfigurationMismatch",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "expected",
"type": "address"
},
{
"internalType": "address",
"name": "actual",
"type": "address"
}
],
"name": "PoolCreationMismatch",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint160",
"name": "expected",
"type": "uint160"
},
{
"internalType": "uint160",
"name": "actual",
"type": "uint160"
}
],
"name": "PoolInitializationMismatch",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
}
],
"name": "PositionConfigurationMismatch",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "RangeAmountOverflow",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "supplied",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "allocation",
"type": "uint256"
}
],
"name": "RangeAmountsExceedAllocation",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "ReserveAlreadyWithdrawn",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint64",
"name": "unlockAt",
"type": "uint64"
}
],
"name": "ReserveStillLocked",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "expected",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "accounted",
"type": "uint256"
}
],
"name": "SupplyReconciliationFailure",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "length",
"type": "uint256"
}
],
"name": "SymbolTooLong",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "supplied",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maximum",
"type": "uint256"
}
],
"name": "TooManyRanges",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"name": "UnexpectedExistingPool",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "collection",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "address",
"name": "from",
"type": "address"
}
],
"name": "UnexpectedNft",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "UnknownToken",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "ZeroLiquidity",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "ZeroRangeAmount",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "previousBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "newBps",
"type": "uint16"
}
],
"name": "DefaultSushiFeeBpsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "caller",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "protocolRecipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint16",
"name": "sushiFeeBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint256",
"name": "quoteCollected",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenCollected",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "quoteToSushi",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenToSushi",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "quoteToCreator",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenToCreator",
"type": "uint256"
}
],
"name": "FeesDistributed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "previousFee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newFee",
"type": "uint256"
}
],
"name": "LaunchFeeUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "LaunchFeesWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"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": "uint256",
"name": "positionId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"indexed": false,
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenDesired",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenUsed",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"name": "PositionCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousRecipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "ProtocolRecipientUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "previousBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "newBps",
"type": "uint16"
}
],
"name": "ProtocolReserveBpsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "ProtocolReserveWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint16",
"name": "previousSushiFeeBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "newSushiFeeBps",
"type": "uint16"
}
],
"name": "SushiFeeBpsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "quoteToken",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "uint8",
"name": "decimals",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalSupply",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint16",
"name": "reserveBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint256",
"name": "reserveAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint64",
"name": "reserveUnlockAt",
"type": "uint64"
},
{
"indexed": false,
"internalType": "uint16",
"name": "initialSushiFeeBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint256",
"name": "positionCount",
"type": "uint256"
}
],
"name": "TokenLaunched",
"type": "event"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "defaultSushiFeeBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "distributeFees",
"outputs": [
{
"internalType": "uint256",
"name": "quoteCollected",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenCollected",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "quoteToSushi",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenToSushi",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
}
],
"internalType": "struct ISushiLaunchpad.TokenConfig",
"name": "tokenConfig",
"type": "tuple"
},
{
"internalType": "address",
"name": "quoteToken",
"type": "address"
},
{
"components": [
{
"internalType": "int24",
"name": "startTick",
"type": "int24"
},
{
"internalType": "int24",
"name": "endTick",
"type": "int24"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"internalType": "struct ISushiLaunchpad.SaleRange[]",
"name": "ranges",
"type": "tuple[]"
},
{
"internalType": "uint64",
"name": "deadline",
"type": "uint64"
}
],
"name": "launch",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "positionIds",
"type": "uint256[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "launchFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "launchInfo",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "quoteToken",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint64",
"name": "reserveUnlockAt",
"type": "uint64"
},
{
"internalType": "bool",
"name": "reserveWithdrawn",
"type": "bool"
},
{
"internalType": "uint256",
"name": "reserveAmount",
"type": "uint256"
}
],
"internalType": "struct ISushiLaunchpad.LaunchInfo",
"name": "info",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "protocolRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "protocolReserveBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "newSushiFeeBps",
"type": "uint16"
}
],
"name": "setDefaultSushiFeeBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newLaunchFee",
"type": "uint256"
}
],
"name": "setLaunchFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "setProtocolRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "newReserveBps",
"type": "uint16"
}
],
"name": "setProtocolReserveBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint16",
"name": "newSushiFeeBps",
"type": "uint16"
}
],
"name": "setSushiFeeBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "sushiFeeBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "v3Factory",
"outputs": [
{
"internalType": "contract ISushiV3Factory",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "withdrawLaunchFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "withdrawProtocolReserve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {ISushiLaunchpad} from "./interfaces/ISushiLaunchpad.sol";
import {
INonfungiblePositionManager,
ISushiV3Factory,
ISushiV3Pool
} from "./interfaces/ISushiV3.sol";
import {TickMath} from "./libraries/TickMath.sol";
import {
TOKEN_DECIMALS,
TOKEN_TOTAL_SUPPLY
} from "./libraries/TokenConstants.sol";
import {SushiLaunchpadToken} from "./SushiLaunchpadToken.sol";
/// @title SushiLaunchpad
/// @notice Creates fixed-supply tokens and permanently custodies their SushiSwap V3 positions.
contract SushiLaunchpad is
ISushiLaunchpad,
Ownable2Step,
ReentrancyGuard,
IERC721Receiver
{
using SafeERC20 for IERC20;
/// @dev Values snapshotted before collecting a token's positions so one
/// distribution cannot mix recipients or fee splits.
struct FeeDistributionCache {
address creator;
address sushiRecipient;
address pool;
address quoteToken;
uint16 bps;
uint256 quoteToCreator;
uint256 tokenToCreator;
}
/// @dev Working state for an atomic launch. Mutable protocol settings are
/// copied here before any external V3 calls are made.
struct LaunchCache {
address token;
address quoteToken;
address pool;
bool tokenIs0;
uint16 reserveBps;
uint16 sushiFeeBps;
uint256 reserveAmount;
uint256 liquidityAllocation;
uint256 tokenUsed;
}
/// @dev Canonical token ordering and deadline shared by every position in
/// one launch.
struct MintContext {
address token;
address pool;
address token0;
address token1;
bool tokenIs0;
uint64 deadline;
}
/// @dev SushiSwap V3's 1% fee tier, expressed in hundredths of a basis point.
uint24 internal constant POOL_FEE = 10_000;
/// @dev Required tick spacing for the 1% SushiSwap V3 fee tier.
int24 internal constant TICK_SPACING = 200;
/// @dev Lowest valid tick aligned to the configured tick spacing.
int24 internal constant MIN_USABLE_TICK = -887_200;
/// @dev Highest valid tick aligned to the configured tick spacing.
int24 internal constant MAX_USABLE_TICK = 887_200;
/// @dev Denominator used for reserve and trading-fee basis points.
uint16 internal constant BPS_DENOMINATOR = 10_000;
/// @dev Maximum protocol reserve for future launches: 10% of supply.
uint16 internal constant MAX_PROTOCOL_RESERVE_BPS = 1_000;
/// @dev Time each launch's protocol reserve remains locked.
uint256 internal constant RESERVE_LOCK_DURATION = 365 days;
/// @dev Maximum number of V3 NFT positions created by one launch.
uint256 internal constant MAX_RANGES = 16;
/// @dev Maximum CREATE2 token addresses checked when avoiding poisoned pools.
uint256 internal constant MAX_ADDRESS_ATTEMPTS = 8;
/// @dev Maximum UTF-8 byte length accepted for a token name.
uint256 internal constant MAX_NAME_LENGTH = 64;
/// @dev Maximum UTF-8 byte length accepted for a token symbol.
uint256 internal constant MAX_SYMBOL_LENGTH = 16;
/// @notice SushiSwap V3 factory used to create and validate launch pools.
ISushiV3Factory public immutable v3Factory;
/// @notice Position manager used to mint launch NFTs owned by this contract.
INonfungiblePositionManager public immutable positionManager;
/// @notice Current recipient of Sushi-owned fees and unlocked reserves.
address public override protocolRecipient;
/// @notice Reserve share copied into each future launch; initially 3%.
uint16 public override protocolReserveBps = 300;
/// @notice Sushi fee share copied into each future launch; initially 70%.
uint16 public override defaultSushiFeeBps = 7_000;
/// @notice Minimum native currency payment required to launch a token.
uint256 public override launchFee = 0.0005 ether;
/// @dev Number of completed launches, mixed into CREATE2 salts. Historical
/// launch counts are indexed from TokenLaunched events.
uint256 private _launchNonce;
// Minimal state needed by live protocol behavior. Historical discovery and
// attribution are reconstructed from canonical events.
mapping(address token => LaunchInfo info) private _launches;
mapping(address token => uint16 bps) private _tokenSushiFeeBps;
mapping(address token => uint256[] positionIds) private _positions;
/// @dev Restricts ERC-721 callbacks to NFTs minted during the active V3 call.
bool private _acceptingPositionNft;
/// @param initialOwner Initial two-step protocol owner.
/// @param initialProtocolRecipient Initial recipient of Sushi-owned assets.
/// @param factory_ SushiSwap V3 factory for the deployment chain.
/// @param positionManager_ SushiSwap V3 non-fungible position manager.
constructor(
address initialOwner,
address initialProtocolRecipient,
address factory_,
address positionManager_
) Ownable(initialOwner) {
if (
initialOwner == address(0) ||
initialProtocolRecipient == address(0) ||
factory_ == address(0) ||
positionManager_ == address(0)
) revert ZeroAddress();
address managerFactory;
try INonfungiblePositionManager(positionManager_).factory() returns (
address value
) {
managerFactory = value;
} catch {
revert InvalidPositionManager();
}
if (managerFactory != factory_) revert InvalidPositionManager();
int24 actualTickSpacing;
try ISushiV3Factory(factory_).feeAmountTickSpacing(POOL_FEE) returns (
int24 value
) {
actualTickSpacing = value;
} catch {
revert InvalidTickSpacing(0);
}
if (actualTickSpacing != TICK_SPACING)
revert InvalidTickSpacing(actualTickSpacing);
protocolRecipient = initialProtocolRecipient;
v3Factory = ISushiV3Factory(factory_);
positionManager = INonfungiblePositionManager(positionManager_);
}
function launch(
TokenConfig calldata tokenConfig,
address quoteToken,
SaleRange[] calldata ranges,
uint64 deadline
)
external
payable
override
nonReentrant
returns (address token, address pool, uint256[] memory positionIds)
{
LaunchCache memory cache;
cache.quoteToken = quoteToken;
cache.reserveBps = protocolReserveBps;
cache.sushiFeeBps = defaultSushiFeeBps;
(cache.reserveAmount, cache.liquidityAllocation) = _validateLaunch(
tokenConfig,
cache.quoteToken,
ranges,
deadline,
cache.reserveBps
);
cache.token = _deployCleanToken(
tokenConfig,
cache.quoteToken,
ranges,
deadline
);
cache.tokenIs0 = cache.token < cache.quoteToken;
cache.pool = _createAndInitializePool(
cache.token,
cache.quoteToken,
cache.tokenIs0,
ranges[0].startTick
);
IERC20 launchToken = IERC20(cache.token);
launchToken.forceApprove(
address(positionManager),
cache.liquidityAllocation
);
(positionIds, cache.tokenUsed) = _mintPositions(
cache.token,
cache.quoteToken,
cache.pool,
cache.tokenIs0,
ranges,
cache.liquidityAllocation,
deadline
);
launchToken.forceApprove(address(positionManager), 0);
uint256 expectedBalance = TOKEN_TOTAL_SUPPLY - cache.tokenUsed;
uint256 actualBalance = launchToken.balanceOf(address(this));
if (actualBalance != expectedBalance) {
revert SupplyReconciliationFailure(expectedBalance, actualBalance);
}
_registerLaunch(
cache.token,
cache.quoteToken,
cache.pool,
cache.reserveAmount,
positionIds.length,
cache.sushiFeeBps
);
_emitTokenLaunched(
cache.token,
cache.pool,
tokenConfig,
positionIds.length,
cache.reserveBps
);
return (cache.token, cache.pool, positionIds);
}
function distributeFees(
address token
)
external
override
nonReentrant
returns (
uint256 quoteCollected,
uint256 tokenCollected,
uint256 quoteToSushi,
uint256 tokenToSushi
)
{
_requireToken(token);
uint256[] storage positionIds = _positions[token];
FeeDistributionCache memory cache;
cache.creator = _launches[token].creator;
cache.sushiRecipient = protocolRecipient;
cache.pool = _launches[token].pool;
cache.quoteToken = _launches[token].quoteToken;
cache.bps = _tokenSushiFeeBps[token];
for (uint256 i; i < positionIds.length; ++i) {
(uint256 amount0, uint256 amount1) = positionManager.collect(
INonfungiblePositionManager.CollectParams({
tokenId: positionIds[i],
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
if (token < cache.quoteToken) {
tokenCollected += amount0;
quoteCollected += amount1;
} else {
quoteCollected += amount0;
tokenCollected += amount1;
}
}
quoteToSushi = Math.mulDiv(quoteCollected, cache.bps, BPS_DENOMINATOR);
tokenToSushi = Math.mulDiv(tokenCollected, cache.bps, BPS_DENOMINATOR);
cache.quoteToCreator = quoteCollected - quoteToSushi;
cache.tokenToCreator = tokenCollected - tokenToSushi;
_transferIfNonzero(
IERC20(cache.quoteToken),
cache.sushiRecipient,
quoteToSushi
);
_transferIfNonzero(IERC20(token), cache.sushiRecipient, tokenToSushi);
_transferIfNonzero(
IERC20(cache.quoteToken),
cache.creator,
cache.quoteToCreator
);
_transferIfNonzero(IERC20(token), cache.creator, cache.tokenToCreator);
emit FeesDistributed(
msg.sender,
token,
cache.pool,
cache.creator,
cache.sushiRecipient,
cache.bps,
quoteCollected,
tokenCollected,
quoteToSushi,
tokenToSushi,
cache.quoteToCreator,
cache.tokenToCreator
);
}
function setProtocolRecipient(
address newRecipient
) external override onlyOwner {
if (newRecipient == address(0)) revert ZeroAddress();
address previousRecipient = protocolRecipient;
protocolRecipient = newRecipient;
emit ProtocolRecipientUpdated(previousRecipient, newRecipient);
}
function setProtocolReserveBps(
uint16 newReserveBps
) external override onlyOwner {
if (newReserveBps > MAX_PROTOCOL_RESERVE_BPS)
revert InvalidReserveBps(newReserveBps);
uint16 previousBps = protocolReserveBps;
protocolReserveBps = newReserveBps;
emit ProtocolReserveBpsUpdated(previousBps, newReserveBps);
}
function setDefaultSushiFeeBps(
uint16 newSushiFeeBps
) external override onlyOwner {
_validateFeeBps(newSushiFeeBps);
uint16 previousBps = defaultSushiFeeBps;
defaultSushiFeeBps = newSushiFeeBps;
emit DefaultSushiFeeBpsUpdated(previousBps, newSushiFeeBps);
}
function setLaunchFee(uint256 newLaunchFee) external override onlyOwner {
uint256 previousFee = launchFee;
launchFee = newLaunchFee;
emit LaunchFeeUpdated(previousFee, newLaunchFee);
}
function setSushiFeeBps(
address token,
uint16 newSushiFeeBps
) external override onlyOwner {
_requireToken(token);
_validateFeeBps(newSushiFeeBps);
uint16 previousBps = _tokenSushiFeeBps[token];
_tokenSushiFeeBps[token] = newSushiFeeBps;
emit SushiFeeBpsUpdated(token, previousBps, newSushiFeeBps);
}
function withdrawProtocolReserve(
address token
) external override onlyOwner nonReentrant {
_requireToken(token);
LaunchInfo storage info = _launches[token];
if (info.reserveWithdrawn) revert ReserveAlreadyWithdrawn(token);
if (block.timestamp < info.reserveUnlockAt)
revert ReserveStillLocked(info.reserveUnlockAt);
info.reserveWithdrawn = true;
address recipient = protocolRecipient;
uint256 amount = info.reserveAmount;
_transferIfNonzero(IERC20(token), recipient, amount);
emit ProtocolReserveWithdrawn(token, recipient, amount);
}
function withdrawLaunchFees() external override onlyOwner nonReentrant {
uint256 amount = address(this).balance;
if (amount == 0) revert NothingToWithdraw();
address recipient = protocolRecipient;
(bool success, ) = payable(recipient).call{value: amount}("");
if (!success) revert NativeTransferFailed();
emit LaunchFeesWithdrawn(recipient, amount);
}
function renounceOwnership() public view override onlyOwner {
revert OwnershipRenunciationDisabled();
}
function launchInfo(
address token
) external view override returns (LaunchInfo memory info) {
_requireToken(token);
return _launches[token];
}
function sushiFeeBps(
address token
) external view override returns (uint16) {
_requireToken(token);
return _tokenSushiFeeBps[token];
}
function onERC721Received(
address operator,
address from,
uint256,
bytes calldata
) external view override returns (bytes4) {
if (
msg.sender != address(positionManager) ||
!_acceptingPositionNft ||
operator != address(this) ||
from != address(0)
) revert UnexpectedNft(msg.sender, operator, from);
return IERC721Receiver.onERC721Received.selector;
}
function _validateLaunch(
TokenConfig calldata tokenConfig,
address quoteToken,
SaleRange[] calldata ranges,
uint64 deadline,
uint16 reserveBps
)
private
view
returns (uint256 reserveAmount, uint256 liquidityAllocation)
{
if (msg.value < launchFee)
revert InsufficientLaunchFee(launchFee, msg.value);
if (block.timestamp > deadline) revert DeadlineExpired(deadline);
if (quoteToken == address(0)) revert ZeroAddress();
if (quoteToken.code.length == 0) revert InvalidQuoteToken(quoteToken);
if (bytes(tokenConfig.name).length == 0) revert EmptyName();
if (bytes(tokenConfig.symbol).length == 0) revert EmptySymbol();
if (bytes(tokenConfig.name).length > MAX_NAME_LENGTH)
revert NameTooLong(bytes(tokenConfig.name).length);
if (bytes(tokenConfig.symbol).length > MAX_SYMBOL_LENGTH)
revert SymbolTooLong(bytes(tokenConfig.symbol).length);
uint256 length = ranges.length;
if (length == 0) revert EmptyRanges();
if (length > MAX_RANGES) revert TooManyRanges(length, MAX_RANGES);
reserveAmount = Math.mulDiv(
TOKEN_TOTAL_SUPPLY,
reserveBps,
BPS_DENOMINATOR
);
liquidityAllocation = TOKEN_TOTAL_SUPPLY - reserveAmount;
uint256 amountSum;
for (uint256 i; i < length; ++i) {
SaleRange calldata range = ranges[i];
if (range.amount == 0) revert ZeroRangeAmount(i);
if (range.startTick >= range.endTick)
revert InvalidRangeOrder(i, range.startTick, range.endTick);
_validateTick(i, range.startTick);
_validateTick(i, range.endTick);
if (i != 0 && range.startTick != ranges[i - 1].endTick) {
revert NonContiguousRanges(
i,
ranges[i - 1].endTick,
range.startTick
);
}
unchecked {
uint256 newSum = amountSum + range.amount;
if (newSum < amountSum) revert RangeAmountOverflow(i);
amountSum = newSum;
}
}
if (amountSum > liquidityAllocation) {
revert RangeAmountsExceedAllocation(amountSum, liquidityAllocation);
}
}
function _validateTick(uint256 index, int24 tick) private pure {
if (tick < MIN_USABLE_TICK || tick > MAX_USABLE_TICK)
revert InvalidTickBounds(index, tick);
if (tick % TICK_SPACING != 0) revert InvalidTickSpacingAt(index, tick);
}
function _deployCleanToken(
TokenConfig calldata tokenConfig,
address quoteToken,
SaleRange[] calldata ranges,
uint64 deadline
) private returns (address token) {
bytes32 initCodeHash = keccak256(
abi.encodePacked(
type(SushiLaunchpadToken).creationCode,
abi.encode(tokenConfig.name, tokenConfig.symbol, address(this))
)
);
bytes32 parameterHash = keccak256(
abi.encode(tokenConfig, quoteToken, ranges, deadline)
);
uint256 nonce = _launchNonce;
for (uint256 i; i < MAX_ADDRESS_ATTEMPTS; ++i) {
bytes32 salt = keccak256(
abi.encode(
msg.sender,
nonce,
parameterHash,
block.prevrandao,
block.number,
i
)
);
address candidate = _computeCreate2Address(salt, initCodeHash);
if (
candidate.code.length != 0 ||
v3Factory.getPool(candidate, quoteToken, POOL_FEE) != address(0)
) continue;
token = _deployToken(salt, tokenConfig);
break;
}
if (token == address(0)) revert NoCleanTokenAddress();
address existingPool = v3Factory.getPool(token, quoteToken, POOL_FEE);
if (existingPool != address(0))
revert UnexpectedExistingPool(token, existingPool);
}
function _deployToken(
bytes32 salt,
TokenConfig calldata tokenConfig
) private returns (address) {
return
address(
new SushiLaunchpadToken{salt: salt}(
tokenConfig.name,
tokenConfig.symbol,
address(this)
)
);
}
function _createAndInitializePool(
address token,
address quoteToken,
bool tokenIs0,
int24 firstStartTick
) private returns (address pool) {
pool = v3Factory.createPool(token, quoteToken, POOL_FEE);
address registeredPool = v3Factory.getPool(token, quoteToken, POOL_FEE);
if (pool == address(0) || registeredPool != pool)
revert PoolCreationMismatch(pool, registeredPool);
ISushiV3Pool sushiPool = ISushiV3Pool(pool);
address token0 = tokenIs0 ? token : quoteToken;
address token1 = tokenIs0 ? quoteToken : token;
if (
sushiPool.token0() != token0 ||
sushiPool.token1() != token1 ||
sushiPool.fee() != POOL_FEE ||
sushiPool.tickSpacing() != TICK_SPACING
) revert PoolConfigurationMismatch(pool);
int24 initialTick = tokenIs0 ? firstStartTick : -firstStartTick;
uint160 expectedSqrtPriceX96 = TickMath.getSqrtRatioAtTick(initialTick);
sushiPool.initialize(expectedSqrtPriceX96);
(uint160 actualSqrtPriceX96, , , , , , ) = sushiPool.slot0();
if (actualSqrtPriceX96 != expectedSqrtPriceX96) {
revert PoolInitializationMismatch(
expectedSqrtPriceX96,
actualSqrtPriceX96
);
}
}
function _mintPositions(
address token,
address quoteToken,
address pool,
bool tokenIs0,
SaleRange[] calldata ranges,
uint256 liquidityAllocation,
uint64 deadline
) private returns (uint256[] memory positionIds, uint256 totalTokenUsed) {
uint256 amountSum;
for (uint256 i; i < ranges.length; ++i) amountSum += ranges[i].amount;
uint256 firstRangeRemainder = liquidityAllocation - amountSum;
MintContext memory context = MintContext({
token: token,
pool: pool,
token0: tokenIs0 ? token : quoteToken,
token1: tokenIs0 ? quoteToken : token,
tokenIs0: tokenIs0,
deadline: deadline
});
positionIds = new uint256[](ranges.length);
for (uint256 i; i < ranges.length; ++i) {
uint256 tokenDesired =
ranges[i].amount + (i == 0 ? firstRangeRemainder : 0);
(uint256 positionId, uint256 tokenUsed) = _mintOnePosition(
context,
ranges[i],
tokenDesired,
i
);
positionIds[i] = positionId;
totalTokenUsed += tokenUsed;
}
}
function _mintOnePosition(
MintContext memory context,
SaleRange calldata range,
uint256 tokenDesired,
uint256 index
) private returns (uint256 positionId, uint256 tokenUsed) {
(int24 tickLower, int24 tickUpper) = _canonicalTicks(
range,
context.tokenIs0
);
uint128 liquidity;
uint256 amount0;
uint256 amount1;
(positionId, liquidity, amount0, amount1) = _mintPosition(
context.token0,
context.token1,
tickLower,
tickUpper,
tokenDesired,
context.tokenIs0,
context.deadline
);
tokenUsed = context.tokenIs0 ? amount0 : amount1;
uint256 quoteTokenUsed = context.tokenIs0 ? amount1 : amount0;
if (liquidity == 0) revert ZeroLiquidity(index);
if (quoteTokenUsed != 0)
revert NonzeroQuoteTokenUsed(index, quoteTokenUsed);
if (tokenUsed > tokenDesired)
revert ExcessTokenConsumption(index, tokenUsed, tokenDesired);
_validatePosition(
positionId,
context.token0,
context.token1,
tickLower,
tickUpper,
liquidity
);
_positions[context.token].push(positionId);
emit PositionCreated(
context.token,
context.pool,
positionId,
tickLower,
tickUpper,
tokenDesired,
tokenUsed,
liquidity
);
}
function _mintPosition(
address token0,
address token1,
int24 tickLower,
int24 tickUpper,
uint256 tokenDesired,
bool tokenIs0,
uint64 deadline
)
private
returns (
uint256 positionId,
uint128 liquidity,
uint256 amount0,
uint256 amount1
)
{
_acceptingPositionNft = true;
(positionId, liquidity, amount0, amount1) = positionManager.mint(
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: POOL_FEE,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: tokenIs0 ? tokenDesired : 0,
amount1Desired: tokenIs0 ? 0 : tokenDesired,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: deadline
})
);
_acceptingPositionNft = false;
}
function _validatePosition(
uint256 positionId,
address expectedToken0,
address expectedToken1,
int24 expectedTickLower,
int24 expectedTickUpper,
uint128 expectedLiquidity
) private view {
(
,
,
address actualToken0,
address actualToken1,
uint24 actualFee,
int24 actualTickLower,
int24 actualTickUpper,
uint128 actualLiquidity,
,
,
,
) = positionManager.positions(positionId);
if (
positionManager.ownerOf(positionId) != address(this) ||
actualToken0 != expectedToken0 ||
actualToken1 != expectedToken1 ||
actualFee != POOL_FEE ||
actualTickLower != expectedTickLower ||
actualTickUpper != expectedTickUpper ||
actualLiquidity != expectedLiquidity
) revert PositionConfigurationMismatch(positionId);
}
function _registerLaunch(
address token,
address quoteToken,
address pool,
uint256 reserveAmount,
uint256 positionIdsLength,
uint16 initialSushiFeeBps
) private {
uint64 reserveUnlockAt =
uint64(block.timestamp) + uint64(RESERVE_LOCK_DURATION);
_launches[token] = LaunchInfo({
creator: msg.sender,
quoteToken: quoteToken,
pool: pool,
reserveUnlockAt: reserveUnlockAt,
reserveWithdrawn: false,
reserveAmount: reserveAmount
});
_tokenSushiFeeBps[token] = initialSushiFeeBps;
++_launchNonce;
assert(_positions[token].length == positionIdsLength);
}
function _emitTokenLaunched(
address token,
address pool,
TokenConfig calldata tokenConfig,
uint256 positionsLength,
uint16 reserveBps
) private {
LaunchInfo storage info = _launches[token];
emit TokenLaunched(
msg.sender,
token,
pool,
info.quoteToken,
tokenConfig.name,
tokenConfig.symbol,
TOKEN_DECIMALS,
TOKEN_TOTAL_SUPPLY,
reserveBps,
info.reserveAmount,
info.reserveUnlockAt,
_tokenSushiFeeBps[token],
positionsLength
);
}
function _canonicalTicks(
SaleRange calldata range,
bool tokenIs0
) private pure returns (int24 lower, int24 upper) {
if (tokenIs0) return (range.startTick, range.endTick);
return (-range.endTick, -range.startTick);
}
function _computeCreate2Address(
bytes32 salt,
bytes32 initCodeHash
) private view returns (address) {
return
address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
initCodeHash
)
)
)
)
);
}
function _requireToken(address token) private view {
if (_launches[token].pool == address(0)) revert UnknownToken(token);
}
function _validateFeeBps(uint16 bps) private pure {
if (bps > BPS_DENOMINATOR) revert InvalidFeeBps(bps);
}
function _transferIfNonzero(
IERC20 token,
address recipient,
uint256 amount
) private {
if (amount != 0) token.safeTransfer(recipient, amount);
}
}
Chain explorer5848msChain node78ms