PERMANENT COLLECTIONProtocol Reference

PermanentCollection

0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 · view on evm.now · deployed at block 25270161

The records-only permanent core of the protocol. It holds no Punks and no ETH. It stores the canonical completion state: the 111-bit collectedMask, the append-only Acquisition[] log, per-Punk custody, per-trait pending and attempt counters, and the first-vaulted-Punk mapping each trait resolves to. The constructor pins the sealed PunksData trait dataset by hash, so the contract can never be deployed against a substituted dataset.

Exactly two addresses can write to it, both fixed forever at setup: patron (the only caller of recordAcquisition) and returnAuctionModule (the only caller of markCustody). Everything else is a view. There is no admin surface, no upgrade path, and no way to remove or rewrite a record.

Concepts

The 111-bit trait model

CryptoPunks have 111 distinct traits across four dimensions (types, head variants, attribute counts, accessories). Each trait is one bit in a uint256. TRAIT_COUNT is 111 and FULL_SET_MASK is (1 << 111) - 1. collectedMask is the artwork's completion state: bit t is set iff a Punk carrying trait t entered PunkVault with t as its recorded target. The mask is strictly monotonic, bits are never unset, and the protocol is complete (isComplete()) when collectedMask == FULL_SET_MASK.

Acquisition never equals collection. recordAcquisition marks one trait pending; only markCustody(punkId, Vaulted) sets a bit on collectedMask, and only the recorded target bit. Other uncollected traits on a vaulted Punk's mask stay available for future acquisitions of other Punks.

Custody state machine

Each Punk has a custody slot (custodyOf), an IPermanentCollection.Custody enum:

Value Name Meaning
0 None never acquired
1 InReturnAuction a 72-hour return auction is live for it
2 ReturnedToMarket its return auction cleared; the Punk went to the winning bidder
3 Vaulted its return auction did not clear; the Punk is in PunkVault forever

Transitions cycle None → InReturnAuction → ReturnedToMarket → InReturnAuction → ...; Vaulted is the only terminal state. A ReturnedToMarket Punk can be re-acquired: recordAcquisition appends a new Acquisition row and re-points the per-Punk index to it, while the prior row's own custody field stays frozen at ReturnedToMarket (the log is append-only). A Vaulted Punk can never be acquired again and there is no withdrawal path from the vault.

Per-Punk readers (getAcquisitionFor, originalSellerOf, custodyOf, acquisitionIndexOf, pendingAcquisitionMaskOf) always reflect the latest row for that Punk.

Canonical target derivation

The target trait of an acquisition is protocol-derived, not caller-chosen. canonicalTargetOf(punkId) returns the rarest trait the Punk carries that is both uncollected and not pending in another return auction, where rarity is the carrier count from the pinned CARRIER_COUNTS table (how many of the 10,000 Punks carry the trait, exposed via traitCarrierCount). Ties break to the lowest bit index. recordAcquisition requires the supplied targetTraitId to equal this canonical value and reverts TargetNotCanonical otherwise, so the parameter is a verified expectation: the call fails loud if the canonical target shifted between the caller's read and the transaction landing, instead of silently recording a different permanent trait. This makes it impossible to waste a scarce-trait carrier on a common trait.

Sole-carrier guard

The sealed dataset contains exactly one rarity-1 trait: bit 23 ("7 Attributes"), carried by exactly one Punk, #8348. Bit 23 can only ever be collected by vaulting #8348 with bit 23 as the recorded target. Because the vault is terminal, vaulting #8348 against any of its common traits would strand bit 23 forever and cap the collection at 110 of 111. So while bit 23 is uncollected, any acquisition of #8348 must record targetTraitId == 23, else recordAcquisition reverts SoleCarrierMustTargetTrait. The guard self-disables once bit 23 is collected and never fires for any other Punk. The pinned pair is exposed as SOLE_CARRIER_TRAIT_BIT and SOLE_CARRIER_PUNK_ID, and soleCarrierConstraint(punkId) reports whether the guard currently binds. The canonical-target rule subsumes this guard (bit 23 is always the rarest pick for #8348 while uncollected); the dedicated check runs first for a specific early revert.

Pending and attempt counters

pendingTraitCount[t] counts in-flight return auctions targeting trait t. It is 0 or 1 by construction: recordAcquisition rejects a second acquisition targeting a trait already in flight (TargetTraitAlreadyPending), and markCustody releases the counter on either outcome. attemptCount[t] counts every acquisition that has ever targeted t and never decrements; ReturnAuctionModule snapshots it when a return auction starts to escalate the reserve (each prior attempt for the same trait adds 1% of the paid price to the reserve).

