STAMP

DocsContract reference

Four contracts, one post.

The StampPaymaster pays the gas (ERC-4337, EntryPoint v0.9). The PostOffice sells the stamps. The DeadLetterOffice turns the markup into $STAMP sent to 0x…dEaD. The CreatorFeeSink sends $STAMP's creator fees to the paymaster's treasury. All four are Solidity 0.8.28, deployed with CREATE2, and none is upgradeable.

StampPaymaster on Robinhood Chain0x0AE674eA03A1F174EB09E2ac5fc913bC6b4130eFExplorer (opens a new tab)

PostOffice on Robinhood Chain0x9d72BA27e3F8f3ddb74dfc6328968289Be2F4F02Explorer (opens a new tab)

DeadLetterOffice on Robinhood Chain0xF4Dc84ee0B40AeC7AdA0EFCee8Ab464e352E8116Explorer (opens a new tab)

CreatorFeeSink on Robinhood Chain0x1eF52798D579e318E7eD10E794a23389C08D6791Explorer (opens a new tab)

Signatures below are generated from the ABIs the SDK ships, so they always match the deployed build. Struct layouts are in contracts/src/interfaces/IStampPaymaster.sol; the protocol is in docs/SPEC.md §3 (the paymaster and the mined stamp) and §12 (prepaid stamps).

StampPaymaster#

An ERC-4337 paymaster that implements IPaymaster directly, is staked, and keeps value inside the system: ETH leaves only through withdrawTreasury. One EntryPoint deposit holds two lanes: the stamp float (prepaid, credited by the PostOffice) and the free lane (topped up from the treasury at each roll). Stamps are never refunded.

Lifecycle, stamps and admin#

acceptOwnership()pending owner
Completes the ownership transfer.
addStake(uint32 unstakeDelaySec) payableowner
Stakes the paymaster in the EntryPoint (required by ERC-7562 to read its own storage in validation).
creditStamps() payableanyone (PostOffice)
Pays for prepaid stamps: msg.value goes into the EntryPoint deposit and the float, outside the free lane's budget. The PostOffice calls it with each booklet's gas share. Nothing here can be withdrawn by a stamp holder.
deposit() payableanyone
Adds msg.value to this paymaster's EntryPoint deposit.
drainDeposit()owner or guardian
Withdraws the whole EntryPoint deposit into the contract (the treasury) and pauses. Kill switch.
fund(bytes32 source) payableanyone
Treasury income from anyone, tagged with a source (the CreatorFeeSink uses keccak256("pons-creator-fees")). Emits Funded.
pause()owner or guardian
Blocks roll() for everyone. Validation never reads the flag, so live epochs keep working until they expire.
postOp(PostOpMode mode, bytes context, uint256 actualGasCost, uint256 actualUserOpFeePerGas)EntryPoint
EntryPoint hook. Accounting only, never reverts. Mined stamp: adds cost and work to the current epoch and emits OpSponsored. Prepaid stamp: takes the cost from the float, records the nullifier and emits StampCancelled (and DoubleSpend on a second use).
refloat()owner
Moves treasury ETH back into the deposit until it covers the float again (after a drain or trim).
renounceOwnership()owner
Leaves the contract without an owner.
roll() returns (uint32 newEpochId)EOA or owner
Closes the current epoch and opens the next. Any EOA once the epoch is over, or the owner at any time. Tops the free lane up to its budget only on a direct EOA call, at most once per epoch length; it only ever sees deposit − float.
setConfig(Config cfg)owner
Stores a new pending config; it applies at the next roll. Checked against the config rules (§3.13), stampPostOpGasLimit within [55,000, 70,000].
setGuardian(address guardian_)owner
Sets the guardian, which can pause and drain but never move value out.
topUpDeposit()owner, once
Initial funding of the free lane's deposit up to the configured budget. Callable once.
transferOwnership(address newOwner)owner
Starts a two-step ownership transfer (Ownable2Step).
trimDeposit(uint256 amount)owner, paused
Withdraws part of the deposit into the treasury, only while paused.
unlockStake()owner
Starts the unstake delay.
unpause()owner
Clears the pause.
validatePaymasterUserOp(PackedUserOperation userOp, bytes32 userOpHash, uint256 maxCost) view returns (bytes context, uint256 validationData)EntryPoint
EntryPoint hook. Reverts only on malformed data, an unknown epoch or a prepaid stamp in an epoch without the STAMPS flag; every policy, PoW, nullifier and co-signature failure is soft (SIG_VALIDATION_FAILED).
withdrawStake()owner
Withdraws the unlocked stake, always to this contract.
withdrawTreasury(address to, uint256 amount)owner
Sends treasury ETH. The only path by which value leaves the system.
writeOffFloat(uint256 amount)owner
Reduces the float by an amount (expired, never-redeemed stamps). The ETH stays in the deposit.

