StoxesFunFactory

0x3a5a312b66a3f3aabb647e75e8e81ccc08b7bb58

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
5,722 bytes
ABI entries
36
19 read · 4 write
License
none

Contract information

Address
0x3a5a312b66a3f3aabb647e75e8e81ccc08b7bb58
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x7703556e26…883208df53

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (19)

MAX_STOCKS()uint256
MAX_TAX_BPS()uint16
MIN_TAX_BPS()uint16
VANITY_SUFFIX()uint16
WETH()address
allTokens(uint256)address
allTokensLength()uint256
computeTokenAddress(address, bytes32)address
creatorOf(address)address
earnings()address
keeper()address
owner()address
portal()address
stockRegistry(address)bool
tokenImplementation()address
tokenInitCodeHash()bytes32
trackerImplementation()address
trackerOf(address)address
uniswapV2Factory()address

Events (4)

KeeperChangedOwnershipTransferredStockRegistrySetTokenLaunched

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenImplementation_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "trackerImplementation_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "portal_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "earnings_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "uniswapV2Factory_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "keeper_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "admin_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "CloneCreationFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidParams",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Reentrancy",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "StockNotRegistered",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TaxOutOfBounds",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "VanitySuffixMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldKeeper",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newKeeper",
        "type": "address"
      }
    ],
    "name": "KeeperChanged",
    "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": "stock",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "StockRegistrySet",
    "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": "stocks",
        "type": "address[]"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "tracker",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "pair",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "buyTaxBps",
        "type": "uint16"
      },
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "sellTaxBps",
        "type": "uint16"
      }
    ],
    "name": "TokenLaunched",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "MAX_STOCKS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_TAX_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_TAX_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "VANITY_SUFFIX",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "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": "deployer",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "computeTokenAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "creatorOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "earnings",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "keeper",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "metaURI",
            "type": "string"
          },
          {
            "internalType": "uint16",
            "name": "buyTaxBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "sellTaxBps",
            "type": "uint16"
          },
          {
            "internalType": "address[]",
            "name": "stocks",
            "type": "address[]"
          },
          {
            "internalType": "address",
            "name": "creatorWallet",
            "type": "address"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          }
        ],
        "internalType": "struct StoxesFunFactory.LaunchParams",
        "name": "params",
        "type": "tuple"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tracker",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pair",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "portal",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newKeeper",
        "type": "address"
      }
    ],
    "name": "setKeeper",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "stock",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "setStockRegistered",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "stockRegistry",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenImplementation",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenInitCodeHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "trackerImplementation",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "trackerOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "uniswapV2Factory",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {OwnableLite, ReentrancyGuardLite} from "./base/Common.sol";
import {Clones} from "./vendor/Clones.sol";
import {
    IStoxesFunTaxToken,
    IStoxesFunDividendTracker,
    IStoxesFunEarnings
} from "./interfaces/IStoxesFun.sol";
import {IUniswapV2Factory} from "./interfaces/IUniswapV2Factory.sol";

interface IERC20Transfer {
    function transfer(address to, uint256 value) external returns (bool);
}

interface IPortalRegister {
    function registerToken(address token, address pair) external;
}