Live reads

bash
# The 111-bit completion mask
cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 "collectedMask()(uint256)" \
  --rpc-url https://ethereum-rpc.publicnode.com

# How many of the 111 traits are permanently collected
cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 "collectedCount()(uint256)" \
  --rpc-url https://ethereum-rpc.publicnode.com

# The trait an acquisition of Punk #8348 would have to target right now
cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 "canonicalTargetOf(uint16)(uint8)" 8348 \
  --rpc-url https://ethereum-rpc.publicnode.com

Write functions

recordAcquisition

solidity
function recordAcquisition(uint16 punkId, uint8 targetTraitId, uint256 mask, address acquirer, address originalSeller, uint256 priceWei) external

Access: patron-only (msg.sender must equal the wired patron address, else NotPatron)

Records a new acquisition after Patron has bought the Punk. Validates, appends one immutable Acquisition row, marks the target trait pending, bumps attemptCount[targetTraitId], and sets the Punk's custody to InReturnAuction. It never touches collectedMask.

Validation order, each with its own revert:

  1. originalSeller must be non-zero (ZeroAddress)
  2. punkId must be below 10,000 (PunkOutOfRange)
  3. custody must be None or ReturnedToMarket; InReturnAuction and Vaulted reject (AlreadyRecorded)
  4. mask must equal punksData.traitMaskOf(punkId) (MaskMismatch)
  5. targetTraitId must be below 111 (BadCategoryId) and set on mask (TargetTraitNotInMask)
  6. the target must not already be collected (TargetTraitAlreadyCollected)
  7. while trait bit 23 is uncollected, Punk #8348 must target bit 23 (SoleCarrierMustTargetTrait)
  8. the target must not be pending in another return auction (TargetTraitAlreadyPending)
  9. targetTraitId must equal canonicalTargetOf(punkId) (TargetNotCanonical; the canonical derivation itself reverts NoEligibleTarget if the Punk carries no collectable trait)

On success it emits AcquisitionRecorded, TraitsPending, and CustodyUpdated(punkId, InReturnAuction). A re-acquisition of a ReturnedToMarket Punk appends a fresh row and re-points the per-Punk index; the prior row is never mutated.

markCustody

solidity
function markCustody(uint16 punkId, IPermanentCollection.Custody outcome) external

Access: returnAuctionModule-only (msg.sender must equal the wired returnAuctionModule address, else NotReturnAuction)

Settles the terminal outcome of a Punk's current return auction. outcome must be ReturnedToMarket (the auction cleared with a buyer) or Vaulted (no bid by the deadline); anything else reverts InvalidCustodyTransition. The Punk must be recorded (NotRecorded) and currently InReturnAuction (CustodyAlreadySet).

Both outcomes release the target trait's pending counter and update custody on the live slot and the latest acquisition row, emitting CustodyUpdated. The Vaulted outcome additionally collects the recorded target trait, and only that trait: if the target bit was uncollected, it records the Punk as firstVaultedPunk for the trait, sets the bit on collectedMask, and emits TraitsCollected. The ReturnedToMarket path never touches collectedMask.

setWiring

solidity
function setWiring(address _patron, address _finalSaleModule, address _punkVault, address _buybackBurner) external

Access: deployer one-shot (OneTimeSetup gate: caller must be the constructor-time deployer, callable exactly once)

Binds the four protocol addresses this contract references: patron and returnAuctionModule (the two authorized writers) plus punkVault and buybackBurner (published for indexers only, never called from inside this contract). All four must be non-zero (ZeroAddress). The call finalizes the OneTimeSetup gate in the same transaction, so a second call reverts (AlreadyFinalized, or AlreadyInitialized if patron were somehow set first). After this call the wiring is permanent: no admin recovery, no upgrade path. Emits WiringFinalized and Finalized. The _finalSaleModule parameter name is the deployed ABI's name for the returnAuctionModule slot.

Read functions

acquisitionCount

solidity
function acquisitionCount() external view returns (uint256)

Total number of acquisitions ever recorded. Monotonic. Pair with getAcquisition for safe paging over the log.

acquisitionIndexOf

solidity
function acquisitionIndexOf(uint16 punkId) external view returns (uint256)

The 0-based index of punkId's latest acquisition row in the log. Reverts NotRecorded for a Punk that has never been acquired. Stable handle: a re-acquisition re-points it to the new row, but existing indices keep addressing their original rows via getAcquisition.

adminContract

solidity
function adminContract() external view returns (ProtocolAdmin)

The ProtocolAdmin address, recorded for provenance only. No code path in this contract consults it; admin gating lives on other contracts' setters.

