Back to Linera Protocol

EVM-Linera Bridge Demo

examples/bridge-demo/README.md

0.15.1712.8 KB
Original Source

EVM-Linera Bridge Demo

A cross-chain token bridge between an EVM chain and Linera. Deposits lock ERC-20 tokens on the EVM side and mint wrapped tokens on Linera; withdrawals burn wrapped tokens and release ERC-20s back to the user.

Architecture

Three components cooperate:

ComponentEVM sideLinera side
Contracts / AppsLightClient (verifies Linera blocks), FungibleBridge (holds ERC-20s, emits deposit events)wrapped-fungible (mints/burns wrapped tokens), evm-bridge (coordinates messaging)
RelayWatches for DepositInitiated events, submits receipt proofsManages a Linera chain (persistent state), forwards blocks to EVM for withdrawals
FrontendMetaMask for EVM transactions@linera/client for Linera queries and signing

Quick start (Docker)

Everything runs in containers -- no Foundry, Linera CLI, or Solidity toolchain needed on the host.

Prerequisites

  • Docker with Compose v2
  • pnpm (for the frontend)
  • MetaMask browser extension
  • The repo checked out with submodules

Steps

From the linera-bridge/ directory:

bash
# 1. Build Wasm binaries and Docker images
make demo

# 2. Start the frontend (separate terminal)
make demo-frontend

make demo is a convenience target that runs:

TargetWhat it does
make build-wasmCompile fungible, wrapped-fungible, and evm-bridge Wasm binaries
make build-allBuild Docker images (linera-test, linera-exporter, linera-bridge, foundry-jq)
make upStart Anvil, ScyllaDB, Linera validator+faucet, block exporter, and relay
make demo-setupDeploy contracts and Linera apps, write .env.local

The frontend starts on http://localhost:5173.

MetaMask setup for local Anvil

The demo runs a local Anvil node that MetaMask doesn't know about. You need to configure three things:

1. Add the Anvil network

MetaMask → Settings → Networks → Add Network → Add a network manually:

FieldValue
Network nameAnvil
RPC URLhttp://localhost:8545
Chain ID31337
Currency symbolETH

Save and switch to the new network.

2. Import the deployer account

The setup script deploys the MockERC20 token from Anvil's default account (account 0), which holds the entire token supply (1000 TT). You need to import this account into MetaMask to have tokens available for deposits.

MetaMask → Account menu → Import account → Paste private key:

0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

This is Anvil's well-known dev key -- never use it on a real network.

3. Import the ERC-20 token

After setup completes, the token contract address is printed in the terminal and written to .env.local as LINERA_TOKEN_ADDRESS. To see your balance in MetaMask:

MetaMask → Tokens → Import tokens:

FieldValue
Token addressthe LINERA_TOKEN_ADDRESS from .env.local
Token symbolTT
Decimals18

You should see a balance of 1000 TT.

Other useful targets

bash
make demo-logs   # tail relay logs
make down        # stop everything and remove volumes

Testnet / direct mode

For real network deployments, run everything directly on the host instead of through Docker.

Prerequisites

  • Foundry (forge, cast)
  • linera CLI (built from this repo or installed)
  • linera-bridge CLI: cargo build -p linera-bridge --features relay
  • pnpm (for the frontend)
  • A funded EVM account (private key with ETH for gas)
  • An EVM RPC endpoint (e.g. Base Sepolia)
  • A running Linera testnet with faucet

1. Build the Wasm binaries

From the linera-bridge/ directory:

bash
make build-wasm

This compiles fungible, wrapped-fungible, and evm-bridge to examples/target/wasm32-unknown-unknown/release/.

2. Initialize wallet and claim a bridge chain

bash
export FAUCET_URL=https://faucet.testnet-conway.linera.net

# Initialize a Linera wallet from the faucet
linera wallet init --faucet "$FAUCET_URL"

# Claim a chain that the relay will use as the "bridge chain"
linera wallet request-chain --faucet "$FAUCET_URL"

Note the chain ID and owner printed by request-chain — you'll need them for both the setup script and the relay.