StampPaymaster views#

DELEGATE() view returns (address)
The only delegate a sender may use (Simple7702Account v0.9).
MAX_DAILY_SPEND() view returns (uint256)
Immutable ceiling on the free lane's daily top-ups.
MAX_POSTOP_GAS_LIMIT() view returns (uint256)
Upper bound for pmPostOpGasLimit (the EntryPoint's unused-postOp-gas penalty threshold).
MAX_STAMP_POSTOP_GAS_LIMIT() view returns (uint256)
Upper bound for stampPostOpGasLimit: a normal redemption never pays the unused-postOp-gas penalty.
MAX_WORK() view returns (uint256)
Bound on the expected total hashes of any op (2^48).
MIN_POSTOP_GAS_LIMIT() view returns (uint256)
Lower bound for pmPostOpGasLimit.
MIN_STAMP_POSTOP_GAS_LIMIT() view returns (uint256)
Lower bound for stampPostOpGasLimit: a stamp postOp's worst case (a cold lane slot and a fresh nullifier) with a margin.
PM_DATA_VERSION() view returns (uint8)
The mined stamp's paymasterData version byte (1).
PM_DATA_VERSION_STAMP() view returns (uint8)
The prepaid stamp's paymasterData version byte (2).
POSTOP_GAS_OVERHEAD() view returns (uint256)
Gas added to each mined op's accounted cost for postOp and the EntryPoint's bookkeeping.
POW_SOLUTIONS() view returns (uint256)
K = 8 sub-solutions per proof.
STAMP_POSTOP_GAS_OVERHEAD() view returns (uint256)
Gas added to each stamp op's accounted cost (its postOp writes a fresh nullifier slot).
accumulator() view returns (Accumulator)
Spend counters of the current epoch (mined stamps).
cancelledAt(bytes32 nullifier) view returns (uint256)
When a nullifier was first cancelled (unix seconds), 0 if never. The relay reads it to refuse a spent stamp.
checkUserOp(PackedUserOperation op, bytes32 userOpHash, uint256 maxCost) view returns (uint256 failMask, uint48 validUntil, uint256 work)
Mirrors validation exactly, including the delegate check, and returns the failMask, validUntil and work (0 for a prepaid stamp).
config() view returns (Config)
The pending configuration the next roll snapshots.
controller() view returns (Controller)
Difficulty controller state and the budget in force.
cosignDigest(bytes32 userOpHash) view returns (bytes32)
The digest the relay co-signer signs for a mined stamp (raw, no EIP-191 prefix).
currentEpochId() view returns (uint32)
The wall-clock current epoch id.
dashboard() view returns (uint32 currentId, Epoch current, Accumulator acc, Controller ctl, Totals tot, uint256 depositWei, uint256 treasuryWei, uint256 stakeWei, uint32 unstakeDelaySec, bool staked, StampLane lane)
One-call snapshot for dashboards and the relay: current epoch, accumulator, controller, totals, deposit, treasury, stake and the stamp lane.
entryPoint() view returns (address)
The EntryPoint v0.9 address.
epochStats(uint32 epochId) view returns (EpochStats)
A closed epoch's ops, spend, work, utilization and close time.
epochStatsRange(uint32 fromId, uint32 toId) view returns (EpochStats[] out)
Stats for epochs fromId..toId inclusive, at most 256 entries.
epochs(uint32 epochId) view returns (Epoch)
An epoch's write-once parameters (start 0 = unknown epoch), stampPostOpGasLimit included. The only storage validation reads.
getDeposit() view returns (uint256)
This paymaster's EntryPoint deposit (the free lane's share plus the float).
guardian() view returns (address)
The guardian address.
owner() view returns (address)
The owner.
pendingOwner() view returns (address)
The pending owner of a two-step transfer.
powChallenge(uint32 epochId, bytes32 userOpHash) view returns (bytes32)
The PoW challenge for a userOpHash in an epoch.
requiredWork(uint32 epochId, uint256 gasSum) view returns (uint256 work, uint256 targetPerSolution)
Expected total hashes and the per-solution target for an op of gasSum in an epoch. Bit-exact with the SDK.
stampDigest(bytes32 userOpHash, bytes32 nullifier) view returns (bytes32)
The digest the relay co-signs for a prepaid stamp: userOpHash and the nullifier (raw, no EIP-191 prefix).
stampLane() view returns (StampLane)
The prepaid-stamp lane: the float (ETH prepaid and not yet spent), the lifetime spend and the number of stamp ops.
totals() view returns (Totals)
Lifetime ops and spend over closed epochs (mined stamps).

