Skip to content

Mainnet Hyperlane bridge setup ​

Operator-facing walkthrough for bringing the sanect ↔ Ethereum mainnet Hyperlane bridge live. Code (orchestrator + custom contracts) is already mainnet-ready and committed; this doc is the laptop-side runbook.

The bridge backs airdrop products #10 (bridge in) + #11 (bridge out) in the Genesis Allocation point system, so users can earn 2 of 13 unlock slots through bridge usage alone. It is NOT required for Day-0 launch — ship the dApps first, stand the bridge up over Day 1–7.


Architecture recap ​

Three custom contracts (already in dex/contracts/bridge/, audited working on Sepolia testnet end-to-end):

  • SanectHypAdapter.sol — HypERC20Collateral on sanect side; locks WSNCT, dispatches transferRemote through the sanect Mailbox
  • WSanectHyp.sol (a.k.a. HypERC20Payable) — synthetic wSNCT on Ethereum; forwards msg.value into mailbox.dispatch{value:} so the Ethereum protocol-fee hook is satisfied
  • StubIgp.sol — no-op InterchainGasPaymaster (we use trusted relayer model, so on-chain gas accounting is irrelevant)
  • TrustedRelayerIsm.sol — Day-0 ISM that trusts one relayer address; Day-7 we upgrade to MessageIdMultisigIsm

Chain IDs / Hyperlane domains: sanect = 7628, Ethereum mainnet = 1.


Pre-flight checks ​

  1. Hardware wallet holding deployer key with ≥0.3 ETH on Ethereum mainnet and ≥1 SNCT on sanect. Bridge deploys cost real money:

    • sanect adapter deploy: ~0.001 SNCT
    • Ethereum synthetic deploy: ~0.05 ETH (synthetic is heavy)
    • enrollRemoteRouter (×2): ~0.01 ETH + ~0.0001 SNCT
  2. Alchemy/Infura Ethereum mainnet RPC endpoint. Free tier is fine for the deploy; relayer wants a paid tier ($50/mo) for steady eth_call load.

  3. Hyperlane CLI installed locally:

    bash
    npm install -g @hyperlane-xyz/cli@latest
    hyperlane --version   # ≥ 5.0.0
  4. foundry installed locally (cast, forge) — the deploy script shells out to both.


Step 1 — Hyperlane core deploy on sanect (one-time) ​

The sanect chain needs its own Mailbox + default ISM + required hooks before any token routers can be enrolled. Hyperlane CLI handles this; we just feed it a chain config.

Create ~/hyperlane-config/chains.yaml:

yaml
sanect:
  name: sanect
  chainId: 7628
  domainId: 7628
  protocol: ethereum
  rpcUrls:
    - http: https://rpc.sanect.com
  blocks:
    confirmations: 1
    reorgPeriod: 1
    estimateBlockTime: 0.4
  nativeToken:
    name: Sanect
    symbol: SNCT
    decimals: 18

Then run:

bash
export HYP_KEY=0x<deployer private key>      # the role-EOA that owns the bridge
hyperlane core deploy --chain sanect --config ~/hyperlane-config/chains.yaml

This deploys: Mailbox, ProxyAdmin, default ISM (permissionless multisig with relayer threshold 1), required hook (Merkle tree hook), default hook (interchainGasPaymaster). Output ends with an addresses block — copy mailbox into SANECT_MAILBOX_ADDR for the next step.

Expected cost: ~0.01 SNCT (essentially free).


Step 2 — Deploy bridge routers (sanect adapter + Ethereum synthetic) ​

scripts/mainnet/deploy-bridge.sh orchestrates both forge deploys and the cross-chain enrollRemoteRouter calls.

bash
export RPC_URL=https://rpc.sanect.com
export ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/<your-key>
export DEPLOYER_PRIVATE_KEY=0x<role-EOA hex; same key as Step 1>
export SANECT_MAILBOX_ADDR=0x<from Step 1 output>
export ETH_MAILBOX_ADDR=0xc005dc82818d67AF737725bD4bf75435d065D239   # canonical Hyperlane L1 mailbox
export WSNCT=0x4f35BD61FEd919aA9933C6333e388b3B7E1108Dc              # from genesis-addresses.json

bash scripts/mainnet/deploy-bridge.sh 2>&1 | tee bridge-deploy-$(date +%s).log

Mainnet guardrails refuse to run if:

  • RPC_URL is not on chain 7628
  • ETH_RPC_URL is not on chain 1
  • Deployer key is the testnet key 0x7cb61d4117...
  • Balance is <0.1 SNCT on sanect OR <0.1 ETH on Ethereum

Final output writes bridge-addresses.json:

json
{
  "SanectHypAdapter": "0x...",
  "WSanectHyp": "0x..."
}

Expected cost: ~0.05 ETH + ~0.01 SNCT.


Step 3 — Stand up the relayer ​

The relayer is a Docker container running the canonical Hyperlane agent image. It watches both Mailboxes for dispatched messages and submits Mailbox.process() on the destination side.

On any always-on host (Railway, VPS, your laptop for testing — but production wants 24/7 uptime or messages stall):

