EquityTokenFactory

0x526cfc648359b03a4a6475c3713c4bc6bf8279dc

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
6,891 bytes
ABI entries
29
16 read · 2 write
License
none

Contract information

Address
0x526cfc648359b03a4a6475c3713c4bc6bf8279dc
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x1ce378b9ad…55531fa398

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (16)

CURVE_BUCKETS()uint8
MAX_VESTING_BPS()uint16
TAIL_BUCKETS()uint8
TICK_SPACING()int24
deployer()address
feeRouter()address
hook()address
launchCount()uint256
locker()address
poolManager()address
predictToken(address, string, uint256)address
quotes(address)bool, uint128, int24, int24, int24
registry()address
tokenImplementation()address
tokenQuote(address)address
vestingVault()address

Events (2)

QuoteAddedTokenCreated

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "contract EquityLaunchHook",
        "name": "hook_",
        "type": "address"
      },
      {
        "internalType": "contract PermanentLocker",
        "name": "locker_",
        "type": "address"
      },
      {
        "internalType": "contract FeeRouter",
        "name": "feeRouter_",
        "type": "address"
      },
      {
        "internalType": "contract Registry",
        "name": "registry_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "tokenImplementation_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "vestingVault_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadQuoteConfig",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedDeployment",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotDeployer",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "QuoteAlreadyListed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "QuoteNotAllowed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "QuotePullFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SymbolRequired",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "graduationQuote",
        "type": "uint128"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "startTick",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "gradTick",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "endTick",
        "type": "int24"
      }
    ],
    "name": "QuoteAdded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "socialClaimId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "launchBlock",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      }
    ],
    "name": "TokenCreated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "CURVE_BUCKETS",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_VESTING_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TAIL_BUCKETS",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "uint128",
        "name": "graduationQuote_",
        "type": "uint128"
      },
      {
        "internalType": "int24",
        "name": "startTick",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "gradTick",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "endTick",
        "type": "int24"
      }
    ],
    "name": "addQuote",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "metadataURI",
            "type": "string"
          },
          {
            "internalType": "bytes32",
            "name": "socialClaimId",
            "type": "bytes32"
          },
          {
            "internalType": "uint16",
            "name": "creatorFeeBps",
            "type": "uint16"
          },
          {
            "internalType": "uint16",
            "name": "claimerShareBps",
            "type": "uint16"
          },
          {
            "internalType": "address",
            "name": "referrer",
            "type": "address"
          },
          {
            "internalType": "bool",
            "name": "vestingEnabled",
            "type": "bool"
          },
          {
            "internalType": "address",
            "name": "quote",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "devBuyQuote",
            "type": "uint256"
          }
        ],
        "internalType": "struct EquityTokenFactory.LaunchParams",
        "name": "p",
        "type": "tuple"
      }
    ],
    "name": "createToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "deployer",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeRouter",
    "outputs": [
      {
        "internalType": "contract FeeRouter",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract EquityLaunchHook",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "locker",
    "outputs": [
      {
        "internalType": "contract PermanentLocker",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "nonce",
        "type": "uint256"
      }
    ],
    "name": "predictToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "quotes",
    "outputs": [
      {
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      },
      {
        "internalType": "uint128",
        "name": "graduationQuote",
        "type": "uint128"
      },
      {
        "internalType": "int24",
        "name": "startTick",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "gradTick",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "endTick",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "registry",
    "outputs": [
      {
        "internalType": "contract Registry",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenImplementation",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokenQuote",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "vestingVault",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId} from "v4-core/types/PoolId.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {LaunchToken} from "./LaunchToken.sol";
import {EquityLaunchHook} from "./EquityLaunchHook.sol";
import {PermanentLocker} from "./PermanentLocker.sol";
import {FeeRouter} from "./FeeRouter.sol";
import {Registry} from "./Registry.sol";

interface IERC20Quote {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function decimals() external view returns (uint8);
}

/// @title EquityTokenFactory — launches memecoins QUOTED IN Robinhood Chain Stock Tokens
/// @notice TokenFactory variant for the equity generation (docs/stock-paired-design.md).
///         Differences from TokenFactory:
///           - the quote asset is a PER-LAUNCH choice from an append-only allowlist of
///             stock tokens; each entry pins that stock's graduation bar and curve ticks
///             at listing time and is immutable afterwards (list only ever grows — the
///             deployer's only power is adding NEW quotes; nothing about live pools or
///             already-listed quotes can ever be changed);
///           - the creator dev-buy is an ERC-20 pull (approve + `devBuyQuote`), not
///             native msg.value — stock tokens cannot be wrapped from ETH.
///         Everything else (fixed 1B supply, curve shape via the locker ladder, fee
///         routing, vesting, registry) matches TokenFactory. The PermanentLocker,
///         FeeRouter, Registry, VestingVault and LaunchToken are REUSED UNCHANGED —
///         they are already currency-agnostic.
contract EquityTokenFactory {
    uint16 public constant MAX_VESTING_BPS = 200; // <=2% creator slice
    int24 public constant TICK_SPACING = 100;
    uint8 public constant CURVE_BUCKETS = 10;
    uint8 public constant TAIL_BUCKETS = 2;

    /// @notice Curve + graduation parameters for one allowlisted stock quote. Set once at
    ///         listing (deployer), immutable afterwards. Ticks are canonical
    ///         (token-as-currency0) like TokenFactory's constants; per-quote because a
    ///         $420 stock and a $4 ETF need different start prices for the same USD FDV.
    struct QuoteConfig {
        bool enabled;
        uint128 graduationQuote; // graduation bar in quote units (18 decimals)
        int24 startTick;
        int24 gradTick;
        int24 endTick;
    }

    struct LaunchParams {
        string name;
        string symbol;
        string metadataURI;
        bytes32 socialClaimId;
        uint16 creatorFeeBps; // 1000-5500
        uint16 claimerShareBps;
        address referrer;
        bool vestingEnabled;
        address quote; // MUST be an allowlisted stock token
        uint256 devBuyQuote; // optional creator dev-buy in quote units (approve first)
    }

    IPoolManager public immutable poolManager;
    EquityLaunchHook public immutable hook;
    PermanentLocker public immutable locker;
    FeeRouter public immutable feeRouter;
    Registry public immutable registry;
    address public immutable tokenImplementation;
    address public immutable vestingVault;
    address public immutable deployer; // may APPEND quotes; nothing else

    uint256 public launchCount;
    mapping(address => QuoteConfig) public quotes;
    /// @notice quote asset for each launched token (routers derive the PoolKey from this)
    mapping(address => address) public tokenQuote;

    event QuoteAdded(address indexed quote, uint128 graduationQuote, int24 startTick, int24 gradTick, int24 endTick);
    event TokenCreated(
        address indexed token,
        address indexed creator,
        bytes32 indexed socialClaimId,
        PoolId poolId,
        uint256 launchBlock,
        string metadataURI,
        address quote
    );

    error SymbolRequired();
    error NotDeployer();
    error QuoteAlreadyListed();
    error QuoteNotAllowed();
    error BadQuoteConfig();
    error QuotePullFailed();

    constructor(
        IPoolManager poolManager_,
        EquityLaunchHook hook_,
        PermanentLocker locker_,
        FeeRouter feeRouter_,
        Registry registry_,
        address tokenImplementation_,
        address vestingVault_
    ) {
        poolManager = poolManager_;
        hook = hook_;
        locker = locker_;
        feeRouter = feeRouter_;
        registry = registry_;
        tokenImplementation = tokenImplementation_;
        vestingVault = vestingVault_;
        deployer = msg.sender;
    }

    // ------------------------------------------------------------------
    // Quote allowlist (append-only; entries immutable once listed)
    // ------------------------------------------------------------------

    /// @notice List a stock token as a launchable quote. APPEND-ONLY: an existing entry
    ///         can never be modified or removed, so params for live launches are fixed
    ///         forever. Curation duty: only canonical Robinhood Stock Tokens (18
    ///         decimals, non-rebasing raw balances) belong here — the deployer verifies
    ///         the contract on Blockscout before listing (see stock-paired-design.md §2).
    function addQuote(address quote, uint128 graduationQuote_, int24 startTick, int24 gradTick, int24 endTick)
        external
    {
        if (msg.sender != deployer) revert NotDeployer();
        if (quotes[quote].enabled) revert QuoteAlreadyListed();
        if (quote == address(0) || graduationQuote_ == 0) revert BadQuoteConfig();
        if (!(startTick < gradTick && gradTick < endTick)) revert BadQuoteConfig();
        if (IERC20Quote(quote).decimals() != 18) revert BadQuoteConfig();
        quotes[quote] = QuoteConfig({
            enabled: true,
            graduationQuote: graduationQuote_,
            startTick: startTick,
            gradTick: gradTick,
            endTick: endTick
        });
        emit QuoteAdded(quote, graduationQuote_, startTick, gradTick, endTick);
    }

    // ------------------------------------------------------------------
    // Launch
    // ------------------------------------------------------------------

    function createToken(LaunchParams calldata p) external returns (address token, PoolId poolId) {
        if (bytes(p.symbol).length == 0) revert SymbolRequired();
        QuoteConfig memory qc = quotes[p.quote];
        if (!qc.enabled) revert QuoteNotAllowed();

        uint256 vestAmount;
        (token, vestAmount) = _deployToken(p);
        poolId = _setupPool(token, p, qc);
        _seed(token, p, qc, vestAmount);

        registry.register(token, msg.sender, poolId, p.socialClaimId, p.metadataURI);
        emit TokenCreated(token, msg.sender, p.socialClaimId, poolId, block.number, p.metadataURI, p.quote);
    }

    /// @dev Clone + initialize the token; register vesting when enabled.
    function _deployToken(LaunchParams calldata p) internal returns (address token, uint256 vestAmount) {
        bytes32 salt = keccak256(abi.encodePacked(msg.sender, p.symbol, launchCount++));
        token = Clones.cloneDeterministic(tokenImplementation, salt);
        vestAmount = p.vestingEnabled ? (LaunchToken(token).TOTAL_SUPPLY() * MAX_VESTING_BPS) / 10_000 : 0;
        LaunchToken(token).initialize(p.name, p.symbol, address(locker), vestingVault, vestAmount);
        if (vestAmount > 0) IVestingVault(vestingVault).register(token, msg.sender, vestAmount);
    }

    /// @dev Register the pool with the hook (per-quote graduation bar), initialize it at
    ///      the quote's start tick, and write the fee split + quote lookup.
    function _setupPool(address token, LaunchParams calldata p, QuoteConfig memory qc) internal returns (PoolId poolId) {
        PoolKey memory key = _poolKey(token, p.quote);
        poolId = key.toId();
        bool tokenIs0 = token < p.quote;
        hook.register(key, token, tokenIs0, qc.graduationQuote);
        poolManager.initialize(key, TickMath.getSqrtPriceAtTick(tokenIs0 ? qc.startTick : -qc.startTick));
        feeRouter.setPoolConfig(poolId, msg.sender, p.referrer, p.socialClaimId, p.creatorFeeBps, p.claimerShareBps);
        tokenQuote[token] = p.quote;
    }

    /// @dev ERC-20 dev-buy pull (vs TokenFactory's native-ETH wrap) + ladder seed. Stock
    ///      tokens are non-fee-on-transfer by curation (§addQuote), so the pulled amount
    ///      equals the received amount.
    function _seed(address token, LaunchParams calldata p, QuoteConfig memory qc, uint256 vestAmount) internal {
        if (p.devBuyQuote > 0) {
            if (!IERC20Quote(p.quote).transferFrom(msg.sender, address(locker), p.devBuyQuote)) revert QuotePullFailed();
        }
        locker.seed(
            PermanentLocker.SeedParams({
                key: _poolKey(token, p.quote),
                token: token,
                tokenIs0: token < p.quote,
                startTick: qc.startTick,
                gradTick: qc.gradTick,
                endTick: qc.endTick,
                curveBuckets: CURVE_BUCKETS,
                tailBuckets: TAIL_BUCKETS,
                supplyToSeed: LaunchToken(token).TOTAL_SUPPLY() - vestAmount,
                initialBuyQuote: p.devBuyQuote,
                buyRecipient: msg.sender
            })
        );
    }

    function _poolKey(address token, address quote) internal view returns (PoolKey memory) {
        bool tokenIs0 = token < quote;
        return PoolKey({
            currency0: Currency.wrap(tokenIs0 ? token : quote),
            currency1: Currency.wrap(tokenIs0 ? quote : token),
            fee: 0,
            tickSpacing: TICK_SPACING,
            hooks: IHooks(address(hook))
        });
    }

    /// @notice CREATE2-deterministic token address prediction (same scheme as TokenFactory).
    function predictToken(address creator, string calldata symbol, uint256 nonce) external view returns (address) {
        bytes32 salt = keccak256(abi.encodePacked(creator, symbol, nonce));
        return Clones.predictDeterministicAddress(tokenImplementation, salt, address(this));
    }
}

interface IVestingVault {
    function register(address token, address creator, uint256 amount) external;
}
Chain explorer2433msChain node82ms