KaitenFactory
0x50a4c73931988de48258f8372511edeafc0ec3a4
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
12,430 bytes
ABI entries
38
19 read · 6 write
License
none
Contract information
- Address
- 0x50a4c73931988de48258f8372511edeafc0ec3a4
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x6B9cbd5569…87b1642A75
- Creation tx
- 0x797b460ef8…0d8f3a17c7
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (19)
FEE() → uint24
GRADUATION_MIN_HOLD() → uint256
GRADUATION_WETH() → uint256
MAX_LAUNCH_ATTEMPTS() → uint256
POSITION_MANAGER() → address
PRIMARY_AMOUNT() → uint256
SECONDARY_AMOUNT() → uint256
SWAP_ROUTER() → address
TOTAL_SUPPLY() → uint256
V3_FACTORY() → address
WETH() → address
allTokens(uint256) → address
allTokensLength() → uint256
graduationArmedAt(address) → uint64
isGraduated(address) → bool
launches(address) → address, address, uint256, uint256, bool, bool
owner() → address
treasury() → address
wethInPrimary(address) → uint256
Events (5)
FeesClaimedGraduatedLaunchedOwnershipTransferredTreasuryUpdated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "treasury_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "BadStartPrice",
"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": "PoolPreInitialized",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "UnknownToken",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wethToCreator",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenToCreator",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wethToTreasury",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenToTreasury",
"type": "uint256"
}
],
"name": "FeesClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wethInPosition",
"type": "uint256"
}
],
"name": "Graduated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "primaryPositionId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "continuationPositionId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "imageURI",
"type": "string"
}
],
"name": "Launched",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "treasury",
"type": "address"
}
],
"name": "TreasuryUpdated",
"type": "event"
},
{
"inputs": [],
"name": "FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRADUATION_MIN_HOLD",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRADUATION_WETH",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_LAUNCH_ATTEMPTS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POSITION_MANAGER",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PRIMARY_AMOUNT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SECONDARY_AMOUNT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SWAP_ROUTER",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "V3_FACTORY",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "WETH",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allTokens",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "allTokensLength",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "checkGraduation",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "claimFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "graduationArmedAt",
"outputs": [
{
"internalType": "uint64",
"name": "armedAt",
"type": "uint64"
}
],
"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": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "imageURI",
"type": "string"
},
{
"internalType": "uint256",
"name": "initialBuyMinOut",
"type": "uint256"
}
],
"name": "launch",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "launches",
"outputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256",
"name": "primaryPositionId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "continuationPositionId",
"type": "uint256"
},
{
"internalType": "bool",
"name": "tokenIsToken0",
"type": "bool"
},
{
"internalType": "bool",
"name": "graduated",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newTreasury",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"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": "address",
"name": "token",
"type": "address"
}
],
"name": "wethInPrimary",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { KaitenToken } from "./KaitenToken.sol";
import { LiquidityAmounts } from "./libraries/LiquidityAmounts.sol";
import {
INonfungiblePositionManager,
ISwapRouter,
IUniswapV3Factory,
IUniswapV3Pool,
IWETH9
} from "./interfaces/ISushiV3.sol";
/// @title KaitenFactory
/// @notice One-click token launchpad on Sushi V3 (Robinhood Chain). Each launch
/// deploys a fixed-supply KaitenToken, creates its WETH pool at ~2 ETH
/// FDV, and seeds two single-sided positions (80% engineered to raise
/// ~4 ETH, 20% continuation band). The LP NFTs are held by this factory
/// forever — there is NO code path to withdraw principal, only collect()
/// fees, split 50% creator / 50% treasury.
contract KaitenFactory is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
// ─────────────────────────── Sushi V3 on Robinhood Chain (verified on-chain) ──
address public constant WETH = 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73;
address public constant V3_FACTORY = 0xE51960f1B45f1C9FB6D166E6a884F866fC70433B;
address public constant POSITION_MANAGER = 0x51d0e5188afe12d502e29D982d20C190e7816107;
address public constant SWAP_ROUTER = 0xB2d8eD81e79eb64A0751352459eC215FbAFad669;
uint24 public constant FEE = 10000; // 1% tier, tickSpacing 200
// ─────────────────────────── Tokenomics ──
uint256 public constant TOTAL_SUPPLY = 100_000_000e18;
uint256 public constant PRIMARY_AMOUNT = 80_000_000e18; // engineered ~4 ETH raise
uint256 public constant SECONDARY_AMOUNT = 20_000_000e18; // continuation band
uint256 public constant GRADUATION_WETH = 4 ether;
/// @notice the primary position's WETH side must hold >= GRADUATION_WETH for at
/// least this long before the `Graduated` latch fires. This is what makes
/// graduation resistant to a transient spot-price spike: a single-tx flash
/// buy-then-sell crosses the line for zero seconds and cannot span it.
uint256 public constant GRADUATION_MIN_HOLD = 10 minutes;
/// @notice max KaitenToken deploys per launch while routing around a griefer who
/// has pre-created hostile pools at our upcoming token addresses.
uint256 public constant MAX_LAUNCH_ATTEMPTS = 8;
// Range ticks (aligned to tickSpacing 200). See ANALYSIS.md for the derivation.
// Orientation A — TOKEN is token0 (TOKEN address < WETH): price = WETH/TOKEN.
int24 internal constant A_TICK_LOWER = -177200; // start FDV ~2 ETH
int24 internal constant A_TICK_UPPER = -159000; // top of primary, FDV ~12.5 ETH
int24 internal constant A_TICK_UPPER2 = -100000; // top of continuation band
// Init AT the primary lower tick: single-sided all-token0 with no dust, and no
// zero-liquidity gap between the start price and the position (so buys trade).
uint160 internal constant A_START_SQRT = 11251822014334092487017933; // sqrtRatio(-177200)
uint160 internal constant A_SQRT_LOWER = 11251822014334092487017933; // sqrtRatio(-177200)
uint160 internal constant A_SQRT_UPPER = 27951883217009894848890166; // sqrtRatio(-159000)
// Orientation B — TOKEN is token1 (TOKEN address > WETH): price = TOKEN/WETH (mirror).
int24 internal constant B_TICK_LOWER = 159000;
int24 internal constant B_TICK_UPPER = 177200;
int24 internal constant B_TICK_LOWER2 = 100000;
// Init AT the primary upper tick: single-sided all-token1 with no dust / no gap.
uint160 internal constant B_START_SQRT = 557874247156599145362199702308705; // sqrtRatio(+177200)
uint160 internal constant B_SQRT_LOWER = 224568115380748325923021391801200; // sqrtRatio(+159000)
uint160 internal constant B_SQRT_UPPER = 557874247156599145362199702308705; // sqrtRatio(+177200)
// ─────────────────────────── State ──
struct Launch {
address creator;
address pool;
uint256 primaryPositionId; // engineered for the 4 ETH graduation target
uint256 continuationPositionId;
bool tokenIsToken0;
bool graduated;
}
/// @notice protocol treasury — receives 50% of fees. Owner-updatable (the ONLY owner power).
address public treasury;
mapping(address token => Launch) public launches;
/// @notice timestamp at which a token's primary WETH side was first seen at/above
/// GRADUATION_WETH and has been continuously observed there since (0 = not
/// armed). Reset to 0 whenever a `checkGraduation` call observes it below.
mapping(address token => uint64 armedAt) public graduationArmedAt;
address[] public allTokens;
// ─────────────────────────── Events ──
event Launched(
address indexed token,
address indexed creator,
address pool,
uint256 primaryPositionId,
uint256 continuationPositionId,
string name,
string symbol,
string imageURI
);
event FeesClaimed(
address indexed token,
address indexed creator,
uint256 wethToCreator,
uint256 tokenToCreator,
uint256 wethToTreasury,
uint256 tokenToTreasury
);
event TreasuryUpdated(address indexed treasury);
event Graduated(address indexed token, address indexed pool, uint256 wethInPosition);
error UnknownToken();
error PoolPreInitialized();
error BadStartPrice();
constructor(address treasury_) Ownable(msg.sender) {
treasury = treasury_ == address(0) ? msg.sender : treasury_;
}
function allTokensLength() external view returns (uint256) {
return allTokens.length;
}
// ─────────────────────────── Launch ──
/// @notice Deploy a token, create its Sushi V3 pool, seed both single-sided
/// positions, and (optionally) buy the first serving as the creator.
/// @param initialBuyMinOut min tokens out for the optional initial buy (msg.value).
function launch(
string calldata name,
string calldata symbol,
string calldata imageURI,
uint256 initialBuyMinOut
) external payable nonReentrant returns (address token) {
// The token address is a deterministic CREATE, so an attacker watching the
// mempool can pre-create+initialize its pool at a hostile price before this
// tx lands (createAndInitializePoolIfNecessary would then silently return
// that pool, and the single-sided mint traps or the initial buy is stolen).
// Defense: never touch a pool we didn't just create at the engineered price.
// If our next token address already has a pool at the wrong price, discard it
// and deploy the next one — routing around targeted griefing.
KaitenToken kt;
bool isT0;
address token0;
address token1;
uint160 startSqrt;
address pool;
uint256 attempts;
while (true) {
// Deploy via CREATE2 with a salt that varies by block, launcher, launch
// index and attempt. This is what makes the griefing above non-permanent:
// a reverting launch rolls back a plain CREATE nonce, so a griefer who
// pre-creates pools at the next few nonce-derived addresses would brick
// the factory forever. With block.timestamp in the salt, a retry in a
// later block lands on fresh addresses the attacker couldn't pre-grief.
bytes32 salt = keccak256(abi.encode(msg.sender, block.timestamp, allTokens.length, attempts));
kt = new KaitenToken{ salt: salt }(name, symbol, imageURI, msg.sender);
token = address(kt);
isT0 = token < WETH;
(token0, token1) = isT0 ? (token, WETH) : (WETH, token);
startSqrt = isT0 ? A_START_SQRT : B_START_SQRT;
address existing = IUniswapV3Factory(V3_FACTORY).getPool(token0, token1, FEE);
if (existing == address(0)) {
// No pool yet — create it ourselves at the engineered start price.
pool = INonfungiblePositionManager(POSITION_MANAGER)
.createAndInitializePoolIfNecessary(token0, token1, FEE, startSqrt);
break;
}
// A pool already exists. Trust it only if it sits at our exact engineered
// price (a harmless pre-create); otherwise skip this token address.
(uint160 sp,,,,,,) = IUniswapV3Pool(existing).slot0();
if (sp == startSqrt) {
pool = existing;
break;
}
unchecked {
++attempts;
}
if (attempts >= MAX_LAUNCH_ATTEMPTS) revert PoolPreInitialized();
}
// Hard invariant: we only ever seed liquidity against the engineered price.
(uint160 nowSqrt,,,,,,) = IUniswapV3Pool(pool).slot0();
if (nowSqrt != startSqrt) revert BadStartPrice();
kt.initializeAntiSnipe(pool); // starts the 5-minute cap window now
IERC20(token).forceApprove(POSITION_MANAGER, TOTAL_SUPPLY);
// Primary (engineered) + continuation single-sided positions.
(int24 pL, int24 pU, int24 cL, int24 cU) = isT0
? (A_TICK_LOWER, A_TICK_UPPER, A_TICK_UPPER, A_TICK_UPPER2)
: (B_TICK_LOWER, B_TICK_UPPER, B_TICK_LOWER2, B_TICK_LOWER);
uint256 primaryId = _mintPosition(token0, token1, isT0, pL, pU, PRIMARY_AMOUNT);
uint256 continuationId = _mintPosition(token0, token1, isT0, cL, cU, SECONDARY_AMOUNT);
launches[token] = Launch({
creator: msg.sender,
pool: pool,
primaryPositionId: primaryId,
continuationPositionId: continuationId,
tokenIsToken0: isT0,
graduated: false
});
allTokens.push(token);
emit Launched(token, msg.sender, pool, primaryId, continuationId, name, symbol, imageURI);
// Optional creator initial buy — exempt from the anti-snipe cap (recipient == creator).
if (msg.value > 0) {
IWETH9(WETH).deposit{ value: msg.value }();
IERC20(WETH).forceApprove(SWAP_ROUTER, msg.value);
ISwapRouter(SWAP_ROUTER)
.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: WETH,
tokenOut: token,
fee: FEE,
recipient: msg.sender,
amountIn: msg.value,
amountOutMinimum: initialBuyMinOut,
sqrtPriceLimitX96: 0
})
);
}
}
function _mintPosition(
address token0,
address token1,
bool isT0,
int24 tickLower,
int24 tickUpper,
uint256 amount
) internal returns (uint256 tokenId) {
(uint256 amount0, uint256 amount1) = isT0 ? (amount, uint256(0)) : (uint256(0), amount);
(tokenId,,,) = INonfungiblePositionManager(POSITION_MANAGER)
.mint(
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: FEE,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: amount0,
amount1Desired: amount1,
amount0Min: 0,
amount1Min: 0,
recipient: address(this), // LP NFT locked in the factory forever
deadline: block.timestamp
})
);
}
// ─────────────────────────── Fees (locked LP: collect only) ──
/// @notice Collect trading fees from both positions and split 50/50 to the
/// token's creator and the treasury. Principal is never touched.
/// Permissionless: destinations are fixed (recorded creator + treasury),
/// so a non-creator caller can never redirect the creator's share.
function claimFees(address token) external nonReentrant {
Launch storage l = launches[token];
if (l.pool == address(0)) revert UnknownToken();
uint128 max = type(uint128).max;
(uint256 p0, uint256 p1) = INonfungiblePositionManager(POSITION_MANAGER)
.collect(INonfungiblePositionManager.CollectParams(l.primaryPositionId, address(this), max, max));
(uint256 c0, uint256 c1) = INonfungiblePositionManager(POSITION_MANAGER)
.collect(
INonfungiblePositionManager.CollectParams(l.continuationPositionId, address(this), max, max)
);
uint256 fee0 = p0 + c0;
uint256 fee1 = p1 + c1;
(uint256 wethFee, uint256 tokenFee) = l.tokenIsToken0 ? (fee1, fee0) : (fee0, fee1);
uint256 wethCreator = wethFee / 2;
uint256 tokenCreator = tokenFee / 2;
address creator = l.creator;
if (wethFee > 0) {
IERC20(WETH).safeTransfer(creator, wethCreator);
IERC20(WETH).safeTransfer(treasury, wethFee - wethCreator);
}
if (tokenFee > 0) {
IERC20(token).safeTransfer(creator, tokenCreator);
IERC20(token).safeTransfer(treasury, tokenFee - tokenCreator);
}
emit FeesClaimed(
token, creator, wethCreator, tokenCreator, wethFee - wethCreator, tokenFee - tokenCreator
);
}
// ─────────────────────────── Treasury (only owner power) ──
function setTreasury(address newTreasury) external onlyOwner {
require(newTreasury != address(0), "zero treasury");
treasury = newTreasury;
emit TreasuryUpdated(newTreasury);
}
// ─────────────────────────── Graduation (indexing only) ──
/// @notice WETH currently held by the primary (engineered) position.
function wethInPrimary(address token) public view returns (uint256) {
Launch storage l = launches[token];
if (l.pool == address(0)) revert UnknownToken();
(,,,,,,, uint128 liquidity,,,,) =
INonfungiblePositionManager(POSITION_MANAGER).positions(l.primaryPositionId);
(uint160 sqrtCurrent,,,,,,) = IUniswapV3Pool(l.pool).slot0();
(uint160 sqrtLower, uint160 sqrtUpper) =
l.tokenIsToken0 ? (A_SQRT_LOWER, A_SQRT_UPPER) : (B_SQRT_LOWER, B_SQRT_UPPER);
(uint256 amount0, uint256 amount1) =
LiquidityAmounts.getAmountsForLiquidity(sqrtCurrent, sqrtLower, sqrtUpper, liquidity);
return l.tokenIsToken0 ? amount1 : amount0;
}
function isGraduated(address token) public view returns (bool) {
return wethInPrimary(token) >= GRADUATION_WETH;
}
/// @notice Latch `Graduated` once — but only after the primary position's WETH
/// side has stayed at/above GRADUATION_WETH for at least
/// GRADUATION_MIN_HOLD. Requires two calls: the first *arms* the sustain
/// timer, a later one (past the hold, still at/above the line) *latches*.
///
/// Rationale (M-1): `wethInPrimary` reads the spot price, which a single
/// transaction can spike (flash buy → check → sell) to momentarily cross
/// 4 ETH. Requiring the level to persist across wall-clock time means a
/// zero-duration spike can never latch — only genuine, sustained
/// accumulation does. A drop below the line disarms the timer.
///
/// Residual (accepted, signal-only): an attacker willing to hold >= 4 ETH
/// of real buy pressure parked in the pool across two spikes MIN_HOLD apart
/// could still latch it — but that is expensive, self-funded, and
/// indistinguishable from real demand; no funds are affected either way.
function checkGraduation(address token) external {
Launch storage l = launches[token];
if (l.pool == address(0)) revert UnknownToken();
if (l.graduated) return;
uint256 weth = wethInPrimary(token);
if (weth < GRADUATION_WETH) {
// fell back below the line — disarm any pending sustain timer
if (graduationArmedAt[token] != 0) graduationArmedAt[token] = 0;
return;
}
uint64 armedAt = graduationArmedAt[token];
if (armedAt == 0) {
// first sighting at/above the line — arm the timer, do NOT latch yet
graduationArmedAt[token] = uint64(block.timestamp);
return;
}
if (block.timestamp >= uint256(armedAt) + GRADUATION_MIN_HOLD) {
l.graduated = true;
emit Graduated(token, l.pool, weth);
}
}
}
Chain explorer5771msChain node72ms