Every event a contract emits reaches the chain as up to four 32-byte topics
and a blob of data. The first topic is the hash of the event’s signature;
what the rest mean is not on the chain at all. It is in the contract’s ABI,
and a transaction page decodes a log only when it can find one.

Three sources, tried in order

The built-in registry. config/abis/*.json holds the events of the
protocols this platform tracks and the token standards everything else
uses: Aave V3 Pool, Compound V3 Comet, Morpho Blue, Maker Vat and Dog,
Uniswap V2 pairs and V3 pools, ERC-20, ERC-721, WETH9 and Ownable. Each
file names where its entries were checked against (a Sourcify-verified
deployment, or the protocol’s published interface), and nothing is in a
file that was not. Registry entries decode on sight: no lookup, on every
page, for every emitter, because the signature hash is the key.

The emitter’s verified ABI. When a log’s signature is not in the
registry, the explorer asks whether the contract that emitted it is
verified: first on Sourcify (open, no key), then on
Etherscan when the platform has an API key. A hit is cached for good in
contract_abis; a miss is cached too, and retried only after a week, so an
unverified contract costs one round trip and not one per visitor. Only the
explorer’s transaction page fetches. Research pages, which render many saved
snapshots at once, read the cache and never the network, so a snapshot’s
decoding can improve after someone opens the transaction in the explorer.

A name alone. With no ABI at all, the signature database
4byte.directory may still know the event’s
name. The card then says so, shows the name, and leaves the parameters
raw: a name is not a layout, and guessing one would put wrong numbers next
to right labels.

The card under the event badge tells you which of the three applied.

Proxies

Most large protocols sit behind an EIP-1967 proxy: the address that emits
the events holds no code of its own, and its ABI, if verified at all, only
describes the proxy. Before fetching, the explorer reads the proxy’s
implementation slot (keccak256("eip1967.proxy.implementation") − 1) over
JSON-RPC. A non-zero answer means the ABI to fetch is the implementation’s,
and the cached row records both addresses. The Aave V3 Pool, for instance,
decodes through PoolInstance at the implementation address, not through
the proxy at 0x87870Bca…. When an implementation is not verified but the
proxy is, the proxy’s own events (Upgraded, AdminChanged) still decode.

An upgrade moves the implementation. The cached ABI does not follow it
until the row is refreshed, which is a known limit rather than a bug: the
old events still decode, and a new one added by the upgrade shows as
unnamed until then.

What “decoded” means

Indexed parameters come from the topics, the rest from the data, laid out
as the Solidity ABI specification says: static values in place, dynamic
ones (strings, bytes, arrays, structs) through offsets into a tail. Signed
integers are read as two’s complement, so a Uniswap V3 Swap shows one
amount negative, which is the pool paying out. An array renders as a list
and a struct as its named fields, so a Seaport order with three
consideration items shows three items. Addresses are complete and link to
the explorer, like everywhere else.

One thing cannot be decoded: an indexed string, bytes, array or struct.
The chain keeps only its keccak hash in the topic, and the card says so
rather than printing the hash as if it were the value.

Maker’s Vat is the special case. Its LogNote event is anonymous, so the
first topic is not a signature hash but the calldata’s function selector.
The decoder matches anonymous events by shape, from the number of indexed
parameters, when the ABI leaves only one candidate.

The coverage figure

The ClickHouse console reports, for the last 10,000
blocks (about a day and a half), the share of logs the explorer would
decode. One aggregate groups the window’s logs by signature and emitter;
each group is then classified against what the platform knows right now:

  • Decoded in full: the signature is in the registry, or the emitter’s
    cached ABI has the event.
  • Named at least: the above, plus signatures where only a name is
    cached. This is always the larger figure.
  • Undecoded: neither. The console lists the largest of these, since
    opening one transaction that emits such a signature fetches the emitter’s
    ABI and moves its logs into the first bucket from then on.

Shares are of logs, not of distinct signatures. One busy Transfer counts
for every log it produced, because that is what a reader of the explorer
sees. The figure is a property of the cache as much as of the registry: it
rises as people open transactions, and it says nothing about signatures
that never appeared in the window.

For developers

  • AbiDecoder.decode(entry, topics, data) decodes one log against one ABI
    event entry and raises AbiDecoder::Error on data that does not fit,
    which the card reports instead of showing a partial result.
  • LogDecoder.decode(topic0, topics, data, decimals:, emitter:, abi:) picks
    the entry in the order above and scales Transfer-style amounts by the
    token’s decimals when known.
  • AbiFetcher.fetch(address) resolves the proxy, tries the sources, and
    writes the search log. AbiFetcher.cached(addresses) is the read-only
    path research pages use.
  • bin/rails abis:seed (also run by db:seed) loads the registry into
    contract_abis with source static; it is idempotent.
  • DecodeCoverage.for_recent_blocks(n) is the console’s report.

The cache table is described in the data dictionary.