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, dispatchestransferRemotethrough the sanect MailboxWSanectHyp.sol(a.k.a.HypERC20Payable) — synthetic wSNCT on Ethereum; forwardsmsg.valueintomailbox.dispatch{value:}so the Ethereum protocol-fee hook is satisfiedStubIgp.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 toMessageIdMultisigIsm
Chain IDs / Hyperlane domains: sanect = 7628, Ethereum mainnet = 1.
Pre-flight checks
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
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.
Hyperlane CLI installed locally:
bashnpm install -g @hyperlane-xyz/cli@latest hyperlane --version # ≥ 5.0.0foundryinstalled 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:
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: 18Then run:
export HYP_KEY=0x<deployer private key> # the role-EOA that owns the bridge
hyperlane core deploy --chain sanect --config ~/hyperlane-config/chains.yamlThis 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.
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).logMainnet guardrails refuse to run if:
RPC_URLis not on chain 7628ETH_RPC_URLis 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:
{
"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):
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:
# 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_KEYIf 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:
- Stand up 3+ Hyperlane validator agents (one per geographic region — same image as the relayer, different command:
./validator) - Each validator gets its own hot key and submits checkpoints to S3/R2
- Run
hyperlane warp configto swap the default ISM for aMessageIdMultisigIsmrequiring 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)
| Symptom | Diagnosis | Recovery |
|---|---|---|
| Message dispatched but never delivered | Relayer container down OR out of gas on dest | Restart container, refill destination-side hot wallet |
Ethereum tx reverts on transferRemote | Forgot --value 1 for the protocol-fee hook | Retry with --value 1 |
Mailbox.dispatch reverts on sanect | StubIgp not initialized OR adapter not initialized | cast call $ADAPTER "interchainGasPaymaster()(address)" should match StubIgp |
| Synthetic balance on Ethereum unchanged after 10 min | Default ISM rejecting metadata | Check 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 script | Re-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.