3. Deploy contracts

Pick a shared directory for coordination files between the setup script and the relay:

bash
export SHARED_DIR="/tmp/bridge-demo-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$SHARED_DIR"

Run the setup script to deploy EVM contracts and Linera apps:

bash
cd examples/bridge-demo

./setup.sh \
  --evm-rpc-url https://base-sepolia.g.alchemy.com/v2/YOUR_KEY \
  --evm-private-key 0x... \
  --evm-chain-id 84532 \
  --linera-bridge-chain-id <CHAIN_ID> \
  --relay-owner <OWNER> \
  --faucet-url "$FAUCET_URL" \
  --linera-wallet ~/.config/linera/wallet.json \
  --linera-keystore ~/.config/linera/keystore.json \
  --linera-storage rocksdb:~/.config/linera/client.db \
  --shared-dir "$SHARED_DIR"

The setup script:

  1. Fetches validator info and deploys the LightClient contract
  2. Deploys a MockERC20 token (or pass --token-address to use an existing one)
  3. Publishes wrapped-fungible and evm-bridge apps on the bridge chain
  4. Deploys the FungibleBridge contract (referencing LightClient + apps)
  5. Funds the bridge with ERC-20 tokens
  6. Writes contract/app addresses to $SHARED_DIR (relay picks them up) and .env.local (frontend reads them)

4. Start the relay

The relay uses the same wallet, keystore, and storage as the linera CLI. By default it reads from ~/.config/linera/ — the same location linera wallet init writes to. You can override with --wallet, --keystore, --storage flags or LINERA_WALLET, LINERA_KEYSTORE, LINERA_STORAGE env vars.

Pass the contract addresses and app IDs from the setup script output:

bash
linera-bridge serve \
  --rpc-url <evm-rpc> \
  --faucet-url "$FAUCET_URL" \
  --linera-bridge-chain-id <chain-id> \
  --linera-bridge-address <bridge-app-id-on-linera> \
  --linera-fungible-address <fungible-app-id-on-linera> \
  --evm-bridge-address <bridge-contract-address-on-evm> \
  --evm-private-key <relayer-private-key>

On restart, run the same command — the relay loads persistent state from the wallet and storage, and syncs from validators to catch up.

5. Start the frontend

bash
pnpm install && pnpm dev

Open http://localhost:5173, connect MetaMask to your EVM network, and use the deposit/withdraw forms.

Setup script flags

FlagDefault (Docker)Default (Direct)Description
--compose-file PATH----Enables Docker mode
--evm-rpc-url URLhttp://anvil:8545http://localhost:8545EVM JSON-RPC endpoint
--evm-private-key KEYAnvil account 0requiredPrivate key for EVM txs
--evm-chain-id ID3133731337EVM chain ID
--light-client-address ADDRread from /shared/deployed if omittedSkip LightClient deploy
--linera-bridge-chain-id IDpolled from relayrequiredLinera bridge chain (64 hex chars)
--token-address ADDRdeployeddeployedSkip MockERC20 deploy
--relay-owner OWNERread from /shared/requiredRelay's AccountOwner (minter)
--faucet-url URLhttp://localhost:8080http://localhost:8080Linera faucet
--relay-url URLhttp://localhost:3001http://localhost:3001Relay HTTP endpoint
--ticker-symbol SYMwTTwTTWrapped token ticker
--fund-amount WEI500e18500e18Fund bridge with ERC-20; 0 to skip
--shared-dir PATHauto (/tmp/bridge-demo-*)autoShared state directory for relay coordination
--wasm-dir PATH/wasm../../examples/target/wasm32-unknown-unknown/releaseDirectory with .wasm binaries
--contracts-dir PATH/contracts../../linera-bridge/src/soliditySolidity source root
--output PATH.env.local.env.localOutput env file
--linera-wallet PATH--auto (temp dir)Path to existing Linera wallet.json
--linera-keystore PATH--auto (temp dir)Path to existing Linera keystore.json
--linera-storage CONFIG--auto (temp dir)Linera storage config (e.g. rocksdb:path/to/db)