StampPaymaster events#

The EntryPoint also emits UserOperationEvent(userOpHash, sender, paymaster, nonce, success, actualGasCost, actualGasUsed). Join it with OpSponsored (a mined stamp) or StampCancelled (a prepaid stamp) by transaction hash and sender.

ConfigUpdated(Config config)
A new pending config was stored.
DepositDrained(uint256 amount, address indexed by)
The deposit was drained into the treasury.
DepositToppedUp(uint256 before, uint256 after_)
A roll topped the free lane's deposit up.
DepositTrimmed(uint256 amount)
Part of the deposit was moved back into the treasury.
DoubleSpend(bytes32 indexed nullifier, address indexed sender, uint256 firstCancelledAt)
A nullifier was cancelled a second time: the relay co-signed one stamp twice. Public by design, with the first cancellation's time.
EpochClosed(uint32 indexed epochId, uint32 ops, uint96 spent, uint96 work, uint16 utilBps)
The previous epoch's final ops, spend, work and utilization.
EpochRolled(uint32 indexed epochId, uint40 start, uint64 workPerGwei, uint64 minWork, uint64 maxFeePerGas, uint64 refFeePerGas, uint96 seed, uint8 flags, address cosigner, address indexed caller)
A new epoch exists: its difficulty, fees, seed, flags and co-signer.
FloatWrittenOff(uint256 amount, uint256 float)
The owner wrote part of the float off (expired stamps); the ETH stays in the deposit.
Funded(address indexed from, uint256 amount, bytes32 indexed source)
Treasury income through fund().
GuardianSet(address indexed guardian)
A new guardian.
OpSponsored(uint32 indexed epochId, uint32 opEpochId, address indexed sender, uint256 cost, uint256 work, bool success)
One sponsored mined op, accounted in postOp. The dashboard joins it with UserOperationEvent by transaction hash.
OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner)
A two-step ownership transfer started.
OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
Ownership changed.
PausedSet(bool paused, address indexed by)
roll() was paused or unpaused.
Refloated(uint256 amount)
The owner moved treasury ETH back into the deposit to cover the float.
StampCancelled(uint32 indexed epochId, bytes32 indexed nullifier, address indexed sender, uint256 cost, bool success)
One prepaid-stamp op, accounted in postOp: its stamp is cancelled (the nullifier as co-signed by the relay, the cost taken from the float).
StampsCredited(address indexed from, uint256 amount, uint256 float)
Prepaid stamps were paid for: the amount went into the deposit and the float.
TreasuryLow(uint256 needed, uint256 available)
A top-up could not reach the budget: the treasury held less than needed.
TreasuryWithdrawn(address indexed to, uint256 amount)
Treasury ETH left the system.

StampPaymaster errors#

Reverts happen only on malformed input, an unknown epoch, a prepaid stamp in an epoch without the STAMPS flag, or an admin misuse. Policy, proof, nullifier and co-signature problems are soft failures, reported as bits by checkUserOp() (see How it works).

ERC165Error(address entryPoint, bytes4 interfaceId)
The constructor's EntryPoint does not support the v0.9 interface.
EntropyUnavailable()
No L2 entropy for the seed on a real chain (the roll reverts rather than use a predictable seed).
EpochNotOver(uint256 endsAt)
A non-owner roll before the epoch's end.
FloatTooSmall(uint256 float, uint256 amount)
writeOffFloat() asked for more than the float.
InvalidConfig(uint8 code)
A config rule failed; the code is the rule number (§3.13).
InvalidConstructorArg(uint8 code)
1: the delegate has no code. 2: maxDailySpend is 0 or above the limit.
InvalidPaymasterData(uint256 length)
The signed paymasterData is not exactly 5 bytes (version ‖ epochId).
InvalidPaymasterSignatureLength(uint256 dataLength, uint256 pmSignatureLength)
The EntryPoint's paymasterSignature length check failed.
InvalidPowSignatureLength(uint256 got, uint256 expected)
A mined stamp's unsigned suffix is not 64 (permissionless) or 129 (co-signed) bytes.
InvalidRange(uint32 fromId, uint32 toId)
epochStatsRange with toId < fromId or more than 256 entries.
InvalidStampSignatureLength(uint256 got, uint256 expected)
A prepaid stamp's unsigned suffix is not exactly nullifier(32) ‖ cosig(65).
NotFromEntryPoint(address msgSender, address entity, address entryPoint)
A paymaster hook called by something other than the EntryPoint.
NotOwnerOrGuardian()
pause() or drainDeposit() from anyone else.
NotPaused()
trimDeposit() while not paused.
OwnableInvalidOwner(address owner)
Ownable: invalid new owner.
OwnableUnauthorizedAccount(address account)
Ownable: caller is not the owner.
RollCallerNotEOA()
roll() from a contract that is not the owner (this blocks rolls inside handleOps).
RollPaused()
roll() while paused.
StampLaneClosed(uint32 epochId)
A prepaid-stamp op referenced an epoch without the STAMPS flag.
TopUpAlreadyDone()
topUpDeposit() called twice.
TransferFailed()
An ETH transfer failed.
UnknownEpoch(uint32 epochId)
The op references an epoch that does not exist.
UnsupportedVersion(uint8 version)
The paymasterData version is neither 1 (mined) nor 2 (prepaid stamp).

PostOffice#

Where booklets are bought, from any wallet: buy(commitment, count) takes count × price, returns any excess, credits the gas share to the paymaster (creditStamps) and sends the markup to the DeadLetterOffice. It stores each commitment once and emits StampsBought, which the relay issues against. Nothing in it can be withdrawn or refunded.

Buying and pricing#

