Skip to content

spec: symmetric deposit bounce-back mechanism#428

Open
dankrad wants to merge 25 commits intomainfrom
dankrad/deposit-bounceback-spec-only
Open

spec: symmetric deposit bounce-back mechanism#428
dankrad wants to merge 25 commits intomainfrom
dankrad/deposit-bounceback-spec-only

Conversation

@dankrad
Copy link
Copy Markdown
Collaborator

@dankrad dankrad commented Apr 23, 2026

Summary

Splits out the spec portion of #360. This PR only touches specs/spec.md and adds a full prose description of the symmetric deposit bounce-back mechanism, plus the corresponding updates to the IZonePortal interface stub.

Companion implementation PR: dankrad/deposit-bounceback-impl (Solidity reference contracts, Rust bindings, tests, and genesis regeneration).

Motivation

Today a withdrawal that reverts on Tempo (blocked callback, TIP-403 change, paused token, etc.) bounces back into the zone as a fresh deposit credited to fallbackRecipient. The deposit side had no such mechanism: if the zone-side mint reverts, the deposit queue gets stuck and the escrowed funds are effectively stranded in the portal.

TIP-403 makes this an operational concern rather than a corner case. The policy for a token can change between the time the user initiates the deposit on Tempo and the time the zone processes it, and the new policy may forbid minting to deposit.to. Without a recovery path, legitimate deposits can be bricked by a policy update between signing and processing.

This spec describes the deposit-side counterpart: every deposit carries a bouncebackRecipient, and if zone-side processing fails the funds are refunded to that address on Tempo via a zero-fee, zero-callback withdrawal.

What changes in the spec

  1. A new section Deposit Failures and Bounce-Back under Deposits, documenting:

    • Why the mechanism is needed (mostly driven by TIP-403 policy drift).
    • The new bouncebackRecipient argument to deposit / depositEncrypted and the TIP-403 validation the portal performs at deposit time to guarantee the refund transfer will succeed.
    • Triggering conditions: any mint revert during ZoneInbox.advanceTempo (including the fallback-to-sender mint after a failed decryption).
    • Zone-side handling: ZoneInbox catches the revert and calls ZoneOutbox.enqueueDepositBounceBack(token, amount, bouncebackRecipient), which enqueues a zero-fee, zero-callback, zero-fallbackRecipient withdrawal.
    • Tempo-side handling: ZonePortal.processWithdrawal refunds the escrowed tokens and emits DepositBounceBack.
    • The no-recursive-bounce rule: bounce-back deposits have bouncebackRecipient = address(0) and bounce-back withdrawals have fallbackRecipient = address(0), so neither can spawn another bounce.
    • Opting out: passing bouncebackRecipient = address(0) restores the pre-existing "liveness failure on revert" behavior; this is also the mode used by the portal itself for internally generated bounce-back deposits.
    • Event summary table covering DepositFailed, EncryptedDepositFailed, DepositBounceBack, and the renamed WithdrawalBounceBack.
    • A mermaid sequence diagram of the bounce-back path.
  2. Updates to Regular Deposits and Encrypted Deposits:

    • New bouncebackRecipient argument documented in the prose and in the ordered deposit steps.
    • The "Deposits always succeed on the zone" paragraph is rewritten to link to the new section.
    • The "if verification fails" paragraph for encrypted deposits now also covers the case where the fallback-to-sender mint reverts.
  3. Updates to the IZonePortal interface stub:

    • deposit / depositEncrypted take bouncebackRecipient.
    • DepositMade gains bouncebackRecipient.
    • New DepositBounceBack event.
    • Rename BounceBackWithdrawalBounceBack for symmetry.
    • Catches the stub up with spec: add deposit counter for transparent deposit confirmation #355: adds depositNumber / lastProcessedDepositNumber fields to DepositMade / EncryptedDepositMade / BatchSubmitted, and adds depositCount() / lastProcessedDepositNumber() views.
  4. A new TOC entry for the new section.

Non-goals

  • No reference contract changes. Those live in the companion implementation PR.
  • No Rust / genesis changes. Those live in the companion implementation PR.

Review hints

  • The new section is self-contained; the easiest way to review is to read specs/spec.md at ### Deposit Failures and Bounce-Back rather than diff-by-diff.
  • Symmetry with the existing ### Withdrawal Failures and Bounce-Back section is intentional — same structure, opposite direction.

Test plan

  • Prose reviewed for internal consistency with the withdrawal bounce-back section
  • Interface stub matches the Solidity signatures in the companion implementation PR
  • TOC entry links correctly to the new section anchor

