Loading
PRICE DISCUSSION DOCS
APP
Updated 2026-10-12·View as Markdown

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

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, 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:

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

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.

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 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