Skip to content

BSC: ERC20 deposits silently missed when eth_getLogs returns incomplete logs at chain head #114

Description

@anhthii

Problem

On BSC mainnet, a user's ERC20 deposit in block 122770723 was not indexed. A later rescan of the same block found it.

Run Range Transfer events Matched
2026-09-19 (live) 122770723 → 122770724 394 0
2026-09-21 (rescan) 122770723 → 122770723 391 1

Transaction https://bscscan.com/tx/0xf78a40c60f15bb20104a8749caa366d77cce390fd3c346abc254f3f144adf523 Block 122770723

If the first result had been complete, block 122770724 would have had only 3 Transfer events. BSC blocks usually have hundreds, so the live query got an incomplete log set. The matching logic ran the same both times; the RPC returned different data.

Root cause

  • The regular worker processes right up to the RPC's head block (internal/worker/regular.go, end := min(..., latest)).
  • BSC produces a block about every 0.45s. Nodes often import a block before its log index is complete. During that gap, eth_getLogs returns partial or empty results with no error.
  • ERC20 detection relies only on eth_getLogs (queryERC20TransfersToMonitoredAddresses in internal/indexer/evm.go). A missing log means the tx is never looked at again.

Solutions

1. Head delay using confirmations

ChainConfig.Confirmations (yaml:"confirmations") is already parsed but nothing reads it. Wire it into RegularWorker.processRegularBlocks so the worker processes up to latest - confirmations:

if latest > rw.config.Confirmations {
    latest -= rw.config.Confirmations
}

Then set confirmations: 10 for bsc_mainnet (about 4–5s, which is past BSC fast finality and the log-indexing gap). Leaving it unset means 0, so other chains behave as they do today.

  • ✅ Small change, configurable per chain, works on every chain
  • ⚠️ Lowers the risk but doesn't remove it if a provider is badly behind

2. Use the finalized block tag as the head (per-chain opt-in)

On chains with fast finality, use eth_getBlockByNumber("finalized") instead of latest as the head. On BSC the finalized head is usually 2–3 blocks behind.

  • ✅ Follows real finality instead of a fixed number
  • ⚠️ A lagging node also reports a lagging finalized head
  • ⚠️ Opt-in only for fast-finality chains. It is far too slow for Ethereum and the L2s, so it will not be used there (see table).

finalized tag support by chain

Chain Supported How far behind the head Good for deposit indexing?
BSC Yes ~2–3 blocks (about 1–2s) ✅ Yes
Avalanche C-Chain Yes ~0, finality is effectively instant ✅ Yes
Polygon PoS Yes Seconds, since its milestone-based finality upgrade. It used to be ~30 min. ✅ Probably. Check with your provider.
Ethereum mainnet Yes ~64–95 blocks, about 13–19 minutes ❌ Too slow, won't use
Arbitrum / Optimism / Base (L2s) Yes Waits for Ethereum to finalize the batch, about 15–30+ minutes ❌ Too slow, won't use
TRON No, its JSON-RPC doesn't support this tag It has its own "solidified" block API instead (about 19 blocks, ~1 min) Needs separate handling
Other EVM chains (Conflux, Lisk, …) Depends on the chain and the node software Check each one

Finality timings change as chains upgrade. Check against current providers before enabling.

Recommendation

Do 1 now with confirmations: 10 for BSC. Add 2 as an optional per-chain head mode for fast-finality chains (BSC, Avalanche) if we want head selection based on real finality.

Recovery

Rescan affected BSC ranges to recover missed deposits. Block 122770723 has been confirmed to recover on rescan.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions