Documentation Index
8 min readChapter 13

13. Events and Logging

Events in Quorlin provide a lightweight, gas-efficient mechanism for smart contracts to communicate state changes to the outside world. When a contract emits an event, the Kortana Virtual Machine (KVM) writes log records directly into the execution receipt of the transaction. Off-chain applications, frontend software, and indexing services can monitor these logs to detect state transitions in real time without incurring the heavy gas costs associated with permanent state storage (map or storage fields).

This chapter details the syntax, semantic rules, ABI serialization, and low-level compilation dynamics of events and logging in Quorlin.


13.1 Event Declarations

An event in Quorlin is declared at the contract level using the event keyword. Event declarations define a typed schema consisting of an event name and zero or more named parameters.

Syntax

event EventName(type1 parameter1, type2 parameter2, ...);

Supported Parameter Types

Event parameters support Quorlin's native core types. The table below illustrates how native Quorlin types correspond to their internal AST representations and canonical Ethereum-compatible ABI type names generated during compilation:

Quorlin TypeInternal AST Type (Type)Canonical ABI Type (abi_type_name)Description
numberType::U256uint256256-bit unsigned integer
addressType::Addressaddress160-bit (20-byte) address string/hash
truthType::BoolboolBoolean value (yes / no)
textType::TextstringUTF-8 encoded text string

Declaring Indexed Parameters

Parameters within an event can be marked with the indexed keyword. When a parameter is marked as indexed, its value is stored as a searchable topic in the log entry rather than in the raw data payload.

event Transfer(address indexed from, address indexed to, number amount);

In this example:

  • from and to are marked as indexed. Their values are hashed (if dynamic) or placed directly into the log's topics array, enabling off-chain tools to filter logs by specific sender or receiver addresses.
  • amount is unindexed. Its value is encoded sequentially into the data bytes payload of the log entry.

13.2 The emit Statement

Events are dispatched at runtime using the emit keyword inside contract functions.

Basic Usage

emit Transfer(caller, recipient, amount);

When an emit statement executes, the compiler evaluates all argument expressions from left to right, packages the indexed and unindexed arguments, and calls the internal KVM logging opcode.

Mutability Context Rules

The Quorlin semantic analyzer enforces strict state mutability rules regarding where events can be emitted:

  1. writes Functions: May freely execute emit statements, as state mutations and execution logs alter the transaction state and state receipt.
  2. constructor: May execute emit statements to log initial state allocations upon contract deployment.
  3. reads Functions: Cannot execute emit statements. Attempting to emit an event inside a reads function results in a compilation error during semantic analysis, as read-only calls must not produce log outputs or alter state receipts.
