Skip to main content

Contract

The Contract module provides a typed abstraction for interacting with deployed Ethereum smart contracts from Zig. It combines ABI encoding/decoding with provider calls into a clean, type-safe interface.

Overview

Contract instances provide four main interfaces:
InterfaceDescriptionProvider Method
readView/pure function callseth_call
writeState-changing transactionseth_sendTransaction
estimateGasGas estimation for writeseth_estimateGas
eventsEvent iterationeth_getLogs

Quick Start

const std = @import("std");
const voltaire = @import("voltaire");
const Contract = voltaire.Contract;
const Provider = voltaire.Provider;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Initialize provider
    var provider = try Provider.init(allocator, "https://eth.llamarpc.com");
    defer provider.deinit();

    // Create contract instance
    const usdc = Contract.init(allocator, .{
        .address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        .abi = @embedFile("erc20.abi.json"),
        .provider = provider,
    });

    // Read balance
    const balance = try usdc.read.balanceOf("0x742d35...");
    defer allocator.free(balance);
    std.debug.print("Balance: {}\n", .{balance});

    // Transfer tokens
    const tx_hash = try usdc.write.transfer("0x742d35...", 1000);
    std.debug.print("Transaction: {s}\n", .{tx_hash});

    // Estimate gas
    const gas = try usdc.estimateGas.transfer("0x742d35...", 1000);
    std.debug.print("Gas estimate: {}\n", .{gas});

    // Iterate events
    var events = try usdc.events.Transfer(.{ .from = "0x742d35..." });
    defer events.deinit();

    while (try events.next()) |log| {
        std.debug.print("{s} -> {s}: {}\n", .{
            log.args.from,
            log.args.to,
            log.args.value,
        });
    }
}

API Reference

Contract.init

pub fn init(allocator: std.mem.Allocator, options: ContractOptions) Contract
Creates a typed contract instance. Parameters:
  • allocator - Memory allocator for dynamic allocations
  • options - Contract configuration:
    • .address - Contract address (hex string)
    • .abi - Contract ABI (JSON bytes, typically from @embedFile)
    • .provider - Initialized provider instance
Returns: Contract instance with read, write, estimateGas, and events interfaces.

Read Methods

Execute view/pure functions:
// Single return value
const balance = try usdc.read.balanceOf(address);
defer allocator.free(balance);

// Multiple return values
const reserves = try pair.read.getReserves();
defer allocator.free(reserves);
std.debug.print("Reserve0: {}, Reserve1: {}\n", .{ reserves[0], reserves[1] });

Write Methods

Send state-changing transactions:
// Basic transfer
const tx_hash = try usdc.write.transfer(to, amount);

// With options
const tx_hash = try usdc.write.transferWithOptions(to, amount, .{
    .gas = 100000,
    .max_fee_per_gas = 30_000_000_000,
    .max_priority_fee_per_gas = 2_000_000_000,
});

Gas Estimation

Estimate gas before sending:
const gas = try usdc.estimateGas.transfer(to, amount);

// Add buffer and use in write
const tx_hash = try usdc.write.transferWithOptions(to, amount, .{
    .gas = gas * 120 / 100, // 20% buffer
});

Events

Iterate over contract events:
// Create event iterator with filter
var events = try usdc.events.Transfer(.{
    .from = sender_address,
    .from_block = 18000000,
});
defer events.deinit();

// Process events
while (try events.next()) |log| {
    std.debug.print("Transfer: {s} -> {s}: {}\n", .{
        log.args.from,
        log.args.to,
        log.args.value,
    });
}

Manual Encoding

Access the ABI directly for manual encoding:
// Encode calldata
const calldata = try usdc.abi.encode(allocator, "transfer", .{ to, amount });
defer allocator.free(calldata);

// Decode return data
const decoded = try usdc.abi.decode(allocator, "balanceOf", return_data);
defer allocator.free(decoded);

Memory Management

Zig requires explicit memory management:
// Results that allocate memory must be freed
const balance = try usdc.read.balanceOf(address);
defer allocator.free(balance);

// Event iterators must be deinitialized
var events = try usdc.events.Transfer(.{});
defer events.deinit();

// Write methods return stack-allocated hashes (no free needed)
const tx_hash = try usdc.write.transfer(to, amount);

Error Handling

Handle contract errors with Zig’s error unions:
const balance = usdc.read.balanceOf(address) catch |err| switch (err) {
    error.ContractReverted => {
        std.debug.print("Contract reverted\n", .{});
        return;
    },
    error.NetworkError => {
        std.debug.print("Network error\n", .{});
        return;
    },
    else => return err,
};

Comptime ABI

For maximum performance, parse ABIs at compile time:
const USDC = Contract.comptime_init(
    "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    @embedFile("erc20.abi.json"),
);

pub fn main() !void {
    var provider = try Provider.init(allocator, rpc_url);
    defer provider.deinit();

    const usdc = USDC.connect(provider);

    // Methods are generated at comptime with full type safety
    const balance = try usdc.read.balanceOf(address);
}