Contracts — PDStickers
PDStickers is the sticker economy's 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).
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
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:
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
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 drawspullSizedistinct stickers from the sheet's pool. The draw is commit-then-reveal, the same recipe as PDProject and the keychain crank:peelburns the sealed copies and commits the draw to its own block, whose hash doesn't exist while the peel runs;revealPack(peelId)(orrevealPacks) 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 leastpullSizeopen-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, is rehearsed on Sepolia again before mainnet. Mainnet deployment rides the platform's launch.
Further reading
- Stickers — Overview — the same machinery, collector's-eye view
- PaymentSplitter — the design StickerSplitter inherits
- PDFactory — the platform wallet both economies read