---
title: "Contracts — PDProject"
description: "The per-Project ERC-721: fully on-chain generative art via SSTORE2 and data-URI tokenURI, payouts inside every mint that no wallet can block, per-token hashes, marketplace attributes, the entry window, and EIP-2981."
category: "contracts"
keywords: ["PDProject", "ERC-721", "tokenURI", "on-chain art", "mint", "entry window", "attributes"]
last_updated: "2026-10-12"
---

# Contracts — PDProject

One PDProject exists per Project on PD — an ERC-721 deployed by the factory and immutable from that moment: **no admin, no pause, no upgrade, no withdraw function, no balance.** It does three jobs: hold the art on-chain, run the mint, and answer the royalty standard.

## The art, on-chain

The artist's generative script is stored across up to 128 SSTORE2 data contracts, each holding one chunk of it as base64 text. A Project's script space is **1.5 MB**: the script, plus the library it binds and the library's unzipper, all on Ethereum. `tokenURI(tokenId)` returns a fully self-contained data URI:

- Metadata JSON with the Output's name, image and attributes.
- An `animation_url` that is a complete `data:text/html` document inlining the script, the token's hash, and — when the Project binds one — a blessed library from the [registry](/docs/contracts/library-registry), stored as base64 of its gzipped source and decoded in the page by the registry's own on-chain inflater before the artist's first line runs.
- An `image` field that starts as an on-chain placeholder SVG and is upgraded **exactly once per token** to an Arweave-pinned preview by the platform's writer key (`setArweaveTxid`) — only after the token is revealed (`NotRevealed` otherwise), so a preview can never be pinned for the wrong Output. PD draws each preview from the token's own on-chain HTML. The preview is a convenience for wallets and marketplaces; the art renders from Ethereum regardless.

Until a token is revealed, its metadata has no `animation_url` at all (the `image` is the placeholder) and its Token Hash attribute reads `pending` — the script never runs on a stand-in hash.

The raw script is readable without touching `tokenURI` at all:

- `getScript()` returns the whole script, and `scriptChunk(i)` one chunk of it (`scriptChunkCount()` chunks in all). Both decode the stored base64 as they read, so they hand back the artist's exact bytes.
- Near the size limit `getScript()` is a heavy read. Read a big script chunk by chunk with `scriptChunk(i)` and join the chunks in order.
- `scriptPointers(i)` is the data contract behind chunk `i`. Its code holds the chunk as base64 text (the last chunk ends with up to 2 raw bytes), not the raw script, so decode it or use `scriptChunk(i)`.

### Marketplace attributes

Every Output's metadata carries six attributes, all sourced from chain state: **Token Hash** (the generative seed), **Token Number**, **Artist** (the immutable artist address), **Language** — the exact tool the Output is made with, copied from the [registry](/docs/contracts/library-registry) as "name version" (`p5.js 1.11.13`) at deployment and frozen; `JavaScript` for vanilla Projects — **Colorway** (the Project's signature color, six hex characters, present only when the Project declared one), and **Edition Size**.

## The mint

```solidity
function mint(uint256 quantity) external payable;   // 1 – 22 per transaction
```

Each mint transaction, atomically:

