InstantLauncher
0xec180fc74118dbb2789eba16ebdbfca869e02f90
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
24,525 bytes
ABI entries
36
16 read · 5 write
License
none
Contract information
- Address
- 0xec180fc74118dbb2789eba16ebdbfca869e02f90
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xd51778f623…DED13626F9
- Creation tx
- 0x1ad923e8fe…3d41b47f57
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (16)
allLaunches(uint256) → address
claimableVested(address) → uint256
hook() → address
launchCount() → uint256
launches(address) → uint64, address, uint8, tuple
owner() → address
pendingOwner() → address
permit2() → address
poolKeyOf(address) → tuple
poolManager() → address
positionManager() → address
predictToken(uint64, string, string, string, bytes32) → address
registry() → address
tokenInitCodeHash(uint64, string, string, string) → bytes32
vestedAmount(address) → uint256
vesting(address) → address, uint128, uint128, uint40, uint32, uint32
Events (5)
DevReserveClaimedDevReserveVestedLaunchedOwnershipTransferStartedOwnershipTransferred
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "owner_",
"type": "address"
},
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "contract ConfigRegistry",
"name": "registry_",
"type": "address"
},
{
"internalType": "contract LaunchpadHook",
"name": "hook_",
"type": "address"
},
{
"internalType": "contract IPositionManager",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "contract IAllowanceTransfer",
"name": "permit2_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "NoVesting",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "TokenOrderingFailed",
"type": "error"
},
{
"inputs": [],
"name": "UnknownLaunch",
"type": "error"
},
{
"inputs": [],
"name": "VanityMismatch",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "DevReserveClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint128",
"name": "amount",
"type": "uint128"
}
],
"name": "DevReserveVested",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "uint64",
"name": "configId",
"type": "uint64"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "PoolId",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
},
{
"indexed": false,
"internalType": "uint40",
"name": "activation",
"type": "uint40"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "tokenURI",
"type": "string"
}
],
"name": "Launched",
"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"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allLaunches",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "claimVested",
"outputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "claimableVested",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "contract LaunchpadHook",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint64",
"name": "configId",
"type": "uint64"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "tokenURI",
"type": "string"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"name": "launch",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "PoolId",
"name": "poolId",
"type": "bytes32"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "launchCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "launches",
"outputs": [
{
"internalType": "uint64",
"name": "configId",
"type": "uint64"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "enum LaunchStatus",
"name": "status",
"type": "uint8"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "poolKey",
"type": "tuple"
}
],
"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": "permit2",
"outputs": [
{
"internalType": "contract IAllowanceTransfer",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "poolKeyOf",
"outputs": [
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "contract IPositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint64",
"name": "configId",
"type": "uint64"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "tokenURI",
"type": "string"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"name": "predictToken",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "registry",
"outputs": [
{
"internalType": "contract ConfigRegistry",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint64",
"name": "configId",
"type": "uint64"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "tokenURI",
"type": "string"
}
],
"name": "tokenInitCodeHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "vestedAmount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "vesting",
"outputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "uint128",
"name": "total",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "claimed",
"type": "uint128"
},
{
"internalType": "uint40",
"name": "start",
"type": "uint40"
},
{
"internalType": "uint32",
"name": "cliff",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "duration",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
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 {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {IPositionManager} from "v4-periphery/interfaces/IPositionManager.sol";
import {Actions} from "v4-periphery/libraries/Actions.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
import {LaunchConfig, DerivedConfig, LaunchStatus, FeeKind, Constants} from "./libraries/Types.sol";
import {LaunchMath} from "./libraries/LaunchMath.sol";
import {ConfigRegistry} from "./ConfigRegistry.sol";
import {LaunchToken} from "./LaunchToken.sol";
import {LaunchpadHook} from "./hooks/LaunchpadHook.sol";
/// @notice Launches a token straight onto Uniswap V4: deploys a fixed-supply token (mining the
/// address so the token is currency0), seeds the ENTIRE supply as a single-sided concentrated
/// position spanning the configured market-cap range, and permanently locks (burns) that
/// position — no code path can ever remove it. The token is tradable in the same transaction.
/// No quote/ETH is required from the creator: single-sided means the position is 100% token.
contract InstantLauncher is Ownable2Step, ReentrancyGuard {
using PoolIdLibrary for PoolKey;
using SafeERC20 for IERC20;
/// Burn address the launch-LP position NFTs are minted to (permanently unremovable).
address internal constant DEAD = 0x000000000000000000000000000000000000dEaD;
error TokenOrderingFailed();
error VanityMismatch();
error UnknownLaunch();
error NoVesting();
error ZeroAddress();
/// Every launch token address ends in these hex digits (Robinhood Chain id, 4663). The salt is
/// mined off-chain (see tokenInitCodeHash / predictToken) so the launch tx just verifies it.
uint160 internal constant VANITY_SUFFIX = 0x4663;
uint160 internal constant VANITY_MASK = 0xffff;
event Launched(
address indexed token,
uint64 indexed configId,
address indexed creator,
PoolId poolId,
uint128 liquidity,
uint40 activation,
string name,
string symbol,
string tokenURI
);
event DevReserveVested(address indexed token, address indexed creator, uint128 amount);
event DevReserveClaimed(address indexed token, address indexed creator, uint256 amount);
struct LaunchRecord {
uint64 configId;
address creator;
LaunchStatus status;
PoolKey poolKey;
}
/// @notice Dev-reserve vesting held by the launcher: cliff then linear release to the creator.
struct VestingSchedule {
address creator;
uint128 total;
uint128 claimed;
uint40 start;
uint32 cliff;
uint32 duration;
}
IPoolManager public immutable poolManager;
ConfigRegistry public immutable registry;
LaunchpadHook public immutable hook;
/// V4 PositionManager: launch LP is minted as NFT position(s) to the burn address through it.
IPositionManager public immutable positionManager;
/// Canonical Permit2, used to authorize the PositionManager to pull seed tokens from the launcher.
IAllowanceTransfer public immutable permit2;
mapping(address token => LaunchRecord) public launches;
mapping(address token => VestingSchedule) public vesting;
address[] public allLaunches;
constructor(
address owner_,
IPoolManager poolManager_,
ConfigRegistry registry_,
LaunchpadHook hook_,
IPositionManager positionManager_,
IAllowanceTransfer permit2_
) Ownable(owner_) {
poolManager = poolManager_;
registry = registry_;
hook = hook_;
positionManager = positionManager_;
permit2 = permit2_;
}
function launchCount() external view returns (uint256) {
return allLaunches.length;
}
function poolKeyOf(address token) external view returns (PoolKey memory) {
if (launches[token].status == LaunchStatus.None) revert UnknownLaunch();
return launches[token].poolKey;
}
// ---------------------------------------------------------------- dev-reserve vesting
/// @notice Tokens vested (cliff + linear) for the dev reserve as of now.
function vestedAmount(address token) public view returns (uint256) {
VestingSchedule memory v = vesting[token];
if (v.total == 0) return 0;
uint256 unlockStart = uint256(v.start) + v.cliff;
if (block.timestamp < unlockStart) return 0;
if (v.duration == 0) return v.total; // fully unlocked at the cliff
uint256 elapsed = block.timestamp - unlockStart;
if (elapsed >= v.duration) return v.total;
return (uint256(v.total) * elapsed) / v.duration;
}
/// @notice Currently-claimable (vested minus already-claimed) dev-reserve tokens.
function claimableVested(address token) external view returns (uint256) {
return vestedAmount(token) - vesting[token].claimed;
}
/// @notice Release vested dev-reserve tokens to the creator. Callable by anyone; always pays the
/// recorded creator. Bypasses the launch-window limits (a vesting release is not a trader buy).
function claimVested(address token) external nonReentrant returns (uint256 amount) {
VestingSchedule storage v = vesting[token];
if (v.total == 0) revert NoVesting();
amount = vestedAmount(token) - v.claimed;
if (amount == 0) return 0;
v.claimed += uint128(amount);
LaunchToken(payable(token)).setPrivilegedTransfer(true);
IERC20(token).safeTransfer(v.creator, amount);
LaunchToken(payable(token)).setPrivilegedTransfer(false);
emit DevReserveClaimed(token, v.creator, amount);
}
// ---------------------------------------------------------------- launch
function launch(
uint64 configId,
string calldata name,
string calldata symbol,
string calldata tokenURI,
bytes32 salt
) external nonReentrant returns (address token, PoolId poolId) {
LaunchConfig memory cfg = registry.getConfig(configId);
DerivedConfig memory d = registry.getDerived(configId);
uint40 activation = cfg.startTime == 0 ? uint40(block.timestamp) : cfg.startTime;
token = _deployToken(cfg, name, symbol, tokenURI, salt);
PoolKey memory key = PoolKey({
currency0: Currency.wrap(token),
currency1: Currency.wrap(cfg.quoteToken),
fee: 0, // hook is the fee engine; the (burned) position earns no LP fee
tickSpacing: Constants.TICK_SPACING,
hooks: IHooks(address(hook))
});
// register with the hook BEFORE initialize (beforeInitialize rejects unregistered pools)
_registerWithHook(key, cfg, token, msg.sender, activation, d.sqrtPriceTargetX96);
poolManager.initialize(key, d.sqrtPriceStartX96);
// seed the whole supply as two single-sided token positions (main band + forever tail), minted
// as NFTs to the burn address via the V4 PositionManager — permanently burned LP that no owner
// (the NFTs live at 0xdead) can ever remove.
_seedBurnedLiquidity(key, d);
// Hand the dev reserve to the creator (liquid or vested), then burn the seeding dust.
// After seeding the launcher holds (totalSupply - baseDeposited) == devReserve + lpDust.
uint256 held; // dev reserve retained here under vesting
if (d.devReserve != 0) {
if (cfg.devVestCliff == 0 && cfg.devVestDuration == 0) {
// immediately liquid: privileged transfer bypasses the launch-window limits
LaunchToken(payable(token)).setPrivilegedTransfer(true);
IERC20(token).safeTransfer(msg.sender, d.devReserve);
LaunchToken(payable(token)).setPrivilegedTransfer(false);
} else {
held = d.devReserve;
vesting[token] = VestingSchedule({
creator: msg.sender,
total: d.devReserve,
claimed: 0,
start: activation,
cliff: cfg.devVestCliff,
duration: cfg.devVestDuration
});
emit DevReserveVested(token, msg.sender, d.devReserve);
}
}
uint256 dust = IERC20(token).balanceOf(address(this)) - held;
if (dust != 0) LaunchToken(payable(token)).burn(dust);
poolId = key.toId(); // computed here (not earlier) to keep launch() under the via-ir stack limit
launches[token] = LaunchRecord({configId: configId, creator: msg.sender, status: LaunchStatus.Live, poolKey: key});
allLaunches.push(token);
emit Launched(
token,
configId,
msg.sender,
poolId,
d.liquidityMain + d.liquidityTail,
activation,
name,
symbol,
tokenURI
);
}
/// @dev Isolated frame for the (large) hook registration, to keep `launch` off the Yul stack limit.
function _registerWithHook(
PoolKey memory key,
LaunchConfig memory cfg,
address token,
address creator,
uint40 activation,
uint160 sqrtPriceTargetX96
) private {
// For meme pools, precompute the Q96-price anchors once from the config's market-cap anchors
// and fixed supply; the hook interpolates the fee in price so it never re-reads totalSupply.
(uint256 priceStartX96, uint256 priceEndX96) = _memePriceAnchors(cfg);
hook.register(
key,
LaunchpadHook.RegisterParams({
token: token,
creator: creator,
partner: cfg.partner,
creatorFeeShareBps: cfg.creatorFeeShareBps,
activation: activation,
sqrtPriceTargetX96: sqrtPriceTargetX96,
schedule: LaunchpadHook.FeeSchedule({
kind: cfg.feeKind,
baseFeeBps: cfg.baseFeeBps,
antiSnipeMode: cfg.antiSnipeMode,
antiSnipeStartBps: cfg.antiSnipeStartBps,
antiSnipeDuration: cfg.antiSnipeDuration,
antiSnipePeriod: cfg.antiSnipePeriod,
antiSnipeDecayBps: cfg.antiSnipeDecayBps,
memeStartFeeBps: cfg.memeStartFeeBps,
memeEndFeeBps: cfg.memeEndFeeBps,
priceStartX96: priceStartX96,
priceEndX96: priceEndX96
})
})
);
// creator-fee routing (validated + defaulted in the registry) — separate call keeps this
// frame small and avoids widening RegisterParams past the Yul stack limit.
hook.registerFlywheel(key, cfg.flywheel);
}
/// @dev The meme fee's Q96-price anchors (zero for Standard configs, where they're unused).
function _memePriceAnchors(LaunchConfig memory cfg) private pure returns (uint256, uint256) {
if (cfg.feeKind != FeeKind.Meme) return (0, 0);
return LaunchMath.memePriceAnchors(cfg.startMarketCap, cfg.memeEndMarketCap, cfg.totalSupply);
}
// ---------------------------------------------------------------- liquidity seeding (burned LP)
/// @dev Seed both single-sided token positions as NFTs minted to `DEAD` via the V4 PositionManager.
/// The positions are 100% currency0 (the token; ranges sit at/above the launch tick), so only
/// currency0 is ever owed — the launcher pre-approves the PositionManager (through Permit2) to pull
/// exactly the seed tokens from its freshly-minted supply. Because the NFTs are owned by the burn
/// address, no one can ever decrease/withdraw the liquidity: the LP is provably burned, and explorers
/// see the position NFTs held by 0xdead. Any `amount1Max` is 0 (no quote is ever pulled).
function _seedBurnedLiquidity(PoolKey memory key, DerivedConfig memory d) private {
address token = Currency.unwrap(key.currency0);
IERC20(token).forceApprove(address(permit2), type(uint256).max);
permit2.approve(token, address(positionManager), type(uint160).max, type(uint48).max);
positionManager.modifyLiquidities(_seedCalldata(key, d), block.timestamp);
}
/// @dev Build the PositionManager calldata that mints both single-sided token positions (main band +
/// forever tail) to the burn address and settles the token owed. Kept in its own (pure) frame so the
/// deep abi.encode marshalling stays under the via-ir stack limit.
function _seedCalldata(PoolKey memory key, DerivedConfig memory d) private pure returns (bytes memory) {
bytes memory actions =
abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR));
bytes[] memory params = new bytes[](3);
params[0] = _mintToBurnParams(key, d.tickLower, d.tickMid, d.liquidityMain);
params[1] = _mintToBurnParams(key, d.tickMid, d.tickUpper, d.liquidityTail);
params[2] = abi.encode(key.currency0, key.currency1); // SETTLE_PAIR (only currency0 is ever owed)
return abi.encode(actions, params);
}
/// @dev Encode one MINT_POSITION action minting a single-sided token position to the burn address.
/// amount1Max = 0 (no quote is ever owed); amount0Max is uncapped — the launcher seeds its own fresh
/// pool atomically, so there is no slippage/MEV surface to guard against.
function _mintToBurnParams(PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidity)
private
pure
returns (bytes memory)
{
return abi.encode(key, tickLower, tickUpper, uint256(liquidity), type(uint128).max, uint128(0), DEAD, bytes(""));
}
// ---------------------------------------------------------------- internals
/// @notice CREATE2 init-code hash for a launch token, so off-chain salt miners (SDK/frontend)
/// compute the exact same token address this contract will deploy. Mine a `salt` whose resulting
/// address is < quoteToken AND ends in `VANITY_SUFFIX`, then pass it to `launch`.
function tokenInitCodeHash(
uint64 configId,
string calldata name,
string calldata symbol,
string calldata tokenURI
) public view returns (bytes32) {
LaunchConfig memory cfg = registry.getConfig(configId);
return keccak256(abi.encodePacked(type(LaunchToken).creationCode, _tokenArgs(cfg, name, symbol, tokenURI)));
}
/// @notice The CREATE2 address for a given salt (mirror of what `launch` deploys).
function predictToken(
uint64 configId,
string calldata name,
string calldata symbol,
string calldata tokenURI,
bytes32 salt
) external view returns (address) {
return _create2Address(salt, tokenInitCodeHash(configId, name, symbol, tokenURI));
}
function _tokenArgs(LaunchConfig memory cfg, string calldata name, string calldata symbol, string calldata tokenURI)
private
view
returns (bytes memory)
{
// NOTE: constructor arity changed in Phase B — the trailing `weth` (== cfg.quoteToken, the
// reward/quote currency) was appended. This changes the token init-code hash; the off-chain
// SDK/frontend vanity miner mirror MUST be updated to match (mine over these exact args).
return abi.encode(
name,
symbol,
tokenURI,
uint256(cfg.totalSupply),
address(hook),
address(registry),
cfg.maxTxBaseAmount,
cfg.maxWalletBaseAmount,
cfg.startTime,
cfg.limitsDuration,
cfg.quoteToken
);
}
function _deployToken(
LaunchConfig memory cfg,
string calldata name,
string calldata symbol,
string calldata tokenURI,
bytes32 salt
) private returns (address token) {
// Deploy via CREATE2 from the in-memory init code (creationCode ++ encoded constructor args).
// This is the exact preimage `tokenInitCodeHash` hashes, so the deployed address matches
// `predictToken`; it also keeps the 11 constructor args off the Yul stack (a `new` with named
// args overflows it under via-ir). See _tokenArgs for the Phase-B arity change (trailing weth).
bytes memory initCode =
abi.encodePacked(type(LaunchToken).creationCode, _tokenArgs(cfg, name, symbol, tokenURI));
assembly ("memory-safe") {
token := create2(0, add(initCode, 0x20), mload(initCode), salt)
}
if (token == address(0)) revert TokenOrderingFailed(); // CREATE2 failed (salt collision)
// the caller must have mined the salt off-chain: token sorts below quote (== currency0)
// and the address ends in the Squeez vanity suffix
if (token >= cfg.quoteToken) revert TokenOrderingFailed();
if (uint160(token) & VANITY_MASK != VANITY_SUFFIX) revert VanityMismatch();
}
function _create2Address(bytes32 salt, bytes32 initCodeHash) private view returns (address) {
return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash)))));
}
}
Chain explorer3083msChain node88ms