contract Vault { number totalDeposited; map<address, number> deposits; event Deposit(address indexed user, number amount); // VALID: Function marked as 'writes' can emit events writes truth deposit(number amount) { deposits[caller] = deposits[caller] + amount; totalDeposited = totalDeposited + amount; emit Deposit(caller, amount); return yes; } // INVALID: Function marked as 'reads' cannot emit events reads number getDeposit(address user) { // emit Deposit(user, 0); // Semantic Error: cannot emit in a read-only context return deposits[user]; } }

13.3 ABI Serialization for Events

Tools operating outside the Kortana node (such as dApp frameworks, block explorers, and client libraries) rely on JSON ABI definitions to decode event logs. Quorlin automatically generates standard Ethereum-compatible JSON ABI metadata for all declared events using quorlin/abi.cpp.

Internal Mapping Logic

During ABI generation, the compiler translates Quorlin types into standard ABI strings using abi_type_name(Type) and emits parameter definitions via parameter_json.

The underlying emission logic from quorlin/abi.cpp formats event parameters as follows:

std::string parameter_json(std::string_view name, Type type, bool indexed, bool with_indexed) { std::string out = "{\"name\":" + quoted(name) + ",\"type\":" + quoted(abi_type_name(type)) + ",\"internalType\":" + quoted(type_name(type)); if (with_indexed) out += ",\"indexed\":" + std::string{indexed ? "true" : "false"}; return out + "}"; }

JSON ABI Representation

Given the Quorlin event declaration:

event TokenApproval(address indexed owner, address indexed spender, number value);

The Quorlin ABI generator produces the following standardized JSON fragment:

{ "anonymous": false, "inputs": [ { "name": "owner", "type": "address", "internalType": "address", "indexed": true }, { "name": "spender", "type": "address", "internalType": "address", "indexed": true }, { "name": "value", "type": "uint256", "internalType": "number", "indexed": false } ], "name": "TokenApproval", "type": "event" }

Notice that internalType preserves Quorlin's native language terminology (number), whereas type emits standard Ethereum ABI types (uint256). This ensures seamless cross-compatibility with external web3 tooling.


13.4 Execution Pipeline and KVM Codegen

Emitting an event involves all four major stages of the Quorlin compilation pipeline:

Source Code (.ql) ──► 1. Lexer ──► 2. Parser ──► 3. Semantic Analyzer ──► 4. Code Generator ──► KVM Bytecode

1. Lexical Analysis (lexer.cpp)

The lexer identifies logging-related tokens and maps them to standard token kinds:

  • "event" $\rightarrow$ TokenKind::Event
  • "emit" $\rightarrow$ TokenKind::Emit
  • "indexed" $\rightarrow$ TokenKind::Indexed

2. Parsing (parser.cpp)

The parser constructs the Abstract Syntax Tree (AST) node for EventDeclaration and EmitStatement. It asserts proper structure, checking for identifier validity, parameter lists, and correct positioning of commas and parentheses.

3. Semantic Analysis (sema.cpp)

The analyzer validates that:

  • The event being emitted exists within the scope of the current contract.
  • The number of arguments passed to emit matches the event declaration.
  • The type of each argument expression is implicitly convertible or strictly equal to the declared parameter type.
  • The statement resides within a mutability-permitted block (writes or constructor).

4. Code Generation (codegen.cpp & kvm/interpreter.cpp)

When generating bytecode for an emit statement:

  1. Topic 0 Generation: The compiler calculates the 32-byte Keccak-256 hash of the canonical event signature string (e.g., keccak256("Transfer(address,address,uint256)")). This becomes topic 0.
  2. Indexed Parameters: Up to 3 additional indexed arguments are evaluated and assigned as topics 1, 2, and 3.
  3. Data Encoding: Non-indexed arguments are evaluated, ABI-encoded into memory sequentially, and pointed to via byte offsets.
  4. Log Instruction: The generator emits the corresponding KVM logging opcode (LOG0, LOG1, LOG2, LOG3, or LOG4) based on the total number of topics.

13.5 Gas Dynamics: Storage vs. Logging

Logging is significantly cheaper than mutating contract storage slots. In the KVM gas schedule:

  • Writing or updating persistent contract storage (sstore) consumes thousands of gas units per 32-byte slot.
  • Logging state changes via emit incurs a small base execution fee plus a nominal cost per byte of data emitted.

KVM Gas Breakdown for Logging

Total Event Gas = Base Log Opcode Cost + (Topic Cost × Number of Topics) + (Data Byte Cost × Data Length)

Because event logs are stored in transaction execution receipts rather than in the consensus-critical world state trie (state::WorldState), nodes do not need to keep event logs in high-speed RAM or persistent trie storage. This structural difference makes logging the ideal pattern for historical data recording, transactional notifications, and off-chain audit logs.


13.6 Complete Practical Example

The following smart contract, Marketplace.ql, demonstrates declaring, emitting, and consuming multiple events in a realistic Quorlin application.

contract Marketplace { // --- State Storage --- address owner; number itemCounter; record Item { number id; address seller; number price; truth active; } map<number, Item> items; // --- Events --- event ItemListed(number indexed itemId, address indexed seller, number price); event ItemSold(number indexed itemId, address indexed buyer, address indexed seller, number price); event ItemDelisted(number indexed itemId, address seller); // --- Constructor --- constructor { owner = caller; itemCounter = 0; } // --- Public Mutating Functions --- writes number listItem(number price) { require price > 0, "Price must be greater than zero"; itemCounter = itemCounter + 1; number newItemId = itemCounter; Item newItem; newItem.id = newItemId; newItem.seller = caller; newItem.price = price; newItem.active = yes; items[newItemId] = newItem; // Emit item listing event emit ItemListed(newItemId, caller, price); return newItemId; } writes truth buyItem(number itemId, number payment) { Item item = items[itemId]; require item.active == yes, "Item is not active"; require payment >= item.price, "Insufficient payment"; item.active = no; items[itemId] = item; // Emit item sold event with 3 indexed topics emit ItemSold(itemId, caller, item.seller, item.price); return yes; } writes truth delistItem(number itemId) { Item item = items[itemId]; require item.active == yes, "Item not active"; require item.seller == caller, "Only seller can delist"; item.active = no; items[itemId] = item; // Emit item delisted event emit ItemDelisted(itemId, caller); return yes; } // --- Public View Functions --- reads truth isItemActive(number itemId) { return items[itemId].active; } }