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:
- Plumb
ctx (or the resolved feature flag) into FilterInboundEvents / NewInboundEventParser. Current signature is (txResult, gatewayID, senderChainID, logger, resolvedTx).
- In
parseInstruction, when the flag is set, skip the seen* flag check (or never set the flag).
- Keep the warn log behind the off branch; switch to info or remove when on.
- 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
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 noMsgVoteInboundis 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-instanceseen*flags).Root cause
zetaclient/chains/solana/observer/inbound_parser.go:24-26defines three per-tx flags:Each parse branch (
inbound_parser.go:86-121for deposit,124-159for SPL,162-197for call) is structured asif !seen { parse; flag = true; append event; return } else { log warn; do nothing }. The doc comment atzetaclient/chains/solana/observer/inbound.go:266-269documents this as intentional behavior "for consistency with EVM chains" — but that rationale is stale: the EVM observer has since addedEnableMultipleCallsFeatureFlag(zetaclient/context/feature_flags.go:7-18, tracked in #4292) to opt into multi-event-per-tx processing atzetaclient/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-93goes through the sameFilterInboundEvents, 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.
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
EnableMultipleCallsFeatureFlagto mirror the EVM observer and complete the work tracked under #4292. Approximately 20-30 LOC:ctx(or the resolved feature flag) intoFilterInboundEvents/NewInboundEventParser. Current signature is(txResult, gatewayID, senderChainID, logger, resolvedTx).parseInstruction, when the flag is set, skip theseen*flag check (or never set the flag).depositinstructions, asserting both events come out when the flag is on and only the first when off.Update the doc comment at
inbound.go:266-269once 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 detectedwarn 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
EnableMultipleCallsFeatureFlag)hackeproof/analysis/blockchain/report_ZCNode-271.md