RunnerV3
0x3e2b5a5975ea2be330589d50b2df42e638350567
Verification
Verified
v0.8.30+commit.73712a01
Type
Contract
13,350 bytes
ABI entries
61
21 read · 11 write
License
none
Contract information
- Address
- 0x3e2b5a5975ea2be330589d50b2df42e638350567
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.30+commit.73712a01
- Optimization
- Enabled
- Creator
- 0x11B5f32718…0a06447E91
- Creation tx
- 0x0d53da48fd…adb3d19fc1
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (21)
BPS() → uint256
DEAD() → address
POOL_FEE() → uint24
TOTAL_SUPPLY() → uint256
accruedFees() → uint256
allTokens(uint256) → address
bonded(address) → bool
creationFee() → uint256
creationPaused() → bool
creatorFees(address) → uint256
creatorShareBps() → uint16
getLaunch(address) → tuple
launches(address) → address, address, uint256, uint16, bool, bool
owner() → address
pendingOwner() → address
poolState(address) → address, address, bool, uint160, int24, uint128
positionManager() → address
renounceOwnership()
tokenCount() → uint256
treasury() → address
weth() → address
Events (10)
CreationFeeUpdatedCreationPausedSetCreatorFeesClaimedCreatorShareUpdatedFeesCollectedLpFeesCollectedOwnershipTransferStartedOwnershipTransferredTokenCreatedTreasuryUpdated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "uint256",
"name": "creationFee_",
"type": "uint256"
},
{
"internalType": "address",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "uint16",
"name": "creatorShareBps_",
"type": "uint16"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "CannotRenounce",
"type": "error"
},
{
"inputs": [],
"name": "CreationFeeTooHigh",
"type": "error"
},
{
"inputs": [],
"name": "CreationIsPaused",
"type": "error"
},
{
"inputs": [],
"name": "CreatorShareTooLow",
"type": "error"
},
{
"inputs": [],
"name": "EthTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientCreationFee",
"type": "error"
},
{
"inputs": [],
"name": "InvalidShare",
"type": "error"
},
{
"inputs": [],
"name": "NoDirectEth",
"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": "PoolPriceOutOfRange",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [],
"name": "TokenTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "Unauthorized",
"type": "error"
},
{
"inputs": [],
"name": "UnknownToken",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAmount",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"name": "CreationFeeUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "CreationPausedSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "CreatorFeesClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "creatorShareBps",
"type": "uint16"
}
],
"name": "CreatorShareUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "treasury",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "FeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethToCreator",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethToTreasury",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokToCreator",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokToTreasury",
"type": "uint256"
}
],
"name": "LpFeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"indexed": false,
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"name": "TokenCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "treasury",
"type": "address"
}
],
"name": "TreasuryUpdated",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DEAD",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "accruedFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allTokens",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "bonded",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "claimCreatorFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "collectFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "collectLpFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "uint256",
"name": "salt",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxCreationFee",
"type": "uint256"
},
{
"internalType": "uint16",
"name": "minCreatorShareBps",
"type": "uint16"
}
],
"name": "createToken",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "creationFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "creationPaused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "creatorFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "creatorShareBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "getLaunch",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256",
"name": "lpTokenId",
"type": "uint256"
},
{
"internalType": "uint16",
"name": "creatorLpShareBps",
"type": "uint16"
},
{
"internalType": "bool",
"name": "wethIsToken0",
"type": "bool"
},
{
"internalType": "bool",
"name": "exists",
"type": "bool"
}
],
"internalType": "struct RunnerV3.Launch",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "launches",
"outputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256",
"name": "lpTokenId",
"type": "uint256"
},
{
"internalType": "uint16",
"name": "creatorLpShareBps",
"type": "uint16"
},
{
"internalType": "bool",
"name": "wethIsToken0",
"type": "bool"
},
{
"internalType": "bool",
"name": "exists",
"type": "bool"
}
],
"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": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "poolState",
"outputs": [
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "bool",
"name": "isBonded",
"type": "bool"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "int24",
"name": "tick",
"type": "int24"
},
{
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"name": "setCreationFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "paused",
"type": "bool"
}
],
"name": "setCreationPaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "bps",
"type": "uint16"
}
],
"name": "setCreatorShareBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "t",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "tokenCount",
"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": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "int256",
"name": "amount0Delta",
"type": "int256"
},
{
"internalType": "int256",
"name": "amount1Delta",
"type": "int256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "uniswapV3SwapCallback",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {RunnerToken} from "./RunnerToken.sol";
import {IWETH, IUniswapV3Pool, INonfungiblePositionManager} from "./interfaces/IUniswapV3.sol";
/// @title RunnerV3 — launchpad that lists tokens on Uniswap V3 FROM BLOCK ONE
///
/// PROTOTYPE / not deployed. Design spec: docs/SINGLE_SIDED_V3_LAUNCHPAD.md.
///
/// Motivation
/// ----------
/// The live `Runner.sol` trades against a virtual-reserve curve and only mints a
/// real Uniswap pool at graduation, so pre-bond tokens are invisible to
/// DexScreener / DEX trackers ("nobody can find them"). RunnerV3 instead launches
/// each token straight into a real Uniswap V3 pool, so it is discoverable
/// immediately — while still capturing fees and staying rug-proof.
///
/// How it works
/// ------------
/// 1. `createToken` deploys the token (fixed 1B supply, minted here), creates the
/// (token, WETH, 1%) pool, and mints ONE **single-sided** position holding the
/// entire supply as tokens (no ETH). Because a token-only V3 range can only
/// SELL tokens as price rises, that position *is* the bonding curve — expressed
/// as real Uniswap liquidity. Buyers swap ETH in on Uniswap; the price walks up
/// the range exactly like the old curve (same launch/"bond" endpoints).
/// 2. **Liquidity is locked from block one.** This contract holds the position NFT
/// and there is NO `decreaseLiquidity`/withdraw path anywhere — principal can
/// never be pulled (rug-proof), the same guarantee the live contract only
/// reaches at graduation. Nothing to migrate, no second market.
/// 3. **Fees:** every swap pays the pool's 1% fee. This contract is the sole LP
/// *by default*, so that fee accrues to our position; `collectLpFees` sweeps
/// it and splits creator/treasury like the live contract's post-grad logic.
/// NOTE: sole-LP status is NOT enforceable — anyone may add liquidity (incl.
/// just-in-time) to the same canonical pool and take a pro-rata share of
/// FUTURE fees. That dilutes revenue but is not theft (Uniswap's per-position
/// fee accounting means outsiders can't touch our already-accrued fees, and
/// principal stays locked). A V4 hook would make capture enforceable if/when
/// V4 ships on this chain. (Affiliate rewards stay off-chain, unchanged.)
/// 4. `bonded(token)` is a read-only milestone (price crossed the ~48-ETH-FDV
/// tick) for the UI/indexers — it changes no liquidity.
///
/// FRONT-RUN DEFENSE: identical to `Runner.sol`. A griefer can pre-create the
/// deterministic (token, WETH, 1%) pool at a bad price. `_syncPoolPrice` re-reads
/// the live price and nudges it toward target with a DUST-BOUNDED (1-wei
/// exact-input) corrective swap before seeding; a zero-liquidity pre-init walks to
/// target for ~free, a funded mis-price is either within tolerance or reverts
/// (griefing DoS, never theft — the 1-wei input structurally caps our spend and
/// the callback is locked to the pool being synced).
///
/// Single-sided tick constants are the SAME for every token per address ordering
/// (all tokens share params), so they are precomputed off-chain and hardcoded
/// below rather than derived on-chain — see docs + the fork test that asserts the
/// position lands single-sided at the intended price.
contract RunnerV3 is Ownable2Step, ReentrancyGuard {
// ------------------------------------------------------------- constants
uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
uint256 public constant BPS = 10_000;
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
/// 1% fee tier (memecoin standard; tick spacing 200).
uint24 public constant POOL_FEE = 10_000;
// Single-sided range, precomputed for the token's ETH price walking from
// P0 = 3.24e-9 (launch, FDV ~3.24 ETH) up to P_TOP = 2.12e-4 (FDV ~212k ETH),
// with the ~48-ETH-FDV "bond" milestone at P1 = 4.82e-8 in between. Two
// orderings because the token/WETH pair orders by address:
//
// Ordering A — WETH < token (token0 = WETH, token1 = token):
// pool price = token/WETH, so the token APPRECIATES as the tick FALLS.
// Single-sided token (=token1) ⇒ init at the UPPER tick (position all token1).
int24 internal constant A_TICK_LOWER = 84_400;
int24 internal constant A_TICK_UPPER = 195_600;
uint160 internal constant A_INIT_SQRT = 1_399_804_099_006_039_538_398_973_723_506_460; // sqrtRatio(195600)
int24 internal constant A_BOND_TICK = 168_400; // bonded once tick <= this
// Ordering B — token < WETH (token0 = token, token1 = WETH):
// pool price = WETH/token, so the token APPRECIATES as the tick RISES.
// Single-sided token (=token0) ⇒ init at the LOWER tick (position all token0).
int24 internal constant B_TICK_LOWER = -195_600;
int24 internal constant B_TICK_UPPER = -84_400;
uint160 internal constant B_INIT_SQRT = 4_484_271_577_604_230_056_945_578; // sqrtRatio(-195600)
int24 internal constant B_BOND_TICK = -168_400; // bonded once tick >= this
/// Backstop tolerance between the live pool price and our target after the
/// corrective swap (bps).
uint256 internal constant INIT_PRICE_TOLERANCE_BPS = 100; // 1%
/// Hard cap on what the 1-wei corrective swap may consume of either token.
uint256 internal constant CORRECTIVE_SWAP_MAX_SPEND = 100; // wei
// ----------------------------------------------------------------- types
struct Launch {
address creator;
address pool;
uint256 lpTokenId; // locked position NFT — never decreased
uint16 creatorLpShareBps; // creator's fee share, snapshotted at creation
bool wethIsToken0;
bool exists;
}
// ----------------------------------------------------------------- state
INonfungiblePositionManager public immutable positionManager;
address public immutable weth;
address public treasury;
uint256 public creationFee;
uint16 public creatorShareBps;
bool public creationPaused;
uint256 public accruedFees; // treasury pull balance (ETH)
mapping(address => uint256) public creatorFees; // creator pull balances (ETH)
mapping(address => Launch) public launches;
address[] public allTokens;
address private _pendingPool; // swap-callback guard
// ---------------------------------------------------------------- events
event TokenCreated(
address indexed token, address indexed creator, string name, string symbol, string metadataURI, address pool
);
event LpFeesCollected(
address indexed token, uint256 ethToCreator, uint256 ethToTreasury, uint256 tokToCreator, uint256 tokToTreasury
);
event CreatorFeesClaimed(address indexed creator, uint256 amount);
event FeesCollected(address indexed treasury, uint256 amount);
event CreationFeeUpdated(uint256 fee);
event TreasuryUpdated(address treasury);
event CreatorShareUpdated(uint16 creatorShareBps);
event CreationPausedSet(bool paused);
// ---------------------------------------------------------------- errors
error ZeroAddress();
error InvalidShare();
error CreationIsPaused();
error InsufficientCreationFee();
error CreationFeeTooHigh();
error CreatorShareTooLow();
error CannotRenounce();
error UnknownToken();
error ZeroAmount();
error EthTransferFailed();
error TokenTransferFailed();
error PoolPriceOutOfRange();
error Unauthorized();
error NoDirectEth();
// ----------------------------------------------------------- constructor
constructor(
address treasury_,
uint256 creationFee_,
address positionManager_,
address weth_,
uint16 creatorShareBps_
) Ownable(msg.sender) {
if (treasury_ == address(0) || positionManager_ == address(0) || weth_ == address(0)) revert ZeroAddress();
if (creatorShareBps_ > BPS) revert InvalidShare();
treasury = treasury_;
creationFee = creationFee_;
positionManager = INonfungiblePositionManager(positionManager_);
weth = weth_;
creatorShareBps = creatorShareBps_;
}
/// Only WETH (on unwrap) may send ETH directly.
receive() external payable {
if (msg.sender != weth) revert NoDirectEth();
}
// ------------------------------------------------------------- create
/// @notice Launch a token straight onto Uniswap. `msg.value` must cover
/// `creationFee` (any surplus is refunded — dev-buys happen on the pool).
/// `salt` makes the token address per-(sender,salt) — retry with a fresh salt
/// if a launch reverts (see the C-1 note below). `maxCreationFee` /
/// `minCreatorShareBps` are caller-supplied guards against the owner
/// front-running a worse fee/share (pass type(uint256).max / 0 to waive).
function createToken(
string calldata name,
string calldata symbol,
string calldata metadataURI,
uint256 salt,
uint256 maxCreationFee,
uint16 minCreatorShareBps
) external payable nonReentrant returns (address token) {
if (creationPaused) revert CreationIsPaused();
if (msg.value < creationFee) revert InsufficientCreationFee();
if (creationFee > maxCreationFee) revert CreationFeeTooHigh();
if (creatorShareBps < minCreatorShareBps) revert CreatorShareTooLow();
// C-1 FIX — deploy via CREATE2 at a per-(sender,salt) address instead of
// the sequential CREATE nonce. The old nonce-derived address was globally
// predictable, so a griefer could pre-seed the deterministic NEXT pool
// with single-sided WETH liquidity (Uniswap only checks balanceOf on the
// non-zero side, so a WETH-only range never touches the not-yet-deployed
// token). That funded mis-price made createToken revert; the revert rolled
// back the nonce; the SAME address recurred forever → the whole launchpad
// was permanently bricked from block one. A per-(sender,salt) address
// isolates creators from each other and lets a griefed creator escape by
// retrying with a fresh salt, reducing the attack to self-healing mempool
// griefing (the attacker pays gas + WETH each attempt and it stops the
// moment they do).
token = address(new RunnerToken{salt: keccak256(abi.encode(msg.sender, salt))}(name, symbol, TOTAL_SUPPLY));
bool wethIsToken0 = weth < token;
// Order the pair and pick the single-sided config for this ordering.
(address token0, address token1) = wethIsToken0 ? (weth, token) : (token, weth);
(int24 tickLower, int24 tickUpper, uint160 initSqrt) = wethIsToken0
? (A_TICK_LOWER, A_TICK_UPPER, A_INIT_SQRT)
: (B_TICK_LOWER, B_TICK_UPPER, B_INIT_SQRT);
// Create + initialize the pool at the launch price, then defend against a
// front-run pre-init by nudging the live price back to target.
address pool = positionManager.createAndInitializePoolIfNecessary(token0, token1, POOL_FEE, initSqrt);
_syncPoolPrice(pool, token0, token1, initSqrt);
// Mint ONE single-sided position holding the entire supply as tokens
// (no ETH side). This contract keeps the NFT; there is no path to remove
// the liquidity, so it is locked from this block forward.
if (!RunnerToken(token).approve(address(positionManager), TOTAL_SUPPLY)) revert TokenTransferFailed();
(uint256 amount0Desired, uint256 amount1Desired) =
wethIsToken0 ? (uint256(0), TOTAL_SUPPLY) : (TOTAL_SUPPLY, uint256(0));
(uint256 tokenId,,,) = positionManager.mint(
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: POOL_FEE,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: amount0Desired,
amount1Desired: amount1Desired,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp
})
);
RunnerToken(token).approve(address(positionManager), 0);
launches[token] = Launch({
creator: msg.sender,
pool: pool,
lpTokenId: tokenId,
creatorLpShareBps: creatorShareBps,
wethIsToken0: wethIsToken0,
exists: true
});
allTokens.push(token);
// Any token dust the single-sided mint didn't consume is burned (fair
// launch — the launchpad keeps no token bag).
uint256 leftover = RunnerToken(token).balanceOf(address(this));
if (leftover > 0 && !RunnerToken(token).transfer(DEAD, leftover)) revert TokenTransferFailed();
if (creationFee > 0) accruedFees += creationFee;
uint256 refund = msg.value - creationFee;
if (refund > 0) _sendEth(msg.sender, refund);
emit TokenCreated(token, msg.sender, name, symbol, metadataURI, pool);
}
// --------------------------------------------------------------- fees
/// @notice Collect the pool's accrued 1% swap fees (this contract is the sole
/// LP) and split them creator/treasury using the share snapshotted at
/// creation. ETH side accrues to pull balances; token side is pushed out.
/// Callable by anyone.
function collectLpFees(address token) external nonReentrant {
Launch storage L = launches[token];
if (!L.exists) revert UnknownToken();
address creator = L.creator;
uint256 share = L.creatorLpShareBps;
(uint256 amount0, uint256 amount1) = positionManager.collect(
INonfungiblePositionManager.CollectParams({
tokenId: L.lpTokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
(uint256 wethAmt, uint256 tokAmt) = L.wethIsToken0 ? (amount0, amount1) : (amount1, amount0);
uint256 ethToCreator;
uint256 ethToTreasury;
if (wethAmt > 0) {
IWETH(weth).withdraw(wethAmt);
ethToCreator = (wethAmt * share) / BPS;
ethToTreasury = wethAmt - ethToCreator;
if (ethToCreator > 0) creatorFees[creator] += ethToCreator;
accruedFees += ethToTreasury;
}
uint256 tokToCreator;
uint256 tokToTreasury;
if (tokAmt > 0) {
tokToCreator = (tokAmt * share) / BPS;
tokToTreasury = tokAmt - tokToCreator;
if (tokToCreator > 0 && !RunnerToken(token).transfer(creator, tokToCreator)) revert TokenTransferFailed();
if (tokToTreasury > 0 && !RunnerToken(token).transfer(treasury, tokToTreasury)) revert TokenTransferFailed();
}
emit LpFeesCollected(token, ethToCreator, ethToTreasury, tokToCreator, tokToTreasury);
}
/// @notice Claim accrued creator fees (ETH) for msg.sender.
function claimCreatorFees() external nonReentrant {
uint256 amount = creatorFees[msg.sender];
if (amount == 0) revert ZeroAmount();
creatorFees[msg.sender] = 0;
emit CreatorFeesClaimed(msg.sender, amount);
_sendEth(msg.sender, amount);
}
/// @notice Push accrued protocol fees (creation fees + treasury LP share) to
/// the treasury. Callable by anyone.
function collectFees() external nonReentrant {
uint256 amount = accruedFees;
if (amount == 0) revert ZeroAmount();
accruedFees = 0;
emit FeesCollected(treasury, amount);
_sendEth(treasury, amount);
}
// ------------------------------------------------ front-run defense
/// If the pool's live price differs from `target`, walk it back with a
/// dust-bounded 1-wei exact-input swap, then require it landed within
/// tolerance. Zero-liquidity pre-init walks to target for ~free; a funded
/// mis-price either sits within tolerance or reverts (griefing DoS, not theft
/// — exact-input of 1 wei caps our spend and the callback is pool-locked).
function _syncPoolPrice(address pool, address token0, address token1, uint160 target) internal {
(uint160 cur,,,,,,) = IUniswapV3Pool(pool).slot0();
if (cur == target) return;
uint256 bal0Before = IERC20(token0).balanceOf(address(this));
uint256 bal1Before = IERC20(token1).balanceOf(address(this));
_pendingPool = pool;
IUniswapV3Pool(pool).swap(address(this), cur > target, int256(1), target, abi.encode(token0, token1));
_pendingPool = address(0);
uint256 bal0After = IERC20(token0).balanceOf(address(this));
uint256 bal1After = IERC20(token1).balanceOf(address(this));
if (bal0Before > bal0After + CORRECTIVE_SWAP_MAX_SPEND) revert PoolPriceOutOfRange();
if (bal1Before > bal1After + CORRECTIVE_SWAP_MAX_SPEND) revert PoolPriceOutOfRange();
(uint160 landed,,,,,,) = IUniswapV3Pool(pool).slot0();
uint256 diff = landed > target ? landed - target : target - landed;
if (diff * BPS > uint256(target) * INIT_PRICE_TOLERANCE_BPS) revert PoolPriceOutOfRange();
}
/// Uniswap V3 swap callback — pays the pool what the 1-wei corrective swap
/// owes. Locked to the pool being synced during an active createToken.
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
if (_pendingPool == address(0) || msg.sender != _pendingPool) revert Unauthorized();
(address token0, address token1) = abi.decode(data, (address, address));
if (amount0Delta > 0) _payPool(token0, uint256(amount0Delta));
if (amount1Delta > 0) _payPool(token1, uint256(amount1Delta));
}
function _payPool(address t, uint256 amount) internal {
bool ok = t == weth ? IWETH(weth).transfer(msg.sender, amount) : RunnerToken(t).transfer(msg.sender, amount);
if (!ok) revert TokenTransferFailed();
}
function _sendEth(address to, uint256 amount) internal {
(bool ok,) = to.call{value: amount}("");
if (!ok) revert EthTransferFailed();
}
// ---------------------------------------------------------------- views
/// Live (instantaneous) bond check from the current tick. Token appreciates as
/// the tick falls (ordering A) or rises (ordering B).
function _liveBonded(Launch storage L) internal view returns (bool) {
(, int24 tick,,,,,) = IUniswapV3Pool(L.pool).slot0();
return L.wethIsToken0 ? (tick <= A_BOND_TICK) : (tick >= B_BOND_TICK);
}
/// @notice Is the token's CURRENT spot price past the ~48-ETH-FDV bond tick?
/// LIVE and instantaneous — deliberately NOT monotonic and NOT a
/// safety/legitimacy signal. There is no on-chain latch (round-2 audit: a
/// permanent latch on a spot read could be set true forever by a single
/// flash-loaned round-trip across the tick), so a transient cross can never
/// stick. Bonding here is a soft discovery/UI marker that moves no liquidity;
/// integrators MUST derive any legitimacy/pricing decision from a TWAP, never
/// from this spot read.
function bonded(address token) public view returns (bool) {
Launch storage L = launches[token];
if (!L.exists) revert UnknownToken();
return _liveBonded(L);
}
/// @notice Current pool spot state for a launched token. `isBonded` /
/// `sqrtPriceX96` / `tick` / `liquidity` are ALL live spot reads — do not
/// trust mid-swap; price off a TWAP for anything that matters.
function poolState(address token)
external
view
returns (address pool, address creator, bool isBonded, uint160 sqrtPriceX96, int24 tick, uint128 liquidity)
{
Launch storage L = launches[token];
if (!L.exists) revert UnknownToken();
pool = L.pool;
creator = L.creator;
(sqrtPriceX96, tick,,,,,) = IUniswapV3Pool(pool).slot0();
liquidity = IUniswapV3Pool(pool).liquidity();
isBonded = _liveBonded(L);
}
function getLaunch(address token) external view returns (Launch memory) {
return launches[token];
}
function tokenCount() external view returns (uint256) {
return allTokens.length;
}
// ---------------------------------------------------------------- admin
function setCreationFee(uint256 fee) external onlyOwner {
creationFee = fee;
emit CreationFeeUpdated(fee);
}
function setTreasury(address t) external onlyOwner {
if (t == address(0)) revert ZeroAddress();
treasury = t;
emit TreasuryUpdated(t);
}
/// Applies to tokens launched AFTER the change (each token snapshots its share
/// at creation, so live creators are never diluted).
function setCreatorShareBps(uint16 bps) external onlyOwner {
if (bps > BPS) revert InvalidShare();
creatorShareBps = bps;
emit CreatorShareUpdated(bps);
}
function setCreationPaused(bool paused) external onlyOwner {
creationPaused = paused;
emit CreationPausedSet(paused);
}
/// Disabled: renouncing would freeze treasury/fee/pause controls forever and
/// could permanently strand protocol fees if `treasury` ever can't receive
/// ETH. Hand ownership to a timelock/Safe via transferOwnership instead.
function renounceOwnership() public view override onlyOwner {
revert CannotRenounce();
}
}
Chain explorer785msChain node80ms