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