Parsing Smart Contract Event Logs: Tracking Pool State with topics and data

🌐 한국어

An on-chain bot has two ways to answer "how much is in this pool right now?" One is to ask the contract for reserves every time (slow, burns RPC calls); the other is to subscribe to event logs and follow the changes. Nearly every on-chain action—transfers, swaps, liquidity additions—leaves an event log. Read those and you track state in real time without polling.

Log Structure — topics and data

A single event log splits into two parts: topics (an indexed array) and data (everything else). The rules go like this.

  • topics[0] — the hash of the event signature. The fingerprint telling you "which event is this."
  • topics[1..] — arguments declared indexed (addresses and such). You can filter on these fields.
  • data — a byte blob holding the remaining non-indexed arguments in order.

For ERC-20's Transfer(address indexed from, address indexed to, uint256 value), for instance, from and to land in topics while value goes into data.

// Precompute the event signature hashes (topics[0])

var (

    transferSig = crypto.Keccak256Hash(

        []byte("Transfer(address,address,uint256)"))

    // UniswapV2 Swap event

    swapSig = crypto.Keccak256Hash(

        []byte("Swap(address,uint256,uint256,uint256,uint256,address)"))

)

Decoding a Transfer Log

Topics are padded to 32 bytes. An address is 20 bytes, so you have to strip the leading 12 to get the real address. Values in data are read as big.Int.

func parseTransfer(vLog types.Log) (from, to common.Address, value *big.Int) {

    // topics[0]=signature, topics[1]=from, topics[2]=to

    from = common.BytesToAddress(vLog.Topics[1].Bytes()) // takes the last 20 bytes

    to = common.BytesToAddress(vLog.Topics[2].Bytes())

    // data holds a single 32-byte value

    value = new(big.Int).SetBytes(vLog.Data)

    return

}

common.BytesToAddress takes the trailing 20 bytes out of 32 and strips the padding for you. Helpers like this cut down on manual byte slicing, but remember the rule: topics are left-padded.

Reading Pool Flow from Swap Events

The UniswapV2 Swap event carries four values in data — amount0In, amount1In, amount0Out, amount1Out. Everything about which direction moved how much is right there. Slice it 32 bytes at a time, in order.

func parseSwap(vLog types.Log) (a0In, a1In, a0Out, a1Out *big.Int) {

    d := vLog.Data // 32 bytes × 4 = 128 bytes

    a0In = new(big.Int).SetBytes(d[0:32])

    a1In = new(big.Int).SetBytes(d[32:64])

    a0Out = new(big.Int).SetBytes(d[64:96])

    a1Out = new(big.Int).SetBytes(d[96:128])

    return

}

Those four values let you compute the reserve delta directly. Reserve0 grows by the token0 that came in and shrinks by the token0 that went out. Accumulate Swap events alone and you keep pool state current locally—without calling getReserves() every time.

Subscribing for Real Time

Build a filter from the pool addresses and event signatures you care about, subscribe, and new logs get pushed onto a channel as they appear.

query := ethereum.FilterQuery{

    Addresses: poolAddrs,                 // pools to track

    Topics:    [][]common.Hash{{swapSig}}, // Swap events only

}

logs := make(chan types.Log)

sub, err := client.SubscribeFilterLogs(ctx, query, logs)

if err != nil {

    return fmt.Errorf("failed to subscribe to logs: %w", err)

}

for {

    select {

    case err := <-sub.Err():

        return err // on drop, resubscribe (same principle as WebSocket reconnect)

    case vLog := <-logs:

        a0In, a1In, a0Out, a1Out := parseSwap(vLog)

        pool.ApplySwap(vLog.Address, a0In, a1In, a0Out, a1Out)

    }

}

What to Watch Out for in Production

  • Account for reorgs. A log you just received can be invalidated by a chain reorganization. If vLog.Removed is true, you must roll that change back.
  • Validate the data length. Event definitions differ subtly between contracts, so check len(vLog.Data) before slicing to avoid a panic.
  • Seed the initial state with one query. Events only tell you about changes. Read the starting reserves once with getReserves() to establish a baseline, then update from events.
  • Confirm token0/token1 ordering. Which token is index 0 differs per pool. They're sorted by address, so nail down the mapping before computing direction.

Event logs are the official receipts the chain leaves behind. Understand the structure of topics and data and your bot can hold live state for hundreds of pools without heavy polling. That's exactly where fast bots separate from slow ones.

This article is for educational and informational purposes and is not investment advice. On-chain automated trading carries risk of principal loss, and you alone are responsible for the outcomes.

댓글

이 블로그의 인기 게시물

한국투자증권 KIS API로 실시간 시세 받기 (WebSocket 실전)

파이썬으로 업비트 API 연동하기 — 시세 조회부터 주문까지 기초

Go로 자동매매 신호봇 프레임워크 설계하기