Build

Program Basics

Core concepts for building Vara.eth programs — setup, lifecycle, and identity.

Program Basics

Vara.eth programs are Gear programs that run on Ethereum. If you're familiar with Gear Protocol or Vara Network, you already know how to build for Vara.eth.

This page covers the essentials. For detailed program development, see the Sails Framework and Gear documentation.

Vara.eth vs Standard Gear Programs

The runtime, actor model, and WASM execution are identical. The only difference is the deployment target.

Project Setup

Create a new program:

cargo sails new my-program
cd my-program

Update Cargo.toml:

[dependencies]
sails-rs = { version = "0.10.1", features = ["ethexe"] }
parity-scale-codec = { version = "3", default-features = false }
scale-info = { version = "2", default-features = false }

The ethexe feature enables Ethereum-specific functionality — event emission to L1, Mirror contract integration, and settlement flows.

parity-scale-codec and scale-info are needed if your program defines custom types for encoding/decoding messages.

Sails Framework | Quick Start

Minimal Example

Here's a complete counter program:

#![no_std]

use sails_rs::prelude::*;

static mut COUNTER: i64 = 0;

#[derive(Default)]
pub struct CounterService;

#[service]
impl CounterService {
    pub fn increment(&mut self) -> i64 {
        unsafe {
            COUNTER += 1;
            COUNTER
        }
    }

    pub fn get(&self) -> i64 {
        unsafe { COUNTER }
    }
}

#[derive(Default)]
pub struct CounterProgram;

#[program]
impl CounterProgram {
    pub fn init() -> Self {
        Self
    }

    pub fn counter(&self) -> CounterService {
        CounterService
    }
}

Custom Data Types

If you use custom structures in messages or state, derive encoding traits:

use parity_scale_codec::{Decode, Encode};
use scale_info::TypeInfo;

#[derive(Encode, Decode, TypeInfo)]
pub struct MyData {
    pub value: u64,
    pub owner: ActorId,
}

Type Restrictions

When you plan to generate a Solidity interface (cargo sails sol) and use "Read/Write as Proxy" on Etherscan, design your public API with Solidity-compatible types in mind.

Safe baseline for ABI-facing methods:

  • Integers: u8, u16, u32, u64, u128, and signed counterparts
  • bool
  • String
  • Vec<u8> (maps to dynamic bytes)
  • Fixed-size byte arrays ([u8; 32] etc.)
  • Structs composed only of supported fields
  • Arrays/vectors of supported element types

Types that commonly cause ABI friction:

  • Rust-specific collections (HashMap, BTreeMap, complex nested containers)
  • Deeply nested enums/tuples and heavily generic public signatures
  • Program-internal types exposed directly in ABI-facing methods

ActorId vs address

ActorId is 32 bytes, while Ethereum address is 20 bytes. In Gear primitives, ActorId::to_address_lossy() converts to H160 with potential loss of the first 12 bytes. The reverse direction (H160 -> ActorId) is represented with zero-padding in the leading 12 bytes. If a method is intended for Solidity wallets/contracts, prefer address-oriented parameters in the ABI-facing surface, or document explicit conversion rules.

Best practice:

  1. Keep your external API minimal and Solidity-friendly.
  2. Run cargo sails sol --idl-path ... early.
  3. Treat generator output as the source of truth: if a type generates awkward Solidity, simplify the Rust signature before deployment.

Reserved Words

When exposing methods through generated Solidity interfaces, avoid names that conflict with Solidity language keywords or EVM special function names.

Avoid using as public method/service names:

  • Solidity keywords like contract, library, interface, function, mapping, event, error
  • Special function names like constructor, receive, fallback

Name collisions in explorers

Even when compilation succeeds, naming your API too close to low-level Ethereum terms can make Etherscan proxy views confusing. Prefer explicit domain names like initCounter, joinMember, getProfile instead of ambiguous names.

Practical naming rules:

  • Use snake_case in Rust, let generated clients map naming as needed.
  • Keep constructor/init semantics explicit (init, init_<domain>).
  • Avoid reusing generic names that may look like transport/runtime internals (send, call, execute, state).

Program Lifecycle

Upload Code → Validate → Create Instance → Top Up → Initialize → Active → (Exit)
   (blob)     (executors)  (Mirror deploy)   (wVARA)   (first msg)  (processing)

Upload

Developer uploads compiled WASM code to Ethereum as an EIP-4844 blob and calls requestCodeValidation(codeId) on the Router.

Validate

Executors fetch the blob, verify the WASM is valid, and include a CodeCommitment in the next batch. If valid, the codeId is marked as approved.

Create Instance

Anyone can deploy a program from a validated codeId by calling createProgram(...) on the Router. This deploys a Mirror contract on Ethereum.

The program's address is deterministic: address = keccak256(codeId, salt).

Top Up

Fund the program's Executable Balance with wVARA. Without balance, the program cannot execute messages.

Funding Executable Balance

Initialize

The first message to a program triggers its constructor (init). Until initialization completes, the program cannot process regular messages.

Initializer Restriction

Only the address that created the program can send the first message.

Active

The program processes messages from its queue in strict (blockNumber, txIndex, logIndex) order. Each processed message produces a StateTransition that gets committed to Ethereum via the Router.

Exit (Optional)

A program can terminate itself by calling msg::exit(). On exit:

  • The program specifies an inheritor address where remaining balances are transferred
  • Executable Balance and remaining program value are transferred to the inheritor
  • The Mirror contract is marked as exited and stops accepting new messages

The inheritor can claim the transferred value via Mirror.transferLockedValueToInheritor().

Program Identity

Every program has two identifiers:

IdentifierFormatWhere Used
ActorId256-bit hashInside Vara.eth runtime, for inter-program messaging
Mirror AddressEthereum address (20 bytes)On Ethereum L1, for user interaction

Both are derived deterministically from (codeId, salt).

Execution Cost Model

Programs don't pay per-message. Instead, each program has an Executable Balance in wVARA:

  1. Every program gets a small free compute threshold per message
  2. Beyond the threshold, execution time is metered at a wvaraPerSecond rate
  3. If the Executable Balance runs out, messages remain queued until the program is topped up
  4. The consumed balance is distributed to validators as rewards

Reverse Gas Model

On this page