Events in Sui are the primary mechanism for off-chain systems to observe on-chain state changes. Poor event design leads to missed state changes, ambiguous interpretations, replay vulnerabilities, and off-chain system failures.
Risk Level
Medium — Can cause off-chain desync, incorrect UI state, or indexer failures.
OWASP / CWE Mapping
OWASP Top 10
MITRE CWE
A04 (Insecure Design), A09 (Security Logging and Monitoring Failures)
CWE-223 (Omission of Security-relevant Information), CWE-778 (Insufficient Logging)
The Problem
Event Design Issues
Missing events — State changes without corresponding events
Ambiguous events — Events that don’t clearly indicate what happened
Duplicate events — Same event for different operations
No event versioning — Breaking changes affect off-chain systems
Vulnerable Example
modulevulnerable::trading{usesui::object::{Self,UID};usesui::event;usesui::tx_context::TxContext;/// VULNERABLE: Ambiguous event — which operation?
publicstructTradeEventhascopy,drop{amount: u64,}/// VULNERABLE: Missing critical context
publicstructTransferEventhascopy,drop{amount: u64,// Missing: from, to, timestamp, tx_digest, token_type
}publicstructOrderhaskey{id: UID,amount: u64,status: u8,}/// VULNERABLE: State change without event
publicentryfuncancel_order(order: &mutOrder){order.status=2;// Cancelled
// NO EVENT! Off-chain systems don't know about this
}/// VULNERABLE: Same event for different operations
publicentryfunbuy(amount: u64){// ... execute buy
event::emit(TradeEvent{amount});// Was this a buy?
}publicentryfunsell(amount: u64){// ... execute sell
event::emit(TradeEvent{amount});// Or a sell?
}/// VULNERABLE: Event emitted before operation might fail
publicentryfunrisky_transfer(amount: u64,ctx: &mutTxContext){event::emit(TransferEvent{amount});// This might abort! But event already emitted
assert!(amount>0,E_ZERO_AMOUNT);// ... transfer logic that might also fail
}}
Impact on Off-Chain Systems
// Off-chain indexer can't determine:
// 1. Was TradeEvent a buy or sell?
// 2. Who initiated the trade?
// 3. What was the token involved?
// 4. What was the order status before change?
asyncfunctionprocessEvent(event){if(event.type==='TradeEvent'){// Ambiguous! Can't update state correctly
}}
Secure Example
modulesecure::trading{usesui::object::{Self,UID,ID};usesui::event;usesui::tx_context::{Self,TxContext};usesui::clock::{Self,Clock};usestd::type_name::{Self,TypeName};// Event version for future compatibility
constEVENT_VERSION: u8=1;/// SECURE: Specific, complete event for each operation
publicstructOrderCreatedEventhascopy,drop{version: u8,order_id: ID,creator: address,token_type: TypeName,amount: u64,price: u64,side: u8,// 0 = buy, 1 = sell
timestamp_ms: u64,}publicstructOrderFilledEventhascopy,drop{version: u8,order_id: ID,filler: address,fill_amount: u64,fill_price: u64,remaining_amount: u64,timestamp_ms: u64,}publicstructOrderCancelledEventhascopy,drop{version: u8,order_id: ID,canceller: address,unfilled_amount: u64,reason: u8,// 0 = user, 1 = expired, 2 = admin
timestamp_ms: u64,}publicstructTransferEventhascopy,drop{version: u8,token_type: TypeName,from: address,to: address,amount: u64,memo: vector<u8>,timestamp_ms: u64,}publicstructOrderhaskey{id: UID,creator: address,amount: u64,filled: u64,price: u64,side: u8,status: u8,}/// SECURE: Event emitted only after successful state change
publicentryfuncreate_order<T>(amount: u64,price: u64,side: u8,clock: &Clock,ctx: &mutTxContext){// Validate first
assert!(amount>0,E_ZERO_AMOUNT);assert!(price>0,E_ZERO_PRICE);assert!(side<=1,E_INVALID_SIDE);// Create order
letorder=Order{id: object::new(ctx),creator: tx_context::sender(ctx),amount,filled: 0,price,side,status: 0,// Active
};letorder_id=object::id(&order);// Transfer order to creator
transfer::transfer(order,tx_context::sender(ctx));// Emit event AFTER successful creation
event::emit(OrderCreatedEvent{version: EVENT_VERSION,order_id,creator: tx_context::sender(ctx),token_type: type_name::get<T>(),amount,price,side,timestamp_ms: clock::timestamp_ms(clock),});}/// SECURE: Event includes before/after state
publicentryfuncancel_order(order: Order,clock: &Clock,ctx: &mutTxContext){assert!(tx_context::sender(ctx)==order.creator,E_NOT_CREATOR);letunfilled=order.amount-order.filled;letorder_id=object::id(&order);// Clean up order
letOrder{id,creator: _,amount: _,filled: _,price: _,side: _,status: _}=order;object::delete(id);// Emit cancellation event
event::emit(OrderCancelledEvent{version: EVENT_VERSION,order_id,canceller: tx_context::sender(ctx),unfilled_amount: unfilled,reason: 0,// User-initiated
timestamp_ms: clock::timestamp_ms(clock),});}/// SECURE: Fill event includes all relevant details
publicentryfunfill_order(order: &mutOrder,fill_amount: u64,clock: &Clock,ctx: &mutTxContext){assert!(order.status==0,E_ORDER_NOT_ACTIVE);assert!(fill_amount<=order.amount-order.filled,E_OVERFILL);order.filled=order.filled+fill_amount;if(order.filled==order.amount){order.status=1;// Filled
};event::emit(OrderFilledEvent{version: EVENT_VERSION,order_id: object::id(order),filler: tx_context::sender(ctx),fill_amount,fill_price: order.price,remaining_amount: order.amount-order.filled,timestamp_ms: clock::timestamp_ms(clock),});}}
Event Design Guidelines
1. Event Naming Convention
/// Use past tense — events describe completed actions
publicstructOrderCreatedhascopy,drop{}// Good
publicstructCreateOrderhascopy,drop{}// Bad
/// Specific names for specific operations
publicstructTokensMintedhascopy,drop{}publicstructTokensBurnedhascopy,drop{}publicstructTokensTransferredhascopy,drop{}
2. Essential Event Fields
publicstructCompleteEventhascopy,drop{// Version for forward compatibility
version: u8,// WHO
actor: address,// WHAT
object_id: ID,operation_type: u8,// WHEN
timestamp_ms: u64,// CONTEXT
old_value: u64,new_value: u64,// OPTIONAL: Additional context
metadata: vector<u8>,}
3. Event Ordering
publicentryfuncomplex_operation(...){// 1. Validate inputs
assert!(valid_input,E_INVALID);// 2. Perform state changes
state.value=new_value;// 3. Emit events AFTER success
event::emit(StateChangedEvent{...});}
// Bad: Emit before potential failure
event::emit(ActionEvent{...});assert!(condition,E_FAILED);// If this fails, event was false
// Good: Emit after success
assert!(condition,E_FAILED);// ... all operations succeed
event::emit(ActionEvent{...});
Testing Checklist
Every state-changing function emits an event
Events include who, what, when, and relevant context
No events are emitted before potential abort points
Different operations emit distinguishable events
Events are versioned for future compatibility
Off-chain systems can reconstruct state from events