Hoodyo

0xfdc0c0576adfa867103d0a552e1671c5486703cd

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
8,117 bytes
ABI entries
50
28 read · 14 write
License
none

Contract information

Address
0xfdc0c0576adfa867103d0a552e1671c5486703cd
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x46becf1f7b…5e724b591d

Token

Name
Hoodyo
Symbol
HOODYO
Decimals
Holders
1,339

Read contract (28)

IMAGES_CID()string
MAX_SUPPLY()uint256
METADATA_CID()string
ROYALTY_BPS()uint96
TIER1_COINS()uint256
TIER2_COINS()uint256
TIER3_COINS()uint256
allowanceOf(address)uint256
balanceOf(address)uint256
baseURI()string
claimableOf(address)uint256
claimed(address)uint256
coin()address
coinDecimals()uint8
coinLocked()bool
getApproved(uint256)address
isApprovedForAll(address, address)bool
metadataLocked()bool
mintOpen()bool
name()string
owner()address
ownerOf(uint256)address
royaltyInfo(uint256, uint256)address, uint256
royaltyReceiver()address
supportsInterface(bytes4)bool
symbol()string
tokenURI(uint256)string
totalSupply()uint256

Events (7)

ApprovalApprovalForAllClaimedCoinSetMintOpenSetOwnershipTransferredTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "baseURI_",
        "type": "string"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "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": "address",
        "name": "holder",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "firstId",
        "type": "uint256"
      }
    ],
    "name": "Claimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "coin",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "decimals",
        "type": "uint8"
      }
    ],
    "name": "CoinSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bool",
        "name": "open",
        "type": "bool"
      }
    ],
    "name": "MintOpenSet",
    "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": "IMAGES_CID",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "METADATA_CID",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROYALTY_BPS",
    "outputs": [
      {
        "internalType": "uint96",
        "name": "",
        "type": "uint96"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TIER1_COINS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TIER2_COINS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TIER3_COINS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "holder",
        "type": "address"
      }
    ],
    "name": "allowanceOf",
    "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": "holder",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "baseURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "claim",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "holder",
        "type": "address"
      }
    ],
    "name": "claimableOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "claimed",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "coin",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "coinDecimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "coinLocked",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "getApproved",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isApprovedForAll",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lockCoin",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lockMetadata",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "metadataLocked",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "mintOpen",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "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": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "ownerMint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ownerOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "salePrice",
        "type": "uint256"
      }
    ],
    "name": "royaltyInfo",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "royaltyReceiver",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "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": "baseURI_",
        "type": "string"
      }
    ],
    "name": "setBaseURI",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "coin_",
        "type": "address"
      }
    ],
    "name": "setCoin",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "open",
        "type": "bool"
      }
    ],
    "name": "setMintOpen",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "setRoyaltyReceiver",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "pure",
    "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": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "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": "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;

/// @title Hoodyo
/// @notice The culture coin and pfp of Robinhood Chain. 2000 Hoodyos, minted in
///         order starting at #1, art revealed instantly from IPFS.
///         Free claim for $Hoodyo coin holders:
///           hold at least 1 coin        -> mint 1
///           hold at least 2,500,000     -> mint 3
///           hold at least 5,000,000     -> mint 5
///         5% royalty on all secondary sales via ERC2981.
interface IERC20Like {
    function balanceOf(address account) external view returns (uint256);
    function decimals() external view returns (uint8);
}

interface IERC721TokenReceiver {
    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data)
        external
        returns (bytes4);
}

