LaunchFactoryV4
0x23ab86d6f6c9f721c57deb3fc00ca225bfffc9eb
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
13,244 bytes
ABI entries
29
16 read · 3 write
License
none
Contract information
- Address
- 0x23ab86d6f6c9f721c57deb3fc00ca225bfffc9eb
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xF8aF351b7A…A4BF86f6aA
- Creation tx
- 0xf97cdcce22…e15665f85c
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (16)
MAX_DEV_BUY_BPS() → uint256
hook() → address
isLaunchedToken(address) → bool
launchCount() → uint256
launches(uint256) → address, uint256
lpLocker() → address
nameUsed(bytes32) → bool
owner() → address
permit2() → address
platformBps() → uint16
platformWallet() → address
poolManager() → address
positionManager() → address
relaunchWhitelist(address) → bool
symbolUsed(bytes32) → bool
tokenInitCodeHash(string, string, uint256, string) → bytes32
Events (2)
LaunchedRelaunchWhitelistUpdated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_poolManager",
"type": "address"
},
{
"internalType": "address",
"name": "_positionManager",
"type": "address"
},
{
"internalType": "address",
"name": "_permit2",
"type": "address"
},
{
"internalType": "address payable",
"name": "_hook",
"type": "address"
},
{
"internalType": "address",
"name": "_lpLocker",
"type": "address"
},
{
"internalType": "address",
"name": "_platformWallet",
"type": "address"
},
{
"internalType": "uint16",
"name": "_platformBps",
"type": "uint16"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "BadFee",
"type": "error"
},
{
"inputs": [],
"name": "BadStockToken",
"type": "error"
},
{
"inputs": [],
"name": "DevBuyTooLarge",
"type": "error"
},
{
"inputs": [],
"name": "NameTaken",
"type": "error"
},
{
"inputs": [],
"name": "OnlyOwner",
"type": "error"
},
{
"inputs": [],
"name": "SymbolTaken",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "positionTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "devBuyTokens",
"type": "uint256"
}
],
"name": "Launched",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "wallet",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "RelaunchWhitelistUpdated",
"type": "event"
},
{
"inputs": [],
"name": "MAX_DEV_BUY_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "contract VladdyHook",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isLaunchedToken",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "uint256",
"name": "totalSupply",
"type": "uint256"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
},
{
"internalType": "uint16",
"name": "feeBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "buybackBps",
"type": "uint16"
},
{
"internalType": "address",
"name": "agent",
"type": "address"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
},
{
"components": [
{
"internalType": "address",
"name": "addr",
"type": "address"
},
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
},
{
"internalType": "uint8",
"name": "kind",
"type": "uint8"
}
],
"internalType": "struct LaunchFactoryV4.RecipientInput[]",
"name": "recipients",
"type": "tuple[]"
},
{
"internalType": "bool",
"name": "stockRewardsOn",
"type": "bool"
},
{
"internalType": "address",
"name": "stockToken",
"type": "address"
}
],
"internalType": "struct LaunchFactoryV4.LaunchParams",
"name": "p",
"type": "tuple"
}
],
"name": "launch",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "launchCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "launches",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "positionTokenId",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lpLocker",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "nameUsed",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "permit2",
"outputs": [
{
"internalType": "contract IPermit2Minimal",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "platformBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "platformWallet",
"outputs": [
{
"internalType": "address",
"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": "address",
"name": "",
"type": "address"
}
],
"name": "relaunchWhitelist",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"name": "setOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "wallet",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "setRelaunchWhitelist",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "symbolUsed",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "uint256",
"name": "totalSupply",
"type": "uint256"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
}
],
"name": "tokenInitCodeHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {LaunchToken} from "../LaunchToken.sol";
import {VladdyHook} from "./VladdyHook.sol";
import {StockRewardsDistributor} from "./StockRewardsDistributor.sol";
interface IPermit2Minimal {
function approve(address token, address spender, uint160 amount, uint48 expiration) external;
}
/// @title LaunchFactoryV4
/// @notice One-call token launch on Uniswap v4 (Robinhood Chain): deploy the ERC-20, open a native
/// ETH / TOKEN pool wired to the VladdyHook (LP fee 0 — the hook takes the creator-chosen
/// fee of 1% / 2% / 3% / 5% off the ETH side of every swap and distributes it instantly),
/// seed a single-sided position with the whole supply, and lock it in `LpLockerV4` forever.
///
/// Native ETH is always currency0 and the token currency1, so there is no ordering branch:
/// the pool starts at the position's upper tick (100% token) and buys push the tick down.
contract LaunchFactoryV4 {
/* ───────────────────────── immutables ───────────────────────── */
IPoolManager public immutable poolManager;
IPositionManager public immutable positionManager;
IPermit2Minimal public immutable permit2;
VladdyHook public immutable hook;
address public immutable lpLocker;
address public immutable platformWallet;
/// @notice Protocol fee applied to every launch, set once at deploy. Callers cannot bypass it.
uint16 public immutable platformBps;
address public owner;
struct RecipientInput {
address addr;
uint16 bps;
uint8 kind; // 0 wallet, 1 twitter, 2 github
}
struct LaunchParams {
string name;
string symbol;
uint256 totalSupply;
string metadataURI;
int24 tickLower; // position range; pool starts at tickUpper (100% token side)
int24 tickUpper;
uint160 sqrtPriceX96; // initial pool price (must equal sqrtPrice at tickUpper)
uint128 liquidity; // liquidity for the single-sided token position
uint16 feeBps; // creator's chosen swap fee: 100 / 200 / 300 / 500 (1% / 2% / 3% / 5%)
uint16 buybackBps; // creator's chosen buyback reserve (platform fee is fixed by the factory)
address agent;
bytes32 salt; // CREATE2 salt (any value; must be unique per identical name/symbol/supply/URI)
RecipientInput[] recipients;
// ── stock-rewards mode (mutually exclusive with buyback/recipients) ──
bool stockRewardsOn; // when true: buy a tokenized stock with the fees and pay it to holders
address stockToken; // the tokenized stock ERC-20 to reward holders with
}
struct Launch {
address token;
uint256 positionTokenId;
}
Launch[] public launches;
mapping(address => bool) public isLaunchedToken;
/// @notice Names/tickers that have already launched (keyed by keccak of the exact string).
mapping(bytes32 => bool) public nameUsed;
mapping(bytes32 => bool) public symbolUsed;
/// @notice Wallets the owner allows to relaunch an already-used name/ticker (e.g. official relaunches).
mapping(address => bool) public relaunchWhitelist;
/// @notice Max share of supply a dev buy may acquire at launch: 10%.
uint256 public constant MAX_DEV_BUY_BPS = 1_000;
address internal constant DEAD = 0x000000000000000000000000000000000000dEaD;
event Launched(address indexed token, uint256 positionTokenId, uint256 devBuyTokens);
event RelaunchWhitelistUpdated(address indexed wallet, bool allowed);
error BadFee();
error OnlyOwner();
error NameTaken();
error SymbolTaken();
error DevBuyTooLarge();
error BadStockToken();
error ZeroAddress();
constructor(
address _poolManager,
address _positionManager,
address _permit2,
address payable _hook,
address _lpLocker,
address _platformWallet,
uint16 _platformBps
) {
if (_platformBps > 10_000) revert BadFee();
// All of these are immutable — a zero from a misconfigured deploy script would produce a
// permanently broken factory, so fail at deploy time instead.
if (
_poolManager == address(0) || _positionManager == address(0) || _permit2 == address(0)
|| _hook == address(0) || _lpLocker == address(0) || _platformWallet == address(0)
) revert ZeroAddress();
poolManager = IPoolManager(_poolManager);
positionManager = IPositionManager(_positionManager);
permit2 = IPermit2Minimal(_permit2);
hook = VladdyHook(_hook);
lpLocker = _lpLocker;
platformWallet = _platformWallet;
platformBps = _platformBps;
owner = msg.sender;
}
/// @notice Launch a token. Any ETH sent along is an atomic "dev buy": swapped for the new token
/// (fee-exempt, straight to the caller) inside the launch tx, capped at 10% of supply.
function launch(LaunchParams calldata p) external payable returns (address token) {
// 0a. Fee must be one of the offered tiers.
if (p.feeBps != 100 && p.feeBps != 200 && p.feeBps != 300 && p.feeBps != 500) revert BadFee();
// 0b. One launch per name/ticker — unless the owner whitelisted the caller for a relaunch.
bytes32 nameKey = keccak256(bytes(p.name));
bytes32 symbolKey = keccak256(bytes(p.symbol));
if (!relaunchWhitelist[msg.sender]) {
if (nameUsed[nameKey]) revert NameTaken();
if (symbolUsed[symbolKey]) revert SymbolTaken();
}
nameUsed[nameKey] = true;
symbolUsed[symbolKey] = true;
// 1. Deploy token via CREATE2, mint full supply to this factory (to seed liquidity).
LaunchToken t = new LaunchToken{salt: p.salt}(p.name, p.symbol, p.totalSupply, p.metadataURI, address(this));
token = address(t);
// 2. Register the fee config with the hook (platform cut fixed by the factory). In stock-rewards
// mode we first deploy the token's distributor and wire it into the token, then register the
// pool in stock mode (no recipients, no buyback).
if (p.stockRewardsOn) {
if (p.stockToken == address(0)) revert BadStockToken();
address[] memory excluded = new address[](4);
excluded[0] = address(poolManager); // v4 custodies the pool's token liquidity
excluded[1] = lpLocker; // holds the LP NFT
excluded[2] = DEAD; // dust burns
excluded[3] = address(this); // the factory holds the full supply transiently
StockRewardsDistributor distributor = new StockRewardsDistributor(token, p.stockToken, excluded);
t.setDistributor(address(distributor));
VladdyHook.Recipient[] memory none = new VladdyHook.Recipient[](0);
hook.registerPool(token, p.feeBps, platformBps, 0, p.agent, none, true, p.stockToken, address(distributor));
} else {
VladdyHook.Recipient[] memory recs = new VladdyHook.Recipient[](p.recipients.length);
for (uint256 i; i < p.recipients.length; ++i) {
recs[i] = VladdyHook.Recipient({
addr: p.recipients[i].addr,
bps: p.recipients[i].bps,
kind: p.recipients[i].kind
});
}
hook.registerPool(token, p.feeBps, platformBps, p.buybackBps, p.agent, recs, false, address(0), address(0));
}
// 3. Initialize the hooked pool at the launch price.
PoolKey memory key = hook.poolKeyFor(token);
poolManager.initialize(key, p.sqrtPriceX96);
// 4. Mint the single-sided position (100% token) directly to the locker.
uint256 tokenId = _mintLockedPosition(key, p, token);
// 5. Burn any rounding dust the mint left behind so the factory never holds supply.
uint256 dust = IERC20(token).balanceOf(address(this));
if (dust > 0) IERC20(token).transfer(0x000000000000000000000000000000000000dEaD, dust);
// 6. Optional atomic dev buy through the hook (fee-exempt), capped at 10% of supply.
uint256 bought;
if (msg.value > 0) {
bought = hook.devBuy{value: msg.value}(token, msg.sender);
if (bought > (p.totalSupply * MAX_DEV_BUY_BPS) / 10_000) revert DevBuyTooLarge();
}
// 7. Record + finalize.
isLaunchedToken[token] = true;
launches.push(Launch({token: token, positionTokenId: tokenId}));
emit Launched(token, tokenId, bought);
}
/// @dev Mint the position via the v4 PositionManager (pulls ERC-20s through Permit2), owner = locker.
function _mintLockedPosition(PoolKey memory key, LaunchParams calldata p, address token)
internal
returns (uint256 tokenId)
{
IERC20(token).approve(address(permit2), type(uint256).max);
permit2.approve(token, address(positionManager), type(uint160).max, type(uint48).max);
tokenId = positionManager.nextTokenId();
bytes memory actions = abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR));
bytes[] memory params = new bytes[](2);
// MINT_POSITION(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData)
params[0] = abi.encode(
key,
p.tickLower,
p.tickUpper,
p.liquidity,
uint128(0), // amount0Max: native ETH side — single-sided, nothing owed
uint128(p.totalSupply), // amount1Max: the token side
lpLocker,
bytes("")
);
// SETTLE_PAIR(currency0, currency1) — pay what we owe from this factory's balances.
params[1] = abi.encode(key.currency0, key.currency1);
positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp + 60);
}
function launchCount() external view returns (uint256) {
return launches.length;
}
/// @notice Init-code hash for a prospective token — lets callers predict a launch's CREATE2
/// address off-chain with a single RPC call:
/// address = keccak256(0xff ++ factory ++ salt ++ initCodeHash)[12:]
function tokenInitCodeHash(string calldata name, string calldata symbol, uint256 totalSupply, string calldata metadataURI)
external
view
returns (bytes32)
{
return keccak256(
abi.encodePacked(type(LaunchToken).creationCode, abi.encode(name, symbol, totalSupply, metadataURI, address(this)))
);
}
function setOwner(address _owner) external {
if (msg.sender != owner) revert OnlyOwner();
owner = _owner;
}
/// @notice Allow (or revoke) a wallet to relaunch an already-taken name/ticker.
function setRelaunchWhitelist(address wallet, bool allowed) external {
if (msg.sender != owner) revert OnlyOwner();
relaunchWhitelist[wallet] = allowed;
emit RelaunchWhitelistUpdated(wallet, allowed);
}
}
Chain explorer3121msChain node92ms