Skip to content

Rust Guide

Ashen supports Rust contracts for developers who want tight control, no_std performance, and the contract-sdk storage/ABI helpers. This guide shows the canonical layout, entrypoint wiring, and build flow.

  • Pinned toolchain: Use the devenv toolchain (or its Docker image). The network validates ELF output from the canonical compilers.
  • RISC-V target: riscv64imac-unknown-none-elf
  • No std: Contracts are no_std + alloc.

A typical Rust contract looks like contracts/sft_v1:

contracts/my_contract/
├── Cargo.toml
├── my_contract.idl
└── src/
├── main.rs
├── lib.rs
└── abi.rs
  • src/main.rs defines the entrypoint.
  • src/lib.rs contains the contract logic and ABI implementation.
  • src/abi.rs is generated from the IDL (do not hand-edit).

Generate Rust ABI bindings from the IDL:

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

This creates src/abi.rs with:

  • selector constants
  • request/response structs
  • a dispatch function
  • a trait you implement for your contract

IDL (contracts/counter/counter.idl):

namespace counter;
interface Counter {
fn get() -> u128;
fn inc() -> u128;
}

Cargo.toml (minimal deps):

[dependencies]
borsh = { version = "1.5.5", default-features = false, features = ["derive"] }
contract-rt = { path = "../../crates/contract-rt" }
contract-sdk = { path = "../../crates/contract-sdk" }

lib.rs (logic + ABI implementation):

#![cfg_attr(target_arch = "riscv64", no_std)]
extern crate alloc;
mod abi;
use contract_sdk::{define_storage, ContractErrorV1, Item};
define_storage! {
namespace: "counter",
pub struct Storage {
pub value: Item<u128>,
}
}
pub struct Contract {
storage: Storage,
}
impl Contract {
pub fn new() -> Result<Self, ContractErrorV1> {
Ok(Self {
storage: Storage::new()?,
})
}
}
impl abi::Counter for Contract {
fn get(&mut self) -> Result<abi::U128Result, ContractErrorV1> {
let value = self.storage.value.get()?.unwrap_or(0);
Ok(abi::U128Result { value })
}
fn inc(&mut self) -> Result<abi::U128Result, ContractErrorV1> {
let value = self.storage.value.get()?.unwrap_or(0) + 1;
self.storage.value.set(&value)?;
Ok(abi::U128Result { value })
}
}

main.rs (entrypoint):

#![cfg_attr(target_arch = "riscv64", no_std)]
#![cfg_attr(target_arch = "riscv64", no_main)]
// Replace `counter` with your library crate name.
contract_rt::entrypoint_v1!(counter::Contract, counter::abi::dispatch);
Terminal window
node contract build --manifest-path contracts/counter/Cargo.toml

Output: target/riscv64imac-unknown-none-elf/release/counter

Terminal window
node contract bundle \
--elf target/riscv64imac-unknown-none-elf/release/counter \
--idl contracts/counter/counter.idl \
--out ./counter.bundle
node contract deploy --bundle ./counter.bundle --key $ASHEN_PRIVATE_KEY --wait

The Rust SDK provides typed storage helpers in contract_sdk::storage:

TypeUseNotes
Item<T>Single valueget/set/exists/clear
Map<K, V>Key-value mapget/set/remove/contains_key
Set<T>Membership setBacked by Map<T, u8>
StorageVec<T>Append-only vectorTracks length + element keys
CountedMap<K, V>Map + O(1) sizeMaintains entry count
IndexedList<T>List + index mapSwap-remove deletions
LazyItem/Map/SetCached wrappersAvoid redundant reads

Example:

use contract_sdk::{define_storage, Item, Map, Set, StorageVec};
define_storage! {
namespace: "example",
pub struct Storage {
pub owner: Item<[u8; 32]>,
pub balances: Map<[u8; 32], u128>,
pub allowlist: Set<[u8; 32]>,
pub history: StorageVec<[u8; 32]>,
}
}

Access lists are hints that help the VM avoid cold storage penalties. The SDK exposes declare_access and prefetch on storage types:

// Map access hint and prefetch
storage.balances.declare_access(&caller)?;
storage.balances.prefetch(&caller)?;
// StorageVec access hint
storage.history.declare_len_access()?;
storage.history.prefetch_element(0)?;

Use these before hot loops or when you know the keys you will touch.

Use typed error codes for deterministic failures:

