[{"path":"/docs/introduction/overview","page":"Overview","heading":"Overview","anchor":"","text":"PERMANENT COLLECTION is an on-chain protocol on Ethereum mainnet that uses a speculative ERC20 to build a permanent collection of CryptoPunks via full-trait coverage. Every protocol contract is deployed, immutable, and source-verified. The protocol has no deadline; it completes only when all 111 Cry"},{"path":"/docs/introduction/overview","page":"Overview","heading":"The loop","anchor":"the-loop","text":"CryptoPunks have 111 distinct traits across 4 dimensions. The protocol's goal is a collection that covers every trait. !The protocol loop: swaps in the official pool skim 6% of volume; 5% funds the live bid held by Patron; an owner of an eligible Punk accepts; the Punk enters a 72-hour return auctio"},{"path":"/docs/introduction/overview","page":"Overview","heading":"Fee flow per 1 ETH of swap volume","anchor":"fee-flow-per-1-eth-of-swap-volume","text":"!Fee split, true to scale: of 1 ETH of swap volume, 6.5% is fees. Expanded, that is 5.00% to the live bid, 0.50% LP fee, and a 1.00% protocol leg from which up to 0.25% is carved for the swap's referrer. The official pool's hook skims 6% of swap volume at swap time and routes it in the same transact"},{"path":"/docs/introduction/overview","page":"Overview","heading":"How this reference is organized","anchor":"how-this-reference-is-organized","text":"- Introduction — this page, all contract addresses, and the conventions the reference uses - Contracts — one page per contract: role, concepts, then every write function, read function, event, and error - Guides — task-oriented walkthroughs: accepting the live bid, bidding on a return auction, runni"},{"path":"/docs/introduction/overview","page":"Overview","heading":"For AI agents","anchor":"for-ai-agents","text":"AI agents is the entry point for automated consumers: recipes for reading protocol state, checking eligibility, finding live auctions, watching events, and running keeper actions, each pointing at the fuller reference. The machine-readable surfaces it builds on: - /protocol-manifest.json: every cont"},{"path":"/docs/introduction/addresses","page":"Addresses","heading":"Addresses","anchor":"","text":"Every protocol contract on Ethereum mainnet (chain id 1), deployed at block 25270161. All contracts are immutable and source-verified. Contract Address Docs --- --- --- PermanentCollection 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 reference Patron 0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c referenc"},{"path":"/docs/introduction/addresses","page":"Addresses","heading":"Launch-stack and external contracts","anchor":"launch-stack-and-external-contracts","text":"Contract Address --- --- The $111 token (ArtCoinsToken) 0x61C9d89fe1212F6b55fF888816A151463287B8ae Skim hook (ArtCoinsHookSkimFee) 0x636c050296B5Cc528D8785169Bf8923716FCa9cc Conversion locker (LP positions) 0x866ea3Dc2bf7A3e77374619cf50EB697FA766aab MEV module (ArtCoinsMevLinearSkim) 0xb038D597365Ff"},{"path":"/docs/introduction/conventions","page":"How to read these docs","heading":"How to read these docs","anchor":"","text":"Every contract page follows the same structure, generated from the deployed contract's ABI and merged with hand-written behavior notes. Because the mechanical parts are derived rather than written, a signature in these docs is the signature on chain."},{"path":"/docs/introduction/conventions","page":"How to read these docs","heading":"Contract pages","anchor":"contract-pages","text":"Each page opens with the mainnet address (linked to evm.now, a multi-chain explorer) and the deploy block, then: - Concepts — the mental model for the contract, when it needs one - Write functions — every state-changing function, main entry points first. Each shows the Solidity signature, an Access "},{"path":"/docs/introduction/conventions","page":"How to read these docs","heading":"Access lines","anchor":"access-lines","text":"Every write function declares who can call it: - permissionless — anyone. Several of these pay the caller a small reward; see Run a keeper - \\<contract\\ -only — callable only by another protocol contract (for example patron-only, module-only, hook-only) - admin-gated — gated by ProtocolAdmin, which "},{"path":"/docs/introduction/conventions","page":"How to read these docs","heading":"Examples","anchor":"examples","text":"Read examples use cast (Foundry) against a free public RPC: Write examples use viem. ABIs for every contract are served at /abis/<ContractName .json and live in the repo at app/lib/abis/."},{"path":"/docs/introduction/conventions","page":"How to read these docs","heading":"Units and terms","anchor":"units-and-terms","text":"- Amounts are wei unless stated otherwise; bps means basis points on the denominator stated at the use site (most contracts use 10,000; the hook's volume caps use 100,000) - Punk ids are uint16 (0..9999); trait ids are uint8 (0..110) - The ERC20 is written $111 in prose. Its on-chain symbol is 111 -"},{"path":"/docs/introduction/conventions","page":"How to read these docs","heading":"Machine-readable surfaces","anchor":"machine-readable-surfaces","text":"Surface Path --- --- Protocol manifest (addresses + ABIs + docs links) /protocol-manifest.json Per-contract ABI /abis/<ContractName .json LLM orientation /llms.txt Search corpus for this reference /docs-search-index.json The markdown source of every page is in the public repo under docs/reference/, "},{"path":"/docs/contracts/the-111-token","page":"The $111 token","heading":"The $111 token","anchor":"","text":"0x61C9d89fe1212F6b55fF888816A151463287B8ae · view on evm.now The protocol's ERC20. Name permanent collection, symbol 111, total supply 1,110,000,000 (fixed at deploy; burns only reduce it). Deployed through the artcoins factory as an ArtCoinsToken instance, paired with native ETH in a Uniswap V4 poo"},{"path":"/docs/contracts/the-111-token","page":"The $111 token","heading":"The official pool","anchor":"the-official-pool","text":"The canonical V4 pool is native-ETH-paired and carries the skim hook. Its pool id is listed on Addresses. Swaps in this pool are what fund the protocol: the hook skims 6% of volume per swap and routes it to the live bid, the protocol leg, and referral attribution in the same transaction. The LP fee "},{"path":"/docs/contracts/the-111-token","page":"The $111 token","heading":"Where burns come from","anchor":"where-burns-come-from","text":"$111 is burned (sent to 0xdead or destroyed via burn, reducing totalSupply) on three paths: - BuybackBurner swaps accumulated ETH for $111 in the official pool and burns it, in permissionless paced steps - VaultBurnPool burns the transfer-tax proceeds it accrues (see below) every time a return aucti"},{"path":"/docs/contracts/the-111-token","page":"The $111 token","heading":"Venue-scoped transfer tax","anchor":"venue-scoped-transfer-tax","text":"The token carries a buy-side transfer tax that exists to keep trading concentrated in the official pool, whose skim funds the protocol. Current rate: 15% (taxBps = 1500), bounded by a structural 20% maximum (TAX BPS ABSOLUTE MAX = 2000) that no role can exceed. When it fires: - Only on $111 leaving "},{"path":"/docs/contracts/the-111-token","page":"The $111 token","heading":"Token admin","anchor":"token-admin","text":"The ERC20's tokenAdmin role is held by TokenAdminPoker, a protocol contract whose owner-gated surface is limited to: binding (and permanently locking) a future pool extension, tuning the referral cap on the hook, and tuning the transfer-tax rate within its bounds. There is no mint function, no pause"},{"path":"/docs/contracts/the-111-token","page":"The $111 token","heading":"Reading token state","anchor":"reading-token-state","text":"The dead-address balance plus the supply delta from burn calls gives the total burned to date; the indexer tracks burn steps and tax burns as first-class tables."},{"path":"/docs/contracts/skim-hook","page":"Skim hook (ArtCoinsHookSkimFee)","heading":"Skim hook (ArtCoinsHookSkimFee)","anchor":"","text":"0x636c050296B5Cc528D8785169Bf8923716FCa9cc · view on evm.now The Uniswap V4 hook attached to the official $111/ETH pool (pool id 0xf860d8f4896aed6cc1c68d234ba728680902f0ae43a459fbee6f6baa8036f795). It takes the protocol's skim on every swap, splits it three ways inside the same transaction, and atte"},{"path":"/docs/contracts/skim-hook","page":"Skim hook (ArtCoinsHookSkimFee)","heading":"The 6% baseline skim","anchor":"the-6-baseline-skim","text":"Every swap on the official pool pays a 6% skim on the ETH side of the swap volume (baselineSkimBps = 6 000 in the hook's 100,000 denominator), taken by the hook itself. This is separate from the pool's 0.5% LP fee (lpFee = 5 000 ppm), which flows to in-range liquidity positions per standard V4 mecha"},{"path":"/docs/contracts/skim-hook","page":"Skim hook (ArtCoinsHookSkimFee)","heading":"Intra-swap flush: the hook holds no balance","anchor":"intra-swap-flush-the-hook-holds-no-balance","text":"The split is computed in beforeSwap (exact-input swaps) or afterSwap (exact-output swaps). At the end of afterSwap of the same swap , the hook burns its ERC-6909 claim tokens, takes native ETH from the pool manager, and forwards every leg to its recipient: - the live-bid leg forwards to LiveBidAdapt"},{"path":"/docs/contracts/skim-hook","page":"Skim hook (ArtCoinsHookSkimFee)","heading":"Anti-sniper window (expired)","anchor":"anti-sniper-window-expired","text":"For the first 30 minutes after pool initialization, the MEV module (ArtCoinsMevLinearSkim at 0xb038D597365FfD108D63C265Bb0621444a1D8B83) reported an elevated skim that decayed linearly from 90% down to the 6% baseline (roughly 2.8 percentage points per minute). The overage above baseline (antiSniper"},{"path":"/docs/contracts/skim-hook","page":"Skim hook (ArtCoinsHookSkimFee)","heading":"Canonical-budget attestation (tax exemption)","anchor":"canonical-budget-attestation-tax-exemption","text":"The $111 token charges a venue-scoped, buy-side transfer tax on $111 leaving a known trading venue. Buys on the official pool are exempt , and this hook is what makes them exempt: in afterSwap (buys) and afterRemoveLiquidity (public LP exits) it attests the realized $111 output into an amount-pinned"},{"path":"/docs/contracts/skim-hook","page":"Skim hook (ArtCoinsHookSkimFee)","heading":"Reading the config","anchor":"reading-the-config","text":"Returns (baselineSkimBps, bountyBps, maxReferralBpsOfVolume, lpFee, bountyRecipient, protocolRecipient, referralPayout, quoteToken). On the official pool: bountyRecipient is LiveBidAdapter, protocolRecipient is ProtocolFeePhaseAdapter, referralPayout is ReferralPayout, and quoteToken is address(0) ("},{"path":"/docs/contracts/skim-hook","page":"Skim hook (ArtCoinsHookSkimFee)","heading":"Events an indexer should watch","anchor":"events-an-indexer-should-watch","text":"Event Fired --- --- SkimSplit(poolId, quoteVolume, bountyAmount, protocolNet, referralPaid) once per swap, with the swap's ETH volume and the realized split SwapAttribution(poolId, swapper, referrer, sourceId, campaignId, quoteVolume, referralPaid) when the swap carried valid attribution hookData Le"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"PermanentCollection","anchor":"","text":"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"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"The 111-bit trait model","anchor":"the-111-bit-trait-model","text":"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 ent"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Custody state machine","anchor":"custody-state-machine","text":"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 aucti"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Canonical target derivation","anchor":"canonical-target-derivation","text":"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 1"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Sole-carrier guard","anchor":"sole-carrier-guard","text":"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"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Pending and attempt counters","anchor":"pending-and-attempt-counters","text":"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 "},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Live reads","anchor":"live-reads","text":"`bash"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"The 111-bit completion mask","anchor":"","text":"cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 \"collectedMask()(uint256)\" \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"How many of the 111 traits are permanently collected","anchor":"","text":"cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 \"collectedCount()(uint256)\" \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"The trait an acquisition of Punk #8348 would have to target right now","anchor":"","text":"cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 \"canonicalTargetOf(uint16)(uint8)\" 8348 \\ --rpc-url https://ethereum-rpc.publicnode.com `"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"recordAcquisition","anchor":"recordacquisition","text":"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 InRetur"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"markCustody","anchor":"markcustody","text":"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 els"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"setWiring","anchor":"setwiring","text":"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, ne"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"acquisitionCount","anchor":"acquisitioncount","text":"Total number of acquisitions ever recorded. Monotonic. Pair with getAcquisition for safe paging over the log."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"acquisitionIndexOf","anchor":"acquisitionindexof","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"adminContract","anchor":"admincontract","text":"The ProtocolAdmin address, recorded for provenance only. No code path in this contract consults it; admin gating lives on other contracts' setters."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"attemptCount","anchor":"attemptcount","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"buybackBurner","anchor":"buybackburner","text":"The BuybackBurner address, published for indexers. Provenance only, never called from inside this contract."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"canonicalTargetOf","anchor":"canonicaltargetof","text":"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 NoEligib"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"collectedCount","anchor":"collectedcount","text":"Number of bits set on collectedMask, 0 through 111."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"collectedMask","anchor":"collectedmask","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"custodyOf","anchor":"custodyof","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"deployedAtBlock","anchor":"deployedatblock","text":"The block number this contract was deployed at. Provenance only; useful as a stable lower bound for indexer backfills."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"EXPECTED_DATASET_HASH","anchor":"expecteddatasethash","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"firstVaultedPunk","anchor":"firstvaultedpunk","text":"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\"."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"FULL_SET_MASK","anchor":"fullsetmask","text":"The completion target: (1 << 111) - 1, all 111 trait bits set."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"getAcquisition","anchor":"getacquisition","text":"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, acquiredAtBloc"},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"getAcquisitionFor","anchor":"getacquisitionfor","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"isCollected","anchor":"iscollected","text":"True iff the given trait id is permanently in the collection. Reverts BadCategoryId for traitId = 111."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"isComplete","anchor":"iscomplete","text":"True iff all 111 trait bits are set, i.e. collectedMask == FULL SET MASK."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"isPending","anchor":"ispending","text":"True iff the trait is uncollected and an in-flight return auction targets it. Reverts BadCategoryId for traitId = 111."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"isRecorded","anchor":"isrecorded","text":"True iff punkId has ever been recorded as an acquisition, in any custody state."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"newBitsCountFor","anchor":"newbitscountfor","text":"Population count of newBitsFor(punkId): how many currently-uncollected traits the Punk carries."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"newBitsFor","anchor":"newbitsfor","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"originalSellerOf","anchor":"originalsellerof","text":"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 "},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"patron","anchor":"patron","text":"The single acquisition entry point. The only address allowed to call recordAcquisition. Fixed forever at setWiring."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"pendingAcquisitionMaskOf","anchor":"pendingacquisitionmaskof","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"pendingMask","anchor":"pendingmask","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"pendingTraitCount","anchor":"pendingtraitcount","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"punksData","anchor":"punksdata","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"punkVault","anchor":"punkvault","text":"The PunkVault address, published for indexers and UI. Provenance only; the vault is never called from inside this contract."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"returnAuctionModule","anchor":"returnauctionmodule","text":"The single custody-marker. The only address allowed to call markCustody. Fixed forever at setWiring."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"setupFinalized","anchor":"setupfinalized","text":"True once setWiring has run. Off-chain tooling checks this before treating the wiring as permanent."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"SOLE_CARRIER_PUNK_ID","anchor":"solecarrierpunkid","text":"The unique Punk (#8348) carrying the dataset's single rarity-1 trait. The only Punk the sole-carrier guard ever constrains."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"SOLE_CARRIER_TRAIT_BIT","anchor":"solecarriertraitbit","text":"The dataset's single rarity-1 trait bit (23, \"7 Attributes\"). While uncollected, an acquisition of its sole carrier must target it."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"soleCarrierConstraint","anchor":"solecarrierconstraint","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"TRAIT_COUNT","anchor":"traitcount","text":"The number of trait bits: 111."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"traitCarrierCount","anchor":"traitcarriercount","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"uncollectedMask","anchor":"uncollectedmask","text":"Complement of collectedMask within FULL SET MASK: the bitmap of traits still to be collected."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"AcquisitionRecorded","anchor":"acquisitionrecorded","text":"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 "},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"CustodyUpdated","anchor":"custodyupdated","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Finalized","anchor":"finalized","text":"Emitted exactly once, when setWiring closes the OneTimeSetup gate. After this event the contract's setup surface is permanently closed."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"TraitsCollected","anchor":"traitscollected","text":"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."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"TraitsPending","anchor":"traitspending","text":"Emitted alongside AcquisitionRecorded with the same single-bit pendingBits value, for indexers that key off per-bit pending state."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"WiringFinalized","anchor":"wiringfinalized","text":"Emitted once at setWiring time with the four wired addresses. patron, returnAuctionModule, and punkVault are indexed. The addresses are immutable thereafter."},{"path":"/docs/contracts/permanent-collection","page":"PermanentCollection","heading":"Errors","anchor":"errors","text":"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. AlreadyRe"},{"path":"/docs/contracts/patron","page":"Patron","heading":"Patron","anchor":"","text":"0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c · view on evm.now · deployed at block 25270161 Patron is the protocol's acquisition entry point. It holds the global live-bid ETH and is the only contract authorized to write acquisitions into PermanentCollection. Two permissionless entry points spend the b"},{"path":"/docs/contracts/patron","page":"Patron","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/patron","page":"Patron","heading":"The priced-listing acceptBid model","anchor":"the-priced-listing-acceptbid-model","text":"acceptBid runs on the 2017 market's exclusive-listing primitive. The Punk's owner lists it exclusively to Patron with where 0 < listingWei <= bidBalance(). The listed price is the seller's explicit \"sell to the protocol\" signal: it must be a real positive value (ZeroListingPrice otherwise), it can't"},{"path":"/docs/contracts/patron","page":"Patron","heading":"acceptListing and the seller allowlist","anchor":"acceptlisting-and-the-seller-allowlist","text":"acceptListing(punkId, targetTraitId) consumes a PUBLIC listing (onlySellTo == address(0)) on the 2017 market, but only when the listing's seller is on the allowed-sellers allowlist and its activation delay has elapsed. This opens a composition surface for recognized peer protocols whose contracts li"},{"path":"/docs/contracts/patron","page":"Patron","heading":"The adapter-only receive and the accounted live bid","anchor":"the-adapter-only-receive-and-the-accounted-live-bid","text":"Every ETH source that funds the live bid (the hook's trading-fee leg, attributed contribute top-ups, bare sends, and the cleared return-auction refund) enters LiveBidAdapter, which buffers and meters it into Patron. Patron's receive() accepts ETH ONLY from the adapter and reverts NotAdapter for any "},{"path":"/docs/contracts/patron","page":"Patron","heading":"Worked example: the full acceptBid flow","anchor":"worked-example-the-full-acceptbid-flow","text":"Three transactions from two parties. The owner lists, anyone finalizes, the owner collects from the 2017 market. `bash RPC=https://ethereum-rpc.publicnode.com MARKET=0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB # 2017 CryptoPunks market PUNK ID=1234"},{"path":"/docs/contracts/patron","page":"Patron","heading":"1. Read the current live bid","anchor":"","text":"BID=$(cast call 0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c \"bidBalance()(uint256)\" --rpc-url $RPC)"},{"path":"/docs/contracts/patron","page":"Patron","heading":"2. Read the protocol-derived canonical target for the Punk","anchor":"","text":"PC=$(cast call 0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c \"permanentCollection()(address)\" --rpc-url $RPC) TARGET=$(cast call $PC \"canonicalTargetOf(uint16)(uint8)\" $PUNK ID --rpc-url $RPC)"},{"path":"/docs/contracts/patron","page":"Patron","heading":"3. Owner lists the Punk EXCLUSIVELY to Patron at the full bid","anchor":"","text":""},{"path":"/docs/contracts/patron","page":"Patron","heading":"(any positive price <= the bid works; the full bid is the default)","anchor":"","text":"cast send $MARKET \"offerPunkForSaleToAddress(uint256,uint256,address)\" \\ $PUNK ID $BID 0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c \\ --rpc-url $RPC --private-key $OWNER KEY"},{"path":"/docs/contracts/patron","page":"Patron","heading":"4. Anyone finalizes. expectedListingWei = the price read in step 3;","anchor":"","text":""},{"path":"/docs/contracts/patron","page":"Patron","heading":"a listing raised past it reverts ListingAboveExpected","anchor":"","text":"cast send 0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c \"acceptBid(uint16,uint8,uint256)\" \\ $PUNK ID $TARGET $BID \\ --rpc-url $RPC --private-key $ANY KEY"},{"path":"/docs/contracts/patron","page":"Patron","heading":"5. Seller collects the payment queued in the market's pendingWithdrawals","anchor":"","text":"cast send $MARKET \"withdraw()\" --rpc-url $RPC --private-key $OWNER KEY A frontend can bundle steps 3 and 4 with wallet-native batched calls (EIP-5792 / Multicall3). Step 5 is always a separate transaction by the seller. Between steps 2 and 4 the canonical target can shift (another acquisition may co"},{"path":"/docs/contracts/patron","page":"Patron","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/patron","page":"Patron","heading":"acceptBid","anchor":"acceptbid","text":"Access: permissionless (owner pre-condition: the Punk must be listed exclusively to Patron on the 2017 market at a positive price) Accepts the live bid for punkId. Requires an existing exclusive listing to Patron (onlySellTo == patron) at price L with 0 < L <= bidBalance(). Anyone may call: the targ"},{"path":"/docs/contracts/patron","page":"Patron","heading":"acceptListing","anchor":"acceptlisting","text":"Access: permissionless (listing pre-condition: the seller must be allowlisted and past its 24h activation delay) Buys an eligible Punk from an allowlisted seller's PUBLIC listing on the 2017 market and pays the caller a finder fee. Step by step: 1. Reverts BidBelowMinimum if the accounted live bid i"},{"path":"/docs/contracts/patron","page":"Patron","heading":"skimSurplus","anchor":"skimsurplus","text":"Access: permissionless Forwards forced or otherwise unaccounted ETH (raw balance minus accountedLiveBidWei) back to LiveBidAdapter. Reverts NoSurplus when the raw balance doesn't exceed the accounted bid, ZeroAddress if the adapter isn't wired yet, and SurplusForwardFailed if the forward call fails."},{"path":"/docs/contracts/patron","page":"Patron","heading":"addAllowedSeller","anchor":"addallowedseller","text":"Access: admin-only (raw ProtocolAdmin.admin(); a lifetime carve-out that bypasses the 1y auto-lock and closes only when the admin role is burned) Adds seller to the acceptListing allowlist. Reverts ZeroAddress for the zero address, NotAdmin for any caller other than the current admin. Idempotent: re"},{"path":"/docs/contracts/patron","page":"Patron","heading":"removeAllowedSeller","anchor":"removeallowedseller","text":"Access: admin-only (raw ProtocolAdmin.admin(); same lifetime carve-out as addAllowedSeller) Removes seller from the allowlist and zeroes its activation timestamp, so a later re-add re-engages the full 24h delay. Idempotent: removing a non-allowlisted seller changes nothing and emits no event. Emits "},{"path":"/docs/contracts/patron","page":"Patron","heading":"setWiring","anchor":"setwiring","text":"Access: deployer one-shot (gated by OneTimeSetup; permanently closed after the first successful call) One-shot wiring of permanentCollection, returnAuctionModule (the finalSaleModule parameter), and liveBidAdapter. Resolves the constructor-time cycle between Patron and the contracts that reference i"},{"path":"/docs/contracts/patron","page":"Patron","heading":"receive","anchor":"receive","text":"Access: adapter-only (msg.sender must be liveBidAdapter) Accepts ETH from LiveBidAdapter and credits it to accountedLiveBidWei. Any other sender reverts NotAdapter; direct top-ups belong on the adapter's receive() or contribute() instead, where they are buffered and metered in. Forced ETH bypasses t"},{"path":"/docs/contracts/patron","page":"Patron","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/patron","page":"Patron","heading":"accountedLiveBidWei","anchor":"accountedlivebidwei","text":"The logical live bid, filled only through LiveBidAdapter. This is the sole value used for acceptBid / acceptListing affordability, finder-fee, and downstream reserve math; force-sent ETH is not counted here."},{"path":"/docs/contracts/patron","page":"Patron","heading":"adminContract","anchor":"admincontract","text":"The immutable ProtocolAdmin reference. Patron's only admin-gated surface is the seller allowlist, which checks the raw admin() getter and so remains editable past the 1y auto-lock until the role is burned."},{"path":"/docs/contracts/patron","page":"Patron","heading":"allowedSellerActiveAt","anchor":"allowedselleractiveat","text":"Timestamp at which an allowlisted seller becomes consumable by acceptListing. Set to block.timestamp + ALLOWLIST DELAY on a fresh add, zeroed on removal, zero for addresses that were never added."},{"path":"/docs/contracts/patron","page":"Patron","heading":"allowedSellers","anchor":"allowedsellers","text":"Whether an address is on the acceptListing allowlist. Allowlisted is necessary but not sufficient: the seller must also be past its allowedSellerActiveAt timestamp."},{"path":"/docs/contracts/patron","page":"Patron","heading":"ALLOWLIST_DELAY","anchor":"allowlistdelay","text":"Constant, 24 hours (86400, uint64 seconds). The delay between addAllowedSeller and the seller's public listings becoming consumable by acceptListing."},{"path":"/docs/contracts/patron","page":"Patron","heading":"bidBalance","anchor":"bidbalance","text":"The current live bid, equal to accountedLiveBidWei. This is the number an acceptBid seller lists against and the ceiling on what the protocol pays."},{"path":"/docs/contracts/patron","page":"Patron","heading":"finderFeeCapBps","anchor":"finderfeecapbps","text":"Constant, 50 (0.5% of the live bid). The proportional cap on the acceptListing finder fee; the fee paid is min(bidBalance 50 / 10 000, finderFeeFixedCap). A protocol constant with no setter."},{"path":"/docs/contracts/patron","page":"Patron","heading":"finderFeeFixedCap","anchor":"finderfeefixedcap","text":"Constant, 0.01 ETH. The absolute cap on the acceptListing finder fee, which binds whenever the live bid exceeds 2 ETH. A protocol constant with no setter."},{"path":"/docs/contracts/patron","page":"Patron","heading":"liveBidAdapter","anchor":"livebidadapter","text":"The single inflow governor, set once at setWiring. The only address receive() accepts ETH from, and the destination of skimSurplus forwards."},{"path":"/docs/contracts/patron","page":"Patron","heading":"MIN_BID_FOR_LISTING","anchor":"minbidforlisting","text":"Constant, 0.5 ETH. The minimum accounted live bid required for acceptListing to fire, defending against dust listings draining finder fees against a trivial bid."},{"path":"/docs/contracts/patron","page":"Patron","heading":"permanentCollection","anchor":"permanentcollection","text":"The records-only core, set once at setWiring. Patron is the only address authorized to call its recordAcquisition writer. Read its canonicalTargetOf(punkId) to obtain the targetTraitId both entry points require."},{"path":"/docs/contracts/patron","page":"Patron","heading":"punksData","anchor":"punksdata","text":"The immutable reference to the sealed PunksData dataset, read for the Punk's trait mask on every acquisition."},{"path":"/docs/contracts/patron","page":"Patron","heading":"punksMarket","anchor":"punksmarket","text":"The immutable reference to the 2017 CryptoPunks market (0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB). All Punk-side listing reads, purchases, and transfers flow through it."},{"path":"/docs/contracts/patron","page":"Patron","heading":"returnAuctionModule","anchor":"returnauctionmodule","text":"The 72-hour return auction, set once at setWiring. Patron transfers every acquired Punk to it and calls startSale immediately after buyPunk."},{"path":"/docs/contracts/patron","page":"Patron","heading":"setupFinalized","anchor":"setupfinalized","text":"Whether the one-shot setup gate has closed (true after setWiring). Off-chain tooling can check this before treating the wiring as permanent."},{"path":"/docs/contracts/patron","page":"Patron","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/patron","page":"Patron","heading":"AllowedSellerAdded","anchor":"allowedselleradded","text":"Emitted when the admin adds a new seller to the acceptListing allowlist. Only fires on a fresh add (the setter is idempotent). Indexers should pair it with a read of allowedSellerActiveAt(seller) since the seller isn't consumable until 24h later."},{"path":"/docs/contracts/patron","page":"Patron","heading":"AllowedSellerRemoved","anchor":"allowedsellerremoved","text":"Emitted when the admin removes an allowlisted seller. Only fires when the seller was actually allowlisted."},{"path":"/docs/contracts/patron","page":"Patron","heading":"BidAccepted","anchor":"bidaccepted","text":"Emitted on every successful acceptBid. punkId and seller are indexed; payout is the listed price the seller is owed through the market's pendingWithdrawals (collected with withdraw(), not pushed by Patron). One BidAccepted corresponds to one new acquisition row in PermanentCollection and one startSa"},{"path":"/docs/contracts/patron","page":"Patron","heading":"Finalized","anchor":"finalized","text":"Emitted exactly once, when setWiring closes the one-shot setup gate. After this event the contract's wiring is permanent."},{"path":"/docs/contracts/patron","page":"Patron","heading":"ListingAccepted","anchor":"listingaccepted","text":"Emitted on every successful acceptListing. punkId, seller (the public-listing seller, the recorded originalSeller), and caller (the finder, the recorded acquirer) are indexed; minValue is the listed price paid through the market and finderFee is what the caller received."},{"path":"/docs/contracts/patron","page":"Patron","heading":"SurplusForwarded","anchor":"surplusforwarded","text":"Emitted when skimSurplus forwards forced/unaccounted ETH back to LiveBidAdapter. caller is whoever triggered the skim; amount is the surplus forwarded. This ETH is not part of the live bid until the adapter meters it back in."},{"path":"/docs/contracts/patron","page":"Patron","heading":"WiringFinalized","anchor":"wiringfinalized","text":"Emitted once at setWiring with the permanently-fixed permanentCollection and returnAuctionModule addresses. The adapter address set in the same call is readable via liveBidAdapter()."},{"path":"/docs/contracts/patron","page":"Patron","heading":"Errors","anchor":"errors","text":"AlreadyFinalized() setWiring was called after the one-shot setup gate already closed. The wiring is permanent; there is no second chance. AlreadyInitialized() setWiring found permanentCollection already set. Redundant protection alongside the setup gate. BidBelowMinimum(uint256 liveBid, uint256 mini"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"ReturnAuctionModule","anchor":"","text":"0x2ced1CA0BC7996c3dFB3D4E685d9d9E7405A7aCe · view on evm.now · deployed at block 25270161 ReturnAuctionModule runs the 72-hour return auction that follows every acquisition. It holds custody of each accepted Punk for the duration of its auction, accepts open ETH bids above a snapshotted reserve, and"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Auction lifecycle","anchor":"auction-lifecycle","text":"1. Patron acquires a Punk (via acceptBid or acceptListing), transfers it to this module, and calls startSale(punkId, acquisitionCost, targetTraitId). The auction opens for AUCTION DURATION (72 hours) 2. Anyone bids via placeBid or placeBidWithReferral. Each bid must meet the reserve and, after the f"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Reserve formula","anchor":"reserve-formula","text":"The reserve is snapshotted at startSale and never moves for that sale: previousAttempts is PermanentCollection.attemptCount(targetTraitId) at startSale time, before recordAcquisition bumps it for this acquisition. So the first attempt against a trait requires a 1% premium over the price paid, the se"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Cleared outcome (a bid met the reserve)","anchor":"cleared-outcome-a-bid-met-the-reserve","text":"The high bidder receives the Punk through a provenance round-trip via ReturnAuctionEscrow: the module transfers the Punk to the escrow, the escrow lists it exclusively back to the module at the hammer price, the module buys it (so the canonical market records a real PunkBought(escrow, module, highBi"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Unsold outcome (no bid)","anchor":"unsold-outcome-no-bid","text":"The Punk transfers to PunkVault, the terminal custodian with no withdrawal path. markCustody(Vaulted) collects only the recorded targetTraitId; other uncollected traits on the Punk's mask remain available through future acquisitions of other Punks. If this is the first vaulting of the target trait, "},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Slot reuse","anchor":"slot-reuse","text":"A settled sale slot is reusable. A Punk that cleared (custody ReturnedToMarket) can be re-acquired, and startSale then fully resets the slot: high bid, high bidder, settled flag, and referrerOfHighBid[punkId] are cleared, and the reserve is re-snapshotted off the new acquisition price with the curre"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Refund queue","anchor":"refund-queue","text":"When a new high bid lands, the previous bid is pushed back to the outbid bidder with a 30,000 gas budget. If the push fails (for example, a contract bidder whose receive needs more gas), the amount accrues in pendingRefund[bidder] and RefundQueued is emitted; the bidder pulls it any time via withdra"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Reading and bidding","anchor":"reading-and-bidding","text":"Read the full sale state, liveness, and settleability with cast: `bash"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Full ReturnAuction struct: (acquisitionCost, highBidWei, highBidder,","anchor":"","text":""},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"startedAt, endsAt, reserveWei, targetTraitId, settled)","anchor":"","text":"cast call 0x2ced1CA0BC7996c3dFB3D4E685d9d9E7405A7aCe \\ \"getSale(uint16)((uint128,uint128,address,uint64,uint64,uint128,uint8,bool))\" \\ 1234 --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Is the auction accepting bids right now?","anchor":"","text":"cast call 0x2ced1CA0BC7996c3dFB3D4E685d9d9E7405A7aCe \"isLive(uint16)(bool)\" 1234 \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Would settle(punkId) succeed right now?","anchor":"","text":"cast call 0x2ced1CA0BC7996c3dFB3D4E685d9d9E7405A7aCe \"isSettleable(uint16)(bool)\" 1234 \\ --rpc-url https://ethereum-rpc.publicnode.com ts import {createWalletClient, http, parseEther, stringToHex} from 'viem'; import {mainnet} from 'viem/chains'; import {abi} from '@/lib/abis/ReturnAuctionModule'; c"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"placeBid","anchor":"placebid","text":"Access: permissionless Place a bid on the return auction for punkId with no referral attribution. Exactly equivalent to placeBidWithReferral(punkId, address(0), bytes32(0)). Requirements, checked in order: - A sale must exist (SaleMissing), not be settled (AlreadySettled), and block.timestamp must b"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"placeBidWithReferral","anchor":"placebidwithreferral","text":"Access: permissionless Place a bid carrying auction-referral attribution. Identical validation and effects to placeBid, plus: referrer is written to referrerOfHighBid[punkId], overwriting any prior value. The slot tracks the current high bidder's referrer only, so an outbid bidder's referrer loses a"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"settle","anchor":"settle","text":"Access: permissionless Finalize the auction for punkId after endsAt. Reverts SaleMissing if no sale exists, AlreadySettled if already settled, SaleLive before the deadline. Settlement pays no protocol-funded caller tip on either path; it is self-incentivized (the cleared path by the winning bidder's"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"withdrawRefund","anchor":"withdrawrefund","text":"Access: permissionless Pull the caller's queued refund balance (bids that failed to push back when outbid). Zeroes pendingRefund[msg.sender] before sending the full amount. Reverts NothingToWithdraw when the balance is zero and TransferFailed when the send fails (the balance is then restored by the "},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"startSale","anchor":"startsale","text":"Access: patron-only (msg.sender == patron, else NotPatron) Open the 72-hour return auction for a freshly acquired Punk. Patron calls this immediately after buyPunk transfers the Punk into this module. - Reverts SaleExists if a live, unsettled sale is already open for punkId. A settled slot is reusab"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"setLiveBidAdapter","anchor":"setlivebidadapter","text":"Access: deployer one-shot (msg.sender == deployer, once, else NotDeployer / AlreadyWired) One-time post-deploy wiring of the LiveBidAdapter address. The adapter's constructor references this module (to gate its poolReplenish), so the adapter deploys after this module and is wired back here. Rejects "},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"setVaultBurnPool","anchor":"setvaultburnpool","text":"Access: deployer one-shot (msg.sender == deployer, once, else NotDeployer / AlreadyWired) One-time post-deploy wiring of the VaultBurnPool address, resolving the same constructor cycle as setLiveBidAdapter (the pool's constructor depends on this module's address). Rejects address(0) (ZeroAddress). E"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"receive","anchor":"receive","text":"Access: escrow-only (msg.sender == address(escrow), else TransferFailed) Accepts ETH only from the settlement escrow when it forwards canonical-market proceeds back during a cleared settle. Bids arrive through the payable bid functions, never here."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"AUCTION_DURATION","anchor":"auctionduration","text":"Constant 72 hours: the span from startSale to the initial endsAt. Late bids can extend the deadline past this."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"buybackBurner","anchor":"buybackburner","text":"The BuybackBurner address that receives the residual burn share (25% of cost) on cleared settles. Immutable."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"CLEARED_BID_BPS","anchor":"clearedbidbps","text":"Constant 6500: the live-bid share of cleared proceeds as bps of acquisitionCost (65% of cost, routed to LiveBidAdapter.poolReplenish). Hard-coded, no setter."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"CLEARED_VAULT_BURN_BPS","anchor":"clearedvaultburnbps","text":"Constant 1000: the in-cost vault-burn share of cleared proceeds as bps of acquisitionCost (10% of cost to VaultBurnPool, on top of the premium that also routes there). The burn share to BuybackBurner is the residual 25%. Hard-coded, no setter."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"endsAt","anchor":"endsat","text":"Current deadline for the sale on punkId, including any anti-snipe extensions. 0 if no sale has ever started."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"escrow","anchor":"escrow","text":"The ReturnAuctionEscrow deployed by this module's constructor and pinned to it. Used only during cleared settles for the provenance round-trip that records a real PunkBought at the hammer price on the canonical market."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"getSale","anchor":"getsale","text":"Full ReturnAuction struct for punkId: acquisitionCost, highBidWei, highBidder, startedAt, endsAt, reserveWei (snapshot at sale start), targetTraitId (collected only on the vault path), settled. Returns a zero-valued struct if no sale has ever started for this Punk. After a re-auction the struct refl"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"highBidderOf","anchor":"highbidderof","text":"Current winning bidder for punkId. address(0) if no bids yet."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"highBidOf","anchor":"highbidof","text":"Current winning bid in wei for punkId. 0 if no bids yet."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"isLive","anchor":"islive","text":"True iff the sale on punkId is currently accepting bids: a sale exists, is not settled, and block.timestamp < endsAt."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"isSettleable","anchor":"issettleable","text":"True iff settle(punkId) would succeed right now: a sale exists, is not settled, and the deadline has passed."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"liveBidAdapter","anchor":"livebidadapter","text":"The LiveBidAdapter that receives the 65%-of-cost live-bid share on cleared settles via poolReplenish. address(0) until the deployer's one-shot setLiveBidAdapter."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"minBidIncrementBps","anchor":"minbidincrementbps","text":"Constant 100 (1%): minimum overbid as bps of the current high bid. The next valid bid must be at least currentHigh × 10 100 / 10 000. Forces geometric growth of locked capital per anti-snipe round, which bounds indefinite-extension griefing. A protocol constant, not admin-tunable."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"patron","anchor":"patron","text":"The Patron hub, the only authorized caller of startSale. Immutable."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"pendingRefund","anchor":"pendingrefund","text":"Queued refund balance in wei for a bidder whose push refund failed. Withdrawable via withdrawRefund."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"permanentCollection","anchor":"permanentcollection","text":"The records-only PermanentCollection core. Read for attemptCount at startSale and for originalSellerOf / acquisitionIndexOf / collectedCount at vault-path settle; receives markCustody on both settle paths. Immutable."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"punksMarket","anchor":"punksmarket","text":"The 2017 CryptoPunks market contract. Immutable."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"punkVault","anchor":"punkvault","text":"The terminal PunkVault custodian that receives unsold Punks and mints the Proof NFTs. Immutable."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"REFERRER_GAS","anchor":"referrergas","text":"Constant 35,000: gas budget for the outgoing send to the auction referrer on cleared settles. Matches ReferralPayout.CLAIM GAS so receiver-side gas behavior observed on one surface applies to both."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"REFERRER_PREMIUM_BPS","anchor":"referrerpremiumbps","text":"Constant 500: the winning bidder's referrer share as bps of the premium (highBid − acquisitionCost), so 5% of the premium. The remainder of the premium flows to VaultBurnPool. Hard-coded, no setter."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"referrerOfHighBid","anchor":"referrerofhighbid","text":"Referrer attached to the current high bid for punkId. Overwritten on every accepted bid and cleared at each startSale, so it tracks only the current auction's current high bidder. address(0) means a no-attribution bid. Read at cleared settle to size the referrer share of the premium."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"reserveOf","anchor":"reserveof","text":"Bid floor in wei for punkId, snapshotted at startSale. A first bid must be at least this amount."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"SNIPE_EXTENSION","anchor":"snipeextension","text":"Constant 1 hour: how far endsAt moves when a bid triggers the anti-snipe extension."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"SNIPE_TRIGGER_WINDOW","anchor":"snipetriggerwindow","text":"Constant 15 minutes: a bid placed with less than this remaining triggers the anti-snipe extension."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"startedAt","anchor":"startedat","text":"block.timestamp when startSale ran for punkId. 0 if never started."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"vaultBurnPool","anchor":"vaultburnpool","text":"The VaultBurnPool that receives the 10%-of-cost slice plus the premium remainder on cleared settles, and is swept on every vault-path settle. address(0) until the deployer's one-shot setVaultBurnPool."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"BidPlaced","anchor":"bidplaced","text":"Emitted on every accepted bid. Carries the bidder, the referrer attached to this bid (address(0) for no attribution), the bid amount, the free-form tag, and endsAt as it stands after any anti-snipe extension this bid triggered. Indexers can credit referrers from this event alone without reading the "},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"LiveBidAdapterSet","anchor":"livebidadapterset","text":"Emitted exactly once, at the deployer's setLiveBidAdapter call."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"PunkVaulted","anchor":"punkvaulted","text":"Emitted on the unsold settle path. The Punk is now in PunkVault permanently and the recorded target trait has been collected. Pair with the vault's Transfer event for the Proof mint when this was the trait's first vaulting."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"RefundQueued","anchor":"refundqueued","text":"Emitted when an outbid refund couldn't push to the previous bidder within the 30k gas budget. The amount is added to pendingRefund[bidder] for later withdrawRefund."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"RefundWithdrawn","anchor":"refundwithdrawn","text":"Emitted when a bidder pulls a previously queued refund."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"ReturnAuctionCleared","anchor":"returnauctioncleared","text":"Emitted on the cleared settle path with the full proceeds split: liveBidShare (65% of cost, to LiveBidAdapter), burnShare (25% of cost, to BuybackBurner), vaultBurnShare (10% of cost plus the premium remainder, to VaultBurnPool), and referrerShare (paid to referrer, 0 when unattributed or when the s"},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"ReturnAuctionExtended","anchor":"returnauctionextended","text":"Emitted when an anti-snipe extension has moved endsAt further into the future. newEndsAt is the new deadline."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"ReturnAuctionStarted","anchor":"returnauctionstarted","text":"Emitted once per startSale. reserveWei is the snapshotted reserve (ceil(acquisitionCost × (101 + previousAttempts) / 100)). A re-auction of a returned Punk emits this again with the new cost and reserve."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"VaultBurnPoolSet","anchor":"vaultburnpoolset","text":"Emitted exactly once, at the deployer's setVaultBurnPool call."},{"path":"/docs/contracts/return-auction-module","page":"ReturnAuctionModule","heading":"Errors","anchor":"errors","text":"AlreadySettled(uint16 punkId) The sale for this punkId has already been settled. Bids and repeat settles are rejected; a new startSale (after a re-acquisition) reopens the slot. AlreadyWired() The one-shot wiring setter (setLiveBidAdapter or setVaultBurnPool) was already called. The wiring is immuta"},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"ReturnAuctionEscrow","anchor":"","text":"Transient settlement escrow that makes the 2017 CryptoPunks market record a real PunkBought(seller, buyer, price) for every cleared return auction, instead of a price-less PunkTransfer. During a cleared settle, ReturnAuctionModule transfers the won Punk here, the escrow lists it exclusively back to "},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"Why the round-trip exists","anchor":"why-the-round-trip-exists","text":"CryptoPunks provenance tooling reads sale history from PunkBought events on the canonical market. A cleared return auction settles inside the module (the winner's bid is already escrowed there), so a plain transferPunk to the winner would record no price. The round-trip inserts a genuine market-reco"},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"listForSettlement","anchor":"listforsettlement","text":"Access: module-only (MODULE, the deploying ReturnAuctionModule; any other caller reverts NotModule) Lists the Punk the escrow currently holds for sale exclusively to the module at the hammer price, via the market's offerPunkForSaleToAddress. The module transfers the Punk in immediately before callin"},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"sweepProceeds","anchor":"sweepproceeds","text":"Access: module-only (any other caller reverts NotModule) Pulls the escrow's post-sale credit out of the market (punksMarket.withdraw(), which triggers this contract's market-gated receive()) and forwards the full ETH balance to the module so it can run the cleared-path proceeds split. Returns silent"},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"receive","anchor":"receive","text":"Access: market-only (only the CryptoPunks market; any other sender reverts UnexpectedEtherSender) Accepts ETH only from the Punk market, which pushes it during the withdraw() call inside sweepProceeds. Any other sender reverts, so ETH can't be parked here from outside and there is nothing for an adm"},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"MODULE","anchor":"module","text":"The immutable ReturnAuctionModule that deployed this escrow and is its only authorized caller. Captured as msg.sender at construction, never changes."},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"punksMarket","anchor":"punksmarket","text":"The immutable 2017 CryptoPunks market address the escrow lists on, buys through, and withdraws from. Set at construction, never changes."},{"path":"/docs/contracts/return-auction-escrow","page":"ReturnAuctionEscrow","heading":"Errors","anchor":"errors","text":"NotModule() listForSettlement or sweepProceeds was called by an address other than MODULE. The escrow acts only on the module's instructions. ProceedsForwardFailed() The ETH forward to the module at the end of sweepProceeds failed. Reverts the call so no proceeds are stranded; unreachable on the liv"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"PunkVault","anchor":"","text":"0x3614692b8C8B22890D66a5DfcBc6F6eAdEdE036f · view on evm.now · deployed at block 25270161 PunkVault is the immutable terminal custodian for vaulted Punks. When a 72-hour return auction ends with no bid, the ReturnAuctionModule transfers the Punk to the vault's address on the 2017 CryptoPunks market "},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Punks are not ERC721 tokens","anchor":"punks-are-not-erc721-tokens","text":"CryptoPunks predate ERC721. A vaulted Punk is held as a plain ownership entry at the market's punkIndexToAddress(punkId) slot, with the vault's address as the owner. Nothing about a vaulted Punk appears in this contract's ERC721 accounting: balanceOf, ownerOf, totalSupply, and the transfer surface c"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Two minters, disjoint id ranges","anchor":"two-minters-disjoint-id-ranges","text":"The two token classes are issued by two distinct immutable minters whose id ranges cannot overlap: - returnAuctionModule (set at construction, never rotatable) is the only caller of receivePunk and mintProofs. mintProofs can only produce token ids 0..110 (the id is derived from the trait id, which i"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Proof metadata is frozen at mint","anchor":"proof-metadata-is-frozen-at-mint","text":"Each Proof records a ProofMeta struct at mint time: the punkId whose vaulting brought the trait into the collection, the traitId (equal to the token id), the 1-based sequence in collection order, and mintedAtBlock. All four fields are written once and never change, even when the Proof is transferred"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"tokenURI dispatch through the RendererRegistry","anchor":"tokenuri-dispatch-through-the-rendererregistry","text":"tokenURI(id) and contractURI() hold no metadata themselves. After local existence checks, both delegate to the wired RendererRegistry, which forwards to the live renderer implementation. The renderer dispatches on the id: ids 0..110 resolve to the Proof renderer, id 111 to the Title JSON. contractUR"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"The owner() slot: marketplace editor only, one-way ratchet","anchor":"the-owner-slot-marketplace-editor-only-one-way-ratchet","text":"The vault exposes ERC-173 owner() solely so OpenSea, Blur, and Magic Eden recognize a wallet as the collection-page editor (banner, profile image, description override, social links). The slot is initialized to the deployer EOA at construction and is only ever settable to address(0) via renounceOwne"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Metadata refresh signals","anchor":"metadata-refresh-signals","text":"The vault emits ERC-7572 ContractURIUpdated() on the Title mint and on every Proof mint, since both change the collection-progress fields marketplaces display, and EIP-4906 MetadataUpdate(tokenId) when a token's rendered attributes may have changed (each mint, plus MetadataUpdate(111) on every recei"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Live reads","anchor":"live-reads","text":"`bash"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Proofs issued so far (0..111)","anchor":"","text":"cast call 0x3614692b8C8B22890D66a5DfcBc6F6eAdEdE036f \"totalProofsMinted()(uint256)\" \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Frozen metadata for Proof token id 20 (trait 20):","anchor":"","text":""},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"(punkId, traitId, sequence, mintedAtBlock)","anchor":"","text":"cast call 0x3614692b8C8B22890D66a5DfcBc6F6eAdEdE036f \\ \"proofMeta(uint256)(uint16,uint8,uint16,uint64)\" 20 \\ --rpc-url https://ethereum-rpc.publicnode.com proofMeta returns all zeros for an unminted id; check isProofMinted(traitId) first to distinguish \"not yet minted\" from a Proof genuinely minted "},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"receivePunk","anchor":"receivepunk","text":"Access: returnAuctionModule-only (NotReturnAuction for any other caller) Registers a Punk as permanently vaulted. The caller must have already executed transferPunk(vault, punkId) on the CryptoPunks market; receivePunk verifies current market ownership via punkIndexToAddress and reverts NotOwnedByVa"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"mintProofs","anchor":"mintproofs","text":"Access: returnAuctionModule-only (NotReturnAuction for any other caller) Mints the Proof for targetTraitId as token id uint256(targetTraitId) (ids 0..110) to recipient, the originalSeller recorded on the acquisition whose vault-settle produced this Proof. Called by ReturnAuctionModule.settle atomica"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"mintToAuction","anchor":"minttoauction","text":"Access: titleAuction-only (NotTitleAuction for any other caller) Mints the Vault Title (token id 111) to the titleAuction contract itself, which escrows it for its auction. One-shot: reverts TitleAlreadyMinted on any second call. Emits TitleMinted(titleAuction), the ERC721 Transfer, MetadataUpdate(1"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"setTitleAuction","anchor":"settitleauction","text":"Access: deployer one-shot (NotDeployer for others; TitleAuctionAlreadySet once wired) Binds the titleAuction address exactly once. Reverts ZeroAddress for address(0). After this call mintToAuction is callable only by the bound address and the slot can never be changed. Emits TitleAuctionSet. Part of"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"setRendererRegistry","anchor":"setrendererregistry","text":"Access: deployer one-shot (NotDeployer for others; RendererRegistryAlreadySet once wired) Binds the RendererRegistry address exactly once. Reverts ZeroAddress for address(0). After this call tokenURI and contractURI delegate to the registry and the slot can never be changed (the registry itself fron"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"renounceOwnership","anchor":"renounceownership","text":"Access: owner-only (NotOwner for any other caller) Permanently sets the ERC-173 owner() slot to address(0) and emits OwnershipTransferred(previousOwner, address(0)). One-way: there is no transferOwnership, so no address can ever hold the slot again, and marketplaces will refuse all future collection"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"approve","anchor":"approve","text":"Access: permissionless (token owner or an isApprovedForAll operator; solmate reverts NOT AUTHORIZED otherwise) Standard ERC721 single-token approval for a Proof or the Title. No protocol-specific behavior."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"setApprovalForAll","anchor":"setapprovalforall","text":"Access: permissionless (sets the caller's own operator flag) Standard ERC721 operator approval covering all of the caller's PunkVault tokens. No protocol-specific behavior."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"transferFrom","anchor":"transferfrom","text":"Access: permissionless (owner, approved spender, or operator of id) Standard solmate ERC721 transfer of a Proof or the Title. Uses string reverts from the solmate base for the standard failure modes (WRONG FROM, NOT AUTHORIZED, INVALID RECIPIENT for a zero to). Transferring a Proof moves ownership o"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"safeTransferFrom","anchor":"safetransferfrom","text":"Access: permissionless (owner, approved spender, or operator of id) transferFrom plus the ERC721 receiver check: if to is a contract it must return the onERC721Received selector (solmate reverts UNSAFE RECIPIENT otherwise). No protocol-specific behavior."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"safeTransferFrom","anchor":"safetransferfrom","text":"Access: permissionless (owner, approved spender, or operator of id) Same as the 3-argument form, forwarding data to the recipient's onERC721Received. No protocol-specific behavior."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"balanceOf","anchor":"balanceof","text":"Standard ERC721 balance over the vault's own tokens (Proofs + Title), not Punks. Reverts (solmate ZERO ADDRESS) for the zero address."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"contractURI","anchor":"contracturi","text":"ERC-7572 collection-level metadata JSON (data URI). Delegates to RendererRegistry.contractURI(address(this)); resolves to the same content as the Title's tokenURI(111). Reverts RendererRegistryNotSet if the registry was never wired."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"getApproved","anchor":"getapproved","text":"Standard ERC721 per-token approval getter. Raw mapping read: returns address(0) for unminted ids rather than reverting."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"isApprovedForAll","anchor":"isapprovedforall","text":"Standard ERC721 operator-approval getter. Raw mapping read."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"isLocked","anchor":"islocked","text":"Per-Punk vaulted flag, keyed by Punk id. Once true, stays true forever. This is the Punk-side record (distinct from the ERC721 accounting, which never includes Punks)."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"isProofMinted","anchor":"isproofminted","text":"True iff the Proof for traitId has been minted. Returns false (rather than reverting) for traitId = 111. Cheaper than ownerOf(traitId), which reverts pre-mint. Use this to guard proofMeta reads."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"lockedPunkCount","anchor":"lockedpunkcount","text":"Count of permanently vaulted Punks. Monotonic. The per-Punk history is the PunkLocked event log; there is no on-chain array accessor for the full list."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"MAX_PROOF_TOKEN_ID","anchor":"maxprooftokenid","text":"Constant 110. Highest valid Proof token id; Proofs occupy ids 0..MAX PROOF TOKEN ID inclusive with tokenId == traitId."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"name","anchor":"name","text":"ERC721 name: Title to PERMANENT COLLECTION Vault."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"owner","anchor":"owner","text":"ERC-173 owner. The deployer EOA until renounceOwnership() is called, then address(0) forever. Read by marketplaces to decide which wallet can edit the collection page; carries no on-chain authority (see Concepts)."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"ownerOf","anchor":"ownerof","text":"Standard ERC721 owner lookup for Proof ids 0..110 and the Title (111). Reverts (solmate NOT MINTED) for unminted or out-of-range ids. Never answers for Punks; use punksMarket.punkIndexToAddress(punkId) for Punk custody."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"PROOF_COUNT","anchor":"proofcount","text":"Constant 111. Number of distinct Proofs, one per trait in the sealed PunksData taxonomy."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"proofMeta","anchor":"proofmeta","text":"Frozen per-Proof metadata, keyed by Proof token id: (punkId, traitId, sequence, mintedAtBlock). Written once at mint and never mutated, even across transfers. Returns all zeros for an unminted id; check isProofMinted first. See the Concepts section for a cast example."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"proofsMintedCount","anchor":"proofsmintedcount","text":"Number of Proofs minted so far (0..111). Equals popcount(proofsMintedMask), tracked explicitly so the renderer composes its \"N of 111\" progress inscription in one read."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"proofsMintedMask","anchor":"proofsmintedmask","text":"Bitmap of minted Proofs: bit traitId is set iff that trait's Proof exists. Single read for computing full issued-so-far state."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"punksMarket","anchor":"punksmarket","text":"The 2017 CryptoPunks market contract (0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB). Immutable. Vaulted Punks live at this contract's punkIndexToAddress slot with the vault as owner."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"rendererRegistry","anchor":"rendererregistry","text":"The wired RendererRegistry address that tokenURI and contractURI delegate to. address(0) only before the one-shot setRendererRegistry wiring (already executed on mainnet)."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"returnAuctionModule","anchor":"returnauctionmodule","text":"The only address that may call receivePunk and mintProofs. Immutable, set at construction (the deploy precomputes the module's CREATE address so the two contracts can reference each other)."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"supportsInterface","anchor":"supportsinterface","text":"ERC165. Advertises ERC721 (0x80ac58cd), ERC721Metadata (0x5b5e139f), ERC165 itself (0x01ffc9a7), and EIP-4906 (0x49064906) so marketplaces know to listen for MetadataUpdate."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"symbol","anchor":"symbol","text":"ERC721 symbol: PERMANENTCOLLECTION."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"TITLE_TOKEN_ID","anchor":"titletokenid","text":"Constant 111. Token id of the one-of-one Vault Title, sitting just past the Proof range."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"titleAuction","anchor":"titleauction","text":"The only address that may call mintToAuction. Wired once via the deployer one-shot setTitleAuction."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"titleMinted","anchor":"titleminted","text":"True iff the Vault Title (token id 111) has been minted."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"titleOwner","anchor":"titleowner","text":"Current holder of the Vault Title. Returns address(0) before the Title is minted; otherwise equivalent to ownerOf(111) (initially the title auction contract, which escrows it until its auction settles)."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"tokenURI","anchor":"tokenuri","text":"Token metadata JSON (data URI) for a minted Proof (ids 0..110) or the minted Title (id 111). Reverts UnknownTokenId(id) for an unminted Proof id or any id at or above 112, TitleNotMinted for id 111 before the Title exists, and RendererRegistryNotSet if the registry was never wired. Resolution delega"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"totalProofsMinted","anchor":"totalproofsminted","text":"Number of Proofs minted so far, as uint256 (same value as proofsMintedCount). Caps at 111."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"totalSupply","anchor":"totalsupply","text":"Total PunkVault-issued ERC721 tokens: minted Proofs plus the Title if minted (maximum 112). For marketplaces and indexers without ERC721Enumerable. Does not count vaulted Punks; that's lockedPunkCount."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Approval","anchor":"approval","text":"Standard ERC721 single-token approval event for Proofs and the Title."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"ApprovalForAll","anchor":"approvalforall","text":"Standard ERC721 operator-approval event."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"ContractURIUpdated","anchor":"contracturiupdated","text":"ERC-7572 collection-metadata refresh hint. Emitted on the Title mint and on every Proof mint, since each changes the collection-progress fields (totalSupply, the renderer's \"N of 111\" inscription, the Title JSON's progress attributes). Marketplaces that honor ERC-7572 re-fetch contractURI() on this "},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"MetadataUpdate","anchor":"metadataupdate","text":"EIP-4906 per-token metadata refresh hint. Emitted with the minted token's id on each mint, and with id 111 on every receivePunk once the Title exists (the Title's rendered attributes include the vaulted-Punk count). Indexers should re-fetch tokenURI( tokenId)."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"OwnershipTransferred","anchor":"ownershiptransferred","text":"ERC-173 ownership transition. Exactly two emissions are possible over the contract's lifetime: (address(0) → deployer) at construction and (deployer → address(0)) at renounceOwnership()."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"ProofMinted","anchor":"proofminted","text":"Emitted once per Proof, alongside the ERC721 Transfer from address(0). Indexers should read: tokenId (= traitId, 0..110), punkId (the Punk whose vaulting brought the trait in), recipient (the originalSeller who gave up the Punk), acquisitionId (0-based index into PermanentCollection's acquisition lo"},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"PunkLocked","anchor":"punklocked","text":"Emitted exactly once per Punk that enters the vault. By design there is no counterpart \"released\" event; the PunkLocked log is the canonical append-only list of vaulted Punks."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"RendererRegistrySet","anchor":"rendererregistryset","text":"Emitted once, at the one-shot setRendererRegistry wiring."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"TitleAuctionSet","anchor":"titleauctionset","text":"Emitted once, at the one-shot setTitleAuction wiring."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"TitleMinted","anchor":"titleminted","text":"Emitted once, at Title mint time, with the title auction contract as to. Mirrors the ERC721 Transfer(address(0), titleAuction, 111) for indexers that key on a protocol-specific event name."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Transfer","anchor":"transfer","text":"Standard ERC721 transfer event for Proofs and the Title, including the mint-time transfers from address(0). Never fired for Punks; Punk movements are events on the CryptoPunks market contract."},{"path":"/docs/contracts/punk-vault","page":"PunkVault","heading":"Errors","anchor":"errors","text":"AlreadyLocked(uint16 punkId) receivePunk was called for a Punk already registered as vaulted. Each Punk locks at most once; there is nothing for the caller to do. InvalidRecipient() mintProofs was called with recipient == address(0). Defense in depth: PermanentCollection.recordAcquisition already en"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"LiveBidAdapter","anchor":"","text":"0x8C72FBc2bB32e76aa54243F76745266a0F92CD01 · view on evm.now · deployed at block 25270161 LiveBidAdapter is the single inflow governor for the live bid. Every ETH source that funds the bid enters this contract first: the hook's bid-leg skim (plus 100% of the anti-sniper overage during the launch win"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Two forwarding modes","anchor":"two-forwarding-modes","text":"Every forward (from sweep() or streamForward()) reads Patron's balance and compares it to activationThreshold: - Fast mode (Patron balance below the threshold). No cooldown, no per-call cap. The forward is clamped so the bid lands exactly at the threshold; any remainder stays buffered and drips in u"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"One shared cooldown clock","anchor":"one-shared-cooldown-clock","text":"sweep() and streamForward() share a single lastSweepBlock. A throttled forward through either path arms the clock, so the combined rate of live-bid growth is bounded at maxSweepWei per minBlocksBetweenSweeps blocks no matter which path fires. Fast-mode forwards do not update lastSweepBlock: the thro"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"streamForward: the pre-swap path","anchor":"streamforward-the-pre-swap-path","text":"The adapter implements IPreSwapStream. The official pool's hook calls streamForward() in beforeSwap, so each swap advances the live bid with the buffered proceeds of prior swaps (the current swap's own skim arrives later, in afterSwap). The stream path is deliberately leaner than sweep(): - forwards"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Keeper reward on sweep","anchor":"keeper-reward-on-sweep","text":"sweep() pays its caller a reward of KEEPER REWARD BPS (0.5%) of the forwarded amount, capped at KEEPER REWARD CAP (0.01 ETH). On a fill-to-threshold fast-mode sweep the reward comes from the buffered remainder (capped at whatever remains); otherwise it is carved off the forward. If the reward send f"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"activationThreshold self-tracking","anchor":"activationthreshold-self-tracking","text":"sweep() first runs an internal sync that reads the records core (permanentCollection, an IPCAcquisitionReader). When a new acquisition has been recorded since the last sync, and that acquisition has the acceptBid shape (its acquirer equals its originalSeller), the threshold is overwritten with 75% o"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"contribute referral split","anchor":"contribute-referral-split","text":"contribute(referrer, tag) is the canonical attributed top-up surface. With a non-zero referrer, REFERRER CONTRIB BPS (500, 5%) of msg.value is sent to the referrer under a REFERRER GAS (35,000) gas budget; the remainder joins the buffer and meters into the live bid on the next forward. The split is "},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Reading sweep readiness","anchor":"reading-sweep-readiness","text":"`bash"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"ETH waiting in the buffer","anchor":"","text":"cast call 0x8C72FBc2bB32e76aa54243F76745266a0F92CD01 \"bufferedEth()(uint256)\" \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"first block at which the next THROTTLED forward is allowed","anchor":"","text":"cast call 0x8C72FBc2bB32e76aa54243F76745266a0F92CD01 \"nextSweepBlock()(uint256)\" \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"the fast/throttled boundary and the current throttle parameters","anchor":"","text":"cast call 0x8C72FBc2bB32e76aa54243F76745266a0F92CD01 \"activationThreshold()(uint256)\" \\ --rpc-url https://ethereum-rpc.publicnode.com cast call 0x8C72FBc2bB32e76aa54243F76745266a0F92CD01 \"maxSweepWei()(uint256)\" \\ --rpc-url https://ethereum-rpc.publicnode.com cast call 0x8C72FBc2bB32e76aa54243F76745"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Patron's balance decides the mode: below activationThreshold = fast mode","anchor":"","text":"cast balance $(cast call 0x8C72FBc2bB32e76aa54243F76745266a0F92CD01 \"patron()(address)\" \\ --rpc-url https://ethereum-rpc.publicnode.com) \\ --rpc-url https://ethereum-rpc.publicnode.com A sweep is worth sending when bufferedEth() 0 and either Patron's balance is below activationThreshold() (fast mode"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"sweep","anchor":"sweep","text":"Access: permissionless Forwards buffered native ETH to Patron and pays the caller a keeper reward. Step by step: 1. Syncs activationThreshold from the latest acceptBid clearing price (fail-open, no-op when nothing changed or auto-tracking is disabled) 2. Returns 0 if the buffer is empty (without con"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"streamForward","anchor":"streamforward","text":"Access: permissionless (implements IPreSwapStream; designed to be called by the official pool's hook in beforeSwap, but anyone may call) Lean forward of already-buffered native ETH into the live bid. No threshold sync, no keeper reward, no escrow interaction. Returns 0 without reverting when the buf"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"contribute","anchor":"contribute","text":"Access: permissionless, payable Attributed top-up into the live bid. Reverts ZeroValue when msg.value is 0. With referrer != address(0), sends 5% of msg.value (REFERRER CONTRIB BPS = 500) to the referrer with a 35,000-gas budget; if the send fails the share folds back into the buffer (fail-closed, t"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"poolReplenish","anchor":"poolreplenish","text":"Access: module-only (msg.sender must equal returnAuctionModule; anyone else reverts NotReturnAuction) Payable entry for the live-bid share of a cleared return auction (65% of the acquisition cost, plus any rerouted settle keeper reward). Gated to the module so the punk-keyed PoolReplenished event ca"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"setMaxSweepWei","anchor":"setmaxsweepwei","text":"Access: admin-only (ProtocolAdmin.checkAdmin; subject to the 1-year auto-lock, no carve-out) Sets the per-forward ETH ceiling used in throttled mode. Bounded to [MAX SWEEP WEI LO, MAX SWEEP WEI HI] (0.01 to 5 ETH); out-of-range values revert OutOfBounds. Emits ParameterChanged(\"maxSweepWei\", old, ne"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"setMinBlocksBetweenSweeps","anchor":"setminblocksbetweensweeps","text":"Access: admin-only (ProtocolAdmin.checkAdmin; subject to the 1-year auto-lock, no carve-out) Sets the cooldown, in blocks, between throttled forwards. Bounded to [MIN BLOCKS LO, MIN BLOCKS HI] (1 to 7,200 blocks, roughly every block up to about a day); out-of-range values revert OutOfBounds. Emits P"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"setActivationThreshold","anchor":"setactivationthreshold","text":"Access: admin-only carve-out (raw ProtocolAdmin.admin(), ignoring the 1-year timer; live until the role is burned via transferAdmin(address(0))) Manual override for the fast/throttled boundary. Bounded to [ACTIVATION THRESHOLD LO, ACTIVATION THRESHOLD HI] (0 to 100 ETH); above the cap reverts OutOfB"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"receive","anchor":"receive","text":"Access: permissionless Accepts native ETH into the buffer from any sender: the hook's bid-leg skim, the conversion locker's LP-fee forwards, and bare top-ups from anyone. Every inflow emits BareTopUp and meters into Patron on the next sweep() / streamForward(). Use contribute(referrer, tag) instead "},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"ACTIVATION_THRESHOLD_HI","anchor":"activationthresholdhi","text":"Upper bound on activationThreshold: 100 ETH (100e18). Both the setter and the auto-sync clamp to it."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"ACTIVATION_THRESHOLD_LO","anchor":"activationthresholdlo","text":"Lower bound on activationThreshold: 0. A zero threshold pins the adapter into throttled mode (every forward is rate-limited)."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"activationThreshold","anchor":"activationthreshold","text":"The Patron-balance level separating fast mode (below) from the throttle (at or above). Self-managed: each sweep() re-syncs it to 75% of the most recent acceptBid clearing price, clamped to 100 ETH, with setActivationThreshold as a bounded manual override in between syncs."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"adminContract","anchor":"admincontract","text":"The immutable ProtocolAdmin instance gating the setters."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"BPS","anchor":"bps","text":"Bps denominator, 10,000. Used by the keeper-reward and contribution-referrer splits."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"bufferedEth","anchor":"bufferedeth","text":"ETH currently buffered in the adapter (its raw balance), waiting to be forwarded to Patron over future sweep() / streamForward() calls."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"KEEPER_REWARD_BPS","anchor":"keeperrewardbps","text":"Keeper reward on sweep() as bps of the forwarded amount: 50 (0.5%)."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"KEEPER_REWARD_CAP","anchor":"keeperrewardcap","text":"Absolute cap on the keeper reward per sweep(): 0.01 ETH (1e16 wei). The reward is min(forward × 0.5%, 0.01 ETH)."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"lastSweepBlock","anchor":"lastsweepblock","text":"Block of the most recent throttled forward, from either sweep() or streamForward() (the two paths share this one clock). Fast-mode forwards don't update it."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"lastSyncedAcquisitionCount","anchor":"lastsyncedacquisitioncount","text":"High-water mark of permanentCollection.acquisitionCount() already examined by the threshold auto-sync. Advances on every successfully-read new acquisition, including skipped acceptListing rows, so a row is never re-examined. Never decreases."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"MAX_SWEEP_WEI_HI","anchor":"maxsweepweihi","text":"Upper bound on maxSweepWei: 5 ETH."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"MAX_SWEEP_WEI_LO","anchor":"maxsweepweilo","text":"Lower bound on maxSweepWei: 0.01 ETH."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"maxSweepWei","anchor":"maxsweepwei","text":"Current per-forward ETH ceiling in throttled mode. Excess buffer stays here for later calls. Not enforced below the activation threshold."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"MIN_BLOCKS_HI","anchor":"minblockshi","text":"Upper bound on minBlocksBetweenSweeps: 7,200 blocks (roughly one day)."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"MIN_BLOCKS_LO","anchor":"minblockslo","text":"Lower bound on minBlocksBetweenSweeps: 1 block (essentially no cooldown)."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"MIN_STREAM_WEI","anchor":"minstreamwei","text":"Dust floor for streamForward(): 0.01 ETH. Below it the stream path no-ops so tiny swaps don't trigger a forward. A fixed floor, not an admin knob."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"minBlocksBetweenSweeps","anchor":"minblocksbetweensweeps","text":"Current cooldown, in blocks, between throttled forwards. Not enforced below the activation threshold."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"nextSweepBlock","anchor":"nextsweepblock","text":"lastSweepBlock + minBlocksBetweenSweeps: the first block at which the next throttled forward is allowed. Fast-mode forwards and no-op calls are allowed at any block regardless of this value."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"patron","anchor":"patron","text":"The immutable Patron hub address. All forwarded ETH goes here; Patron's receive() accepts ETH only from this adapter."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"permanentCollection","anchor":"permanentcollection","text":"The records core, as an IPCAcquisitionReader. Read-only source for the threshold auto-sync. address(0) disables auto-tracking (the threshold is then the constructor seed plus manual sets)."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"REFERRER_CONTRIB_BPS","anchor":"referrercontribbps","text":"Referrer share of an attributed contribute() call, as bps of msg.value: 500 (5%). Hard-coded, no setter."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"REFERRER_GAS","anchor":"referrergas","text":"Gas budget for the outgoing send to the contribution referrer: 35,000."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"returnAuctionModule","anchor":"returnauctionmodule","text":"The immutable return-auction module address, the only authorized caller of poolReplenish. address(0) leaves poolReplenish permanently uncallable."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"ActivationThresholdSynced","anchor":"activationthresholdsynced","text":"Emitted when the auto-sync inside sweep() processes a new acceptBid acquisition. clearingPrice is the raw recorded acquisition price, applied is the value written to activationThreshold (75% of the price, clamped to 100 ETH), and acquisitionCount is the records core's count at sync time. acceptListi"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"BareTopUp","anchor":"baretopup","text":"Emitted on every direct ETH send into receive(): the hook's bid-leg skim, the locker's LP-fee forwards, and unattributed top-ups. sender is indexed; amount is the wei received. The ETH joins the buffer and appears in a later Swept."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Contribution","anchor":"contribution","text":"Emitted on every contribute() call. contributor, referrer, and tag are indexed; amount is the full msg.value; referrerShare is the wei actually paid to the referrer (0 on a no-referrer call or when the referrer's send failed and the share stayed buffered). Read referrerShare rather than recomputing "},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"KeeperReward","anchor":"keeperreward","text":"Emitted when the sweep() caller's reward send succeeds. caller is indexed; amount is the reward paid."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"KeeperRewardFailed","anchor":"keeperrewardfailed","text":"Emitted when the reward send fails (the caller can't receive ETH). The sweep itself succeeded; the reward ETH stays buffered for the next sweep."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"ParameterChanged","anchor":"parameterchanged","text":"Emitted by the three setters. key (indexed) is the parameter name as a short bytes32 string (\"maxSweepWei\", \"minBlocksBetweenSweeps\", \"activationThreshold\"), with oldValue and newValue."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"PoolReplenished","anchor":"poolreplenished","text":"Emitted when the return-auction module routes a cleared auction's live-bid share into the buffer via poolReplenish. punkId (indexed) keys the event to the settled auction; amount is the wei received. Unspoofable: only the module can call the emitting function."},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Swept","anchor":"swept","text":"Emitted on every non-zero forward, from sweep() and streamForward() alike. ethSwept is the buffer at call time, ethForwarded is what reached Patron, ethBuffered is what remains after the forward and any keeper reward. ethSwept - ethForwarded - ethBuffered deltas reveal the reward paid on a sweep; st"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"ThresholdCrossed","anchor":"thresholdcrossed","text":"Emitted exactly when a forward takes Patron's balance from below activationThreshold to at or above it. Fires at most once per crossing; it can fire again only after the live bid drops back below (for example, spent by acceptBid) and crosses again. Useful for \"the live bid has entered realistic Punk"},{"path":"/docs/contracts/live-bid-adapter","page":"LiveBidAdapter","heading":"Errors","anchor":"errors","text":"ForwardFailed() The ETH forward to Patron failed. Structurally unreachable in production (Patron accepts the adapter as its sole permitted sender); if it surfaces, the wiring is wrong. InSwap() The call arrived during an authorized extension's swap window (PCSwapContext.inSwap is set). Dormant at la"},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"BuybackBurner","anchor":"","text":"0xf8a2D6F8c58626eE3BcDb4638F2a2f30Fe021242 · view on evm.now · deployed at block 25270161 BuybackBurner converts queued ETH into $111 and removes it from circulation. It receives ETH from cleared return-auction settles, from VaultBurnPool sweeps, and from any direct send to its receive() function. A"},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Step pacing","anchor":"step-pacing","text":"Steps are paced by block number. executeStep reverts StepTooEarly until block.number = lastStepBlock + minBlocksBetweenSteps. lastStepBlock is written to the current block BEFORE the contract enters the V4 unlock callback, so a same-transaction re-entry of executeStep (for example from the keeper-re"},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Step sizing and the price-impact cap","anchor":"step-sizing-and-the-price-impact-cap","text":"Each step draws min(remainingEth, maxStepWei) from the queue. The swap is protected by two independent bounds: - maxStepWei caps the ETH offered per step (admin-tunable within [MAX STEP WEI LO, MAX STEP WEI HI], that is 0.01 to 10 ETH) - maxSlippageBps = 500 is a compile-time constant with no setter"},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Caller reward","anchor":"caller-reward","text":"The caller earns min(step × 50 / 10 000, 0.01 ETH), that is at most 0.5% of the step and never more than 0.01 ETH (EXEC REWARD BPS, EXEC REWARD CAP). The reward is deducted from the step before the swap and pro-rated to the ETH actually consumed on a partial fill, so a clamped swap pays a proportion"},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Checking step readiness","anchor":"checking-step-readiness","text":"A step is executable when the pacing window has elapsed AND there is queued ETH. Read both in one shot: The step is ready when the current block number is at or past nextExecutableBlock() and quoteStepAmount() is nonzero."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Executing a step (viem)","anchor":"executing-a-step-viem","text":""},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"executeStep","anchor":"executestep","text":"Access: permissionless Swaps up to maxStepWei of queued ETH for $111 in the official pool and burns everything received, paying the caller a bounded ETH reward. Step by step: 1. Reverts StepTooEarly(nextBlock) if block.number < lastStepBlock + minBlocksBetweenSteps 2. Sizes the step at min(remaining"},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"setup","anchor":"setup","text":"Access: deployer one-shot, gated by OneTimeSetup (original deployer only, before finalization) Wires the $111 token address and the official pool's V4 hook address in a single call, then permanently closes the setup gate ( markFinalized, which emits Finalized). Reverts NotDeployer for any other call"},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"setMinBlocksBetweenSteps","anchor":"setminblocksbetweensteps","text":"Access: admin-only, ProtocolAdmin.checkAdmin(msg.sender); locks at the 1-year admin expiry Sets the minimum block delta between executeStep calls. Reverts OutOfBounds outside [MIN BLOCKS LO, MIN BLOCKS HI] (1 to 50,400 blocks, the upper bound about one week). Emits ParameterChanged with key \"minBloc"},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"setMaxStepWei","anchor":"setmaxstepwei","text":"Access: admin-only, ProtocolAdmin.checkAdmin(msg.sender); locks at the 1-year admin expiry Sets the ETH ceiling for a single step's swap. Reverts OutOfBounds outside [MAX STEP WEI LO, MAX STEP WEI HI] (0.01 to 10 ETH). Emits ParameterChanged with key \"maxStepWei\". Acts as a soft impact guard alongsi"},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"unlockCallback","anchor":"unlockcallback","text":"Access: PoolManager-only, reverts NotPoolManager for any other caller The V4 unlock callback invoked by PoolManager.unlock during executeStep. Decodes the swap amount, builds the pool key (native ETH is always currency0, so the swap is always zeroForOne), reads the current sqrtPriceX96 from slot0, a"},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"receive","anchor":"receive","text":"Access: permissionless, accepts ETH from any sender Credits msg.value to remainingEth and emits BurnEthDeposited. This is how all inflows arrive: the cleared return-auction settle share, the VaultBurnPool ETH sweep, and voluntary top-ups."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"adminContract","anchor":"admincontract","text":"The ProtocolAdmin contract gating the two parameter setters. Immutable, set at construction."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"EXEC_REWARD_BPS","anchor":"execrewardbps","text":"Constant 50. The caller reward as basis points of the step size (0.5%)."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"EXEC_REWARD_CAP","anchor":"execrewardcap","text":"Constant 0.01 ether. The absolute cap on a single step's caller reward."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"hook","anchor":"hook","text":"The official pool's V4 hook address, used in the pool key. Zero until setup runs."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"lastStepBlock","anchor":"laststepblock","text":"Block number of the most recent executeStep. Combined with minBlocksBetweenSteps to pace steps."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"MAX_STEP_WEI_HI","anchor":"maxstepweihi","text":"Constant 10 ether. Upper bound for setMaxStepWei."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"MAX_STEP_WEI_LO","anchor":"maxstepweilo","text":"Constant 0.01 ether. Lower bound for setMaxStepWei."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"maxSlippageBps","anchor":"maxslippagebps","text":"Constant 500. The per-call price-impact cap in basis points (5%), applied through the swap's sqrtPriceLimitX96. Compile-time constant with no setter. When the limit binds, V4 partial-fills the swap and the unspent ETH stays queued."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"maxStepWei","anchor":"maxstepwei","text":"Current ETH ceiling for a single step's swap. Admin-tunable within [0.01, 10] ETH until the 1-year lock."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"MIN_BLOCKS_HI","anchor":"minblockshi","text":"Constant 50 400 (about one week of blocks). Upper bound for setMinBlocksBetweenSteps."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"MIN_BLOCKS_LO","anchor":"minblockslo","text":"Constant 1. Lower bound for setMinBlocksBetweenSteps. Load-bearing for reentrancy safety: it guarantees a same-tx reentrant executeStep reverts StepTooEarly."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"minBlocksBetweenSteps","anchor":"minblocksbetweensteps","text":"Current minimum block delta between steps. Admin-tunable within [1, 50 400] until the 1-year lock."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"nextExecutableBlock","anchor":"nextexecutableblock","text":"The earliest block at which executeStep will succeed: lastStepBlock + minBlocksBetweenSteps. Keepers should compare against the current block number before submitting."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"poolFee","anchor":"poolfee","text":"The V4 pool fee field used in the pool key. May be a dynamic-fee sentinel; the actual fee is read from the hook at swap time. Immutable."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"poolKey","anchor":"poolkey","text":"The full V4 PoolKey for the official pool (currency0 = native ETH, currency1 = the $111 token, plus fee, tick spacing, and hook). Useful for off-chain tooling that reads pool state directly."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"poolManager","anchor":"poolmanager","text":"The V4 singleton PoolManager. Immutable."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"poolTickSpacing","anchor":"pooltickspacing","text":"The V4 pool tick spacing used in the pool key. Immutable."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"quoteStepAmount","anchor":"quotestepamount","text":"ETH that would be drawn by the next step, including the caller-reward share: min(remainingEth, maxStepWei). Zero means executeStep would revert NothingToBurn."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"remainingEth","anchor":"remainingeth","text":"ETH queued for swap and burn. Credited by every inflow through receive(), debited by each step's actual spend, and re-credited with the reward share on the rare failed reward send. Tracks address(this).balance."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"setupFinalized","anchor":"setupfinalized","text":"Whether setup has run and permanently closed the one-shot wiring gate. Off-chain tooling should confirm this is true before treating token and hook as permanent."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"token","anchor":"token","text":"The $111 token address. Zero until setup runs. The burn target: after each swap the contract calls burn on this token with everything received."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"totalEthBurned","anchor":"totalethburned","text":"Monotonic counter of total ETH ever spent on swaps (excludes the caller-reward share)."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"totalTokensBurned","anchor":"totaltokensburned","text":"Monotonic counter of total $111 ever delivered to burn."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"BurnEthDeposited","anchor":"burnethdeposited","text":"Emitted on every ETH inflow through receive(). source (indexed) is the sender, amount the deposit, remainingEth the queue balance after the credit. Indexers can attribute inflows by source: the return-auction module (cleared settles), VaultBurnPool (sweeps), or anything else (direct top-ups)."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"ExecutionRewardFailed","anchor":"executionrewardfailed","text":"Emitted when the caller-reward send fails. The reward ETH is credited back to remainingEth, so nothing leaves accounting; the burn itself already succeeded. caller is the executeStep sender, amount the unsent reward."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"ExecutionRewardPaid","anchor":"executionrewardpaid","text":"Emitted when the caller reward is sent successfully. caller (indexed) is the executeStep sender, amount the ETH paid (pro-rated on a partial fill)."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Finalized","anchor":"finalized","text":"Emitted exactly once, when setup completes and permanently closes the one-shot wiring gate."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"ParameterChanged","anchor":"parameterchanged","text":"Emitted by both setters. key (indexed) is the parameter name as a bytes32 string (\"minBlocksBetweenSteps\" or \"maxStepWei\"), with the old and new values."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"TokensBurned","anchor":"tokensburned","text":"Emitted on every successful step. ethSpent is the ETH actually consumed by the swap (may be less than the step size on a partial fill), tokensBurned the $111 delivered to burn, remainingEth the queue balance after the debit. The canonical event for tracking burn progress."},{"path":"/docs/contracts/buyback-burner","page":"BuybackBurner","heading":"Errors","anchor":"errors","text":"AlreadyFinalized() setup was called after the one-shot gate closed. The wiring is permanent; there is no retry. ExcessInputSpent(uint256 ethSpent, uint256 swapAmount) The V4 swap reported more ETH consumed than the exact-input amount requested. Defends against a hook with return-delta permissions by"},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"VaultBurnPool","anchor":"","text":"0xf5c3eC7e185d0a592264791D523496EA6e368753 · view on evm.now · deployed at block 25270161 Burn accumulator released on every vault-path settle. The pool holds two assets. First, ETH: ReturnAuctionModule.settle forwards the vault-burn share of every cleared return auction here (the full clearing prem"},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"Two legs, one trigger, strict ordering","anchor":"two-legs-one-trigger-strict-ordering","text":"sweep() runs the $111 leg first and treats it as required: burning the contract's own balance can't revert, so the side-pool tax burn is guaranteed to complete on every vault-path settle. The ETH leg runs second and is best-effort: if the forward to BuybackBurner fails, the ETH simply stays in the p"},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"Where the balances come from","anchor":"where-the-balances-come-from","text":"The ETH side accumulates from cleared return auctions: on each cleared settle, ReturnAuctionModule sends (highBid - cost) minus any auction referrer share, plus cost x 1 000 / 10 000, to this pool. Direct sends to receive() add to the same balance and compound the next vault outcome's impulse. The $"},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"Reading the pool","anchor":"reading-the-pool","text":"`bash"},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"ETH pending for the next vault-path sweep (wei)","anchor":"","text":"cast call 0xf5c3eC7e185d0a592264791D523496EA6e368753 \"balance()(uint256)\" \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"accrued $111 awaiting burn","anchor":"","text":"cast call 0x61C9d89fe1212F6b55fF888816A151463287B8ae \"balanceOf(address)(uint256)\" 0xf5c3eC7e185d0a592264791D523496EA6e368753 \\ --rpc-url https://ethereum-rpc.publicnode.com `"},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"sweep","anchor":"sweep","text":"Access: returnAuctionModule-only (any other caller reverts NotReturnAuctionModule) Releases both legs. Step one, the $111 burn: if token is wired and the pool holds a non-zero $111 balance, the full balance is burned via token.burn (total supply drops) and SidePoolTaxBurned is emitted. Step two, the"},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"setup","anchor":"setup","text":"Access: deployer one-shot (onlySetup: deployer-only until finalized, then closed forever) Wires the $111 token whose transfer-tax proceeds burn here. Callable once, by the deployer captured at construction; reverts ZeroAddress for a zero token, NotDeployer for any other caller, and AlreadyFinalized "},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"receive","anchor":"receive","text":"Access: permissionless Accepts ETH from any sender. The routine inflow is ReturnAuctionModule forwarding the cleared-path vault-burn share, but direct top-ups from anyone are accepted and simply enlarge what the next vault-path sweep forwards to BuybackBurner. No state mutation and no event, to keep"},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"balance","anchor":"balance","text":"Current ETH balance in wei, equivalent to what the next sweep would attempt to forward to BuybackBurner. The accrued $111 side isn't covered by this view; read balanceOf(pool) on the token for that."},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"buybackBurner","anchor":"buybackburner","text":"The immutable BuybackBurner address that receives every swept ETH balance. Set at construction, never changes."},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"returnAuctionModule","anchor":"returnauctionmodule","text":"The immutable ReturnAuctionModule address, the only account allowed to call sweep. Set at construction, never changes."},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"setupFinalized","anchor":"setupfinalized","text":"Whether the one-shot setup has run. true means token is wired permanently and the setup surface is closed; false means the $111 burn leg is still dormant and the deployer can still call setup."},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"token","anchor":"token","text":"The $111 token wired by setup, whose venue-tax proceeds accrue here and burn on each vault-path sweep. address(0) until setup runs."},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"Finalized","anchor":"finalized","text":"Emitted exactly once, by the successful setup call. After this event the pool's wiring is permanent; off-chain tooling can treat token as final."},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"SidePoolTaxBurned","anchor":"sidepooltaxburned","text":"Emitted on each sweep that burned a non-zero accrued $111 balance, with the amount burned. Each occurrence is a real total-supply reduction of the $111 token, so an indexer can sum these for the tax-burn contribution to cumulative supply reduction, distinct from BuybackBurner's buy-and-burn."},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"Swept","anchor":"swept","text":"Emitted on each sweep whose ETH forward to BuybackBurner succeeded, with the amount forwarded. A sweep with a zero ETH balance, or one whose forward failed (balance retained for next time), emits no Swept."},{"path":"/docs/contracts/vault-burn-pool","page":"VaultBurnPool","heading":"Errors","anchor":"errors","text":"AlreadyFinalized() setup was called after it already ran once. The token wiring is permanent; there's nothing further to configure. InSwap() A notInSwap-decorated function was entered while PCSwapContext reports a swap in progress. Unreachable at launch (no authorized extension is bound, so the flag"},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"ProtocolFeePhaseAdapter","anchor":"","text":"0xed3E9D3Bf693372060b7ce62aDB49650145b2ba9 · view on evm.now · deployed at block 25270161 Lean forwarder for the protocol leg of the official pool's fee skim. On every swap, the artcoins skim hook splits its 6% baseline skim into a live-bid leg and a protocol leg; the protocol leg (about 16.67% of t"},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"Escrow-pull, not hook-push","anchor":"escrow-pull-not-hook-push","text":"The hook never sends ETH directly to this adapter during a swap. It deposits the protocol leg into the fee escrow under this adapter's address, keeping the per-swap flush cheap and the adapter out of the swap's call graph. Anyone can then batch an arbitrary number of swaps' worth of accrual into one"},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"Checking and draining the accrual","anchor":"checking-and-draining-the-accrual","text":"`bash"},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"ETH already claimed onto the adapter (wei)","anchor":"","text":"cast balance 0xed3E9D3Bf693372060b7ce62aDB49650145b2ba9 \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"ETH still waiting in the fee escrow under the adapter's address","anchor":"","text":"cast call <feeEscrow \"availableFees(address,address)(uint256)\" \\ 0xed3E9D3Bf693372060b7ce62aDB49650145b2ba9 0x0000000000000000000000000000000000000000 \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"drain both to the controller (permissionless)","anchor":"","text":"cast send 0xed3E9D3Bf693372060b7ce62aDB49650145b2ba9 \"sweep()\" \\ --rpc-url https://ethereum-rpc.publicnode.com --private-key $PRIVATE KEY Read <feeEscrow from the adapter itself with cast call 0xed3E9D3Bf693372060b7ce62aDB49650145b2ba9 \"feeEscrow()(address)\". The address(0)` second argument is the e"},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"sweep","anchor":"sweep","text":"Access: permissionless Claims the adapter's accrued native-ETH balance from the fee escrow (claim(address(this), address(0)), best-effort: a reverting claim emits ClaimFailed and execution continues), then forwards the adapter's entire ETH balance to the controller. Returns silently if the balance i"},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"receive","anchor":"receive","text":"Access: permissionless Accepts native ETH from any sender with no state mutation and no event. The hook doesn't use this path (it deposits into the escrow instead); it exists to catch direct or stray sends, which simply join the balance forwarded by the next sweep. ETH sent here can only exit toward"},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"controller","anchor":"controller","text":"The immutable protocol fee controller address (PC's dedicated artcoins ProtocolFeeController instance, configured 86.67% treasury / 13.33% LAYER burn). Every sweep forwards to it. Set at construction, never changes."},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"feeEscrow","anchor":"feeescrow","text":"The immutable artcoins fee escrow address the hook deposits the protocol leg into, under this adapter's address. sweep claims from it before forwarding. Set at construction, never changes."},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"ClaimFailed","anchor":"claimfailed","text":"Emitted when the escrow claim inside sweep reverts, with the escrow address. Non-fatal: the same call still forwards whatever ETH is already on the adapter. Repeated occurrences point at an escrow-side problem worth investigating; the escrowed accrual itself is untouched and claimable once the escro"},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"Forwarded","anchor":"forwarded","text":"Emitted on every successful sweep, with the recipient (the controller, indexed) and the amount forwarded. An indexer can sum these for the total protocol-leg ETH delivered to the controller."},{"path":"/docs/contracts/protocol-fee-phase-adapter","page":"ProtocolFeePhaseAdapter","heading":"Errors","anchor":"errors","text":"ForwardFailed() The controller rejected the ETH transfer at the end of sweep. The whole call reverted, so the balance remains on the adapter; retry once the controller accepts ETH again. InSwap() A notInSwap-decorated function was entered while PCSwapContext reports a swap in progress. Unreachable a"},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"ReferralPayout","anchor":"","text":"0xB03Cbd862F47059e928C113182814c676eA29d4c · view on evm.now · deployed at block 25270161 Pull-based per-referrer ETH ledger for swap referrals on the official pool. When a swap carries valid attribution hookData (a PCAttribution with a non-zero referrer), the artcoins skim hook computes the referra"},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"Where credits come from","anchor":"where-credits-come-from","text":"The referral slice is deducted from the protocol leg of the hook's 6% baseline skim, never from the live-bid leg. It pays from the first swap: any swap whose hookData decodes to a valid PCAttribution with a non-zero referrer produces a notify call within that swap's afterSwap flush. Per-swap context"},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"The 35k-gas send budget","anchor":"the-35k-gas-send-budget","text":"claim and claimFor send with a fixed CLAIM GAS budget of 35,000 gas, enough for most contract receivers (Safes, splitters) while capping what a pathological recipient can burn. If the send reverts or runs out of gas the balance is reinstated in full and the call reverts TransferFailed, so a failed c"},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"Reading and claiming with viem","anchor":"reading-and-claiming-with-viem","text":"Or from the command line:"},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"claim","anchor":"claim","text":"Access: permissionless Pays out balances[msg.sender] to msg.sender. Zeroes the balance, then sends the full amount with a 35,000-gas budget. Reverts NothingToClaim if the caller's balance is zero. If the send fails (recipient reverts or exceeds the gas budget) the balance is reinstated and the call "},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"claimFor","anchor":"claimfor","text":"Access: permissionless Same payout path as claim, but for an arbitrary referrer: the ETH always goes to the referrer address, never to the caller. Useful for keepers, frontends that sweep house balances, or pushing funds to a referrer that can't easily initiate transactions. Reverts NothingToClaim f"},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"notify","anchor":"notify","text":"Access: hook-only (the immutable hook address set at construction; every other caller reverts Unauthorized) The single credit path. The skim hook calls this with the swap's referral slice attached as msg.value during the same swap's flush. Credits balances[referrer] += msg.value and emits ReferralCr"},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"receive","anchor":"receive","text":"Access: permissionless Stray-ETH catcher. Anyone can send ETH here directly; it increases the contract's balance but is not credited to any referrer and cannot be claimed. Intentional: the hook's notify is the only authoritative source of credits, so accidental sends can't be mis-attributed. Don't s"},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"balances","anchor":"balances","text":"Claimable ETH (wei) per referrer address. Increases on each hook notify credit, zeroes on a successful claim/claimFor, reinstated on a failed one. This is the number to surface in a referrer dashboard."},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"CLAIM_GAS","anchor":"claimgas","text":"Gas budget for the per-claim send, a constant 35 000. Generous enough for most contract receivers, capped so a malicious recipient can't burn unbounded gas in claim/claimFor."},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"hook","anchor":"hook","text":"The immutable artcoins skim hook address authorized to call notify. Set at construction, never changes."},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"ReferralClaimed","anchor":"referralclaimed","text":"Emitted on every successful claim/claimFor with the referrer (indexed) and the amount paid out. An indexer can pair ReferralCredited minus ReferralClaimed totals per referrer to reconstruct the live claimable balance without a contract read."},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"ReferralCredited","anchor":"referralcredited","text":"Emitted on every hook notify credit with the referrer (indexed) and the amount added to their balance. One event per attributed swap. For per-swap context (sourceId, campaignId, volume), join against the hook's attribution events from the same transaction."},{"path":"/docs/contracts/referral-payout","page":"ReferralPayout","heading":"Errors","anchor":"errors","text":"NothingToClaim() claim/claimFor was called for a referrer with a zero balance. Check balances(referrer) before claiming. TransferFailed() The payout send to the referrer reverted or exceeded the 35,000-gas budget. The balance was reinstated in full; fix the recipient's receive handler and retry. Una"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"PunkVaultTitleAuction","anchor":"","text":"0xA7139153A88DA7C2E2304F5Ce39026676Ebf45fd · view on evm.now · deployed at block 25270161 One-shot, permissionless English auction that sells the Vault Title, ERC721 token id 111 on PunkVault. The Title sits just past the 111 Proofs (token ids 0..110) and is a single one-of-one. The auction contract"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Title mint is decoupled from the auction","anchor":"title-mint-is-decoupled-from-the-auction","text":"The Title is minted into this contract's escrow at launch by the permissionless, idempotent mintTitle() (a no-op once titleMinted is set), so token id 111 exists and its tokenURI and marketplace page resolve from the start. Minting does NOT open bidding. The auction opens separately via kickoff(), w"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Lifecycle","anchor":"lifecycle","text":"1. Pre-kickoff. The Title sits in escrow here. Bidding is closed; bid() reverts AuctionNotLive. kickoff() becomes callable once collection.collectedCount() = KICKOFF THRESHOLD (22 collected traits; each vaulted Punk collects exactly one trait, so this is also 22 Punks vaulted). isKickoffReady() repo"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Pull-based ETH movement","anchor":"pull-based-eth-movement","text":"The contract never lets an unwilling recipient block state transitions: - Outbid refunds are pushed with a 30,000-gas call; if the push fails, the amount accrues in pendingRefund[bidder] for later withdrawRefund() - Settle proceeds are never pushed. settle only credits pendingProceeds[payoutRecipien"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Reading the auction state","anchor":"reading-the-auction-state","text":""},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"bid","anchor":"bid","text":"Access: permissionless (payable) Place a bid of msg.value wei. Reverts AuctionNotLive before kickoff() or after a winning settle, AuctionEnded at or past endsAt, and ZeroBid for a zero-value call. The bid must be strictly greater than highBidWei (BidNotHigherThanCurrent) AND at least highBidWei 10 5"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"settle","anchor":"settle","text":"Access: permissionless Finalize (or restart) the auction once block.timestamp = endsAt. Reverts AuctionNotLive before kickoff, AlreadySettled after a winning settle, and AuctionLive while the clock is still running. With a high bidder: sets settled, credits 100% of highBidWei to pendingProceeds[payo"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"kickoff","anchor":"kickoff","text":"Access: permissionless, gated on the collected-trait threshold Start the 24-hour auction clock. Reverts AlreadyKickedOff on a repeat call and ThresholdNotReached while collection.collectedCount() < KICKOFF THRESHOLD (22). Sets kickedOff, sets endsAt = block.timestamp + AUCTION DURATION, mints the Ti"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"mintTitle","anchor":"minttitle","text":"Access: permissionless, idempotent Mint the Title (token id 111) from PunkVault into this auction's escrow. Called once at launch so the Title exists and its tokenURI and marketplace page resolve from the start, independent of the auction. A no-op if titleMinted is already set, so repeat calls succe"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"withdrawRefund","anchor":"withdrawrefund","text":"Access: permissionless, pays msg.sender's own queued refund Pull the caller's queued outbid refund (amounts that failed the 30,000-gas push during bid). Zeroes pendingRefund[msg.sender] before sending the full balance with an unbounded call. Reverts NothingToWithdraw on a zero balance and TransferFa"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"withdrawProceeds","anchor":"withdrawproceeds","text":"Access: permissionless, funds always go to the credited recipient Pull queued settle proceeds for recipient (in practice the immutable payoutRecipient, credited 100% of the high bid at settle). Anyone may trigger the transfer; the ETH always goes to the credited address, so proceeds stay claimable e"},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"withdrawProceeds","anchor":"withdrawproceeds","text":"Access: permissionless, pays msg.sender's own credit Convenience wrapper equivalent to withdrawProceeds(msg.sender). Use the withdrawProceeds(address) overload to pull on behalf of another credited recipient."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"AUCTION_DURATION","anchor":"auctionduration","text":"Constant 24 hours (86,400 seconds). The span from kickoff to the initial endsAt, and the length of each no-bid restart extension."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"collection","anchor":"collection","text":"The immutable PermanentCollection records core, read for collectedCount() in the kickoff gate."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"endsAt","anchor":"endsat","text":"Current auction deadline as a unix timestamp (uint64). Zero before kickoff. Moves forward on anti-snipe extensions and on no-bid settle restarts."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"highBidder","anchor":"highbidder","text":"Address of the current high bidder. address(0) before the first bid."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"highBidWei","anchor":"highbidwei","text":"Current high bid in wei (uint128). Zero before the first bid."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"isKickoffReady","anchor":"iskickoffready","text":"True iff kickoff() would succeed right now: not yet kicked off and collection.collectedCount() = 22."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"isLive","anchor":"islive","text":"True iff the auction is currently accepting bids: kicked off, not settled, and block.timestamp < endsAt."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"isSettleable","anchor":"issettleable","text":"True iff settle() would succeed right now: kicked off, not settled, and block.timestamp = endsAt. Note the no-bidder case is still \"settleable\"; that call restarts the clock rather than finalizing."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"kickedOff","anchor":"kickedoff","text":"True once kickoff() has run. Never resets."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"KICKOFF_THRESHOLD","anchor":"kickoffthreshold","text":"Constant 22. Number of collected traits required before kickoff() is callable. Each vaulted Punk collects exactly one trait, so this equals 22 Punks vaulted."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"MIN_INCREASE_BPS","anchor":"minincreasebps","text":"Constant 500 (5% on a 10,000 denominator). Minimum increment a new bid must clear over the current high; the bid must also be strictly greater in wei terms."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"minNextBid","anchor":"minnextbid","text":"Minimum acceptable bid right now: highBidWei 10 500 / 10 000, rounded down by integer division. Returns 0 before any bids; the first bid only needs to be non-zero. Because bid() also requires strict wei superiority, send at least max(minNextBid(), highBidWei() + 1)."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"payoutRecipient","anchor":"payoutrecipient","text":"The immutable address credited 100% of the winning bid at settle. Set once at construction; no rotation path, no admin path. It is a payout destination chosen at deploy time (any EOA, multisig, or splitter)."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"pendingProceeds","anchor":"pendingproceeds","text":"Per-address pull-queue balance for settle proceeds, in wei. Nonzero for payoutRecipient between a winning settle and the corresponding withdrawProceeds claim."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"pendingRefund","anchor":"pendingrefund","text":"Per-address pull-queue balance for outbid refunds whose 30,000-gas push failed, in wei. Claimed by the bidder via withdrawRefund()."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"settled","anchor":"settled","text":"True once a winning settle() has run. Stays false through no-bid restarts."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"SNIPE_EXTENSION","anchor":"snipeextension","text":"Constant 1 hours (3,600 seconds). How far past block.timestamp the deadline moves when a bid lands inside the trigger window."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"SNIPE_TRIGGER_WINDOW","anchor":"snipetriggerwindow","text":"Constant 15 minutes (900 seconds). A bid with less than this remaining before endsAt triggers the anti-snipe extension."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"TITLE_TOKEN_ID","anchor":"titletokenid","text":"Constant 111. Token id of the Vault Title on PunkVault, just past the 111 Proofs (ids 0..110). Mirrors PunkVault.TITLE TOKEN ID."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"titleMinted","anchor":"titleminted","text":"True once the Title (token id 111) has been minted into this auction's escrow via mintTitle() (or the fallback mint inside kickoff()). Never resets. Independent of the auction state: true does not imply bidding is open."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"TRAIT_COUNT","anchor":"traitcount","text":"Constant 111. Total trait bits; matches PermanentCollection.TRAIT COUNT."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"vault","anchor":"vault","text":"The immutable PunkVault that mints the Title into this contract's escrow and from which the Title transfers to the winner at settle."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Bid","anchor":"bid","text":"Emitted on every accepted bid. endsAt reflects the deadline AFTER any anti-snipe extension this bid triggered, so an indexer can drive its countdown from this event alone."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Extended","anchor":"extended","text":"Emitted when an anti-snipe extension moved endsAt further into the future, alongside the Bid event that triggered it."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Kickoff","anchor":"kickoff","text":"Emitted when the auction clock (re)starts: once from kickoff(), and again from every no-bid settle() restart. Indexers should treat each Kickoff as a deadline refresh, not assume it fires once."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"ProceedsQueued","anchor":"proceedsqueued","text":"Emitted at a winning settle when 100% of the high bid is credited to payoutRecipient in the pull queue. The ETH has not moved yet."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"ProceedsWithdrawn","anchor":"proceedswithdrawn","text":"Emitted when a queued proceeds credit was successfully pulled via withdrawProceeds."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"RefundQueued","anchor":"refundqueued","text":"Emitted during bid() when the 30,000-gas refund push to the outbid bidder failed and the amount was queued in pendingRefund instead."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"RefundWithdrawn","anchor":"refundwithdrawn","text":"Emitted when a queued refund was successfully pulled via withdrawRefund()."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Settled","anchor":"settled","text":"Emitted on the winning settle path: the Title transferred to winner and highBid was credited to payoutRecipient. Terminal; at most one per deployment."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"SettledNoBidder","anchor":"settlednobidder","text":"Emitted on the no-bidder settle path. The auction did not finalize; it extended by another AUCTION DURATION and re-emitted Kickoff."},{"path":"/docs/contracts/title-auction","page":"PunkVaultTitleAuction","heading":"Errors","anchor":"errors","text":"AlreadyKickedOff() kickoff() was called after the auction clock already started. Nothing to do; read endsAt for the current deadline. AlreadySettled() settle() was called after a winning settle. The auction is over. AuctionEnded() bid() was called at or after endsAt. Call settle() instead (or wait f"},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"PCSwapContext","anchor":"","text":"0xf271a4aFAA206f5EBE392CC68845aB6F11Af2Ed3 · view on evm.now · deployed at block 25270161 Shared reentrancy-detection registry for the protocol. It exposes a single boolean, inSwap, held in EIP-1153 transient storage, that an authorized extension sets before running swap callbacks and clears after. "},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"Why this exists: synchronous swap-callback composability","anchor":"why-this-exists-synchronous-swap-callback-composability","text":"The skim hook on the official pool carries an unbound extension slot. The protocol's composability design lets a future dispatcher occupy that slot and invoke third-party callbacks synchronously inside each swap's afterSwap. Those callbacks are untrusted code running mid-swap, so every fund-moving p"},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"The decorated surface","anchor":"the-decorated-surface","text":"Seven protocol contracts inherit the notInSwap modifier, which reads inSwap() here and reverts if true: Contract Functions --- --- Patron acceptBid, acceptListing ReturnAuctionModule placeBid, placeBidWithReferral, settle, withdrawRefund BuybackBurner executeStep LiveBidAdapter contribute, poolReple"},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"Activation and lock","anchor":"activation-and-lock","text":"Arming the guard is a deliberate, reversible-until-locked sequence by the owner: 1. setAuthorizedExtension(dispatcher) authorizes the dispatcher to toggle the flag. Re-callable, and address(0) revokes 2. lockAuthorizedExtension() freezes the binding forever. After this, setAuthorizedExtension revert"},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"Reading the state","anchor":"reading-the-state","text":"inSwap read from off-chain is always false (transient storage only holds a value inside a transaction); the meaningful off-chain reads are the extension binding and its lock."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"enterSwap","anchor":"enterswap","text":"Access: extension-only (msg.sender must equal the current authorizedExtension, which must be non-zero; otherwise reverts NotAuthorizedExtension) Sets the transient inSwap flag to true and emits SwapEntered. Called by the authorized dispatcher immediately before it runs its callback loop, so that eve"},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"exitSwap","anchor":"exitswap","text":"Access: extension-only (same gate as enterSwap) Clears the transient inSwap flag and emits SwapExited. Called by the authorized dispatcher after its callback loop completes. Even if this call were skipped, the flag would auto-clear at transaction end because it lives in transient storage."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"setAuthorizedExtension","anchor":"setauthorizedextension","text":"Access: owner-only (reverts NotOwner otherwise; reverts AuthorizedExtensionAlreadyLocked after the lock) Authorizes ext to toggle the inSwap flag. Re-callable until lockAuthorizedExtension freezes the binding; pass address(0) to revoke (future re-authorization stays possible until the lock). Emits A"},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"lockAuthorizedExtension","anchor":"lockauthorizedextension","text":"Access: owner-only (reverts NotOwner otherwise; reverts AuthorizedExtensionAlreadyLocked if already locked) One-way freeze of the extension binding. After this call setAuthorizedExtension reverts forever and the current authorizedExtension value (whatever it is, including address(0)) is permanent. E"},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"transferOwnership","anchor":"transferownership","text":"Access: owner-only (reverts NotOwner otherwise) Transfers the owner role. Reverts ZeroAddress for address(0): burning the owner outright would foreclose the future binding path, so the intended way to permanently disable this contract is lockAuthorizedExtension, optionally followed by a transfer to "},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"authorizedExtension","anchor":"authorizedextension","text":"The contract currently allowed to call enterSwap / exitSwap. address(0) at launch, which makes the flag permanently false until an extension dispatcher is authorized."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"authorizedExtensionLocked","anchor":"authorizedextensionlocked","text":"True once lockAuthorizedExtension has been called. From then on the extension binding can never change."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"inSwap","anchor":"inswap","text":"The transient in-swap flag that notInSwap-decorated protocol functions read. True only between an enterSwap and the matching exitSwap within a single transaction; always false when read from off-chain, and permanently false while no extension is authorized."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"owner","anchor":"owner","text":"Holder of the three admin calls (setAuthorizedExtension, lockAuthorizedExtension, transferOwnership). Expected to be the same key as TokenAdminPoker.owner, since binding a dispatcher requires coordinated calls on both contracts."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"AuthorizedExtensionLocked","anchor":"authorizedextensionlocked","text":"Emitted exactly once, on lockAuthorizedExtension. The extension binding is permanent from this point."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"AuthorizedExtensionSet","anchor":"authorizedextensionset","text":"Emitted on every setAuthorizedExtension with the new extension (indexed). An address(0) value is a revocation. The latest event carries the live binding."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"OwnershipTransferred","anchor":"ownershiptransferred","text":"Emitted at construction (from address(0)) and on every transferOwnership. Tracks who can authorize or lock the extension binding."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"SwapEntered","anchor":"swapentered","text":"Emitted on every enterSwap. Useful for indexers counting armed callback windows once a dispatcher is live; never emitted while no extension is authorized."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"SwapExited","anchor":"swapexited","text":"Emitted on every exitSwap, closing the window opened by the matching SwapEntered in the same transaction."},{"path":"/docs/contracts/pc-swap-context","page":"PCSwapContext","heading":"Errors","anchor":"errors","text":"AuthorizedExtensionAlreadyLocked() setAuthorizedExtension or lockAuthorizedExtension was called after the binding was already locked. The lock is one-way; there is nothing to retry. NotAuthorizedExtension() enterSwap / exitSwap was called by anything other than the currently authorized extension, or"},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"TokenAdminPoker","anchor":"","text":"0xA96a11257890ED1C43C16c098E286e18e45E6258 · view on evm.now · deployed at block 25270161 Retained-admin holder of the $111 token's tokenAdmin role. The token itself sees this contract as its admin, but the contract exposes only four narrow forwards: bind or lock the official pool's per-swap fee ext"},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"Why the admin is retained","anchor":"why-the-admin-is-retained","text":"The pool extension slot on the skim hook is empty at launch. A future synchronous extension dispatcher (Design B) binds through this contract's bindExtension; once the extension is proven in production, lockExtension freezes the binding permanently. Retaining the owner key is what keeps that path op"},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"The two-key carve-out pattern","anchor":"the-two-key-carve-out-pattern","text":"setHookMaxReferralBps and setTokenTaxBps accept msg.sender == owner OR msg.sender == adminContract.admin(). Each rate tracks a market regime that shifts over the protocol's lifetime (referral economics, side-pool competition), so it should stay tunable past the 1-year ProtocolAdmin timer and past an"},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"Reading the wiring","anchor":"reading-the-wiring","text":"The last component of poolKey is the skim hook, the only contract the extension and referral-cap setters can ever call."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"setup","anchor":"setup","text":"Access: owner-only, one-shot (reverts AlreadySetup on a second call) Pins the $111 token address and the canonical pool key (including its hook). Reverts ZeroAddress if the token or the key's hook is zero. Pinning here is what lets bindExtension / lockExtension / setHookMaxReferralBps drop their tar"},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"bindExtension","anchor":"bindextension","text":"Access: owner-only (reverts NotOwner otherwise; reverts NotSetup before setup) Binds (or re-binds, or swaps) the per-swap fee extension on the pinned pool's hook by forwarding setPoolExtension(poolKey, extension, \"\"). Works because this contract holds the token-admin role; the hook additionally requ"},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"lockExtension","anchor":"lockextension","text":"Access: owner-only (reverts NotOwner otherwise; reverts NotSetup before setup) One-way freeze of the pool's extension binding, forwarding lockPoolExtension(poolKey) to the hook. Intended for after a bound extension has proven itself in production; once called, the binding can never change again. Emi"},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"setHookMaxReferralBps","anchor":"sethookmaxreferralbps","text":"Access: two-key (either owner or the current ProtocolAdmin.admin() EOA; all other callers revert NotAuthorized; reverts NotSetup before setup) Updates the referral cap on the skim hook for the pinned pool by forwarding setMaxReferralBpsOfVolume(poolKey, newCap). The hook enforces the hard upper boun"},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"setTokenTaxBps","anchor":"settokentaxbps","text":"Access: two-key (either owner or the current ProtocolAdmin.admin() EOA; all other callers revert NotAuthorized; reverts NotSetup if the token isn't pinned yet) Updates the $111 token's venue-scoped buy-tax rate by forwarding setTaxBps(newBps) to the token. The token enforces the bound (newBps <= tax"},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"transferOwnership","anchor":"transferownership","text":"Access: owner-only (reverts NotOwner otherwise) Transfers the retained owner key. Reverts ZeroAddress for address(0); to effectively renounce, transfer to a dead-but-non-zero address, which makes the owner-only functions unreachable while leaving the two-key rate setters alive through the ProtocolAd"},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"adminContract","anchor":"admincontract","text":"The immutable ProtocolAdmin reference used by the two-key gates. Its current admin() is accepted alongside owner in setHookMaxReferralBps and setTokenTaxBps."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"owner","anchor":"owner","text":"The protocol's launch key (or its successor via transferOwnership). Gates setup, bindExtension, lockExtension, and transferOwnership, and is one of the two accepted keys on the rate setters."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"poolKey","anchor":"poolkey","text":"The canonical pool key pinned by setup: currency pair, LP fee, tick spacing, and the hook address. The hook component is the only contract bindExtension / lockExtension / setHookMaxReferralBps can ever call."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"setupDone","anchor":"setupdone","text":"True once setup has run. While false, every forward reverts NotSetup (the tax setter checks the pinned token instead, with the same effect)."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"token","anchor":"token","text":"The $111 token address pinned by setup, target of setTokenTaxBps. address(0) until setup."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"ExtensionBound","anchor":"extensionbound","text":"Emitted on every bindExtension with the hook (indexed) and the extension (indexed). A rebind emits again with the new extension; address(0) means the slot was cleared."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"ExtensionLocked","anchor":"extensionlocked","text":"Emitted once, on lockExtension, with the hook (indexed). From this point the pool's extension binding is permanent."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"MaxReferralBpsSet","anchor":"maxreferralbpsset","text":"Emitted on every setHookMaxReferralBps with the hook (indexed) and the new cap. The latest event carries the live referral ceiling for attributed swaps."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"OwnershipTransferred","anchor":"ownershiptransferred","text":"Emitted at construction (from address(0)) and on every transferOwnership. Tracks who holds the retained owner key."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"TokenTaxBpsSet","anchor":"tokentaxbpsset","text":"Emitted on every setTokenTaxBps with the token (indexed) and the new rate in bps. The latest event carries the live venue-scoped buy-tax rate."},{"path":"/docs/contracts/token-admin-poker","page":"TokenAdminPoker","heading":"Errors","anchor":"errors","text":"AlreadySetup() setup was called a second time. The token and pool pin once and never change. NotAuthorized() A rate setter (setHookMaxReferralBps / setTokenTaxBps) was called by an address that is neither owner nor the current ProtocolAdmin.admin(). NotOwner() An owner-only function (setup, bindExte"},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"ProtocolAdmin","anchor":"","text":"0x71112262e8A5825Cc9F201BDe8E370C05ec98339 · view on evm.now · deployed at block 25270161 The protocol's single, time-locked admin role. One address (admin) holds it, and its authority auto-expires one year after the last renewal (ADMIN TIMER DURATION = 365 days). Other protocol contracts consult th"},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"What checkAdmin gates (locks at the 1-year expiry)","anchor":"what-checkadmin-gates-locks-at-the-1-year-expiry","text":"These setters call checkAdmin(msg.sender) and become permanently unreachable once isLocked() is true: Contract Functions --- --- LiveBidAdapter setMaxSweepWei, setMinBlocksBetweenSweeps BuybackBurner setMinBlocksBetweenSteps, setMaxStepWei RendererRegistry setImplementation, freeze Every gated param"},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"The four lifetime carve-outs (survive the lock)","anchor":"the-four-lifetime-carve-outs-survive-the-lock","text":"Four setters stay callable past the 1-year expiry as long as the relevant role hasn't been burned. Each tracks a market regime that shifts over the protocol's lifetime, where freezing the launch value would be wrong: - Patron.addAllowedSeller / removeAllowedSeller reads admin() directly (not checkAd"},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"The burn is the off-switch","anchor":"the-burn-is-the-off-switch","text":"transferAdmin(address(0)) permanently disables the role: checkAdmin returns false, the raw-admin carve-outs stop matching any caller, and no future assignment is possible. Because burning strictly reduces power, it is not timer-gated. A key compromised after the timer lapsed can still be neutralised"},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"Reading the role state","anchor":"reading-the-role-state","text":""},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"transferAdmin","anchor":"transferadmin","text":"Access: admin-only (msg.sender must equal the current admin; any other caller reverts NotAdmin) The role's single write path, with two distinct behaviors keyed on newAdmin: - newAdmin != address(0): renewal or rotation. Requires the role to still be active (reverts Locked if the timer has expired or"},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"admin","anchor":"admin","text":"The current admin address, or address(0) after a burn. The raw-admin carve-outs (Patron's seller allowlist, LiveBidAdapter.setActivationThreshold, and the two TokenAdminPoker two-key setters) compare callers against this value directly, so they honor it even after the timer lapses."},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"ADMIN_TIMER_DURATION","anchor":"admintimerduration","text":"The renewal period, a constant 365 days (31,536,000 seconds). Every successful renewal or rotation sets adminTimerExpires this far into the future. Not configurable."},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"adminBurned","anchor":"adminburned","text":"True iff transferAdmin(address(0)) has been called. Once set it never clears; the role is permanently disabled."},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"adminTimerExpires","anchor":"admintimerexpires","text":"Unix timestamp at which the role auto-locks. Reset to now plus one year by every renewal or rotation. The lock is inclusive: the role is already locked at exactly this timestamp."},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"checkAdmin","anchor":"checkadmin","text":"The gate other contracts consult: returns true iff caller is the current admin AND the role is still active (!isLocked()). Setters gated on this lock permanently at expiry or burn."},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"isLocked","anchor":"islocked","text":"True if admin powers gated on checkAdmin are no longer exercisable, either because the role was burned or because block.timestamp = adminTimerExpires. Note that a true value from timer expiry alone does NOT disable the raw-admin carve-outs or the burn path; only adminBurned does that."},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"timeUntilLock","anchor":"timeuntillock","text":"Seconds remaining until the timer expires, or 0 if already locked. Handy for a dashboard countdown to the renewal deadline."},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"AdminBurned","anchor":"adminburned","text":"Emitted exactly once, on transferAdmin(address(0)). From this point every admin surface in the protocol is permanently disabled, including the lifetime carve-outs gated on this contract's admin()."},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"AdminTransferred","anchor":"admintransferred","text":"Emitted at construction (with previousAdmin = address(0)) and on every renewal or rotation. Carries the outgoing admin, the incoming admin (never address(0) on this event; burns emit AdminBurned instead), and the fresh expiry timestamp. An indexer can track the live expiry from the latest newTimerEx"},{"path":"/docs/contracts/protocol-admin","page":"ProtocolAdmin","heading":"Errors","anchor":"errors","text":"Locked() A renewal or rotation (newAdmin != address(0)) was attempted after the timer expired or after a burn. Lapsed roles can't be revived; the only remaining action is the burn. NotAdmin() transferAdmin was called by an address other than the current admin."},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"RendererRegistry","anchor":"","text":"0x760421B7916917Ffd72ECeAa4c1F7ffC4D12eEc7 · view on evm.now · deployed at block 25270161 The stable address fronting the protocol's renderer. The $111 ERC20 and PunkVault both store this registry as their immutable metadata renderer reference; the actual rendering logic lives at implementation (the"},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"Why a registry at all","anchor":"why-a-registry-at-all","text":"The renderer is the one part of the system where a display bug is plausible and harmless, so it gets the protocol's narrowest mutability window instead of being immutable from day one. The artcoins factory wires the $111 token's metadataRenderer to this address, and PunkVault pins it as the source o"},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"What is and isn't checked on `setImplementation`","anchor":"what-is-and-isnt-checked-on-setimplementation","text":"There is no on-chain interface probe. setImplementation guards exactly two foot-guns: the zero address (ZeroAddress) and an address with no contract code, an EOA or destroyed contract (NotAContract). A candidate that has code but renders wrongly is not caught on-chain by design: the registry moves n"},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"Reading through the registry","anchor":"reading-through-the-registry","text":"`bash"},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"Current implementation","anchor":"","text":"cast call 0x760421B7916917Ffd72ECeAa4c1F7ffC4D12eEc7 \"implementation()(address)\" \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"The Vault Title metadata, forwarded to the live renderer","anchor":"","text":"cast call 0x760421B7916917Ffd72ECeAa4c1F7ffC4D12eEc7 \"tokenURI(uint256)(string)\" 111 \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"Is the implementation permanent yet?","anchor":"","text":"cast call 0x760421B7916917Ffd72ECeAa4c1F7ffC4D12eEc7 \"isLocked()(bool)\" \\ --rpc-url https://ethereum-rpc.publicnode.com `"},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"setImplementation","anchor":"setimplementation","text":"Access: admin-only (ProtocolAdmin.checkAdmin(msg.sender) must be true, so the caller must hold the admin role with its 1-year timer unexpired) Points the registry at a new renderer implementation. Reverts AlreadyFrozen if freeze() has been called, NotAdmin if the caller isn't the active admin, ZeroA"},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"freeze","anchor":"freeze","text":"Access: admin-only (ProtocolAdmin.checkAdmin(msg.sender)), one-way Permanently locks the current implementation. Reverts AlreadyFrozen if already frozen and NotAdmin for anyone but the active admin. There is no unfreeze; after this call setImplementation reverts forever regardless of admin state. Em"},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"adminContract","anchor":"admincontract","text":"The immutable ProtocolAdmin instance gating setImplementation and freeze. Same 1-year heartbeat-renewable timer as the rest of the protocol's mutable surfaces."},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"contractURI","anchor":"contracturi","text":"Forwarded ERC-7572 contractURI(address token). The $111 ERC20 calls this on its configured metadata renderer to resolve both its own contractURI() and zero-arg tokenURI(); PunkVault.contractURI() calls it with the vault's address. The live implementation keys the returned symbol field off the token "},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"frozen","anchor":"frozen","text":"True iff freeze() has been called. Distinct from isLocked(): frozen only reflects the explicit freeze, not admin expiry."},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"implementation","anchor":"implementation","text":"The current renderer implementation address. All four pass-through views forward here. Updated by setImplementation while the registry is unfrozen and the admin role active."},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"isLocked","anchor":"islocked","text":"True iff the implementation is permanent, for either reason: freeze() was called, or the admin role is no longer exercisable (adminContract.isLocked()). The value to surface in a UI that wants to say \"the renderer can/can't still change\"."},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"svg","anchor":"svg","text":"Forwarded raw SVG payload from the implementation: the full mosaic image with no JSON envelope. Useful for off-chain tools that want the image without base64-decoding a metadata blob."},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"tokenURI","anchor":"tokenuri","text":"Forwarded zero-arg tokenURI(), the ERC20-flavored metadata. The artcoins ERC20's metadata path consumes this signature; it resolves to the same collection JSON as contractURI with symbol \"111\"."},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"tokenURI","anchor":"tokenuri","text":"Forwarded tokenURI(uint256 id) for the PunkVault ERC721 tokens. The live implementation dispatches ids 0..110 to the Proof renderer, id 111 to the Vault Title render, and reverts UnknownTokenId for anything higher. Calling it here for an in-range but unminted Proof reverts ProofNotMinted at the Proo"},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"Frozen","anchor":"frozen","text":"Emitted exactly once, when freeze() is called, with the block number. After this event the implementation can never change again."},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"ImplementationUpdated","anchor":"implementationupdated","text":"Emitted at deployment (with previous == address(0)) and on every successful setImplementation, with the previous and next implementation addresses, both indexed. The full renderer history of the protocol is this event stream."},{"path":"/docs/contracts/renderer-registry","page":"RendererRegistry","heading":"Errors","anchor":"errors","text":"AlreadyFrozen() setImplementation or freeze was called after freeze(). The implementation is permanent; there is nothing the caller can do. NotAContract() The setImplementation candidate has no contract code (an EOA, a typo, or a destroyed contract). Deploy the renderer first, then register it. NotA"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"PermanentCollectionMosaicRenderer","anchor":"","text":"0x40aa9edb0063ca5d12eef8A53be45F7e15a3Fe10 · view on evm.now · deployed at block 25270161 The shipped renderer, installed behind RendererRegistry. It composes the artwork as a true mosaic of the actual collected CryptoPunks: an 11×10 grid of 24×24 trait cells plus one pulled-out \"final type\" cell be"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"The canvas","anchor":"the-canvas","text":"The design coordinate space is 356×356 (viewBox=\"0 0 356 356\"): a 24-px outer pad around an 11×10 grid of 28-px cells (24-px tile + 4-px gap). The root SVG's intrinsic width/height are emitted at 8× (2848) so a right-click \"Copy Image\" rasterizes at print- and share-friendly resolution; display sizi"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"Cell states","anchor":"cell-states","text":"State Rendering --- --- Uncollected flat #1c1c1c cell + the trait's icon Pending the uncollected look + a 1-px dashed #454545 border (a return auction is live for a Punk targeting this trait) Collected #8F918B swatch + the first-vaulted Punk's pixel fragment Trait icons come in three visual families"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"The two caches","anchor":"the-two-caches","text":"Rendering is cache-first. Collected cells read the vaulted Punk's tile from PunkSvgFragmentCache; uncollected and pending cells read the trait icon from TraitIconCache. Both caches are public, immutable, and start empty; anyone can pay gas to bake entries. When an entry isn't baked the renderer fall"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"Reading the artwork","anchor":"reading-the-artwork","text":"`bash"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"The raw mosaic SVG, no JSON envelope","anchor":"","text":"cast call 0x40aa9edb0063ca5d12eef8A53be45F7e15a3Fe10 \"svg()(string)\" \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"The Vault Title metadata (base64 JSON data URI)","anchor":"","text":"cast call 0x40aa9edb0063ca5d12eef8A53be45F7e15a3Fe10 \"tokenURI(uint256)(string)\" 111 \\ --rpc-url https://ethereum-rpc.publicnode.com Canonical consumers go through RendererRegistry` (0x760421B7916917Ffd72ECeAa4c1F7ffC4D12eEc7), which forwards the same four views."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"cacheTrait","anchor":"cachetrait","text":"Access: permissionless Bakes the Punk tile for traitId's first-vaulted Punk into the public PunkSvgFragmentCache, so future renders of that cell read one SSTORE2 pointer instead of recomputing from raw pixels. Looks up collection.firstVaultedPunk(traitId) and reverts TraitNotCollected if the trait h"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"cachedPunkForTrait","anchor":"cachedpunkfortrait","text":"Convenience wrapper around collection.firstVaultedPunk(traitId). Returns the Punk responsible for the trait's entry into the collection and an exists flag, or (0, false) if the trait is uncollected."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"collection","anchor":"collection","text":"The immutable PermanentCollection records core. Read for collectedMask, pendingMask, collectedCount, isComplete, and per-trait firstVaultedPunk on every render."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"contractURI","anchor":"contracturi","text":"ERC-7572 contract-level metadata. Returns the collection envelope, a base64 JSON data URI with name \"PERMANENT COLLECTION\", the live mosaic image, and Traits Collected / Traits Total / Punks Vaulted attributes. Both the PunkVault collection page and the $111 ERC20 resolve through this view, so the s"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"isTraitCached","anchor":"istraitcached","text":"True iff traitId is collected AND its first-vaulted Punk has a baked fragment in PunkSvgFragmentCache. Returns false for uncollected traits rather than reverting, so callers don't need an existence check first. The value to read before deciding whether cacheTrait is worth calling."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"proofRenderer","anchor":"proofrenderer","text":"The immutable PermanentCollectionProofRenderer address that tokenURI(uint256) delegates Proof ids 0..110 to. Pinned at construction; the on-chain way to discover the Proof renderer's address."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"punksData","anchor":"punksdata","text":"The immutable sealed PunksData contract, the pixel, palette, and trait taxonomy source everything is drawn from. The same instance PermanentCollection pins by dataset hash."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"punkSvgCache","anchor":"punksvgcache","text":"The immutable public PunkSvgFragmentCache holding baked full-Punk SVG fragments for collected cells."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"svg","anchor":"svg","text":"The raw mosaic SVG string for the current chain state, no JSON envelope. Reads collectedMask, pendingMask, and collectedCount and renders all 111 cells plus the footer. Useful for off-chain tooling; the metadata views embed this same image as a base64 data URI."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"tokenURI","anchor":"tokenuri","text":"Zero-arg ERC20-flavored metadata, consumed by the $111 token's metadata passthrough. Returns the same collection envelope as contractURI with symbol \"111\": name \"PERMANENT COLLECTION\", the live mosaic image, and the Traits Collected / Traits Total / Punks Vaulted attributes."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"tokenURI","anchor":"tokenuri","text":"ERC721 metadata dispatch for PunkVault-issued token ids: - 0 <= id <= 110: delegated to proofRenderer.tokenURI(id), the corresponding Proof (tokenId == traitId directly). Reverts ProofNotMinted there if that Proof hasn't been minted - id == 111: the Vault Title, rendered here. A base64 JSON envelope"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"traitIconBytes","anchor":"traiticonbytes","text":"Inspection view exposing the exact per-cell trait-icon bytes the renderer composes internally, so tests and off-chain tools can verify that TraitIconCache's baked bytes match what this renderer would produce on the fly. Takes traitId in 0..110 and reverts (with a require string) otherwise. Fetches p"},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"traitIconCache","anchor":"traiticoncache","text":"The immutable public TraitIconCache holding baked trait-icon fragments for uncollected and pending cells. Empty at deploy; permissionlessly bakeable."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"vault","anchor":"vault","text":"The immutable PunkVault reference, read for lockedPunkCount when building the Punks Vaulted attribute in the metadata envelopes."},{"path":"/docs/contracts/mosaic-renderer","page":"PermanentCollectionMosaicRenderer","heading":"Errors","anchor":"errors","text":"TraitNotCollected(uint8 traitId) cacheTrait was called for a trait with no first-vaulted Punk yet. There's nothing to bake; wait until a Punk carrying the trait is vaulted. UnknownTokenId(uint256 id) tokenURI(uint256) was called with an id of 112 or higher. Only ids 0..110 (Proofs) and 111 (the Vaul"},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"PermanentCollectionProofRenderer","anchor":"","text":"On-chain SVG + JSON renderer for the 111 Proof NFTs issued by PunkVault (token ids 0..110, where tokenId == traitId directly). Each Proof attests to a single trait's first-vaulting and is minted to the address that originally gave up the Punk. The image is composed entirely on-chain: a 24×24 trait t"},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"Minted vs unminted","anchor":"minted-vs-unminted","text":"The distinction lives in two places. In the image: svg(traitId) is total over 0..110 and reflects live mint state, drawing the faint acquired-Punk layer once the Proof is minted and the trait icon alone before. In the metadata: tokenURI(id) exists only for a minted Proof and reverts ProofNotMinted o"},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"What the metadata says","anchor":"what-the-metadata-says","text":"All per-Proof data comes from vault.proofMeta(id), frozen at mint time, plus the trait name from the sealed PunksData contract. The name is Permanent Collection Proof <traitId (<traitName ), so the Proof number matches the token id, not the order of collection. The description names the CryptoPunk w"},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"How the image is built","anchor":"how-the-image-is-built","text":"The trait icon comes from TraitIconCache.buildFragment(traitId), a pure view that computes the fragment whether or not the trait has been baked into the cache. The acquired-Punk layer comes from the same PunkSvgFragmentCache the mosaic renderer uses, reading the baked fragment when present and compu"},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"MAX_PROOF_TOKEN_ID","anchor":"maxprooftokenid","text":"Highest valid Proof token id, a constant 110. Proofs occupy 0..110 with tokenId == traitId; id 111 is the Vault Title, which this contract refuses to claim."},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"PROOF_COUNT","anchor":"proofcount","text":"Total number of Proofs in the collection, a constant 111. The cap never changes."},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"punksData","anchor":"punksdata","text":"The immutable sealed PunksData contract, read for the human-readable trait name inscribed in the Proof's metadata."},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"punkSvgCache","anchor":"punksvgcache","text":"The immutable public PunkSvgFragmentCache supplying the acquired Punk's pixel fragment for the faint background layer on a minted Proof. The same instance the mosaic renderer uses."},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"svg","anchor":"svg","text":"Raw SVG payload for the Proof image at traitId, no JSON envelope. Reverts UnknownTokenId for traitId = 111. Total over the valid range: it renders for unminted traits too (trait icon alone on the background), and reflects live mint state, adding the 5%-opacity acquired Punk once the trait's Proof is"},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"tokenURI","anchor":"tokenuri","text":"The data-URI-encoded ERC721 JSON metadata for a minted Proof. Reverts UnknownTokenId(id) for id 110 and ProofNotMinted(id) for an in-range id whose Proof hasn't been minted (checked via vault.proofMeta(id).mintedAtBlock == 0). For a minted Proof, returns the base64 JSON envelope described in the con"},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"traitIconCache","anchor":"traiticoncache","text":"The immutable public TraitIconCache whose pure-view buildFragment supplies the isolated trait icon, working for baked and unbaked traits alike."},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"vault","anchor":"vault","text":"The immutable PunkVault reference, read for proofMeta(tokenId) (punk id, trait id, sequence, minted-at block) at render time. proofMeta returns a zero-valued struct for a never-minted id, which is how this renderer detects mint state."},{"path":"/docs/contracts/proof-renderer","page":"PermanentCollectionProofRenderer","heading":"Errors","anchor":"errors","text":"ProofNotMinted(uint256 id) tokenURI was called for an in-range Proof id (0..110) that hasn't been minted. There is no preview envelope; use svg(traitId) for the pre-mint image. UnknownTokenId(uint256 id) The id is outside the Proof range: tokenURI with id 110 or svg with traitId = 111. Id 111 is the"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"PunkSvgFragmentCache","anchor":"","text":"0x3ab4b628AB844a723235F08554C49B5Dd54c56BD · view on evm.now · deployed at block 25270161 Public, permissionless on-chain cache of compact 24x24 SVG fragments derived from the canonical CryptoPunks pixel data. Each cached entry is one Punk's full tile as SVG path markup (one <path per distinct opaqu"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"How the renderers consume it","anchor":"how-the-renderers-consume-it","text":"Both renderers use the same consult-then-fallback read: The Mosaic renderer at 0x40aa9edb0063ca5d12eef8A53be45F7e15a3Fe10 does this once per collected trait cell when composing tokenURI(111) (the Vault Title image). The Proof renderer does it once per minted Proof tokenURI for the 5%-opacity backgro"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Who warms the cache, and why","anchor":"who-warms-the-cache-and-why","text":"cachePunk is fully permissionless: any account can bake any Punk at any time, and the call is idempotent. The intended cadence is one bake per vaulting: after a trait is collected, anyone calls the Mosaic renderer's cacheTrait(traitId) wrapper, which resolves the trait's first-vaulted Punk from Perm"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Gas: bake once, read forever","anchor":"gas-bake-once-read-forever","text":"An uncached read (buildFragment, or a renderer falling back to it) walks the full 576-pixel indexed buffer plus a 256-entry palette and typically costs a few million gas per Punk. A cached read is one storage load plus an SSTORE2 EXTCODECOPY, roughly 50k gas. That difference matters because marketpl"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Checking and warming from the command line","anchor":"checking-and-warming-from-the-command-line","text":"`bash"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Is Punk 8348 baked?","anchor":"","text":"cast call 0x3ab4b628AB844a723235F08554C49B5Dd54c56BD \"isCached(uint16)(bool)\" 8348 \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Bake it (any funded account)","anchor":"","text":"cast send 0x3ab4b628AB844a723235F08554C49B5Dd54c56BD \"cachePunk(uint16)\" 8348 \\ --rpc-url https://ethereum-rpc.publicnode.com --private-key $PRIVATE KEY"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Read the standalone SVG","anchor":"","text":"cast call 0x3ab4b628AB844a723235F08554C49B5Dd54c56BD \"svgOf(uint16)(string)\" 8348 \\ --rpc-url https://ethereum-rpc.publicnode.com `"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"cachePunk","anchor":"cachepunk","text":"Access: permissionless Bakes punkId's SVG fragment into a fresh SSTORE2 storage contract and records the pointer. Reverts InvalidPunkId for punkId = 10000. If the Punk is already cached the call is a cheap no-op that returns the existing pointer without redeploying, so double-bakes and races between"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"buildFragment","anchor":"buildfragment","text":"Live, cacheless re-derivation of punkId's fragment: the exact bytes cachePunk would store, computed fresh from PunksData on every call. Reverts InvalidPunkId for punkId = 10000. This is the fallback path both renderers take for an uncached Punk, so a render is never blocked on a bake, just slower. C"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"EXPECTED_DATASET_HASH","anchor":"expecteddatasethash","text":"The pinned PunksData dataset hash, constant 0x92117ce6cb6bb70f9ffb9bf51ebbca6a84eae10e70639295d9c4a07958cd1f68. The constructor rejects any punksData whose datasetHash() doesn't match, so the cache can only ever derive from the sealed dataset. Same value pinned by PermanentCollection.EXPECTED DATASE"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"fragmentOf","anchor":"fragmentof","text":"Raw cached fragment bytes for punkId: SVG path markup with one <path per distinct opaque color the Punk uses, each packing that color's maximal horizontal pixel runs as closed 1-pixel-tall subpaths, coordinates in the 0..23 range. Reverts NotCached if the Punk hasn't been baked. This is the read the"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"isCached","anchor":"iscached","text":"True iff punkId has been baked. The cheap existence probe consumers should call before choosing between fragmentOf (cached) and buildFragment (live). Never reverts; an out-of-range id simply returns false."},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"pointerOf","anchor":"pointerof","text":"The SSTORE2 storage-contract address holding punkId's fragment. Reverts NotCached if unbaked. For callers that want to compose at the SSTORE2 layer (or verify the pointer's code) rather than materializing the bytes through this contract."},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"PUNK_DIM","anchor":"punkdim","text":"Width and height of a Punk tile in pixels, constant 24 per the 2017 CryptoPunks specification. Fragment coordinates are in the 0..23 range."},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"punksData","anchor":"punksdata","text":"The immutable PunksData source of pixel and palette data, 0x9cF9C8eA737A7d5157d3F4282aCe30880a7A117C, validated against EXPECTED DATASET HASH at construction. Every fragment, cached or live, derives from this contract alone."},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"svgOf","anchor":"svgof","text":"Convenience view: the cached fragment wrapped in a standalone <svg viewBox=\"0 0 24 24\" element, ready to render as-is. Reverts NotCached if the Punk hasn't been baked. Useful for previewers, tests, and any consumer that wants a complete tile rather than a composable fragment."},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"PunkCached","anchor":"punkcached","text":"Emitted exactly once per Punk, on its first successful cachePunk, with the Punk id (indexed), the SSTORE2 pointer address (indexed), and the fragment byte length. Idempotent re-calls don't re-emit. An indexer can reconstruct the full cache state (which Punks are baked and where) from this event alon"},{"path":"/docs/contracts/punk-svg-fragment-cache","page":"PunkSvgFragmentCache","heading":"Errors","anchor":"errors","text":"EmptySvg(uint16 punkId) cachePunk derived an empty fragment, meaning PunksData returned an all-transparent 24x24 tile for the id. Defense in depth: no real Punk is all-transparent, so this is unreachable against the sealed dataset. InvalidPunkId(uint16 punkId) cachePunk or buildFragment was called w"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"TraitIconCache","anchor":"","text":"0xCd7eE161a7aA9f49F7d970CF3B31fE5ac6D20Ca7 · view on evm.now · deployed at block 25270161 Public, permissionless on-chain cache of compact SVG fragments for the 111 CryptoPunks trait icons, companion to PunkSvgFragmentCache (that one caches whole-Punk tiles for collected cells; this one caches the t"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"Concepts","anchor":"concepts","text":""},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"How the renderers consume it","anchor":"how-the-renderers-consume-it","text":"The Mosaic renderer at 0x40aa9edb0063ca5d12eef8A53be45F7e15a3Fe10 composes each uncollected or pending cell of the Vault Title image (tokenURI(111)) through a cache-first path: 1. Rotation short-circuit: for trait ids {0, 1, 4, 5, 6, 15} it always recomputes on the fly, picking the current block's P"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"Who warms the cache, and why","anchor":"who-warms-the-cache-and-why","text":"cacheTrait is fully permissionless and idempotent: any account can bake any cacheable trait, in any order, at any time, and races between bakers waste little gas. The cache ships empty and \"turns on\" gradually as bakes accumulate, no team coordination required. The payoff is eth call budgets: market"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"Checking and warming from the command line","anchor":"checking-and-warming-from-the-command-line","text":"`bash"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"Is trait 23 baked? Is it even cacheable?","anchor":"","text":"cast call 0xCd7eE161a7aA9f49F7d970CF3B31fE5ac6D20Ca7 \"isCached(uint8)(bool)\" 23 \\ --rpc-url https://ethereum-rpc.publicnode.com cast call 0xCd7eE161a7aA9f49F7d970CF3B31fE5ac6D20Ca7 \"isRotationTrait(uint8)(bool)\" 23 \\ --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"Bake it (any funded account)","anchor":"","text":"cast send 0xCd7eE161a7aA9f49F7d970CF3B31fE5ac6D20Ca7 \"cacheTrait(uint8)\" 23 \\ --rpc-url https://ethereum-rpc.publicnode.com --private-key $PRIVATE KEY"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"Read the standalone SVG","anchor":"","text":"cast call 0xCd7eE161a7aA9f49F7d970CF3B31fE5ac6D20Ca7 \"svgOf(uint8)(string)\" 23 \\ --rpc-url https://ethereum-rpc.publicnode.com `"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"Write functions","anchor":"write-functions","text":""},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"cacheTrait","anchor":"cachetrait","text":"Access: permissionless Bakes traitId's SVG fragment into a fresh SSTORE2 storage contract and records the pointer. Reverts InvalidTraitId for traitId = 111 and RotationTraitNotCacheable for the six per-block-rotating ids ({0, 1, 4, 5, 6, 15}). If the trait is already cached the call returns the exis"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"Read functions","anchor":"read-functions","text":""},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"buildFragment","anchor":"buildfragment","text":"Compute, without storing, the fragment bytes for traitId: the exact bytes cacheTrait would bake, derived fresh from PunksData on every call. Reverts InvalidTraitId for traitId = 111. Unlike cacheTrait, this works for the six rotation trait ids too, returning the current block's pick (it reads block."},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"canonicalPunkForTrait","anchor":"canonicalpunkfortrait","text":"The pinned exemplar Punk id for traitId, decoded from the embedded CANONICAL IDS table (2 bytes per trait, identical to the Mosaic renderer's table). Pure view so any consumer can reuse the mapping without redoing the decode. Reverts InvalidTraitId for traitId = 111. For type and head-variant traits"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"EXPECTED_DATASET_HASH","anchor":"expecteddatasethash","text":"The pinned PunksData dataset hash, constant 0x92117ce6cb6bb70f9ffb9bf51ebbca6a84eae10e70639295d9c4a07958cd1f68. The constructor rejects any punksData whose datasetHash() doesn't match. Same value pinned by PunkSvgFragmentCache and PermanentCollection."},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"fragmentOf","anchor":"fragmentof","text":"Raw cached fragment bytes for traitId: a sequence of <rect elements (one per maximal horizontal run of same-colored pixels, or one per dot slot for attribute-count traits) with coordinates in the 0..23 range. Reverts InvalidTraitId for traitId = 111 and NotCached if the trait hasn't been baked. This"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"isCached","anchor":"iscached","text":"True iff traitId has been baked. Reverts InvalidTraitId for traitId = 111. The existence probe consumers call before choosing between fragmentOf (cached) and their own fallback. Rotation trait ids always return false, since cacheTrait rejects them."},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"isRotationTrait","anchor":"isrotationtrait","text":"True iff traitId is one of the six per-block-rotating ids ({0, 1, 4, 5, 6, 15}, the rare types and their matching head variants). Pure view; reverts InvalidTraitId for traitId = 111. Rotation traits can be read via buildFragment (current block's pick) but never baked; cacheTrait reverts RotationTrai"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"pointerOf","anchor":"pointerof","text":"The SSTORE2 storage-contract address holding traitId's fragment. Reverts InvalidTraitId for traitId = 111 and NotCached if unbaked. For callers composing at the SSTORE2 layer directly rather than materializing the bytes through this contract."},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"PUNK_DIM","anchor":"punkdim","text":"The 24x24 dimension of canonical Punk tiles, constant 24. Fragment coordinates are in the 0..23 range."},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"punksData","anchor":"punksdata","text":"The immutable PunksData source of pixel and palette data, 0x9cF9C8eA737A7d5157d3F4282aCe30880a7A117C, validated against EXPECTED DATASET HASH at construction. Every fragment, cached or live, derives from this contract plus the pinned CANONICAL IDS table."},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"svgOf","anchor":"svgof","text":"Convenience view: the cached fragment wrapped in a standalone <svg viewBox=\"0 0 24 24\" element, ready to render as-is. Reverts InvalidTraitId for traitId = 111 and NotCached if the trait hasn't been baked."},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"TOTAL_TRAITS","anchor":"totaltraits","text":"Number of distinct CryptoPunks traits, constant 111. Valid trait ids are 0..110."},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"Events","anchor":"events","text":""},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"TraitCached","anchor":"traitcached","text":"Emitted exactly once per trait, on its first successful cacheTrait, with the trait id (indexed), the SSTORE2 pointer address (indexed), and the fragment byte length. Idempotent re-calls don't re-emit. An indexer can reconstruct full cache coverage (at most 105 events, since the 6 rotation ids are ne"},{"path":"/docs/contracts/trait-icon-cache","page":"TraitIconCache","heading":"Errors","anchor":"errors","text":"EmptyFragment(uint8 traitId) cacheTrait derived an empty fragment for the trait. Defense in depth: no valid trait icon resolves to zero bytes given the pinned CANONICAL IDS table and the sealed PunksData state, so this is unreachable on the live deployment. InvalidTraitId(uint8 traitId) A function w"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"Accept the live bid","anchor":"","text":"The protocol maintains a single global ETH live bid, held by Patron, open to any eligible Punk. Accepting it is not one transaction: the owner lists the Punk exclusively to Patron on the 2017 CryptoPunks market, a second transaction (from anyone) finalizes the acceptance, and the seller collects pay"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"Step 0: check eligibility","anchor":"step-0-check-eligibility","text":"Three reads against PermanentCollection at 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 and Patron tell you whether a Punk can be accepted right now. 1. Custody. custodyOf(punkId) must be None (0, never acquired) or ReturnedToMarket (2, previously acquired and returned through a cleared return auction"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"0 = never acquired, 2 = returned to market: both eligible","anchor":"","text":"cast call $PC \"custodyOf(uint16)(uint8)\" $PUNK ID --rpc-url $RPC"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"The trait this acquisition will target (reverts NoEligibleTarget if none)","anchor":"","text":"cast call $PC \"canonicalTargetOf(uint16)(uint8)\" $PUNK ID --rpc-url $RPC"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"The current live bid in wei","anchor":"","text":"cast call $PATRON \"bidBalance()(uint256)\" --rpc-url $RPC `"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"The sole-carrier rule for Punk #8348","anchor":"the-sole-carrier-rule-for-punk-8348","text":"Exactly one trait in the sealed dataset has a single carrier: trait bit 23 (\"7 Attributes\"), carried only by Punk #8348. While that trait is uncollected, any acquisition of #8348 must target trait 23, or it reverts SoleCarrierMustTargetTrait. You don't need special handling: canonicalTargetOf(8348) "},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"Step 1: the owner lists exclusively to Patron","anchor":"step-1-the-owner-lists-exclusively-to-patron","text":"The seller calls the 2017 market's exclusive-listing function, with Patron as the only allowed buyer: Constraints on listingWei: - it must be positive: a zero-price listing reverts ZeroListingPrice at acceptance time - it must be at most the live bid: a listing above bidBalance() reverts ListingExce"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"Step 2: anyone finalizes with acceptBid","anchor":"step-2-anyone-finalizes-with-acceptbid","text":"- punkId: the listed Punk (must be below 10,000, else InvalidPunkId) - targetTraitId: the value you read from canonicalTargetOf(punkId) - expectedListingWei: the caller's overpay cap. If the seller re-listed at a higher price after your read, the call reverts ListingAboveExpected instead of paying m"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"What each revert means","anchor":"what-each-revert-means","text":"Revert Meaning --- --- InvalidPunkId(punkId) punkId = 10 000 PunkNotListedToHub(punkId) no active listing, or the listing isn't exclusive to Patron ZeroListingPrice(punkId) the exclusive listing has price 0 ListingExceedsBid(listing, liveBid) listed price is above the current live bid; wait for the "},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"Step 3: the seller collects from the market","anchor":"step-3-the-seller-collects-from-the-market","text":"buyPunk does not push ETH to the seller. The 2017 market credits the listed price to pendingWithdrawals[seller], and the seller pulls it: The market pays the seller, not Patron. One caveat for contract accounts: the market's withdraw() uses a 2300-gas transfer, so a smart-contract seller (or an EIP-"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"What happens next","anchor":"what-happens-next","text":"The accepted Punk immediately enters a 72-hour return auction on ReturnAuctionModule. The opening reserve is listingWei x (101 + previousAttempts) / 100, rounded up, where previousAttempts counts prior return auctions against the same target trait. Two outcomes: - someone bids at least the reserve :"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"Full cast walkthrough","anchor":"full-cast-walkthrough","text":"`bash RPC=https://ethereum-rpc.publicnode.com MARKET=0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB PATRON=0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c PC=0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 PUNK ID=1234"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"── Eligibility reads ──","anchor":"","text":"cast call $PC \"custodyOf(uint16)(uint8)\" $PUNK ID --rpc-url $RPC # want 0 or 2 TARGET=$(cast call $PC \"canonicalTargetOf(uint16)(uint8)\" $PUNK ID --rpc-url $RPC) BID=$(cast call $PATRON \"bidBalance()(uint256)\" --rpc-url $RPC awk '{print $1}')"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"── 1. Seller lists exclusively to Patron at the full bid ──","anchor":"","text":"cast send $MARKET \\ \"offerPunkForSaleToAddress(uint256,uint256,address)\" \\ $PUNK ID $BID $PATRON \\ --private-key $SELLER KEY --rpc-url $RPC"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"── 2. Anyone finalizes. expectedListingWei = the listing price just set ──","anchor":"","text":"cast send $PATRON \\ \"acceptBid(uint16,uint8,uint256)\" \\ $PUNK ID $TARGET $BID \\ --private-key $ANY KEY --rpc-url $RPC"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"── 3. Seller collects the payment queued on the market ──","anchor":"","text":"cast call $MARKET \"pendingWithdrawals(address)(uint256)\" $SELLER ADDRESS --rpc-url $RPC cast send $MARKET \"withdraw()\" --private-key $SELLER KEY --rpc-url $RPC `"},{"path":"/docs/guides/accept-the-live-bid","page":"Accept the live bid","heading":"Full viem walkthrough","anchor":"full-viem-walkthrough","text":"Contract reference: Patron, PermanentCollection, ReturnAuctionModule."},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"Bid on a return auction","anchor":"","text":"Every Punk the protocol acquires enters a 72-hour return auction on ReturnAuctionModule at 0x2ced1CA0BC7996c3dFB3D4E685d9d9E7405A7aCe. Bid at or above the reserve and win, and the Punk is yours at settle. If nobody bids by the deadline, the Punk is vaulted permanently and its target trait becomes a "},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"Finding live auctions","anchor":"finding-live-auctions","text":"Auctions are keyed by punkId. To discover them, watch the module's events: Then confirm state with the read surface: - isLive(punkId) - bool: true while the auction accepts bids - isSettleable(punkId) - bool: true once the deadline has passed and settle would succeed - getSale(punkId) returns the fu"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"Minimum-bid math","anchor":"minimum-bid-math","text":"Two rules, both enforced on-chain: First bid: meet the reserve. The reserve is snapshotted at auction start as acquisitionCost x (101 + previousAttempts) / 100, rounded up, where previousAttempts counts prior return auctions against the same target trait. The first attempt against a trait opens at a"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"first bid: RESERVE. later bids: HIGH + HIGH/100 (rounded down, min +1 wei)","anchor":"","text":"`"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"placeBid vs placeBidWithReferral","anchor":"placebid-vs-placebidwithreferral","text":"placeBid is the plain entry point and is exactly placeBidWithReferral(punkId, address(0), bytes32(0)). placeBidWithReferral attaches attribution for frontends and aggregators: - if this bid is the winning bid at settle, referrer earns REFERRER PREMIUM BPS = 500 (5%) of the premium, where the premium"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"Anti-snipe extension","anchor":"anti-snipe-extension","text":"Any accepted bid placed within the final 15 minutes (SNIPE TRIGGER WINDOW) moves the deadline to the bid's timestamp plus 1 hour (SNIPE EXTENSION) and emits ReturnAuctionExtended. The extension is uncapped: an actively contested Punk stays in bidding for as long as bidders keep escalating, and each "},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"Refunds when outbid","anchor":"refunds-when-outbid","text":"When you're outbid, the module pushes your ETH back immediately with a 30,000-gas send. If that push fails (a contract bidder whose receive needs more gas), the amount is queued in pendingRefund[bidder] and RefundQueued is emitted. Pull it any time: Reverts NothingToWithdraw when your balance is zer"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"Settling after the deadline","anchor":"settling-after-the-deadline","text":"Anyone may call once block.timestamp = endsAt (before that it reverts SaleLive; after a settle it reverts AlreadySettled). There is no keeper tip: as the winning bidder your incentive is that you receive the Punk only on settle. Cleared (a winning bid exists). The Punk is delivered to the winning bi"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"cast walkthrough","anchor":"cast-walkthrough","text":"`bash RPC=https://ethereum-rpc.publicnode.com RAM=0x2ced1CA0BC7996c3dFB3D4E685d9d9E7405A7aCe PUNK ID=1234"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"── State ──","anchor":"","text":"cast call $RAM \"isLive(uint16)(bool)\" $PUNK ID --rpc-url $RPC RESERVE=$(cast call $RAM \"reserveOf(uint16)(uint256)\" $PUNK ID --rpc-url $RPC awk '{print $1}') HIGH=$(cast call $RAM \"highBidOf(uint16)(uint128)\" $PUNK ID --rpc-url $RPC awk '{print $1}')"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"── First bid at the reserve ──","anchor":"","text":"cast send $RAM \"placeBid(uint16)\" $PUNK ID \\ --value $RESERVE --private-key $KEY --rpc-url $RPC"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"── Or a later bid with referral attribution (HIGH + 1%; bc avoids 64-bit","anchor":"","text":""},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"shell-arithmetic overflow on wei values) ──","anchor":"","text":"BID=$(echo \"$HIGH + $HIGH / 100\" bc) cast send $RAM \"placeBidWithReferral(uint16,address,bytes32)\" \\ $PUNK ID $REFERRER 0x0000000000000000000000000000000000000000000000000000000000000000 \\ --value $BID --private-key $KEY --rpc-url $RPC"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"── If outbid and the push refund failed ──","anchor":"","text":"cast call $RAM \"pendingRefund(address)(uint256)\" $MY ADDRESS --rpc-url $RPC cast send $RAM \"withdrawRefund()\" --private-key $KEY --rpc-url $RPC"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"── After the deadline ──","anchor":"","text":"cast call $RAM \"isSettleable(uint16)(bool)\" $PUNK ID --rpc-url $RPC cast send $RAM \"settle(uint16)\" $PUNK ID --private-key $KEY --rpc-url $RPC `"},{"path":"/docs/guides/bid-on-a-return-auction","page":"Bid on a return auction","heading":"viem walkthrough","anchor":"viem-walkthrough","text":"Contract reference: ReturnAuctionModule, LiveBidAdapter, PunkVault, VaultBurnPool."},{"path":"/docs/guides/run-a-keeper","page":"Run a keeper","heading":"Run a keeper","anchor":"","text":"Four permissionless functions move value between protocol contracts. None of them requires a role, an allowlist entry, or any prior setup. Two pay the caller a small ETH reward; the other two pay nothing but keep the pipeline flowing. Anyone can run a keeper against them. Call Contract Reward What i"},{"path":"/docs/guides/run-a-keeper","page":"Run a keeper","heading":"LiveBidAdapter.sweep()","anchor":"livebidadaptersweep","text":"Address: 0x8C72FBc2bB32e76aa54243F76745266a0F92CD01 The adapter buffers every ETH inflow that funds the live bid and meters it into Patron. sweep() forwards buffered ETH in one of two modes, keyed on Patron's balance versus activationThreshold(): - Fast mode (patron.balance < activationThreshold()):"},{"path":"/docs/guides/run-a-keeper","page":"Run a keeper","heading":"BuybackBurner.executeStep(minOut)","anchor":"buybackburnerexecutestepminout","text":"Address: 0xf8a2D6F8c58626eE3BcDb4638F2a2f30Fe021242 Swaps up to maxStepWei() of queued ETH for $111 on the official pool and burns the output. Pacing: reverts StepTooEarly(nextBlock) before nextExecutableBlock(), and NothingToBurn() when remainingEth() is 0. The reward is 0.5% of the step (EXEC REWA"},{"path":"/docs/guides/run-a-keeper","page":"Run a keeper","heading":"ReturnAuctionModule.settle(punkId)","anchor":"returnauctionmodulesettlepunkid","text":"Address: 0x2ced1CA0BC7996c3dFB3D4E685d9d9E7405A7aCe Resolves a return auction once block.timestamp = endsAt(punkId). Reverts SaleLive(punkId) before then, SaleMissing(punkId) for a Punk with no sale, and AlreadySettled(punkId) after. Two branches: - Cleared (a high bid exists): the Punk goes to the "},{"path":"/docs/guides/run-a-keeper","page":"Run a keeper","heading":"ProtocolFeePhaseAdapter.sweep()","anchor":"protocolfeephaseadaptersweep","text":"Address: 0xed3E9D3Bf693372060b7ce62aDB49650145b2ba9 The hook deposits the protocol fee leg into the artcoins fee escrow under this adapter's address on every swap. sweep() claims it from the escrow and forwards the full balance to the protocol fee controller. No reward. A failed escrow claim is non-"},{"path":"/docs/guides/run-a-keeper","page":"Run a keeper","heading":"A minimal viem keeper loop","anchor":"a-minimal-viem-keeper-loop","text":"The sketch below checks readiness for all four calls each pass, simulates every candidate, and sends the rewarded ones only when the reward covers the estimated gas. Fill in your own transport and account handling. The protocol's own dashboard at /debug/distribution evaluates the same readiness pred"},{"path":"/docs/guides/swap-with-attribution","page":"Swap with referral attribution","heading":"Swap with referral attribution","anchor":"","text":"Every swap on the official $111 pool can carry an attribution payload in the V4 hookData. When the payload names a valid referrer, the skim hook carves a referral slice out of the protocol fee leg and credits it to the referrer in ReferralPayout, in the same transaction as the swap. Frontends, walle"},{"path":"/docs/guides/swap-with-attribution","page":"Swap with referral attribution","heading":"How the slice is sized","anchor":"how-the-slice-is-sized","text":"Per swap: - att.referralBps is what your payload requests, in a 100k denominator (250 = 0.25% of swap volume) - maxReferralBpsOfVolume is the pool's cap, set to 250 (0.25% of volume) at launch and admin-tunable up to the hook's hard limit MAX REFERRAL CAP OF VOLUME = 1000 (1% of volume) - the slice "},{"path":"/docs/guides/swap-with-attribution","page":"Swap with referral attribution","heading":"The encoding: a single-struct abi.encode","anchor":"the-encoding-a-single-struct-abiencode","text":"The hook decodes the swap's hookData as a 1-tuple : abi.decode(hookData, (PoolSwapData)). The struct shapes: The gotcha: encode the outer envelope as a single struct argument, abi.encode(poolSwapData), never as a 2-tuple of raw bytes like abi.encode(bytes(\"\"), inner). The 2-tuple form is off by a 32"},{"path":"/docs/guides/swap-with-attribution","page":"Swap with referral attribution","heading":"TypeScript encoding example","anchor":"typescript-encoding-example","text":"This mirrors the encoder the official frontend uses: Pass the result as the hookData argument of the V4 swap (Universal Router, PoolManager.swap, or whatever router you use). If a swap carries no attribution, pass '0x' instead. Requesting more than the cap is harmless: the hook clamps to maxReferral"},{"path":"/docs/guides/swap-with-attribution","page":"Swap with referral attribution","heading":"Where the credit lands","anchor":"where-the-credit-lands","text":"On each attributed swap the hook: 1. emits SwapAttribution(poolId, swapper, referrer, sourceId, campaignId, quoteVolume, referralPaid) with the per-swap context 2. pays the slice into ReferralPayout at 0xB03Cbd862F47059e928C113182814c676eA29d4c via its hook-only notify(referrer), which increments ba"},{"path":"/docs/guides/swap-with-attribution","page":"Swap with referral attribution","heading":"Claiming","anchor":"claiming","text":"The send carries a CLAIM GAS = 35 000 gas budget. A claim with a zero balance reverts NothingToClaim(); a recipient that rejects the ETH reverts TransferFailed() with the balance reinstated, so funds are never lost. Stray ETH sent directly to the contract is accepted but never credited to anyone. Or"},{"path":"/docs/guides/swap-with-attribution","page":"Swap with referral attribution","heading":"The ?ref= URL pattern","anchor":"the-ref-url-pattern","text":"The official frontend resolves the referrer it encodes, in priority order: a ?ref=0x... query parameter on the current visit (persisted to localStorage, so it stays sticky across later visits), then the previously stored value, then an operator-configured default. Any address case is accepted and no"},{"path":"/docs/guides/contribute-to-the-bid","page":"Contribute to the live bid","heading":"Contribute to the live bid","anchor":"","text":"Any address can grow the live bid by sending ETH to LiveBidAdapter at 0x8C72FBc2bB32e76aa54243F76745266a0F92CD01. The adapter is the single faucet into the bid: Patron accepts ETH only from it, so every contribution, fee, and refund enters here, buffers, and meters into the bid under one rate policy"},{"path":"/docs/guides/contribute-to-the-bid","page":"Contribute to the live bid","heading":"contribute(referrer, tag): the attributed top-up","anchor":"contributereferrer-tag-the-attributed-top-up","text":"This is the canonical on-chain destination for capital that wants to align with the collection. It pays a referrer share and logs a structured event for indexers: - REFERRER CONTRIB BPS = 500: 5% of msg.value goes to referrer, hard-coded with no setter and no admin. The remaining 95% joins the buffe"},{"path":"/docs/guides/contribute-to-the-bid","page":"Contribute to the live bid","heading":"Bare sends","anchor":"bare-sends","text":"A plain ETH transfer to the adapter also works: receive() accepts from any sender and emits BareTopUp(sender, amount). The ETH joins the same buffer. Use contribute instead whenever you want attribution; a bare send carries none. Never send directly to Patron: its receive() reverts NotAdapter for an"},{"path":"/docs/guides/contribute-to-the-bid","page":"Contribute to the live bid","heading":"How buffered ETH reaches the bid","anchor":"how-buffered-eth-reaches-the-bid","text":"Buffered ETH forwards to Patron through the permissionless sweep() (which pays the caller a small keeper reward) and the swap-driven streamForward(), under a two-mode meter keyed on Patron's balance against the adapter's activationThreshold: below the threshold the buffer forwards without cooldown, "},{"path":"/docs/guides/contribute-to-the-bid","page":"Contribute to the live bid","heading":"Integration pattern: launchpads and mints","anchor":"integration-pattern-launchpads-and-mints","text":"The primary integration target is a \"route X% of proceeds to Permanent Collection\" option on NFT launchpads, mint contracts, wallet widgets, and treasury flows. The shape: 1. carve the chosen percentage out of the proceeds at your contract or backend 2. call contribute(referrer, tag) with that ETH. "},{"path":"/docs/guides/contribute-to-the-bid","page":"Contribute to the live bid","heading":"viem example","anchor":"viem-example","text":"From a Solidity integration, the same call is: Contract reference: LiveBidAdapter, Patron."},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"Read the collection state","anchor":"","text":"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."},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"The headline numbers","anchor":"the-headline-numbers","text":"`bash"},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"Traits permanently collected (0..111)","anchor":"","text":"cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 \"collectedCount()(uint256)\" --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"The live bid, in wei","anchor":"","text":"cast call 0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c \"bidBalance()(uint256)\" --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"Punks vaulted so far","anchor":"","text":"cast call 0x3614692b8C8B22890D66a5DfcBc6F6eAdEdE036f \"lockedPunkCount()(uint256)\" --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"Proofs minted so far (0..111)","anchor":"","text":"cast call 0x3614692b8C8B22890D66a5DfcBc6F6eAdEdE036f \"totalProofsMinted()(uint256)\" --rpc-url https://ethereum-rpc.publicnode.com"},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"Is the work complete (all 111 trait bits collected)?","anchor":"","text":"cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 \"isComplete()(bool)\" --rpc-url https://ethereum-rpc.publicnode.com `"},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"PermanentCollection: the records core","anchor":"permanentcollection-the-records-core","text":"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 Populatio"},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"Patron: the live bid","anchor":"patron-the-live-bid","text":"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 "},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"ReturnAuctionModule: per-Punk sale state","anchor":"returnauctionmodule-per-punk-sale-state","text":"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), ta"},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"PunkVault: Proofs and the Title","anchor":"punkvault-proofs-and-the-title","text":"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 isProofMint"},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"Metadata of the Proof for trait 23","anchor":"","text":"cast call 0x3614692b8C8B22890D66a5DfcBc6F6eAdEdE036f \"proofMeta(uint256)(uint16,uint8,uint16,uint64)\" 23 --rpc-url https://ethereum-rpc.publicnode.com `"},{"path":"/docs/guides/read-collection-state","page":"Read the collection state","heading":"Decoding the 111-bit masks","anchor":"decoding-the-111-bit-masks","text":"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: Reading and decoding with viem: And with cast, printing the mask as hex for eyeb"},{"path":"/docs/offchain/ai-agents","page":"AI agents","heading":"AI agents","anchor":"","text":"This page is the entry point for automated consumers of the protocol: agents, indexers, keeper bots, and retrieval pipelines. It links out to the full reference rather than restating it. Everything below is a public read or a permissionless call; nothing here needs an API key. The protocol is a set "},{"path":"/docs/offchain/ai-agents","page":"AI agents","heading":"Machine-readable surfaces","anchor":"machine-readable-surfaces","text":"Surface URL What it is --- --- --- Protocol manifest /protocol-manifest.json Canonical origin, addresses, ABI paths, and docs links per contract Contract ABIs /abis/<ContractName .json One plain ABI array per contract LLM orientation /llms.txt A compact map of the whole reference, with absolute link"},{"path":"/docs/offchain/ai-agents","page":"AI agents","heading":"Recipe 1: read headline protocol state","anchor":"recipe-1-read-headline-protocol-state","text":"Cheapest first. The indexer's protocolCounter(id: \"global\") row holds the running totals in one query: For the live bid and the count without an indexer, read the chain directly: The site also serves /api/live-bid and /api/stats (cached JSON) for the same numbers. Full read surface: read the collect"},{"path":"/docs/offchain/ai-agents","page":"AI agents","heading":"Recipe 2: check whether a Punk is eligible","anchor":"recipe-2-check-whether-a-punk-is-eligible","text":"A Punk is eligible for the live bid when it carries an uncollected, not-pending trait. The one call that answers it is PermanentCollection.canonicalTargetOf(punkId), which returns the protocol-derived target trait or reverts NoEligibleTarget: The /api/eligibility?punkId=<id endpoint resolves the who"},{"path":"/docs/offchain/ai-agents","page":"AI agents","heading":"Recipe 3: find live return auctions","anchor":"recipe-3-find-live-return-auctions","text":"Every accepted Punk sits in a 72-hour return auction until it clears or is vaulted. The indexer lists the open ones, soonest-ending first: On-chain, ReturnAuctionModule.isLive(punkId), getSale(punkId), and isSettleable(punkId) answer the same per Punk. See ReturnAuctionModule and bid on a return auc"},{"path":"/docs/offchain/ai-agents","page":"AI agents","heading":"Recipe 4: watch protocol events","anchor":"recipe-4-watch-protocol-events","text":"Two ways in. For historical or aggregate views, query the indexer: each table names the events that feed it. For live reaction, subscribe to logs with viem. The events index lists every event and its contract. Load the exact event signature for any contract from its ABI at /abis/<ContractName .json "},{"path":"/docs/offchain/ai-agents","page":"AI agents","heading":"Recipe 5: run keeper actions","anchor":"recipe-5-run-keeper-actions","text":"Four permissionless calls keep the protocol moving; three pay the caller a bounded reward. Check readiness off-chain, simulate, then send: - LiveBidAdapter.sweep() meters the buffer into the live bid - BuybackBurner.executeStep(minOut) buys and burns $111 in paced steps - ReturnAuctionModule.settle("},{"path":"/docs/offchain/ai-agents","page":"AI agents","heading":"Recipe 6: load the protocol manifest and ABIs","anchor":"recipe-6-load-the-protocol-manifest-and-abis","text":"Fetch the manifest once, then instantiate any contract from it: The same pattern works for any contract in the manifest. Details and the full ABI list: ABIs and the protocol manifest."},{"path":"/docs/offchain/ai-agents","page":"AI agents","heading":"Recipe 7: use the public REST endpoints","anchor":"recipe-7-use-the-public-rest-endpoints","text":"The site exposes cached JSON at /api/ for the reads its own pages need: /api/live-bid, /api/stats, /api/eligibility, /api/auction-bids, /api/punks-with-trait, and more. These serve the frontend and are not a versioned API contract; on-chain reads and the indexer are the canonical interfaces. Respons"},{"path":"/docs/offchain/ai-agents","page":"AI agents","heading":"Recipe 8: use the docs search index as retrieval context","anchor":"recipe-8-use-the-docs-search-index-as-retrieval-context","text":"/docs-search-index.json is this reference split into retrievable records, one per section. Each record is: Embed or keyword-match over heading + text, then resolve a citation URL as ${origin}${path} (append #${anchor} when set). For a smaller map of the same content, /llms.txt lists every page with "},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Indexer (GraphQL)","anchor":"","text":"The protocol runs a Ponder indexer that follows every protocol contract from the deploy block and maintains the tables below. It is the right data source for anything historical or aggregate: acquisition history, auction bid logs, burn totals, referral ledgers. Current single values (the live bid, a"},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Endpoint","anchor":"endpoint","text":"The GraphQL endpoint is deployment-configured, not a fixed public URL. The site's server reads it from the INDEXER URL environment variable and queries it server-side only; the browser never talks to the indexer directly. To run your own instance, point the Ponder app at a mainnet RPC and it serves "},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Tables","anchor":"tables","text":""},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Acquisitions and the live bid","anchor":"acquisitions-and-the-live-bid","text":"Table What it records Key fields Fed by --- --- --- --- acquisition Per-Punk latest acquisition, keyed by punkId and overwritten on re-acquisition of a returned Punk (mirrors the on-chain per-Punk readers) punkId, targetTraitId, mask, acquirer, originalSeller, priceWei, custody (InReturnAuction / Re"},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Return auctions","anchor":"return-auctions","text":"Table What it records Key fields Fed by --- --- --- --- returnAuction Per-Punk auction state, keyed by punkId; reset when a returned Punk is re-auctioned punkId, targetTraitId, acquisitionCost, reserveWei, startedAt, endsAt, highBidWei, highBidder, extensions, settled, outcome (Cleared / Vaulted), b"},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Vault and Proofs","anchor":"vault-and-proofs","text":"Table What it records Key fields Fed by --- --- --- --- vaultedPunk One row per Punk permanently vaulted punkId, collectedTraitId, vaultedAtBlock, txHash ReturnAuctionModule.PunkVaulted proof One row per Proof NFT (token ids 0..110, tokenId == traitId), minted on the first vaulting of a trait; the m"},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Fees and burns","anchor":"fees-and-burns","text":"Table What it records Key fields Fed by --- --- --- --- adapterSweep LiveBidAdapter forwards into Patron; keeper rewards land as sibling rows (-reward id suffix) joinable by txHash ethSwept, ethForwarded, ethBuffered, keeper, keeperReward LiveBidAdapter.Swept / KeeperReward vaultBurnSweep VaultBurnP"},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Title auction","anchor":"title-auction","text":"Table What it records Key fields Fed by --- --- --- --- titleAuctionState Singleton row (id: \"global\") tracking the Vault Title auction lifecycle kickedOff, settled, endsAt, highBidWei, highBidder, winner, finalHighBidWei, restartCount, extensionsThisRound PunkVaultTitleAuction.Kickoff / Bid / Exten"},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Referrals","anchor":"referrals","text":"Table What it records Key fields Fed by --- --- --- --- referrer Per-referrer aggregate, keyed by address; balance mirrors ReferralPayout.balances(referrer) on chain balance (= totalCredited - totalClaimed), totalCredited, totalClaimed, lastCreditedAt, lastClaimedAt ReferralPayout.ReferralCredited /"},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Counters","anchor":"counters","text":"Table What it records Key fields Fed by --- --- --- --- protocolCounter Singleton row (id: \"global\") of protocol-wide headline numbers, cheaper than aggregating the event tables collectedCount, acquisitionCount, vaultedCount, clearedCount, proofsMinted, totalEthBurned, totalTokensBurned, totalBounty"},{"path":"/docs/offchain/indexer","page":"Indexer (GraphQL)","heading":"Example queries","anchor":"example-queries","text":"Headline protocol stats: Open return auctions, soonest-ending first: Every Proof minted so far, in collection order, with current holders:"},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"REST endpoints","anchor":"","text":"The site at permanentcollection.art exposes a set of JSON read endpoints under /api. They exist to serve the site's own frontend and are not a versioned API contract : shapes can change with any deploy, there is no deprecation policy, and rate limits protect the site, not integrators. The canonical "},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"Live bid and stats","anchor":"live-bid-and-stats","text":""},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/live-bid","anchor":"get-apilive-bid","text":"No parameters. The current live bid plus the ETH already collected upstream of Patron that will enter the bid on the next sweep. liveBidWei is Patron.bidBalance(); pendingWei is LiveBidAdapter's ETH balance (its buffer). protocolLegPendingWei is always \"0\" (the protocol leg never funds the bid; the "},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/stats","anchor":"get-apistats","text":"No parameters. The indexer's protocolCounter singleton wrapped in a health envelope: reachable: false means the indexer was down (counter is null); ok: false with reachable: true means no counter row yet. Server cache window 30 seconds."},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/price/{chainId}/{address}","anchor":"get-apipricechainidaddress","text":"Path params: chainId (1 for mainnet) and a token address. A GeckoTerminal proxy returning {\"priceUsd\": number null, \"change24h\": number null}. Unrecognized chain ids return the null shape. Edge-cached: s-maxage=60, stale-while-revalidate=600."},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"Accepting the live bid","anchor":"accepting-the-live-bid","text":""},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/eligibility","anchor":"get-apieligibility","text":"Query params: punkId (0..9999, required), caller (0x address, optional). Resolves a Punk against live protocol state for the accept-the-bid flow. Returns the eligibility record plus pre-rendered visuals: - punkId, owner, caller, isOwnedByCaller - mask (the Punk's trait bitmask, decimal string) - unc"},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/owned-punks","anchor":"get-apiowned-punks","text":"Query param: address (0x address, required). The Punk ids held by that address, with per-Punk rendered tiles and 2017-market context: {punkIds, svgsByPunkId, bidsByPunkId, listingsByPunkId}. bidsByPunkId carries the highest standing market bid ({valueWei, bidder}); listingsByPunkId carries public li"},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/owned-trait-options","anchor":"get-apiowned-trait-options","text":"Query param: owner (0x address, required). The rarest-first trait options across the owner's Punks for the accept-the-bid picker: {options, punkSilhouettes, listedPunkIds}, where listedPunkIds marks Punks already listed exclusively to Patron."},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/sold-by","anchor":"get-apisold-by","text":"Query param: seller (0x address, required). Recent acceptBid acceptances where this address was the seller, for the proceeds-claim surface: {sold: [{punkId, amountWei, blockNumber, timestamp, txHash}]}. Scans a recent window (currently the last 500 accepted-bid events), so it is a convenience view, "},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"Auctions","anchor":"auctions","text":""},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/auction-bids","anchor":"get-apiauction-bids","text":"Query param: punkId (0..9999, required). The bid history of that Punk's return auction, from the indexer: {bids: [{bidder, amount, blockNumber, timestamp, txHash}]}. Server cache window 30 seconds; 503 on an indexer outage."},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/title-auction/bids","anchor":"get-apititle-auctionbids","text":"No parameters. The Vault Title auction's bid history: {bids: [{bidder, amount, endsAt, extended, blockNumber, timestamp, txHash}]}. Server cache window 30 seconds."},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"Traits and referrals","anchor":"traits-and-referrals","text":""},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/punks-with-trait","anchor":"get-apipunks-with-trait","text":"Query params: traitId (0..110, required), offset (default 0), limit (default 60, max 120). A page of the Punk ids carrying that trait, ordered rarest first, with rendered tiles: {punkIds, svgsByPunkId, total}. Served from in-memory data (no chain or indexer read)."},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"GET /api/referral","anchor":"get-apireferral","text":"Query param: address (0x address, required). The referrer's ledger status, composed from the indexer's aggregate plus a chain read of the hook's within-swap accrual: balance mirrors ReferralPayout.balances(referrer) (claimable via ReferralPayout). Server cache window 30 seconds, keyed per address; 5"},{"path":"/docs/offchain/rest-api","page":"REST endpoints","heading":"Internal routes","anchor":"internal-routes","text":"The /api namespace also contains internal and operational routes (config, rpc, referral-alias, keeper-report, screenshot helpers for tests). They exist but are not part of the public surface and aren't documented here."},{"path":"/docs/offchain/abis-and-manifest","page":"ABIs and the protocol manifest","heading":"ABIs and the protocol manifest","anchor":"","text":"This page documents the machine-readable files themselves. For task-oriented recipes that use them (reading state, checking eligibility, watching events, running keeper actions), start at AI agents. Everything a program (or an agent) needs to work with the protocol is served as static JSON from the "},{"path":"/docs/offchain/abis-and-manifest","page":"ABIs and the protocol manifest","heading":"The protocol manifest","anchor":"the-protocol-manifest","text":"/protocol-manifest.json is the single machine-readable root. Top-level fields: name, chainId, deployBlock, canonicalPoolId (the official V4 pool id), docs, and llms. Under contracts, one entry per protocol contract keyed by contract name: external carries the launch-stack and reference addresses tha"},{"path":"/docs/offchain/abis-and-manifest","page":"ABIs and the protocol manifest","heading":"Served ABIs","anchor":"served-abis","text":"Each file under /abis/ is a plain JSON ABI array, directly usable as the abi argument in viem, ethers, or wagmi. Available contract names: Protocol contracts (each with a matching reference page under /docs/contracts/): PermanentCollection, Patron, ReturnAuctionModule, ReturnAuctionEscrow, PunkVault"},{"path":"/docs/offchain/abis-and-manifest","page":"ABIs and the protocol manifest","heading":"Loading the manifest with viem","anchor":"loading-the-manifest-with-viem","text":"The same pattern works for any contract in the manifest; swap the key and the function."},{"path":"/docs/offchain/abis-and-manifest","page":"ABIs and the protocol manifest","heading":"Importing from the repo","anchor":"importing-from-the-repo","text":"The site is built from the public repo at github.com/ripe0x/permanent-collection. The ABI files live at app/lib/abis/, each as both a raw <Name .json and a typed TypeScript module <Name .ts exporting abi with as const typing for viem/wagmi type inference: If you vendor ABIs into your own project, pr"},{"path":"/docs/reference/access-control","page":"Access control","heading":"Access control","anchor":"","text":"Every state-changing function in the protocol and who may call it. Generated from the per-contract references; the per-function pages carry the full behavior."},{"path":"/docs/reference/access-control","page":"Access control","heading":"PermanentCollection","anchor":"permanentcollection","text":"Function Access --- --- recordAcquisition patron-only (msg.sender must equal the wired patron address, else NotPatron) markCustody returnAuctionModule-only (msg.sender must equal the wired returnAuctionModule address, else NotReturnAuction) setWiring deployer one-shot (OneTimeSetup gate: caller must"},{"path":"/docs/reference/access-control","page":"Access control","heading":"Patron","anchor":"patron","text":"Function Access --- --- acceptBid permissionless (owner pre-condition: the Punk must be listed acceptListing permissionless (listing pre-condition: the seller must be allowlisted skimSurplus permissionless addAllowedSeller admin-only (raw ProtocolAdmin.admin(); a lifetime carve-out that removeAllowe"},{"path":"/docs/reference/access-control","page":"Access control","heading":"ReturnAuctionModule","anchor":"returnauctionmodule","text":"Function Access --- --- placeBid permissionless placeBidWithReferral permissionless settle permissionless withdrawRefund permissionless startSale patron-only (msg.sender == patron, else NotPatron) setLiveBidAdapter deployer one-shot (msg.sender == deployer, once, else setVaultBurnPool deployer one-s"},{"path":"/docs/reference/access-control","page":"Access control","heading":"ReturnAuctionEscrow","anchor":"returnauctionescrow","text":"Function Access --- --- listForSettlement module-only (MODULE, the deploying ReturnAuctionModule; any other caller reverts NotModule) sweepProceeds module-only (any other caller reverts NotModule) receive market-only (only the CryptoPunks market; any other sender reverts UnexpectedEtherSender)"},{"path":"/docs/reference/access-control","page":"Access control","heading":"PunkVault","anchor":"punkvault","text":"Function Access --- --- receivePunk returnAuctionModule-only (NotReturnAuction for any other caller) mintProofs returnAuctionModule-only (NotReturnAuction for any other caller) mintToAuction titleAuction-only (NotTitleAuction for any other caller) setTitleAuction deployer one-shot (NotDeployer for o"},{"path":"/docs/reference/access-control","page":"Access control","heading":"LiveBidAdapter","anchor":"livebidadapter","text":"Function Access --- --- sweep permissionless streamForward permissionless (implements IPreSwapStream; designed to be called by the official pool's hook in beforeSwap, but anyone may call) contribute permissionless, payable poolReplenish module-only (msg.sender must equal returnAuctionModule; anyone "},{"path":"/docs/reference/access-control","page":"Access control","heading":"BuybackBurner","anchor":"buybackburner","text":"Function Access --- --- executeStep permissionless setup deployer one-shot, gated by OneTimeSetup (original deployer only, before finalization) setMinBlocksBetweenSteps admin-only, ProtocolAdmin.checkAdmin(msg.sender); locks at the 1-year admin expiry setMaxStepWei admin-only, ProtocolAdmin.checkAdm"},{"path":"/docs/reference/access-control","page":"Access control","heading":"VaultBurnPool","anchor":"vaultburnpool","text":"Function Access --- --- sweep returnAuctionModule-only (any other caller reverts NotReturnAuctionModule) setup deployer one-shot (onlySetup: deployer-only until finalized, then closed forever) receive permissionless"},{"path":"/docs/reference/access-control","page":"Access control","heading":"ProtocolFeePhaseAdapter","anchor":"protocolfeephaseadapter","text":"Function Access --- --- sweep permissionless receive permissionless"},{"path":"/docs/reference/access-control","page":"Access control","heading":"ReferralPayout","anchor":"referralpayout","text":"Function Access --- --- claim permissionless claimFor permissionless notify hook-only (the immutable hook address set at construction; every other caller reverts Unauthorized) receive permissionless"},{"path":"/docs/reference/access-control","page":"Access control","heading":"PunkVaultTitleAuction","anchor":"punkvaulttitleauction","text":"Function Access --- --- bid permissionless (payable) settle permissionless kickoff permissionless, gated on the collected-trait threshold mintTitle permissionless, idempotent withdrawRefund permissionless, pays msg.sender's own queued refund withdrawProceeds permissionless, funds always go to the cr"},{"path":"/docs/reference/access-control","page":"Access control","heading":"PCSwapContext","anchor":"pcswapcontext","text":"Function Access --- --- enterSwap extension-only (msg.sender must equal the current authorizedExtension, which must be non-zero; otherwise reverts NotAuthorizedExtension) exitSwap extension-only (same gate as enterSwap) setAuthorizedExtension owner-only (reverts NotOwner otherwise; reverts Authorize"},{"path":"/docs/reference/access-control","page":"Access control","heading":"TokenAdminPoker","anchor":"tokenadminpoker","text":"Function Access --- --- setup owner-only, one-shot (reverts AlreadySetup on a second call) bindExtension owner-only (reverts NotOwner otherwise; reverts NotSetup before setup) lockExtension owner-only (reverts NotOwner otherwise; reverts NotSetup before setup) setHookMaxReferralBps two-key (either o"},{"path":"/docs/reference/access-control","page":"Access control","heading":"ProtocolAdmin","anchor":"protocoladmin","text":"Function Access --- --- transferAdmin admin-only (msg.sender must equal the current admin; any other caller reverts NotAdmin)"},{"path":"/docs/reference/access-control","page":"Access control","heading":"RendererRegistry","anchor":"rendererregistry","text":"Function Access --- --- setImplementation admin-only (ProtocolAdmin.checkAdmin(msg.sender) must be true, so the caller must hold the admin role with its 1-year timer unexpired) freeze admin-only (ProtocolAdmin.checkAdmin(msg.sender)), one-way"},{"path":"/docs/reference/access-control","page":"Access control","heading":"PermanentCollectionMosaicRenderer","anchor":"permanentcollectionmosaicrenderer","text":"Function Access --- --- cacheTrait permissionless"},{"path":"/docs/reference/access-control","page":"Access control","heading":"PunkSvgFragmentCache","anchor":"punksvgfragmentcache","text":"Function Access --- --- cachePunk permissionless"},{"path":"/docs/reference/access-control","page":"Access control","heading":"TraitIconCache","anchor":"traiticoncache","text":"Function Access --- --- cacheTrait permissionless"},{"path":"/docs/reference/errors","page":"Errors index","heading":"Errors index","anchor":"","text":"Every custom error the protocol can revert with, its contract, and the condition that raises it. AlreadyFinalized · BuybackBurner\\ setup was called after the one-shot gate closed. AlreadyFinalized · Patron\\ setWiring was called after the one-shot setup gate already closed. AlreadyFinalized · Permane"},{"path":"/docs/reference/events","page":"Events index","heading":"Events index","anchor":"","text":"Every event the protocol emits, grouped by contract. Signatures and field meanings live on the per-contract pages."},{"path":"/docs/reference/events","page":"Events index","heading":"PermanentCollection","anchor":"permanentcollection","text":"AcquisitionRecorded · Emitted once per recordAcquisition. CustodyUpdated · Emitted on every custody transition: InReturnAuction at record time, then ReturnedToMarket or Vaulted at settle, and InReturnAuction again on a re-acquisition. Finalized · Emitted exactly once, when setWiring closes the OneTi"},{"path":"/docs/reference/events","page":"Events index","heading":"Patron","anchor":"patron","text":"AllowedSellerAdded · Emitted when the admin adds a new seller to the acceptListing allowlist. AllowedSellerRemoved · Emitted when the admin removes an allowlisted seller. BidAccepted · Emitted on every successful acceptBid. Finalized · Emitted exactly once, when setWiring closes the one-shot setup g"},{"path":"/docs/reference/events","page":"Events index","heading":"ReturnAuctionModule","anchor":"returnauctionmodule","text":"BidPlaced · Emitted on every accepted bid. LiveBidAdapterSet · Emitted exactly once, at the deployer's setLiveBidAdapter call. PunkVaulted · Emitted on the unsold settle path. RefundQueued · Emitted when an outbid refund couldn't push to the previous bidder within the 30k gas budget. RefundWithdrawn"},{"path":"/docs/reference/events","page":"Events index","heading":"PunkVault","anchor":"punkvault","text":"Approval · Standard ERC721 single-token approval event for Proofs and the Title. ApprovalForAll · Standard ERC721 operator-approval event. ContractURIUpdated · ERC-7572 collection-metadata refresh hint. MetadataUpdate · EIP-4906 per-token metadata refresh hint. OwnershipTransferred · ERC-173 ownersh"},{"path":"/docs/reference/events","page":"Events index","heading":"LiveBidAdapter","anchor":"livebidadapter","text":"ActivationThresholdSynced · Emitted when the auto-sync inside sweep() processes a new acceptBid acquisition. BareTopUp · Emitted on every direct ETH send into receive(): the hook's bid-leg skim, the locker's LP-fee forwards, and unattributed top-ups. Contribution · Emitted on every contribute() call"},{"path":"/docs/reference/events","page":"Events index","heading":"BuybackBurner","anchor":"buybackburner","text":"BurnEthDeposited · Emitted on every ETH inflow through receive(). ExecutionRewardFailed · Emitted when the caller-reward send fails. ExecutionRewardPaid · Emitted when the caller reward is sent successfully. Finalized · Emitted exactly once, when setup completes and permanently closes the one-shot w"},{"path":"/docs/reference/events","page":"Events index","heading":"VaultBurnPool","anchor":"vaultburnpool","text":"Finalized · Emitted exactly once, by the successful setup call. SidePoolTaxBurned · Emitted on each sweep that burned a non-zero accrued $111 balance, with the amount burned. Swept · Emitted on each sweep whose ETH forward to BuybackBurner succeeded, with the amount forwarded."},{"path":"/docs/reference/events","page":"Events index","heading":"ProtocolFeePhaseAdapter","anchor":"protocolfeephaseadapter","text":"ClaimFailed · Emitted when the escrow claim inside sweep reverts, with the escrow address. Forwarded · Emitted on every successful sweep, with the recipient (the controller, indexed) and the amount forwarded."},{"path":"/docs/reference/events","page":"Events index","heading":"ReferralPayout","anchor":"referralpayout","text":"ReferralClaimed · Emitted on every successful claim/claimFor with the referrer (indexed) and the amount paid out. ReferralCredited · Emitted on every hook notify credit with the referrer (indexed) and the amount added to their balance."},{"path":"/docs/reference/events","page":"Events index","heading":"PunkVaultTitleAuction","anchor":"punkvaulttitleauction","text":"Bid · Emitted on every accepted bid. Extended · Emitted when an anti-snipe extension moved endsAt further into the future, alongside the Bid event that triggered it. Kickoff · Emitted when the auction clock (re)starts: once from kickoff(), and again from every no-bid settle() restart. ProceedsQueued"},{"path":"/docs/reference/events","page":"Events index","heading":"PCSwapContext","anchor":"pcswapcontext","text":"AuthorizedExtensionLocked · Emitted exactly once, on lockAuthorizedExtension. AuthorizedExtensionSet · Emitted on every setAuthorizedExtension with the new extension (indexed). OwnershipTransferred · Emitted at construction (from address(0)) and on every transferOwnership. SwapEntered · Emitted on e"},{"path":"/docs/reference/events","page":"Events index","heading":"TokenAdminPoker","anchor":"tokenadminpoker","text":"ExtensionBound · Emitted on every bindExtension with the hook (indexed) and the extension (indexed). ExtensionLocked · Emitted once, on lockExtension, with the hook (indexed). MaxReferralBpsSet · Emitted on every setHookMaxReferralBps with the hook (indexed) and the new cap. OwnershipTransferred · E"},{"path":"/docs/reference/events","page":"Events index","heading":"ProtocolAdmin","anchor":"protocoladmin","text":"AdminBurned · Emitted exactly once, on transferAdmin(address(0)). AdminTransferred · Emitted at construction (with previousAdmin = address(0)) and on every renewal or rotation."},{"path":"/docs/reference/events","page":"Events index","heading":"RendererRegistry","anchor":"rendererregistry","text":"Frozen · Emitted exactly once, when freeze() is called, with the block number. ImplementationUpdated · Emitted at deployment (with previous == address(0)) and on every successful setImplementation, with the previous and next implementation addresses, both indexed."},{"path":"/docs/reference/events","page":"Events index","heading":"PunkSvgFragmentCache","anchor":"punksvgfragmentcache","text":"PunkCached · Emitted exactly once per Punk, on its first successful cachePunk, with the Punk id (indexed), the SSTORE2 pointer address (indexed), and the fragment byte length."},{"path":"/docs/reference/events","page":"Events index","heading":"TraitIconCache","anchor":"traiticoncache","text":"TraitCached · Emitted exactly once per trait, on its first successful cacheTrait, with the trait id (indexed), the SSTORE2 pointer address (indexed), and the fragment byte length."}]
