Emitting Events
Events are emitted using a Log effect, which records data to the blockchain that external systems can observe. This section covers how to emit events in your Fe contracts.
The emit Syntax
Section titled “The emit Syntax”Emit an event through a Log effect:
#[event]struct Transfer { #[indexed] from: Address, #[indexed] to: Address, amount: u256,}
fn emit_transfer(from: Address, to: Address, amount: u256) uses (log: mut Log) { log.emit(event: Transfer { from, to, amount })}The emit method takes an event struct instance and records it to the blockchain.
Emitting in Message Handlers
Section titled “Emitting in Message Handlers”Events are typically emitted within message handlers:
pub struct TokenStorage { pub balances: StorageMap<Address, u256>,}
#[event]struct Transfer { #[indexed] from: Address, #[indexed] to: Address, amount: u256,}
impl TokenStorage { fn do_transfer(mut self, from: Address, to: Address, amount: u256) -> bool uses (log: mut Log) { let from_bal = self.balances.get(key: from) if from_bal < amount { return false }
self.balances.set(key: from, value: from_bal - amount) let to_bal = self.balances.get(key: to) self.balances.set(key: to, value: to_bal + amount)
// Emit event after successful state change log.emit(event: Transfer { from, to, amount })
true }}Emit After State Changes
Section titled “Emit After State Changes”Emit an event on the path where the state change actually happens:
pub struct TokenStorage { pub balances: StorageMap<Address, u256>,}
#[event]struct Transfer { #[indexed] from: Address, #[indexed] to: Address, amount: u256,}
impl TokenStorage { fn transfer(mut self, from: Address, to: Address, amount: u256) -> bool uses (log: mut Log) { // 1. Validate let from_bal = self.balances.get(key: from) if from_bal < amount { return false }
// 2. Update state self.balances.set(key: from, value: from_bal - amount) self.balances.set(key: to, value: self.balances.get(key: to) + amount)
// 3. Emit event (state change succeeded) log.emit(event: Transfer { from, to, amount })
true }}If a call reverts, the EVM discards its logs along with its state changes, so a reverted transfer never leaves a Transfer event behind. This function signals failure by returning false instead of reverting, so an event emitted before the balance check would be recorded for a transfer that never happened. Within a successful call, emitting before or after the storage writes records the same log.
Contract Integration
Section titled “Contract Integration”In contracts, storage is a contract field, while Log is an effect: the contract declares it in its own uses clause (contract Token uses (ctx: Ctx, log: mut Log)), and each handler that emits lists it again in the handler’s uses (uses (ctx, mut store, mut log)):
pub struct TokenStorage { pub balances: StorageMap<Address, u256>,}
impl TokenStorage { fn do_transfer(mut self, from: Address, to: Address, amount: u256) -> bool uses (log: mut Log) { true }}
#[event]struct TransferEvent { #[indexed] from: Address, #[indexed] to: Address, amount: u256,}
msg TokenMsg { #[selector = sol("transfer(address,uint256)")] Transfer { to: Address, amount: u256 } -> bool,}
contract Token uses (ctx: Ctx, log: mut Log) { mut store: TokenStorage,
recv TokenMsg { Transfer { to, amount } -> bool uses (ctx, mut store, mut log) { store.do_transfer(from: ctx.caller(), to, amount) } }}Multiple Event Types
Section titled “Multiple Event Types”Emit different event types from the same handler:
#[event]struct Transfer { #[indexed] from: Address, #[indexed] to: Address, amount: u256,}
#[event]struct Approval { #[indexed] owner: Address, #[indexed] spender: Address, amount: u256,}
impl TokenStorage { fn transfer_from(mut self, spender: Address, from: Address, to: Address, amount: u256) -> bool uses (log: mut Log) { // Check and update allowance let allowed = self.allowances.get(key: (from, spender)) if allowed < amount { return false } self.allowances.set(key: (from, spender), value: allowed - amount)
// Perform transfer let from_bal = self.balances.get(key: from) self.balances.set(key: from, value: from_bal - amount) self.balances.set(key: to, value: self.balances.get(key: to) + amount)
// Emit both events log.emit(event: Approval { owner: from, spender, amount: allowed - amount, }) log.emit(event: Transfer { from, to, amount })
true }}When to Emit Events
Section titled “When to Emit Events”State Changes
Section titled “State Changes”Emit when persistent state changes:
impl TokenStorage { fn mint(mut self, to: Address, amount: u256) uses (log: mut Log) { self.balances.set(key: to, value: self.balances.get(key: to) + amount) self.total_supply = self.total_supply + amount
log.emit(event: Transfer { from: Address { inner: 0 }, to, amount }) }
fn burn(mut self, from: Address, amount: u256) uses (log: mut Log) { self.balances.set(key: from, value: self.balances.get(key: from) - amount) self.total_supply = self.total_supply - amount
log.emit(event: Transfer { from, to: Address { inner: 0 }, amount }) }}Administrative Actions
Section titled “Administrative Actions”Emit for ownership and configuration changes:
#[event]struct OwnershipTransferred { #[indexed] previous_owner: Address, #[indexed] new_owner: Address,}
impl AdminStorage { fn transfer_ownership(mut self, new_owner: Address) uses (log: mut Log) { let previous = self.owner self.owner = new_owner
log.emit(event: OwnershipTransferred { previous_owner: previous, new_owner, }) }}Not for Read-Only Queries
Section titled “Not for Read-Only Queries”Don’t emit events from queries. Contracts often call getters with STATICCALL (for example through a Solidity view interface), and a log inside a static call makes the call revert. Off-chain eth_call results are never recorded, and in a transaction a log for a read costs gas without recording a state change.
Event Helpers
Section titled “Event Helpers”Create helper functions for common events:
fn emit_transfer(from: Address, to: Address, amount: u256) uses (log: mut Log) { log.emit(event: Transfer { from, to, amount })}
fn emit_approval(owner: Address, spender: Address, amount: u256) uses (log: mut Log) { log.emit(event: Approval { owner, spender, amount })}
impl TokenStorage { fn transfer(mut self, from: Address, to: Address, amount: u256) -> bool uses (log: mut Log) { // ... transfer logic ...
emit_transfer(from, to, amount) true }}Conditional Emission
Section titled “Conditional Emission”Only emit when something meaningful happens:
impl TokenStorage { fn set_approval(mut self, owner: Address, spender: Address, new_amount: u256) uses (log: mut Log) { let current = self.allowances.get(key: (owner, spender))
// Only emit if value actually changes if current != new_amount { self.allowances.set(key: (owner, spender), value: new_amount) log.emit(event: Approval { owner, spender, amount: new_amount }) } }}Summary
Section titled “Summary”| Pattern | Description |
|---|---|
log.emit(event: Event { ... }) | Emit an event |
| Emit on the success path | No event for changes that did not happen |
| Multiple events | Same handler can emit different types |
| Helper functions | Centralize event emission |
| Conditional emit | Only emit on meaningful changes |
Events are your contract’s public record. Emit them consistently after successful state changes to enable reliable off-chain indexing.