V4LaunchFactory
0xadcad0b3dbb1ca35ba9d097bd773cf8b6beea95e
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
20,634 bytes
ABI entries
67
31 read · 5 write
License
none
Contract information
- Address
- 0xadcad0b3dbb1ca35ba9d097bd773cf8b6beea95e
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xA8F6966e13…D69CE07C8a
- Creation tx
- 0x64786de47d…193464ea7b
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (31)
HOOK_FLAGS() → uint160
INITIAL_TOKEN_TICK() → int24
MAX_LOCKED_DUST() → uint256
MAX_NAME_LENGTH() → uint256
MAX_SYMBOL_LENGTH() → uint256
MAX_TOKEN_URI_LENGTH() → uint256
POOL_LP_FEE() → uint24
POOL_TICK_SPACING() → int24
TOKEN_SUPPLY() → uint256
allTokens(uint256) → address
bootstrapAmount() → uint256
bootstrapper() → address
effectiveSalt(address, bytes32, bytes32) → bytes32
feeEscrow() → address
hookCreationCodeHash(address, bytes32) → bytes32
launchGeometry(address) → int24, int24, int24, bool
launches(address) → address, address, address, bytes32, bytes32, address, uint256, uint128, uint256, uint256
liquidityLockDeployer() → address
owner() → address
paused() → bool
pendingOwner() → address
permit2() → address
poolManager() → address
positionManager() → address
predictHookAddress(bytes32, address, bytes32) → address
predictTokenAddress(address, tuple) → address
renounceOwnership()
tokenSaltUsed(bytes32) → bool
totalTokens() → uint256
universalRouter() → address
weth() → address
Events (5)
OwnershipTransferStartedOwnershipTransferredPausedUnpausedV4TokenLaunched
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "contract V4FeeEscrow",
"name": "feeEscrow_",
"type": "address"
},
{
"internalType": "contract IPositionManager",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "contract IAllowanceTransfer",
"name": "permit2_",
"type": "address"
},
{
"internalType": "contract V4LiquidityLockDeployer",
"name": "liquidityLockDeployer_",
"type": "address"
},
{
"internalType": "address",
"name": "universalRouter_",
"type": "address"
},
{
"internalType": "uint256",
"name": "bootstrapAmount_",
"type": "uint256"
},
{
"internalType": "address",
"name": "owner_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "string",
"name": "check",
"type": "string"
}
],
"name": "DeploymentVerificationFailed",
"type": "error"
},
{
"inputs": [],
"name": "EnforcedPause",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "configuredFactory",
"type": "address"
}
],
"name": "EscrowNotWired",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenDust",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "wethDust",
"type": "uint256"
}
],
"name": "ExcessLockedDust",
"type": "error"
},
{
"inputs": [],
"name": "ExpectedPause",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "predicted",
"type": "address"
},
{
"internalType": "address",
"name": "deployed",
"type": "address"
}
],
"name": "HookAddressMismatch",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "hook",
"type": "address"
}
],
"name": "HookPermissionsMismatch",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "InvalidBootstrapAmount",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "supplied",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "required",
"type": "uint256"
}
],
"name": "InvalidBootstrapValue",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "InvalidContract",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "length",
"type": "uint256"
}
],
"name": "InvalidNameLength",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "length",
"type": "uint256"
}
],
"name": "InvalidSymbolLength",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "supplied",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "required",
"type": "uint256"
}
],
"name": "InvalidTokenSupply",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "length",
"type": "uint256"
}
],
"name": "InvalidTokenURI",
"type": "error"
},
{
"inputs": [],
"name": "LiquidityCalculationFailed",
"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": "OwnershipRenounceDisabled",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "TokenAlreadyExists",
"type": "error"
},
{
"inputs": [],
"name": "TokenDeploymentFailed",
"type": "error"
},
{
"inputs": [],
"name": "UnsupportedTokenURIScheme",
"type": "error"
},
{
"inputs": [],
"name": "UppercaseTokenURI",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroCreatorId",
"type": "error"
},
{
"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": false,
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "Paused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "Unpaused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "hook",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "creatorId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "address",
"name": "launcher",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "liquidityLock",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
},
{
"indexed": false,
"internalType": "uint256",
"name": "bootstrapTokenAmount",
"type": "uint256"
}
],
"name": "V4TokenLaunched",
"type": "event"
},
{
"inputs": [],
"name": "HOOK_FLAGS",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "INITIAL_TOKEN_TICK",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_LOCKED_DUST",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_NAME_LENGTH",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_SYMBOL_LENGTH",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_TOKEN_URI_LENGTH",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_LP_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_TICK_SPACING",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOKEN_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allTokens",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "bootstrapAmount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "bootstrapper",
"outputs": [
{
"internalType": "contract V4PoolBootstrapper",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "launcher",
"type": "address"
},
{
"internalType": "bytes32",
"name": "creatorId",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "tokenSalt",
"type": "bytes32"
}
],
"name": "effectiveSalt",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "feeEscrow",
"outputs": [
{
"internalType": "contract V4FeeEscrow",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "bytes32",
"name": "creatorId",
"type": "bytes32"
}
],
"name": "hookCreationCodeHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "tokenURI",
"type": "string"
},
{
"internalType": "bytes32",
"name": "creatorId",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "tokenSalt",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "hookSalt",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "tokenSupply",
"type": "uint256"
}
],
"internalType": "struct V4LaunchFactory.LaunchParams",
"name": "params",
"type": "tuple"
}
],
"name": "launch",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "hook",
"type": "address"
},
{
"internalType": "address",
"name": "liquidityLock",
"type": "address"
},
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "creatorId",
"type": "bytes32"
},
{
"internalType": "address",
"name": "launcher",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenSupply",
"type": "uint256"
},
{
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
},
{
"internalType": "uint256",
"name": "bootstrapTokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "createdAt",
"type": "uint256"
}
],
"internalType": "struct V4LaunchFactory.LaunchRecord",
"name": "record",
"type": "tuple"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "launchGeometry",
"outputs": [
{
"internalType": "int24",
"name": "initialTick",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "bool",
"name": "tokenIsCurrency0",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "launches",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "hook",
"type": "address"
},
{
"internalType": "address",
"name": "liquidityLock",
"type": "address"
},
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "creatorId",
"type": "bytes32"
},
{
"internalType": "address",
"name": "launcher",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenSupply",
"type": "uint256"
},
{
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
},
{
"internalType": "uint256",
"name": "bootstrapTokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "createdAt",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "liquidityLockDeployer",
"outputs": [
{
"internalType": "contract V4LiquidityLockDeployer",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "paused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"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": [],
"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": "bytes32",
"name": "hookSalt",
"type": "bytes32"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "bytes32",
"name": "creatorId",
"type": "bytes32"
}
],
"name": "predictHookAddress",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "launcher",
"type": "address"
},
{
"components": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "tokenURI",
"type": "string"
},
{
"internalType": "bytes32",
"name": "creatorId",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "tokenSalt",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "hookSalt",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "tokenSupply",
"type": "uint256"
}
],
"internalType": "struct V4LaunchFactory.LaunchParams",
"name": "params",
"type": "tuple"
}
],
"name": "predictTokenAddress",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "effectiveSalt",
"type": "bytes32"
}
],
"name": "tokenSaltUsed",
"outputs": [
{
"internalType": "bool",
"name": "used",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalTokens",
"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": "universalRouter",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "unpause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {IAllowanceTransfer} from "@uniswap/v4-periphery/lib/permit2/src/interfaces/IAllowanceTransfer.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {LiquidityAmounts} from "@uniswap/v4-periphery/src/libraries/LiquidityAmounts.sol";
import {CreatorFeeHook} from "./CreatorFeeHook.sol";
import {V4FeeEscrow} from "./V4FeeEscrow.sol";
import {V4LaunchToken} from "./V4LaunchToken.sol";
import {V4LiquidityLock} from "./V4LiquidityLock.sol";
import {V4LiquidityLockDeployer} from "./V4LiquidityLockDeployer.sol";
import {V4PoolBootstrapper} from "./V4PoolBootstrapper.sol";
import {IV4FeeEscrow} from "./interfaces/IV4FeeEscrow.sol";
/// @notice Creates a fixed-supply token, its immutable fee hook, and permanently locked one-sided liquidity.
contract V4LaunchFactory is Ownable2Step, Pausable {
using PoolIdLibrary for PoolKey;
using SafeERC20 for IERC20;
using StateLibrary for IPoolManager;
uint160 public constant HOOK_FLAGS = uint160(
Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG
| Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG
);
uint24 public constant POOL_LP_FEE = 0;
int24 public constant POOL_TICK_SPACING = 60;
int24 public constant INITIAL_TOKEN_TICK = -204_180;
uint256 public constant TOKEN_SUPPLY = 1_000_000_000 ether;
uint256 public constant MAX_NAME_LENGTH = 64;
uint256 public constant MAX_SYMBOL_LENGTH = 16;
uint256 public constant MAX_TOKEN_URI_LENGTH = 512;
uint256 public constant MAX_LOCKED_DUST = 1 gwei;
error ZeroAddress();
error InvalidContract(address account);
error InvalidBootstrapAmount(uint256 amount);
error InvalidBootstrapValue(uint256 supplied, uint256 required);
error ZeroCreatorId();
error InvalidTokenSupply(uint256 supplied, uint256 required);
error InvalidNameLength(uint256 length);
error InvalidSymbolLength(uint256 length);
error InvalidTokenURI(uint256 length);
error UnsupportedTokenURIScheme();
error UppercaseTokenURI();
error TokenAlreadyExists();
error TokenDeploymentFailed();
error HookAddressMismatch(address predicted, address deployed);
error HookPermissionsMismatch(address hook);
error EscrowNotWired(address configuredFactory);
error LiquidityCalculationFailed();
error ExcessLockedDust(uint256 tokenDust, uint256 wethDust);
error DeploymentVerificationFailed(string check);
error OwnershipRenounceDisabled();
struct LaunchParams {
string name;
string symbol;
string tokenURI;
bytes32 creatorId;
bytes32 tokenSalt;
bytes32 hookSalt;
uint256 tokenSupply;
}
struct LaunchRecord {
address token;
address hook;
address liquidityLock;
bytes32 poolId;
bytes32 creatorId;
address launcher;
uint256 tokenSupply;
uint128 liquidity;
uint256 bootstrapTokenAmount;
uint256 createdAt;
}
struct SeedResult {
V4LiquidityLock lock;
uint128 liquidity;
uint160 sqrtPriceX96;
int24 initialTick;
int24 tickLower;
int24 tickUpper;
}
event V4TokenLaunched(
address indexed token,
address indexed hook,
bytes32 indexed creatorId,
address launcher,
address liquidityLock,
bytes32 poolId,
uint128 liquidity,
uint256 bootstrapTokenAmount
);
IPoolManager public immutable poolManager;
IERC20 public immutable weth;
V4FeeEscrow public immutable feeEscrow;
IPositionManager public immutable positionManager;
IAllowanceTransfer public immutable permit2;
V4LiquidityLockDeployer public immutable liquidityLockDeployer;
address public immutable universalRouter;
uint256 public immutable bootstrapAmount;
V4PoolBootstrapper public immutable bootstrapper;
mapping(bytes32 effectiveSalt => bool used) public tokenSaltUsed;
mapping(address token => LaunchRecord record) public launches;
address[] public allTokens;
constructor(
IPoolManager poolManager_,
address weth_,
V4FeeEscrow feeEscrow_,
IPositionManager positionManager_,
IAllowanceTransfer permit2_,
V4LiquidityLockDeployer liquidityLockDeployer_,
address universalRouter_,
uint256 bootstrapAmount_,
address owner_
) Ownable(owner_) {
if (
address(poolManager_) == address(0) || weth_ == address(0) || address(feeEscrow_) == address(0)
|| address(positionManager_) == address(0) || address(permit2_) == address(0)
|| address(liquidityLockDeployer_) == address(0) || universalRouter_ == address(0)
) {
revert ZeroAddress();
}
if (bootstrapAmount_ == 0 || bootstrapAmount_ > type(uint128).max) {
revert InvalidBootstrapAmount(bootstrapAmount_);
}
if (address(poolManager_).code.length == 0) revert InvalidContract(address(poolManager_));
if (weth_.code.length == 0) revert InvalidContract(weth_);
if (address(feeEscrow_).code.length == 0) revert InvalidContract(address(feeEscrow_));
if (address(positionManager_).code.length == 0) revert InvalidContract(address(positionManager_));
if (address(permit2_).code.length == 0) revert InvalidContract(address(permit2_));
if (address(liquidityLockDeployer_).code.length == 0) {
revert InvalidContract(address(liquidityLockDeployer_));
}
if (universalRouter_.code.length == 0) revert InvalidContract(universalRouter_);
if (address(feeEscrow_.weth()) != weth_) revert DeploymentVerificationFailed("escrow WETH");
if (address(positionManager_.poolManager()) != address(poolManager_)) {
revert DeploymentVerificationFailed("position manager");
}
poolManager = poolManager_;
weth = IERC20(weth_);
feeEscrow = feeEscrow_;
positionManager = positionManager_;
permit2 = permit2_;
liquidityLockDeployer = liquidityLockDeployer_;
universalRouter = universalRouter_;
bootstrapAmount = bootstrapAmount_;
bootstrapper = new V4PoolBootstrapper(address(this), universalRouter_, weth_);
}
function launch(LaunchParams calldata params) external payable whenNotPaused returns (LaunchRecord memory record) {
if (params.creatorId == bytes32(0)) revert ZeroCreatorId();
if (params.tokenSupply != TOKEN_SUPPLY) revert InvalidTokenSupply(params.tokenSupply, TOKEN_SUPPLY);
if (msg.value != bootstrapAmount) revert InvalidBootstrapValue(msg.value, bootstrapAmount);
if (feeEscrow.factory() != address(this)) revert EscrowNotWired(feeEscrow.factory());
_validateTokenStrings(params.name, params.symbol);
_validateTokenURI(params.tokenURI);
bytes32 effectiveTokenSalt = effectiveSalt(msg.sender, params.creatorId, params.tokenSalt);
if (tokenSaltUsed[effectiveTokenSalt]) revert TokenAlreadyExists();
tokenSaltUsed[effectiveTokenSalt] = true;
V4LaunchToken token = _deployToken(params, effectiveTokenSalt);
CreatorFeeHook hook = _deployHook(params.hookSalt, address(token), params.creatorId);
PoolKey memory key = _poolKey(address(token), address(hook));
feeEscrow.registerHook(address(hook), params.creatorId);
SeedResult memory seeded = _seedPool(token, key, params.tokenSupply);
PoolId poolId = key.toId();
record.token = address(token);
record.hook = address(hook);
record.liquidityLock = address(seeded.lock);
record.poolId = PoolId.unwrap(poolId);
record.creatorId = params.creatorId;
record.launcher = msg.sender;
record.tokenSupply = params.tokenSupply;
record.liquidity = seeded.liquidity;
record.bootstrapTokenAmount =
bootstrapper.bootstrap{value: msg.value}(key, address(token), address(seeded.lock), hook);
if (poolManager.getLiquidity(poolId) != seeded.liquidity) {
revert DeploymentVerificationFailed("active liquidity");
}
uint256 lockedTokenBalance = token.balanceOf(address(seeded.lock));
if (
lockedTokenBalance < record.bootstrapTokenAmount
|| lockedTokenBalance - record.bootstrapTokenAmount > MAX_LOCKED_DUST
) {
revert DeploymentVerificationFailed("bootstrap token lock");
}
record.createdAt = block.timestamp;
launches[address(token)] = record;
allTokens.push(address(token));
_emitLaunch(record);
}
function predictTokenAddress(address launcher, LaunchParams calldata params) external view returns (address) {
bytes32 salt = effectiveSalt(launcher, params.creatorId, params.tokenSalt);
bytes memory bytecode = abi.encodePacked(
type(V4LaunchToken).creationCode,
abi.encode(params.name, params.symbol, params.tokenURI, params.tokenSupply, address(this))
);
return Create2.computeAddress(salt, keccak256(bytecode));
}
function predictHookAddress(bytes32 hookSalt, address token, bytes32 creatorId) public view returns (address) {
bytes memory bytecode = abi.encodePacked(
type(CreatorFeeHook).creationCode,
abi.encode(poolManager, address(weth), feeEscrow, address(this), token, creatorId)
);
return Create2.computeAddress(hookSalt, keccak256(bytecode));
}
function hookCreationCodeHash(address token, bytes32 creatorId) external view returns (bytes32) {
return keccak256(
abi.encodePacked(
type(CreatorFeeHook).creationCode,
abi.encode(poolManager, address(weth), feeEscrow, address(this), token, creatorId)
)
);
}
function effectiveSalt(address launcher, bytes32 creatorId, bytes32 tokenSalt) public pure returns (bytes32) {
return keccak256(abi.encode(launcher, creatorId, tokenSalt));
}
function totalTokens() external view returns (uint256) {
return allTokens.length;
}
/// @notice Returns the ordering-aware boundary range used for every token-only launch.
function launchGeometry(address token)
public
view
returns (int24 initialTick, int24 tickLower, int24 tickUpper, bool tokenIsCurrency0)
{
tokenIsCurrency0 = token < address(weth);
if (tokenIsCurrency0) {
initialTick = INITIAL_TOKEN_TICK;
tickLower = INITIAL_TOKEN_TICK;
tickUpper = TickMath.maxUsableTick(POOL_TICK_SPACING);
} else {
initialTick = -INITIAL_TOKEN_TICK;
tickLower = TickMath.minUsableTick(POOL_TICK_SPACING);
tickUpper = -INITIAL_TOKEN_TICK;
}
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function renounceOwnership() public pure override {
revert OwnershipRenounceDisabled();
}
function _emitLaunch(LaunchRecord memory record) internal {
emit V4TokenLaunched(
record.token,
record.hook,
record.creatorId,
record.launcher,
record.liquidityLock,
record.poolId,
record.liquidity,
record.bootstrapTokenAmount
);
}
function _deployToken(LaunchParams calldata params, bytes32 salt) internal returns (V4LaunchToken token) {
bytes memory bytecode = abi.encodePacked(
type(V4LaunchToken).creationCode,
abi.encode(params.name, params.symbol, params.tokenURI, params.tokenSupply, address(this))
);
address deployed;
assembly ("memory-safe") {
deployed := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
if (deployed == address(0)) revert TokenDeploymentFailed();
token = V4LaunchToken(deployed);
}
function _deployHook(bytes32 salt, address token, bytes32 creatorId) internal returns (CreatorFeeHook hook) {
address predicted = predictHookAddress(salt, token, creatorId);
hook = new CreatorFeeHook{salt: salt}(
poolManager, address(weth), IV4FeeEscrow(address(feeEscrow)), address(this), token, creatorId
);
if (address(hook) != predicted) revert HookAddressMismatch(predicted, address(hook));
if (uint160(address(hook)) & Hooks.ALL_HOOK_MASK != HOOK_FLAGS) {
revert HookPermissionsMismatch(address(hook));
}
}
function _liquidityForLaunch(uint256 tokenSupply, int24 tickLower, int24 tickUpper, bool tokenIsCurrency0)
internal
pure
returns (uint128 liquidity)
{
uint160 sqrtLowerX96 = TickMath.getSqrtPriceAtTick(tickLower);
uint160 sqrtUpperX96 = TickMath.getSqrtPriceAtTick(tickUpper);
liquidity = tokenIsCurrency0
? LiquidityAmounts.getLiquidityForAmount0(sqrtLowerX96, sqrtUpperX96, tokenSupply)
: LiquidityAmounts.getLiquidityForAmount1(sqrtLowerX96, sqrtUpperX96, tokenSupply);
if (liquidity == 0) revert LiquidityCalculationFailed();
}
function _seedPool(V4LaunchToken token, PoolKey memory key, uint256 tokenSupply)
internal
returns (SeedResult memory result)
{
bool tokenIsCurrency0;
(result.initialTick, result.tickLower, result.tickUpper, tokenIsCurrency0) = launchGeometry(address(token));
result.lock = liquidityLockDeployer.deploy(
positionManager, permit2, address(this), address(token), key, result.tickLower, result.tickUpper
);
IERC20(address(token)).safeTransfer(address(result.lock), tokenSupply);
result.sqrtPriceX96 = TickMath.getSqrtPriceAtTick(result.initialTick);
poolManager.initialize(key, result.sqrtPriceX96);
result.liquidity = _liquidityForLaunch(tokenSupply, result.tickLower, result.tickUpper, tokenIsCurrency0);
(uint256 amount0, uint256 amount1) = result.lock.seed(result.liquidity);
uint256 tokenAmount = tokenIsCurrency0 ? amount0 : amount1;
uint256 wethAmount = tokenIsCurrency0 ? amount1 : amount0;
if (tokenAmount > tokenSupply || wethAmount != 0) revert DeploymentVerificationFailed("seed amounts");
uint256 tokenDust = token.balanceOf(address(result.lock));
uint256 wethDust = weth.balanceOf(address(result.lock));
if (tokenDust > MAX_LOCKED_DUST || wethDust != 0) revert ExcessLockedDust(tokenDust, wethDust);
uint128 positionLiquidity = positionManager.getPositionLiquidity(result.lock.positionTokenId());
if (positionLiquidity != result.liquidity) revert DeploymentVerificationFailed("position liquidity");
if (IERC721(address(positionManager)).ownerOf(result.lock.positionTokenId()) != address(result.lock)) {
revert DeploymentVerificationFailed("position owner");
}
}
function _poolKey(address token, address hook) internal view returns (PoolKey memory key) {
(address currency0, address currency1) = token < address(weth) ? (token, address(weth)) : (address(weth), token);
key = PoolKey({
currency0: Currency.wrap(currency0),
currency1: Currency.wrap(currency1),
fee: POOL_LP_FEE,
tickSpacing: POOL_TICK_SPACING,
hooks: IHooks(hook)
});
}
function _validateTokenStrings(string calldata name_, string calldata symbol_) internal pure {
uint256 nameLength = bytes(name_).length;
uint256 symbolLength = bytes(symbol_).length;
if (nameLength == 0 || nameLength > MAX_NAME_LENGTH) revert InvalidNameLength(nameLength);
if (symbolLength == 0 || symbolLength > MAX_SYMBOL_LENGTH) revert InvalidSymbolLength(symbolLength);
}
function _validateTokenURI(string calldata tokenURI_) internal pure {
bytes calldata value = bytes(tokenURI_);
uint256 length = value.length;
if (length == 0 || length > MAX_TOKEN_URI_LENGTH) revert InvalidTokenURI(length);
if (
length <= 7 || value[0] != "i" || value[1] != "p" || value[2] != "f" || value[3] != "s" || value[4] != ":"
|| value[5] != "/" || value[6] != "/"
) {
revert UnsupportedTokenURIScheme();
}
for (uint256 i = 7; i < length; ++i) {
if (value[i] >= 0x41 && value[i] <= 0x5A) revert UppercaseTokenURI();
}
}
}
Chain explorer3735msChain node82ms