My dev blog where I dive deep into TypeScript, Postgres, Data science, Infrastructure, Ethereum, and more...

Tightly packing Ethereum event logs

24th Oct 2023

Events on Ethereum are a emitted from contracts. It’s a great place to put data you want to query historical data for.

For example, ERC-20 tokens (like UCDC) use the Transfer event to log when a token has been transferred. It consists of three arguments and is structured like this: Transfer(address from, address to, uint256 amount).

This is how a sample Transfer event looks like:

1  000000000000000000000000B42C838346Ce40aa3B22582A635c01D56C8C7943 (from)
2  000000000000000000000000728713b41fcEc5C071359Ecf2802Fa0B62bec5a8 (to)
3  0000000000000000000000000000000000000000000000000000000000000001 (amount)

As you can see, all arguments are padded with 0s to take up 32 bytes. Isn’t this a lot of useless space?

Even if you define an event with many small data types, it won’t be tightly packed.

This is how an event TestEvent(uint8 someNumber, bool someBool) could look like:

1  000000000000000000000000000000000000000000000000000000000000000F (someNumber)
2  0000000000000000000000000000000000000000000000000000000000000001 (someBool)

Lots of unused space…

Why doesn’t Solidity allow tightly packing event arguments?

I don’t know…

Maybe it is because this data is only stored in transaction receipts and is therefore not so important?

Manually packing event arguments

Even if Solidity doesn’t automatically pack event arguments, we could always do it ourselves.

Imagine we have an event with four uint64 communityIds.

We would have Communities(uint64 communityId1, uint64 communityId2, uint64 communityId3, uint64 communityId4)

1  0000000000000000000000000000000000000000000000000000000000000000 (communityId1) // the min number 0
2  0000000000000000000000000000000000000000000000001111111111111111 (communityId2)
3  0000000000000000000000000000000000000000000000001234567890abcdef (communityId2)
4  000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF (communityId4) // the max number

Instead of these taking four 32-byte words, we could manually put them all into the same space.

Communities(uint256 communities)

1  000000000000000011111111111111111234567890abcdefFFFFFFFFFFFFFFFF (communityIds)
//   (communityId)  (communityId2)  (communityId3)  (communityId4)

The problem with this is that it would be harder to parse.

Tools like Viem or Ethers would automatically parse the whole communityIds hex as one giant number.

In Dune however, you could be able to parse it with this query:

Take a look at the event here: https://goerli.etherscan.io/address/0xf39011e462241b31bfc0018eb6913c8d6b87a3b1#events

Conclusion

It's a shame that Solidity doesn't automatically tightly pack event data. Doing it manually comes with trade-offs.

The best option in most cases is probably to stick with the default, don’t worry about the extra 0000s and not outsmart ourselves.


Tools