1. Requires **exact payment**: `mintPrice × quantity` plus the live per-token storage fee (`currentStorageFeeWei()`, read from the factory). No overpayment path exists. The one exception is the Project's own artist, who pays the storage fee alone (no mint price, so no shares), as often as they like, through the same entry window.
2. Commits each new token (IDs 1-indexed, sequential) to its seed block: `seedBlock[tokenId] = block.number`. The `tokenHash` is `keccak256(blockhash(seedBlock), tokenId, project)`, which is unknowable while the mint runs (a block's own hash doesn't exist until it seals), so a minter has nothing to read and revert on. `reveal(tokenId)` / `revealBatch(ids)` write it from the next block on. Both are permissionless and write-once, and the caller and timing change nothing. The seed block's hash stays readable for 8,191 blocks (`BLOCKHASH` for the last 256, then the EIP-2935 history contract). Past that, a reveal re-commits the token to the current block (`RevealDeferred`) and a later reveal completes it, so every token stays revealable forever.
3. Pushes **95% of the mint price to the artist, 5% to the platform wallet, and the storage fee to the storage wallet** — inside the same transaction. Every share is force-sent: the recipient gets a plain call with 50,000 gas, and if it reverts or runs past that, the ETH is delivered anyway in the same transaction. No recipient, artist or fee wallet, can block or veto a mint. The contract never holds mint funds.
4. Emits `Minted(minter, tokenId, seedBlock)` per token; `Revealed(tokenId, tokenHash)` follows at reveal, with `MetadataUpdate(tokenId)` (ERC-4906) at reveal and again when the preview is pinned, so marketplaces refresh the Output on their own.

The artist share is computed as basis points (9,500 / 10,000) with the platform taking the exact complement, so no rounding dust is ever stranded.

## The entry window

Every Project opens with a **60-second entry window** — the opening minute. It is not optional and not a setting: the length is a constant in the contract, no caller passes it, and no key can skip, shorten or extend it, so an artist deploying straight against the factory gets exactly the same window as one going through the app. While the window runs, on-chain minting is closed and entries are collected as pre-signed transactions treated as **simultaneous** — arrival order inside the window buys nothing, which is the anti-snipe point. If demand fits supply, the platform's settlement key closes the window and seats the entries; they land as normal mints, then minting opens to everyone. If oversubscribed, the close is the lock: it seats nobody and anchors the root of the locked entry list in the close event, before the draw's random number exists. The draw's winners are then seated on-chain round by round and mint first; a drop that sells out through the draw finishes there, and otherwise the leftovers open first-come at the deadline. Tokens minted during a contested settlement are transfer-sealed until about the end of the settlement room (15 minutes at most) — the seal is set at the lock, is write-once and can never be extended.

The window has two clocks. The **entry minute** (`WINDOW_SECONDS`, 60) is when presses count, and the settlement key cannot close the window before it ends (`EntryMinuteOpen`). The **settlement room** (`SETTLE_SECONDS`, 15 minutes) follows, giving the draw and the winners' mints time to land. The load-bearing guarantee is **fail-open**: if the settlement key never shows up, minting opens by itself at the immutable deadline, `windowDeadline` (deploy + 60 seconds + 15 minutes). No key, outage, or platform failure can ever permanently halt a mint. The settlement key's whole power is drop choreography — it can never touch funds, supply or pricing, none of its calls work past the deadline, and winners pay the exact same price through the exact same push-splits as any minter.

`dropPhase()` answers where a drop stands: `0` window open · `1` settling · `2` normal minting.

## Royalties

```solidity
function royaltyInfo(uint256, uint256 salePrice)
    external view returns (address receiver, uint256 royaltyAmount);
// → (paymentSplitter, salePrice * 5%)
```

The receiver is the Project's own [PaymentSplitter](/docs/contracts/payment-splitter), deployed beside it by the factory. `supportsInterface` advertises ERC-721, ERC-2981 and ERC-4906.

## Reads worth knowing

| Function | Returns |
| --- | --- |
| `totalSupply()` | Outputs minted so far |
| `tokenHashes(tokenId)` | The Output's immutable generative seed (zero until revealed) |
| `seedBlock(tokenId)` | The block the Output's seed is committed to |
| `currentStorageFeeWei()` | The live per-token storage fee |
| `contractURI()` | Project-level metadata for marketplaces |
| `getScript()` | The full raw generative script, decoded from chain (big scripts: `scriptChunk(i)`) |

## Further reading

- [PDFactory](/docs/contracts/pd-factory) — deployment and constraints
- [PaymentSplitter](/docs/contracts/payment-splitter) — where the 5% goes
- [Outputs in the app](/docs/app/outputs) — the same machinery, rendered
