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.Removedis 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.
댓글
댓글 쓰기