use contract_sdk::{ContractErrorV1, require};
const ERR_UNAUTHORIZED: u32 = 1;
const ERR_ZERO_AMOUNT: u32 = 2;
require!(amount > 0, ERR_ZERO_AMOUNT);
require!(caller == owner, ERR_UNAUTHORIZED);
return Err(ContractErrorV1::code(ERR_UNAUTHORIZED));

For manual ABI paths, you can return raw error payloads:

use contract_sdk::{revert_code, revert_other};
let bytes = revert_code(ERR_UNAUTHORIZED);
let bytes = revert_other("bad input".to_string());

For deploys/calls, you can point ASHEN_PRIVATE_KEY at:

  • a key file: export ASHEN_PRIVATE_KEY=@./dev.key.json
  • a keystore handle: export ASHEN_PRIVATE_KEY=keystore:my-key

Create a keystore key:

Terminal window
ashen keystore init
ashen keystore add --label my-key

This shows a small contract with:

  • owner-only mint
  • balances map
  • event emission
  • access list hints
use contract_sdk::{define_storage, ContractErrorV1, Host, Item, Map, Emittable, require};
use borsh::{BorshDeserialize, BorshSerialize};
const ERR_UNAUTHORIZED: u32 = 1;
#[derive(BorshSerialize, BorshDeserialize)]
pub struct MintEvent {
pub to: [u8; 32],
pub amount: u128,
}
define_storage! {
namespace: "faucet",
pub struct Storage {
pub owner: Item<[u8; 32]>,
pub balances: Map<[u8; 32], u128>,
}
}
pub struct Contract { storage: Storage }
impl Contract {
pub fn new() -> Result<Self, ContractErrorV1> {
Ok(Self { storage: Storage::new()? })
}
fn require_owner(&self, caller: [u8; 32]) -> Result<(), ContractErrorV1> {
let owner = self.storage.owner.get()?.unwrap_or([0u8; 32]);
require!(caller == owner, ERR_UNAUTHORIZED);
Ok(())
}
}
// ABI methods would live here (generated in abi.rs)
impl Contract {
pub fn mint(&mut self, to: [u8; 32], amount: u128) -> Result<(), ContractErrorV1> {
let caller = Host::caller_key()?;
self.require_owner(caller)?;
self.storage.balances.declare_access(&to)?;
self.storage.balances.prefetch(&to)?;
let balance = self.storage.balances.get(&to)?.unwrap_or(0) + amount;
self.storage.balances.set(&to, &balance)?;
MintEvent { to, amount }.emit(b"Mint")?;
Ok(())
}
}

This example shows:

  • building calldata for another contract
  • Host::call with typed decode
  • emitting a log with two indexed topics
extern crate alloc;
use alloc::vec::Vec;
use contract_sdk::{Address, ContractErrorV1, Host, HostError};
use borsh::BorshSerialize;
// Simple event payload
#[derive(BorshSerialize)]
pub struct SwapEvent {
pub sender: [u8; 32],
pub amount_in: u128,
pub amount_out: u128,
}
pub fn swap_through_router(
router: Address,
sender: [u8; 32],
amount_in: u128,
) -> Result<u128, ContractErrorV1> {
// ABI selector + args (example; use IDL-generated helpers when available)
let mut calldata = Vec::new();
calldata.extend_from_slice(&0x1234_5678u32.to_be_bytes()); // selector
calldata.extend_from_slice(&borsh::to_vec(&amount_in).unwrap());
// Call router contract
let amount_out: u128 = Host::call(&router, 0, 500_000, &calldata)
.map_err(|e| e.into_contract_error())?;
// Emit event with two topics: event sig + sender
let mut topics = [[0u8; 32]; 2];
topics[0] = Host::blake3(b"Swap(address,uint128,uint128)")?;
topics[1] = sender;
let event = SwapEvent {
sender,
amount_in,
amount_out,
};
let data = borsh::to_vec(&event).map_err(|_| HostError::BorshEncode)?;
Host::emit_log(&topics, &data)?;
Ok(amount_out)
}

Notes:

  • Prefer IDL-generated helpers instead of manual selector encoding when possible.
  • Host::call decodes Result<T, ContractErrorV1> and surfaces reverts as CallError.
  • /contracts/idl-and-abi/ for interface definitions
  • /contracts/examples/ for production Rust + Zig contracts
  • /reference/sdk/ for SDK API details