HoodPunks
0xceeb3ddfbfdc1bfc485468371093eb851f90aa6a
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
16,020 bytes
ABI entries
66
33 read · 22 write
License
none
Contract information
- Address
- 0xceeb3ddfbfdc1bfc485468371093eb851f90aa6a
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x2dDb1f1fC3…53c6D62924
- Creation tx
- 0x6b61d3c226…97f3dd9493
Token
- Name
- Robinhood Punks
- Symbol
- HOODPUNKS
- Decimals
- —
- Holders
- 1,470
- Market
- View token page →
Read contract (33)
DEV_RESERVE_MAX() → uint256
MAX_SUPPLY() → uint256
ROYALTY_BPS() → uint256
balanceOf(address) → uint256
bidsPage(uint256, uint256) → bool[], uint256[], address[]
claimAllowance(address) → uint256
claimedCount(address) → uint256
claimsLeft(address) → uint256
claimsOpen() → bool
coin() → address
coinLocked() → bool
coinUnit() → uint256
data() → address
devReserved() → uint256
exists(uint256) → bool
getApproved(uint256) → address
isApprovedForAll(address, address) → bool
migrationLocked() → bool
name() → string
offersPage(uint256, uint256) → bool[], uint256[], address[]
owner() → address
ownerOf(uint256) → address
ownersPage(uint256, uint256) → address[]
pendingWithdrawals(address) → uint256
punkBids(uint256) → bool, address, uint256
punkOffers(uint256) → bool, address, uint256, address
punkSvg(uint256) → string
royaltyInfo(uint256, uint256) → address, uint256
royaltyReceiver() → address
supportsInterface(bytes4) → bool
symbol() → string
tokenURI(uint256) → string
totalSupply() → uint256
Events (10)
ApprovalApprovalForAllClaimsOpenSetPunkBidEnteredPunkBidWithdrawnPunkBoughtPunkClaimedPunkNoLongerForSalePunkOfferedTransfer
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "dataContract",
"type": "address"
},
{
"internalType": "address",
"name": "coinAddress",
"type": "address"
}
],
"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": false,
"internalType": "bool",
"name": "open",
"type": "bool"
}
],
"name": "ClaimsOpenSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "fromAddress",
"type": "address"
}
],
"name": "PunkBidEntered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "fromAddress",
"type": "address"
}
],
"name": "PunkBidWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "fromAddress",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "toAddress",
"type": "address"
}
],
"name": "PunkBought",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "claimer",
"type": "address"
}
],
"name": "PunkClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
}
],
"name": "PunkNoLongerForSale",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "minValue",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "toAddress",
"type": "address"
}
],
"name": "PunkOffered",
"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": "DEV_RESERVE_MAX",
"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": "ROYALTY_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minPrice",
"type": "uint256"
}
],
"name": "acceptBidForPunk",
"outputs": [],
"stateMutability": "nonpayable",
"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": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "start",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "count",
"type": "uint256"
}
],
"name": "bidsPage",
"outputs": [
{
"internalType": "bool[]",
"name": "hasBid",
"type": "bool[]"
},
{
"internalType": "uint256[]",
"name": "values",
"type": "uint256[]"
},
{
"internalType": "address[]",
"name": "bidders",
"type": "address[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
}
],
"name": "buyPunk",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16[]",
"name": "punkIds",
"type": "uint16[]"
}
],
"name": "claim",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "wallet",
"type": "address"
}
],
"name": "claimAllowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "claimedCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "wallet",
"type": "address"
}
],
"name": "claimsLeft",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "claimsOpen",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "coin",
"outputs": [
{
"internalType": "contract IERC20Like",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "coinLocked",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "coinUnit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "data",
"outputs": [
{
"internalType": "contract HoodPunksData",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "devReserved",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
}
],
"name": "enterBidForPunk",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "exists",
"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": "lockMigration",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16[]",
"name": "punkIds",
"type": "uint16[]"
},
{
"internalType": "address[]",
"name": "owners_",
"type": "address[]"
}
],
"name": "migrateClaims",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "migrationLocked",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minSalePriceInWei",
"type": "uint256"
}
],
"name": "offerPunkForSale",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minSalePriceInWei",
"type": "uint256"
},
{
"internalType": "address",
"name": "toAddress",
"type": "address"
}
],
"name": "offerPunkForSaleTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "start",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "count",
"type": "uint256"
}
],
"name": "offersPage",
"outputs": [
{
"internalType": "bool[]",
"name": "forSale",
"type": "bool[]"
},
{
"internalType": "uint256[]",
"name": "prices",
"type": "uint256[]"
},
{
"internalType": "address[]",
"name": "sellers",
"type": "address[]"
}
],
"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": "o",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16[]",
"name": "punkIds",
"type": "uint16[]"
}
],
"name": "ownerReserve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "start",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "count",
"type": "uint256"
}
],
"name": "ownersPage",
"outputs": [
{
"internalType": "address[]",
"name": "out",
"type": "address[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "pendingWithdrawals",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "punkBids",
"outputs": [
{
"internalType": "bool",
"name": "hasBid",
"type": "bool"
},
{
"internalType": "address",
"name": "bidder",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
}
],
"name": "punkNoLongerForSale",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "punkOffers",
"outputs": [
{
"internalType": "bool",
"name": "isForSale",
"type": "bool"
},
{
"internalType": "address",
"name": "seller",
"type": "address"
},
{
"internalType": "uint256",
"name": "minValue",
"type": "uint256"
},
{
"internalType": "address",
"name": "onlySellTo",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
}
],
"name": "punkSvg",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"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": "dataArg",
"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": "bool",
"name": "open",
"type": "bool"
}
],
"name": "setClaimsOpen",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "coinAddress",
"type": "address"
}
],
"name": "setCoin",
"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"
},
{
"inputs": [],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "punkId",
"type": "uint256"
}
],
"name": "withdrawBidForPunk",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {HoodPunksData} from "./HoodPunksData.sol";
interface IERC20Like {
function balanceOf(address account) external view returns (uint256);
function decimals() external view returns (uint8);
}
interface IERC721Receiver {
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data)
external returns (bytes4);
}
library Base64 {
bytes internal constant TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function encode(bytes memory data) internal pure returns (string memory) {
if (data.length == 0) return "";
string memory table = string(TABLE);
uint256 encodedLen = 4 * ((data.length + 2) / 3);
string memory result = new string(encodedLen + 32);
assembly {
let tablePtr := add(table, 1)
let resultPtr := add(result, 32)
for { let i := 0 } lt(i, mload(data)) { i := add(i, 3) } {
let input := and(mload(add(data, add(32, i))), 0xffffff0000000000000000000000000000000000000000000000000000000000)
input := shr(232, input)
mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F))))
resultPtr := add(resultPtr, 1)
mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F))))
resultPtr := add(resultPtr, 1)
mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F))))
resultPtr := add(resultPtr, 1)
mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F))))
resultPtr := add(resultPtr, 1)
}
switch mod(mload(data), 3)
case 1 {
mstore8(sub(resultPtr, 1), 0x3d)
mstore8(sub(resultPtr, 2), 0x3d)
}
case 2 { mstore8(sub(resultPtr, 1), 0x3d) }
mstore(result, encodedLen)
}
return result;
}
}
/// @title HoodPunks
/// @notice 2000 fully on-chain pixel punks on Robinhood Chain. Free claim,
/// gated by coin holdings, with a CryptoPunks style native
/// marketplace (offers, bids, buys in ETH) built into the token.
contract HoodPunks {
// ------------------------------------------------------------------
// ERC721 core
// ------------------------------------------------------------------
string public constant name = "Robinhood Punks";
string public constant symbol = "HOODPUNKS";
uint256 public constant MAX_SUPPLY = 2000;
mapping(uint256 => address) internal _owners;
mapping(address => uint256) internal _balances;
mapping(uint256 => address) public getApproved;
mapping(address => mapping(address => bool)) public isApprovedForAll;
uint256 public totalSupply;
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);
// ------------------------------------------------------------------
// Project state
// ------------------------------------------------------------------
HoodPunksData public immutable data;
IERC20Like public coin;
uint256 public coinUnit; // 10 ** coin decimals
address public owner;
bool public claimsOpen;
bool public coinLocked;
uint256 public constant DEV_RESERVE_MAX = 200;
uint256 public devReserved;
/// 2% creator royalty on native marketplace sales, ERC2981 for external ones.
uint256 public constant ROYALTY_BPS = 200;
address public royaltyReceiver = 0x2dDb1f1fC31b4495dA195F422b215053c6D62924;
/// one time migration hatch to honor claims made on a prior contract
bool public migrationLocked;
mapping(address => uint256) public claimedCount;
event PunkClaimed(uint256 indexed punkId, address indexed claimer);
event ClaimsOpenSet(bool open);
// ------------------------------------------------------------------
// Native marketplace (CryptoPunks style, pull payments)
// ------------------------------------------------------------------
struct Offer {
bool isForSale;
address seller;
uint256 minValue; // in wei (ETH is the gas coin on Robinhood Chain)
address onlySellTo; // zero address = anyone
}
struct Bid {
bool hasBid;
address bidder;
uint256 value;
}
mapping(uint256 => Offer) public punkOffers;
mapping(uint256 => Bid) public punkBids;
mapping(address => uint256) public pendingWithdrawals;
event PunkOffered(uint256 indexed punkId, uint256 minValue, address indexed toAddress);
event PunkNoLongerForSale(uint256 indexed punkId);
event PunkBidEntered(uint256 indexed punkId, uint256 value, address indexed fromAddress);
event PunkBidWithdrawn(uint256 indexed punkId, uint256 value, address indexed fromAddress);
event PunkBought(uint256 indexed punkId, uint256 value, address indexed fromAddress, address indexed toAddress);
uint256 private _reentrancy = 1;
modifier nonReentrant() {
require(_reentrancy == 1, "reentrant");
_reentrancy = 2;
_;
_reentrancy = 1;
}
modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }
constructor(address dataContract, address coinAddress) {
owner = msg.sender;
data = HoodPunksData(dataContract);
if (coinAddress != address(0)) _setCoin(coinAddress);
}
// ------------------------------------------------------------------
// Admin
// ------------------------------------------------------------------
function transferOwnership(address newOwner) external onlyOwner { owner = newOwner; }
/// @notice Point at the coin used for claim gating. Lock it once live.
function setCoin(address coinAddress) external onlyOwner {
require(!coinLocked, "coin locked");
_setCoin(coinAddress);
}
function lockCoin() external onlyOwner {
require(address(coin) != address(0), "no coin");
coinLocked = true;
}
function _setCoin(address coinAddress) internal {
coin = IERC20Like(coinAddress);
uint8 dec = 18;
(bool ok, bytes memory ret) =
coinAddress.staticcall(abi.encodeWithSignature("decimals()"));
if (ok && ret.length >= 32) dec = uint8(abi.decode(ret, (uint256)));
coinUnit = 10 ** dec;
}
function setClaimsOpen(bool open) external onlyOwner {
require(address(coin) != address(0), "no coin");
claimsOpen = open;
emit ClaimsOpenSet(open);
}
function setRoyaltyReceiver(address receiver) external onlyOwner {
require(receiver != address(0), "zero address");
royaltyReceiver = receiver;
}
/// @notice Re-mint punks claimed on a prior contract to their rightful
/// owners, crediting their claim counts. Locked before claims open.
function migrateClaims(uint16[] calldata punkIds, address[] calldata owners_) external onlyOwner {
require(!migrationLocked, "migration locked");
require(punkIds.length == owners_.length, "length mismatch");
for (uint256 i = 0; i < punkIds.length; i++) {
claimedCount[owners_[i]] += 1;
emit PunkClaimed(punkIds[i], owners_[i]);
_mint(owners_[i], punkIds[i]);
}
}
function lockMigration() external onlyOwner { migrationLocked = true; }
/// @notice Team reserve, hard capped at DEV_RESERVE_MAX punks.
function ownerReserve(uint16[] calldata punkIds) external onlyOwner {
require(devReserved + punkIds.length <= DEV_RESERVE_MAX, "reserve cap");
devReserved += punkIds.length;
for (uint256 i = 0; i < punkIds.length; i++) _mint(msg.sender, punkIds[i]);
}
// ------------------------------------------------------------------
// Claim: hold the coin, pick your punks
// ------------------------------------------------------------------
/// @notice How many punks a wallet may claim in total, from live coin balance.
/// 100k => 1, 500k => 2, 1.5M => 3, 3M => 4, 5M+ => 5.
function claimAllowance(address wallet) public view returns (uint256) {
if (address(coin) == address(0)) return 0;
uint256 bal = coin.balanceOf(wallet);
if (bal >= 5_000_000 * coinUnit) return 5;
if (bal >= 3_000_000 * coinUnit) return 4;
if (bal >= 1_500_000 * coinUnit) return 3;
if (bal >= 500_000 * coinUnit) return 2;
if (bal >= 100_000 * coinUnit) return 1;
return 0;
}
function claimsLeft(address wallet) public view returns (uint256) {
uint256 allowance = claimAllowance(wallet);
uint256 used = claimedCount[wallet];
return allowance > used ? allowance - used : 0;
}
/// @notice Claim specific punks by id. Free, gas only.
function claim(uint16[] calldata punkIds) external {
require(claimsOpen, "claims closed");
require(punkIds.length > 0, "no punks");
require(punkIds.length <= claimsLeft(msg.sender), "over allowance");
claimedCount[msg.sender] += punkIds.length;
for (uint256 i = 0; i < punkIds.length; i++) {
emit PunkClaimed(punkIds[i], msg.sender);
_mint(msg.sender, punkIds[i]);
}
}
// ------------------------------------------------------------------
// ERC721 logic
// ------------------------------------------------------------------
function ownerOf(uint256 tokenId) public view returns (address o) {
o = _owners[tokenId];
require(o != address(0), "not minted");
}
function balanceOf(address account) external view returns (uint256) {
require(account != address(0), "zero address");
return _balances[account];
}
function exists(uint256 tokenId) public view returns (bool) {
return _owners[tokenId] != address(0);
}
function _mint(address to, uint256 tokenId) internal {
require(tokenId < MAX_SUPPLY, "bad punk");
require(_owners[tokenId] == address(0), "already minted");
_owners[tokenId] = to;
_balances[to] += 1;
totalSupply += 1;
emit Transfer(address(0), to, tokenId);
}
function approve(address to, uint256 tokenId) external {
address o = ownerOf(tokenId);
require(msg.sender == o || isApprovedForAll[o][msg.sender], "not authorized");
getApproved[tokenId] = to;
emit Approval(o, 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 o = ownerOf(tokenId);
require(o == from, "wrong from");
require(to != address(0), "zero address");
require(
msg.sender == o || msg.sender == getApproved[tokenId] || isApprovedForAll[o][msg.sender],
"not authorized"
);
_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 dataArg) public {
transferFrom(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, dataArg), "unsafe receiver");
}
function _transfer(address from, address to, uint256 tokenId) internal {
delete getApproved[tokenId];
// an offer made by the previous owner is no longer valid
if (punkOffers[tokenId].isForSale) {
punkOffers[tokenId] = Offer(false, address(0), 0, address(0));
emit PunkNoLongerForSale(tokenId);
}
_owners[tokenId] = to;
_balances[from] -= 1;
_balances[to] += 1;
emit Transfer(from, to, tokenId);
// if the new owner had a live bid on this punk, refund it to them
Bid memory bid = punkBids[tokenId];
if (bid.hasBid && bid.bidder == to) {
punkBids[tokenId] = Bid(false, address(0), 0);
pendingWithdrawals[to] += bid.value;
}
}
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory dataArg)
private returns (bool)
{
if (to.code.length == 0) return true;
try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, dataArg) returns (bytes4 ret) {
return ret == IERC721Receiver.onERC721Received.selector;
} catch {
return false;
}
}
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return interfaceId == 0x80ac58cd // ERC721
|| interfaceId == 0x5b5e139f // ERC721Metadata
|| interfaceId == 0x2a55205a // ERC2981 royalties
|| interfaceId == 0x01ffc9a7; // ERC165
}
/// @notice ERC2981: external marketplaces honor the same 2% royalty.
function royaltyInfo(uint256, uint256 salePrice) external view returns (address, uint256) {
return (royaltyReceiver, (salePrice * ROYALTY_BPS) / 10_000);
}
// ------------------------------------------------------------------
// Marketplace
// ------------------------------------------------------------------
function offerPunkForSale(uint256 punkId, uint256 minSalePriceInWei) external {
require(ownerOf(punkId) == msg.sender, "not your punk");
punkOffers[punkId] = Offer(true, msg.sender, minSalePriceInWei, address(0));
emit PunkOffered(punkId, minSalePriceInWei, address(0));
}
function offerPunkForSaleTo(uint256 punkId, uint256 minSalePriceInWei, address toAddress) external {
require(ownerOf(punkId) == msg.sender, "not your punk");
punkOffers[punkId] = Offer(true, msg.sender, minSalePriceInWei, toAddress);
emit PunkOffered(punkId, minSalePriceInWei, toAddress);
}
function punkNoLongerForSale(uint256 punkId) external {
require(ownerOf(punkId) == msg.sender, "not your punk");
punkOffers[punkId] = Offer(false, address(0), 0, address(0));
emit PunkNoLongerForSale(punkId);
}
function buyPunk(uint256 punkId) external payable nonReentrant {
Offer memory offer = punkOffers[punkId];
require(offer.isForSale, "not for sale");
require(offer.onlySellTo == address(0) || offer.onlySellTo == msg.sender, "reserved sale");
require(msg.value >= offer.minValue, "underpaid");
address seller = offer.seller;
require(seller == ownerOf(punkId), "stale offer");
require(msg.sender != seller, "own punk");
_transfer(seller, msg.sender, punkId);
uint256 royalty = (msg.value * ROYALTY_BPS) / 10_000;
pendingWithdrawals[royaltyReceiver] += royalty;
pendingWithdrawals[seller] += msg.value - royalty;
emit PunkBought(punkId, msg.value, seller, msg.sender);
}
function enterBidForPunk(uint256 punkId) external payable nonReentrant {
require(exists(punkId), "not minted");
require(ownerOf(punkId) != msg.sender, "own punk");
require(msg.value > 0, "zero bid");
Bid memory existing = punkBids[punkId];
require(msg.value > existing.value, "bid too low");
if (existing.value > 0) {
pendingWithdrawals[existing.bidder] += existing.value;
}
punkBids[punkId] = Bid(true, msg.sender, msg.value);
emit PunkBidEntered(punkId, msg.value, msg.sender);
}
function withdrawBidForPunk(uint256 punkId) external nonReentrant {
Bid memory bid = punkBids[punkId];
require(bid.hasBid && bid.bidder == msg.sender, "no bid");
punkBids[punkId] = Bid(false, address(0), 0);
emit PunkBidWithdrawn(punkId, bid.value, msg.sender);
(bool sent, ) = msg.sender.call{value: bid.value}("");
require(sent, "send failed");
}
function acceptBidForPunk(uint256 punkId, uint256 minPrice) external nonReentrant {
require(ownerOf(punkId) == msg.sender, "not your punk");
Bid memory bid = punkBids[punkId];
require(bid.hasBid && bid.value > 0, "no bid");
require(bid.value >= minPrice, "bid below min");
punkBids[punkId] = Bid(false, address(0), 0);
_transfer(msg.sender, bid.bidder, punkId);
uint256 royalty = (bid.value * ROYALTY_BPS) / 10_000;
pendingWithdrawals[royaltyReceiver] += royalty;
pendingWithdrawals[msg.sender] += bid.value - royalty;
emit PunkBought(punkId, bid.value, msg.sender, bid.bidder);
}
function withdraw() external nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "nothing to withdraw");
pendingWithdrawals[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "send failed");
}
// ------------------------------------------------------------------
// Metadata, fully on-chain
// ------------------------------------------------------------------
function punkSvg(uint256 punkId) external view returns (string memory) {
return data.punkSvg(punkId);
}
function tokenURI(uint256 tokenId) external view returns (string memory) {
require(tokenId < MAX_SUPPLY, "bad punk");
string memory image = Base64.encode(bytes(data.punkSvg(tokenId)));
bytes memory json = abi.encodePacked(
'{"name":"Robinhood Punk #', _u2s(tokenId),
'","description":"Robinhood Punks: 2000 pixel punks living fully on chain on Robinhood Chain, claimed free by holders of $PUNKS. Art, metadata and marketplace all inside the contract.",',
'"attributes":', data.punkAttributesJson(tokenId),
',"image":"data:image/svg+xml;base64,', image, '"}'
);
return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json)));
}
function _u2s(uint256 v) internal pure returns (bytes memory) {
if (v == 0) return "0";
uint256 t = v; uint256 len;
while (t > 0) { len++; t /= 10; }
bytes memory b = new bytes(len);
while (v > 0) { len--; b[len] = bytes1(uint8(48 + (v % 10))); v /= 10; }
return b;
}
// ------------------------------------------------------------------
// Batched read views for the website
// ------------------------------------------------------------------
function ownersPage(uint256 start, uint256 count) external view returns (address[] memory out) {
out = new address[](count);
for (uint256 i = 0; i < count; i++) {
uint256 id = start + i;
if (id < MAX_SUPPLY) out[i] = _owners[id];
}
}
function offersPage(uint256 start, uint256 count)
external view
returns (bool[] memory forSale, uint256[] memory prices, address[] memory sellers)
{
forSale = new bool[](count);
prices = new uint256[](count);
sellers = new address[](count);
for (uint256 i = 0; i < count; i++) {
uint256 id = start + i;
if (id >= MAX_SUPPLY) continue;
Offer memory o = punkOffers[id];
// only surface offers that are still valid for the current owner
if (o.isForSale && o.seller == _owners[id]) {
forSale[i] = true;
prices[i] = o.minValue;
sellers[i] = o.seller;
}
}
}
function bidsPage(uint256 start, uint256 count)
external view
returns (bool[] memory hasBid, uint256[] memory values, address[] memory bidders)
{
hasBid = new bool[](count);
values = new uint256[](count);
bidders = new address[](count);
for (uint256 i = 0; i < count; i++) {
uint256 id = start + i;
if (id >= MAX_SUPPLY) continue;
Bid memory b = punkBids[id];
if (b.hasBid) {
hasBid[i] = true;
values[i] = b.value;
bidders[i] = b.bidder;
}
}
}
}
Chain explorer2111msChain node91ms