Skip to content
This guide targets Fe 26.4. Fe is not production-ready. Install Fe 26.4

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.

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.

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 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.

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)
}
}
}

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
}
}

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 })
}
}

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,
})
}
}

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.

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
}
}

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 })
}
}
}
PatternDescription
log.emit(event: Event { ... })Emit an event
Emit on the success pathNo event for changes that did not happen
Multiple eventsSame handler can emit different types
Helper functionsCentralize event emission
Conditional emitOnly emit on meaningful changes

Events are your contract’s public record. Emit them consistently after successful state changes to enable reliable off-chain indexing.