contract Hoodyo {
    // ------------------------------------------------------------------
    // Collection constants
    // ------------------------------------------------------------------
    string public constant name = "Hoodyo";
    string public constant symbol = "HOODYO";
    uint256 public constant MAX_SUPPLY = 2000;
    uint96 public constant ROYALTY_BPS = 500; // 5%

    /// @notice On chain provenance seal. IPFS CIDs are content hashes: these
    ///         constants commit this contract, forever, to the exact bytes of
    ///         all 2000 images and metadata files. Change one pixel anywhere
    ///         and the hash no longer matches. Once lockMetadata() is called
    ///         the pointer can never move either. The chain holds the
    ///         fingerprint of every Hoodyo, permanently.
    string public constant IMAGES_CID = "bafybeidgooynpbednxqqifrxqiicbxnji4qp7toerefxcphf7shbpi3gd4";
    string public constant METADATA_CID = "bafybeibebfg5utq6wsxdcocwmg7rsz2ktfbg72efs7zvhouojty2eteiki";

    // Claim tiers, denominated in whole coins (scaled by coin decimals when checked)
    uint256 public constant TIER1_COINS = 1;
    uint256 public constant TIER2_COINS = 2_500_000;
    uint256 public constant TIER3_COINS = 5_000_000;

    // ------------------------------------------------------------------
    // State
    // ------------------------------------------------------------------
    address public owner;
    address public royaltyReceiver;

    string public baseURI;
    bool public metadataLocked;

    address public coin;          // $Hoodyo ERC20, set once known
    uint8 public coinDecimals;
    bool public coinLocked;
    bool public mintOpen;

    uint256 public totalSupply;   // also the last minted token id (sequential from 1)
    mapping(address => uint256) public claimed;

    mapping(uint256 => address) private _ownerOf;
    mapping(address => uint256) private _balanceOf;
    mapping(uint256 => address) public getApproved;
    mapping(address => mapping(address => bool)) public isApprovedForAll;

    // ------------------------------------------------------------------
    // Events
    // ------------------------------------------------------------------
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    event CoinSet(address indexed coin, uint8 decimals);
    event MintOpenSet(bool open);
    event Claimed(address indexed holder, uint256 count, uint256 firstId);

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    constructor(string memory baseURI_) {
        owner = msg.sender;
        royaltyReceiver = msg.sender;
        baseURI = baseURI_;
    }

    // ------------------------------------------------------------------
    // Claim
    // ------------------------------------------------------------------

    /// @notice How many total mints a wallet qualifies for at its live coin balance.
    function allowanceOf(address holder) public view returns (uint256) {
        if (coin == address(0)) return 0;
        uint256 bal = IERC20Like(coin).balanceOf(holder);
        uint256 unit = 10 ** uint256(coinDecimals);
        if (bal >= TIER3_COINS * unit) return 5;
        if (bal >= TIER2_COINS * unit) return 3;
        if (bal >= TIER1_COINS * unit) return 1;
        return 0;
    }

    /// @notice Mints remaining for a wallet right now.
    function claimableOf(address holder) external view returns (uint256) {
        uint256 allowed = allowanceOf(holder);
        uint256 used = claimed[holder];
        if (used >= allowed) return 0;
        uint256 left = allowed - used;
        uint256 supplyLeft = MAX_SUPPLY - totalSupply;
        return left < supplyLeft ? left : supplyLeft;
    }

    /// @notice Free claim. Holder pays only gas. Mints sequentially from #1.
    function claim(uint256 count) external {
        require(mintOpen, "mint not open");
        require(count > 0, "zero count");
        require(totalSupply + count <= MAX_SUPPLY, "sold out");
        uint256 allowed = allowanceOf(msg.sender);
        require(claimed[msg.sender] + count <= allowed, "exceeds allowance");
        claimed[msg.sender] += count;

        uint256 firstId = totalSupply + 1;
        for (uint256 i = 0; i < count; i++) {
            uint256 id = firstId + i;
            _ownerOf[id] = msg.sender;
            emit Transfer(address(0), msg.sender, id);
        }
        _balanceOf[msg.sender] += count;
        totalSupply += count;
        emit Claimed(msg.sender, count, firstId);
    }

    /// @notice Owner mint for treasury / marketing, obeys max supply.
    function ownerMint(address to, uint256 count) external onlyOwner {
        require(to != address(0), "zero address");
        require(count > 0, "zero count");
        require(totalSupply + count <= MAX_SUPPLY, "sold out");
        uint256 firstId = totalSupply + 1;
        for (uint256 i = 0; i < count; i++) {
            uint256 id = firstId + i;
            _ownerOf[id] = to;
            emit Transfer(address(0), to, id);
        }
        _balanceOf[to] += count;
        totalSupply += count;
    }

    // ------------------------------------------------------------------
    // Admin
    // ------------------------------------------------------------------
    function setCoin(address coin_) external onlyOwner {
        require(!coinLocked, "coin locked");
        require(coin_ != address(0), "zero address");
        coin = coin_;
        uint8 dec = 18;
        try IERC20Like(coin_).decimals() returns (uint8 d) {
            dec = d;
        } catch {}
        coinDecimals = dec;
        emit CoinSet(coin_, dec);
    }

    function lockCoin() external onlyOwner {
        require(coin != address(0), "coin not set");
        coinLocked = true;
    }

    function setMintOpen(bool open) external onlyOwner {
        mintOpen = open;
        emit MintOpenSet(open);
    }

    function setBaseURI(string calldata baseURI_) external onlyOwner {
        require(!metadataLocked, "metadata locked");
        baseURI = baseURI_;
    }

    function lockMetadata() external onlyOwner {
        metadataLocked = true;
    }

    function setRoyaltyReceiver(address receiver) external onlyOwner {
        require(receiver != address(0), "zero address");
        royaltyReceiver = receiver;
    }

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "zero address");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }

    // ------------------------------------------------------------------
    // ERC721
    // ------------------------------------------------------------------
    function ownerOf(uint256 tokenId) public view returns (address) {
        address holder = _ownerOf[tokenId];
        require(holder != address(0), "nonexistent token");
        return holder;
    }

    function balanceOf(address holder) external view returns (uint256) {
        require(holder != address(0), "zero address");
        return _balanceOf[holder];
    }

    function tokenURI(uint256 tokenId) external view returns (string memory) {
        require(_ownerOf[tokenId] != address(0), "nonexistent token");
        return string(abi.encodePacked(baseURI, _toString(tokenId), ".json"));
    }

    function approve(address to, uint256 tokenId) external {
        address holder = ownerOf(tokenId);
        require(msg.sender == holder || isApprovedForAll[holder][msg.sender], "not authorized");
        getApproved[tokenId] = to;
        emit Approval(holder, to, tokenId);
    }

    function setApprovalForAll(address operator, bool approved) external {
        isApprovedForAll[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    function transferFrom(address from, address to, uint256 tokenId) public {
        address holder = ownerOf(tokenId);
        require(holder == from, "wrong from");
        require(to != address(0), "zero address");
        require(
            msg.sender == holder || msg.sender == getApproved[tokenId] || isApprovedForAll[holder][msg.sender],
            "not authorized"
        );
        delete getApproved[tokenId];
        _ownerOf[tokenId] = to;
        _balanceOf[from] -= 1;
        _balanceOf[to] += 1;
        emit Transfer(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId) external {
        safeTransferFrom(from, to, tokenId, "");
    }

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public {
        transferFrom(from, to, tokenId);
        if (to.code.length > 0) {
            require(
                IERC721TokenReceiver(to).onERC721Received(msg.sender, from, tokenId, data)
                    == IERC721TokenReceiver.onERC721Received.selector,
                "unsafe recipient"
            );
        }
    }

    // ------------------------------------------------------------------
    // ERC2981 royalties: 5% on every secondary sale
    // ------------------------------------------------------------------
    function royaltyInfo(uint256, uint256 salePrice) external view returns (address, uint256) {
        return (royaltyReceiver, (salePrice * ROYALTY_BPS) / 10_000);
    }

    // ------------------------------------------------------------------
    // ERC165
    // ------------------------------------------------------------------
    function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
        return interfaceId == 0x01ffc9a7 // ERC165
            || interfaceId == 0x80ac58cd // ERC721
            || interfaceId == 0x5b5e139f // ERC721Metadata
            || interfaceId == 0x2a55205a; // ERC2981
    }

    // ------------------------------------------------------------------
    // Internals
    // ------------------------------------------------------------------
    function _toString(uint256 value) internal pure returns (string memory) {
        if (value == 0) return "0";
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + (value % 10)));
            value /= 10;
        }
        return string(buffer);
    }
}
Chain explorer4558msChain node79ms