bash
mkdir -p ~/hyperlane-relayer-data
docker run -d \
  --name hyperlane-relayer \
  --restart unless-stopped \
  -v ~/hyperlane-relayer-data:/data \
  -v ~/hyperlane-config:/config \
  -e CONFIG_FILES=/config/agent-config.json \
  -e HYP_KEY=$DEPLOYER_PRIVATE_KEY \
  -e HYP_DB=/data \
  -e HYP_RELAYCHAINS=sanect,ethereum \
  -e HYP_DEFAULTSIGNER_TYPE=hexKey \
  -e HYP_DEFAULTSIGNER_KEY=$DEPLOYER_PRIVATE_KEY \
  gcr.io/abacus-labs-dev/hyperlane-agent:main \
  ./relayer

~/hyperlane-config/agent-config.json should be the Hyperlane CLI chain registry export (hyperlane registry export --chain sanect,ethereum) plus your two Mailbox addresses. The same file shape testnet uses is in CLAUDE.md "Hyperlane bridge" — copy that and swap chain IDs.

Why a hot key here is acceptable Day 0: the relayer can't mint or withdraw on its own — it can only submit messages already dispatched. Worst case if compromised: attacker spams process() and drains the relayer's gas balance. Refill is operator's job; no protocol risk.

Pre-fund the relayer with ~0.5 ETH on Ethereum and ~5 SNCT on sanect to cover the first few weeks of process() calls.


Step 4 — Round-trip test (1 wei each direction) ​

Same flow as testnet:

bash
# sanect → Ethereum
ADAPTER=$(jq -r .SanectHypAdapter bridge-addresses.json)
SYNTHETIC=$(jq -r .WSanectHyp bridge-addresses.json)

cast send $WSNCT "deposit()" --value 1 \
  --rpc-url $RPC_URL --private-key $DEPLOYER_PRIVATE_KEY
cast send $WSNCT "approve(address,uint256)" $ADAPTER 1 \
  --rpc-url $RPC_URL --private-key $DEPLOYER_PRIVATE_KEY
cast send $ADAPTER "transferRemote(uint32,bytes32,uint256)" \
  1 $(cast --to-uint256 <recipient>) 1 \
  --rpc-url $RPC_URL --private-key $DEPLOYER_PRIVATE_KEY

# Wait 1–3 min for relayer delivery, then check Ethereum side
cast call $SYNTHETIC "balanceOf(address)(uint256)" <recipient> \
  --rpc-url $ETH_RPC_URL
# Expect: 1

# Ethereum → sanect (--value 1 covers Ethereum's protocol fee hook)
cast send $SYNTHETIC "transferRemote(uint32,bytes32,uint256)" \
  7628 $(cast --to-uint256 <recipient>) 1 --value 1 \
  --rpc-url $ETH_RPC_URL --private-key $DEPLOYER_PRIVATE_KEY

If delivery stalls >5 min, check docker logs hyperlane-relayer — most common cause is the relayer key being out of gas on the destination side.


Step 5 — Wire dex-app frontend ​

Set on the dex-app Railway service (build args, since they're NEXT_PUBLIC_*):

NEXT_PUBLIC_SANECT_BRIDGE_ADAPTER=<SanectHypAdapter from bridge-addresses.json>
NEXT_PUBLIC_ETH_BRIDGE_SYNTHETIC=<WSanectHyp from bridge-addresses.json>

Redeploy dex-app. Bridge tab in https://swap.sanect.com lights up.

Until these vars are set, dex-app/lib/bridge.ts defaults to empty strings and the bridge tab shows a "bridge not yet live" placeholder (intentional — no testnet leak).


Step 6 — CLAUDE.md update ​

Append to the "Hyperlane bridge" section of CLAUDE.md a mainnet deployed-addresses table mirroring the testnet one. Operators reading the file later need to know which addresses are live without grepping git history.


Day-7 follow-up: multi-validator ISM ​

Day-0 ISM is the permissionless multisig Hyperlane CLI deploys by default — secure enough for low volume but it trusts Hyperlane's validator set, not ours. Upgrade to a MessageIdMultisigIsm with our validators within the first week:

  1. Stand up 3+ Hyperlane validator agents (one per geographic region — same image as the relayer, different command: ./validator)
  2. Each validator gets its own hot key and submits checkpoints to S3/R2
  3. Run hyperlane warp config to swap the default ISM for a MessageIdMultisigIsm requiring 2-of-3 of those validators

See https://docs.hyperlane.xyz/docs/protocol/ISM/multisig-ISM for the config shape. The bridge can keep operating during the swap — only new messages use the new ISM.


What can go wrong (and isn't fatal) ​

SymptomDiagnosisRecovery
Message dispatched but never deliveredRelayer container down OR out of gas on destRestart container, refill destination-side hot wallet
Ethereum tx reverts on transferRemoteForgot --value 1 for the protocol-fee hookRetry with --value 1
Mailbox.dispatch reverts on sanectStubIgp not initialized OR adapter not initializedcast call $ADAPTER "interchainGasPaymaster()(address)" should match StubIgp
Synthetic balance on Ethereum unchanged after 10 minDefault ISM rejecting metadataCheck docker logs hyperlane-relayer for "No ISM found" — usually means a validator address mismatch in the relayer config
Adapter says "router not enrolled"enrollRemoteRouter calls failed silently in the deploy scriptRe-run the two enrollRemoteRouter cast sends from the script manually

The bridge is non-custodial at the protocol level — even if the relayer goes permanently offline, locked WSNCT in the adapter can be reclaimed by changing the ISM via the protocol-governance EOA. No funds get stuck.