/// @title  StoxesFunFactory — the stoxes.fun launch entry point on Robinhood Chain
/// @author stoxes.fun
/// @notice Self-serve launcher: anyone deploys a stoxes.fun tax token with a chosen
///         name, ticker, socials URI, buy/sell tax (1%–10% per side, immutable) and a
///         dividend stock chosen from the curated registry of official Robinhood
///         Chain stock tokens. Uses the same clone-factory pattern as flap.sh's
///         verified Portal (one audited implementation, EIP-1167 clones per launch).
///
///         Launch wiring (single transaction):
///           1. clone + initialize the dividend tracker (stock + keeper + admin);
///           2. pre-create the Uniswap V2 pair (token/WETH) so the token's pool
///              registry is complete from block one;
///           3. clone + initialize the tax token (full 1B supply minted here);
///           4. hand the supply + token ownership to the Portal and register the
///              curve;
///           5. lock the creator's earnings wallet in the earnings splitter
///              (immutable — the 0.25pt/side creator cut always pays there).
contract StoxesFunFactory is OwnableLite, ReentrancyGuardLite {
    /// @notice Platform tax bounds: 1%–10% per side, immutable platform-wide.
    uint16 public constant MIN_TAX_BPS = 100;
    uint16 public constant MAX_TAX_BPS = 1000;

    /// @notice Every launched token address must end in this suffix ("SAFE") —
    ///         same vanity contract as the non-taxed V3 launch mode. The token
    ///         clone is CREATE2-deployed, so the address is mined off-chain
    ///         against the constant EIP-1167 init-code hash and ENFORCED here.
    uint16 public constant VANITY_SUFFIX = 0x5afe;

    /// @notice Hard bound on dividend stocks per launch (mirrors the tracker's bound).
    uint256 public constant MAX_STOCKS = 10;

    /// @notice The audited implementations cloned per launch.
    address public immutable tokenImplementation;
    address public immutable trackerImplementation;

    /// @notice Core wiring (immutable at deploy).
    address public immutable portal;
    address public immutable earnings;
    address public immutable uniswapV2Factory;
    address public immutable WETH;

    /// @notice The stoxes.fun keeper wallet baked into each launch.
    address public keeper;

    /// @notice Curated registry of official Robinhood Chain stock tokens usable as
    ///         dividend assets.
    mapping(address => bool) public stockRegistry;

    /// @notice All launched tokens, in launch order.
    address[] public allTokens;

    /// @notice token => tracker / creator lookups.
    mapping(address => address) public trackerOf;
    mapping(address => address) public creatorOf;

    struct LaunchParams {
        string name;
        string symbol;
        string metaURI; // stoxes.fun-hosted metadata JSON (socials, logo)
        uint16 buyTaxBps; // 100..1000, immutable
        uint16 sellTaxBps; // 100..1000, immutable
        address[] stocks; // dividend stocks from the registry (1..MAX_STOCKS)
        address creatorWallet; // immutable earnings wallet
        bytes32 salt; // browser-mined so the token address ends in 0x5afe
    }

    event TokenLaunched(
        address indexed token,
        address indexed creator,
        address[] stocks,
        address tracker,
        address pair,
        uint16 buyTaxBps,
        uint16 sellTaxBps
    );
    event StockRegistrySet(address indexed stock, bool allowed);
    event KeeperChanged(address indexed oldKeeper, address indexed newKeeper);

    error TaxOutOfBounds();
    error StockNotRegistered();
    error InvalidParams();
    error VanitySuffixMismatch(address token);

    constructor(
        address tokenImplementation_,
        address trackerImplementation_,
        address portal_,
        address earnings_,
        address uniswapV2Factory_,
        address weth_,
        address keeper_,
        address admin_
    ) {
        if (
            tokenImplementation_ == address(0) || trackerImplementation_ == address(0)
                || portal_ == address(0) || earnings_ == address(0) || uniswapV2Factory_ == address(0)
                || weth_ == address(0) || keeper_ == address(0)
        ) revert InvalidParams();
        tokenImplementation = tokenImplementation_;
        trackerImplementation = trackerImplementation_;
        portal = portal_;
        earnings = earnings_;
        uniswapV2Factory = uniswapV2Factory_;
        WETH = weth_;
        keeper = keeper_;
        _initOwner(admin_);
    }

    /// @notice Launches a new stoxes.fun tax token. Fully self-serve.
    function launch(LaunchParams calldata params)
        external
        nonReentrant
        returns (address token, address tracker, address pair)
    {
        if (
            params.buyTaxBps < MIN_TAX_BPS || params.buyTaxBps > MAX_TAX_BPS
                || params.sellTaxBps < MIN_TAX_BPS || params.sellTaxBps > MAX_TAX_BPS
        ) revert TaxOutOfBounds();
        uint256 stockCount = params.stocks.length;
        if (stockCount == 0 || stockCount > MAX_STOCKS) revert InvalidParams();
        for (uint256 i = 0; i < stockCount; i++) {
            if (!stockRegistry[params.stocks[i]]) revert StockNotRegistered();
            for (uint256 j = 0; j < i; j++) {
                if (params.stocks[j] == params.stocks[i]) revert InvalidParams();
            }
        }
        if (params.creatorWallet == address(0)) revert InvalidParams();
        if (bytes(params.name).length == 0 || bytes(params.symbol).length == 0) {
            revert InvalidParams();
        }

        // 1. Dividend tracker clone (admin = factory owner for keeper rotation only).
        tracker = Clones.clone(trackerImplementation);

        // 2. Token clone address is needed for the pair; clone first, init after.
        //    CREATE2 with the browser-mined salt, bound to the caller so a
        //    front-runner cannot steal a mined address. The vanity suffix is
        //    ENFORCED — a launch whose address does not end in 0x5afe reverts.
        token = Clones.cloneDeterministic(
            tokenImplementation, keccak256(abi.encode(msg.sender, params.salt))
        );
        if (uint16(uint160(token)) != VANITY_SUFFIX) revert VanitySuffixMismatch(token);

        IStoxesFunDividendTracker(tracker).initialize(token, params.stocks, keeper, owner());

        // 3. Pre-create the V2 pair so it is in the pool registry from block one.
        //    If a griefer pre-created the pair at the predicted CREATE2 address,
        //    reuse it instead of letting `createPair` revert and brick the launch.
        pair = IUniswapV2Factory(uniswapV2Factory).getPair(token, WETH);
        if (pair == address(0)) {
            pair = IUniswapV2Factory(uniswapV2Factory).createPair(token, WETH);
        }

        address[] memory pools = new address[](1);
        pools[0] = pair;
        IStoxesFunTaxToken(token).initialize(
            IStoxesFunTaxToken.InitParams({
                name: params.name,
                symbol: params.symbol,
                metaURI: params.metaURI,
                buyTaxBps: params.buyTaxBps,
                sellTaxBps: params.sellTaxBps,
                taxProcessor: keeper,
                dividendTracker: tracker,
                portal: portal,
                pools: pools
            })
        );

        // 4. The token has NO owner (owner() == 0 from birth — renounced for
        //    scanners). The Portal is its lifecycle authority via initialize,
        //    and receives the curve inventory here.
        IERC20Transfer(token).transfer(portal, 1e9 ether);
        IPortalRegister(portal).registerToken(token, pair);

        // 5. Lock the creator's earnings wallet forever.
        IStoxesFunEarnings(earnings).registerToken(token, params.creatorWallet);

        allTokens.push(token);
        trackerOf[token] = tracker;
        creatorOf[token] = msg.sender;

        emit TokenLaunched(
            token, msg.sender, params.stocks, tracker, pair, params.buyTaxBps, params.sellTaxBps
        );
    }

    // ------------------------------------------------------------------
    // Admin
    // ------------------------------------------------------------------

    /// @notice Curates the official Robinhood stock-token registry.
    function setStockRegistered(address stock, bool allowed) external onlyOwner {
        if (stock == address(0)) revert InvalidParams();
        stockRegistry[stock] = allowed;
        emit StockRegistrySet(stock, allowed);
    }

    /// @notice Rotates the keeper used for FUTURE launches (existing tokens rotate
    ///         via the Portal / tracker admin paths).
    function setKeeper(address newKeeper) external onlyOwner {
        if (newKeeper == address(0)) revert InvalidParams();
        emit KeeperChanged(keeper, newKeeper);
        keeper = newKeeper;
    }

    // ------------------------------------------------------------------
    // Views
    // ------------------------------------------------------------------

    function allTokensLength() external view returns (uint256) {
        return allTokens.length;
    }

    /// @notice The CREATE2 clone address `salt` would produce for `deployer` —
    ///         the browser miner's ground truth (staticCall to sanity-check a
    ///         salt before sending the launch tx).
    function computeTokenAddress(address deployer, bytes32 salt) external view returns (address) {
        return Clones.predictDeterministicAddress(
            tokenImplementation, keccak256(abi.encode(deployer, salt)), address(this)
        );
    }

    /// @notice The constant EIP-1167 init-code hash the in-browser salt miner
    ///         needs (a pure function of the token implementation address).
    function tokenInitCodeHash() external view returns (bytes32) {
        return Clones.initCodeHash(tokenImplementation);
    }
}
Chain explorer2114msChain node72ms