RHB

0xb9fe2adf7c25ebe4081a2e228b02be05ebdf7410

Verification
Verified
0.8.24+commit.e11b9ed9
Type
Contract
14,971 bytes
ABI entries
105
33 read · 19 write
License
none

Contract information

Address
0xb9fe2adf7c25ebe4081a2e228b02be05ebdf7410
Chain
Robinhood Chain (4663)
Compiler
0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x3c674ca4b7…f305923b5d

Token

Name
ROBBIN HOOD BABIES
Symbol
HOODIO
Decimals
18
Holders
784

Read contract (33)

LP_ALLOCATION()uint256
MAX_PER_TX()uint256
MAX_PER_WALLET()uint256
MAX_PER_WALLET_HARD_CAP()uint256
MAX_SUPPLY()uint256
MINT_ALLOCATION()uint256
PRICE()uint256
ROYALTY_BPS()uint96
TOKENS_PER_HOODIE()uint256
allowance(address, address)uint256
balanceOf(address)uint256
baseURI()string
decimals()uint8
getSkipNFT(address)bool
merkleRoot()bytes32
metadataFrozen()bool
mintFinalized()bool
mintedHoodies()uint256
mirror()address
mirrorERC721()address
name()string
numberMinted(address)uint256
owner()address
payout()address
pendingOwner()address
phase()uint8
provenanceHash()bytes32
provenanceSet()bool
rootLocked()bool
royaltyReceiver()address
symbol()string
totalSupply()uint256
treasury()address

Events (15)

ApprovalBaseURISetMaxPerWalletSetMerkleRootSetMetadataFrozenEventMintFinalizedOwnershipTransferStartedOwnershipTransferredPayoutSetPhaseChangedProvenanceSetRoyaltyReceiverSetSkipNFTSetTransferWithdrawn

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "royaltyReceiver_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "price_",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "baseURI_",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "ApprovalCallerNotOwnerNorApproved",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ContractMintDisallowed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DNAlreadyInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DNNotInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FnSelectorNotRecognized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "IncorrectPayment",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientAllowance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidUnit",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LinkMirrorContractFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxPerTxExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MetadataFrozen",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MintAllocationExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MintAlreadyFinalized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MirrorAddressIsZero",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotAllowlisted",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PayoutFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ProvenanceAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ProvenanceLockedAfterClosed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RootLockedAfterPublic",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SenderNotMirror",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenDoesNotExist",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TotalSupplyOverflow",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferCallerNotOwnerNorApproved",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferFromIncorrectOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferToZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WalletCapExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WalletCapLockedAfterClosed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WalletCapTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WithdrawFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WrongPhase",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroQuantity",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "uri",
        "type": "string"
      }
    ],
    "name": "BaseURISet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "previous",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "current",
        "type": "uint256"
      }
    ],
    "name": "MaxPerWalletSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "root",
        "type": "bytes32"
      }
    ],
    "name": "MerkleRootSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "MetadataFrozenEvent",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "leftoverUnitsToTreasury",
        "type": "uint256"
      }
    ],
    "name": "MintFinalized",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "payout",
        "type": "address"
      }
    ],
    "name": "PayoutSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "enum RHB.Phase",
        "name": "previous",
        "type": "uint8"
      },
      {
        "indexed": true,
        "internalType": "enum RHB.Phase",
        "name": "current",
        "type": "uint8"
      }
    ],
    "name": "PhaseChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "provenance",
        "type": "bytes32"
      }
    ],
    "name": "ProvenanceSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "RoyaltyReceiverSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "status",
        "type": "bool"
      }
    ],
    "name": "SkipNFTSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Withdrawn",
    "type": "event"
  },
  {
    "stateMutability": "payable",
    "type": "fallback"
  },
  {
    "inputs": [],
    "name": "LP_ALLOCATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_PER_TX",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_PER_WALLET",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_PER_WALLET_HARD_CAP",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MINT_ALLOCATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PRICE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROYALTY_BPS",
    "outputs": [
      {
        "internalType": "uint96",
        "name": "",
        "type": "uint96"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOKENS_PER_HOODIE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "allowance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "qty",
        "type": "uint256"
      },
      {
        "internalType": "bytes32[]",
        "name": "proof",
        "type": "bytes32[]"
      }
    ],
    "name": "bandMint",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "baseURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "decimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "finalizeMint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "freezeMetadata",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "getSkipNFT",
    "outputs": [
      {
        "internalType": "bool",
        "name": "result",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "merkleRoot",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "metadataFrozen",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "mintFinalized",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "mintedHoodies",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "mirror",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "mirrorERC721",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "numberMinted",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "payout",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "phase",
    "outputs": [
      {
        "internalType": "enum RHB.Phase",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "provenanceHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "provenanceSet",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "qty",
        "type": "uint256"
      }
    ],
    "name": "publicMint",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rootLocked",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "royaltyReceiver",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "uri",
        "type": "string"
      }
    ],
    "name": "setBaseURI",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newCap",
        "type": "uint256"
      }
    ],
    "name": "setMaxPerWallet",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "root",
        "type": "bytes32"
      }
    ],
    "name": "setMerkleRoot",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "p",
        "type": "address"
      }
    ],
    "name": "setPayout",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "enum RHB.Phase",
        "name": "p",
        "type": "uint8"
      }
    ],
    "name": "setPhase",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "h",
        "type": "bytes32"
      }
    ],
    "name": "setProvenance",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "setRoyaltyReceiver",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "skipNFT",
        "type": "bool"
      }
    ],
    "name": "setSkipNFT",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {DN404} from "dn404/DN404.sol";
