---
title: "Contracts — PDFactory"
description: "The single deployed factory behind every PD Project: createProject, staged uploads for big scripts, the artist whitelist, the 60-day cooldown, supply bounds, platform wallets, and the storage-fee corridor."
category: "contracts"
keywords: ["PDFactory", "createProject", "stageScriptChunk", "createProjectFromStaged", "whitelist", "cooldown", "isProject", "colorway", "entry window"]
last_updated: "2026-10-12"
---

# Contracts — PDFactory

PDFactory is deployed once and creates every Project on the platform. It is the protocol's only governance surface: the artist whitelist, the platform wallets, and the storage-fee corridor live here — and none of its powers reach into a Project once deployed.

## createProject

```solidity
function createProject(
    string calldata name,           // ≤ 50 bytes
    string calldata symbol,         // ≤ 50 bytes
    uint256 maxSupply,              // 22 – 9,999
    uint256 mintPrice,              // wei, immutable
    uint256 libraryId,              // finalized registry entry, or none
    bytes[] calldata scriptChunks,  // ≤ 128 chunks, stored via SSTORE2
    string calldata colorway,       // "" or exactly six hex chars
    uint256 expectedInflaterId      // the unzipper you previewed with
) external returns (address project);
```

There is no window parameter and no second overload. Every Project deploys with PDProject's fixed [60-second entry window](/docs/contracts/pd-project#the-entry-window), because the length is a constant in the Project contract rather than something a caller chooses — so no deploy path, including calling this contract directly, can produce a Project without one.

Callable by whitelisted artists only. Atomically deploys the artist's **PDProject** and its **PaymentSplitter**, records the new address in `isProject`, and emits `ProjectCreated`. The artist address is `msg.sender`, immutably. The `colorway` is the Project's signature color — six bare hex characters, emitted as a marketplace attribute on every Output; pass empty to omit it. `expectedInflaterId` is the registry's unzipper the artist tested with: if it has changed by the time the transaction lands, the deploy reverts (`InflaterMismatch`) instead of binding code the artist never previewed. Vanilla Projects (no library) ignore it.

### Scripts: chunks and the script space

A Project's script space is **1.5 MB**: script, library and unzipper together (`MAX_ARTWORK_BYTES`). The script arrives in chunks of up to 18,429 bytes (`MAX_CHUNK_BYTES`), each stored in its own SSTORE2 data contract as base64 text. Every chunk but the last is a multiple of 3 bytes, and every chunk after the first is sent **prefixed with the 13 bytes of script before it** (`OVERLAP_BYTES`): the factory checks the prefix against what it already holds and runs the URL-guard over prefix and chunk together, so a banned pattern split across two chunks is caught like any other. Only the chunk itself is stored.

A one-chunk script usually fits in one `createProject`. A bigger one goes up in pieces, one transaction each, from the artist's own wallet:

```solidity
function stageScriptChunk(uint256 index, bytes calldata data) external;   // index = chunks staged so far
function createProjectFromStaged(
    string calldata name,
    string calldata symbol,
    uint256 maxSupply,
    uint256 mintPrice,
    uint256 libraryId,
    bytes32 scriptHash,             // the rolling hash of the staged chunks
    string calldata colorway,
    uint256 expectedInflaterId
) external returns (address project);
function clearStagedScript() external;                                     // start over
function stagedScript(address artist) external view
    returns (address[] memory pointers, uint256 totalBytes, bytes32 scriptHash);
```

Staged chunks belong to the wallet that sent them; no other key can add to, bind or clear them. `index` must equal the number already staged, so a retried transaction can never land twice. `scriptHash` is the artist's commitment to the exact script: `h = keccak256(h ‖ keccak256(chunk))` over the raw chunks in order (prefixes excluded), starting from zero, so a missing, extra or stale chunk can never become a Project. `createProjectFromStaged` runs every check `createProject` does and consumes the staged set. An interrupted upload picks up where `stagedScript` says it stopped.

Internally, the `new PDProject` runs in the factory's **deployment arm** (`projectDeployer`) — a zero-governance satellite the factory spawns in its own constructor and is alone allowed to call. It exists purely for the EVM's 24,576-byte contract size limit; behavior is identical to an inline deploy.

### Constraints enforced at creation

| Constraint | Value |
| --- | --- |
| Artist whitelisted | `whitelistedArtists[msg.sender]` |
| Supply bounds | 22 ≤ `maxSupply` ≤ 9,999 (token IDs stay four digits) |
| Per-artist cooldown | 60 days between creations, clocked from creation |
| Script | 1–128 chunks of at most 18,429 bytes; every chunk but the last a multiple of 3; each after the first prefixed with the 13 bytes before it |
| Script space | Script + library + unzipper ≤ 1.5 MB; library + unzipper ≤ 512 KB and 64 chunks |
| Script content | No phone-home or non-determinism patterns (the on-chain URL-guard), across chunk boundaries too |
| Text fields | Strict JSON/UTF-8 validation — malformed metadata can never deploy |
| Colorway | Empty, or exactly six hex characters (bare, no `#`) |
| Library | Must be a finalized registry entry when bound, with the unzipper the artist named |

## The public registry

```solidity
mapping(address => bool)      public isProject;         // the authenticity check
mapping(address => address[]) public artistProjects;    // per-artist deployments
mapping(address => bool)      public whitelistedArtists;
mapping(address => uint256)   public lastProjectTimestamp;
```

`isProject(address)` is the one-call authenticity verification for any contract claiming to be a PD Project — see [the collector verification pattern](/docs/for-collectors/the-secondary-market).

## Governance surface

The admin can whitelist artists (`whitelistArtist` / `removeArtist`) and rotate the five operational keys — `platformWallet`, `storageFeeWallet`, `storageFeeWriter` (the key that pins each Output's write-once Arweave preview), `settlementKey` (the key that choreographs contested-drop windows), and `attestationKey` (the key that signs [keychain](/docs/contracts/pd-keychains) luck and polish attestations — deliberately a separate key from settlement, so no single secret holds both jobs) — each through a propose/accept two-step, so a rotation cannot brick payouts with a typo.

The two destinations that actually receive pushed ether — `platformWallet` and `storageFeeWallet` — accept with a small payment the factory bounces straight back by plain call, the way a mint's share normally arrives. A destination that cannot take ether (a contract with no way to receive) can therefore never become the live wallet; the acceptance itself is the proof of receipt. A sale never depends on it: every mint force-sends each share, so even a wallet that later stops taking ether still gets paid and can never halt a sale. Deployed Projects read these keys live, which is why a rotation applies platform-wide with no per-Project action.

The **storage fee** (`storageFeeWei`) — the flat per-token fee each mint routes to the storage wallet — is tuned by the admin, but only within `[storageFeeFloor, storageFeeCeiling]`, an immutable corridor fixed in the constructor. No key, ever, can set it outside that corridor; it is the ceiling on that power, forever.

## Events

`ProjectCreated`, `ScriptChunkStaged`, `StagedScriptCleared`, `ArtistWhitelisted`, `ArtistRemoved`, `AdminTransferStarted/Transferred`, `PlatformWalletProposed/Updated`, `StorageFeeWalletProposed/Updated`, `StorageFeeWriterProposed/Updated`, `SettlementKeyProposed/Updated`, `AttestationKeyProposed/Updated`, `StorageFeeUpdated`.

## Further reading

- [PDProject](/docs/contracts/pd-project) — what the factory deploys
- [Library Registry](/docs/contracts/library-registry) — what `libraryId` points at
- [The Mint Flow](/docs/for-artists/the-mint-flow)
