
Solidity Events and Indexed Topics: Designing Queryable, Low-Cost On-Chain Logs
Why events matter in production contracts
Events are written to transaction logs, not contract storage. That distinction is important:
- Storage is permanent contract state and is expensive to write.
- Logs are cheaper, append-only, and intended for off-chain consumption.
- Indexed topics make it possible to filter logs efficiently by specific values.
In practice, events are useful for:
- tracking token transfers and approvals
- building front-end activity feeds
- powering subgraphs and analytics tools
- recording administrative actions
- creating audit trails for governance and compliance
Because logs are not readable by other contracts during execution, events are an off-chain interface. They should be designed for indexers, explorers, and backend services—not for on-chain business logic.
Event anatomy: data versus indexed topics
A Solidity event can contain both regular data fields and indexed fields.
event OrderFilled(
address indexed trader,
uint256 indexed orderId,
address asset,
uint256 amount,
uint256 price
);What indexing does
Indexed parameters are stored in log topics, which makes them searchable by clients. Non-indexed parameters are stored in the log data payload.
A log can have up to three indexed parameters in addition to the event signature topic. That means you must choose carefully which fields deserve indexing.
Practical rule of thumb
Index fields that you will frequently filter by:
- user addresses
- token addresses
- IDs
- status categories
- parent/child relationships
Do not index fields that are only displayed, such as:
- human-readable labels
- large numeric values used only for presentation
- metadata URIs
- long strings or bytes arrays
Designing event schemas for real applications
A good event schema is stable, minimal, and easy to query. The goal is not to mirror every storage field. The goal is to expose the right operational facts.
Example: a marketplace order lifecycle
Consider a marketplace contract that supports listing, filling, and canceling orders.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MarketplaceEvents {
event OrderCreated(
uint256 indexed orderId,
address indexed seller,
address asset,
uint256 amount,
uint256 price
);
event OrderFilled(
uint256 indexed orderId,
address indexed seller,
address indexed buyer,
uint256 amountFilled,
uint256 totalPaid
);
event OrderCancelled(
uint256 indexed orderId,
address indexed seller
);
}This design works well because:
orderIdlets indexers follow a specific order.sellerandbuyersupport account-level filtering.- the emitted values describe the state transition clearly.
- each event represents a distinct business action.
Notice that we do not emit every internal variable. We emit the minimum useful set that allows reconstruction of the order lifecycle.
Choosing indexed fields strategically
Indexing improves queryability, but it also affects gas cost and event design flexibility.
| Field type | Best use | Tradeoff |
|---|---|---|
indexed address | user, token, vault, operator | cheap to filter, ideal for common lookups |
indexed uint256 | IDs, nonces, sequence numbers | excellent for entity tracking |
indexed bytes32 | hashes, role IDs, commitments | compact and query-friendly |
| non-indexed string/bytes | labels, URIs, metadata | readable but not filterable |
| non-indexed structs via flattened fields | detailed payloads | more verbose, but easier to decode |
When not to index
Avoid indexing fields that are:
- rarely queried
- large and mostly informational
- high-cardinality but not useful for filtering
- likely to change format over time
For example, if you emit a metadata URI for an NFT, indexing it is usually wasteful. Clients typically fetch metadata by token ID, not by URI.
Event design patterns that scale
1. Emit one event per meaningful state transition
A contract should emit events when a user-visible state transition occurs. Examples:
- funds deposited
- position opened
- role granted
- configuration updated
- proposal executed
Avoid emitting events for every internal helper call. That creates noise and makes analytics harder.
2. Use consistent naming
Use past-tense names for completed actions:
DepositMadeRoleGrantedPositionClosed
This makes logs easier to read and aligns with the idea that events describe something that already happened.
3. Keep payloads deterministic
Event data should be reproducible from contract state and transaction inputs. Avoid including derived values that can be computed off-chain unless they are important for auditability.
4. Prefer explicit fields over packed meaning
Instead of emitting a single ambiguous uint256 data, emit named fields:
event FeeUpdated(uint256 oldFeeBps, uint256 newFeeBps);This is easier for indexers, dashboards, and auditors to interpret.
Emitting events from state-changing functions
A common best practice is to emit events after state has been updated, so the log reflects the final result of the transaction.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Vault {
mapping(address => uint256) public balances;
event Deposited(address indexed account, uint256 amount, uint256 newBalance);
event Withdrawn(address indexed account, uint256 amount, uint256 newBalance);
function deposit() external payable {
require(msg.value > 0, "No value");
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value, balances[msg.sender]);
}
function withdraw(uint256 amount) external {
require(amount > 0, "Invalid amount");
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "Transfer failed");
emit Withdrawn(msg.sender, amount, balances[msg.sender]);
}
}Why this is useful
The event includes newBalance, which helps off-chain consumers display the updated account state without making an additional RPC call. That said, this is a design choice: if the balance is easy to query, you may omit it to save log data.
Events versus storage: what belongs where?
Events are not a replacement for storage. They are complementary.
| Need | Use storage | Use events |
|---|---|---|
| contract logic depends on the value | yes | no |
| other contracts must read it on-chain | yes | no |
| front-end or analytics needs history | no | yes |
| permanent audit trail is useful | no | yes |
| value must be queryable by address or ID | maybe | yes, with indexed topics |
A good mental model is:
- storage = current truth for contract execution
- events = historical record for observers
If a value is needed to enforce rules, store it. If it is needed to observe behavior, emit it.
Common mistakes to avoid
Emitting too many events
Over-logging increases gas usage and makes off-chain processing noisy. If a function emits several events, ask whether each one is truly necessary.
Indexing everything
Only three parameters can be indexed, and each indexed field should support a real query pattern. Indexing every field is not possible and not useful.
Using events as a source of truth for contracts
Other contracts cannot reliably use logs as input during execution. If a rule depends on a value, store it in state.
Changing event signatures casually
Event signatures are part of your public interface. Changing parameter order, types, or indexed status can break indexers and analytics tools.
If you must evolve an event, consider:
- adding a new event instead of modifying the old one
- keeping legacy events for backward compatibility
- documenting the version change clearly
Emitting ambiguous values
A field like uint256 status is hard to interpret unless the meaning is documented. Prefer enums in storage and explicit event names or comments for emitted values.
Advanced considerations for protocol designers
Event versioning
For long-lived protocols, event schemas should be treated like APIs. If you expect future changes, design for versioning early.
A common approach is:
- keep the original event
- introduce a new event with a version suffix or more explicit fields
- update off-chain consumers gradually
Example:
event PositionUpdated(address indexed trader, uint256 size);
event PositionUpdatedV2(address indexed trader, uint256 size, uint256 collateral);Correlating related events
Complex workflows often span multiple events. Use stable identifiers to connect them:
orderIdproposalIdpositionIdnoncecommitmentHash
This is especially important for systems that have asynchronous off-chain processing.
Emitting hashes for large payloads
If a payload is too large or too sensitive to emit directly, emit its hash instead.
event DocumentCommitted(bytes32 indexed documentHash, address indexed author);This lets you prove that a document existed at a specific time without storing the full content on-chain.
Testing event behavior
Event tests are important because logs are part of your contract’s external contract with the world.
In Solidity tests or JavaScript/TypeScript test suites, verify:
- the event is emitted
- the correct event name is used
- indexed fields match expected values
- payload values reflect the final state
- no unexpected duplicate events are emitted
A good test should fail if a developer accidentally changes the event order, removes a field, or emits the wrong address.
Best practices checklist
Before finalizing an event design, check the following:
- Does this event represent a meaningful external action?
- Are the indexed fields the ones users will actually filter by?
- Is the payload minimal but sufficient?
- Will the event remain understandable in six months?
- Can off-chain systems reconstruct the needed history from logs alone?
- Is the naming consistent with the rest of the contract?
- Would adding another event improve clarity more than adding more fields?
If the answer to most of these is yes, the event design is probably solid.
Conclusion
Events are one of the most important tools for building production-grade Solidity systems. They make contracts observable, enable efficient indexing, and provide a durable history of meaningful state transitions. The best event designs are not the most verbose—they are the most intentional.
Treat event schemas as part of your public API. Index only what you need to query, emit only what matters, and keep the structure stable enough for off-chain consumers to rely on. That discipline pays off quickly in cleaner integrations, lower gas costs, and better operational visibility.
