PERMANENT COLLECTIONProtocol Reference

Read the collection state

Everything below is a free view call against mainnet. If you'd rather query indexed history (acquisitions, bids, settles, referral credits) over GraphQL instead of the chain, the protocol runs a public indexer; see Indexer.

The headline numbers

bash
# Traits permanently collected (0..111)
cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 "collectedCount()(uint256)" --rpc-url https://ethereum-rpc.publicnode.com

# The live bid, in wei
cast call 0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c "bidBalance()(uint256)" --rpc-url https://ethereum-rpc.publicnode.com

# Punks vaulted so far
cast call 0x3614692b8C8B22890D66a5DfcBc6F6eAdEdE036f "lockedPunkCount()(uint256)" --rpc-url https://ethereum-rpc.publicnode.com

# Proofs minted so far (0..111)
cast call 0x3614692b8C8B22890D66a5DfcBc6F6eAdEdE036f "totalProofsMinted()(uint256)" --rpc-url https://ethereum-rpc.publicnode.com

# Is the work complete (all 111 trait bits collected)?
cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 "isComplete()(bool)" --rpc-url https://ethereum-rpc.publicnode.com

PermanentCollection: the records core

Address: 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34. Full reference: PermanentCollection.

Coverage masks and counters

View Returns
collectedMask() -> uint256 The canonical 111-bit completion mask. Monotonic; bit i set means trait i is permanently collected
collectedCount() -> uint256 Population count of collectedMask (0..111)
uncollectedMask() -> uint256 Complement within the full set
pendingMask() -> uint256 Bits that are uncollected AND targeted by an in-flight return auction
isComplete() -> bool True iff collectedMask equals the full 111-bit set
isCollected(uint8 traitId) -> bool Single-bit read of collectedMask
isPending(uint8 traitId) -> bool True iff uncollected and at least one in-flight return auction targets it
pendingTraitCount(uint8) -> uint16 Per-trait in-flight counter (0 or 1 by protocol invariant)
attemptCount(uint8) -> uint256 How many acquisitions have ever targeted this trait; drives the return auction reserve
firstVaultedPunk(uint8 traitId) -> (uint16 punkId, bool exists) The Punk whose vaulting collected the trait; exists false while uncollected
traitCarrierCount(uint8) -> uint16 How many of the 10,000 Punks carry the trait in the sealed dataset

Targeting

