Smart Contract Events & Logging

Implementing events and logging in smart contracts for transparency and off-chain monitoring.

Advanced⏱️ 50 min📚 Prerequisites: 1

Smart Contract Events & Logging

Events allow smart contracts to communicate with off-chain applications. They're essential for transparency and monitoring.

Why Events?

  • Transparency: Log all important actions
  • Off-chain Integration: DApps can listen to events
  • Indexing: Easy to query historical data
  • Debugging: Track contract execution
  • Analytics: Monitor contract usage

Event Structure

RUST
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Event {
    pub event_type: String,
    pub contract_address: String,
    pub data: EventData,
    pub block_height: u64,
    pub tx_hash: String,
    pub timestamp: u64,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum EventData {
    Transfer {
        from: String,
        to: String,
        amount: u64,
    },
    Approval {
        owner: String,
        spender: String,
        amount: u64,
    },
    Mint {
        to: String,
        amount: u64,
    },
    Burn {
        from: String,
        amount: u64,
    },
}

Event Emitter

RUST
struct EventEmitter {
    events: Vec<Event>,
}

impl EventEmitter {
    fn new() -> Self {
        EventEmitter {
            events: Vec::new(),
        }
    }
    
    fn emit(
        &mut self,
        event_type: String,
        contract_address: String,
        data: EventData,
        block_height: u64,
        tx_hash: String,
    ) {
        let event = Event {
            event_type,
            contract_address,
            data,
            block_height,
            tx_hash,
            timestamp: current_timestamp(),
        };
        
        self.events.push(event);
    }
    
    fn get_events(&self, event_type: Option<&str>) -> Vec<&Event> {
        if let Some(et) = event_type {
            self.events.iter()
                .filter(|e| e.event_type == et)
                .collect()
        } else {
            self.events.iter().collect()
        }
    }
}

Token Contract with Events

RUST
struct TokenContract {
    name: String,
    symbol: String,
    total_supply: u64,
    balances: HashMap<String, u64>,
    event_emitter: EventEmitter,
    contract_address: String,
}

impl TokenContract {
    fn transfer(
        &mut self,
        from: String,
        to: String,
        amount: u64,
        tx_hash: String,
        block_height: u64,
    ) -> Result<(), String> {
        // Validate and transfer
        let balance = self.balances.get(&from).copied().unwrap_or(0);
        if balance < amount {
            return Err(String::from("Insufficient balance"));
        }
        
        *self.balances.entry(from.clone()).or_insert(0) -= amount;
        *self.balances.entry(to.clone()).or_insert(0) += amount;
        
        // Emit Transfer event
        self.event_emitter.emit(
            String::from("Transfer"),
            self.contract_address.clone(),
            EventData::Transfer {
                from,
                to,
                amount,
            },
            block_height,
            tx_hash,
        );
        
        Ok(())
    }
    
    fn mint(
        &mut self,
        to: String,
        amount: u64,
        tx_hash: String,
        block_height: u64,
    ) {
        *self.balances.entry(to.clone()).or_insert(0) += amount;
        self.total_supply += amount;
        
        // Emit Mint event
        self.event_emitter.emit(
            String::from("Mint"),
            self.contract_address.clone(),
            EventData::Mint { to, amount },
            block_height,
            tx_hash,
        );
    }
}

Event Indexing

RUST
struct EventIndex {
    by_type: HashMap<String, Vec<usize>>,
    by_contract: HashMap<String, Vec<usize>>,
    by_address: HashMap<String, Vec<usize>>,
}

impl EventIndex {
    fn new() -> Self {
        EventIndex {
            by_type: HashMap::new(),
            by_contract: HashMap::new(),
            by_address: HashMap::new(),
        }
    }
    
    fn index_event(&mut self, event: &Event, index: usize) {
        // Index by type
        self.by_type
            .entry(event.event_type.clone())
            .or_insert_with(Vec::new)
            .push(index);
        
        // Index by contract
        self.by_contract
            .entry(event.contract_address.clone())
            .or_insert_with(Vec::new)
            .push(index);
        
        // Index by address (from Transfer events)
        if let EventData::Transfer { from, to, .. } = &event.data {
            self.by_address
                .entry(from.clone())
                .or_insert_with(Vec::new)
                .push(index);
            self.by_address
                .entry(to.clone())
                .or_insert_with(Vec::new)
                .push(index);
        }
    }
    
    fn query_by_type(&self, event_type: &str) -> Vec<usize> {
        self.by_type.get(event_type).cloned().unwrap_or_default()
    }
    
    fn query_by_contract(&self, contract: &str) -> Vec<usize> {
        self.by_contract.get(contract).cloned().unwrap_or_default()
    }
    
    fn query_by_address(&self, address: &str) -> Vec<usize> {
        self.by_address.get(address).cloned().unwrap_or_default()
    }
}

Event Query Interface

RUST
struct EventQuery {
    events: Vec<Event>,
    index: EventIndex,
}

impl EventQuery {
    fn new() -> Self {
        EventQuery {
            events: Vec::new(),
            index: EventIndex::new(),
        }
    }
    
    fn add_event(&mut self, event: Event) {
        let index = self.events.len();
        self.events.push(event.clone());
        self.index.index_event(&event, index);
    }
    
    fn get_transfers(&self, address: &str) -> Vec<&Event> {
        let indices = self.index.query_by_address(address);
        indices.iter()
            .filter_map(|&i| self.events.get(i))
            .filter(|e| matches!(e.data, EventData::Transfer { .. }))
            .collect()
    }
    
    fn get_contract_events(&self, contract: &str, event_type: Option<&str>) -> Vec<&Event> {
        let indices = self.index.query_by_contract(contract);
        indices.iter()
            .filter_map(|&i| self.events.get(i))
            .filter(|e| {
                if let Some(et) = event_type {
                    e.event_type == et
                } else {
                    true
                }
            })
            .collect()
    }
}

Best Practices

  1. Emit for All State Changes: Log important actions
  2. Include Relevant Data: All data needed for off-chain processing
  3. Use Consistent Naming: Standard event names
  4. Index Events: For efficient querying
  5. Gas Consideration: Events cost gas, but are worth it
  6. Event Standards: Follow platform-specific standards

Event Standards

ERC-20 Events

RUST
// Transfer event
EventData::Transfer {
    from: String,
    to: String,
    amount: u64,
}

// Approval event
EventData::Approval {
    owner: String,
    spender: String,
    amount: u64,
}

ERC-721 Events

RUST
// Transfer event
EventData::Transfer {
    from: String,
    to: String,
    token_id: u64,
}

// Approval event
EventData::Approval {
    owner: String,
    approved: String,
    token_id: u64,
}

Event Monitoring

RUST
struct EventMonitor {
    subscribers: HashMap<String, Vec<Box<dyn Fn(&Event)>>>,
}

impl EventMonitor {
    fn new() -> Self {
        EventMonitor {
            subscribers: HashMap::new(),
        }
    }
    
    fn subscribe(&mut self, event_type: String, callback: Box<dyn Fn(&Event)>) {
        self.subscribers
            .entry(event_type)
            .or_insert_with(Vec::new)
            .push(callback);
    }
    
    fn notify(&self, event: &Event) {
        if let Some(callbacks) = self.subscribers.get(&event.event_type) {
            for callback in callbacks {
                callback(event);
            }
        }
    }
}

Use Cases

  • DApp Integration: Frontend listens to events
  • Analytics: Track contract usage
  • Notifications: Alert users of important events
  • Indexing: Build searchable event databases
  • Compliance: Audit trail of all actions

Code Examples

Event Emission

Emitting events from a contract

RUST
use std::collections::HashMap;

#[derive(Clone, Debug)]
struct Event {
    event_type: String,
    data: String,
    block_height: u64,
}

struct EventEmitter {
    events: Vec<Event>,
}

impl EventEmitter {
    fn new() -> Self {
        EventEmitter { events: Vec::new() }
    }
    
    fn emit(&mut self, event_type: String, data: String, block_height: u64) {
        self.events.push(Event {
            event_type,
            data,
            block_height,
        });
    }
    
    fn get_events(&self, event_type: Option<&str>) -> Vec<&Event> {
        if let Some(et) = event_type {
            self.events.iter()
                .filter(|e| e.event_type == et)
                .collect()
        } else {
            self.events.iter().collect()
        }
    }
}

struct TokenContract {
    balances: HashMap<String, u64>,
    emitter: EventEmitter,
}

impl TokenContract {
    fn new() -> Self {
        TokenContract {
            balances: HashMap::new(),
            emitter: EventEmitter::new(),
        }
    }
    
    fn transfer(
        &mut self,
        from: String,
        to: String,
        amount: u64,
        block_height: u64,
    ) -> Result<(), String> {
        let balance = self.balances.get(&from).copied().unwrap_or(0);
        if balance < amount {
            return Err(String::from("Insufficient balance"));
        }
        
        *self.balances.entry(from.clone()).or_insert(0) -= amount;
        *self.balances.entry(to.clone()).or_insert(0) += amount;
        
        // Emit Transfer event
        let event_data = format!("from: {}, to: {}, amount: {}", from, to, amount);
        self.emitter.emit(
            String::from("Transfer"),
            event_data,
            block_height,
        );
        
        Ok(())
    }
    
    fn get_transfer_events(&self) -> Vec<&Event> {
        self.emitter.get_events(Some("Transfer"))
    }
}

fn main() {
    let mut contract = TokenContract::new();
    contract.balances.insert(String::from("alice"), 1000);
    
    contract.transfer(
        String::from("alice"),
        String::from("bob"),
        500,
        100,
    ).unwrap();
    
    for event in contract.get_transfer_events() {
        println!("Event: {} - {}", event.event_type, event.data);
    }
}

Explanation:

Events are emitted when important actions occur. They provide a log of all contract activities that can be queried off-chain.

Exercises

Add Events to Contract

Add event emission to a contract function!

Easy

Starter Code:

RUST
struct Contract {
    value: u64,
    events: Vec<String>,
}

impl Contract {
    fn set_value(&mut self, new_value: u64) {
        self.value = new_value;
        // Emit event here
    }
}

fn main() {
    let mut contract = Contract {
        value: 0,
        events: Vec::new(),
    };
    
    contract.set_value(100);
    println!("Events: {:?}", contract.events);
}