acceptOwnership()pending owner
Completes the ownership transfer.
buy(bytes32 commitment, uint16 count) payable returns (uint64 orderId)anyone
Buys a booklet of count (1 to 100) stamps committed to by commitment = keccak256(keccak256(info) ‖ blinded…). Pays count × price and returns any excess in the same call; the gas share goes to paymaster.creditStamps, the markup to the DeadLetterOffice. A commitment is paid for once. Emits StampsBought.
renounceOwnership()owner
Leaves the contract without an owner.
setBounds(uint128 minPrice_, uint128 maxPrice_)owner
Sets [minPrice, maxPrice]; the current price must stay inside them.
setMarkup(uint16 markupBps_)owner
Sets the markup over the gas quote, at most 10,000 bps (the price is then at most twice the quote).
setPrice(uint128 newPrice)pricer or owner
Sets the price per stamp within [minPrice, maxPrice]. The pricer (the relay's keeper) moves it by at most ±50% and at most once per 10 minutes; the owner may set any price in bounds.
setPricer(address pricer_)owner
Sets the pricer (the relay's keeper EOA).
transferOwnership(address newOwner)owner
Starts a two-step ownership transfer (Ownable2Step).

PostOffice views#

MAX_BOOKLET() view returns (uint16)
Largest booklet in one purchase (100).
MAX_MARKUP_BPS() view returns (uint16)
Largest markup (10,000 bps).
MAX_PRICE_STEP_BPS() view returns (uint16)
Largest pricer move per update (5,000 bps, ±50%).
MIN_PRICE_INTERVAL() view returns (uint32)
Shortest time between two pricer updates (600 s).
deadLetterOffice() view returns (address)
Where the markup of every sale goes.
grossSales() view returns (uint128)
Total wei paid for stamps.
markupBps() view returns (uint16)
The markup: price = quote × (10,000 + markupBps) / 10,000.
maxPrice() view returns (uint128)
Upper price bound (owner).
minPrice() view returns (uint128)
Lower price bound (owner).
orderOf(bytes32 commitment) view returns (uint64 orderId)
The order that paid for a commitment (0 = none).
orders() view returns (uint64)
Number of booklets sold (the last order id).
owner() view returns (address)
The owner.
paymaster() view returns (address)
The StampPaymaster the gas share is credited to.
pendingOwner() view returns (address)
The pending owner of a two-step transfer.
price() view returns (uint128)
Wei per stamp, gas share plus markup.
priceUpdatedAt() view returns (uint40)
When the price last changed (unix seconds).
pricer() view returns (address)
The account allowed to follow the gas quote within the bounds.
quote(uint256 count) view returns (uint256 cost, uint256 gasShare, uint256 markup)
What a booklet of count stamps costs now, and how the payment splits into gas share and markup.
stampsSold() view returns (uint64)
Number of stamps sold.

PostOffice events#

BoundsSet(uint256 minPrice, uint256 maxPrice)
The price bounds changed.
MarkupSet(uint16 markupBps)
The markup changed.
OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner)
A two-step ownership transfer started.
OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
Ownership changed.
PriceSet(uint256 price, address indexed by)
The price changed, and who changed it.
PricerSet(address indexed pricer)
A new pricer.
StampsBought(uint64 indexed orderId, address indexed buyer, bytes32 indexed commitment, uint16 count, uint256 price, uint256 gasShare, uint256 markup)
A booklet was paid for: order id, buyer, commitment, count, price, gas share and markup. The relay issues against it; the buyer is public.

PostOffice errors#

BadCount(uint256 count)
buy() with 0 or more than 100 stamps.
CommitmentUsed(bytes32 commitment, uint64 orderId)
buy() with a commitment that was already paid for, and the order that paid it.
InvalidArgument()
Constructor: the paymaster has no code, or the DeadLetterOffice is the zero address.
InvalidBounds(uint256 minPrice, uint256 maxPrice)
minPrice is 0 or above maxPrice.
InvalidMarkup(uint16 markupBps)
A markup above 10,000 bps.
NotPricer()
setPrice() from anyone but the pricer or the owner.
OwnableInvalidOwner(address owner)
Ownable: invalid new owner.
OwnableUnauthorizedAccount(address account)
Ownable: caller is not the owner.
PriceOutOfBounds(uint256 price, uint256 minPrice, uint256 maxPrice)
A price outside [minPrice, maxPrice].
PriceStepTooLarge(uint256 from, uint256 to)
A pricer update beyond ±50% of the current price.
PriceTooSoon(uint256 nextAt)
A pricer update less than 10 minutes after the previous one.
TransferFailed()
An ETH transfer failed.
Underpaid(uint256 cost, uint256 paid)
buy() with less than count × price.
ZeroCommitment()
buy() with a zero commitment.

DeadLetterOffice#

Where the markup ends up. It receives the markup of every sale and, when the keeper calls burn, buys $STAMP on its own market (the Pons bonding curve before graduation, the Uniswap v4 pool after) and sends it to 0x000000000000000000000000000000000000dEaD. There is no withdrawal, no sweep and no other recipient; the tokens never sit in the contract.

Burning#

acceptOwnership()pending owner
Completes the ownership transfer.
burn(uint256 ethIn, uint256 minTokensOut) returns (uint256 out)keeper or owner
Buys $STAMP with ethIn of this contract's ETH and sends it to 0x…dEaD: on the Pons bonding curve before graduation, on the token's Uniswap v4 pool after. Reverts unless at least minTokensOut reach the dead address.
renounceOwnership()owner
Leaves the contract without an owner.
setKeeper(address keeper_)owner
Sets the keeper (the relay's keeper EOA).
setToken(address token_)owner, once
Sets $STAMP once; it must be a Pons v2 launch.
transferOwnership(address newOwner)owner
Starts a two-step ownership transfer (Ownable2Step).
unlockCallback(bytes data) returns (bytes)PoolManager
Uniswap v4 callback of a burn: one exact-input swap of ETH for the token, settled from this contract, taken straight to the dead address.

DeadLetterOffice views#

DEAD() view returns (address)
0x000000000000000000000000000000000000dEaD: Pons tokens have no burn(), so the dead address is the burn.
keeper() view returns (address)
The account that chooses when and how much to burn.
owner() view returns (address)
The owner.
pendingOwner() view returns (address)
The pending owner of a two-step transfer.
pons() view returns (address)
The Pons v2 launch factory.
poolKey() view returns (PoolKey)
The token's v4 pool once graduated: native ETH against the token, on the Pons meme hook.
poolManager() view returns (address)
The Uniswap v4 PoolManager.
token() view returns (address)
$STAMP, once set.
totalBurned() view returns (uint256)
Lifetime tokens sent to the dead address.
totalEthSpent() view returns (uint256)
Lifetime ETH spent on buybacks.

DeadLetterOffice events#

Burned(address indexed by, uint256 ethIn, uint256 tokensToDead, bool onCurve)
A buyback: who, ETH in, tokens to the dead address, and whether it went through the curve or the pool.
KeeperSet(address indexed keeper)
A new keeper.
OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner)
A two-step ownership transfer started.
OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
Ownership changed.
TokenSet(address indexed token)
$STAMP was set (once).

DeadLetterOffice errors#

InsufficientBalance(uint256 balance, uint256 wanted)
burn() with more ETH than the contract holds.
MarketUnavailable(GraduationPhase phase)
The token is between its bonding curve and its pool: wait for the pool.
NoToken()
burn() before the token is set.
NotAPonsLaunch(address token)
setToken() with an address that is not a Pons v2 launch.
NotKeeper()
burn() from anyone but the keeper or the owner.
NotPoolManager()
unlockCallback() from anyone but the PoolManager.
OwnableInvalidOwner(address owner)
Ownable: invalid new owner.
OwnableUnauthorizedAccount(address account)
Ownable: caller is not the owner.
ReentrancyGuardReentrantCall()
A re-entrant call was refused.
Slippage(uint256 out, uint256 minOut)
Fewer tokens than minTokensOut reached the dead address.
TokenAlreadySet()
setToken() a second time: the token is set once.
ZeroAmount()
burn() with ethIn = 0.

CreatorFeeSink#

The creator fee recipient of $STAMP on Pons v2. Anyone may call collect(): it brings the fees in and forwards every wei to the paymaster's treasury through fund(keccak256("pons-creator-fees")), where the rolls top up the free lane. It has no owner and no withdrawal.

Collecting#

collect() returns (uint256 collected)anyone
Sweeps $STAMP's creator fees (from the curve before graduation, from the meme hook after) into the Pons escrow, claims them and forwards every wei to the paymaster's treasury with fund(SOURCE). Anyone may call it: the money only ever goes to the paymaster.
setToken(address token_)admin, once
Sets $STAMP once (admin); it must be a Pons v2 launch.

CreatorFeeSink views#

SOURCE() view returns (bytes32)
The fund() source tag: keccak256("pons-creator-fees").
admin() view returns (address)
May set the token, once (a constructor argument).
paymaster() view returns (address)
The StampPaymaster whose treasury receives the fees.
pending() view returns (uint256)
Fees already credited to this contract in the Pons escrow, plus ETH waiting here (a sweep may add more).
pons() view returns (address)
The Pons v2 launch factory.
token() view returns (address)
$STAMP, once set.
totalCollected() view returns (uint256)
Lifetime wei forwarded to the paymaster.

CreatorFeeSink events#

Collected(address indexed by, uint256 amount)
A collection: who called it and how much went to the paymaster.
TokenSet(address indexed token)
$STAMP was set (once).

CreatorFeeSink errors#

NotAPonsLaunch(address token)
setToken() with an address that is not a Pons v2 launch.
NotAdmin()
setToken() from anyone but the admin.
ReentrancyGuardReentrantCall()
A re-entrant call was refused.
TokenAlreadySet()
setToken() a second time: the token is set once.