Ethereum → Vara App Guide
Introduction
This article describes the flow for building a cross-chain application from Ethereum to Vara. The guide demonstrates how to design an event-based “ping-pong” dApp, where the message originates on Ethereum and is securely delivered and processed on Vara.
Unlike the reverse direction (Vara → Ethereum), where ZK-proofs are used to guarantee trustless delivery, the Ethereum → Vara route is based on a Light Client mechanism. Here, there are no ZK-proofs on the Ethereum side. Instead, trust is achieved through on-chain light client validation of Ethereum’s consensus — specifically, through the beacon chain and its finalized checkpoints.
In this design, event logs on Ethereum serve as the source of truth. Applications emit events as part of contract execution. These events are included in Ethereum's canonical chain history (the beacon chain), and can be cryptographically proven by anyone — but only after finality is reached on Ethereum.
A permissionless relayer observes the beacon chain, builds receipt proofs for the relevant events, and submits them to Vara. On Vara, a light client validates that the proof corresponds to an actually finalized event on Ethereum, and only then is the custom Vara program triggered.
This mechanism is fundamentally event-based and asynchronous. Application developers are not required to build the bridge or proof logic themselves — only to emit and handle events, while the trustless bridge and relayers manage the cross-chain transport.
For simplification, developers can now use the ready-made GearJS Bridge library, which performs most of the heavy lifting: proof generation, slot synchronization, and submission to Vara. This allows application developers to focus purely on emitting events on Ethereum and writing the receiving logic on Vara.
High-Level Architecture
Complete source code on GitHub
The Ethereum → Vara cross-chain application consists of three core components:
-
Ethereum Smart Contract (Ping Emitter)
Minimal contractEthPingerwithping()that emitsPingFromEthereum(address indexed from). Location:eth-vara-pinger-eth/ -
Relayer Service (Off-chain)
- Connects to Ethereum and Vara.
- Subscribes to
PingFromEthereum(Ethereum). - For each event: calls
relayEthToVara({ transactionHash, ... }). - Optionally sets
wait: trueto block until the required checkpoint is finalized. Location:eth-vara-relayer-node/
-
Vara Program (Receiver)
- Checkpoint Light Client — tracks finalized Ethereum checkpoints.
- Historical Proxy — validates proofs and forwards messages.
- Ping Receiver — your program exposing
SubmitReceipt(slot, transaction_index, receipt_rlp). Location:eth-vara-receiver-vara/
Implementation Details
Ethereum Side
A minimal contract that emits the event:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract EthPinger {
event PingFromEthereum(address indexed from);
function ping() external {
emit PingFromEthereum(msg.sender);
}
}Relayer Side (using the library)
The relayer’s job can be summarized in four steps: connect to Ethereum, connect to Vara, listen for events, and relay them using the library.
Create Ethereum clients (HTTP + WS)
// via viem
export const ethereumPublicClient = createPublicClient({ ... });
export const ethereumWalletClient = createWalletClient({ ... });
// via ethers (for WS subscriptions)
const ethWs = new ethers.WebSocketProvider(ETHEREUM_WS_RPC_URL);
listenPingFromEthereum(ethWs, (from, event) => {
const txHash = event.transactionHash;
handleEvent(txHash);
});Connect to Vara and initialize Sails + bridge events
varaProvider = await GearApi.create({ providerAddress: VARA_RPC_URL });
sails = new Sails(parser);
sails.parseIdl(CROSS_PING_IDL);
sails.setApi(varaProvider);
sails.setProgramId(CROSS_PING_PROGRAM_ID);
listenPingSent((e) => {
console.log("Ping confirmed on Vara:", e);
});Subscribe to Ethereum event
const contract = new ethers.Contract(ETH_CONTRACT_ADDRESS, ETH_PINGER_ABI, provider);
contract.on("PingFromEthereum", (_, payload) => {
const txHash = payload.log.transactionHash;
handleEvent(txHash);
});Relay to Vara via GearJS Bridge
import { relayEthToVara } from "@gear-js/bridge";
async function handleEvent(txHash: `0x${string}`) {
const { ok, error } = await relayEthToVara({
transactionHash: txHash,
beaconRpcUrl: BEACON_API_URL,
ethereumPublicClient,
gearApi: varaProvider!,
checkpointClientId: CHECKPOINT_LIGHT_CLIENT,
historicalProxyId: HISTORICAL_PROXY_ID,
clientId: PING_RECEIVER_PROGRAM_ID,
clientServiceName: "PingReceiver",
clientMethodName: "SubmitReceipt",
signer,
wait: true,
});
if (error) console.error("Proxy error:", error);
else console.log("Relayed OK:", ok);
}That’s all: listen to the Ethereum event → forward tx hash → the library handles proofs & finality.
Full code is available in the relayer package.
Vara Side
Your program exposes a service method that the Historical Proxy will call after validation.
The method must be exported so it’s invokable externally.
A minimal implementation that emits an internal event on receipt:
#![no_std]
use sails_rs::prelude::*;
#[sails_rs::event]
#[derive(Encode, Decode, TypeInfo)]
pub enum Event {
ReceiptSubmitted(u64, u32),
}
pub struct PingReceiverService;
#[service(events = Event)]
impl PingReceiverService {
#[export]
pub fn submit_receipt(
&mut self,
slot: u64,
transaction_index: u32,
_receipt_rlp: Vec<u8>,
) -> Result<(), String> {
self.emit_event(Event::ReceiptSubmitted(slot, transaction_index))
.map_err(|_| "Failed to emit event".to_string())?;
Ok(())
}
}Program location:
Names must match what you pass from the relayer:
clientServiceName = "PingReceiver"clientMethodName = "SubmitReceipt"
Verification
Slot synchronization with the Light Client takes around 15–20 minutes (after the Ethereum event is finalized). Once the slot is finalized, a message from HistoricalProxy is delivered to the application program on Vara.