View Returns
canonicalTargetOf(uint16 punkId) -> uint8 The trait an acquisition of this Punk would record right now: the rarest uncollected, non-pending trait it carries (ties break to the lowest bit). Reverts NoEligibleTarget(punkId) if the Punk carries nothing targetable
soleCarrierConstraint(uint16 punkId) -> (bool required, uint8 requiredTraitId) Whether the sole-carrier guard constrains this Punk (true only for #8348 while trait bit 23 is uncollected)
newBitsFor(uint16 punkId) -> uint256 / newBitsCountFor(uint16) -> uint256 Mask (and count) of the Punk's traits not yet collected

The acquisitions log (append-only)

View Returns
acquisitionCount() -> uint256 Total rows ever recorded; monotonic
getAcquisition(uint256 idx) -> Acquisition Row by 0-based index; page with acquisitionCount()
getAcquisitionFor(uint16 punkId) -> Acquisition The Punk's current row; reverts NotRecorded(punkId) if never acquired
acquisitionIndexOf(uint16 punkId) -> uint256 0-based index of the Punk's current row; reverts NotRecorded
isRecorded(uint16 punkId) -> bool Whether the Punk has ever been acquired
originalSellerOf(uint16 punkId) -> address The address that gave up the Punk (the future Proof recipient); zero if unrecorded
custodyOf(uint16 punkId) -> Custody Current custody: 0 None (never acquired), 1 InReturnAuction, 2 ReturnedToMarket, 3 Vaulted (terminal)

The Acquisition struct fields, in ABI order: punkId (uint16), targetTraitId (uint8), mask (uint256), pendingMaskAtAcquisition (uint256), acquirer (address), originalSeller (address), priceWei (uint256), acquiredAtBlock (uint256), custody (uint8).

Patron: the live bid

Address: 0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c. Full reference: Patron.

bidBalance() -> uint256 is the live bid: the adapter-accounted ETH that acceptBid / acceptListing pay from. Use it, not the raw contract balance, which can exceed the bid if ETH was force-sent. accountedLiveBidWei() returns the same value.

ReturnAuctionModule: per-Punk sale state

Address: 0x2ced1CA0BC7996c3dFB3D4E685d9d9E7405A7aCe. Full reference: ReturnAuctionModule.

View Returns
getSale(uint16 punkId) -> ReturnAuction Full sale struct: acquisitionCost (uint128), highBidWei (uint128), highBidder (address), startedAt (uint64), endsAt (uint64), reserveWei (uint128), targetTraitId (uint8), settled (bool). All-zero if no sale ever started
reserveOf(uint16) -> uint256 Bid floor, snapshotted at sale start
highBidOf(uint16) -> uint128 / highBidderOf(uint16) -> address Current winning bid and bidder (0 / zero address before the first bid)
endsAt(uint16) -> uint64 / startedAt(uint16) -> uint64 Deadline (anti-snipe extensions included) and start time
isLive(uint16) -> bool Currently accepting bids
isSettleable(uint16) -> bool settle(punkId) would succeed right now
referrerOfHighBid(uint16) -> address Referrer attached to the current high bid
pendingRefund(address) -> uint256 Queued outbid refunds awaiting withdrawRefund()

Enumerate sales from the ReturnAuctionStarted(punkId, acquisitionCost, reserveWei, startedAt, endsAt) event log; a settled slot is reusable, so always confirm with the live views.

PunkVault: Proofs and the Title

Address: 0x3614692b8C8B22890D66a5DfcBc6F6eAdEdE036f. Full reference: PunkVault.

The vault is the ERC721 issuer of the 111 Proofs (token ids 0..110, tokenId == traitId) and the Vault Title (token id 111).

View Returns
totalProofsMinted() -> uint256 Proofs minted so far, caps at 111
isProofMinted(uint8 traitId) -> bool Whether the trait's Proof exists (false for ids >= 111)
proofMeta(uint256 tokenId) -> (uint16 punkId, uint8 traitId, uint16 sequence, uint64 mintedAtBlock) Per-Proof metadata, frozen at mint. sequence is the 1-based collection order, which diverges from tokenId because traits vault in arbitrary order
proofsMintedMask() -> uint256 Bitmap of minted Proofs, bit traitId; one read instead of 111
lockedPunkCount() -> uint256 / isLocked(uint16) -> bool Vaulted-Punk counter and per-Punk flag
titleMinted() -> bool / titleOwner() -> address Title state; titleOwner is zero before the mint
totalSupply() -> uint256 Minted Proofs plus the Title if minted
ownerOf(uint256) / tokenURI(uint256) Standard ERC721; tokenURI reverts UnknownTokenId for an unminted Proof id
bash
# Metadata of the Proof for trait 23
cast call 0x3614692b8C8B22890D66a5DfcBc6F6eAdEdE036f "proofMeta(uint256)(uint16,uint8,uint16,uint64)" 23 --rpc-url https://ethereum-rpc.publicnode.com

Decoding the 111-bit masks

Every mask (collectedMask, pendingMask, uncollectedMask, per-Punk trait masks, proofsMintedMask) uses the same convention: bit i corresponds to trait id i, for i in 0..110. The full set is (1 << 111) - 1.

Plain BigInt:

ts
const TRAIT_COUNT = 111;

/** Trait ids present in a mask. */
function traitIdsOf(mask: bigint): number[] {
    const ids: number[] = [];
    for (let i = 0; i < TRAIT_COUNT; i++) {
        if ((mask >> BigInt(i)) & 1n) ids.push(i);
    }
    return ids;
}

const has = (mask: bigint, traitId: number) => ((mask >> BigInt(traitId)) & 1n) === 1n;
const count = (mask: bigint) => traitIdsOf(mask).length;

Reading and decoding with viem:

ts
import {createPublicClient, http, parseAbi} from 'viem';
import {mainnet} from 'viem/chains';

const client = createPublicClient({chain: mainnet, transport: http('https://ethereum-rpc.publicnode.com')});
const abi = parseAbi([
    'function collectedMask() view returns (uint256)',
    'function pendingMask() view returns (uint256)',
    'function canonicalTargetOf(uint16 punkId) view returns (uint8)',
]);

const collected = await client.readContract({
    address: '0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34',
    abi,
    functionName: 'collectedMask',
});
console.log('collected traits:', traitIdsOf(collected)); // e.g. [3, 17, 54]

And with cast, printing the mask as hex for eyeballing:

bash
cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 "collectedMask()(uint256)" --rpc-url https://ethereum-rpc.publicnode.com | cast to-hex

Trait names for each id come from the sealed PunksData contract at 0x9cF9C8eA737A7d5157d3F4282aCe30880a7A117C, or from the indexer's trait entities.

Read the collection state · Permanent Collection