KeysOfSherwood

0x02312c3bff61d8d36c6355df1196efbd1a58ed1b

Verification
Verified
v0.8.34+commit.80d5c536
Type
Contract
18,126 bytes
ABI entries
53
20 read · 12 write
License
none

Contract information

Address
0x02312c3bff61d8d36c6355df1196efbd1a58ed1b
Chain
Robinhood Chain (4663)
Compiler
v0.8.34+commit.80d5c536
Optimization
Disabled
Creation tx
0x8a2012a40b…83643a9ddc

Token

Name
Keys of Sherwood
Symbol
KEYS
Decimals
Holders
3,064

Read contract (20)

BRONZE_END()uint256
BRONZE_START()uint256
GOLD_END()uint256
GOLD_START()uint256
MAX_SUPPLY()uint256
SILVER_END()uint256
SILVER_START()uint256
balanceOf(address)uint256
getApproved(uint256)address
imagesCID()string
isApprovedForAll(address, address)bool
name()string
owner()address
ownerOf(uint256)address
royaltyInfo(uint256, uint256)address, uint256
supportsInterface(bytes4)bool
symbol()string
tierName(uint256)string
tierOf(uint256)uint8
tokenURI(uint256)string

Events (6)

AirdroppedApprovalApprovalForAllBatchMintedOwnershipTransferredTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "_imagesCID",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "royaltyReceiver",
        "type": "address"
      },
      {
        "internalType": "uint96",
        "name": "royaltyFeeBps",
        "type": "uint96"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "numerator",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "denominator",
        "type": "uint256"
      }
    ],
    "name": "ERC2981InvalidDefaultRoyalty",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC2981InvalidDefaultRoyaltyReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "numerator",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "denominator",
        "type": "uint256"
      }
    ],
    "name": "ERC2981InvalidTokenRoyalty",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC2981InvalidTokenRoyaltyReceiver",
    "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": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "Airdropped",
    "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": false,
        "internalType": "uint256",
        "name": "startId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "BatchMinted",
    "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": "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": "BRONZE_END",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "BRONZE_START",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GOLD_END",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GOLD_START",
    "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": "SILVER_END",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SILVER_START",
    "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": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "recipients",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "tokenIds",
        "type": "uint256[]"
      }
    ],
    "name": "batchAirdrop",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "startId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "batchMintToSelf",
    "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": [],
    "name": "imagesCID",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "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": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "mint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "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": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "salePrice",
        "type": "uint256"
      }
    ],
    "name": "royaltyInfo",
    "outputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "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": "address",
        "name": "receiver",
        "type": "address"
      },
      {
        "internalType": "uint96",
        "name": "feeBps",
        "type": "uint96"
      }
    ],
    "name": "setDefaultRoyalty",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "newCID",
        "type": "string"
      }
    ],
    "name": "setImagesCID",
    "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": "tierName",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "tierOf",
    "outputs": [
      {
        "internalType": "enum KeysOfSherwood.Tier",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "pure",
    "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.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

/// @title Keys of Sherwood
/// @notice 3,333 transferable membership keys (Bronze / Silver / Gold),
///         minted by the owner and distributed via manual airdrop.
contract KeysOfSherwood is ERC721, ERC2981, Ownable {
    // ---------------------------------------------------------------
    // Supply configuration
    // ---------------------------------------------------------------
    uint256 public constant MAX_SUPPLY = 3333;

    // TokenId ranges (inclusive), 1-indexed
    uint256 public constant BRONZE_START = 1;
    uint256 public constant BRONZE_END   = 2333;   // 2,333 keys (70%)
    uint256 public constant SILVER_START = 2334;
    uint256 public constant SILVER_END   = 3000;   // 667 keys (20%)
    uint256 public constant GOLD_START   = 3001;
    uint256 public constant GOLD_END     = 3333;   // 333 keys (10%)

    enum Tier { Bronze, Silver, Gold }

    // IPFS CID of the folder containing bronze_key.jpg / silver_key.jpg / gold_key.jpg
    // e.g. "bafybeifs5qyqstujtdoxy2rilhk2beqwowfrtm7orjcnkejsjwkexxmkxq"
    string public imagesCID;

    // Tracks which tokenIds have already been minted, to prevent double mint
    mapping(uint256 => bool) private _minted;

    event BatchMinted(uint256 startId, uint256 count, address indexed to);
    event Airdropped(uint256 indexed tokenId, address indexed to);

    constructor(string memory _imagesCID, address royaltyReceiver, uint96 royaltyFeeBps)
        ERC721("Keys of Sherwood", "KEYS")
        Ownable(msg.sender)
    {
        imagesCID = _imagesCID;
        // e.g. royaltyFeeBps = 500 -> 5% secondary royalties
        _setDefaultRoyalty(royaltyReceiver, royaltyFeeBps);
    }

    function setImagesCID(string calldata newCID) external onlyOwner {
        imagesCID = newCID;
    }

    // ---------------------------------------------------------------
    // Tier lookup
    // ---------------------------------------------------------------
    function tierOf(uint256 tokenId) public pure returns (Tier) {
        require(tokenId >= BRONZE_START && tokenId <= GOLD_END, "Invalid tokenId");
        if (tokenId <= BRONZE_END) return Tier.Bronze;
        if (tokenId <= SILVER_END) return Tier.Silver;
        return Tier.Gold;
    }

    function tierName(uint256 tokenId) external pure returns (string memory) {
        Tier t = tierOf(tokenId);
        if (t == Tier.Bronze) return "Bronze Key";
        if (t == Tier.Silver) return "Silver Key";
        return "Gold Key";
    }

    // ---------------------------------------------------------------
    // Minting (owner-only, mints straight to your own wallet first)
    // ---------------------------------------------------------------

    /// @notice Mint a single specific tokenId to an address (usually yourself).
    function mint(address to, uint256 tokenId) public onlyOwner {
        require(tokenId >= BRONZE_START && tokenId <= GOLD_END, "tokenId out of range");
        require(!_minted[tokenId], "Already minted");
        _minted[tokenId] = true;
        _safeMint(to, tokenId);
    }

    /// @notice Mint a contiguous range of tokenIds to a single address (e.g. yourself).
    /// @dev Use this to pre-mint all 3,333 keys to your own wallet before airdropping.
    function batchMintToSelf(uint256 startId, uint256 count) external onlyOwner {
        require(startId >= BRONZE_START, "start below range");
        require(startId + count - 1 <= GOLD_END, "exceeds max supply");
        for (uint256 i = 0; i < count; i++) {
            uint256 tokenId = startId + i;
            require(!_minted[tokenId], "Already minted");
            _minted[tokenId] = true;
            _safeMint(msg.sender, tokenId);
        }
        emit BatchMinted(startId, count, msg.sender);
    }

    // ---------------------------------------------------------------
    // Airdrop helper (owner sends already-minted keys out to holders)
    // ---------------------------------------------------------------

    /// @notice Send previously minted tokenIds (currently owned by you) to recipients.
    /// @dev recipients[i] receives tokenIds[i]. Arrays must be the same length.
    ///      Call this after batchMintToSelf, using your wallet-to-tier CSV.
    function batchAirdrop(address[] calldata recipients, uint256[] calldata tokenIds)
        external
        onlyOwner
    {
        require(recipients.length == tokenIds.length, "Length mismatch");
        for (uint256 i = 0; i < recipients.length; i++) {
            safeTransferFrom(msg.sender, recipients[i], tokenIds[i]);
            emit Airdropped(tokenIds[i], recipients[i]);
        }
    }

    // ---------------------------------------------------------------
    // Metadata (generated fully on-chain — no JSON files needed anywhere)
    // ---------------------------------------------------------------

    function _tierInfo(Tier t)
        internal
        pure
        returns (
            string memory name_,
            string memory metal,
            string memory gem,
            string memory imageFile,
            string memory description,
            uint256 tierSupply
        )
    {
        if (t == Tier.Bronze) {
            return (
                "Bronze Key",
                "Bronze",
                "Smoky Topaz",
                "bronze_key.jpg",
                "A Bronze Key of Sherwood - your first step into the band. Forged in the outlaw's hideout, it unlocks lifetime allowlist access, a share of secondary royalties, and priority on every future Sherwood airdrop.",
                BRONZE_END - BRONZE_START + 1
            );
        } else if (t == Tier.Silver) {
            return (
                "Silver Key",
                "Silver",
                "Pale Sapphire",
                "silver_key.jpg",
                "A Silver Key of Sherwood - proof you've fought alongside the band, not just watched from the treeline. Unlocks lifetime allowlist access, a share of secondary royalties, and priority on every future Sherwood airdrop.",
                SILVER_END - SILVER_START + 1
            );
        } else {
            return (
                "Gold Key",
                "Gold",
                "Emerald",
                "gold_key.jpg",
                "A Gold Key of Sherwood - the rarest trust Robin Hood gives. Grants a seat at the War Council, lifetime allowlist access, a share of secondary royalties, and first priority on every future Sherwood airdrop.",
                GOLD_END - GOLD_START + 1
            );
        }
    }

    function _positionInTier(uint256 tokenId, Tier t) internal pure returns (uint256) {
        if (t == Tier.Bronze) return tokenId - BRONZE_START + 1;
        if (t == Tier.Silver) return tokenId - SILVER_START + 1;
        return tokenId - GOLD_START + 1;
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_ownerOf(tokenId) != address(0), "Token does not exist");

        Tier t = tierOf(tokenId);
        (
            string memory name_,
            string memory metal,
            string memory gem,
            string memory imageFile,
            string memory description,
            uint256 tierSupply
        ) = _tierInfo(t);

        uint256 position = _positionInTier(tokenId, t);

        string memory json = string(
            abi.encodePacked(
                '{"name":"', name_, ' #', Strings.toString(position), '",',
                '"description":"', description, '",',
                '"image":"ipfs://', imagesCID, '/', imageFile, '",',
                '"attributes":[',
                    '{"trait_type":"Collection","value":"Keys of Sherwood"},',
                    '{"trait_type":"Tier","value":"', name_, '"},',
                    '{"trait_type":"Metal","value":"', metal, '"},',
                    '{"trait_type":"Gem","value":"', gem, '"},',
                    '{"trait_type":"Tier Supply","value":"', Strings.toString(tierSupply), '"}',
                ']}'
            )
        );

        return string(
            abi.encodePacked(
                "data:application/json;base64,",
                Base64.encode(bytes(json))
            )
        );
    }

    // ---------------------------------------------------------------
    // Royalties (ERC-2981) - lets marketplaces pay secondary royalties
    // ---------------------------------------------------------------
    function setDefaultRoyalty(address receiver, uint96 feeBps) external onlyOwner {
        _setDefaultRoyalty(receiver, feeBps);
    }

    // Required override for multiple inheritance (ERC721 + ERC2981)
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC2981)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
Chain explorer2985msChain node77ms