Made with Cursor

Adds a new "Deposit Failures and Bounce-Back" section to the zones
specification documenting the symmetric deposit-side counterpart of the
existing withdrawal bounce-back mechanism.

Every deposit now carries a `bouncebackRecipient`. If the zone-side mint
reverts (e.g. because a TIP-403 policy active on the zone at processing
time forbids crediting the target), `ZoneInbox` catches the revert and
enqueues a zero-fee, zero-callback withdrawal through
`ZoneOutbox.enqueueDepositBounceBack`. The portal's `processWithdrawal`
then refunds the escrowed tokens to `bouncebackRecipient` on Tempo and
emits `DepositBounceBack`. The portal validates `bouncebackRecipient`
against the token's TIP-403 policy at deposit time so that the refund
transfer is guaranteed to be accepted.

The existing `BounceBack` event is renamed to `WithdrawalBounceBack` for
symmetry; bounce-back deposits (from failed withdrawals) and bounce-back
withdrawals (from failed deposits) are one-shot and cannot recurse.

Also updates the IZonePortal interface stub in the spec to:

- add bouncebackRecipient to deposit / depositEncrypted signatures and
  to DepositMade,
- rename BounceBack to WithdrawalBounceBack and add the new
  DepositBounceBack event,
- include depositCount() / lastProcessedDepositNumber() views and the
  depositNumber / lastProcessedDepositNumber event fields that were
  added in #355 but not yet reflected in the stub.

No code changes in this PR; the reference Solidity implementation and
Rust-side changes will land in a follow-up implementation PR.

Made-with: Cursor
dankrad added 2 commits April 24, 2026 00:47
Makes the Triggering conditions list in the deposit bounce-back section
explicit about the encrypted-deposit failure path: an invalid encryption
(Chaum-Pedersen proof rejection, AES-GCM tag mismatch, or plaintext
mismatch) causes the zone to fall back to minting to the depositor, and
if that fallback mint then reverts (e.g. the depositor is blocked by a
TIP-403 policy on the zone) the deposit bounces back to
bouncebackRecipient on Tempo.

Made-with: Cursor
Correct the previous "portal has a TIP-403 bypass" claim — no such
bypass exists in the current TIP-20 precompile. tempoxyz/tempo#3711
(TIP-1049: System-Contract Transfer Policy Exemption) adds a
`systemForceTransfer` entry point and a `ZoneFactory`-gated authority
predicate that every per-zone `ZonePortal` will satisfy. Cross-link
TIP-1049 from the bounce-back section and reword the Tempo-side refund
paragraph to describe the pre-/post-TIP-1049 behavior accurately.

Made-with: Cursor
@dankrad
Copy link
Copy Markdown
Collaborator Author

dankrad commented Apr 24, 2026