import {DN404Mirror} from "dn404/DN404Mirror.sol";
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import {LibString} from "solady/utils/LibString.sol";

/**
 * @title ROBBIN HOOD BABIES ($HOODIO)
 * @notice Dual-nature token (DN404 / ERC-7631) for the RHB collection on
 *         Robinhood L2. One Hoodie NFT = 1,000,000 $HOODIO tokens (the DN404
 *         unit): the ERC-20 side trades on DEXes while the mirror ERC-721
 *         tracks whole-unit holders. Accumulating a whole unit from the pool
 *         mints a fresh Hoodie; selling below one burns it ("win or lose your
 *         NFT on trade" — pure reroll, no locking).
 *
 * Supply model (units of 1 Hoodie = 1,000,000 tokens = 1e24 base):
 *  - Total token supply = 8,888,000,000 $HOODIO (8888 units), fixed forever.
 *  - MAX_SUPPLY        = 8888 Hoodie units total.
 *  - MINT_ALLOCATION   = 4444 sold via Band (allowlist) then Public phases at
 *                        PRICE per unit, MAX_PER_WALLET combined across phases.
 *  - LP_ALLOCATION     = 4444 minted to the treasury at deploy (skip-NFT on,
 *                        so no Hoodies materialize) for the team-seeded
 *                        Uniswap LP on Robinhood Chain.
 *  - finalizeMint()    closes minting forever and sweeps any unminted
 *                        allocation to the treasury, bringing totalSupply to
 *                        exactly MAX_SUPPLY * _unit(). Replaces burnRemaining().
 *
 * Invariants:
 *  - ERC-20 totalSupply can never exceed MAX_SUPPLY * _unit(): mintable paths are
 *    capped by MINT_ALLOCATION (lifetime counter `mintedHoodies`, never
 *    decremented) and finalizeMint mints exactly the leftover, once.
 *  - A wallet can never mint more than MAX_PER_WALLET units across BOTH
 *    phases: the counter (DN404 aux storage) is lifetime and never decremented
 *    — transferring tokens away does not reset it.
 *  - A proof for address A can never mint for address B: the merkle leaf is
 *    keccak256(abi.encodePacked(msg.sender)), matching the off-chain TS tree
 *    (sorted-pair keccak256; see docs/merkle-format.md).
 *  - The merkle root locks permanently the first time phase enters Public.
 *  - baseURI is immutable after freezeMetadata(); provenance is set once,
 *    only while Closed.
 *
 * Royalties (5%) live on the mirror (the ERC-721 marketplaces see); DEX trades
 * of the ERC-20 side inherently bypass ERC-2981 — accepted trade-off.
 */
contract RHB is DN404, Ownable2Step {
    // ---------------------------------------------------------------------
    // Errors
    // ---------------------------------------------------------------------
    error WrongPhase();
    error ZeroQuantity();
    error MintAllocationExceeded();
    error MintAlreadyFinalized();
    error MaxPerTxExceeded();
    error WalletCapExceeded();
    error NotAllowlisted();
    error ContractMintDisallowed();
    error IncorrectPayment();
    error MetadataFrozen();
    error RootLockedAfterPublic();
    error ProvenanceAlreadySet();
    error ProvenanceLockedAfterClosed();
    error WalletCapTooHigh();
    error WalletCapLockedAfterClosed();
    error WithdrawFailed();
    error PayoutFailed();
    error ZeroAddress();

    // ---------------------------------------------------------------------
    // Phase
    // ---------------------------------------------------------------------
    enum Phase {
        Closed,
        Band, // allowlist
        Public
    }

    // ---------------------------------------------------------------------
    // Constants / config (units = whole Hoodies; 1 unit = 1,000,000 tokens)
    // ---------------------------------------------------------------------

    /// @dev ERC-20 tokens per Hoodie NFT (the DN404 unit, pre-decimals).
    ///      Total token supply = MAX_SUPPLY * TOKENS_PER_HOODIE = 8,888,000,000.
    uint256 public constant TOKENS_PER_HOODIE = 1_000_000;

    /// @dev Total collection size in Hoodie units, fixed forever.
    uint256 public constant MAX_SUPPLY = 8888;

    /// @dev Units sold through the Band/Public mint phases.
    uint256 public constant MINT_ALLOCATION = 4444;

    /// @dev Units minted to the treasury at deploy for the team-seeded LP.
    uint256 public constant LP_ALLOCATION = 4444;

    /// @dev Max whole Hoodies mintable in a single transaction.
    uint256 public constant MAX_PER_TX = 10;

    /// @dev Absolute ceiling the tunable per-wallet cap can never exceed.
    uint256 public constant MAX_PER_WALLET_HARD_CAP = 20;

    /// @dev Combined per-wallet mint cap across Band + Public, in units.
    ///      Owner-tunable only while phase == Closed, never above the hard cap.
    uint256 public MAX_PER_WALLET = 20;

    /// @dev Mint price per whole Hoodie, in wei. Launch price: 0.02 ETH.
    uint256 public immutable PRICE;

    /// @dev 5% royalty (ERC-2981, denominator 10000) — set on the mirror.
    uint96 public constant ROYALTY_BPS = 500;

    // ---------------------------------------------------------------------
    // Mutable state
    // ---------------------------------------------------------------------
    Phase public phase = Phase.Closed;
    bytes32 public merkleRoot;
    bytes32 public provenanceHash;
    bool public provenanceSet;
    bool public metadataFrozen;
    bool public mintFinalized;

    /// @dev Latches true the first time the mint enters Public (one-way).
    bool public rootLocked;

    /// @dev Units minted via Band + Public. Lifetime counter, never decremented.
    uint256 public mintedHoodies;

    /// @dev Receives the LP allocation and the finalizeMint leftover.
    address public treasury;

    /// @dev Receives mint proceeds DIRECTLY inside every paid mint (no
    ///      withdraw step). MUST be an address that accepts plain ETH
    ///      without reverting (an EOA) — a reverting payout bricks minting
    ///      until setPayout points somewhere sane.
    address public payout;

    address public royaltyReceiver;
    string private _baseTokenURI;

    // ---------------------------------------------------------------------
    // Events
    // ---------------------------------------------------------------------
    event PhaseChanged(Phase indexed previous, Phase indexed current);
    event MerkleRootSet(bytes32 indexed root);
    event BaseURISet(string uri);
    event MetadataFrozenEvent();
    event ProvenanceSet(bytes32 indexed provenance);
    event MaxPerWalletSet(uint256 previous, uint256 current);
    event MintFinalized(uint256 leftoverUnitsToTreasury);
    event Withdrawn(address indexed to, uint256 amount);
    event RoyaltyReceiverSet(address indexed receiver);
    event PayoutSet(address indexed payout);

    // ---------------------------------------------------------------------
    // Constructor
    // ---------------------------------------------------------------------
    /**
     * @param owner_           Safe multisig that owns the contract (and, via
     *                         pull, the mirror).
     * @param royaltyReceiver_ Receives the 5% ERC-2981 royalty (on the mirror).
     * @param price_           Mint price per whole Hoodie, in wei.
     * @param baseURI_         Placeholder ("unhooded") base URI.
     * @param treasury_        Receives LP_ALLOCATION now and any finalizeMint
     *                         leftover later. Skip-NFT is enabled for it, so
     *                         no Hoodies materialize in the treasury.
     */
    constructor(
        address owner_,
        address royaltyReceiver_,
        uint256 price_,
        string memory baseURI_,
        address treasury_
    ) Ownable(owner_) {
        if (owner_ == address(0) || royaltyReceiver_ == address(0) || treasury_ == address(0)) {
            revert ZeroAddress();
        }
        PRICE = price_;
        treasury = treasury_;
        payout = owner_; // proceeds land here mint-by-mint; owner can redirect
        royaltyReceiver = royaltyReceiver_;
        _baseTokenURI = baseURI_;

        address mirror = address(new RHBMirror(msg.sender, royaltyReceiver_));
        // Mints the LP allocation to the treasury with skip-NFT set to true
        // (DN404 does this for the initial supply owner automatically).
        _initializeDN404(LP_ALLOCATION * _unit(), treasury_, mirror);

        // NOTE: the mirror's ERC-173 owner() cannot be synced here — pullOwner()
        // staticcalls this base's owner(), whose code is not yet deployed during
        // construction. The Deploy script calls mirror.pullOwner() right after
        // deploy so marketplaces (OpenSea "claim collection") see the owner.
        // (Audit LOW-1.) Re-run pullOwner() after any ownership handover too.

        emit RoyaltyReceiverSet(royaltyReceiver_);
    }

    // ---------------------------------------------------------------------
    // Mint
    // ---------------------------------------------------------------------

    /**
     * @notice Allowlist mint. Requires phase == Band and a valid merkle proof
     *         for the caller. Per-wallet cap applies across BOTH phases.
     * @param qty   Whole Hoodies to mint (each = 1e18 base units).
     * @param proof Merkle proof for leaf keccak256(abi.encodePacked(msg.sender)).
     */
    function bandMint(uint256 qty, bytes32[] calldata proof) external payable {
        if (phase != Phase.Band) revert WrongPhase();
        bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
        if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert NotAllowlisted();
        _mintChecked(msg.sender, qty);
    }

    /**
     * @notice Public mint. Requires phase == Public. Blocks contract mints via
     *         the tx.origin guard. Per-wallet cap applies across both phases.
     * @param qty Whole Hoodies to mint.
     */
    function publicMint(uint256 qty) external payable {
        if (phase != Phase.Public) revert WrongPhase();
        if (msg.sender != tx.origin) revert ContractMintDisallowed();
        _mintChecked(msg.sender, qty);
    }

    /**
     * @dev Shared mint path enforcing the per-tx limit, the mint allocation,
     *      the lifetime per-wallet cap (DN404 aux storage — transfers never
     *      decrement it) and exact payment (over- AND underpayment revert).
     */
    function _mintChecked(address to, uint256 qty) internal {
        if (qty == 0) revert ZeroQuantity();
        if (qty > MAX_PER_TX) revert MaxPerTxExceeded();
        if (mintFinalized) revert MintAlreadyFinalized();
        if (mintedHoodies + qty > MINT_ALLOCATION) revert MintAllocationExceeded();
        uint256 minted = uint256(_getAux(to)) + qty;
        if (minted > MAX_PER_WALLET) revert WalletCapExceeded();
        if (msg.value != PRICE * qty) revert IncorrectPayment();
        _setAux(to, uint88(minted));
        mintedHoodies += qty;
        _mint(to, qty * _unit());
        // Proceeds go straight to the payout wallet — nothing accumulates
        // in the contract and mint day needs no withdraw call.
        if (msg.value != 0) {
            (bool ok,) = payable(payout).call{value: msg.value}("");
            if (!ok) revert PayoutFailed();
        }
    }

    /**
     * @notice Close minting forever and sweep the unminted allocation to the
     *         treasury (for the LP / later distribution). After this call,
     *         totalSupply == MAX_SUPPLY * 1e18 exactly and no path can ever
     *         mint again. Replaces the old burnRemaining() catalyst.
     */
    function finalizeMint() external onlyOwner {
        if (mintFinalized) revert MintAlreadyFinalized();
        mintFinalized = true;
        uint256 leftover = MINT_ALLOCATION - mintedHoodies;
        if (leftover != 0) _mint(treasury, leftover * _unit());
        emit MintFinalized(leftover);
    }

    // ---------------------------------------------------------------------
    // Owner: phase / allowlist
    // ---------------------------------------------------------------------

    /// @notice Advance the mint phase (Closed -> Band -> Public).
    /// @dev The first transition into Public permanently latches rootLocked.
    function setPhase(Phase p) external onlyOwner {
        Phase previous = phase;
        phase = p;
        if (p == Phase.Public) rootLocked = true;
        emit PhaseChanged(previous, p);
    }

    /// @notice Set/update the allowlist root. Permanently locked once the mint
    ///         has entered Public at any point (one-way latch).
    function setMerkleRoot(bytes32 root) external onlyOwner {
        if (rootLocked) revert RootLockedAfterPublic();
        merkleRoot = root;
        emit MerkleRootSet(root);
    }

    /// @notice Tune the combined per-wallet cap before launch. Only while
    ///         phase == Closed and never above MAX_PER_WALLET_HARD_CAP.
    function setMaxPerWallet(uint256 newCap) external onlyOwner {
        if (phase != Phase.Closed) revert WalletCapLockedAfterClosed();
        if (newCap == 0) revert ZeroQuantity();
        if (newCap > MAX_PER_WALLET_HARD_CAP) revert WalletCapTooHigh();
        uint256 previous = MAX_PER_WALLET;
        MAX_PER_WALLET = newCap;
        emit MaxPerWalletSet(previous, newCap);
    }

    // ---------------------------------------------------------------------
    // Owner: metadata
    // ---------------------------------------------------------------------

    /// @notice THE UNHOODING: swap placeholder base URI for the real CID.
    function setBaseURI(string calldata uri) external onlyOwner {
        if (metadataFrozen) revert MetadataFrozen();
        _baseTokenURI = uri;
        emit BaseURISet(uri);
    }

    /// @notice Irreversibly lock the base URI (trust signal, post-reveal).
    function freezeMetadata() external onlyOwner {
        metadataFrozen = true;
        emit MetadataFrozenEvent();
    }

    /// @notice Set the provenance hash exactly once, only while phase == Closed.
    function setProvenance(bytes32 h) external onlyOwner {
        if (provenanceSet) revert ProvenanceAlreadySet();
        if (phase != Phase.Closed) revert ProvenanceLockedAfterClosed();
        provenanceHash = h;
        provenanceSet = true;
        emit ProvenanceSet(h);
    }

    // ---------------------------------------------------------------------
    // Owner: funds / royalty
    // ---------------------------------------------------------------------

    /// @notice Sweep stray ETH to the owner. Mint proceeds never accumulate
    ///         here (they forward per mint to `payout`); this only exists for
    ///         ETH sent to the contract by accident.
    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        (bool ok,) = payable(owner()).call{value: balance}("");
        if (!ok) revert WithdrawFailed();
        emit Withdrawn(owner(), balance);
    }

    /// @notice Redirect mint proceeds. Keep this an EOA: a payout address
    ///         that reverts on receive bricks every paid mint until changed.
    function setPayout(address p) external onlyOwner {
        if (p == address(0)) revert ZeroAddress();
        payout = p;
        emit PayoutSet(p);
    }

    /// @notice Update the royalty receiver on the mirror (kept at the fixed 5%).
    function setRoyaltyReceiver(address receiver) external onlyOwner {
        if (receiver == address(0)) revert ZeroAddress();
        royaltyReceiver = receiver;
        RHBMirror(payable(mirrorERC721())).setRoyalty(receiver);
        emit RoyaltyReceiverSet(receiver);
    }

    // ---------------------------------------------------------------------
    // Views / DN404 overrides
    // ---------------------------------------------------------------------

    /// @notice Units minted by an address via the mint phases (lifetime;
    ///         transfers never decrement it). Kept for web-ABI compatibility.
    function numberMinted(address account) external view returns (uint256) {
        return _getAux(account);
    }

    /// @notice The mirror ERC-721 ("the Hoodies collection" marketplaces see).
    function mirror() external view returns (address) {
        return mirrorERC721();
    }

    /// @notice The current base URI (placeholder pre-reveal, real CID after).
    function baseURI() external view returns (string memory) {
        return _baseTokenURI;
    }

    /// @dev DN404 unit: 1 Hoodie NFT per 1,000,000 tokens (18 decimals).
    ///      MUST stay constant after _initializeDN404 (it is).
    function _unit() internal view override returns (uint256) {
        return TOKENS_PER_HOODIE * 1e18;
    }

    function name() public pure override returns (string memory) {
        return "ROBBIN HOOD BABIES";
    }

    function symbol() public pure override returns (string memory) {
        return "HOODIO";
    }

    /// @dev Token ids are 1-indexed (DN404 default). Same URI shape as the
    ///      previous ERC-721A contract: baseURI + id.
    function _tokenURI(uint256 id) internal view override returns (string memory result) {
        if (bytes(_baseTokenURI).length != 0) {
            result = string(abi.encodePacked(_baseTokenURI, LibString.toString(id)));
        }
    }
}

