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
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