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-programUpdate 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 boolStringVec<u8>(maps to dynamicbytes)- 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:
- Keep your external API minimal and Solidity-friendly.
- Run
cargo sails sol --idl-path ...early. - 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_casein 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.
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
exitedand stops accepting new messages
The inheritor can claim the transferred value via Mirror.transferLockedValueToInheritor().
Program Identity
Every program has two identifiers:
| Identifier | Format | Where Used |
|---|---|---|
ActorId | 256-bit hash | Inside Vara.eth runtime, for inter-program messaging |
| Mirror Address | Ethereum 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:
- Every program gets a small free compute threshold per message
- Beyond the threshold, execution time is metered at a
wvaraPerSecondrate - If the Executable Balance runs out, messages remain queued until the program is topped up
- The consumed balance is distributed to validators as rewards