Skip to content

fix(data-fetcher): authenticate bridge event emitter before indexing transfers#613

Open
vladb-ai wants to merge 2 commits into
matter-labs:mainfrom
vladb-ai:fix/authenticate-bridge-event-emitter
Open

fix(data-fetcher): authenticate bridge event emitter before indexing transfers#613
vladb-ai wants to merge 2 commits into
matter-labs:mainfrom
vladb-ai:fix/authenticate-bridge-event-emitter

Conversation

@vladb-ai
Copy link
Copy Markdown

@vladb-ai vladb-ai commented Jun 1, 2026

Problem

The bridge transfer handlers in data-fetcher select work purely by event topic and their matches() returned true unconditionally, never checking the log emitter. As a result any unprivileged L2 contract can emit ABI-shaped bridge logs and have them indexed as real bridge deposits/withdrawals with attacker-chosen from / to / amount / token:

  • DepositFinalizedAssetRouter / WithdrawalInitiatedAssetRouter (Asset Router path)
  • FinalizeDeposit / WithdrawalInitiated (legacy shared-bridge path)

These fabricated transfers surface in the explorer UI and the public API (e.g. on a victim address's /transfers), i.e. misrepresentation of transaction data. No fund safety impact; data-integrity only.

Fix

Asset Router handlers — require the emitter to be the L2 Asset Router system contract (L2_ASSET_ROUTER_ADDRESS, 0x…010003). This is deployed at the same fixed address on Era and every ZK chain, so the check is chain-agnostic with no config.

Legacy shared-bridge handlers — legacy events have no single authoritative emitter (the canonical shared bridge plus deployment-specific custom token bridges, e.g. Lido wstETH). They are authenticated against a trusted set built from:

  1. zks_getBridgeContracts (canonical bridges; cached), and
  2. a configurable allowlist via the new TRUSTED_LEGACY_BRIDGE_ADDRESSES env var (comma-separated).

Chains that don't implement zks_getBridgeContracts (e.g. ZKsync OS) fall back to the allowlist; they emit no legacy events.

Verification

  • Unit tests: full data-fetcher suite green (400 tests; new accept/reject cases for both handler families + getTrustedLegacyBridgeAddresses). Lint clean.
  • Cross-chain (live RPC): every Asset Router deposit/withdrawal on Era mainnet (443+ samples) and the deposit on ZKsync OS mainnet is emitted solely by 0x…010003; the patched handler accepts those and rejects other emitters.
  • Legacy (real Era log): against the real Lido wstETH withdrawal — canonical-only set drops it (the trade-off), TRUSTED_LEGACY_BRIDGE_ADDRESSES re-includes it, and an attacker emitter is rejected.
  • End-to-end: rebuilt data-fetcher rejects real on-chain attacker-emitted bridge logs while still parsing router/bridge-attributed ones.

Operator note

To keep indexing legitimate custom legacy bridges, set TRUSTED_LEGACY_BRIDGE_ADDRESSES. For Era mainnet this includes at least the Lido wstETH bridge 0xe1d6a50e7101c8f8db77352897ee3f1ac53f782b. Recommend scanning historical legacy-topic emitters per network to seed this list before rollout so no legitimate custom-bridge transfers are dropped.

🤖 Generated with Claude Code

…transfers

The bridge transfer handlers selected work purely by event topic and
returned matches() === true unconditionally, so any L2 contract could emit
ABI-shaped DepositFinalizedAssetRouter / WithdrawalInitiatedAssetRouter or
legacy FinalizeDeposit / WithdrawalInitiated logs and have them indexed as
real bridge deposits/withdrawals with attacker-chosen from/to/amount/token.

Asset Router handlers: require the log emitter to be the L2 Asset Router
system contract (L2_ASSET_ROUTER_ADDRESS, 0x..010003), which is deployed at
the same fixed address on Era and every ZK chain (verified on Era mainnet
and ZKsync OS mainnet).

Legacy shared-bridge handlers: legacy events have no single authoritative
emitter (canonical shared bridge plus deployment-specific custom token
bridges, e.g. Lido wstETH), so authenticate against a trusted set built from
zks_getBridgeContracts (cached) plus a configurable allowlist via the new
TRUSTED_LEGACY_BRIDGE_ADDRESSES env var. Chains without zks_getBridgeContracts
(e.g. ZKsync OS) fall back to the allowlist; they emit no legacy events.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vladb-ai vladb-ai requested a review from a team as a code owner June 1, 2026 11:27
…ache RPC fallback

Follow-up to the bridge emitter authentication fix, addressing review findings:

1. Observability for the legacy allowlist trade-off: with an empty allowlist the
   legacy handlers silently drop real custom-bridge transfers (e.g. Lido wstETH).
   Add a skipped_untrusted_legacy_bridge_logs_total metric and a debug log naming
   the skipped emitter so operators can discover bridges to allowlist instead of
   losing the data silently. Logic moved into BlockchainService.isTrustedLegacyBridgeEmitter.

2. Document the TRUSTED_LEGACY_BRIDGE_ADDRESSES env var in .env.example and the
   data-fetcher README, including the Era mainnet Lido wstETH example.

3. Cache the trusted-set fallback on deterministic zks_getBridgeContracts failures
   (e.g. -32601 method not found) so a failing call is not re-issued for every
   legacy log; only transient network errors remain uncached and are retried.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants