ChibiCoreV2
0x60cb7082c8c14b4237c6a24c65e7c2e7abe2bd17
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
8,938 bytes
ABI entries
75
18 read · 20 write
License
none
Contract information
- Address
- 0x60cb7082c8c14b4237c6a24c65e7c2e7abe2bd17
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x6d606cc634…2C7B835Bb9
- Creation tx
- 0x23770831b6…8eea2e2741
Token
- Name
- Urufu Gēmu
- Symbol
- CHIBI
- Decimals
- —
- Holders
- 299
- Market
- View token page →
Read contract (18)
MAX_AIRDROP_ID() → uint256
airdropClosed() → bool
airdropMintedCount() → uint256
artSlotOf(uint256) → uint32
balanceOf(address) → uint256
getApproved(uint256) → address
isApprovedForAll(address, address) → bool
isController(address) → bool
name() → string
nextContinuationId() → uint256
owner() → address
ownerOf(uint256) → address
pendingOwner() → address
raidPowerOf(uint256) → uint256
roleOf(uint256) → uint8
supportsInterface(bytes4) → bool
symbol() → string
tokenURI(uint256) → string
Events (15)
AirdropClosedEventApprovalApprovalForAllArtSlotAssignedBaseURIUpdatedBatchMetadataUpdateChibiBurnedChibiMintedControllerAddedControllerRemovedMetadataUpdateOwnershipTransferStartedOwnershipTransferredRaidPowerSetTransfer
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
},
{
"internalType": "string",
"name": "initialBaseURI",
"type": "string"
},
{
"internalType": "uint256",
"name": "maxAirdropId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AirdropAlreadyClosed",
"type": "error"
},
{
"inputs": [],
"name": "AirdropClosed",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "actualTotalSupply",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "expectedTotalSupply",
"type": "uint256"
}
],
"name": "AirdropIncomplete",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "ERC721IncorrectOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ERC721InsufficientApproval",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "approver",
"type": "address"
}
],
"name": "ERC721InvalidApprover",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "ERC721InvalidOperator",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "ERC721InvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "receiver",
"type": "address"
}
],
"name": "ERC721InvalidReceiver",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "ERC721InvalidSender",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ERC721NonexistentToken",
"type": "error"
},
{
"inputs": [],
"name": "InvalidBatchMint",
"type": "error"
},
{
"inputs": [],
"name": "InvalidTokenId",
"type": "error"
},
{
"inputs": [],
"name": "InvalidTokenRange",
"type": "error"
},
{
"inputs": [],
"name": "LengthMismatch",
"type": "error"
},
{
"inputs": [],
"name": "NotController",
"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": "TokenAlreadyMinted",
"type": "error"
},
{
"inputs": [],
"name": "TokenDoesNotExist",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "highestAirdroppedId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "continuationStart",
"type": "uint256"
}
],
"name": "AirdropClosedEvent",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "enum GameTypes.Role",
"name": "role",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint32",
"name": "artSlot",
"type": "uint32"
}
],
"name": "ArtSlotAssigned",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "string",
"name": "newBaseURI",
"type": "string"
}
],
"name": "BaseURIUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "_fromTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "_toTokenId",
"type": "uint256"
}
],
"name": "BatchMetadataUpdate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ChibiBurned",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "enum GameTypes.Role",
"name": "role",
"type": "uint8"
}
],
"name": "ChibiMinted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "controller",
"type": "address"
}
],
"name": "ControllerAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "controller",
"type": "address"
}
],
"name": "ControllerRemoved",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "_tokenId",
"type": "uint256"
}
],
"name": "MetadataUpdate",
"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": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "raidPower",
"type": "uint256"
}
],
"name": "RaidPowerSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [],
"name": "MAX_AIRDROP_ID",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "controller",
"type": "address"
}
],
"name": "addController",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "airdropClosed",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "enum GameTypes.Role",
"name": "role",
"type": "uint8"
},
{
"internalType": "uint32",
"name": "artSlot",
"type": "uint32"
},
{
"internalType": "uint256",
"name": "raidPower",
"type": "uint256"
}
],
"name": "airdropMint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "tos",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "tokenIds",
"type": "uint256[]"
},
{
"internalType": "enum GameTypes.Role[]",
"name": "roles",
"type": "uint8[]"
},
{
"internalType": "uint32[]",
"name": "artSlots",
"type": "uint32[]"
},
{
"internalType": "uint256[]",
"name": "raidPowers",
"type": "uint256[]"
}
],
"name": "airdropMintBatch",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "airdropMintedCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "artSlotOf",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "enum GameTypes.Role[]",
"name": "roles",
"type": "uint8[]"
},
{
"internalType": "uint256[]",
"name": "raidPowers",
"type": "uint256[]"
},
{
"internalType": "uint32[]",
"name": "artSlots",
"type": "uint32[]"
}
],
"name": "batchMint",
"outputs": [
{
"internalType": "uint256[]",
"name": "tokenIds",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "fromTokenId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "toTokenId",
"type": "uint256"
}
],
"name": "broadcastMetadataUpdate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "burn",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "expectedTotalSupply",
"type": "uint256"
}
],
"name": "closeAirdrop",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "isController",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "enum GameTypes.Role",
"name": "role",
"type": "uint8"
}
],
"name": "mint",
"outputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "enum GameTypes.Role",
"name": "role",
"type": "uint8"
},
{
"internalType": "uint32",
"name": "artSlot",
"type": "uint32"
}
],
"name": "mintWithArt",
"outputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "nextContinuationId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "raidPowerOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "controller",
"type": "address"
}
],
"name": "removeController",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "roleOf",
"outputs": [
{
"internalType": "enum GameTypes.Role",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "newBaseURI",
"type": "string"
}
],
"name": "setBaseURI",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "raidPower",
"type": "uint256"
}
],
"name": "setRaidPower",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {GameTypes} from "./libraries/GameTypes.sol";
/// @title ChibiCoreV2
/// @notice Robinhood-side ChibiCore for the Base → RH airdrop cutover.
/// Functionally equivalent to `ChibiCore` (v1: roles, artSlots,
/// raid power, controller gating, ERC-4906 metadata broadcasts) but
/// built on OpenZeppelin ERC721 rather than ERC721A so it can mint
/// out-of-order specific tokenIds during the airdrop distribution.
///
/// Two mint paths coexist from deploy:
///
/// 1. `airdropMint(...)` / `airdropMintBatch(...)` — owner only.
/// Used by the deployer / multisig to reproduce a Base chibi at
/// its exact tokenId, role, and art slot. Callable until
/// `closeAirdrop()`.
///
/// 2. IChibiCore continuation mints (`mint` / `mintWithArt` /
/// `batchMint`) — controller only. Assigns sequential ids from
/// `_nextContinuationId` (initialized to `MAX_AIRDROP_ID + 1` in
/// the constructor). Available from deploy; not gated by the
/// airdrop close switch.
///
/// @dev ── Why not ERC721A ──
/// ERC721A packs sequential mint metadata into a single storage slot,
/// efficient for `mintQuantity(N)` but incompatible with the "mint
/// tokenId #47 first, then #12, then #200" pattern the airdrop needs
/// to preserve v1 identity. Standard OZ ERC721 pays per-token mint
/// gas; that's fine because airdrop distribution is a one-time deploy
/// event batched by the owner.
///
/// ── Why role/artSlot mirror v1 ──
/// Role is immutable at mint (like on v1). Art slot is set once at
/// mint and never changes. This preserves "same chibi identity"
/// across the airdrop — from the holder's perspective, RH chibi
/// #47 IS Base chibi #47.
contract ChibiCoreV2 is ERC721, Ownable2Step {
using Strings for uint256;
/// @dev ERC-4906 interface ID.
bytes4 private constant _INTERFACE_ID_ERC4906 = 0x49064906;
// ─── Errors ─────────────────────────────────────────────────────
error NotController();
error ZeroAddress();
error TokenDoesNotExist();
error InvalidBatchMint();
error InvalidTokenId();
error TokenAlreadyMinted();
error AirdropClosed();
error AirdropAlreadyClosed();
error AirdropIncomplete(uint256 actualTotalSupply, uint256 expectedTotalSupply);
error InvalidTokenRange();
error LengthMismatch();
// ─── Events ─────────────────────────────────────────────────────
event ControllerAdded(address indexed controller);
event ControllerRemoved(address indexed controller);
event BaseURIUpdated(string newBaseURI);
event ChibiMinted(uint256 indexed tokenId, address indexed to, GameTypes.Role role);
event ArtSlotAssigned(uint256 indexed tokenId, GameTypes.Role role, uint32 artSlot);
event RaidPowerSet(uint256 indexed tokenId, uint256 raidPower);
event ChibiBurned(uint256 indexed tokenId);
event AirdropClosedEvent(uint256 highestAirdroppedId, uint256 continuationStart);
/// @dev EIP-4906 events declared locally.
event MetadataUpdate(uint256 _tokenId);
event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId);
// ─── Storage ────────────────────────────────────────────────────
/// @notice Highest tokenId reservable for airdrop (inclusive). Set at
/// deploy to the Base snapshot's highest minted tokenId.
uint256 public immutable MAX_AIRDROP_ID;
mapping(address account => bool allowed) private _controllers;
mapping(uint256 tokenId => GameTypes.Role role) private _roles;
mapping(uint256 tokenId => uint32 slot) private _artSlots;
mapping(uint256 tokenId => uint256 power) private _raidPower;
/// @notice Next tokenId assigned by continuation `mint()`. Initialized
/// to `MAX_AIRDROP_ID + 1` in the constructor so ranges are
/// disjoint even while airdrop is open.
uint256 private _nextContinuationId;
/// @notice True once `closeAirdrop()` has been called; airdrop mints
/// are permanently locked out.
bool public airdropClosed;
/// @notice Count of airdrop mints so far (incremented in _airdropMintOne).
/// Read by `closeAirdrop(expectedTotalSupply)` to gate the
/// permanent close against fat-finger before all chibis mint.
/// NOT the same as `_nextContinuationId - MAX_AIRDROP_ID - 1`;
/// that counts continuation mints only. This counts airdrop mints.
uint256 public airdropMintedCount;
string private _baseTokenURI;
modifier onlyController() {
if (!_controllers[msg.sender]) revert NotController();
_;
}
// ─── Constructor ────────────────────────────────────────────────
/// @param initialOwner Deploy-time owner. Handles airdrop mints, then
/// transfers to multisig via Ownable2Step.
/// @param initialBaseURI Metadata prefix (same CDN as v1 for identity parity).
/// @param maxAirdropId Highest tokenId reservable for airdrop. Set to
/// the highest minted tokenId on the Base snapshot.
constructor(address initialOwner, string memory initialBaseURI, uint256 maxAirdropId)
ERC721(unicode"Urufu Gēmu", "CHIBI")
Ownable(_requireNonZero(initialOwner))
{
if (maxAirdropId == 0) revert InvalidTokenId();
_baseTokenURI = initialBaseURI;
MAX_AIRDROP_ID = maxAirdropId;
_nextContinuationId = maxAirdropId + 1;
}
// ─── Views ──────────────────────────────────────────────────────
function roleOf(uint256 tokenId) external view returns (GameTypes.Role) {
_requireMinted(tokenId);
return _roles[tokenId];
}
function artSlotOf(uint256 tokenId) external view returns (uint32) {
_requireMinted(tokenId);
return _artSlots[tokenId];
}
function raidPowerOf(uint256 tokenId) external view returns (uint256) {
_requireMinted(tokenId);
return _raidPower[tokenId];
}
function isController(address account) external view returns (bool) {
return _controllers[account];
}
function nextContinuationId() external view returns (uint256) {
return _nextContinuationId;
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
_requireMinted(tokenId);
return bytes(_baseTokenURI).length == 0 ? "" : string.concat(_baseTokenURI, tokenId.toString());
}
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == _INTERFACE_ID_ERC4906 || super.supportsInterface(interfaceId);
}
// ─── Airdrop mint (owner-only, pre-close) ───────────────────────
/// @notice Mint a chibi at a specific v1 tokenId to `to`.
/// @dev Owner-only. Reverts once `closeAirdrop()` has been called.
/// TokenId must be in `[1, MAX_AIRDROP_ID]` and un-minted.
/// Uses `_mint` (not `_safeMint`) — recipient is the same address
/// that held the chibi on Base. That address is proven able to
/// hold ERC-721s. `_safeMint`'s receiver check would otherwise
/// silently trap EIP-7702-delegated EOAs whose delegate does not
/// implement `IERC721Receiver`.
function airdropMint(
address to,
uint256 tokenId,
GameTypes.Role role,
uint32 artSlot,
uint256 raidPower
) external onlyOwner {
_airdropMintOne(to, tokenId, role, artSlot, raidPower);
}
/// @notice Batched airdrop mint — mints many chibis (each to its own
/// recipient) in one tx. Per-index arrays parallel each other.
/// @dev Owner-only. Reverts once `closeAirdrop()` has been called.
/// Skip-if-already-minted is intentional to make batch retries
/// idempotent (a script re-run after a partial tx doesn't fail
/// on the successfully-minted head of the batch).
function airdropMintBatch(
address[] calldata tos,
uint256[] calldata tokenIds,
GameTypes.Role[] calldata roles,
uint32[] calldata artSlots,
uint256[] calldata raidPowers
) external onlyOwner {
uint256 n = tos.length;
if (n == 0) revert InvalidBatchMint();
if (
tokenIds.length != n || roles.length != n || artSlots.length != n
|| raidPowers.length != n
) revert LengthMismatch();
for (uint256 i = 0; i < n; i++) {
uint256 id = tokenIds[i];
// Skip already-minted for batch idempotency.
if (_ownerOf(id) != address(0)) continue;
_airdropMintOne(tos[i], id, roles[i], artSlots[i], raidPowers[i]);
}
}
/// @notice Permanently close the airdrop mint path. Continuation mints
/// are unaffected — they were running from constructor time.
/// @dev Owner-only, one-shot. Requires the operator to pass the
/// expected total supply, so a fat-finger close-before-all-mints
/// reverts with a clear error instead of permanently locking the
/// airdrop path partway through. The operator computes this off
/// chain (== chibi count from the airdrop plan JSON, currently
/// 1905 on Base at snapshot) and passes it in. Rehearsal /
/// runbook enforce that this equals `plan.totals.chibiCount`.
/// @param expectedTotalSupply airdrop plan's chibi count. `totalSupply()`
/// must equal this exactly before the close is allowed.
function closeAirdrop(uint256 expectedTotalSupply) external onlyOwner {
if (airdropClosed) revert AirdropAlreadyClosed();
uint256 actual = airdropMintedCount;
if (actual != expectedTotalSupply) {
revert AirdropIncomplete(actual, expectedTotalSupply);
}
airdropClosed = true;
emit AirdropClosedEvent(MAX_AIRDROP_ID, _nextContinuationId);
}
// ─── Continuation mint (IChibiCore surface) ─────────────────────
function mint(address to, GameTypes.Role role) external onlyController returns (uint256 tokenId) {
return _continuationMint(to, role, 0, 0);
}
function mintWithArt(address to, GameTypes.Role role, uint32 artSlot)
external
onlyController
returns (uint256 tokenId)
{
return _continuationMint(to, role, artSlot, 0);
}
function batchMint(
address to,
GameTypes.Role[] calldata roles,
uint256[] calldata raidPowers,
uint32[] calldata artSlots
) external onlyController returns (uint256[] memory tokenIds) {
if (to == address(0)) revert ZeroAddress();
uint256 quantity = roles.length;
if (quantity == 0 || quantity != raidPowers.length || quantity != artSlots.length) {
revert InvalidBatchMint();
}
tokenIds = new uint256[](quantity);
for (uint256 i = 0; i < quantity; i++) {
tokenIds[i] = _continuationMint(to, roles[i], artSlots[i], raidPowers[i]);
}
}
function setRaidPower(uint256 tokenId, uint256 raidPower) external onlyController {
_requireMinted(tokenId);
_raidPower[tokenId] = raidPower;
emit RaidPowerSet(tokenId, raidPower);
}
function burn(uint256 tokenId) external onlyController {
_requireMinted(tokenId);
_burn(tokenId);
delete _roles[tokenId];
delete _artSlots[tokenId];
delete _raidPower[tokenId];
emit ChibiBurned(tokenId);
}
// ─── Owner admin ────────────────────────────────────────────────
function addController(address controller) external onlyOwner {
if (controller == address(0)) revert ZeroAddress();
_controllers[controller] = true;
emit ControllerAdded(controller);
}
function removeController(address controller) external onlyOwner {
if (controller == address(0)) revert ZeroAddress();
_controllers[controller] = false;
emit ControllerRemoved(controller);
}
function setBaseURI(string calldata newBaseURI) external onlyOwner {
_baseTokenURI = newBaseURI;
emit BaseURIUpdated(newBaseURI);
emit BatchMetadataUpdate(1, MAX_AIRDROP_ID);
}
function broadcastMetadataUpdate(uint256 fromTokenId, uint256 toTokenId) external onlyOwner {
if (fromTokenId == 0 || fromTokenId > toTokenId) revert InvalidTokenRange();
emit BatchMetadataUpdate(fromTokenId, toTokenId);
}
// ─── Internals ──────────────────────────────────────────────────
function _airdropMintOne(
address to,
uint256 tokenId,
GameTypes.Role role,
uint32 artSlot,
uint256 raidPower
) private {
if (airdropClosed) revert AirdropClosed();
if (to == address(0)) revert ZeroAddress();
if (tokenId == 0 || tokenId > MAX_AIRDROP_ID) revert InvalidTokenId();
if (_ownerOf(tokenId) != address(0)) revert TokenAlreadyMinted();
_roles[tokenId] = role;
_artSlots[tokenId] = artSlot;
if (raidPower > 0) {
_raidPower[tokenId] = raidPower;
emit RaidPowerSet(tokenId, raidPower);
}
_mint(to, tokenId);
unchecked { airdropMintedCount++; }
emit ChibiMinted(tokenId, to, role);
emit ArtSlotAssigned(tokenId, role, artSlot);
}
function _continuationMint(address to, GameTypes.Role role, uint32 artSlot, uint256 raidPower)
private
returns (uint256 tokenId)
{
if (to == address(0)) revert ZeroAddress();
tokenId = _nextContinuationId++;
_roles[tokenId] = role;
_artSlots[tokenId] = artSlot;
if (role == GameTypes.Role.Wolf && raidPower > 0) {
_raidPower[tokenId] = raidPower;
emit RaidPowerSet(tokenId, raidPower);
}
_safeMint(to, tokenId);
emit ChibiMinted(tokenId, to, role);
emit ArtSlotAssigned(tokenId, role, artSlot);
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
function _requireMinted(uint256 tokenId) private view {
if (_ownerOf(tokenId) == address(0)) revert TokenDoesNotExist();
}
function _requireNonZero(address account) private pure returns (address) {
if (account == address(0)) revert ZeroAddress();
return account;
}
}
Chain explorer1986msChain node76ms