Solidity Events and NatSpec Documentation: A Practical Guide
How to write Solidity events and NatSpec documentation correctly — why they matter for audits, indexing, and developer experience, with practical examples.
Two things get skipped in most Solidity codebases that should never be optional: events and NatSpec documentation. Both are lightweight to write and expensive to omit. Missing events make off-chain systems brittle and incident response slow. Missing NatSpec makes audits more expensive and integrations harder. Neither is a nice-to-have.
Why Solidity Events Matter
Events write data to the transaction log — a separate, cheaper storage layer outside of contract state. Indexed off-chain by services like The Graph, Etherscan, and custom indexers, events are how the outside world knows what happened inside your contract.
Without events, external systems must either read state repeatedly (expensive and unreliable) or cannot track contract activity at all. With events, any observer can reconstruct the full history of your contract's state changes from the logs.
The gas comparison makes the decision obvious: writing a new value to a cold storage slot costs around 20,000 gas. Emitting an event costs roughly 375 base gas plus per-byte topic and data costs. For state that is only consumed off-chain, events are the correct mechanism.
How to Write Solidity Events Correctly
Declare events at the top of the contract, before state variables:
event Transfer(
address indexed from,
address indexed to,
uint256 amount
);
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
Emit them in the function body after the state update:
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount); // after state update
}
When to Use indexed
The indexed keyword allows the event parameter to be used as a filter in log queries. You can have up to three indexed parameters per event. Indexed parameters are stored as topics (32 bytes each in the log structure) rather than as ABI-encoded data.
Index parameters that subscribers will filter on: addresses, token IDs, status codes. Do not index parameters that are only informational or that are already derivable from context.
For string and bytes types, indexing stores a keccak256 hash of the value rather than the value itself. This means you can check for equality but cannot decode the original string from the log.
What Events to Emit
Every meaningful state change should emit an event. This includes:
- Token transfers and approvals
- Ownership or role changes
- Configuration updates (fee changes, parameter updates)
- User-facing actions (deposits, withdrawals, claims)
- Pause or unpause events
- Contract upgrades (in upgradeable systems)
If a state change could affect a user's balance, permissions, or expectations — emit an event. Do not emit events for internal computations that have no external significance.
Solidity NatSpec Documentation
NatSpec (Natural Language Specification) is Solidity's inline documentation format. Block explorers like Etherscan use it to display human-readable descriptions to users before they sign transactions. Auditors use it to understand intent quickly. Frontend developers use it to generate accurate UI copy.
A well-documented contract is faster and cheaper to audit — auditors spend less time inferring intent from code.
NatSpec Tags
/// @title A simple token vault
/// @author YourTeam
/// @notice This contract holds tokens on behalf of users and allows withdrawal after a lock period
/// @dev Uses the Checks-Effects-Interactions pattern throughout; no external calls before state updates
contract TokenVault {
/// @notice Deposit tokens into the vault
/// @dev Caller must approve this contract before calling
/// @param amount The number of tokens to deposit (in the token's native decimals)
function deposit(uint256 amount) external {
// ...
}
/// @notice Withdraw tokens after the lock period expires
/// @param amount The number of tokens to withdraw
/// @return success True if the withdrawal succeeded
function withdraw(uint256 amount) external returns (bool success) {
// ...
}
/// @notice Emitted when a user deposits tokens
/// @param user The depositing address
/// @param amount The amount deposited
event Deposited(address indexed user, uint256 amount);
}
The core tags:
@title— contract-level, one-line description@notice— human-readable explanation for end users (shown by Etherscan)@dev— technical notes for developers and auditors@param— description of each function parameter@return— description of each return value@inheritdoc— inherit documentation from a parent interface
Where NatSpec Matters Most
Etherscan and wallet UIs. When users are about to sign a transaction, @notice from the called function is displayed in some wallets and on verified contracts. Writing @notice as if a non-technical user will read it is the correct approach.
Audit readiness. Auditors who have clear intent documentation can focus on verifying that the code matches the intent, rather than spending time inferring what the intent was. Clear NatSpec directly reduces audit hours and cost.
Developer integrations. Other developers integrating with your contract benefit from accurate parameter descriptions and return value documentation. This reduces integration bugs.
A Practical NatSpec Standard
Apply these rules to every contract you ship:
- Every
publicandexternalfunction has@noticeand@paramfor each parameter - Every function with a non-trivial return value has
@return - Every event has
@noticewith the trigger condition and@paramfor each field - Every state variable that is not obvious from its name and type has a
/// @devor/// @noticecomment - The contract itself has
@titleand@notice
This takes roughly 10-15 minutes per contract and pays back multiples in audit efficiency.
Generating Documentation from NatSpec
Foundry can generate documentation from NatSpec automatically:
forge doc
This produces a documentation site in docs/ from your NatSpec annotations. Running this during development keeps documentation in sync with code and surfaces functions that are missing annotations.
Events and NatSpec are not polish — they are engineering discipline. If you want a contract system delivered with complete event coverage and NatSpec that reflects actual intent, Clixo builds Solidity systems where documentation is a deliverable, not an afterthought.