attemptCount

solidity
function attemptCount(uint8) external view returns (uint256)

Per-trait counter of how many acquisitions have ever targeted the given trait id. Increments once per recordAcquisition, never decrements. ReturnAuctionModule snapshots it into the reserve escalation for the trait's next return auction.

buybackBurner

solidity
function buybackBurner() external view returns (address)

The BuybackBurner address, published for indexers. Provenance only, never called from inside this contract.

canonicalTargetOf

solidity
function canonicalTargetOf(uint16 punkId) external view returns (uint8)

The trait an acquisition of punkId would have to target right now: the rarest (fewest carriers in the sealed dataset) trait the Punk carries that is both uncollected and not pending in another return auction, ties broken to the lowest bit index. Reverts PunkOutOfRange for punkId >= 10000 and NoEligibleTarget if every trait the Punk carries is already collected or in flight. Frontends read this to pre-fill the target and preview which trait a vault outcome would collect; the value can shift whenever collectedMask or a pending counter changes, which is why recordAcquisition re-verifies it.

collectedCount

solidity
function collectedCount() external view returns (uint256)

Number of bits set on collectedMask, 0 through 111.

collectedMask

solidity
function collectedMask() external view returns (uint256)

The canonical 111-bit completion mask. Bit t is set iff trait t is permanently collected. Only markCustody(punkId, Vaulted) updates it, and strictly monotonically.

custodyOf

solidity
function custodyOf(uint16 punkId) external view returns (IPermanentCollection.Custody)

The current custody value for punkId. Returns None (0) for a Punk that has never been acquired. See the custody state machine above for the enum.

deployedAtBlock

solidity
function deployedAtBlock() external view returns (uint256)

The block number this contract was deployed at. Provenance only; useful as a stable lower bound for indexer backfills.

EXPECTED_DATASET_HASH

solidity
function EXPECTED_DATASET_HASH() external view returns (bytes32)

The pinned hash of the sealed PunksData trait dataset. The constructor reverts unless the referenced PunksData contract reports exactly this datasetHash, so every trait mask this contract verifies against comes from one fixed dataset.

firstVaultedPunk

solidity
function firstVaultedPunk(uint8 traitId) external view returns (uint16 punkId, bool exists)

For a trait id, the first Punk vaulted with that trait as its recorded target, as (punkId, exists). Returns (0, false) for an uncollected trait. Reverts BadCategoryId for traitId >= 111. Because a trait is collected exactly once, "first" is also "only".

FULL_SET_MASK

solidity
function FULL_SET_MASK() external view returns (uint256)

The completion target: (1 << 111) - 1, all 111 trait bits set.

getAcquisition

solidity
function getAcquisition(uint256 idx) external view returns (PermanentCollection.Acquisition)

Reads one Acquisition row by 0-based log index. Reverts with a plain array out-of-bounds panic past acquisitionCount(). Row fields: punkId, targetTraitId, the full trait mask verified at record time, pendingMaskAtAcquisition (the single target bit), acquirer, originalSeller, priceWei, acquiredAtBlock, and the row's own frozen custody.

getAcquisitionFor

solidity
function getAcquisitionFor(uint16 punkId) external view returns (PermanentCollection.Acquisition)

Reads the latest Acquisition row for punkId. Reverts NotRecorded if the Punk has never been acquired. After a re-acquisition this returns the newest row; older rows stay reachable by index via getAcquisition.

isCollected

solidity
function isCollected(uint8 traitId) external view returns (bool)

True iff the given trait id is permanently in the collection. Reverts BadCategoryId for traitId >= 111.

isComplete

solidity
function isComplete() external view returns (bool)

True iff all 111 trait bits are set, i.e. collectedMask == FULL_SET_MASK.

isPending

solidity
function isPending(uint8 traitId) external view returns (bool)

True iff the trait is uncollected and an in-flight return auction targets it. Reverts BadCategoryId for traitId >= 111.

isRecorded

solidity
function isRecorded(uint16 punkId) external view returns (bool)

True iff punkId has ever been recorded as an acquisition, in any custody state.

newBitsCountFor

solidity
function newBitsCountFor(uint16 punkId) external view returns (uint256)

Population count of newBitsFor(punkId): how many currently-uncollected traits the Punk carries.

newBitsFor

solidity
function newBitsFor(uint16 punkId) external view returns (uint256)

The Punk's trait mask intersected with the currently-uncollected set, traitMaskOf(punkId) & ~collectedMask. A live measure of what the Punk could still contribute; unlike pendingAcquisitionMaskOf, it shrinks as other acquisitions collect bits. Returns 0 for punkId >= 10000 instead of reverting.

