Skip to content

Zig Guide

Ashen contracts can be written in Zig and compiled to the Ashen VM’s RV64 freestanding target. This guide walks through the canonical contract layout, entrypoint patterns, and build flow using the Ashen Zig SDK.

  • Use the pinned toolchain. Build with the exact Zig toolchain provided by devenv (or the Docker image built from it). Validators validate the ELF output from the canonical compilers; mismatched toolchains are rejected.
  • Ashen SDK: Contracts must use contracts/ashen-sdk. Do not reimplement storage, dispatch, or syscall helpers.
  • IDL file: Every contract has a *.idl file that defines its interface.

A typical Zig contract lives under contracts/<name>/:

contracts/my_contract/
├── build.zig
├── linker.ld
├── my_contract.idl
└── src/
├── main.zig
├── abi.zig
└── my_contract.manifest.v1.json
  • build.zig targets RV64 freestanding and wires in ashen-sdk.
  • linker.ld keeps code/data at low addresses for the VM.
  • abi.zig + *.manifest.v1.json are generated from IDL.

The recommended pattern is to use the SDK dispatch helper with IDL-generated stubs:

const sdk = @import("ashen-sdk");
const abi = @import("abi.zig");
const Counter = struct {
pub fn get(self: *Counter, args: abi.GetArgs) abi.ContractResult(abi.GetResult) {
_ = self;
_ = args;
const value: u128 = sdk.storage.readU128OrDefault("count", 0);
return .{ .Ok = .{ .value = value } };
}
pub fn inc(self: *Counter, args: abi.IncArgs) abi.ContractResult(abi.IncResult) {
_ = self;
_ = args;
const value: u128 = sdk.storage.readU128OrDefault("count", 0) + 1;
_ = sdk.storage.writeU128("count", value);
return .{ .Ok = .{ .value = value } };
}
};
export fn _start(calldata_ptr: [*]const u8, calldata_len: usize) sdk.ByteSlice {
return sdk.dispatch.run(Counter, abi.dispatch, calldata_ptr, calldata_len);
}
pub const panic = sdk.panic;

Notes:

  • sdk.dispatch.run handles heap reset, calldata slicing, and error encoding.
  • The entrypoint must be named _start.
  • Always export pub const panic = sdk.panic;.

This is a minimal end-to-end example you can copy into a new contract.

Terminal window
mkdir -p contracts/counter/src

contracts/counter/counter.idl:

namespace counter;
interface Counter {
fn get() -> u128;
fn inc() -> u128;
}
Terminal window
cargo run -p idl-abi-gen -- \
--idl contracts/counter/counter.idl \
--out-dir contracts/counter/src \
--zig-stubs

contracts/counter/src/main.zig:

const sdk = @import("ashen-sdk");
const abi = @import("abi.zig");
const Counter = struct {
pub fn get(self: *Counter, args: abi.GetArgs) abi.ContractResult(abi.GetResult) {
_ = self;
_ = args;
const value: u128 = sdk.storage.readU128OrDefault("count", 0);
return .{ .Ok = .{ .value = value } };
}
pub fn inc(self: *Counter, args: abi.IncArgs) abi.ContractResult(abi.IncResult) {
_ = self;
_ = args;
const value: u128 = sdk.storage.readU128OrDefault("count", 0) + 1;
_ = sdk.storage.writeU128("count", value);
return .{ .Ok = .{ .value = value } };
}
};
export fn _start(calldata_ptr: [*]const u8, calldata_len: usize) sdk.ByteSlice {
return sdk.dispatch.run(Counter, abi.dispatch, calldata_ptr, calldata_len);
}
pub const panic = sdk.panic;
Terminal window
cd contracts/counter
zig build -Doptimize=ReleaseSmall

Output: zig-out/bin/counter

Terminal window
node contract bundle \
--elf contracts/counter/zig-out/bin/counter \
--idl contracts/counter/counter.idl \
--out ./counter.bundle
node contract deploy --bundle ./counter.bundle --key $ASHEN_PRIVATE_KEY --wait
Terminal window
ashen call 0xCONTRACT get '[]' --key $ASHEN_PRIVATE_KEY --wait
ashen call 0xCONTRACT inc '[]' --key $ASHEN_PRIVATE_KEY --wait

Generate Zig ABI stubs from the contract IDL:

Terminal window
cargo run -p idl-abi-gen -- \
--idl contracts/my_contract/my_contract.idl \
--out-dir contracts/my_contract/src \
--zig-stubs

This produces:

  • src/abi.zig (selectors, args/results, dispatch)
  • src/my_contract.manifest.v1.json (deploy manifest metadata)

Regenerate these files whenever you change the IDL.

Terminal window
cd contracts/my_contract
zig build -Doptimize=ReleaseSmall

Output: zig-out/bin/my_contract

Use the deployment workflow in the CLI guide:

  • Build the Zig ELF (zig build -Doptimize=ReleaseSmall)
  • Bundle with IDL (node contract bundle)
  • Deploy (node contract deploy)

See: /guides/deploying-contracts/

const balance = sdk.storage.readU128("balance") orelse 0;
_ = sdk.storage.writeU128("balance", balance + 1);
const events = sdk.events;
const Transfer = events.define("Transfer(address,address,uint256)");
Transfer.emit2(from, to, events.toTopicU128(amount), &[_]u8{});
try sdk.guards.enterNonReentrant();
defer sdk.guards.exitNonReentrant();
  • /contracts/ashen-sdk/ for the SDK API surface
  • /contracts/idl-and-abi/ for interface definitions
  • /contracts/examples/ for production-grade Zig contracts