How a deposit works

  1. User approves ERC-20 spend on FungibleBridge
  2. User calls FungibleBridge.deposit(chainId, appId, owner, amount)
  3. Relay's EVM scanner detects the DepositInitiated event
  4. Relay generates a receipt inclusion proof (MPT) and submits it to the evm-bridge Linera app
  5. evm-bridge verifies the proof and tells wrapped-fungible to mint tokens
  6. User's Linera balance updates

How a withdrawal works

  1. User calls wrapped-fungible.transfer targeting the relay's chain with an Address20 owner (their EVM address)
  2. On the relay chain, the Credit message triggers an auto-burn and emits a BurnEvent on the "burns" stream
  3. Relay detects the BurnEvent and forwards the Linera block to FungibleBridge.addBlock()
  4. FungibleBridge verifies the block via LightClient, deserializes the BurnEvent, and transfers ERC-20 tokens to the user's EVM address

Active scanning and auto-retry

The relay actively scans both chains to detect missed or failed bridging requests and automatically retries them:

  • EVM→Linera deposits: the relay polls EVM for DepositInitiated events and checks the Linera evm-bridge app to see if each deposit has been processed. Unprocessed deposits are retried by regenerating the MPT proof and resubmitting.
  • Linera→EVM burns: the relay scans Linera blocks for BurnEvent events on the "burns" stream and checks EVM for matching ERC-20 Transfer events. Unforwarded burns are retried by re-reading the block containing the auto-burn from chain storage and re-calling addBlock.

This means the relay self-heals after crashes or transient RPC failures without operator intervention. On-chain replay protection (processed_deposits on Linera, verifiedBlocks on EVM) makes retries safe.

Monitoring endpoints:

EndpointDescription
GET /monitor/statusSummary counts of pending/completed deposits and burns
GET /monitor/deposits?status=pendingList pending deposits
GET /monitor/burns?status=pendingList unforwarded burns

Relay flags for tuning:

FlagDefaultDescription
--monitor-scan-interval30Seconds between chain scan iterations
--monitor-start-block0EVM block to start scanning from
--max-retries10Max retry attempts before marking an item as failed

Frontend environment

The frontend reads these variables from .env.local (generated by setup.sh):

LINERA_FAUCET_URL
LINERA_APPLICATION_ID       # wrapped-fungible app ID
LINERA_BRIDGE_APP_ID        # evm-bridge app ID
LINERA_RELAY_URL
LINERA_BRIDGE_ADDRESS       # FungibleBridge contract
LINERA_TOKEN_ADDRESS        # ERC-20 token contract
LINERA_BRIDGE_CHAIN_ID
LINERA_EVM_CHAIN_ID

All are required -- the Vite dev server will refuse to start if any are missing.

EVM finality verification

The evm-bridge Linera app verifies that deposit blocks are finalized by querying an EVM JSON-RPC endpoint (the rpc_endpoint parameter). This endpoint hostname must be in the Linera network's HTTP request allow list.

  • Docker mode: The compose file passes --http-request-allow-list to linera net up, defaulting to anvil. Override with the HTTP_REQUEST_ALLOW_LIST env var (e.g. HTTP_REQUEST_ALLOW_LIST=base-sepolia.g.alchemy.com).
  • Testnet mode: The Linera testnet validators must whitelist the RPC hostname (e.g. base-sepolia.g.alchemy.com). Contact the testnet operators to add your RPC provider's hostname to the allow list.

If the RPC hostname is not whitelisted, deposits will fail with UnauthorizedHttpRequest. As a workaround, set rpc_endpoint to empty in the evm-bridge parameters to skip finality verification (not recommended for production).

Troubleshooting

  • MetaMask shows 0 ETH (local): make sure you switched to the Anvil network and imported the correct private key.
  • Token balance is 0: double-check that you imported the token using the address from .env.local, and that you are on the correct account.
  • Deposit hangs: check relay logs -- the relay needs to be running and healthy before submitting deposits.