Update: the "portal has a TIP-403 bypass" claim from the original draft was inaccurate — TIP-20 currently has no such mechanism. The bypass is being introduced by TIP-1049: System-Contract Transfer Policy Exemption (tempoxyz/tempo#3711), which:

  • Adds ITIP20.systemForceTransfer(from, to, amount) to the TIP-20 precompile, skipping the TIP-403 check while preserving every other TIP-20 check (pause, zero-recipient, balance, spending-limit, events).
  • Gates access via a Transfer Policy Exemption List with a ZoneFactory authority predicate, so every per-zone ZonePortal is covered automatically (no per-zone hardfork needed).
  • Specifies the zones integration point (ZonePortal.processWithdrawal on the gasLimit == 0 / deposit-bounce-back path).

This PR now cites TIP-1049 directly in the Deposit Failures and Bounce-Back section and rewords the Tempo-side refund paragraph to describe the pre- and post-TIP-1049 behavior accurately:

  • Pre-TIP-1049: safety is preserved, but a policy edit between deposit and refund can still cause the refund transfer to revert; the entry re-enters the pending list.
  • Post-TIP-1049: ZonePortal uses systemForceTransfer on that path and the refund is guaranteed-live against policy drift.

Pushed as docs(spec): cite TIP-1049 for deposit bounce-back refund liveness (8f2969c).

dankrad added 2 commits April 24, 2026 17:15
The TIP was renumbered on tempoxyz/tempo#3723 to avoid a collision with
tempoxyz/tempo#3645 (jxom, 'admin access keys'), which already claimed
TIP-1049. Update the three cross-references in the deposit bounce-back
section.

Made-with: Cursor
…lback

Change encrypted-deposit failure semantics: on invalid encryption the
zone now bounces back directly to bouncebackRecipient on Tempo with
no zone-side mint attempted. To make that unconditional,
depositEncrypted requires bouncebackRecipient != address(0) at deposit
time and reverts otherwise. Regular deposit() retains its opt-out
semantics (pass address(0) to disable bounce-back).

Updates:
- Encrypted Deposits: non-zero bouncebackRecipient requirement.
- Encrypted-deposit field-visibility table: update sender rationale,
  add bouncebackRecipient row.
- Onchain Decryption Verification: remove mint-to-sender fallback.
- Sequence diagram: branch on verification success vs. failure.
- Deposit Failures and Bounce-Back: split Validation by deposit type;
  restructure Triggering conditions; update Zone-side handling; clarify
  Opting out is deposit()-only.
- Events summary: drop stale fallback-mint wording.
- IZonePortal stub: NatSpec on deposit and depositEncrypted.
Comment thread specs/spec.md Outdated
dankrad added 2 commits April 25, 2026 16:56
Make deposit-time bouncebackRecipient validation uniform: deposit() and
depositEncrypted() both require a non-zero refund target and revert with
MissingBouncebackRecipient otherwise. The previous spec allowed
deposit() to opt out of bounce-back via a zero recipient, which would
stall the deposit queue on a failed mint instead of recovering — that
opt-out is removed.

Updates:
- Regular Deposits: deposit() validates bouncebackRecipient != 0 and
  authorizes it against the token's TIP-403 policy.
- Encrypted Deposits prose: drop the 'Unlike deposit()' framing, both
  entry points share the same requirement.
- Deposit Failures and Bounce-Back / Validation at deposit time:
  collapsed the per-entry-point list into a single statement; called
  out the no-user-facing-opt-out invariant.
- Triggering conditions: removed the 'queue stalls on opt-out' branch
  for regular deposits, since the entry point now rejects zero.
- Sequence diagram: added 'require bouncebackRecipient != address(0)'
  before the TIP-403 check.
- No recursive bounces: clarified that the address(0) sentinel for the
  portal-internal _enqueueWithdrawalBounceBack path bypasses the user
  entry point, so it is not affected by the deposit-time check.
- Renamed the 'Opting out' subsection to 'No user-facing opt-out' and
  rewrote it to explain that the address(0) sentinel is reserved for
  internal use only.
- IZonePortal NatSpec for deposit() / depositEncrypted(): both now
  document MissingBouncebackRecipient on zero recipient.
Add an opt-in `rejected` flag on QueuedDeposit that lets the sequencer
mark any user-initiated deposit (regular or encrypted) as rejected when
calling advanceTempo. A rejected deposit is treated as a deposit-time
failure: the zone skips the zone-side mint (and, for encrypted deposits,
the onchain decryption verification) and enqueues a bounce-back to
bouncebackRecipient.

The flag is sequencer-supplied off-chain data and is NOT committed to
the deposit queue hash chain. A `rejected = true` flag on an internal
withdrawal-bounce-back deposit (bouncebackRecipient == address(0)) is
silently ignored to preserve the terminal-bounce invariant.

A new DepositRejected event distinguishes operator-initiated rejection
from TIP-403 / mint failures and from invalid-encryption failures.

Updates the Deposit Failures and Bounce-Back section, the regular and
encrypted deposit sequence diagrams, the Onchain Decryption Verification
section, the events summary table, and the IZoneInbox interface stub.
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
dankrad added 2 commits April 25, 2026 18:14
Co-authored-by: dankrad <mail@dankradfeist.de>
Document the withdrawal-side counterpart of the deposit-side bounce-back
guarantees. requestWithdrawal now requires fallbackRecipient != address(0)
and TIP-403-authorizes it on the zone at request time, mirroring the
checks on bouncebackRecipient at deposit time. The zone-side refund mint
on the terminal withdrawal-bounce-back path uses systemForceMint (TIP-1052)
to bypass TIP-403 mint-recipient authorization, so the refund can never
revert on policy grounds, eliminating the existing liveness risk where
a policy edit between request time and bounce-back processing could
stall ZoneInbox.advanceTempo.

The zone-side TIP-1052 admission of ZoneInbox is documented in the TIP.

Co-authored-by: dankrad <mail@dankradfeist.de>
Made-with: Cursor
@dankrad dankrad force-pushed the dankrad/deposit-bounceback-spec-only branch from fcbc881 to 38a7787 Compare April 25, 2026 15:32
Comment thread specs/spec.md Outdated
dankrad added 4 commits April 25, 2026 18:39
Co-authored-by: dankrad <mail@dankradfeist.de>
Add FIXED_BOUNCEBACK_GAS (250,000) and a per-deposit bouncebackFee
= FIXED_BOUNCEBACK_GAS * zoneGasRate, paid in the deposit token.
The fee is only consumed if the deposit actually bounces back, so
the steady-state cost of a successful deposit is unchanged. To make
the fee available without charging the user upfront, the portal
validates amount >= depositFee + bouncebackFee at deposit time
(reverts DepositTooSmall otherwise) and snapshots bouncebackFee on
the queued deposit; the bouncebackFee itself is part of the net
amount minted on the zone on success, so a successful deposit
returns it to the user implicitly. On bounce-back, ZonePortal.
processWithdrawal deducts the snapshotted bouncebackFee from the
queued amount, transfers amount - bouncebackFee to bouncebackRecipient,
and pays bouncebackFee to the sequencer to compensate for the
worst-case Tempo gas (notably new-account creation for the
bouncebackRecipient, which can hit ~250k gas on its own).

Snapshotting at deposit time means later edits to zoneGasRate cannot
retroactively raise the fee on already-queued deposits, preserving
the invariant that a malicious sequencer cannot extract additional
value from queued deposits via rejection plus a rate hike.

Internal withdrawal-bounce-back deposits (bouncebackRecipient ==
address(0)) carry bouncebackFee == 0 since they are terminal and
do not bounce back themselves.

DepositMade, EncryptedDepositMade, and DepositBounceBack now carry
bouncebackFee so off-chain observers can see exactly what was
snapshotted and what was deducted.

Made-with: Cursor
The bounce-back transfer runs on Tempo, not on the zone, so its gas
cost is Tempo gas and the fee should be denominated by tempoGasRate,
not zoneGasRate. Previously this commit priced bouncebackFee at
FIXED_BOUNCEBACK_GAS * zoneGasRate, which is the rate for zone-side
work and has no relationship to the actual cost of the refund.

Add a portal-local tempoGasRate (sequencer-managed via
ZonePortal.setTempoGasRate()) used solely for the deposit
bounce-back fee, sitting symmetrically next to the existing portal
zoneGasRate. The portal's tempoGasRate is independent of the
ZoneOutbox tempoGasRate that prices withdrawal fees: the two are
stored on different chains, not automatically synchronized, and may
legitimately differ since they price different Tempo-side work
(new-account-creation-dominated bounce-back transfers vs callback-
gas-dominated user withdrawals).

bouncebackFee = FIXED_BOUNCEBACK_GAS * tempoGasRate is snapshotted
on the queued deposit at deposit time exactly as before; only the
rate it is computed from has changed.

Made-with: Cursor
Reflect the actual worst-case Tempo gas of paying out a deposit
bounce-back. The 250k figure was a quick estimate for the new-account-
creation case; 300k is closer to the realistic upper bound once the
TIP-403 systemForceTransfer path, balance update, and event emission
are included on top of the cold-account write.

The amount >= depositFee + bouncebackFee check on both deposit() and
depositEncrypted() ensures every user-initiated deposit covers the
bumped fee at deposit time; without this the deposit reverts with
DepositTooSmall and is never queued.

Made-with: Cursor
Co-authored-by: dankrad <mail@dankradfeist.de>
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Co-authored-by: dankrad <mail@dankradfeist.de>
Comment thread specs/spec.md
Comment thread specs/spec.md Outdated
Co-authored-by: dankrad <mail@dankradfeist.de>
Comment thread specs/spec.md Outdated
dankrad added 2 commits April 28, 2026 00:00
Co-authored-by: dankrad <mail@dankradfeist.de>
Replace remaining TIP-1049 references with TIP-1052, and point all TIP
links to tempoxyz/tempo#3723 instead of
tempoxyz/tempo's main branch since the TIP isn't merged yet.

Made-with: Cursor
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
dankrad added 3 commits April 28, 2026 13:35
Co-authored-by: dankrad <mail@dankradfeist.de>
Bounce-back liveness no longer depends on TIP-1052. Failed deposit-
bounceback transfers on Tempo and failed withdrawal-bounceback mints on
the zone now park funds in per-recipient registries on `ZonePortal` and
`ZoneInbox` respectively, and the recipient claims them via
`claimRefund(token)` once the active TIP-403 policy permits.

Also collapse the two tempo-priced gas rates into a single canonical
`ZonePortal.tempoGasRate`. The zone reads it via `TempoState` when
computing withdrawal fees and snapshots it onto the queued withdrawal,
so a later sequencer-driven rate change cannot retroactively raise the
fee on an in-flight withdrawal. `tempoGasRate` defaults to
`TEMPO_T0_BASE_FEE = 10_000_000_000` at zone genesis.

Drops the upfront TIP-403 checks on `bouncebackRecipient` and
`fallbackRecipient`, since the registry catches every policy-rejected
refund without any liveness loss. Both deposit entry points still
reject `bouncebackRecipient == address(0)` to keep the queue advancing.

Made-with: Cursor
Comment thread specs/spec.md Outdated
| `tempoGasRate` | `ZonePortal.setTempoGasRate()` | Deposit bounce-back fees on Tempo: `FIXED_BOUNCEBACK_GAS (300,000) * tempoGasRate`; withdrawal fees on Tempo: `(WITHDRAWAL_BASE_GAS (50,000) + gasLimit) * tempoGasRate` |

Both rates are denominated in token units per gas unit. A single uniform `zoneGasRate` applies to all tokens. Fees are paid in the same token being deposited or withdrawn.
Both rates live on `ZonePortal` on Tempo and are sequencer-managed there. `zoneGasRate` is read on Tempo at deposit time. `tempoGasRate` is read on Tempo at deposit time (for the bounce-back fee snapshot) and is read on the zone at withdrawal-request time, where the outbox proxies it through the [`TempoState`](#tempostate-predeploy) predeploy via `readTempoStorageSlot(ZONE_PORTAL, TEMPO_GAS_RATE_SLOT)`. There is therefore a single Tempo-side source of truth for Tempo-priced work, used symmetrically for deposit refunds (Tempo) and withdrawal execution (Tempo).
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Both rates live on `ZonePortal` on Tempo and are sequencer-managed there. `zoneGasRate` is read on Tempo at deposit time. `tempoGasRate` is read on Tempo at deposit time (for the bounce-back fee snapshot) and is read on the zone at withdrawal-request time, where the outbox proxies it through the [`TempoState`](#tempostate-predeploy) predeploy via `readTempoStorageSlot(ZONE_PORTAL, TEMPO_GAS_RATE_SLOT)`. There is therefore a single Tempo-side source of truth for Tempo-priced work, used symmetrically for deposit refunds (Tempo) and withdrawal execution (Tempo).
Both rates live on `ZonePortal` on Tempo and are set by the sequencer. `zoneGasRate` is read on Tempo at deposit time. `tempoGasRate` is read on Tempo at deposit time (for the bounce-back fee snapshot) and is read on the zone at withdrawal-request time, where the outbox proxies it through the [`TempoState`](#tempostate-predeploy) predeploy via `readTempoStorageSlot(ZONE_PORTAL, TEMPO_GAS_RATE_SLOT)`. There is therefore a single Tempo-side source of truth for Tempo-priced work, used for both deposit refunds (Tempo) and withdrawal execution (Tempo).

Removes paragraphs that were intentionally trimmed in earlier review
commits but came back in 1463d52:

- "To address this symmetrically..." duplicate intro
- "**No recursive bounces.**" subsection in Deposit Failures (kept the
  two terminal-path bullets, dropped the header + "Both terminal paths
  use the standard transfer/mint" rationale)
- "**No recursive bounces.**" subsection in Withdrawal Failures
- "**No user-facing opt-out.**" paragraph
- "This is intentionally a sequencer-side decision..." rationale
- "The \`rejected\` flag is supplied by the sequencer..." paragraph
- Mid-sentence "There is no user-facing opt-out: every user-initiated
  deposit..." in Validation at deposit time
- "Every deposit must carry a usable refund target..." in step 2 of
  Regular Deposits
- The 3-paragraph fee rationale in Deposit Fees ("Charging only the
  deposit fee...", "Snapshotting at deposit time...", "The portal-
  internal withdrawal-bounce-back path bypasses...")
- "The encrypted-deposit case makes this requirement particularly
  important..." trailing rationale on depositEncrypted
- "This is symmetric to the deposit-side requirement..." rationale on
  requestWithdrawal

Also tightens the Gas Rate Configuration section, the Tempo-side
refund / Zone-side handling prose, and the Withdrawal Fees footnote
to spec-style cadence.

Made-with: Cursor
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Comment thread specs/spec.md Outdated
Co-authored-by: dankrad <mail@dankradfeist.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants