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
- Creator
- 0x3F2B83E07e…37b701007e
- Creation tx
- 0x3c674ca4b7…f305923b5d
Token
- Name
- ROBBIN HOOD BABIES
- Symbol
- HOODIO
- Decimals
- 18
- Holders
- 784
- Market
- View token page →
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