StandardTokenFactoryV4StockV3
0xe662ce5fea0a911fd903eb7c9493aa4a00cffc3c
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
21,351 bytes
ABI entries
64
28 read · 9 write
License
none
Contract information
- Address
- 0xe662ce5fea0a911fd903eb7c9493aa4a00cffc3c
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0xd03543d97B…F446Cd1418
- Creation tx
- 0x57baedaf12…7cd1fb9209
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (28)
DEFAULT_ADMIN_ROLE() → bytes32
DEFAULT_TOKEN_RESTRICTION_BLOCKS() → uint32
FACTORY_VERSION() → uint32
MAX_TOKEN_RESTRICTION_BLOCKS() → uint32
PAUSER_ROLE() → bytes32
TEMPLATE_VERSION() → uint32
TOKEN_MAX_TX_BPS() → uint16
TOKEN_MAX_WALLET_BPS() → uint16
getLaunch(uint256) → tuple
getRoleAdmin(bytes32) → bytes32
hasRole(bytes32, address) → bool
hashLaunchRequest(address, uint256, tuple) → bytes32
identityVerifier() → address
launchFeeManager() → address
launchModuleFactory() → address
launchRequirements() → address
metadataRegistry() → address
paused() → bool
poolManager() → address
predictTokenAddress(tuple, address) → address
projectNumberByToken(address) → uint256
projectRegistry() → address
stockRegistry() → address
supportsInterface(bytes4) → bool
templateRegistry() → address
tokenAt(uint256) → address
tokenCount() → uint256
tokenRestrictionBlocks() → uint32
Events (9)
LaunchOpenedMetadataRegistryConfiguredPausedRoleAdminChangedRoleGrantedRoleRevokedTokenCreatedTokenRestrictionBlocksUpdatedUnpaused
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "admin",
"type": "address"
},
{
"internalType": "contract ProjectRegistry",
"name": "projectRegistry_",
"type": "address"
},
{
"internalType": "contract LaunchTemplateRegistry",
"name": "templateRegistry_",
"type": "address"
},
{
"internalType": "contract IIdentityAttestationVerifier",
"name": "identityVerifier_",
"type": "address"
},
{
"internalType": "contract ILaunchRequirements",
"name": "launchRequirements_",
"type": "address"
},
{
"internalType": "contract ILaunchFeeManager",
"name": "launchFeeManager_",
"type": "address"
},
{
"internalType": "contract EquifoldV4StockLaunchModuleFactoryV3",
"name": "launchModuleFactory_",
"type": "address"
},
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AccessControlBadConfirmation",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "bytes32",
"name": "neededRole",
"type": "bytes32"
}
],
"name": "AccessControlUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "EnforcedPause",
"type": "error"
},
{
"inputs": [],
"name": "ExpectedPause",
"type": "error"
},
{
"inputs": [],
"name": "IncorrectLaunchFee",
"type": "error"
},
{
"inputs": [],
"name": "InvalidAddress",
"type": "error"
},
{
"inputs": [],
"name": "InvalidCreatorFee",
"type": "error"
},
{
"inputs": [],
"name": "InvalidIdentityAttestation",
"type": "error"
},
{
"inputs": [],
"name": "InvalidInitialBuy",
"type": "error"
},
{
"inputs": [],
"name": "InvalidLaunchModule",
"type": "error"
},
{
"inputs": [],
"name": "InvalidRestrictionBlocks",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "TemplateDisabled",
"type": "error"
},
{
"inputs": [],
"name": "TokenDeploymentFailed",
"type": "error"
},
{
"inputs": [],
"name": "UnpredictableSalt",
"type": "error"
},
{
"inputs": [],
"name": "UnsupportedTemplateVersion",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "projectNumber",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "launchModule",
"type": "address"
}
],
"name": "LaunchOpened",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "metadataRegistry",
"type": "address"
}
],
"name": "MetadataRegistryConfigured",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "Paused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "previousAdminRole",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "newAdminRole",
"type": "bytes32"
}
],
"name": "RoleAdminChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "RoleGranted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "RoleRevoked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "projectNumber",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "launchModule",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "templateKey",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint32",
"name": "templateVersion",
"type": "uint32"
},
{
"indexed": false,
"internalType": "uint16",
"name": "creatorFeeBps",
"type": "uint16"
}
],
"name": "TokenCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint32",
"name": "previousBlocks",
"type": "uint32"
},
{
"indexed": false,
"internalType": "uint32",
"name": "newBlocks",
"type": "uint32"
}
],
"name": "TokenRestrictionBlocksUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "Unpaused",
"type": "event"
},
{
"inputs": [],
"name": "DEFAULT_ADMIN_ROLE",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DEFAULT_TOKEN_RESTRICTION_BLOCKS",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FACTORY_VERSION",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_TOKEN_RESTRICTION_BLOCKS",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PAUSER_ROLE",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TEMPLATE_VERSION",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOKEN_MAX_TX_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOKEN_MAX_WALLET_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"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": "templateKey",
"type": "bytes32"
},
{
"internalType": "uint32",
"name": "templateVersion",
"type": "uint32"
},
{
"internalType": "uint16",
"name": "creatorFeeBps",
"type": "uint16"
},
{
"internalType": "bytes",
"name": "identityAttestation",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "launchData",
"type": "bytes"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct StandardTokenFactoryV4StockV3.LaunchRequest",
"name": "request",
"type": "tuple"
},
{
"internalType": "address",
"name": "stock",
"type": "address"
},
{
"internalType": "int24",
"name": "launchTick",
"type": "int24"
}
],
"name": "createToken",
"outputs": [
{
"internalType": "uint256",
"name": "projectNumber",
"type": "uint256"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "launchModule",
"type": "address"
}
],
"stateMutability": "payable",
"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": "templateKey",
"type": "bytes32"
},
{
"internalType": "uint32",
"name": "templateVersion",
"type": "uint32"
},
{
"internalType": "uint16",
"name": "creatorFeeBps",
"type": "uint16"
},
{
"internalType": "bytes",
"name": "identityAttestation",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "launchData",
"type": "bytes"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct StandardTokenFactoryV4StockV3.LaunchRequest",
"name": "request",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "initialBuyAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "address",
"name": "stock",
"type": "address"
},
{
"internalType": "int24",
"name": "launchTick",
"type": "int24"
}
],
"name": "createTokenAndBuy",
"outputs": [
{
"internalType": "uint256",
"name": "projectNumber",
"type": "uint256"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "launchModule",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "projectNumber",
"type": "uint256"
}
],
"name": "getLaunch",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "launchModule",
"type": "address"
},
{
"internalType": "bytes32",
"name": "templateKey",
"type": "bytes32"
},
{
"internalType": "uint32",
"name": "templateVersion",
"type": "uint32"
},
{
"internalType": "uint16",
"name": "creatorFeeBps",
"type": "uint16"
},
{
"internalType": "bytes32",
"name": "normalizedNameHash",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "normalizedSymbolHash",
"type": "bytes32"
}
],
"internalType": "struct StandardTokenFactoryV4StockV3.TokenLaunchRecord",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
}
],
"name": "getRoleAdmin",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "grantRole",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "hasRole",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"components": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "bytes32",
"name": "templateKey",
"type": "bytes32"
},
{
"internalType": "uint32",
"name": "templateVersion",
"type": "uint32"
},
{
"internalType": "uint16",
"name": "creatorFeeBps",
"type": "uint16"
},
{
"internalType": "bytes",
"name": "identityAttestation",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "launchData",
"type": "bytes"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct StandardTokenFactoryV4StockV3.LaunchRequest",
"name": "request",
"type": "tuple"
}
],
"name": "hashLaunchRequest",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "identityVerifier",
"outputs": [
{
"internalType": "contract IIdentityAttestationVerifier",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "launchFeeManager",
"outputs": [
{
"internalType": "contract ILaunchFeeManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "launchModuleFactory",
"outputs": [
{
"internalType": "contract EquifoldV4StockLaunchModuleFactoryV3",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "launchRequirements",
"outputs": [
{
"internalType": "contract ILaunchRequirements",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "metadataRegistry",
"outputs": [
{
"internalType": "contract IMetadataRegistry",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "paused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"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": "templateKey",
"type": "bytes32"
},
{
"internalType": "uint32",
"name": "templateVersion",
"type": "uint32"
},
{
"internalType": "uint16",
"name": "creatorFeeBps",
"type": "uint16"
},
{
"internalType": "bytes",
"name": "identityAttestation",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "launchData",
"type": "bytes"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct StandardTokenFactoryV4StockV3.LaunchRequest",
"name": "request",
"type": "tuple"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
}
],
"name": "predictTokenAddress",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "projectNumberByToken",
"outputs": [
{
"internalType": "uint256",
"name": "projectNumber",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "projectRegistry",
"outputs": [
{
"internalType": "contract ProjectRegistry",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "callerConfirmation",
"type": "address"
}
],
"name": "renounceRole",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "role",
"type": "bytes32"
},
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "revokeRole",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IMetadataRegistry",
"name": "metadataRegistry_",
"type": "address"
}
],
"name": "setMetadataRegistry",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "newBlocks",
"type": "uint32"
}
],
"name": "setTokenRestrictionBlocks",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "stockRegistry",
"outputs": [
{
"internalType": "contract StockQuoteRegistry",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "templateRegistry",
"outputs": [
{
"internalType": "contract LaunchTemplateRegistry",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenAt",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenRestrictionBlocks",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "unpause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IPoolManager } from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { IIdentityAttestationVerifier } from "../interfaces/IIdentityAttestationVerifier.sol";
import { ILaunchFeeManager } from "../interfaces/ILaunchFeeManager.sol";
import { ILaunchRequirements } from "../interfaces/ILaunchRequirements.sol";
import { IMetadataRegistry } from "../interfaces/IMetadataRegistry.sol";
import { IStandardTokenLaunchModule } from "../interfaces/IStandardTokenLaunchModule.sol";
import { ProductType } from "../types/ProductType.sol";
import { Gen4LaunchToken } from "./Gen4LaunchToken.sol";
import { Gen4RestrictedTokenV4Pool } from "./Gen4RestrictedTokenV4Pool.sol";
import { EquifoldV4StockLaunchModuleFactoryV3 } from "./EquifoldV4StockLaunchModuleFactoryV3.sol";
import { LaunchTemplateRegistry } from "./LaunchTemplateRegistry.sol";
import { ProjectRegistry } from "./ProjectRegistry.sol";
import { StockQuoteRegistry } from "./StockQuoteRegistry.sol";
/// @title Equifold Standard Token Factory (gen4 stock-paired v4 launch, v3 — hookless)
/// @author Equifold
/// @notice The gen4 stock-paired vertical's token factory — a byte-faithful clone of
/// {StandardTokenFactoryV4StockV2} with the gen4 deltas: (1) the launch token is deployed via
/// `CREATE2` under a caller-supplied `salt` (vanity address), falling back to block-level
/// entropy when `salt == 0` so the address stays unpredictable ahead of the launch; (2) the
/// identity attestation is bound to that salt (folded into {hashLaunchRequest}) and consumes
/// the registry-free gen4 verifier ({IdentityAttestationVerifierV2}) — NO on-chain
/// `TokenIdentityRegistry` uniqueness, so the same normalized name/symbol may relaunch; (3) it
/// pins `STANDARD_TEMPLATE` version 11 and mints {Gen4RestrictedTokenV4Pool} with the gen4
/// anti-snipe window ({tokenRestrictionBlocks}, 2 blocks by default and admin-settable); and
/// (4) its launch module is created via
/// {EquifoldV4StockLaunchModuleFactoryV3.createModule}. The {StockQuoteRegistry} allowlist
/// (read from the module factory so both bind the same instance) and the per-launch stock pick
/// are UNCHANGED from v2.
/// @dev CRITICAL INVARIANT: the entire launch — token deploy, module creation, pool initialize, and
/// the optional dev buy — happens in ONE transaction through {createToken}/{createTokenAndBuy}.
/// Because a launch pool is hookless and its `PoolKey` is fully deterministic from the CREATE2
/// token address, a non-atomic flow could be front-run (pre-initialization at an attacker price);
/// this factory preserves the atomic `_createToken` control flow to close that window, and the
/// module's tolerant init tolerates a correctly-priced pre-init while failing closed on a wrong
/// one.
contract StandardTokenFactoryV4StockV3 is AccessControl, Pausable, ReentrancyGuard {
error IncorrectLaunchFee();
error InvalidInitialBuy();
error InvalidAddress();
error InvalidCreatorFee();
error InvalidIdentityAttestation();
error InvalidLaunchModule();
error InvalidRestrictionBlocks();
error TemplateDisabled();
error TokenDeploymentFailed();
error UnpredictableSalt();
error UnsupportedTemplateVersion();
uint32 public constant FACTORY_VERSION = 1;
/// @notice The only launch-template version this gen4 vertical accepts.
/// @dev 11, not 10: v10 is the FIRST gen4 generation, still live on mainnet behind the
/// already-deployed factories. This redeployed generation (settable anti-snipe window,
/// slimmer launch-token init code, `predictTokenAddress`, scanner aliases) registers its
/// OWN `STANDARD_TEMPLATE` entry so the two generations are distinguishable on-chain and
/// a v10 launch request can never be served by a v11 factory (or vice versa). Economics
/// are byte-identical to v10 — this is purely a generation marker. Keep in lockstep with
/// `EXPECTED_TEMPLATE_VERSION` in `script/DeployRobinhoodGen4.s.sol` (asserted there).
uint32 public constant TEMPLATE_VERSION = 11;
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
/// @notice Anti-snipe caps baked into every launch token this factory mints: a 3% max-wallet cap
/// and a 3.3% max cumulative-pool-buy cap. Unchanged and NOT admin-settable.
uint16 public constant TOKEN_MAX_WALLET_BPS = 300;
uint16 public constant TOKEN_MAX_TX_BPS = 330;
/// @notice Initial anti-snipe window, in blocks, applied to every launch token: 2 blocks, the
/// value the live Pons launchpad (`PonsLauncherToken`) runs on this same chain.
/// @dev Why 2 and not the original 300: on Robinhood the `block.number` opcode is L1-tied and
/// advances only ~1 per ~8-12s — it is NOT the fast wall-clock block time — so a
/// "300 blocks ≈ 30s" window was really ~40-60 MINUTES, long enough to block external
/// routers (GMGN) and ordinary trading right after launch. Exact semantics at 2: the
/// token sets `restrictionEndBlock = launchBlock + 2` and enforces while
/// `block.number <= restrictionEndBlock`, so the LAUNCH block bans every pool buy but
/// this factory's atomic dev buy, blocks `launchBlock + 1` AND `launchBlock + 2` are
/// capped at max-wallet / max-cumulative-buy, and the window lifts at `launchBlock + 3`.
/// 0 is not an option — {Gen4RestrictedTokenV4Pool}'s constructor reverts
/// `InvalidRestrictionConfig` on `restrictionBlocks == 0`.
uint32 public constant DEFAULT_TOKEN_RESTRICTION_BLOCKS = 2;
/// @notice Hard ceiling {setTokenRestrictionBlocks} enforces, so a fat-fingered update cannot
/// recreate the ~40-60 minute window that made launches untradeable.
uint32 public constant MAX_TOKEN_RESTRICTION_BLOCKS = 300;
/// @notice The anti-snipe window, in blocks, minted into the next launch token. STORAGE, not a
/// constant: it is admin-settable via {setTokenRestrictionBlocks} precisely so tuning it
/// never again requires redeploying the whole vertical.
/// @dev CRITICAL: this value is a {Gen4RestrictedTokenV4Pool} constructor argument, so it is
/// part of the CREATE2 init code. Anything predicting a launch address off-chain (the
/// API's `f01d` vanity grinder) MUST read it from here at grind time rather than
/// hardcoding it — a divergence makes every predicted address wrong.
uint32 public tokenRestrictionBlocks;
struct LaunchRequest {
string name;
string symbol;
string metadataURI;
bytes32 templateKey;
uint32 templateVersion;
uint16 creatorFeeBps;
bytes identityAttestation;
bytes launchData;
bytes32 salt;
}
struct TokenLaunchRecord {
address creator;
address token;
address launchModule;
bytes32 templateKey;
uint32 templateVersion;
uint16 creatorFeeBps;
bytes32 normalizedNameHash;
bytes32 normalizedSymbolHash;
}
struct LaunchCreation {
uint256 projectNumber;
address token;
address launchModule;
bytes32 normalizedNameHash;
bytes32 normalizedSymbolHash;
}
ProjectRegistry public immutable projectRegistry;
LaunchTemplateRegistry public immutable templateRegistry;
IIdentityAttestationVerifier public immutable identityVerifier;
ILaunchRequirements public immutable launchRequirements;
ILaunchFeeManager public immutable launchFeeManager;
EquifoldV4StockLaunchModuleFactoryV3 public immutable launchModuleFactory;
/// @notice The allowlist of Robinhood tokenized stocks eligible to quote a launch pool, read from
/// the module factory so both bind the same registry instance.
StockQuoteRegistry public immutable stockRegistry;
/// @notice The v4 `PoolManager` every launch token's anti-snipe check compares transfer senders
/// against (`from == poolManager`). Must be the SAME instance the launch module factory
/// opens pools through.
IPoolManager public immutable poolManager;
IMetadataRegistry public metadataRegistry;
uint256 public tokenCount;
mapping(uint256 index => address token) public tokenAt;
mapping(uint256 projectNumber => TokenLaunchRecord launch) private _launches;
mapping(address token => uint256 projectNumber) public projectNumberByToken;
event TokenCreated(
uint256 indexed projectNumber,
address indexed token,
address indexed creator,
address launchModule,
bytes32 templateKey,
uint32 templateVersion,
uint16 creatorFeeBps
);
event LaunchOpened(
uint256 indexed projectNumber, address indexed token, address indexed launchModule
);
event MetadataRegistryConfigured(address indexed metadataRegistry);
event TokenRestrictionBlocksUpdated(uint32 previousBlocks, uint32 newBlocks);
constructor(
address admin,
ProjectRegistry projectRegistry_,
LaunchTemplateRegistry templateRegistry_,
IIdentityAttestationVerifier identityVerifier_,
ILaunchRequirements launchRequirements_,
ILaunchFeeManager launchFeeManager_,
EquifoldV4StockLaunchModuleFactoryV3 launchModuleFactory_,
IPoolManager poolManager_
) {
if (
admin == address(0) || address(projectRegistry_) == address(0)
|| address(templateRegistry_) == address(0)
|| address(identityVerifier_) == address(0)
|| address(launchRequirements_) == address(0)
|| address(launchFeeManager_) == address(0)
|| address(launchModuleFactory_) == address(0)
|| address(poolManager_) == address(0)
) revert InvalidAddress();
if (
address(projectRegistry_).code.length == 0
|| address(templateRegistry_).code.length == 0
|| address(identityVerifier_).code.length == 0
|| address(launchRequirements_).code.length == 0
|| address(launchFeeManager_).code.length == 0
|| address(launchModuleFactory_).code.length == 0
|| address(poolManager_).code.length == 0
) revert InvalidAddress();
projectRegistry = projectRegistry_;
templateRegistry = templateRegistry_;
identityVerifier = identityVerifier_;
launchRequirements = launchRequirements_;
launchFeeManager = launchFeeManager_;
launchModuleFactory = launchModuleFactory_;
stockRegistry = StockQuoteRegistry(launchModuleFactory_.stockRegistry());
poolManager = poolManager_;
tokenRestrictionBlocks = DEFAULT_TOKEN_RESTRICTION_BLOCKS;
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(PAUSER_ROLE, admin);
}
/// @notice Retunes the anti-snipe window applied to FUTURE launches. Admin-only.
/// @dev Bounded to `[1, MAX_TOKEN_RESTRICTION_BLOCKS]`: 0 would make every launch revert in
/// {Gen4RestrictedTokenV4Pool}'s constructor, and an unbounded value could restore the
/// ~40-60 minute window. Already-launched tokens are unaffected — each one froze its own
/// `restrictionEndBlock` at construction. Because this value feeds the launch token's
/// CREATE2 init code, any off-chain address predictor must re-read it after a change.
function setTokenRestrictionBlocks(
uint32 newBlocks
) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (newBlocks == 0 || newBlocks > MAX_TOKEN_RESTRICTION_BLOCKS) {
revert InvalidRestrictionBlocks();
}
uint32 previousBlocks = tokenRestrictionBlocks;
tokenRestrictionBlocks = newBlocks;
emit TokenRestrictionBlocksUpdated(previousBlocks, newBlocks);
}
function setMetadataRegistry(
IMetadataRegistry metadataRegistry_
) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (address(metadataRegistry) != address(0)) revert InvalidAddress();
if (address(metadataRegistry_) == address(0) || address(metadataRegistry_).code.length == 0)
{
revert InvalidAddress();
}
metadataRegistry = metadataRegistry_;
emit MetadataRegistryConfigured(address(metadataRegistry_));
}
/// @notice Creates a standard token through the configured identity, requirements, and launch
/// modules, quoting the launch pool in `stock` and opening it at `launchTick`.
function createToken(
LaunchRequest calldata request,
address stock,
int24 launchTick
)
external
payable
whenNotPaused
nonReentrant
returns (uint256 projectNumber, address token, address launchModule)
{
return _createToken(request, 0, 0, 0, stock, launchTick);
}
/// @notice Creates a token and executes a disclosed creator buy atomically.
function createTokenAndBuy(
LaunchRequest calldata request,
uint256 initialBuyAmount,
uint256 minTokensOut,
uint256 deadline,
address stock,
int24 launchTick
)
external
payable
whenNotPaused
nonReentrant
returns (uint256 projectNumber, address token, address launchModule)
{
if (initialBuyAmount == 0 || minTokensOut == 0 || deadline < block.timestamp) {
revert InvalidInitialBuy();
}
return _createToken(request, initialBuyAmount, minTokensOut, deadline, stock, launchTick);
}
function _createToken(
LaunchRequest calldata request,
uint256 initialBuyAmount,
uint256 minTokensOut,
uint256 deadline,
address stock,
int24 launchTick
) private returns (uint256 projectNumber, address token, address launchModule) {
// The launch pool must be quoted in an allowlisted, enabled Robinhood tokenized stock.
// Reverts StockNotEnabled before any state is touched or external module is created.
stockRegistry.getStock(stock);
LaunchTemplateRegistry.StandardTokenTemplate memory template =
templateRegistry.getTemplate(request.templateKey, request.templateVersion);
if (!template.enabled) revert TemplateDisabled();
// gen4 pins the launch-template version: only STANDARD_TEMPLATE v11 launches this vertical.
if (template.version != TEMPLATE_VERSION) revert UnsupportedTemplateVersion();
// The template, not the creator, fixes the initial fee. A creator may
// still reduce it later through the curve's one-way reduction path.
if (request.creatorFeeBps != template.creatorFeeBps) revert InvalidCreatorFee();
if (msg.value != template.launchFeeWei + initialBuyAmount) revert IncorrectLaunchFee();
LaunchCreation memory creation;
creation.projectNumber = projectRegistry.nextProjectNumber();
(creation.normalizedNameHash, creation.normalizedSymbolHash, creation.launchModule) =
_prepareLaunch(request, creation.projectNumber, stock, launchTick);
creation.token = _deployToken(request, template, creation.projectNumber);
// The token mints its whole supply to THIS factory (its `msg.sender`), never to the module:
// keeping the module address out of the token's constructor is what makes the CREATE2 init
// code — and therefore a ground vanity address — independent of chain state. The supply is
// forwarded here, in the same atomic frame, so the module still holds exactly `totalSupply()`
// when `initializeToken` runs (which it re-checks) and nobody else can ever move it.
SafeERC20.safeTransfer(IERC20(creation.token), creation.launchModule, template.tokenSupply);
IStandardTokenLaunchModule(creation.launchModule).initializeToken(creation.token);
_finalizeLaunch(request, template, creation, initialBuyAmount, minTokensOut, deadline);
return (creation.projectNumber, creation.token, creation.launchModule);
}
function _finalizeLaunch(
LaunchRequest calldata request,
LaunchTemplateRegistry.StandardTokenTemplate memory template,
LaunchCreation memory creation,
uint256 initialBuyAmount,
uint256 minTokensOut,
uint256 deadline
) private {
_registerProject(
creation.projectNumber,
creation.token,
request,
template,
creation.normalizedNameHash,
creation.normalizedSymbolHash
);
if (address(metadataRegistry) != address(0)) {
metadataRegistry.registerMetadata(
creation.projectNumber,
msg.sender,
request.metadataURI,
keccak256(bytes(request.metadataURI))
);
}
launchFeeManager.collectLaunchFee{ value: template.launchFeeWei }(
msg.sender, creation.projectNumber, request.templateKey, request.templateVersion
);
_recordLaunch(
request,
creation.projectNumber,
creation.token,
creation.launchModule,
creation.normalizedNameHash,
creation.normalizedSymbolHash
);
if (initialBuyAmount != 0) {
// The launch token gates its single-recipient launch-block exemption on its DEPLOYER
// (this factory), so the open/route/close pair lives here rather than inside the module.
// It is still one atomic frame: the exemption cannot outlive this call.
Gen4RestrictedTokenV4Pool(creation.token).setInitialBuyRecipient(msg.sender);
IStandardTokenLaunchModule(creation.launchModule).initialBuy{ value: initialBuyAmount }(
minTokensOut, msg.sender, deadline
);
Gen4RestrictedTokenV4Pool(creation.token).setInitialBuyRecipient(address(0));
}
}
function _prepareLaunch(
LaunchRequest calldata request,
uint256 projectNumber,
address stock,
int24 launchTick
) private returns (bytes32 normalizedNameHash, bytes32 normalizedSymbolHash, address module) {
bytes32 launchParametersHash = hashLaunchRequest(msg.sender, projectNumber, request);
(normalizedNameHash, normalizedSymbolHash) = identityVerifier.consumeAttestation(
msg.sender,
request.name,
request.symbol,
launchParametersHash,
request.identityAttestation
);
if (normalizedNameHash == bytes32(0) || normalizedSymbolHash == bytes32(0)) {
revert InvalidIdentityAttestation();
}
launchRequirements.reserveLaunch(
msg.sender, projectNumber, request.templateKey, request.templateVersion
);
module = launchModuleFactory.createModule(msg.sender, projectNumber, stock, launchTick);
if (module == address(0) || module.code.length == 0) revert InvalidLaunchModule();
}
function _recordLaunch(
LaunchRequest calldata request,
uint256 projectNumber,
address token,
address launchModule,
bytes32 normalizedNameHash,
bytes32 normalizedSymbolHash
) private {
_launches[projectNumber] = TokenLaunchRecord({
creator: msg.sender,
token: token,
launchModule: launchModule,
templateKey: request.templateKey,
templateVersion: request.templateVersion,
creatorFeeBps: request.creatorFeeBps,
normalizedNameHash: normalizedNameHash,
normalizedSymbolHash: normalizedSymbolHash
});
projectNumberByToken[token] = projectNumber;
tokenAt[tokenCount] = token;
++tokenCount;
emit TokenCreated(
projectNumber,
token,
msg.sender,
launchModule,
request.templateKey,
request.templateVersion,
request.creatorFeeBps
);
emit LaunchOpened(projectNumber, token, launchModule);
}
/// @notice The address {createToken}/{createTokenAndBuy} would deploy the launch token to for
/// `request` from `creator`, under `request.salt`.
/// @dev The on-chain mirror of the API's off-chain vanity grinder. That grinder must stay local
/// (it tries many salts; an RPC per salt is a non-starter), so this view is not on its hot
/// path — it exists as the authoritative CROSS-CHECK: the grinder hand-transcribes this
/// constructor's struct field order in TypeScript, and a Solidity field reorder would
/// otherwise mis-predict every address with nothing failing loudly. Reverts on a zero salt,
/// which is the {_entropySalt} path and is deliberately unpredictable.
function predictTokenAddress(
LaunchRequest calldata request,
address creator
) external view returns (address) {
if (request.salt == bytes32(0)) revert UnpredictableSalt();
LaunchTemplateRegistry.StandardTokenTemplate memory template =
templateRegistry.getTemplate(request.templateKey, request.templateVersion);
return address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
request.salt,
keccak256(_launchTokenInitCode(request, template, creator))
)
)
)
)
);
}
/// @dev The exact CREATE2 init code the launch token is deployed from: creation bytecode ++ the
/// abi-encoded constructor args. Shared verbatim by {_deployToken} and
/// {predictTokenAddress} so the prediction can never drift from the deployment.
/// NOTE the deliberate absence of `projectNumber` and the launch-module address — see
/// {Gen4LaunchToken}: every input here is either fixed deploy-time config or the creator's own
/// request, so a ground salt lands even if another launch settles in between.
function _launchTokenInitCode(
LaunchRequest calldata request,
LaunchTemplateRegistry.StandardTokenTemplate memory template,
address creator
) private view returns (bytes memory) {
return abi.encodePacked(
type(Gen4RestrictedTokenV4Pool).creationCode,
abi.encode(
Gen4LaunchToken.Initialization({
name: request.name,
symbol: request.symbol,
decimals: template.tokenDecimals,
initialSupply: template.tokenSupply,
burnable: template.burnable,
creator: creator,
metadataURI: request.metadataURI
}),
Gen4RestrictedTokenV4Pool.RestrictionConfigV4({
poolManager: address(poolManager),
maxWalletBps: TOKEN_MAX_WALLET_BPS,
maxTxBps: TOKEN_MAX_TX_BPS,
restrictionBlocks: tokenRestrictionBlocks
})
)
);
}
/// @dev Deploys the launch token via `CREATE2`. A caller-supplied non-zero `salt` yields a vanity
/// (deterministic) address; a zero `salt` falls back to {_entropySalt}, mixing block-level
/// entropy unknowable before this block is mined so the address is unpredictable ahead of the
/// launch. Raw `create2` over {_launchTokenInitCode} rather than `new T{salt}(...)` so the
/// deploy and {predictTokenAddress} consume ONE byte-identical init code.
function _deployToken(
LaunchRequest calldata request,
LaunchTemplateRegistry.StandardTokenTemplate memory template,
uint256 projectNumber
) private returns (address token) {
bytes32 salt = request.salt == bytes32(0)
? _entropySalt(projectNumber, request.name, request.symbol)
: request.salt;
bytes memory initCode = _launchTokenInitCode(request, template, msg.sender);
// Raw CREATE2 over the shared init-code buffer: `new T{salt}(...)` would rebuild the
// constructor args in a SECOND place, which is exactly the drift `predictTokenAddress`
// exists to rule out.
//
// On failure the constructor's OWN revert data is bubbled verbatim (incident-response
// debuggability: a bare `TokenDeploymentFailed()` hides which of the token's constructor
// guards tripped). It costs ~30 bytes of runtime and weakens nothing — `create2` is still
// what makes {predictTokenAddress} a guarantee rather than an estimate. A failure with NO
// returndata (the salt already holds a contract, or the sub-call ran out of gas) has nothing
// to bubble and falls through to {TokenDeploymentFailed}.
//
// Memory-safe: reads a Solidity-allocated `bytes`, and on the revert path writes only ABOVE
// the free-memory pointer, immediately before reverting.
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
token := create2(0, add(initCode, 0x20), mload(initCode), salt)
if iszero(token) {
if returndatasize() {
let reason := mload(0x40)
returndatacopy(reason, 0, returndatasize())
revert(reason, returndatasize())
}
}
}
if (token == address(0)) revert TokenDeploymentFailed();
}
/// @dev Block-level entropy salt used when the caller supplies `salt == 0`. Mixes
/// `blockhash(block.number - 1)` and `block.prevrandao` (unknowable before this block is
/// mined) with per-launch disambiguators (`msg.sender`, `projectNumber`, name, symbol) so two
/// salt-less launches in the SAME block cannot collide. Copied from
/// {StandardTokenFactoryV4StockV2}.
function _entropySalt(
uint256 projectNumber,
string memory name,
string memory symbol
) private view returns (bytes32) {
return keccak256(
abi.encode(
blockhash(block.number - 1),
block.prevrandao,
msg.sender,
projectNumber,
name,
symbol
)
);
}
function _registerProject(
uint256 projectNumber,
address token,
LaunchRequest calldata request,
LaunchTemplateRegistry.StandardTokenTemplate memory template,
bytes32 normalizedNameHash,
bytes32 normalizedSymbolHash
) private {
projectRegistry.registerProject(
projectNumber,
ProjectRegistry.Registration({
creator: msg.sender,
productType: ProductType.STANDARD_TOKEN,
primaryContract: token,
templateKey: request.templateKey,
templateVersion: request.templateVersion,
metadataHash: keccak256(bytes(request.metadataURI)),
normalizedNameHash: normalizedNameHash,
normalizedSymbolHash: normalizedSymbolHash,
isGenesis: template.isGenesis,
isOfficial: template.isOfficial
})
);
}
/// @notice Hash bound into identity attestations to prevent cross-chain and
/// cross-request replay. gen4 additionally folds in `request.salt` so an attestation authorizes a
/// specific deterministic deploy address.
/// @dev The (factory-sequenced) project number is intentionally NOT bound
/// into the hash. Binding `nextProjectNumber()` serialized all launches — two
/// concurrent launches invalidated each other's attestation, enabling a
/// front-run DoS — while adding no replay protection beyond the per-creator
/// nonce already enforced by the verifier. The parameter is retained for ABI
/// stability and remains ignored.
function hashLaunchRequest(
address creator,
uint256,
LaunchRequest calldata request
) public view returns (bytes32) {
return keccak256(
abi.encode(
block.chainid,
address(this),
creator,
keccak256(bytes(request.name)),
keccak256(bytes(request.symbol)),
keccak256(bytes(request.metadataURI)),
request.templateKey,
request.templateVersion,
request.creatorFeeBps,
keccak256(request.launchData),
request.salt
)
);
}
function getLaunch(
uint256 projectNumber
) external view returns (TokenLaunchRecord memory) {
return _launches[projectNumber];
}
function pause() external onlyRole(PAUSER_ROLE) {
_pause();
}
function unpause() external onlyRole(PAUSER_ROLE) {
_unpause();
}
}
Chain explorer2152msChain node80ms