All articles
Web3 FoundationsSeptember 16, 20269 min read

Events and indexed: What a Contract Writes Down and Cannot Read Back

Events are the announcements a contract writes into the transaction log instead of its storage. What emit does, what indexed buys you, and why every wallet history is built out of them.

By Carlos (Bloqarl)

TL;DR

  • An event is an announcement a contract writes into the transaction log, which lives outside contract storage and is no part of the contract's memory.
  • A contract can write an event and can never read one back. That asymmetry explains the rest: events exist for the outside world, not for the code that emits them.
  • indexed marks a parameter as searchable. Up to three per event become topics you can filter on, and the rest is data you read only once you have the log.
  • Your wallet's transaction history and every transfer table on a block explorer are assembled out of these logs, not out of contract storage.
  • Logs cost far less gas than storage, which is why history belongs in events and current state in variables.

What is an event in Solidity?

An event is a named announcement a contract can write into the transaction log. You declare its shape once with event, then write one with emit. The result is kept by nodes alongside the transaction, outside the contract's storage, where anything off chain can read it afterwards.

Uniswap V2's token declares two, and they may be the most-read lines of Solidity ever written:

    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

A declaration is a shape, not an action. Nothing happens until a function emits one, and here is the only place in that file where a balance moves:

    function _transfer(address from, address to, uint value) private {
        balanceOf[from] = balanceOf[from].sub(value);
        balanceOf[to] = balanceOf[to].add(value);
        emit Transfer(from, to, value);
    }

Three lines: take from one balance, add to another, announce it. The first two change what the contract knows. The third changes nothing inside the contract and writes a record for the outside world.

Why can a contract never read its own events?

Because the log is written outward, and the EVM gives contracts no instruction to read it back. A contract can append entries to the log, and it can never query them, not its own and not another contract's. Anything a contract needs to act on later has to be in storage.

This is the asymmetry that makes sense of everything else. In _transfer, the two balanceOf lines are the contract's memory and it will read them on the next call. The emit line is a broadcast. Delete the balanceOf lines and keep the emit, and the log would claim a transfer that never happened, with the contract unable to notice, because it cannot read what it wrote.

So events are never a substitute for state variables. They are a parallel record, cheaper than storage, existing only for readers who are not the contract: a user interface, an indexer, a wallet.

The practical version: if a fact is only in an event, no contract on chain can ever act on it. That tells you a lot about what a system is and is not designed to do.

What does indexed do in a Solidity event?

indexed makes a parameter searchable. Indexed parameters become topics, which nodes keep in a filterable structure, so you can ask for every log matching a value without downloading everything. You get three per event at most, and the remaining parameters are stored as plain data.

WETH9 declares four events and marks addresses, never amounts:

    event  Approval(address indexed src, address indexed guy, uint wad);
    event  Transfer(address indexed src, address indexed dst, uint wad);
    event  Deposit(address indexed dst, uint wad);
    event  Withdrawal(address indexed src, uint wad);

The topic list is what a query runs against. Ask a node for every log from this address where topic one is your address, and you get every WETH transfer you ever sent, in seconds, without reading a single balance. Ask for every log where the amount was exactly 1 ETH and you cannot, because the amount is not a topic. It sits in the data half, readable only once you have the log.

Three is the ceiling because a log has four topic slots and the first holds the event's signature, which is how a reader tells a Transfer from a Deposit.

Why does Transfer index the addresses and not the amount?

Because of what people actually ask. Every real question about token movement starts with a person: what did this address receive, what did it send, who held this token. Those are queries by address, so the addresses become the topics, and almost nobody starts a question with an amount.

Index it the other way and a wallet would have to download every transfer the token ever emitted and filter locally to show one person's history. Indexing the addresses is what makes that history cheap to assemble, which is what your wallet does when it lists your tokens. The same event carries minting and burning too, with address(0) standing in for the missing party.

Why are events cheaper than storage?

Because nodes never have to make a log available to the chain's execution again. A storage write must stay queryable by any future call, forever, which is what makes it one of the most expensive operations in Solidity. A log is written once, attached to its transaction, and never read by the machine.

That difference in obligation is a large difference in price, and it settles where each kind of information belongs:

  • Current state goes in storage, because the contract has to read it back. Balances, owners, allowances.
  • History goes in events, because nobody on chain needs it. Who transferred what, when, to whom.

A contract that stored every transfer it processed would cost a fortune and be no better at its job: the only readers of that history are off chain, and the log serves them already.

How do you read an emit site?

Read the state change first, then the announcement, and ask whether the two describe the same thing: which variable moved, by how much, and whether the emitted parameters say that. They are separate lines, and nothing in the language checks one against the other.

WETH9 is the clean case:

    function deposit() public payable {
        balanceOf[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }
    function withdraw(uint wad) public {
        require(balanceOf[msg.sender] >= wad);
        balanceOf[msg.sender] -= wad;
        msg.sender.transfer(wad);
        emit Withdrawal(msg.sender, wad);
    }

deposit credits a balance by msg.value and announces exactly msg.value credited to exactly the address credited. withdraw debits wad and announces wad. Each announcement describes the state change directly above it, and both name msg.sender, so the log records who did it.

Ordering is the other thing to notice, and real code does it both ways. ENS emits before writing in setResolver:

        emit NewResolver(node, resolver);
        records[node].resolver = resolver;

and after writing in _setResolverAndTTL:

        if (resolver != records[node].resolver) {
            records[node].resolver = resolver;
            emit NewResolver(node, resolver);
        }

Both produce the same log, because a revert undoes the whole transaction and no log survives either way. A log is never written early and left behind: it appears only if the transaction succeeds.

Why does ENS emit a separate event for every field?

Because a log reader wants to know which thing changed, and one event per field says so without guessing. An ENS record holds an owner, a resolver and a TTL, so the registry declares one event for each of them, and a reader gets the answer from the event's name.

They are declared in the interface the registry implements:

    // Logged when the owner of a node assigns a new owner to a subnode.
    event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner);

    // Logged when the owner of a node transfers ownership to a new account.
    event Transfer(bytes32 indexed node, address owner);

    // Logged when the resolver for a node changes.
    event NewResolver(bytes32 indexed node, address resolver);

    // Logged when the TTL of a node changes
    event NewTTL(bytes32 indexed node, uint64 ttl);

Compare that with a single RecordChanged event carrying all three fields. A reader would know something changed and would have to diff against the previous record to see what. Because node is indexed on all four, you can also pull one domain's whole history in a single filtered query.

The second gain shows up in _setResolverAndTTL, which sets both fields and emits only for the ones that actually differ from what was there. A reader of that log sees changes, not restatements. The same reading applied to a live contract's log tab is reading it on Etherscan.

Delete the line and ask what breaks

  • Delete the emit Transfer line from _transfer and balances still move correctly, but your wallet stops showing the transfer and every indexer on the token goes blind. The contract never notices.
  • Delete indexed from both addresses and the log still contains them, but nobody can filter by them. One address's history would mean downloading every transfer the token ever emitted.
  • Add a fourth indexed parameter and it will not compile. Three is the limit, because the first topic slot belongs to the event signature.
  • Swap the addresses to emit Transfer(to, from, value) and the contract behaves identically while every reader of the log sees the money flowing the wrong way.

Related questions

What is the difference between an event and a state variable? A state variable lives in storage and the contract reads it back on later calls. An event lives in the transaction log, is written once, and cannot be read by any contract. One is memory, the other a broadcast.

Can a smart contract read events from another contract? No. There is no EVM instruction to read logs, so no contract can query its own entries or anyone else's. Contracts communicate through calls and return values, never through events.

How many indexed parameters can an event have? Three for a normal event. A log carries four topic slots and the first holds the event signature, which is how readers identify which event a log represents. Anonymous events free that slot and can index four, at the cost of being harder to recognise.

Are events stored on the blockchain? They are kept by nodes as part of the transaction record, so they are on chain in the ordinary sense. They are not in contract storage, and nodes may prune old logs, so availability depends on who keeps them.

Why are events cheaper than storage writes? Because a log never has to be readable by future contract execution. A storage write must stay queryable forever, and that permanent obligation is what the chain charges for.

Does emitting an event change the contract's state? No. Emitting appends to the log and leaves every storage value as it was. That is why an event and the state change it describes are two separate lines, and why the pair can disagree without the contract minding.

Where to go next

The whole subject collapses into one sentence: a contract writes events and can never read them. Everything else follows from that one direction of travel, including why indexed exists, why history lives in logs while balances live in storage, and why an emit line can describe a state change it does not verify.

Now put it to work on a whole file rather than a keyword: how to read a smart contract.

Tagged

SoliditySmart ContractsLearn to Code