/**
 * @title RHBMirror
 * @notice The ERC-721 face of $HOODIO — what marketplaces and holders see.
 *         Adds ERC-2981 royalties on top of the stock DN404 mirror. The
 *         royalty is managed by the base contract's owner (the Safe).
 */
contract RHBMirror is DN404Mirror, ERC2981 {
    error NotContractOwner();

    /// @dev Royalty is permanently fixed at 5%. The rate is NOT caller-settable
    ///      — only the receiver address can ever change. This makes the on-chain
    ///      guarantee match the docs (owner cannot raise it toward 100%).
    uint96 public constant ROYALTY_BPS = 500;

    constructor(address deployer, address royaltyReceiver_)
        DN404Mirror(deployer)
    {
        _setDefaultRoyalty(royaltyReceiver_, ROYALTY_BPS);
    }

    /// @notice Update ONLY the royalty receiver; the 5% rate is immutable.
    ///         Callable by the base contract's CURRENT owner (queried live, so
    ///         an Ownable2Step handover carries over).
    function setRoyalty(address receiver) external {
        if (msg.sender != Ownable(baseERC20()).owner() && msg.sender != baseERC20()) {
            revert NotContractOwner();
        }
        _setDefaultRoyalty(receiver, ROYALTY_BPS);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(DN404Mirror, ERC2981)
        returns (bool)
    {
        return DN404Mirror.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId);
    }
}
Chain explorer3355msChain node82ms