ADR-0009: A deposit is a forced-inclusion entry
solieum-bridge-portal::deposit locked lamports in the vault and emitted an event. Nothing on L1 obliged anyone to credit the L2, nothing on L1 recorded the deposit beyond a counter, and the depositor had no way…
Status: accepted. Dated 2026-09-02.
Status: accepted (2026-09-02) · Prompt: §6 C3 / C8 / C9; stuck-funds runbook D1
Context
solieum-bridge-portal::deposit locked lamports in the vault and emitted an event. Nothing on L1 obliged anyone to credit the L2, nothing on L1 recorded the deposit beyond a counter, and the depositor had no way back. That is a one-way door, and it was the single worst row in the stuck-funds runbook: value in, no path out.
Two designs were considered for the credit path:
- Watch the event / add per-deposit records to the portal. The node reads deposits off the portal and credits them. This creates a second queue with its own deadline, watermark and fault rule — a copy of the inbox, drifting from it.
- Make the deposit an inbox entry. The portal enqueues the deposit on the existing forced-inclusion inbox by CPI. The inbox already fixes a deadline on-chain, already faults the chain for an entry left behind, already publishes the bytes, and the node already drains it every block.
Decision
- deposit(amount, recipient) does three things in order: transfers amount to the vault; transfers the inbox's minimum inclusion fee plus the entry's rent from the depositor to a relay PDA ([b"relay"], system-owned, no data); then CPIs inbox.enqueue with the relay as the signing submitter and a 94-byte deposit record as the payload: "solieum-dep-v1" ‖ seq_le ‖ depositor ‖ recipient ‖ amount_le.
- The relay's address is the L2's trust anchor. The node credits an inbox entry as a deposit only when entry.submitter == relay_pda. Only a call through the portal program can sign for that address, and the program only signs after the vault has been credited. Anyone else enqueueing a deposit-shaped payload is just a forced transaction that fails to parse as one, consumed as a no-op like any junk entry.
- Crediting is a derivation rule, not an execution. On the L2 the credit is a record of its own (published kind 3, carrying the L1 entry index and the record), applied by the same function in production and replay: lamports appear at recipient, no fee, no instruction. The invariant L2 supply ≤ L1 locked holds by construction because the only mint path is a relay entry, and every relay entry follows a vault credit.
- Refund. refund_deposit(index) pays the depositor back from the vault when the entry is not included and its deadline has passed. Permissionless; the destination is fixed by the record; a [b"refund", index] marker makes it happen once. The inclusion fee and the entry's rent are not refunded — they are the cost of asking the chain to do something, and the fee is already reclaimable through the inbox's own rules.
- Tokens are out of scope. deposit_token still only emits an event; the L2 has no token state to credit. The program says so in its docs rather than implying otherwise.
Consequences
- A deposit costs the depositor the inbox fee plus the entry's rent on top of the amount (about 0.008 SOL at devnet rent). That is visible, and it is what forced inclusion costs.
- The portal now depends on the inbox program (CPI feature). Settlement still does not depend on the portal.
- Program interface changed (Deposit accounts, Deposited event, new Refund account and refund_deposit); the Portal account layout did not, so the devnet portal's initialized PDA and vault carry over. The devnet upgrade is an operator step: the program-data account (389,752 bytes) must be extended to fit the 459,400-byte build (~0.51 SOL, permanent), then the upgrade posts a 2.91 SOL buffer that is refunded on success. The one deposit made on devnet before this ADR has no inbox entry and stays movable only by the upgrade key.
- The node's inbox drain gains a branch, the published record set gains kind 3, replay learns it, and genesis (or a run-time flag) names the portal so the relay can be derived.
Reversal triggers
A validity proof for the L2 (ADR-0002 multi-prover) would let deposits be credited by proof instead of by inclusion obligation; revisit then. A token-bearing L2 state revisits the token half.