Events¶
Events are how contracts publish state changes to the chain log. The VM LOG opcode records an entry with topics + data into the call frame; on a successful tx, frame logs are merged into the receipt.
Declaration¶
event Transfer(from: address, to: address, amount: uint);
event Approval(owner: address, spender: address, amount: uint);
Field types are declared but not used for runtime encoding — every arg word is 32 bytes regardless. Field types are part of the signature hash that becomes topic 0 (see below).
Signature hash (topic 0)¶
sig_string = name + "(" + ",".join(type_name for _, type_name in params) + ")"
topic_0 = int.from_bytes(sha3_256(sig_string.encode()), "big")
For event Transfer(from: address, to: address, amount: uint):
Type names use the Fourier keyword spelling: uint, address, bool, bytes.
Emit semantics¶
emit NAME(arg1, ..., argN) compiles to a LOG<k> opcode where k = min(1 + N, 4):
- The first up-to-3 args become indexed topics (
topic_1, topic_2, topic_3). - Any remaining args are written to memory and emitted as
data.
So:
Arg count N | Topics | Data |
|---|---|---|
| 0 | [sig] | empty |
| 1 | [sig, arg0] | empty |
| 2 | [sig, arg0, arg1] | empty |
| 3 | [sig, arg0, arg1, arg2] | empty |
| 4+ | [sig, arg0, arg1, arg2] | arg3 \|\| arg4 \|\| ... packed 32 bytes each |
Topic 0 is always the signature hash. Topics are 256-bit ints; data is a byte string of (N - 3) * 32 bytes when N > 3.
Bytecode layout¶
For emit Transfer(sender, to, amount) (3 args → 4 topics, no data):
PUSH amount ; topic_3 (last indexed arg, deepest stack slot)
PUSH to ; topic_2
PUSH sender ; topic_1
PUSH topic_0_hash ; topic_0 (signature)
PUSH 0 ; data length
PUSH 0 ; data offset (SCRATCH_A)
LOG 4
The LOG handler in vm/machine.py:
offset = pop()
length = pop()
topics = [pop() for _ in range(n_topics)] # [topic_0, topic_1, ...]
self._mem_expand(f, offset, length)
self._charge(f, 375 * n_topics + 8 * length)
f.logs.append(LogEntry(
address=f.address,
topics=topics,
data=bytes(f.memory[offset:offset + length]),
))
Constraints¶
- Maximum 4 topics. Topic 0 is reserved for the signature hash, so you get up to 3 indexed args.
- A
LOGopcode is forbidden in aSTATICCALLframe; emitting an event inside astaticcall_bcallee reverts. - Emit args are evaluated left-to-right. Side effects in earlier args happen before later args.
Gas¶
Per vm/opcodes.py and vm/machine.py:
base LOG = 375
per topic = 375 → 375 * n_topics
per data byte = 8 → 8 * length
memory expansion to (offset, length) — standard EVM-like quadratic
Reading logs off-chain¶
Each LogEntry becomes part of the transaction receipt:
{
"address": "<emitting contract address, hex>",
"topics": ["<topic_0 hash>", "<topic_1>", ...],
"data": "<hex>"
}
A subscriber filters by topics[0] (the signature hash). Compute the expected topic_0 client-side:
import hashlib
sig = "Transfer(address,address,uint)"
topic0 = hashlib.sha3_256(sig.encode()).hexdigest()
Pattern: indexable args go first¶
Because the first 3 args become topics (cheaper to filter), put the fields you'll query by — sender/recipient addresses, ids — first. Put bulky or rarely-queried fields last (they end up in data):