StoxesFunPortal
0xa0e82b5bf840e268718a143032cd630a3bf1850e
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
7,310 bytes
ABI entries
39
13 read · 7 write
License
none
Contract information
- Address
- 0xa0e82b5bf840e268718a143032cd630a3bf1850e
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x9Cf1e0bff5…CcA354c20B
- Creation tx
- 0x533055c3f0…34937a09f6
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (13)
CURVE_FEE_BPS() → uint256
WETH() → address
curveH() → uint256
curveK() → uint256
curveR() → uint256
factory() → address
graduationReserve() → uint256
owner() → address
priceOf(address) → uint256
quoteBuy(address, uint256) → uint256
quoteSell(address, uint256) → uint256
tokenState(address) → uint8, uint128, uint128, address
treasury() → address
Events (6)
BoughtFactorySetGraduatedOwnershipTransferredSoldTokenRegistered
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "address",
"name": "admin_",
"type": "address"
},
{
"internalType": "uint256",
"name": "curveR_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "curveH_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "curveK_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "graduationReserve_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "EthTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "InvalidParams",
"type": "error"
},
{
"inputs": [],
"name": "NotBonding",
"type": "error"
},
{
"inputs": [],
"name": "NotFactory",
"type": "error"
},
{
"inputs": [],
"name": "NotOwner",
"type": "error"
},
{
"inputs": [],
"name": "Reentrancy",
"type": "error"
},
{
"inputs": [],
"name": "SlippageExceeded",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newSupply",
"type": "uint256"
}
],
"name": "SupplyExceedsTotalSupply",
"type": "error"
},
{
"inputs": [],
"name": "UnexpectedEth",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroTrade",
"type": "error"
},
{
"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": "fee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
}
],
"name": "Bought",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "factory",
"type": "address"
}
],
"name": "FactorySet",
"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": "ethLiquidity",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenLiquidity",
"type": "uint256"
}
],
"name": "Graduated",
"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": "seller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "fee",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"name": "Sold",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pair",
"type": "address"
}
],
"name": "TokenRegistered",
"type": "event"
},
{
"inputs": [],
"name": "CURVE_FEE_BPS",
"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": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"name": "addTokenPool",
"outputs": [],
"stateMutability": "nonpayable",
"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": "curveH",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "curveK",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "curveR",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduationReserve",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "priceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
}
],
"name": "quoteBuy",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"name": "quoteSell",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pair",
"type": "address"
}
],
"name": "registerToken",
"outputs": [],
"stateMutability": "nonpayable",
"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": "address",
"name": "factory_",
"type": "address"
}
],
"name": "setFactory",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newProcessor",
"type": "address"
}
],
"name": "setTokenTaxProcessor",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenState",
"outputs": [
{
"internalType": "enum StoxesFunPortal.TokenStatus",
"name": "status",
"type": "uint8"
},
{
"internalType": "uint128",
"name": "reserve",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "circulatingSupply",
"type": "uint128"
},
{
"internalType": "address",
"name": "pair",
"type": "address"
}
],
"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"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {OwnableLite, ReentrancyGuardLite} from "./base/Common.sol";
import {LibCurve} from "./libraries/LibCurve.sol";
import {IStoxesFunTaxToken} from "./interfaces/IStoxesFun.sol";
import {IUniswapV2Pair} from "./interfaces/IUniswapV2Pair.sol";
import {IWETH} from "./interfaces/IWETH.sol";
interface IERC20Minimal {
function transfer(address to, uint256 value) external returns (bool);
function balanceOf(address) external view returns (uint256);
}
/// @title StoxesFunPortal — the stoxes.fun bonding-curve trading venue
/// @author stoxes.fun
/// @notice Constant-product virtual-reserve bonding curve for freshly launched
/// stoxes.fun tax tokens, forked from flap.sh's Blockscout-verified Portal
/// (0xfB60F3eA527763fd40Cb8345028CC3eFF1ceE857, MIT):
/// - Curve math is flap's verified `LibCurve` VERBATIM:
/// (1e9 ether + h - s) * (r + eth) = k, supply rounded down on buys,
/// reserve rounded up on sells — both roundings favor the protocol.
/// - Curve fee: 1% on buys and 1% on sells (flap's exact fee schedule),
/// pushed straight to the stoxes.fun treasury.
/// - Graduation: when the real ETH reserve reaches the target, the token
/// migrates to Uniswap V2 — remaining curve tokens + the FULL reserve
/// are seeded as liquidity and 100% of the LP is minted to 0xdead
/// (burned forever; nobody can ever pull the liquidity).
///
/// stoxes.fun deltas vs flap: single quote asset (native ETH via WETH), a
/// single V2 migration path (no V3/V4/multi-dex router surface), and the
/// pair is pre-created at launch so the token's pool registry is complete
/// from block one.
contract StoxesFunPortal is OwnableLite, ReentrancyGuardLite {
using LibCurve for LibCurve.Curve;
enum TokenStatus {
Unknown,
Bonding,
Graduated
}
struct TokenState {
TokenStatus status;
uint128 reserve; // real ETH collected by the curve (fee excluded)
uint128 circulatingSupply; // tokens sold off the curve
address pair; // the pre-created Uniswap V2 pair
}
/// @notice Curve fee on each side, in basis points (flap-verified schedule: 1%/1%).
uint256 public constant CURVE_FEE_BPS = 100;
/// @notice WETH on Robinhood Chain.
address public immutable WETH;
/// @notice The stoxes.fun treasury receiving curve fees.
address public immutable treasury;
/// @notice Virtual curve parameters shared by every launch (set at deploy).
uint256 public immutable curveR; // virtual ETH reserve
uint256 public immutable curveH; // virtual token reserve offset
uint256 public immutable curveK; // constant product
/// @notice Real ETH reserve target that triggers graduation.
uint256 public immutable graduationReserve;
/// @notice The factory allowed to register launches.
address public factory;
mapping(address => TokenState) public tokenState;
event TokenRegistered(address indexed token, address indexed pair);
event Bought(
address indexed token, address indexed buyer, uint256 ethIn, uint256 fee, uint256 tokensOut
);
event Sold(
address indexed token, address indexed seller, uint256 tokensIn, uint256 fee, uint256 ethOut
);
event Graduated(
address indexed token, address indexed pair, uint256 ethLiquidity, uint256 tokenLiquidity
);
event FactorySet(address indexed factory);
error NotFactory();
error NotBonding();
error InvalidParams();
error ZeroTrade();
error SlippageExceeded();
error EthTransferFailed();
error UnexpectedEth();
constructor(
address weth_,
address treasury_,
address admin_,
uint256 curveR_,
uint256 curveH_,
uint256 curveK_,
uint256 graduationReserve_
) {
if (weth_ == address(0) || treasury_ == address(0)) revert InvalidParams();
if (curveR_ == 0 || curveK_ == 0 || graduationReserve_ == 0) revert InvalidParams();
WETH = weth_;
treasury = treasury_;
curveR = curveR_;
curveH = curveH_;
curveK = curveK_;
graduationReserve = graduationReserve_;
_initOwner(admin_);
}
/// @dev The Portal never expects plain inbound ETH: buys arrive via the
/// payable `buy`, and WETH flows are contract calls. Reject strays so
/// user ETH sent by mistake cannot be silently absorbed into the curve
/// reserve accounting.
receive() external payable {
revert UnexpectedEth();
}
/// @notice Wires/rotates the factory pointer (admin-gated, repeatable —
/// used for factory rotations; affects FUTURE launches only).
function setFactory(address factory_) external onlyOwner {
if (factory_ == address(0)) revert InvalidParams();
factory = factory_;
emit FactorySet(factory_);
}
/// @notice Registers a fresh launch. The factory has already cloned + initialized
/// the token and transferred the full 1B supply to this Portal.
function registerToken(address token, address pair) external {
if (msg.sender != factory) revert NotFactory();
if (tokenState[token].status != TokenStatus.Unknown) revert InvalidParams();
tokenState[token] =
TokenState({status: TokenStatus.Bonding, reserve: 0, circulatingSupply: 0, pair: pair});
emit TokenRegistered(token, pair);
}
/// @notice Admin passthrough: rotate a token's keeper (tax processor). The Portal
/// owns each token, so owner-gated token calls surface here, admin-gated.
function setTokenTaxProcessor(address token, address newProcessor) external onlyOwner {
IStoxesFunTaxToken(token).setTaxProcessor(newProcessor);
}
/// @notice Admin passthrough: register an additional pool in a token's taxed-pool
/// registry (AntiFarmer). Without this, secondary pools created after
/// graduation would be a practical tax bypass — the Portal owns each
/// token, so the owner-gated `addPool` must surface here.
function addTokenPool(address token, address pool) external onlyOwner {
IStoxesFunTaxToken(token).addPool(pool);
}
// ------------------------------------------------------------------
// Curve trading
// ------------------------------------------------------------------
function _curve() internal view returns (LibCurve.Curve memory) {
return LibCurve.fromRHK(curveR, curveH, curveK);
}
/// @notice Buys `token` off the curve with the attached ETH. 1% fee to treasury;
/// any ETH beyond the graduation target is refunded before graduating.
/// @param minTokensOut Slippage floor for the tokens received.
function buy(address token, uint256 minTokensOut)
external
payable
nonReentrant
returns (uint256 tokensOut)
{
TokenState memory ts = tokenState[token];
if (ts.status != TokenStatus.Bonding) revert NotBonding();
if (msg.value == 0) revert ZeroTrade();
uint256 fee = (msg.value * CURVE_FEE_BPS) / 10_000;
uint256 ethIn = msg.value - fee;
uint256 refund = 0;
// Clamp at the graduation target; refund the overshoot (fee is charged on
// the accepted amount only).
uint256 room = graduationReserve - ts.reserve;
if (ethIn > room) {
refund = ethIn - room;
ethIn = room;
fee = (ethIn * CURVE_FEE_BPS) / (10_000 - CURVE_FEE_BPS); // fee on accepted principal
refund = msg.value - ethIn - fee;
}
LibCurve.Curve memory curve = _curve();
uint256 newSupply = curve.estimateSupply(ts.reserve + ethIn);
tokensOut = newSupply - ts.circulatingSupply;
if (tokensOut == 0) revert ZeroTrade();
if (tokensOut < minTokensOut) revert SlippageExceeded();
ts.reserve += uint128(ethIn);
ts.circulatingSupply += uint128(tokensOut);
tokenState[token] = ts;
// Token leaves the Portal's curve inventory toward the buyer.
IERC20Minimal(token).transfer(msg.sender, tokensOut);
_sendEth(treasury, fee);
if (refund > 0) _sendEth(msg.sender, refund);
emit Bought(token, msg.sender, ethIn, fee, tokensOut);
if (ts.reserve >= graduationReserve) {
_graduate(token, ts);
}
}
/// @notice Sells `tokensIn` of `token` back to the curve for ETH (1% fee on the
/// proceeds). Requires prior approval of the Portal on the token.
/// @param minEthOut Slippage floor for the net ETH received.
function sell(address token, uint256 tokensIn, uint256 minEthOut)
external
nonReentrant
returns (uint256 ethOut)
{
TokenState memory ts = tokenState[token];
if (ts.status != TokenStatus.Bonding) revert NotBonding();
if (tokensIn == 0) revert ZeroTrade();
LibCurve.Curve memory curve = _curve();
uint256 newCirculating = ts.circulatingSupply - tokensIn;
uint256 newReserve = curve.estimateReserve(newCirculating);
uint256 grossOut = ts.reserve - newReserve;
if (grossOut == 0) revert ZeroTrade();
uint256 fee = (grossOut * CURVE_FEE_BPS) / 10_000;
ethOut = grossOut - fee;
if (ethOut < minEthOut) revert SlippageExceeded();
ts.reserve = uint128(newReserve);
ts.circulatingSupply = uint128(newCirculating);
tokenState[token] = ts;
// Pull the tokens back into curve inventory (BondingCurve state allows
// Portal<->wallet transfers; pools are blocked, the Portal is not a pool).
(bool ok, bytes memory data) = token.call(
abi.encodeWithSelector(0x23b872dd, msg.sender, address(this), tokensIn)
);
if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert ZeroTrade();
_sendEth(treasury, fee);
_sendEth(msg.sender, ethOut);
emit Sold(token, msg.sender, tokensIn, fee, ethOut);
}
// ------------------------------------------------------------------
// Graduation
// ------------------------------------------------------------------
/// @dev Seeds the pre-created V2 pair with the ENTIRE remaining curve inventory
/// and the FULL real ETH reserve, then mints 100% of the LP to 0xdead.
function _graduate(address token, TokenState memory ts) internal {
IStoxesFunTaxToken t = IStoxesFunTaxToken(token);
t.startMigration();
uint256 tokenLiquidity = IERC20Minimal(token).balanceOf(address(this));
uint256 ethLiquidity = ts.reserve;
// Wrap the reserve and seed the pair directly (first mint on a fresh pair).
IWETH(WETH).deposit{value: ethLiquidity}();
IWETH(WETH).transfer(ts.pair, ethLiquidity);
IERC20Minimal(token).transfer(ts.pair, tokenLiquidity);
IUniswapV2Pair(ts.pair).mint(address(0xdead)); // 100% LP burned forever
t.finalizeMigration();
ts.status = TokenStatus.Graduated;
ts.reserve = 0;
tokenState[token] = ts;
emit Graduated(token, ts.pair, ethLiquidity, tokenLiquidity);
}
// ------------------------------------------------------------------
// Views
// ------------------------------------------------------------------
/// @notice Spot price (wei per 1e18 tokens) on the curve.
function priceOf(address token) external view returns (uint256) {
TokenState memory ts = tokenState[token];
return _curve().price(ts.circulatingSupply);
}
/// @notice Quote for a buy of `ethAmount` (gross, fee included), pre-clamp.
function quoteBuy(address token, uint256 ethAmount) external view returns (uint256) {
TokenState memory ts = tokenState[token];
uint256 fee = (ethAmount * CURVE_FEE_BPS) / 10_000;
uint256 newSupply = _curve().estimateSupply(ts.reserve + (ethAmount - fee));
return newSupply - ts.circulatingSupply;
}
/// @notice Quote for selling `tokenAmount`, net of the curve fee.
function quoteSell(address token, uint256 tokenAmount) external view returns (uint256) {
TokenState memory ts = tokenState[token];
uint256 newReserve = _curve().estimateReserve(ts.circulatingSupply - tokenAmount);
uint256 grossOut = ts.reserve - newReserve;
return grossOut - (grossOut * CURVE_FEE_BPS) / 10_000;
}
function _sendEth(address to, uint256 amount) internal {
if (amount == 0) return;
(bool ok,) = to.call{value: amount}("");
if (!ok) revert EthTransferFailed();
}
}
Chain explorer2852msChain node81ms