originalSellerOf

solidity
function originalSellerOf(uint16 punkId) external view returns (address)

The address that gave up punkId to the protocol on its latest acquisition: the previous owner on acceptBid (equal to the recorded acquirer), or the public listing's seller on acceptListing (distinct from the caller, who is the finder). Returns address(0) for an unrecorded Punk. PunkVault.mintProofs reads this at vault-settle time to address the Proof NFT.

patron

solidity
function patron() external view returns (address)

The single acquisition entry point. The only address allowed to call recordAcquisition. Fixed forever at setWiring.

pendingAcquisitionMaskOf

solidity
function pendingAcquisitionMaskOf(uint16 punkId) external view returns (uint256)

The single-bit pending mask recorded on punkId's latest acquisition row (the target bit at record time). Frozen on the record for provenance; it does not shrink as other acquisitions collect bits. Returns 0 for an unrecorded Punk.

pendingMask

solidity
function pendingMask() external view returns (uint256 m)

Bitmap of every trait that is uncollected and currently targeted by an in-flight return auction. One call instead of looping pendingTraitCount 111 times; the renderer consumes this.

pendingTraitCount

solidity
function pendingTraitCount(uint8) external view returns (uint16)

Per-trait counter of in-flight return auctions whose recorded target is this trait. Always 0 or 1: recordAcquisition enforces at most one in-flight acquisition per trait, and markCustody releases it on either outcome.

punksData

solidity
function punksData() external view returns (IPunksData)

The sealed canonical CryptoPunks trait dataset contract. Every mask this contract verifies or derives comes from punksData.traitMaskOf(punkId); its datasetHash was pinned against EXPECTED_DATASET_HASH at construction.

punkVault

solidity
function punkVault() external view returns (address)

The PunkVault address, published for indexers and UI. Provenance only; the vault is never called from inside this contract.

returnAuctionModule

solidity
function returnAuctionModule() external view returns (address)

The single custody-marker. The only address allowed to call markCustody. Fixed forever at setWiring.

setupFinalized

solidity
function setupFinalized() external view returns (bool)

True once setWiring has run. Off-chain tooling checks this before treating the wiring as permanent.

SOLE_CARRIER_PUNK_ID

solidity
function SOLE_CARRIER_PUNK_ID() external view returns (uint16)

