Docs
/
Protocol

Stamps

A recipient can charge strangers to reach them. Three rules make the economics hold up, and all three are enforced on chain rather than by the interface.

Gas already gives mail a floor price, which makes mass spam expensive. Stamps go further: a recipient sets a price that unknown senders must escrow to reach them. Answer and it goes back. Ignore it and it is yours.

The three rules

These are not interface conventions. Each is enforced by the contract, which matters because a rule that lives in the frontend is a rule an attacker skips by calling the contract directly.

RULE 01

Answering is always free

Replying to a message somebody paid to send you never costs a stamp. Without this, a recipient could bait you and raise their price before you replied.

RULE 02

Time never refunds a spammer

After thirty days the escrow belongs to the recipient. If waiting returned the money, a patient spammer would simply wait.

RULE 03

You cannot take it and shut the door

Blocking a sender returns their escrow. Keeping the money and the block at once is refused on chain.

Escrow lifecycle

Held escrowed at send time Refunded back to the sender refundStamp() Claimed to the recipient claimStamp() / settleExpired() Reclaimed back to the sender reclaimStamp() recipient replies, or blocks the sender recipient claims, or 30 days elapse and anyone settles only if the recipient blocked the sender or left the registry CONSTRAINTS refundStamp is rejected once the 30 day window has passed, so time alone never returns money to a spammer. claimStamp is rejected while the recipient has the sender blocked, so the money and the block cannot both be kept.
Figure 6. Each message carries its own escrow row. Nothing is aggregated per sender-recipient pair.

Per message, not per pair

Every message has its own Stamp record keyed by mailId. An early revision aggregated escrow per sender-recipient pair, and the consequence was severe: replying once to a spammer who had sent fifty stamped messages refunded the entire batch. Per-message escrow makes each payment settle on its own terms.

struct Stamp {
  address     sender;      // who paid it
  uint96      amount;      // wei escrowed for this one message
  address     recipient;   // who it was paid to reach
  uint64      stampedAt;
  StampStatus status;      // None, Held, Refunded, Claimed, Reclaimed
}

Who mails free

SenderRequired stamp
Recipient has no price setZero. An open inbox is the default.
On the recipient's allowlist, within its windowZero. Allowlist entries are time-bounded.
Answering a live stamp the recipient paid to reach themZero, by rule one.
Anyone elseThe recipient's current stampPrice.
Blocked by the recipientCannot send at any price. Reverts with Blocked.
Replying to free mail is free

An early fix for rule one made replies free only when answering a stamped message, and reverted with NoSuchStamp when replying to ordinary free mail. That broke the default path for every user. The second audit caught it; the reply path now handles both cases.

Settlement paths

refundStamp(mailId)
Recipient returns the escrow. Rejected once the message is past expiry.
claimStamp(mailId)
Recipient takes the escrow. Rejected while they have the sender blocked.
settleExpired(mailId)
Permissionless. After thirty days anyone can push the escrow to the recipient, so funds are never stranded by an inactive party.
reclaimStamp(mailId)
Sender recovers the escrow, but only when the recipient blocked them or left the registry. Not a general timeout.

Each has a batch form that skips rows it cannot settle rather than reverting the whole call, and emits BatchSettled(caller, count, total) so the caller can see what actually moved.

Payouts cannot be used as a weapon

A naive implementation pushes value with a plain transfer, which lets a contract recipient revert on receipt and permanently block the counterparty's refunds and replies. bMail sends with a fixed PAYOUT_GAS budget of 60,000, guards it against the available gas, and credits the amount internally if the transfer fails.

// the failure path never blocks the protocol
credits[to] += amount;
emit PaymentCredited(to, amount);
// the owner pulls later via withdrawCredits()

Bounds

STAMP_EXPIRY
30 days
MAX_STAMP
100 ether, an upper sanity bound on any single escrow
minStamp
Immutable, fixed at deployment
MAX_ENVELOPE_BYTES
32 KB
FREE_ENVELOPE_BYTES
4 KB
MAX_BATCH
64 entries per batch call
Open economic decisions

Whether the protocol takes a fee on claims, whether a slice is burned, and whether the default stamp price should be non-zero are deliberately left unset. They are business calls, not defects. See the roadmap.

bMail is an independent, community-built project. It is not affiliated with, endorsed by, or operated by Binance or BNB Chain.