Event Logs and Pool Reserves: Decode Safely, Track Sync and Reorgs

한국어로 읽기

TECH INSIGHT / EXPLAINED

A correctly decoded log is not automatically a correct pool state. First identify the contract and ABI; then decide whether the event describes a trade or a complete reserve update.

Three colleagues identify logs, decode byte fields and reconcile reserve snapshots.
AI-generated educational illustration. Three colleagues identify logs, decode byte fields and reconcile reserve snapshots.

Recognize the event before reading bytes

For a non-anonymous event, topics[0] is the Keccak-256 event signature. Indexed address arguments occupy topics; non-indexed arguments use ABI-encoded data. Indexed dynamic values are hashes rather than a directly recoverable string or array.

An ERC-20 Transfer decoder should require the expected emitting contract, matching signature, three topics and 32 data bytes before extracting addresses and amount. These checks reject a truncated receipt instead of panicking or silently accepting the wrong ABI.

Swap is not the complete reserve history

Uniswap V2 Swap describes four traded amounts. Liquidity minting, burning and explicit syncing also affect reserves, so accumulating Swap deltas alone misses state changes. Use the pair’s Sync(uint112,uint112) reserve values, together with a pinned initial snapshot. This rule concerns the V2 pair implementation; another protocol may expose a different model.

def decode_sync(data: bytes):
    if len(data) != 64:
        raise ValueError('Sync requires two ABI words')
    values = (int.from_bytes(data[:32], 'big'),
              int.from_bytes(data[32:], 'big'))
    if any(v >= 2**112 for v in values):
        raise ValueError('not canonical uint112 values')
    return values

payload = (100).to_bytes(32, 'big') + (200).to_bytes(32, 'big')
assert decode_sync(payload) == (100, 200)

Connect the snapshot to the stream

At a chosen block, obtain getReserves and preserve its block hash. Backfill logs after that block, order by block number, transaction index and log index, and deduplicate using block hash, transaction hash and log index. Buffer live events while catching up so the handoff does not create a gap.

A WebSocket connection delivers new notifications, not a durable queue. Reconnect using a persisted checkpoint and historical log queries. On removed logs or a changed checkpoint hash, roll back affected state or rebuild from a trusted ancestor. Keep provisional and confirmed state distinguishable.

A small decoder, a larger boundary

The example validates the two words of Sync; it deliberately does not subscribe, choose a contract or handle reorgs. Test those boundaries separately. Raw token integers also need token decimals for display; decimals never change the reserve integer itself.

Validate left padding before extracting an address

An indexed address occupies the rightmost 20 bytes of a 32-byte topic; the preceding 12 bytes must be zero. Simply truncating to an H160 can make a word with invalid high bytes appear to be a valid address. Check length and padding against the address encoding in the Solidity ABI first.

def indexed_address(word):
    if len(word) != 32 or any(word[:12]):
        raise ValueError("noncanonical address topic")
    return "0x" + word[12:].hex()

An independent Python experiment accepted a canonical 32-byte word and rejected 31-byte, 33-byte, and nonzero-high-padding inputs. This function receives bytes already decoded from hexadecimal. Validate the JSON string's 0x prefix, even length, and hexadecimal characters separately before calling it. String length is not the same as decoded byte length.

Identical signatures can hide different indexed layouts

ERC-20 and ERC-721 Transfer events both use the signature Transfer(address,address,uint256). In ordinary ERC-20, the amount is in data; the ERC-721 specification makes tokenId indexed, placing it in a topic. Because the signature hash does not encode indexed annotations, topic0 alone cannot distinguish these layouts.

This provides a concrete reason to combine the emitting contract's expected ABI with topic-count and data-length checks. Quarantine a structural mismatch rather than automatically reinterpreting it through another parser. Apply equivalent strictness to Sync: accept the maximum uint112 value but reject 2^112, so a word with noncanonical upper padding cannot become a reserve value.

Handle removed before ordinary deduplication

If removed=true arrives with the same key as an already applied log, dropping it as “already seen” loses the rollback. First classify it as a change in canonical-chain status and update the stored log's application status. Repeated delivery of a normal log and removal of that log have different meanings. The Geth subscription documentation describes removed logs and renewed delivery on the replacement chain.

Include chain ID when multiple chains share a store. Block hash, transaction hash, and log index identify a particular log on a branch; block height alone cannot distinguish old and replacement branches. When removal notifications are missed or delivered out of order, do not choose current state from notification arrival order. Recheck canonical ancestry through the block headers.

Journal blocks even when they contain no matching logs

Suppose synthetic checkpoint block 100 has reserves (100, 200), and applying old block 101a changes them to (110, 190). Replacement block 101b records (95, 210). Roll back to common ancestor 100 and apply 101b. Subtracting the old Sync values from the new values is incorrect because Sync replaces the full reserves rather than describing a delta.

Journal block number, hash, parent hash, and the before/after state. Advance the checkpoint for a new block 102b even if it contains no relevant logs. Saving state and checkpoint atomically reduces the risk of applying logs while retaining an older position after a crash. The independent synthetic replay applied a duplicate only once, rolled back 101a, and restored (95, 210) on the replacement branch. It did not exercise a production subscription or an actual chain reorganization.

Sources and further reading

Solidity ABI events · Uniswap V2 pair source · Geth subscriptions

Content and examples reviewed: 2026-10-01

댓글