
Solidity Events: Logging Contract Activity for Off-Chain Applications
What Solidity events are
An event is a declaration in a contract that creates a log entry when it is emitted. These logs are stored in the transaction receipt and can be accessed by off-chain applications such as:
- Web frontends
- Backend services
- Indexers like The Graph
- Monitoring and alerting systems
- Analytics dashboards
Unlike storage variables, events are not meant for on-chain reading by other contracts. Their purpose is to make contract activity observable outside the blockchain.
Why events matter
Blockchain state is expensive to read and update. Events provide a cheaper way to expose important changes without storing extra data in contract storage. For example, instead of keeping a separate history array of transfers, a token contract can emit a Transfer event every time tokens move.
This gives you:
- Lower gas cost than storing historical records
- Easier integration with user interfaces
- Better support for audit trails and analytics
- A standard way to track contract behavior
When to use events
Use events whenever a state change should be visible to off-chain systems.
Common examples include:
- A user deposits or withdraws funds
- Ownership changes
- A new order is created or filled
- A voting action is recorded
- A configuration parameter is updated
Do not use events as a replacement for essential contract state. If your contract logic depends on a value, store it in state variables. Events are for notification, not for persistent business logic.
Events vs storage
| Feature | Events | Storage |
|---|---|---|
| Purpose | Notify off-chain systems | Persist contract state |
| Readability from contracts | Not directly usable | Directly readable |
| Gas cost | Lower for logging | Higher for persistent writes |
| Historical tracking | Good | Requires custom design |
| Suitable for business logic | No | Yes |
A good rule is: if the contract must remember it, store it; if external systems only need to know it happened, emit an event.
Declaring an event
An event is declared with the event keyword.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract PaymentVault {
event Deposit(address indexed sender, uint256 amount);
}This declaration defines a log schema. It does not emit anything by itself. To create a log entry, you must call emit inside a function.
emit Deposit(msg.sender, msg.value);Emitting an event in a contract
Here is a practical example of a simple vault contract that accepts deposits and withdrawals.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract PaymentVault {
mapping(address => uint256) public balances;
event Deposit(address indexed sender, uint256 amount);
event Withdrawal(address indexed recipient, uint256 amount);
function deposit() external payable {
require(msg.value > 0, "No ether sent");
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawal(msg.sender, amount);
}
}What this example shows
Depositis emitted after the balance is updatedWithdrawalis emitted after the transfer succeeds- The event includes the actor and the amount
- The
balancesmapping remains the source of truth for contract logic
This pattern makes the contract easy to monitor from a frontend or backend service.
Indexed parameters and filtering
Solidity allows you to mark up to three event parameters as indexed. Indexed parameters are stored in the log topics, which makes them searchable by off-chain tools.
event Deposit(address indexed sender, uint256 amount);Here, sender is indexed, so a client can filter logs by sender address.
Why indexing is useful
If your application needs to query logs for a specific user, order, or asset, indexing makes that possible efficiently. For example:
- Find all deposits made by one address
- Track all transfers involving a specific token ID
- Watch updates for a given marketplace listing
Important trade-off
Indexed parameters are easier to filter, but the full value is not always as convenient to decode in every context. Also, only three parameters can be indexed per event, so choose them carefully.
A practical guideline:
- Index identifiers you will query often
- Do not index everything
- Keep the event focused on the most useful search keys
Designing useful event payloads
A good event should be informative, compact, and stable.
Include the minimum useful context
For most contracts, an event should answer these questions:
- Who triggered the action?
- What happened?
- How much or which asset was involved?
- What identifier or reference ties this to application state?
For example, a marketplace contract might emit:
event OrderFilled(
uint256 indexed orderId,
address indexed buyer,
address seller,
uint256 price
);This gives off-chain systems enough information to update a UI, record analytics, and reconcile business logic.
Avoid redundant data
Do not emit values that can be trivially derived from other event fields or from current state. Extra data increases gas usage and can make logs harder to interpret.
For example, if msg.sender is already the caller, you usually do not need to emit both caller and sender unless they represent different concepts.
Keep names consistent
Use clear, action-oriented names:
DepositWithdrawalOrderCreatedOrderCancelledRoleGranted
Avoid vague names like Updated unless the event is very small and the context is obvious.
Event patterns in real contracts
Different contract types benefit from different event designs.
Token contracts
Token contracts typically emit events for transfers and approvals. This is how wallets and explorers track balances and allowances.
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);These events are so common that many tools expect them by convention.
Governance contracts
A governance system might emit:
event ProposalCreated(uint256 indexed proposalId, address indexed proposer, string description);
event VoteCast(uint256 indexed proposalId, address indexed voter, bool support, uint256 weight);This lets a frontend display proposal history and vote participation without scanning storage-heavy data structures.
Marketplace contracts
A marketplace might use:
event ListingCreated(uint256 indexed listingId, address indexed seller, uint256 price);
event ListingPurchased(uint256 indexed listingId, address indexed buyer, uint256 price);
event ListingCancelled(uint256 indexed listingId, address indexed seller);These events provide a complete activity trail for each listing.
Emitting events at the right time
The order of operations matters. Emit events only after the relevant state change has succeeded.
Good practice
- Validate inputs
- Update state
- Perform external interactions if needed
- Emit the event after success
In many cases, emitting after the state update is the clearest approach because the log reflects the final result.
Example: state first, event second
balances[msg.sender] -= amount;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawal(msg.sender, amount);This ensures the event is only emitted when the withdrawal completes successfully.
Why this matters
If you emit an event too early and a later step fails, the whole transaction reverts and the log disappears. That is usually fine, but it can make code harder to reason about. Emitting after success keeps the event aligned with the final contract state.
Events and contract upgrades
Even in non-upgradeable contracts, event design deserves attention because logs become part of your application’s public interface.
Treat event schemas as stable APIs
Changing event names or parameter order can break:
- Frontend listeners
- Indexers
- Analytics jobs
- Monitoring scripts
If you need to evolve an event, consider adding a new event instead of changing the old one.
Example strategy
Suppose you originally emit:
event OrderCreated(uint256 indexed orderId, address indexed seller, uint256 price);Later you want to include a token address. Rather than changing the old event, add a new one:
event OrderCreatedV2(
uint256 indexed orderId,
address indexed seller,
address token,
uint256 price
);This preserves compatibility with existing consumers.
Common mistakes to avoid
1. Using events as storage
Events are not readable by other contracts. If your contract needs to know a past value, store it in state.
2. Emitting too much data
Large event payloads increase gas costs and can make logs harder to process. Keep them focused.
3. Forgetting indexed fields
If you know you will query by address or ID, index those fields from the start.
4. Emitting before validation
If a function may revert, emit the event only after all checks and critical operations succeed.
5. Changing event signatures casually
A signature change can break off-chain consumers. Treat event definitions as part of your public interface.
Practical checklist for event design
Before finalizing an event, ask:
- Will off-chain systems need to filter by this field?
- Is the event name specific and action-oriented?
- Does the payload contain enough context to interpret the action?
- Is any field redundant?
- Could this schema need to remain stable for a long time?
A well-designed event usually has a small number of fields, one or two indexed identifiers, and a clear semantic meaning.
Summary
Events are the bridge between on-chain execution and off-chain applications. They let your Solidity contract publish structured logs that frontends, indexers, and monitoring systems can consume efficiently.
Use events when you want to:
- Track important contract actions
- Support filtering and analytics
- Reduce storage usage
- Make your application easier to integrate
Design them carefully, keep them stable, and emit them only when the underlying state change is complete. That approach will make your contracts easier to maintain and much easier to build on.
