Open
Conversation
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
This was referenced Apr 23, 2026
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
Collaborator
Author
|
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:
This PR now cites TIP-1049 directly in the
Pushed as |
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.
danrobinson
reviewed
Apr 24, 2026
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.
dankrad
commented
Apr 25, 2026
dankrad
commented
Apr 25, 2026
dankrad
commented
Apr 25, 2026
dankrad
commented
Apr 25, 2026
dankrad
commented
Apr 25, 2026
dankrad
commented
Apr 25, 2026
dankrad
commented
Apr 25, 2026
dankrad
commented
Apr 25, 2026
dankrad
commented
Apr 25, 2026
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
fcbc881 to
38a7787
Compare
dankrad
commented
Apr 25, 2026
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>
dankrad
commented
Apr 27, 2026
dankrad
commented
Apr 27, 2026
dankrad
commented
Apr 27, 2026
dankrad
commented
Apr 27, 2026
dankrad
commented
Apr 27, 2026
dankrad
commented
Apr 27, 2026
Co-authored-by: dankrad <mail@dankradfeist.de>
dankrad
commented
Apr 27, 2026
dankrad
commented
Apr 27, 2026
Co-authored-by: dankrad <mail@dankradfeist.de>
dankrad
commented
Apr 27, 2026
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
dankrad
commented
Apr 28, 2026
dankrad
commented
Apr 28, 2026
dankrad
commented
Apr 28, 2026
dankrad
commented
Apr 28, 2026
dankrad
commented
Apr 28, 2026
dankrad
commented
Apr 28, 2026
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
dankrad
commented
Apr 29, 2026
| | `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). |
Collaborator
Author
There was a problem hiding this comment.
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
dankrad
commented
Apr 29, 2026
dankrad
commented
May 5, 2026
dankrad
commented
May 5, 2026
Co-authored-by: dankrad <mail@dankradfeist.de>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Splits out the spec portion of #360. This PR only touches
specs/spec.mdand adds a full prose description of the symmetric deposit bounce-back mechanism, plus the corresponding updates to theIZonePortalinterface 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
A new section Deposit Failures and Bounce-Back under Deposits, documenting:
bouncebackRecipientargument todeposit/depositEncryptedand the TIP-403 validation the portal performs at deposit time to guarantee the refund transfer will succeed.ZoneInbox.advanceTempo(including the fallback-to-sender mint after a failed decryption).ZoneInboxcatches the revert and callsZoneOutbox.enqueueDepositBounceBack(token, amount, bouncebackRecipient), which enqueues a zero-fee, zero-callback, zero-fallbackRecipientwithdrawal.ZonePortal.processWithdrawalrefunds the escrowed tokens and emitsDepositBounceBack.bouncebackRecipient = address(0)and bounce-back withdrawals havefallbackRecipient = address(0), so neither can spawn another bounce.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.DepositFailed,EncryptedDepositFailed,DepositBounceBack, and the renamedWithdrawalBounceBack.Updates to Regular Deposits and Encrypted Deposits:
bouncebackRecipientargument documented in the prose and in the ordered deposit steps.Updates to the
IZonePortalinterface stub:deposit/depositEncryptedtakebouncebackRecipient.DepositMadegainsbouncebackRecipient.DepositBounceBackevent.BounceBack→WithdrawalBounceBackfor symmetry.depositNumber/lastProcessedDepositNumberfields toDepositMade/EncryptedDepositMade/BatchSubmitted, and addsdepositCount()/lastProcessedDepositNumber()views.A new TOC entry for the new section.
Non-goals
Review hints
specs/spec.mdat### Deposit Failures and Bounce-Backrather than diff-by-diff.### Withdrawal Failures and Bounce-Backsection is intentional — same structure, opposite direction.Test plan
Made with Cursor