RobinFunFactory
0x653a69c1fc844bf6596a322a05e07151e36f46f4
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
15,898 bytes
ABI entries
52
27 read · 12 write
License
none
Contract information
- Address
- 0x653a69c1fc844bf6596a322a05e07151e36f46f4
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xf94a68B1d0…fd5fc58821
- Creation tx
- 0xa44ef28978…d6ef38ddb4
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (27)
CURVE_SUPPLY() → uint256
DEAD() → address
LP_SUPPLY() → uint256
LP_USD() → uint256
MAX_NAME_LEN() → uint256
MAX_SYMBOL_LEN() → uint256
MAX_TRADE_FEE_BPS() → uint256
RAISE_USD() → uint256
TOTAL_SUPPLY() → uint256
TREASURY_USD() → uint256
VIRTUAL_USD_RESERVE() → uint256
WETH() → address
allTokens(uint256) → address
allTokensLength() → uint256
creationFee() → uint256
currentPrice(address) → uint256
curves(address) → uint256, uint256, uint256, uint256, uint256, uint256, uint256, bool, bool, address, address
ethUsdPrice() → uint256
owner() → address
quoteBuy(address, uint256) → uint256
quoteSell(address, uint256) → uint256
router() → address
totalLiveRealEth() → uint256
tradeFeeBps() → uint256
treasury() → address
treasuryEscrow() → uint256
uniFactory() → address
Events (11)
BuyFeeCollectedGraduatedOwnerChangedParamsChangedPriceUpdatedReadyToGraduateSellTokenCreatedTreasuryChangedTreasuryEscrowed
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_router",
"type": "address"
},
{
"internalType": "address",
"name": "_treasury",
"type": "address"
},
{
"internalType": "uint256",
"name": "_ethUsdPrice",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "buyer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newRealEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newPriceWeiPerToken",
"type": "uint256"
}
],
"name": "Buy",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "FeeCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pair",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "lpEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "lpTokens",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "treasuryEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "liquidityBurned",
"type": "uint256"
}
],
"name": "Graduated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnerChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "creationFee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tradeFeeBps",
"type": "uint256"
}
],
"name": "ParamsChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "ethUsdPrice",
"type": "uint256"
}
],
"name": "PriceUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "realEth",
"type": "uint256"
}
],
"name": "ReadyToGraduate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "seller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newRealEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newPriceWeiPerToken",
"type": "uint256"
}
],
"name": "Sell",
"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": "uint256",
"name": "ethUsdSnapshot",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "virtualEth",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "raiseTarget",
"type": "uint256"
}
],
"name": "TokenCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "newTreasury",
"type": "address"
}
],
"name": "TreasuryChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "TreasuryEscrowed",
"type": "event"
},
{
"inputs": [],
"name": "CURVE_SUPPLY",
"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": "LP_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "LP_USD",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_NAME_LEN",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_SYMBOL_LEN",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_TRADE_FEE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "RAISE_USD",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TREASURY_USD",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VIRTUAL_USD_RESERVE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"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"
},
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
}
],
"name": "buy",
"outputs": [
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "claimTreasuryEscrow",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "uint256",
"name": "initialBuyEth",
"type": "uint256"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
}
],
"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": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "currentPrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "curves",
"outputs": [
{
"internalType": "uint256",
"name": "virtualEth",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "realEth",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenReserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "raiseTarget",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "lpEth",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "treasuryEth",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "k",
"type": "uint256"
},
{
"internalType": "bool",
"name": "readyToGraduate",
"type": "bool"
},
{
"internalType": "bool",
"name": "graduated",
"type": "bool"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "ethUsdPrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "graduate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "graduateWithRecovery",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "ethIn",
"type": "uint256"
}
],
"name": "quoteBuy",
"outputs": [
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokensIn",
"type": "uint256"
}
],
"name": "quoteSell",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "refreshTokenPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "router",
"outputs": [
{
"internalType": "contract IUniswapV2Router02",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokensIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minEthOut",
"type": "uint256"
}
],
"name": "sell",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_price",
"type": "uint256"
}
],
"name": "setEthUsdPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"name": "setOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_creationFee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_tradeFeeBps",
"type": "uint256"
}
],
"name": "setParams",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_treasury",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "sweepOrphanedETH",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "totalLiveRealEth",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tradeFeeBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "treasuryEscrow",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "uniFactory",
"outputs": [
{
"internalType": "contract IUniswapV2Factory",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {RobinToken} from "./RobinToken.sol";
import {
IUniswapV2Factory,
IUniswapV2Router02,
IUniswapV2Pair,
IWETH
} from "./interfaces/IUniswapV2.sol";
/// @title RobinFunFactory
/// @notice four.meme-style launchpad for Robinhood Chain (chainId 4663).
/// Each token launches on a constant-product (x*y=k) bonding curve with virtual reserves.
/// Targets are denominated in USD and snapshotted to ETH at creation time using an
/// owner/keeper-set ETH/USD price (Robinhood Chain has no canonical oracle yet).
///
/// Economy (per-token, snapshotted at creation):
/// * total supply ............ 1,000,000,000 (1e9 * 1e18)
/// * curve sale allocation ... 700,000,000 (70%)
/// * LP allocation ........... 300,000,000 (30%)
/// * virtual USD reserve ..... $4,006.153846 -> init mcap $4,000
/// * raise to graduate ....... $9,300 (in ETH at snapshot price)
/// * on graduation ........... $9,000 -> LP, $300 -> treasury
/// * LP tokens ............... 100% burned (sent to 0x...dEaD)
///
/// The curve runs purely in ETH after the snapshot; USD only sets the starting point so
/// creators get consistent dollar targets regardless of ETH price.
///
/// SECURITY HARDENING (post-audit):
/// * Graduation is DECOUPLED from buy: hitting the raise sets `readyToGraduate`; a
/// separate, permissionless `graduate(token)` finalizes. A failing LP step can never
/// brick buys or freeze a curve.
/// * Liquidity is added by transferring tokens + WETH directly to the pair and calling
/// pair.mint() — NOT router.addLiquidityETH. Any reserves an attacker pre-donates to
/// the pair are absorbed into the burned LP (they cannot steal the raise, and the
/// router's quote()-revert DoS path is avoided entirely).
/// * Treasury payouts are best-effort (never revert a user trade if treasury rejects ETH);
/// the cut is escrowed for later pull if the push fails.
/// * Per-curve ETH accounting: graduation spends from the curve's own realEth, and any
/// surplus is returned to that curve's treasury cut. An owner sweep recovers only
/// genuinely orphaned ETH (balance minus the sum of live curves' realEth).
contract RobinFunFactory {
/*//////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////*/
uint256 public constant TOTAL_SUPPLY = 1_000_000_000 ether; // 1e9 * 1e18
uint256 public constant CURVE_SUPPLY = 700_000_000 ether; // 70% sold on the curve
uint256 public constant LP_SUPPLY = 300_000_000 ether; // 30% paired into the LP
// USD targets (8 decimals, like a Chainlink USD feed).
// $4,006.153846 with 8 decimals -> init mcap $4,000.
uint256 public constant VIRTUAL_USD_RESERVE = 400_615_384_600;
uint256 public constant RAISE_USD = 9300 * 1e8; // $9,300 graduation trigger
uint256 public constant LP_USD = 9000 * 1e8; // $9,000 into the LP
uint256 public constant TREASURY_USD = 300 * 1e8; // $300 to the treasury
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
uint256 public constant MAX_TRADE_FEE_BPS = 300; // hard cap 3%
uint256 public constant MAX_NAME_LEN = 32;
uint256 public constant MAX_SYMBOL_LEN = 11;
/*//////////////////////////////////////////////////////////////
IMMUTABLES
//////////////////////////////////////////////////////////////*/
IUniswapV2Router02 public immutable router;
IUniswapV2Factory public immutable uniFactory;
address public immutable WETH;
/*//////////////////////////////////////////////////////////////
STORAGE
//////////////////////////////////////////////////////////////*/
address public owner;
address public treasury;
/// @notice ETH/USD price with 8 decimals (e.g. $3000 => 3000e8). Set by owner/keeper.
uint256 public ethUsdPrice;
/// @notice flat creation fee in ETH (wei). 0 by default.
uint256 public creationFee;
/// @notice trading fee in basis points charged on each buy/sell (e.g. 100 = 1%).
uint256 public tradeFeeBps;
/// @notice sum of all live (non-graduated) curves' realEth — used to scope the owner sweep.
uint256 public totalLiveRealEth;
/// @notice escrowed treasury payouts that failed to push (pull-payment fallback).
uint256 public treasuryEscrow;
struct Curve {
uint256 virtualEth; // virtual ETH reserve (USD reserve converted at snapshot)
uint256 realEth; // real ETH collected so far
uint256 tokenReserve; // curve token reserve; decreases on buy
uint256 raiseTarget; // ETH needed to graduate (RAISE_USD at snapshot)
uint256 lpEth; // ETH put into LP at graduation (LP_USD at snapshot)
uint256 treasuryEth; // ETH to treasury at graduation (TREASURY_USD at snapshot)
uint256 k; // invariant = virtualEth * y0
bool readyToGraduate; // raise reached, awaiting finalize
bool graduated; // LP created + burned
address creator; // who launched the token (identity)
address feeRecipient; // where the creator's half of the trade fee is paid (creator's choice)
}
mapping(address => Curve) public curves;
address[] public allTokens;
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event TokenCreated(
address indexed token,
address indexed creator,
string name,
string symbol,
uint256 ethUsdSnapshot,
uint256 virtualEth,
uint256 raiseTarget
);
event Buy(
address indexed token,
address indexed buyer,
uint256 ethIn,
uint256 tokensOut,
uint256 newRealEth,
uint256 newPriceWeiPerToken
);
event Sell(
address indexed token,
address indexed seller,
uint256 tokensIn,
uint256 ethOut,
uint256 newRealEth,
uint256 newPriceWeiPerToken
);
event ReadyToGraduate(address indexed token, uint256 realEth);
event Graduated(
address indexed token,
address indexed pair,
uint256 lpEth,
uint256 lpTokens,
uint256 treasuryEth,
uint256 liquidityBurned
);
event FeeCollected(address indexed token, uint256 amount);
event TreasuryEscrowed(uint256 amount);
event PriceUpdated(uint256 ethUsdPrice);
event OwnerChanged(address indexed newOwner);
event TreasuryChanged(address indexed newTreasury);
event ParamsChanged(uint256 creationFee, uint256 tradeFeeBps);
/*//////////////////////////////////////////////////////////////
REENTRANCY GUARD
//////////////////////////////////////////////////////////////*/
uint256 private _locked = 1;
modifier nonReentrant() {
require(_locked == 1, "REENTRANCY");
_locked = 2;
_;
_locked = 1;
}
modifier onlyOwner() {
require(msg.sender == owner, "NOT_OWNER");
_;
}
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(address _router, address _treasury, uint256 _ethUsdPrice) {
require(_router != address(0) && _treasury != address(0), "ZERO_ADDR");
require(_ethUsdPrice > 0, "ZERO_PRICE");
owner = msg.sender;
treasury = _treasury;
ethUsdPrice = _ethUsdPrice;
tradeFeeBps = 100; // 1% default
router = IUniswapV2Router02(_router);
WETH = IUniswapV2Router02(_router).WETH();
uniFactory = IUniswapV2Factory(IUniswapV2Router02(_router).factory());
}
/*//////////////////////////////////////////////////////////////
ADMIN
//////////////////////////////////////////////////////////////*/
function setEthUsdPrice(uint256 _price) external onlyOwner {
require(_price > 0, "ZERO_PRICE");
ethUsdPrice = _price;
emit PriceUpdated(_price);
}
/// @notice Push the current ETH/USD price into an already-graduated token so its $5 auto-swap
/// trigger doesn't drift as ETH moves (the token's setter is onlyFactory).
function refreshTokenPrice(address token) external onlyOwner {
RobinToken(payable(token)).setEthUsdPrice(ethUsdPrice);
}
function setOwner(address _owner) external onlyOwner {
require(_owner != address(0), "ZERO_ADDR");
owner = _owner;
emit OwnerChanged(_owner);
}
function setTreasury(address _treasury) external onlyOwner {
require(_treasury != address(0), "ZERO_ADDR");
treasury = _treasury;
emit TreasuryChanged(_treasury);
}
function setParams(uint256 _creationFee, uint256 _tradeFeeBps) external onlyOwner {
require(_tradeFeeBps <= MAX_TRADE_FEE_BPS, "FEE_TOO_HIGH");
creationFee = _creationFee;
tradeFeeBps = _tradeFeeBps;
emit ParamsChanged(_creationFee, _tradeFeeBps);
}
/// @notice Recover ETH that is not backing any live curve (dust, stray sends, escrow already
/// accounted separately). Cannot touch live curves' realEth or treasury escrow.
function sweepOrphanedETH(address to) external onlyOwner nonReentrant {
require(to != address(0), "ZERO_ADDR");
uint256 reserved = totalLiveRealEth + treasuryEscrow;
uint256 bal = address(this).balance;
require(bal > reserved, "NOTHING_TO_SWEEP");
_safeTransferETH(to, bal - reserved);
}
/// @notice Treasury (or owner) pulls escrowed payouts that failed to push.
function claimTreasuryEscrow() external nonReentrant {
require(msg.sender == treasury || msg.sender == owner, "NOT_TREASURY");
uint256 amt = treasuryEscrow;
require(amt > 0, "NOTHING");
treasuryEscrow = 0;
_safeTransferETH(treasury, amt);
}
/*//////////////////////////////////////////////////////////////
VIEW HELPERS
//////////////////////////////////////////////////////////////*/
function allTokensLength() external view returns (uint256) {
return allTokens.length;
}
function _usdToWei(uint256 usd8, uint256 price8) internal pure returns (uint256) {
return (usd8 * 1e18) / price8;
}
function quoteBuy(address token, uint256 ethIn) public view returns (uint256 tokensOut) {
Curve storage c = curves[token];
require(!c.graduated && !c.readyToGraduate && c.creator != address(0), "NOT_TRADABLE");
uint256 fee = (ethIn * tradeFeeBps) / 10_000;
uint256 netEth = ethIn - fee;
tokensOut = _getTokensOut(c, netEth);
}
function quoteSell(address token, uint256 tokensIn) public view returns (uint256 ethOut) {
Curve storage c = curves[token];
require(!c.graduated && !c.readyToGraduate && c.creator != address(0), "NOT_TRADABLE");
uint256 grossEth = _getEthOut(c, tokensIn);
uint256 fee = (grossEth * tradeFeeBps) / 10_000;
ethOut = grossEth - fee;
}
function currentPrice(address token) external view returns (uint256) {
Curve storage c = curves[token];
if (c.creator == address(0)) return 0;
uint256 x = c.virtualEth + c.realEth;
return (x * 1e18) / c.tokenReserve;
}
/*//////////////////////////////////////////////////////////////
CURVE MATH
//////////////////////////////////////////////////////////////*/
function _getTokensOut(Curve storage c, uint256 dEth) internal view returns (uint256) {
uint256 x = c.virtualEth + c.realEth;
uint256 newX = x + dEth;
uint256 newY = c.k / newX;
return c.tokenReserve - newY;
}
function _getEthOut(Curve storage c, uint256 dTok) internal view returns (uint256) {
uint256 y = c.tokenReserve;
uint256 newY = y + dTok;
uint256 x = c.virtualEth + c.realEth;
uint256 newX = c.k / newY;
return x - newX;
}
/*//////////////////////////////////////////////////////////////
CREATE TOKEN
//////////////////////////////////////////////////////////////*/
/// @param feeRecipient where the creator's half of the trade fee is paid (on the curve AND on
/// the post-graduation DEX auto-swap). Pass address(0) to default it to the caller.
function createToken(
string calldata name,
string calldata symbol,
uint256 initialBuyEth,
address feeRecipient
)
external
payable
nonReentrant
returns (address token)
{
uint256 nl = bytes(name).length;
uint256 sl = bytes(symbol).length;
require(nl > 0 && nl <= MAX_NAME_LEN, "BAD_NAME");
require(sl > 0 && sl <= MAX_SYMBOL_LEN, "BAD_SYMBOL");
require(msg.value >= creationFee + initialBuyEth, "INSUFFICIENT_ETH");
uint256 price8 = ethUsdPrice;
// default the fee recipient to the caller when not specified
address recipient = feeRecipient == address(0) ? msg.sender : feeRecipient;
// token carries a 1% fee-on-transfer (0.5% feeRecipient / 0.5% treasury) that applies on the
// DEX after graduation; the factory is fee-exempt so curve trades stay exact.
RobinToken t = new RobinToken(name, symbol, TOTAL_SUPPLY, msg.sender, recipient, treasury);
token = address(t);
uint256 virtualEth = _usdToWei(VIRTUAL_USD_RESERVE, price8);
uint256 raiseTarget = _usdToWei(RAISE_USD, price8);
uint256 treasuryEth = _usdToWei(TREASURY_USD, price8);
// Derive lpEth as the remainder so lpEth + treasuryEth == raiseTarget exactly
// (eliminates the independent-flooring dust the audit flagged).
uint256 lpEth = raiseTarget - treasuryEth;
// y0 = CURVE_SUPPLY * (virtualEth + raiseTarget) / raiseTarget
uint256 y0 = (CURVE_SUPPLY * (virtualEth + raiseTarget)) / raiseTarget;
uint256 k = virtualEth * y0;
curves[token] = Curve({
virtualEth: virtualEth,
realEth: 0,
tokenReserve: y0,
raiseTarget: raiseTarget,
lpEth: lpEth,
treasuryEth: treasuryEth,
k: k,
readyToGraduate: false,
graduated: false,
creator: msg.sender,
feeRecipient: recipient
});
allTokens.push(token);
emit TokenCreated(token, msg.sender, name, symbol, price8, virtualEth, raiseTarget);
if (creationFee > 0) {
_payTreasury(creationFee);
}
if (initialBuyEth > 0) {
_buy(token, msg.sender, initialBuyEth);
}
uint256 spent = creationFee + initialBuyEth;
if (msg.value > spent) {
_safeTransferETH(msg.sender, msg.value - spent);
}
}
/*//////////////////////////////////////////////////////////////
BUY
//////////////////////////////////////////////////////////////*/
function buy(address token, uint256 minTokensOut)
external
payable
nonReentrant
returns (uint256 tokensOut)
{
require(msg.value > 0, "ZERO_ETH");
tokensOut = _buy(token, msg.sender, msg.value);
require(tokensOut >= minTokensOut, "SLIPPAGE");
}
function _buy(address token, address buyer, uint256 ethIn) internal returns (uint256 tokensOut) {
Curve storage c = curves[token];
require(c.creator != address(0), "NO_TOKEN");
require(!c.graduated && !c.readyToGraduate, "NOT_TRADABLE");
// Cap so realEth never overshoots raiseTarget; refund the rest.
uint256 remaining = c.raiseTarget - c.realEth;
uint256 grossForFull =
tradeFeeBps == 0 ? remaining : (remaining * 10_000) / (10_000 - tradeFeeBps);
uint256 effectiveEth = ethIn;
uint256 refund = 0;
if (effectiveEth > grossForFull) {
refund = effectiveEth - grossForFull;
effectiveEth = grossForFull;
}
require(effectiveEth > 0, "CURVE_FULL");
uint256 fee = (effectiveEth * tradeFeeBps) / 10_000;
uint256 netEth = effectiveEth - fee;
tokensOut = _getTokensOut(c, netEth);
require(tokensOut > 0, "ZERO_OUT");
// effects
c.realEth += netEth;
c.tokenReserve -= tokensOut;
totalLiveRealEth += netEth;
// interactions — curve trade fee split 0.5% feeRecipient / 0.5% treasury (in ETH)
if (fee > 0) {
_payTradeFee(c.feeRecipient, fee);
emit FeeCollected(token, fee);
}
require(RobinToken(payable(token)).transfer(buyer, tokensOut), "XFER_FAIL");
uint256 price = ((c.virtualEth + c.realEth) * 1e18) / c.tokenReserve;
emit Buy(token, buyer, effectiveEth, tokensOut, c.realEth, price);
if (refund > 0) {
_safeTransferETH(buyer, refund);
}
// mark ready (does NOT call the DEX — graduation is finalized separately)
if (c.realEth >= c.raiseTarget) {
c.readyToGraduate = true;
emit ReadyToGraduate(token, c.realEth);
}
}
/*//////////////////////////////////////////////////////////////
SELL
//////////////////////////////////////////////////////////////*/
function sell(address token, uint256 tokensIn, uint256 minEthOut)
external
nonReentrant
returns (uint256 ethOut)
{
Curve storage c = curves[token];
require(c.creator != address(0), "NO_TOKEN");
require(!c.graduated && !c.readyToGraduate, "NOT_TRADABLE");
require(tokensIn > 0, "ZERO_TOKENS");
require(RobinToken(payable(token)).transferFrom(msg.sender, address(this), tokensIn), "XFER_FAIL");
uint256 grossEth = _getEthOut(c, tokensIn);
require(grossEth <= c.realEth, "INSUFFICIENT_LIQUIDITY");
uint256 fee = (grossEth * tradeFeeBps) / 10_000;
ethOut = grossEth - fee;
require(ethOut >= minEthOut, "SLIPPAGE");
// effects
c.realEth -= grossEth;
c.tokenReserve += tokensIn;
totalLiveRealEth -= grossEth;
// interactions — curve trade fee split 0.5% feeRecipient / 0.5% treasury (in ETH)
if (fee > 0) {
_payTradeFee(c.feeRecipient, fee);
emit FeeCollected(token, fee);
}
_safeTransferETH(msg.sender, ethOut);
uint256 price = ((c.virtualEth + c.realEth) * 1e18) / c.tokenReserve;
emit Sell(token, msg.sender, tokensIn, ethOut, c.realEth, price);
}
/*//////////////////////////////////////////////////////////////
GRADUATION
//////////////////////////////////////////////////////////////*/
/// @notice Finalize a token that has reached its raise. Permissionless — anyone (the buyer,
/// a keeper, the creator) can call it. Decoupled from buy so a DEX hiccup can never
/// brick the curve or block trading.
function graduate(address token) external nonReentrant {
Curve storage c = curves[token];
require(c.readyToGraduate && !c.graduated, "NOT_READY");
uint256 lpEth = c.lpEth;
uint256 treasuryEth = c.treasuryEth;
uint256 curveEth = c.realEth; // this curve's own collected ETH
require(lpEth + treasuryEth <= curveEth, "ACCOUNTING");
address pair = uniFactory.getPair(token, WETH);
if (pair == address(0)) {
pair = uniFactory.createPair(token, WETH);
}
// SECURITY: the normal path only mints into a VIRGIN pair (no third-party LP yet).
// * totalSupply()==0 => no one has minted. A pure donation (reserves!=0 but ts==0) is
// absorbed into the LP we mint and then burn — attacker just gifts the burned pool.
// * totalSupply()>0 => someone PRE-MINTED a (possibly skewed) position. Minting now
// would under-credit us and let their LP capture part of the raise. We REFUSE here and
// leave finalization to graduateWithRecovery (owner). Because graduation is decoupled
// from buy(), a hostile pre-mint cannot brick trading — it only forces the recovery path.
require(IUniswapV2Pair(pair).totalSupply() == 0, "PAIR_POLLUTED");
// commit state (effects before interactions)
c.graduated = true;
c.readyToGraduate = false;
totalLiveRealEth -= curveEth;
// add liquidity by transferring tokens + WETH straight to the pair, then mint to ourselves.
require(RobinToken(payable(token)).transfer(pair, LP_SUPPLY), "LP_TOKEN_XFER");
IWETH(WETH).deposit{value: lpEth}();
require(IWETH(WETH).transfer(pair, lpEth), "LP_WETH_XFER");
uint256 liquidity = IUniswapV2Pair(pair).mint(address(this));
require(liquidity > 0, "NO_LIQUIDITY");
// BURN 100% of the LP we just minted.
require(IUniswapV2Pair(pair).transfer(DEAD, liquidity), "LP_BURN_FAIL");
// Wire the token's post-DEX auto-swap: now the token charges 1% on swaps, accumulates,
// and auto-sells to ETH (50/50 creator/treasury) once worth >= $5. The pair must NOT be
// fee-exempt (so swaps are taxed); the factory + token stay exempt.
RobinToken(payable(token)).configureDex(address(router), pair, WETH, ethUsdPrice);
// treasury cut: this curve's treasuryEth plus any per-curve ETH surplus (normally 0).
uint256 treasuryPayout = treasuryEth + (curveEth - lpEth - treasuryEth);
if (treasuryPayout > 0) {
_payTreasury(treasuryPayout);
}
// burn any unsold curve tokens so they can't dilute the market
uint256 leftoverTokens = RobinToken(payable(token)).balanceOf(address(this));
if (leftoverTokens > 0) {
RobinToken(payable(token)).transfer(DEAD, leftoverTokens);
}
emit Graduated(token, pair, lpEth, LP_SUPPLY, treasuryPayout, liquidity);
}
/// @notice Recovery finalization for the rare case where an attacker pre-minted a polluted
/// pair to block the normal path. Owner-only. Instead of donating the raise into a
/// poisoned pool (where a third-party LP would capture it), the full raise is returned
/// to the treasury, which can refund buyers or relaunch. The attacker's pre-mint is
/// left stranded — they only wasted their own funds. The curve token allocation is
/// burned so it can't be dumped. This path is NOT permissionless to avoid griefers
/// forcing tokens down it; it exists only as an escape hatch.
function graduateWithRecovery(address token) external onlyOwner nonReentrant {
Curve storage c = curves[token];
require(c.readyToGraduate && !c.graduated, "NOT_READY");
address pair = uniFactory.getPair(token, WETH);
// only usable when the normal path is actually blocked by a polluted pair
require(pair != address(0) && IUniswapV2Pair(pair).totalSupply() > 0, "USE_NORMAL");
uint256 curveEth = c.realEth;
c.graduated = true;
c.readyToGraduate = false;
totalLiveRealEth -= curveEth;
// return the entire raise to the treasury (no value handed to the attacker's pool)
if (curveEth > 0) {
_payTreasury(curveEth);
}
// burn the LP token allocation + any unsold curve tokens
uint256 bal = RobinToken(payable(token)).balanceOf(address(this));
if (bal > 0) {
RobinToken(payable(token)).transfer(DEAD, bal);
}
emit Graduated(token, pair, 0, 0, curveEth, 0);
}
/*//////////////////////////////////////////////////////////////
UTILS
//////////////////////////////////////////////////////////////*/
/// @dev Best-effort treasury push. If the treasury rejects ETH, escrow it for a later pull
/// instead of reverting the user's trade (prevents a single bad treasury from DoS'ing
/// the whole launchpad).
function _payTreasury(uint256 amount) internal {
if (amount == 0) return;
(bool ok,) = treasury.call{value: amount}("");
if (!ok) {
treasuryEscrow += amount;
emit TreasuryEscrowed(amount);
}
}
/// @dev Split the curve trade fee (in ETH) 50/50: half to the creator's chosen feeRecipient,
/// half to the treasury (i.e. 0.5%/0.5% at the default 1% tradeFeeBps). Best-effort to the
/// recipient (a reverting recipient just forfeits to treasury), and best-effort to the
/// treasury (escrow fallback) — never reverts the trade.
function _payTradeFee(address feeRecipient, uint256 fee) internal {
if (fee == 0) return;
uint256 toCreator = fee / 2;
uint256 toTreasury = fee - toCreator; // remainder -> treasury
if (toCreator > 0) {
(bool ok,) = feeRecipient.call{value: toCreator}("");
if (!ok) {
// recipient can't receive -> add their share to the treasury cut
toTreasury += toCreator;
}
}
_payTreasury(toTreasury);
}
/// @dev Strict ETH transfer for user-facing payouts/refunds — must succeed.
function _safeTransferETH(address to, uint256 amount) internal {
(bool ok,) = to.call{value: amount}("");
require(ok, "ETH_XFER_FAIL");
}
receive() external payable {}
}
Chain explorer5994msChain node82ms