---
title: "Contracts — PDStickers"
description: "The sticker shop on-chain: ERC-1155 with fully on-chain SVG art, the sealed-sheet token model, FIXED and PACK peel modes, in-transaction payout splits, and StickerSplitter royalty vaults."
category: "contracts"
keywords: ["PDStickers", "ERC-1155", "sealed sheet", "peel", "StickerSplitter", "royalties"]
last_updated: "2026-10-17"
---

# Contracts — PDStickers

PDStickers is the [sticker economy's](/docs/stickers/overview) on-chain rail: one ERC-1155 contract holding every sticker and every sealed sheet, with the artwork on-chain: each sticker's full SVG, or, for PD's built-in stickers, a few bytes of parameters that the immutable **PDStickerRenderer** draws into the same SVG at read time. It follows the protocol's house rules — money never rests in it, metadata is self-contained, logic is immutable — and it reads the same factory the art contracts read, so the platform wallet rotates in exactly one place. Its companion, **StickerSplitter**, is the royalty vault (a three-way sibling of the Project [PaymentSplitter](/docs/contracts/payment-splitter)).

## The token model

Two kinds of token share the contract, in namespaces that cannot collide:

| Token | ID space | What it is |
| --- | --- | --- |
| Sticker | `1 …` (≤ 99 per sheet) | One sticker design; balance = how many you hold |
| Sealed sheet | `2^128 + sheetId` | An unopened sheet of its type — identical copies stack |

Buying mints sealed sheets; **`peel`** burns them and mints the stickers inside. Because identical sealed sheets share one token ID, a hundred unopened packs are a single wallet line (×100), marketplaces list them as one item, and a collection stays clean until its owner chooses the reveal. `uri(id)` answers for both kinds with base64 JSON whose image is the on-chain SVG as a data URI — sealed sheets return their wrapper art — with nothing off-chain (a built-in sticker's SVG is drawn by PDStickerRenderer, itself on-chain and immutable). The collection is **Price Discussion Stickers** (`name()` and `contractURI()`), and every token is named for its kind and number: a sealed sheet is `Price Discussion Sticker Sheet #<sheetId>`, a sticker `Price Discussion Sticker #<id>`. Both kinds carry Sheet and Type traits, and a sticker's own name is its Name trait (JSON-escaped).

## Purchase — exact payment, everything pushed

```solidity
function purchaseSheet(uint256 sheetId, uint256 quantity) external payable;
```

Requires the sheet active, within its print run (`maxSheets` — `0` means open), and **exact payment** (`priceWei × quantity`). At most 100 sealed sheets per purchase. Every share is force-sent inside the purchase transaction, before the mint, so a payee that refuses ETH can never block a sale and nothing is left in the contract when the purchase ends:

<svg viewBox="0 0 720 240" role="img" aria-labelledby="sticker-split-title" style="width:100%;height:auto;display:block;margin:0 0 14px">
<title id="sticker-split-title">A sheet purchase splits in-transaction: 5% to the platform wallet read live from the factory; the 95% creator side pays the collaborator's locked share first and the admin takes the remainder.</title>
<g font-family="'Courier New', Courier, monospace" font-size="13" font-weight="bold">
<rect x="10" y="88" width="180" height="58" fill="var(--stat-bg)" stroke="currentColor" stroke-width="1.5"/>
<text x="100" y="112" fill="currentColor" text-anchor="middle">purchaseSheet()</text>
<text x="100" y="131" fill="currentColor" text-anchor="middle" font-weight="normal" font-size="12">exact ETH in</text>
<path d="M190 100 L 320 44" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M318 39 L 331 40 L 322 50 Z" fill="currentColor"/>
<path d="M190 117 H 320" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M320 112 L 332 117 L 320 122 Z" fill="currentColor"/>
<path d="M190 134 L 320 190" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M322 184 L 331 194 L 318 195 Z" fill="currentColor"/>
<rect x="335" y="14" width="375" height="52" fill="var(--stat-bg)" stroke="currentColor" stroke-width="1.5"/>
<text x="345" y="36" fill="currentColor">PLATFORM · 5%</text>
<text x="345" y="55" fill="currentColor" font-weight="normal" font-size="12">factory.platformWallet(), read live — same rate as mints</text>
<rect x="335" y="91" width="375" height="52" fill="var(--stat-bg)" stroke="currentColor" stroke-width="1.5"/>
<text x="345" y="113" fill="currentColor">COLLABORATOR · their locked share</text>
<text x="345" y="132" fill="currentColor" font-weight="normal" font-size="12">collabBps of the 95% creator side, when the sheet has one</text>
<rect x="335" y="168" width="375" height="52" fill="var(--stat-bg)" stroke="currentColor" stroke-width="1.5"/>
<text x="345" y="190" fill="currentColor">ADMIN · the remainder</text>
<text x="345" y="209" fill="currentColor" font-weight="normal" font-size="12">the creator side after the collaborator's slice</text>
</g>
</svg>

The split uses the Project split's rates: 5% platform off the top (the wallet read live from `factory.platformWallet()`, the same rotation point mints pay), the remaining 95% is the creator side, and a sheet's collaborator — locked at `createSheet`, immutable forever — takes their `collabBps` slice of that side first. The platform share is floored, so the rounding wei goes to the creator side.

## Peel — FIXED and PACK

```solidity
function peel(uint256 sheetId, uint256 count) external payable returns (uint256 packPeelId);   // ≤ 10 per transaction
function revealPack(uint256 peelId) external;   // PACK only — anyone, from the next block
```

Burns `count` sealed sheets and delivers the stickers inside, one batch — at the peel for a FIXED sheet, at the reveal for a PACK. A FIXED peel sends no ETH. A PACK peel sends exactly `count × revealFeeWei`, the **reveal fee**, pushed whole and force-sent to the factory's storage-fee wallet, the wallet that sends the reveal and pays its gas; the admin can tune it only inside a corridor fixed in the code (0.00005–0.002 ETH per copy), and a peel sent at a stale fee reverts. What arrives depends on the sheet's mode, fixed before it ever went on sale:

- **FIXED** (`pullSize == 0`) — one of every sticker in the sheet, per sealed copy. Supply was **reserved at purchase**, so a sealed FIXED sheet is unconditionally peelable, forever.
- **PACK** (`pullSize > 0`) — each sealed copy draws `pullSize` **distinct** stickers from the sheet's pool. The draw is **commit-then-reveal**, the same recipe as [PDProject](/docs/contracts/pd-project) and the keychain crank: `peel` burns the sealed copies and commits the draw to its own block, whose hash doesn't exist while the peel runs; `revealPack(peelId)` (or `revealPacks`) draws from that hash and delivers from the next block on, each sheet's peels strictly in the order they were committed, so nobody can reorder reveals to steer a capped sticker's last copy. Anyone may reveal; the hash stays readable for 8,191 blocks (about 27 hours) through the EIP-2935 history contract, and past that the peel re-commits to a fresh block. No wallet, contract or smart account can see a draw and revert it (only the builder of the commit block can grind its hash), and delivery goes straight to the keeper's balance, so refusing a draw can't buy a re-roll either. Capped stickers leave the pool as they exhaust: supply is the rarity. While a PACK sheet is live or owes any copy a draw, at least `pullSize` open-edition stickers must stay active (enforced at activation and whenever a sticker is retired), so no sealed pack can ever become unpeelable by exhaustion.

## Royalties — StickerSplitter

`royaltyInfo` signals **5%** on every secondary sale — stickers *and* sealed sheets — paid to a **StickerSplitter** vault carrying the Project split: 60/40 creator-side/platform, i.e. **3% / 2% of the sale**, identical to the art market. Solo sheets share one vault (deployed in the constructor, creator side to the admin); each collab sheet gets its own vault at `createSheet`, splitting the creator side on the sheet's locked terms. StickerSplitter inherits the PaymentSplitter design wholesale: empty `receive()`, cumulative accounting with monotonic entitlements (no stranded wei, no underflow), permissionless withdrawals that can only pay the sheet's collaborator (immutable), the shop's current admin or the factory's current platform wallet, independent ETH and per-token ERC-20 legs, and the platform side reading the factory live, so a platform-wallet rotation also moves every share still unwithdrawn. Each vault also has `redeemBlurPool()`, which turns Blur Pool royalties into ETH for the ETH split.

## Governance and lifecycle

Sheets are admin-published: `createSheet` (an inactive shell with its sealed-wrapper SVG and locked collab terms) → `addSticker` (a full SVG) or `addGeneratedSticker` (renderer parameters, checked by the renderer before they're stored) × N (a sticker's home sheet is permanent) → optional `setSheetPull` → `setSheetActive(true)`. A live sheet's pool never grows — restocking means deactivating, appending, reactivating; a PACK draw reads the pool at reveal, so a restock or a sticker toggle also reaches peels committed but not yet revealed — and a sealed copy always settles on exactly the terms it was sold under: while any sealed copies are outstanding, a FIXED sheet's pool cannot grow (its supply was reserved at purchase, so a new sticker would have nothing reserved for it) and no sheet's FIXED/PACK mode can be switched. PACK sheets restock freely — they draw at reveal and check supply there. `setSheetPrice` retunes a sheet at any time (the ~$22 target outlives ETH drift) and applies only to the next purchase. The admin is transferable by the same two-step propose/accept the factory uses (proposing the zero address withdraws a proposal); collab vaults re-route their admin side instantly, since they read `admin()` live. There is no `receive()` — `withdraw()` exists only to sweep force-sent dust.

## Reads worth knowing

| Function | Returns |
| --- | --- |
| `sheets(sheetId)` | The whole sheet record: pool, price, print run, sold/peeled, mode, collab terms, vault |
| `sealedTokenId(sheetId)` | The sealed token ID — what `balanceOf` takes |
| `sealedOutstanding(sheetId)` | Sealed copies not yet delivered: still sealed, or a pack peel waiting on its reveal |
| `packPeels(peelId)` | A committed pack peel awaiting its reveal: keeper, seed block, count, sheet, place in the sheet's reveal line |
| `poolAvailable(sheetId)` | Stickers currently drawable (active, supply left) |
| `canPurchaseSheet(sheetId)` | One-call pre-flight: can this sheet be bought right now |
| `getStickerSVG(id)` / `getSheetCoverSVG(id)` | The on-chain art (a built-in sticker's is drawn by the renderer at read time) |
| `generated(id)` / `renderer` | Whether a sticker is drawn from parameters, and the renderer that draws it |
| `revealFeeWei` | The PACK reveal fee per sealed copy, read live |

## Status

PDStickers and StickerSplitter are part of the security-reviewed contract suite and covered by the repository's Foundry tests. An earlier revision was deployed to Sepolia and exercised end-to-end (purchase splits verified to the wei, peel, royalty vault withdrawals, collab sheets, and a real marketplace ERC-1155 sale against the sealed sheet); the launch build, with force-sent payouts, the 100-sheet purchase cap and in-order pack reveals, was rehearsed on a simulated copy of mainnet. Both are live on Ethereum mainnet with their source code verified on Etherscan; their addresses are on [Audit Buddy](/docs/contracts/audit-buddy).

## Further reading

- [Stickers — Overview](/docs/stickers/overview) — the same machinery, collector's-eye view
- [PaymentSplitter](/docs/contracts/payment-splitter) — the design StickerSplitter inherits
- [PDFactory](/docs/contracts/pd-factory) — the platform wallet both economies read
