Skip to content

Solana observer drops same-type gateway deposits after the first in a single tx #4605

Description

@kingpinXD

Summary

The Solana inbound parser only emits the first instruction of each type (deposit, deposit_spl_token, call) per Solana transaction. Every subsequent same-type instruction in the same tx is silently dropped with only a warn log. The on-chain Solana gateway program accepts the second instruction (funds land in the gateway PDA or token vault), but no MsgVoteInbound is produced, so no CCTX is created and the user gets no corresponding ZRC20 credit on ZetaChain.

Same class of bug as ZCNode-243/264 on the Sui side, with a different mechanism (Sui hard-fails on EventIndex != 0; Solana intentionally clamps via per-parser-instance seen* flags).

Root cause

zetaclient/chains/solana/observer/inbound_parser.go:24-26 defines three per-tx flags:

seenDeposit    bool
seenDepositSPL bool
seenCall       bool

Each parse branch (inbound_parser.go:86-121 for deposit, 124-159 for SPL, 162-197 for call) is structured as if !seen { parse; flag = true; append event; return } else { log warn; do nothing }. The doc comment at zetaclient/chains/solana/observer/inbound.go:266-269 documents this as intentional behavior "for consistency with EVM chains" — but that rationale is stale: the EVM observer has since added EnableMultipleCallsFeatureFlag (zetaclient/context/feature_flags.go:7-18, tracked in #4292) to opt into multi-event-per-tx processing at zetaclient/chains/evm/observer/v2_inbound.go:163-166, and the Solana side was never wired into that flag.

The tracker recovery path at zetaclient/chains/solana/observer/inbound_tracker.go:84-93 goes through the same FilterInboundEvents, so manually submitting a tracker for the same Solana tx hash returns the same single event and votes the same first deposit. Recovery requires a code change or off-chain admin intervention.

Impact

Permissionless, externally triggerable, but no fund extraction primitive against the protocol.

  • User loses short-term access to dropped deposits. Funds aren't burned — they sit in the gateway PDA / token vault with no corresponding ZRC20 mint on ZetaChain.
  • Protocol doesn't lose funds. The dropped deposit increases the gateway PDA balance with no offsetting ZRC20-SOL liability, so the bridge is over-collateralized in the protocol's favor until manual reconciliation.
  • Attacker (sabotage variant) spends roughly the cost of their own decoy deposit, gets that decoy's ZRC20 back, and doesn't capture the victim's dropped deposit. No profit primitive.

Realistic exposure: benign batching by a Solana program that wraps gateway deposits (payroll, swap routing, multi-recipient distributor) would silently drop every deposit after the first without malice or even awareness. That's the dominant risk shape — operational and UX, not security.

Fix

Gate the clamp behind EnableMultipleCallsFeatureFlag to mirror the EVM observer and complete the work tracked under #4292. Approximately 20-30 LOC:

  1. Plumb ctx (or the resolved feature flag) into FilterInboundEvents / NewInboundEventParser. Current signature is (txResult, gatewayID, senderChainID, logger, resolvedTx).
  2. In parseInstruction, when the flag is set, skip the seen* flag check (or never set the flag).
  3. Keep the warn log behind the off branch; switch to info or remove when on.
  4. Add test coverage for a Solana tx with two deposit instructions, asserting both events come out when the flag is on and only the first when off.

Update the doc comment at inbound.go:266-269 once the flag is wired — the "consistency with EVM" rationale is no longer accurate.

Adjacent hardening worth considering in the same PR: an operational runbook for detecting and recovering dropped deposits — alert on multiple deposits detected warn logs, with a follow-up to manually CCTX the dropped instruction via governance for any historical drops between fix-deploy and feature-flag-on.

Severity

Low. Real bug, permissionless trigger, but no protocol fund extraction, no attacker profit primitive. Dominant risk is silent breakage of benign Solana batching patterns, recoverable via admin intervention.

References

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions