This tutorial demonstrates how to configure token bridging from an EVM-compatible chain to your rollup using Hyperlane.
Anvil is used for demonstration because it can be run locally.
- Start the rollup
- Start Docker Compose with Anvil and Hyperlane agents
- Configure warp routes
- Make inbound transfers
- Make outbound transfers
- Troubleshooting
Start the rollup and keep it running throughout this tutorial.
$ cargo runStart Anvil (local Ethereum node) and the Hyperlane agents.
Consider login into Github docker login ghcr.io if there's rate-limit issue about pulling the images
$ make start-hyperlane-ethtest
✔ Network hyperlane_default Created 0.0s
✔ Container hyperlane-anvil-1 Healthy 5.7s
✔ Container hyperlane-hyperlane-core-deploy-1 Exited 102.8s
✔ Container hyperlane-hyperlane-warp-deploy-1 Exited 175.4s
✔ Container hyperlane-relayer-1 Started 175.5s
✔ Container hyperlane-validator-ethtest-1 Started 175.5s
waiting for containers to become operational (timeout: 300 seconds)...
[2025-09-22 19:18:18] Health check - validator: 'starting', relayer: 'starting' (elapsed: 0s)
[2025-09-22 19:18:18] Waiting for hyperlane containers to be up and running...
[2025-09-22 19:18:21] Health check - validator: 'healthy', relayer: 'healthy' (elapsed: 3s)
[2025-09-22 19:18:21] ✔ All hyperlane containers are healthy
✔ Hyperlane ethtest containers are ready.This command will deploy Hyperlane core contracts and set up the warp route.
Print the warp route configuration on Ethtest. Notice that the remoteRouters map is initially empty:
$ make print-hyperlane-ethtest-warp
✅ Warp route config read successfully:
ethtest:
owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
mailbox: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"
hook: "0x0000000000000000000000000000000000000000"
interchainSecurityModule:
address: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed"
type: testIsm
remoteRouters: {}
name: Ether
symbol: ETH
decimals: 18
isNft: false
contractVersion: 10.1.5
type: native
allowedRebalancers: []
allowedRebalancingBridges: {}
proxyAdmin:
address: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c"
owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
destinationGas: {}Verify the validator announcement message:
$ cat integrations/hyperlane/docker-data/validator-ethtest/signatures/announcement.json
{
"value": {
"validator": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
"mailbox_address": "0x0000000000000000000000008a791620dd6260079bf849dc5567adc3f2fdc318",
"mailbox_domain": 3133790210,
"storage_location": "file:///ethtest-validator-signatures"
},
"signature": {
"r": "0xe41dbc8132819dfacf08219a66c1ad553f9bacc76bf62df5a2b2b037cb5b365f",
"s": "0x527981e9f2a77fd152cbd0161051620d5587f4b7691d467327d9d29ee12e177a",
"v": 27
},
"serialized_signature": "0xe41dbc8132819dfacf08219a66c1ad553f9bacc76bf62df5a2b2b037cb5b365f527981e9f2a77fd152cbd0161051620d5587f4b7691d467327d9d29ee12e177a1b"
}Confirm that the relayer is running and metrics show meaningful data. For example, check the balance for ethtest:
$ curl -Ss http://127.0.0.1:9091/metrics | grep 'hyperlane_wallet_balance'
hyperlane_wallet_balance{agent="relayer",chain="ethtest",hyperlane_baselib_version="0.1.0",token_address="none",token_name="Native",token_symbol="Native",wallet_address="3c44cdddb6a900fa2b585dd299e03d12fa4293bc",wallet_name="relayer"} 10000First, install dependencies for the JavaScript scripts if not already done:
$ cd examples/starter-js && npm installSet up the warp route on the rollup side. This script will:
- Register a warp router and add a remote route on ethtest
- Configure relayer state on the rollup
$ npm run hyperlane-warp-setup
Summary:
Route ID: 0x9c081539d40ef7b02d359c5d694e006f0c1130097466cd22d062e07065c6987a
Token ID: token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvfVerify that the total supply of this token is initially 0:
$ curl -Ss http://127.0.0.1:12346/modules/bank/tokens/token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvf/total-supply
{"amount":"0","token_id":"token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvf"}Verify the warp configuration. Pay attention to these important fields:
- ISM configuration:
validatorsshould match the validator key address remote_token_idshould match the configuration on ethtestenrolled_destinationsshould include the domain ID of ethtestlocal_token_idwill be used later to verify transfers
$ curl -Ss http://127.0.0.1:12346/modules/warp/state/warp-routes/items/0x9c081539d40ef7b02d359c5d694e006f0c1130097466cd22d062e07065c6987a | jq
{
"key": "0x9c081539d40ef7b02d359c5d694e006f0c1130097466cd22d062e07065c6987a",
"value": {
"token_source": {
"Synthetic": {
"remote_token_id": "0x0000000000000000000000004ed7c70f96b99c776995fb64377f0d4ab3b0e1c1",
"local_decimals": 18,
"remote_decimals": 18,
"local_token_id": "token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvf"
}
},
"admin": {
"InsecureOwner": "0xd2c1be33a0bcd2007136afd8ed61cc7561ada747"
},
"ism": {
"MessageIdMultisig": {
"validators": [
"0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
],
"threshold": 1
}
},
"enrolled_destinations": [
3133790210
],You can also check the enrolled routers specifically:
$ curl -Ss http://127.0.0.1:12346/modules/warp/route/0x9c081539d40ef7b02d359c5d694e006f0c1130097466cd22d062e07065c6987a/routers
[{"domain":3133790210,"address":"0x0000000000000000000000004ed7c70f96b99c776995fb64377f0d4ab3b0e1c1"}]$ npm run hyperlane-enroll-router-on-ethtest
[✓] Enrolling remote router...
Contract: 0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1
Domain: 5555
Router: 0x9c081539d40ef7b02d359c5d694e006f0c1130097466cd22d062e07065c6987aNow the remoteRouters should contain an entry for the rollup:
$ cd ../../ && make print-hyperlane-ethtest-warp
✅ Warp route config read successfully:
ethtest:
owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
mailbox: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"
hook: "0x0000000000000000000000000000000000000000"
interchainSecurityModule:
address: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed"
type: testIsm
remoteRouters:
"5555":
address: "0x9c081539d40ef7b02d359c5d694e006f0c1130097466cd22d062e07065c6987a"
name: Ether
symbol: ETH
decimals: 18
isNft: false
contractVersion: 10.1.5
type: native
allowedRebalancers: []
allowedRebalancingBridges: {}
proxyAdmin:
address: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c"
owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
destinationGas:
"5555": "0"Before initiating an inbound transfer to 0xD2C1bE33A0BcD2007136afD8Ed61CC7561aDa747, let's verify that its balance of bridged ETH is zero.
The bank endpoint will return a 404 error for non-existent balances:
$ curl -Ss http://127.0.0.1:12346/modules/bank/tokens/token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvf/balances/0xD2C1bE33A0BcD2007136afD8Ed61CC7561aDa747
{"status":404,"message":"Balance '0xD2C1bE33A0BcD2007136afD8Ed61CC7561aDa747' not found","details":{"id":"0xD2C1bE33A0BcD2007136afD8Ed61CC7561aDa747"}}Navigate to examples/starter-js and execute the inbound transfer:
$ npm run hyperlane-inbound
Making inbound warp transfer...
Contract: 0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1
Domain: 5555
Router: 0x9c081539d40ef7b02d359c5d694e006f0c1130097466cd22d062e07065c6987a
Recipient: 0x000000000000000000000000D2C1bE33A0BcD2007136afD8Ed61CC7561aDa747
Amount: 0.01 ETH
Gas: 0.0 ETH
Total: 0.01 ETH
Transaction sent: 0xda1dbcb27ad6d12a53f3137559628ac39f09cc578be740288deb7d7bca6d452bWait a moment for the transfer to process, then verify that the total supply of the synthetic token has increased along with the recipient's balance:
$ curl -Ss http://127.0.0.1:12346/modules/bank/tokens/token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvf/total-supply
{"amount":"10000000000000000","token_id":"token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvf"}$ sleep 60 && curl -Ss http://127.0.0.1:12346/modules/bank/tokens/token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvf/balances/0xD2C1bE33A0BcD2007136afD8Ed61CC7561aDa747
{"amount":"10000000000000000","token_id":"token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvf"}Now we'll send funds back to 0xD2C1bE33A0BcD2007136afD8Ed61CC7561aDa747 on ethtest.
First, verify that its balance is zero on ethtest:
$ curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xD2C1bE33A0BcD2007136afD8Ed61CC7561aDa747", "latest"],"id":1}' http://127.0.0.1:8545
{"jsonrpc":"2.0","id":1,"result":"0x0"}Then initiate the outbound transfer:
$ npm run hyperlane-outbound
[✓] Receipt result: successful
[✓] Mailbox/DispatchId (HyperlaneId): 0x873e0bfeb9251c268fbc483b4dae63a548360dd7594a1768aeb5a1532dd16e5c
[✓] Warp/TokenTransferredRemote:
Route ID: 0x9c081539d40ef7b02d359c5d694e006f0c1130097466cd22d062e07065c6987a
To Domain: 3133790210
Recipient: 0x000000000000000000000000d2c1be33a0bcd2007136afd8ed61cc7561ada747
Amount (hex): 0x0000000000000000000000000000000000000000000000000000702d54e2f800
Amount (decimal): 123340000000000Wait for the transfer to process and verify that the balance on ethtest has increased:
$ sleep 30 && curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xD2C1bE33A0BcD2007136afD8Ed61CC7561aDa747", "latest"],"id":1}' http://127.0.0.1:8545
{"jsonrpc":"2.0","id":1,"result":"0x702d54e2f800"}Confirm that the total supply of the synthetic token has decreased accordingly:
$ curl -Ss http://127.0.0.1:12346/modules/bank/tokens/token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvf/total-supply
{"amount":"9876660000000000","token_id":"token_195zght0wmhcx9j462jtj9lypdua4xw07r6jnjfjsddsmzeh2wsfqrhddvf"}Check Configuration:
- Verify
CHAIN_IDandDOMAIN_IDfor both chains in all configuration files:constants.tomlintegrations/hyperlane/configs/agent-config.jsonintegrations/hyperlane/configs/chains/ethtest/metadata.yamlexamples/starter-js/src/hyperlane/consts.ts
- Ensure mailbox addresses match across all configurations:
grep -i 'mailbox' integrations/hyperlane/configs/chains/ethtest/addresses.yaml grep -i 'mailbox' integrations/hyperlane/configs/agent-config.json - Verify that warp routes are enrolled on both chains:
- On ethtest: use
make print-hyperlane-ethtest-warpand check that remoteRouters contains the correct DOMAIN_ID (note the route ID) - On rollup:
curl http://127.0.0.1:12346/modules/warp/route/<ROUTE_ID>/routers
- On ethtest: use
- Ensure Anvil is configured for periodic block production
- Verify that the validator is posting checkpoints:
ls integrations/hyperlane/docker-data/validator-ethtest/signaturesshould contain at least one file0_with_id.json - Check for errors or warnings in relayer logs:
docker logs hyperlane-relayer-1 | grep -i -E 'error|warn' - Verify that the relayer has sufficient balance:
curl http://127.0.0.1:9091/metrics | grep hyperlane_wallet_balance - Confirm the validator key is present in the ISM
MessageIdMultisigconfiguration:curl -Ss http://127.0.0.1:12346/modules/warp/state/warp-routes/items/<WARP_ROUTE_ID> | jq - If you see "default IGP fee amount not set" error:
Check:
curl http://127.0.0.1:12346/modules/interchain-gas-paymaster/state/relayer-default-gas/items/<RELAYER_SOV_ADDRESS>Ensure the relayer address in the message is correct
Check relayer balance:
curl -Ss http://127.0.0.1:9091/metrics | grep 'hyperlane_wallet_balance'
# HELP hyperlane_wallet_balance Current native token balance for the wallet addresses in the `wallets` set
# TYPE hyperlane_wallet_balance gauge
hyperlane_wallet_balance{agent="relayer",chain="ethtest",hyperlane_baselib_version="0.1.0",token_address="none",token_name="Native",token_symbol="Native",wallet_address="3c44cdddb6a900fa2b585dd299e03d12fa4293bc",wallet_name="relayer"} 10000
Verify no critical errors:
curl -Ss http://127.0.0.1:9091/metrics | grep 'hyperlane_critical_error'
hyperlane_critical_error{agent="relayer",chain="ethtest",hyperlane_baselib_version="0.1.0"} 0
hyperlane_critical_error{agent="relayer",chain="sovstarter",hyperlane_baselib_version="0.1.0"} 0