EquityTokenFactory
0x9a92fa82466ef93853b28d3c2719081c89d68db3
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
6,891 bytes
ABI entries
29
16 read · 2 write
License
none
Contract information
- Address
- 0x9a92fa82466ef93853b28d3c2719081c89d68db3
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xD61cd94965…80Bdb88E18
- Creation tx
- 0x9b85fa7575…62833967b6
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 explorer3207msChain node81ms