ADR-0010: A withdrawal is initiated on the L2 and proven from its own block

The L1 half of a withdrawal has existed and been deployed for some time: prove_withdrawal verifies a state-tree inclusion proof against a FINAL settlement root and binds it to (id, amount, recipient);…

Status: accepted. Dated 2026-09-02.

Status: accepted (2026-09-02) · Prompt: §6 C5 / C9, §7.2 inv. 2/3/7; stuck-funds runbook W1/W5

Context

The L1 half of a withdrawal has existed and been deployed for some time: prove_withdrawal verifies a state-tree inclusion proof against a FINAL settlement root and binds it to (id, amount, recipient); finalize_withdrawal pays after a delay, once, to that recipient, and cannot be reached by the pause flag. What did not exist was any way for a user of the L2 to put a withdrawal leaf into the tree the root commits to, or to obtain a proof against the root that actually finalized. The harness hand-inserted the leaf. That is a mechanism, not an exit.

Decision

  • Initiation is an ordinary signed transaction. A withdrawal is a wire-format Solana transaction whose single instruction targets a reserved L2 program address, sha256("solieum-l2-bridge-v1"), with data [1][recipient(32)][amount u64 le], signed by the account being debited. The node intercepts it before the SVM runtime (which would refuse the unknown program): the flat fee is charged as for any transaction, the payer is debited amount (too little balance is a reported drop, fee kept, like any failed instruction), and a leaf is inserted. No new record kind: the bytes are published as kind 1 and replay re-derives the leaf from them, so a verifier holds a signed authorization for every leaf it recomputes.
  • The leaf is exactly what the portal recomputes. Key TreeKey(Hash32(id)) in the same state tree as the accounts; value sha256("solieum-wd" ‖ id ‖ amount_le ‖ recipient), the portal's WD_DOMAIN commitment. id = sha256("solieum-wd-id" ‖ signature), unique and derivable from the published bytes. Leaves never expire; the portal's [b"wd", id] record, created by the proof, is the double-pay guard.
  • The proof comes from the finalized block's tree, not the head. Sibling hashes change as other leaves change, so a proof is root-specific. The node rebuilds the state at the block whose root is FINAL by replaying its own published log to that height (root-verified, as every replay is) and proves from that tree. The proof wire format is the portal's: key ‖ value_hash ‖ [side ‖ sibling]*.
  • Prove and finalize are permissionless. Any wallet can send them; the node ships them as withdraw --prove / --finalize for convenience, the same way finalize and force are conveniences over permissionless instructions.

Consequences

  • The settlement module gains the proof encoder, the two instruction builders, embedded and cluster clients, and a decoder for the portal's withdrawal record. The node gains the intercept, the leaf map, replay-to-block, and the CLI.
  • Proven in-process against the deployed program bytes: deposit funds the vault; a leaf is committed in a settled root; the root finalizes after the window; a proof before finality is refused; an inflated claim on the honest proof bytes is refused; the exact claim is accepted; a second proof of the id is refused; payout before the delay is refused; on time it pays exactly once to the recorded recipient and cannot be repeated.
  • On devnet a withdrawal cannot be proven before the first root finalizes (2026-09-09); the initiate step and the leaf can be exercised now, and the proof and payout then.
  • Lamports only, like the deposit path. Token withdrawals have their L1 half but no L2 token state to initiate from.

Reversal triggers

A validity proof (ADR-0002 multi-prover) shortens the wait but not the shape. If the L2 ever executes the bridge as a real program rather than an intercept, the instruction encoding and the id derivation stay as defined here so existing leaves remain provable.