RobinPadFactory
0xc25c1e209313856e3a66fdd3afd98abe90b047f6
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
14,030 bytes
ABI entries
65
29 read · 10 write
License
none
Contract information
- Address
- 0xc25c1e209313856e3a66fdd3afd98abe90b047f6
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xC07397cD86…6B053E6Bd5
- Creation tx
- 0xaea5720738…de061d812c
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (29)
DEFAULT_ADMIN_ROLE() → bytes32
GRADUATION_TARGET() → uint256
MAX_LAUNCH_FEE() → uint256
PAUSER_ROLE() → bytes32
POOL_FEE_TIER() → uint24
STANDARD_SUPPLY() → uint256
UPGRADER_ROLE() → bytes32
allTokens(uint256) → address
feeRouter() → address
getAllTokens() → address[]
getRoleAdmin(bytes32) → bytes32
hasRole(bytes32, address) → bool
isGraduated(address) → bool
isLaunchedToken(address) → bool
launchFee() → uint256
lpLocker() → address
paused() → bool
platformFeeRecipient() → address
positionManager() → address
referralRegistry() → address
supportsInterface(bytes4) → bool
swapRouter() → address
tokenCount() → uint256
tokenCreator(address) → address
tokenImplementation() → address
tokenLpTokenId(address) → uint256
tokenPool(address) → address
v3Factory() → address
weth() → address
Events (6)
LaunchFeeUpdatedPlatformFeeRecipientUpdatedRoleAdminChangedRoleGrantedRoleRevokedTokenLaunched
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "v3Factory_",
"type": "address"
},
{
"internalType": "address",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "address",
"name": "swapRouter_",
"type": "address"
},
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "address",
"name": "lpLocker_",
"type": "address"
},
{
"internalType": "address",
"name": "referralRegistry_",
"type": "address"
},
{
"internalType": "address",
"name": "feeRouter_",
"type": "address"
},
{
"internalType": "address",
"name": "tokenImplementation_",
"type": "address"
},
{
"internalType": "address",
"name": "admin",
"type": "address"
},
{
"internalType": "address",
"name": "platformFeeRecipient_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AccessControlBadConfirmation",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "bytes32",
"name": "neededRole",
"type": "bytes32"
}
],
"name": "AccessControlUnauthorizedAccount",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "target",
"type": "address"
}
],
"name": "AddressEmptyCode",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "AddressInsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "DeadlineExpired",
"type": "error"
},
{
"inputs": [],
"name": "ERC1167FailedCreateClone",
"type": "error"
},
{
"inputs": [],
"name": "FailedInnerCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "requested",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "max",
"type": "uint256"
}
],
"name": "FeeTooHigh",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "sent",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "required",
"type": "uint256"
}
],
"name": "InsufficientPayment",
"type": "error"
},
{
"inputs": [],
"name": "LPMintFailed",
"type": "error"
},
{
"inputs": [],
"name": "NotPaused",
"type": "error"
},
{
"inputs": [],
"name": "Paused",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "PoolAlreadyExists",
"type": "error"
},
{
"inputs": [],
"name": "PoolInitFailed",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [],
"name": "RefundFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "oldFee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newFee",
"type": "uint256"
}
],
"name": "LaunchFeeUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "oldRecipient",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "PlatformFeeRecipientUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "previousAdminRole",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "newAdminRole",
"type": "bytes32"
}
],
"name": "RoleAdminChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "RoleGranted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "RoleRevoked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "lpTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "launchFeePaid",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "timestamp",
"type": "uint256"
}
],
"name": "TokenLaunched",
"type": "event"
},
{
"inputs": [],
"name": "DEFAULT_ADMIN_ROLE",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRADUATION_TARGET",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_LAUNCH_FEE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PAUSER_ROLE",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_FEE_TIER",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "STANDARD_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "UPGRADER_ROLE",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allTokens",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "uint16",
"name": "royaltyBps",
"type": "uint16"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "uint256",
"name": "buyAmount",
"type": "uint256"
},
{
"internalType": "address",
"name": "referrer",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minAmountOut",
"type": "uint256"
}
],
"name": "createToken",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "feeRouter",
"outputs": [
{
"internalType": "contract FeeRouter",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getAllTokens",
"outputs": [
{
"internalType": "address[]",
"name": "",
"type": "address[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
}
],
"name": "getRoleAdmin",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "grantRole",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "hasRole",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "isGraduated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isLaunchedToken",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "launchFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lpLocker",
"outputs": [
{
"internalType": "contract LPLocker",
"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": "platformFeeRecipient",
"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": "referralRegistry",
"outputs": [
{
"internalType": "contract ReferralRegistry",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "callerConfirmation",
"type": "address"
}
],
"name": "renounceRole",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "rescueETH",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "revokeRole",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newFee",
"type": "uint256"
}
],
"name": "setLaunchFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "setPlatformFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newImpl",
"type": "address"
}
],
"name": "setTokenImplementation",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "swapRouter",
"outputs": [
{
"internalType": "contract ISwapRouter",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenCreator",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenImplementation",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenLpTokenId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenPool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "unpause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "v3Factory",
"outputs": [
{
"internalType": "contract IUniswapV3Factory",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "contract IWETH",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {TokenTemplate} from "./TokenTemplate.sol";
import {LPLocker} from "./LPLocker.sol";
import {ReferralRegistry} from "./ReferralRegistry.sol";
import {FeeRouter} from "./FeeRouter.sol";
import {IUniswapV3Factory} from "./interfaces/IUniswapV3Factory.sol";
import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol";
import {ISwapRouter} from "./interfaces/ISwapRouter.sol";
import {IWETH} from "./interfaces/IWETH.sol";
/**
* @title RobinPadFactory
* @notice Direct-to-Uniswap-V3 token launchpad (Model B - Single-Sided LP).
*
* @dev Launch flow (single atomic transaction):
* 1. Deploy ERC-20 token via ERC-1167 clone (1B supply, fixed)
* 2. Create V3 pool (token/WETH, 1% fee tier)
* 3. Deposit 1B tokens as single-sided LP (no WETH seed required)
* 4. Lock LP NFT permanently in LPLocker
* 5. Optional: creator initial buy via SwapRouter
* 6. Collect platform fee (launchFee ETH)
*
* Security model:
* - ReentrancyGuard on createToken
* - Deadline parameter prevents mempool stuck transactions
* - Salt prevents token address collision / duplicate pool
* - LP NFT goes directly to LPLocker — cannot be retrieved by anyone
* - Fee recipient is a hot-wallet address, not this contract
* - No ETH stored in this contract after createToken (forwarded immediately)
* - Admin cannot rug: launchFee capped at MAX_LAUNCH_FEE
* - Admin cannot change immutable infrastructure (v3Factory, positionManager, etc.)
* - Pauser can only pause — admin alone can unpause
* - tokenImplementation upgrade requires UPGRADER_ROLE (separate from admin)
*
* Fee model:
* - 0.0011 ETH platform fee per launch (admin-configurable, max 0.01 ETH)
* - 1% V3 pool fee accrues to locked LP; split 60% creator / 30% platform / 10% referral
*
* @custom:security No private keys, no backdoors, no selfdestruct, no delegatecall.
*/
contract RobinPadFactory is ReentrancyGuard, AccessControl {
using SafeERC20 for IERC20;
using Clones for address;
// ── Roles ──────────────────────────────────────────────────────────────
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
// ── Immutable infrastructure (set in constructor, never changeable) ────
IUniswapV3Factory public immutable v3Factory;
INonfungiblePositionManager public immutable positionManager;
ISwapRouter public immutable swapRouter;
IWETH public immutable weth;
LPLocker public immutable lpLocker;
ReferralRegistry public immutable referralRegistry;
FeeRouter public immutable feeRouter;
// ── Mutable config (admin-controlled, bounded) ────────────────────────
/// @notice ERC-1167 implementation for clone. Changeable only by UPGRADER_ROLE.
address public tokenImplementation;
/// @notice ETH fee paid per token launch. Capped at MAX_LAUNCH_FEE.
uint256 public launchFee = 0.0011 ether;
/// @notice Recipient of launchFee. Must be non-zero before any launch.
address public platformFeeRecipient;
// ── Constants ─────────────────────────────────────────────────────────
uint24 public constant POOL_FEE_TIER = 10_000; // 1%
uint256 public constant STANDARD_SUPPLY = 1_000_000_000 * 10 ** 18; // 1B
uint256 public constant GRADUATION_TARGET = 4.2 ether; // UI milestone
uint256 public constant MAX_LAUNCH_FEE = 0.01 ether; // admin cap
// ── Pause ─────────────────────────────────────────────────────────────
bool public paused;
// ── Bookkeeping ───────────────────────────────────────────────────────
mapping(address => bool) public isLaunchedToken;
address[] public allTokens;
mapping(address => address) public tokenCreator;
mapping(address => address) public tokenPool;
mapping(address => uint256) public tokenLpTokenId;
// ── Events ────────────────────────────────────────────────────────────
event TokenLaunched(
address indexed token,
address indexed creator,
address indexed pool,
uint256 lpTokenId,
uint256 launchFeePaid,
uint256 timestamp
);
event LaunchFeeUpdated(uint256 oldFee, uint256 newFee);
event PlatformFeeRecipientUpdated(address oldRecipient, address newRecipient);
// ── Errors ────────────────────────────────────────────────────────────
error DeadlineExpired(uint256 deadline);
error Paused();
error NotPaused();
error PoolAlreadyExists(address token);
error LPMintFailed();
error InsufficientPayment(uint256 sent, uint256 required);
error FeeTooHigh(uint256 requested, uint256 max);
error ZeroAddress();
error PoolInitFailed();
error RefundFailed();
// ── Modifiers ─────────────────────────────────────────────────────────
modifier whenNotPaused() {
if (paused) revert Paused();
_;
}
modifier whenPaused() {
if (!paused) revert NotPaused();
_;
}
// ── Constructor ───────────────────────────────────────────────────────
constructor(
address v3Factory_,
address positionManager_,
address swapRouter_,
address weth_,
address lpLocker_,
address referralRegistry_,
address feeRouter_,
address tokenImplementation_,
address admin,
address platformFeeRecipient_
) {
if (
v3Factory_ == address(0) ||
positionManager_ == address(0) ||
swapRouter_ == address(0) ||
weth_ == address(0) ||
lpLocker_ == address(0) ||
referralRegistry_ == address(0) ||
feeRouter_ == address(0) ||
tokenImplementation_ == address(0) ||
admin == address(0) ||
platformFeeRecipient_ == address(0)
) revert ZeroAddress();
v3Factory = IUniswapV3Factory(v3Factory_);
positionManager = INonfungiblePositionManager(positionManager_);
swapRouter = ISwapRouter(swapRouter_);
weth = IWETH(weth_);
lpLocker = LPLocker(lpLocker_);
referralRegistry = ReferralRegistry(referralRegistry_);
feeRouter = FeeRouter(feeRouter_);
tokenImplementation = tokenImplementation_;
platformFeeRecipient = platformFeeRecipient_;
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(PAUSER_ROLE, admin);
// UPGRADER_ROLE NOT granted at deploy — admin must explicitly grant it
}
// ── Core: Launch ──────────────────────────────────────────────────────
/**
* @notice Launch a token directly into Uniswap V3 in one atomic tx.
*
* @param name Token name (stored in ERC-20, emitted on-chain)
* @param symbol Token symbol
* @param royaltyBps Creator royalty hint 0-200 bps (informational for FeeRouter)
* @param metadataURI IPFS/HTTPS URI for logo + metadata JSON
* @param buyAmount Wei to spend on creator initial buy (0 = skip)
* @param referrer Referral address (address(0) = none)
* @param deadline Unix timestamp; tx reverts if included in a block after this
* @param minAmountOut Minimum tokens out for creator buy (slippage protection; 0 = no check)
* @return token Newly deployed ERC-20 address
* @return pool Uniswap V3 pool address
*/
function createToken(
string calldata name,
string calldata symbol,
uint16 royaltyBps,
string calldata metadataURI,
uint256 buyAmount,
address referrer,
uint256 deadline,
uint256 minAmountOut
) external payable nonReentrant whenNotPaused returns (address token, address pool) {
// ── Deadline guard ────────────────────────────────────────────────
// Standard deadline pattern (same as Uniswap): validator-level timestamp
// manipulation tolerance (seconds) is negligible against a caller-chosen
// deadline meant to bound minutes/hours of mempool exposure.
// forge-lint: disable-next-line(block-timestamp)
if (block.timestamp > deadline) revert DeadlineExpired(deadline);
// ── Payment: must cover platform fee; remainder is creator buy budget ──
uint256 totalRequired = launchFee;
if (msg.value < totalRequired) revert InsufficientPayment(msg.value, totalRequired);
uint256 creatorBuyBudget = msg.value - totalRequired;
// Enforce buyAmount matches actual ETH sent (prevent griefing)
if (buyAmount > creatorBuyBudget) {
buyAmount = creatorBuyBudget;
}
// ── Step 1: Deploy ERC-20 via ERC-1167 clone ─────────────────────
// Salt includes msg.sender + block.number to prevent predictable address attacks
bytes32 salt = keccak256(abi.encodePacked(name, symbol, msg.sender, block.number, block.timestamp));
token = tokenImplementation.cloneDeterministic(salt);
// royaltyBps capped at 200 inside TokenTemplate.initialize
TokenTemplate(token).initialize(name, symbol, msg.sender, royaltyBps, metadataURI);
// ── Step 2: Create V3 pool ────────────────────────────────────────
(address token0, address token1) = token < address(weth)
? (token, address(weth))
: (address(weth), token);
pool = v3Factory.getPool(token0, token1, POOL_FEE_TIER);
if (pool != address(0)) revert PoolAlreadyExists(token);
pool = v3Factory.createPool(token0, token1, POOL_FEE_TIER);
// ── Step 3: Initialize pool price ────────────────────────────────
// 1 token ≈ 1 nanoETH (10^-9 ETH). Tick: ±207200 depending on ordering.
uint160 sqrtPriceX96;
int24 tickLower;
int24 tickUpper;
if (token0 == token) {
sqrtPriceX96 = 2505777000000000000000000; // token is token0: price ~1e-9
tickLower = -207200;
tickUpper = 887200;
} else {
sqrtPriceX96 = 2505160000000000000000000000000000; // token is token1
tickLower = -887200;
tickUpper = 207200;
}
(bool ok,) = pool.call(abi.encodeWithSignature("initialize(uint160)", sqrtPriceX96));
if (!ok) revert PoolInitFailed();
// ── Step 4: Single-sided LP mint (1B tokens, 0 WETH) ─────────────
IERC20(token).forceApprove(address(positionManager), STANDARD_SUPPLY);
INonfungiblePositionManager.MintParams memory mintP = INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: POOL_FEE_TIER,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: token0 == token ? STANDARD_SUPPLY : 0,
amount1Desired: token1 == token ? STANDARD_SUPPLY : 0,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: deadline
});
(uint256 lpTokenId, uint128 liquidity,,) = positionManager.mint(mintP);
if (liquidity == 0) revert LPMintFailed();
// Defensive: reset any dangling allowance if mint() didn't consume the full
// approved amount (single-sided full-range mint should consume it exactly).
if (IERC20(token).allowance(address(this), address(positionManager)) > 0) {
IERC20(token).forceApprove(address(positionManager), 0);
}
// ── Step 5: Lock LP NFT permanently ──────────────────────────────
positionManager.approve(address(lpLocker), lpTokenId);
positionManager.safeTransferFrom(address(this), address(lpLocker), lpTokenId);
// Verify NFT actually transferred — prevents any approval-hijack scenario
require(positionManager.ownerOf(lpTokenId) == address(lpLocker), "Lock failed");
lpLocker.recordLock(lpTokenId, token, msg.sender);
// ── Step 6: Pay platform fee ──────────────────────────────────────
// Forward immediately — do NOT accumulate ETH in this contract
if (launchFee > 0) {
(bool feeOk,) = payable(platformFeeRecipient).call{value: launchFee}("");
require(feeOk, "Fee transfer failed");
}
// ── Step 7: Register in FeeRouter ────────────────────────────────
feeRouter.registerToken(token, msg.sender);
// ── Step 8: Creator initial buy (optional) ────────────────────────
uint256 actualBuy = buyAmount > 0 ? buyAmount : creatorBuyBudget;
if (actualBuy > 0) {
weth.deposit{value: actualBuy}();
weth.approve(address(swapRouter), actualBuy);
ISwapRouter.ExactInputSingleParams memory swapP = ISwapRouter.ExactInputSingleParams({
tokenIn: address(weth),
tokenOut: token,
fee: POOL_FEE_TIER,
recipient: msg.sender,
deadline: deadline,
amountIn: actualBuy,
amountOutMinimum: minAmountOut,
sqrtPriceLimitX96: 0
});
swapRouter.exactInputSingle(swapP);
}
// Refund any ETH sent beyond launchFee + actualBuy (e.g. buyAmount
// param was less than the budget implied by msg.value) — otherwise it
// would sit stranded in this contract, recoverable only by admin via
// rescueETH() to platformFeeRecipient, never back to the caller.
uint256 unusedBudget = creatorBuyBudget - actualBuy;
if (unusedBudget > 0) {
(bool refundOk,) = payable(msg.sender).call{value: unusedBudget}("");
if (!refundOk) revert RefundFailed();
}
// ── Step 9: Register referral ─────────────────────────────────────
if (referrer != address(0) && referrer != msg.sender) {
if (!referralRegistry.hasReferrer(msg.sender)) {
try referralRegistry.registerReferrer(msg.sender, referrer) {} catch {}
}
}
// ── Step 10: Record launch ────────────────────────────────────────
isLaunchedToken[token] = true;
allTokens.push(token);
tokenCreator[token] = msg.sender;
tokenPool[token] = pool;
tokenLpTokenId[token] = lpTokenId;
emit TokenLaunched(token, msg.sender, pool, lpTokenId, launchFee, block.timestamp);
}
// ── View helpers ──────────────────────────────────────────────────────
function getAllTokens() external view returns (address[] memory) {
return allTokens;
}
function tokenCount() external view returns (uint256) {
return allTokens.length;
}
/**
* @notice Simple graduation check: has the pool received ≥ 4.2 ETH in buys?
* Off-chain check is more reliable; this is a helper hint only.
*/
function isGraduated(address token) external view returns (bool) {
address pool = tokenPool[token];
if (pool == address(0)) return false;
// ETH side = WETH balance of pool
uint256 wethInPool = IERC20(address(weth)).balanceOf(pool);
return wethInPool >= GRADUATION_TARGET;
}
// ── Admin setters (bounded / role-gated) ─────────────────────────────
/// @notice Update launch fee. Capped at MAX_LAUNCH_FEE (0.01 ETH) — admin cannot rug via fee.
function setLaunchFee(uint256 newFee) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (newFee > MAX_LAUNCH_FEE) revert FeeTooHigh(newFee, MAX_LAUNCH_FEE);
emit LaunchFeeUpdated(launchFee, newFee);
launchFee = newFee;
}
/// @notice Update platform fee recipient. Must be non-zero.
function setPlatformFeeRecipient(address newRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (newRecipient == address(0)) revert ZeroAddress();
emit PlatformFeeRecipientUpdated(platformFeeRecipient, newRecipient);
platformFeeRecipient = newRecipient;
}
/// @notice Update token implementation (new features, never changes existing tokens).
/// Requires separate UPGRADER_ROLE — admin alone cannot do this.
function setTokenImplementation(address newImpl) external onlyRole(UPGRADER_ROLE) {
if (newImpl == address(0)) revert ZeroAddress();
tokenImplementation = newImpl;
}
// ── Pausing ───────────────────────────────────────────────────────────
function pause() external onlyRole(PAUSER_ROLE) {
paused = true;
}
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) whenPaused {
paused = false;
}
// ── ETH safety ────────────────────────────────────────────────────────
/**
* @notice Rescue any ETH accidentally sent to this contract.
* Only admin, only goes to platformFeeRecipient (not arbitrary address).
*/
function rescueETH() external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant {
uint256 bal = address(this).balance;
require(bal > 0, "Nothing to rescue");
(bool ok,) = payable(platformFeeRecipient).call{value: bal}("");
require(ok, "Rescue failed");
}
receive() external payable {}
}
/// @dev Minimal interface for reading V3 pool slot0 (used off-chain via view calls).
interface IUniswapV3PoolState {
function slot0() external view returns (
uint160 sqrtPriceX96,
int24 tick,
uint16 observationIndex,
uint16 observationCardinality,
uint16 observationCardinalityNext,
uint8 feeProtocol,
bool unlocked
);
}
Chain explorer2541msChain node88ms