The unique Punk (#8348) carrying the dataset's single rarity-1 trait. The only Punk the sole-carrier guard ever constrains.

SOLE_CARRIER_TRAIT_BIT

solidity
function SOLE_CARRIER_TRAIT_BIT() external view returns (uint8)

The dataset's single rarity-1 trait bit (23, "7 Attributes"). While uncollected, an acquisition of its sole carrier must target it.

soleCarrierConstraint

solidity
function soleCarrierConstraint(uint16 punkId) external view returns (bool required, uint8 requiredTraitId)

Whether acquiring punkId is currently constrained by the sole-carrier guard, as (required, requiredTraitId). Returns (true, 23) only for Punk #8348 while trait bit 23 is uncollected; (false, 0) in every other case. Frontends read this to pre-fill the only valid target and warn before a wasted call.

TRAIT_COUNT

solidity
function TRAIT_COUNT() external view returns (uint256)

The number of trait bits: 111.

traitCarrierCount

solidity
function traitCarrierCount(uint8 traitId) external pure returns (uint16)

The number of the 10,000 Punks carrying the given trait in the sealed dataset, from the pinned CARRIER_COUNTS table. Pure. Reverts BadCategoryId for traitId >= 111. This is the rarity metric canonicalTargetOf minimizes.

uncollectedMask

solidity
function uncollectedMask() external view returns (uint256)

Complement of collectedMask within FULL_SET_MASK: the bitmap of traits still to be collected.

Events

AcquisitionRecorded

solidity
event AcquisitionRecorded(uint16 indexed punkId, uint8 indexed targetTraitId, address indexed acquirer, address originalSeller, uint256 mask, uint256 pendingBits, uint256 priceWei, uint256 acquiredAtBlock)

Emitted once per recordAcquisition. Indexed: punkId, targetTraitId, acquirer. Data: originalSeller (the future Proof NFT recipient), the Punk's full verified mask, pendingBits (the single target bit), priceWei, and acquiredAtBlock. One event per log row; a re-acquisition of a returned Punk emits it again with the new row's values.

CustodyUpdated

solidity
event CustodyUpdated(uint16 indexed punkId, IPermanentCollection.Custody outcome)

Emitted on every custody transition: InReturnAuction at record time, then ReturnedToMarket or Vaulted at settle, and InReturnAuction again on a re-acquisition. An indexer can replay these to reconstruct the full custody history of a Punk.

Finalized

solidity
event Finalized()

Emitted exactly once, when setWiring closes the OneTimeSetup gate. After this event the contract's setup surface is permanently closed.

TraitsCollected

solidity
event TraitsCollected(uint16 indexed punkId, uint256 newlyCollectedBits, uint256 collectedCount, bool isComplete)

Emitted when a bit transitions to permanently collected: exactly one bit per markCustody(punkId, Vaulted), the recorded target. Carries newlyCollectedBits (the single bit), the running collectedCount, and the cached isComplete flag so off-chain consumers need no follow-up read.

TraitsPending

solidity
event TraitsPending(uint16 indexed punkId, uint256 pendingBits)

Emitted alongside AcquisitionRecorded with the same single-bit pendingBits value, for indexers that key off per-bit pending state.

WiringFinalized

solidity
event WiringFinalized(address indexed patron, address indexed returnAuctionModule, address indexed punkVault, address buybackBurner)

Emitted once at setWiring time with the four wired addresses. patron, returnAuctionModule, and punkVault are indexed. The addresses are immutable thereafter.

Errors

AlreadyFinalized()

An onlySetup-gated call (i.e. setWiring) landed after the setup gate was already closed. The wiring is permanent; there is nothing to retry.

AlreadyInitialized()

setWiring found patron already set. Same terminal condition as AlreadyFinalized: wiring happens exactly once.

AlreadyRecorded(uint16 punkId)

recordAcquisition was called for a Punk whose custody is InReturnAuction or Vaulted. A live auction must settle first; a vaulted Punk can never be acquired again.

BadCategoryId(uint8 categoryId)

A trait id argument was 111 or higher. Valid trait ids are 0 through 110.

CustodyAlreadySet(uint16 punkId)

markCustody was called for a Punk that is not currently InReturnAuction. The current auction's outcome was already marked.

DatasetHashMismatch(bytes32 expected, bytes32 actual)

Constructor-only: the supplied PunksData contract's datasetHash did not equal EXPECTED_DATASET_HASH. Deployment against a substituted dataset fails.

InvalidCustodyTransition(uint16 punkId)

markCustody received an outcome other than ReturnedToMarket or Vaulted. Those are the only two terminal outcomes of a return auction.

MaskMismatch(uint16 punkId, uint256 expected, uint256 provided)

The mask supplied to recordAcquisition did not match punksData.traitMaskOf(punkId). Re-read the canonical mask and retry.

NoEligibleTarget(uint16 punkId)

The Punk carries no trait that is both uncollected and not already pending in another return auction, so there is nothing an acquisition could target. Raised by canonicalTargetOf (directly and inside recordAcquisition).

NotDeployer()

An onlySetup-gated call came from an address other than the constructor-time deployer.

NotPatron()

recordAcquisition was called by an address other than the wired patron. There is no other acquisition entry point.

NotRecorded(uint16 punkId)

The Punk has never been acquired. Raised by markCustody, acquisitionIndexOf, and getAcquisitionFor.

NotReturnAuction()

markCustody was called by an address other than the wired returnAuctionModule. There is no other custody-marker.

PunkOutOfRange(uint16 punkId)

punkId was 10,000 or higher. Valid CryptoPunk indices are 0 through 9999.

SoleCarrierMustTargetTrait(uint16 punkId, uint8 requiredTraitId)

An acquisition of Punk #8348 targeted a trait other than bit 23 while bit 23 is uncollected. Supply targetTraitId = 23; soleCarrierConstraint reports when this applies.

TargetNotCanonical(uint16 punkId, uint8 provided, uint8 canonical)

The supplied targetTraitId did not equal canonicalTargetOf(punkId). The error carries both values. Re-read the canonical target and retry; the target is protocol-derived, not caller-chosen.

TargetTraitAlreadyCollected(uint8 targetTraitId)

The target trait's bit is already set on collectedMask. Collected traits can never be targeted again.

TargetTraitAlreadyPending(uint8 targetTraitId)

Another in-flight return auction already targets this trait. At most one acquisition per trait can be in flight; wait for that auction to settle.

TargetTraitNotInMask(uint16 punkId, uint8 targetTraitId)

The target trait bit is not set on the Punk's verified trait mask. The Punk does not carry the trait.

ZeroAddress()

A required address argument was zero: any of the four setWiring addresses, or originalSeller in recordAcquisition.

PermanentCollection · Permanent Collection