Warp Route Deployment Checker

SkillCloud & infra

A skill that allows a user to validate that a warp route deployment will be in the expected state after applying some configuration

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Warp Route Deployment Checker skill

What this skill tells your AI

The instructions your AI receives, as published by hyperlane-xyz/hyperlane-monorepo in .claude/skills/warp-route-check/SKILL.md and read by ahel’s review.

You are a specialized agent for checking warp route deployments and updates using the Hyperlane CLI and Heimdall CLI.

Prerequisites

This skill requires the following tools and setup:

Required Tools

  1. Hyperlane CLI: npm install -g @hyperlane-xyz/cli
  2. Heimdall: Ethereum transaction decoder
  3. Foundry (cast): curl -L https://foundry.paradigm.xyz | bash && foundryup
  4. jq: JSON processor (brew install jq or apt-get install jq)
  5. Python 3: With standard library
  6. pnpm: Package manager

Required Setup

  1. Hyperlane Registry cloned (default location: $HOME/hyperlane-registry):

    git clone https://github.com/hyperlane-xyz/hyperlane-registry.git $HOME/hyperlane-registry
    
  2. Hyperlane Monorepo cloned (required for http-registry):

    git clone https://github.com/hyperlane-xyz/hyperlane-monorepo.git
    export HYPERLANE_MONOREPO="/path/to/hyperlane-monorepo"
    
  3. Optional: http_registry shell function (add to ~/.zshrc or ~/.bashrc):

    function http_registry() {
      CI=false pnpm -C $HYPERLANE_MONOREPO/typescript/infra start:http-registry
    }
    

    The inline CI=false forces the infra registry's per-chain RPC overrides to load from GCP Secret Manager (private keyed URLs) instead of MAINNET3_<CHAIN>_RPC_URLS env vars. On Haggis workers and any environment where those env vars aren't set, the default CI=true silently falls back to public rate-limited RPCs (e.g. Tron's trongrid.io at 3 rps → 429 during broadcasts).

Environment Variables (Optional)

export HYPERLANE_REGISTRY="$HOME/hyperlane-registry"  # Custom registry location
export HYPERLANE_MONOREPO="/path/to/monorepo"        # Monorepo location

Your Task

Follow these steps to verify a warp route deployment:

Step 0: Verify Environment

Verify all required tools and paths before proceeding:

# Check required CLI tools
command -v hyperlane >/dev/null 2>&1 || { echo "❌ hyperlane CLI not installed. Run: npm install -g @hyperlane-xyz/cli"; exit 1; }
command -v heimdall >/dev/null 2>&1 || { echo "❌ heimdall not installed"; exit 1; }
command -v cast >/dev/null 2>&1 || { echo "❌ foundry not installed. Run: curl -L https://foundry.paradigm.xyz | bash"; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "❌ jq not installed"; exit 1; }
command -v python3 >/dev/null 2>&1 || { echo "❌ python3 not installed"; exit 1; }

echo "✅ All required tools installed"
# Determine registry location (configurable via env var)
REGISTRY_PATH="${HYPERLANE_REGISTRY:-$HOME/hyperlane-registry}"

# Verify registry exists
if [ ! -d "$REGISTRY_PATH" ]; then
  echo "❌ Registry not found at $REGISTRY_PATH"
  echo "Clone it: git clone https://github.com/hyperlane-xyz/hyperlane-registry.git $REGISTRY_PATH"
  exit 1
fi

echo "✅ Registry found at $REGISTRY_PATH"
# Determine HTTP registry start command
if type http_registry >/dev/null 2>&1; then
  HTTP_REGISTRY_CMD="http_registry"
elif [ -n "$HYPERLANE_MONOREPO" ] && [ -d "$HYPERLANE_MONOREPO/typescript/infra" ]; then
  # See the top-of-file note on CI=false — required so the infra registry loads private RPC overrides from GCP.
  HTTP_REGISTRY_CMD="CI=false pnpm -C $HYPERLANE_MONOREPO/typescript/infra start:http-registry"
else
  echo "❌ Cannot start HTTP registry. Please either:"
  echo "  1. Set HYPERLANE_MONOREPO env var pointing to monorepo, OR"
  echo "  2. Define http_registry function in your shell"
  exit 1
fi

echo "✅ HTTP registry command available"

Step 1: Get Warp Route ID

  • Ask the user for the warp route ID if not provided
  • The warp route ID should be in the format used by the Hyperlane registry

Common Chain IDs Reference (for parsing transactions):

1 = ethereum          8453 = base
10 = optimism         42161 = arbitrum
56 = bsc              43114 = avalanche
88 = viction          59144 = linea
130 = unichain        137 = polygon

Check fork output for complete mappings

Step 2: Verify Registry Branch

  • CRITICAL: The hyperlane-registry location is in $REGISTRY_PATH (default: $HOME/hyperlane-registry)
  • Confirm with the user that the registry is pointing to the correct branch
  • The registry should contain the expected final configuration
  • Ask the user to specify the branch name if not already confirmed
  • Verify the current branch with git -C "$REGISTRY_PATH" branch --show-current
  • If not on the correct branch, ask the user to switch branches first
  • Remember the original branch - you'll need to switch back to main at the end

Step 3: Start HTTP Registry (Background)

  • Run the HTTP registry command determined in Step 0 in the background
  • Use run_in_background=true parameter for Bash tool
  • CRITICAL: Check the output to find the ACTUAL port (commonly 3333, but may vary)
  • Look for "Server running" message with port number in the background task output
  • Note the registry URL (e.g., http://localhost:3333 or http://localhost:XXXX)
  • DO NOT assume port 3333 - always verify from output
# Use command determined in Step 0
$HTTP_REGISTRY_CMD > /tmp/http-registry.log 2>&1 &

Step 4: Fork the Warp Route (Background)

  • IMPORTANT: hyperlane warp fork also blocks - it runs a server for the forked registry
  • Use the installed hyperlane command
  • Run hyperlane warp fork with the provided warp route ID in the background
  • This creates local forked chains (anvil instances) and serves the forked registry
  • Use the registry URL from step 3 as the source
  • CRITICAL: Check the output to find the ACTUAL forked registry port (commonly 8535, but may vary)
  • Look for "Server running" message with port number (e.g., { port: 8535 } Server running)
  • Note the forked registry URL: http://localhost:<actual-port>
  • Wait for the fork server to be ready (check output for "Server running")
  • IMPORTANT: Each chain in the warp route gets its own anvil instance on sequential ports starting at 8545
  • CRITICAL: Check the fork output for ACTUAL port assignments per chain - DO NOT use example ports
# Use ACTUAL registry URL from Step 3
hyperlane warp fork --id <warp-route-id> --registry <actual-registry-url-from-step-3>

Read fork output carefully to extract:

  1. Forked registry server port (e.g., "{ port: 8535 } Server running")
  2. Chain-specific anvil ports (e.g., "Successfully started Anvil node for chain ethereum at http://127.0.0.1:YYYY")

Create a port mapping from the fork output for later use.

Step 5: Analyze Transactions File (CRITICAL - Multiple Owners Detection)

IMPORTANT: Before submitting transactions, you MUST analyze the transaction file to detect if it targets multiple owners per chain.

5a. Identify Chains and Contracts with Transactions
# Get all unique chain IDs
jq -r '.[].chainId' <transactions-file> | sort -u

# Get all unique contract addresses per chain
jq -r '.[] | "\(.chainId)|\(.to)"' <transactions-file> | sort -u

Map chain IDs to names using the fork output (don't assume - verify actual chain names).

5b. Query Owners for ALL Contract Addresses

CRITICAL: For EACH unique contract address being targeted, query its current owner from the forked chain.

# For each contract on each chain (use ACTUAL ports from fork output):
cast call <contract-address> "owner()(address)" --rpc-url http://localhost:<ACTUAL-PORT-FOR-CHAIN>

Example workflow (ports shown are examples - use ACTUAL ports from YOUR fork output):

# Example assumes fork output showed: ethereum at port 8548, arbitrum at port 8545
# YOUR ports may differ - always check YOUR fork output

# Query ethereum contracts (use YOUR actual port from fork output)
cast call 0xe1De... "owner()(address)" --rpc-url http://localhost:<YOUR-ETH-PORT>
cast call 0xcf4ec... "owner()(address)" --rpc-url http://localhost:<YOUR-ETH-PORT>

# Query arbitrum contracts (use YOUR actual port from fork output)
cast call 0xAd435... "owner()(address)" --rpc-url http://localhost:<YOUR-ARB-PORT>

Create complete owner mapping:

{
  "<chainId>_<contractAddress>": "<actual-owner-address>",
  "1_0xe1De9910fe71cC216490AC7FCF019e13a34481D7": "0x3965...",
  "1_0xcf4ecA86606372B975FaF04a97e8eE3AfeA5a02D": "0x8Ff4..."
}

Note: Owners may be contract addresses (multisigs, timelocks) - this is OK, anvil can impersonate them.

5c. Detect Multiple Owners Per Chain

Group transactions by chain and owner to detect if splitting is needed:

# Pseudocode logic
for each chain:
    unique_owners = set of owners for contracts on this chain
    if len(unique_owners) > 1:
        # Chain has multiple owners - splitting required
        splitting_needed = True

If ANY chain has multiple owners: Proceed to Step 5d (split transactions) If all chains have single owner: Skip to Step 5e (create single strategy)

5d. Split Transactions by Owner (If Multiple Owners Detected)

When to do this: If Step 5c detected multiple owners per chain.

How to split:

  1. Create owner-to-contract mapping from Step 5b
  2. Group transactions by the owner of their target contract
  3. Create separate transaction files for each unique owner
  4. Each file contains only transactions targeting contracts owned by that owner
# Split transactions by owner
owner_groups = group_by(transactions, lambda tx: owner_map[f"{tx.chainId}_{tx.to}"])

for owner, txs in owner_groups:
    write_file(f"/tmp/transactions-owner-{owner}.json", txs)

Example: If 171 transactions target 17 different owners across 6 chains, create 17 separate transaction files.

Proceed to Step 5f for each split file.

5e. Create Single Strategy File (If Single Owner Per Chain)

When to do this: If Step 5c found no multiple owners per chain.

Create one strategy file with one owner per chain:

<chain-name>:
  submitter:
    chain: <chain-name>
    type: impersonatedAccount
    userAddress: '<actual-owner-from-step-5b>'

Save to /tmp/<route>-strategy.yaml

Proceed to Step 5g.

5f. Create Multiple Strategy Files (If Transactions Were Split)

When to do this: If Step 5d split transactions.

For each split transaction file, create a corresponding strategy file:

  1. Identify which chains are in that transaction file
  2. For each chain, use the owner that matches the split group
  3. Create chain-specific strategy with that owner
<chain-name>:
  submitter:
    chain: <chain-name>
    type: impersonatedAccount
    userAddress: '<owner-for-this-split>'

Save each as /tmp/strategy-owner-<owner>.yaml

5g. Fund All Owner Accounts

Use anvil_setBalance to fund ALL unique owner addresses on their respective chains (use ACTUAL ports from fork output):

BALANCE="0x56BC75E2D63100000"  # 100 ETH

# For each owner on each chain (use YOUR ACTUAL port from fork output):
cast rpc anvil_setBalance <owner-address> $BALANCE --rpc-url http://localhost:<YOUR-ACTUAL-PORT>

Example (ports shown are examples - use YOUR actual ports from fork output):

# Example assumes fork output showed ethereum at port 8548
# YOUR port may differ - use YOUR actual port from fork output

cast rpc anvil_setBalance 0x3965... $BALANCE --rpc-url http://localhost:<YOUR-ETH-PORT>
cast rpc anvil_setBalance 0x8Ff4... $BALANCE --rpc-url http://localhost:<YOUR-ETH-PORT>

Fund ALL unique owners you identified in Step 5b.

Step 6: Submit Transactions

6a. Determine Submission Approach
  • Single strategy: Submit once with one transaction file (from Step 5e)
  • Multiple strategies: Submit multiple times, once per split file (from Step 5f)
6b. Submit Each Transaction File

For each transaction file and its corresponding strategy:

# Set dummy HYP_KEY (required even with impersonation)
HYP_KEY="0x0000000000000000000000000000000000000000000000000000000000000001"

# Submit transactions using installed hyperlane command with ACTUAL forked registry URL
hyperlane submit \
  --transactions <transactions-file> \
  --strategy <corresponding-strategy-file> \
  --registry <YOUR-ACTUAL-forked-registry-url-from-step-4> \
  --yes

CRITICAL:

  • Use the installed hyperlane command
  • No need for --id parameter - submit auto-detects chains from transactions file
  • Use YOUR ACTUAL forked registry URL from Step 4 (NOT the source registry URL from Step 3)
  • Dummy HYP_KEY is required even with impersonation

Track submission results:

  • Count successful submissions
  • Count failed submissions
  • Note any error messages

If using split files: Submit all files sequentially, tracking success/failure for each.

Step 7: Decode Transactions with Heimdall

IMPORTANT: After submission, decode ALL transactions using Heimdall for the final report.

# For each transaction in the original combined file:
for tx in transactions:
    decoded = run_command(f"heimdall decode {tx.data} --default")
    store_decoded(tx.chainId, tx.to, tx.annotation, decoded)

Save decoded output as JSON for inclusion in final report:

[
  {
    "index": 1,
    "chain": "ethereum",
    "to": "0x...",
    "annotation": "...",
    "decoded": "..."
  }
]

Save to /tmp/decoded-transactions.json

Step 8: Run Warp Check

Execute hyperlane warp check against the forked registry (use YOUR ACTUAL URL from Step 4):

hyperlane warp check \
  --id <warp-route-id> \
  --registry <YOUR-ACTUAL-forked-registry-url-from-step-4> \
  --yes

Capture full output to /tmp/warp-check.log for the final report.

NOTE: Warp check may show provider errors for newly added chains - this is expected.

Step 8b: Test transferRemote on Fork

After verifying configuration with warp check, test that an actual token transfer works on the forked chains. This catches issues that config checks miss: broken transferRemote calldata, token accounting bugs (lock/mint/burn/unlock), and message dispatch failures.

8b.1 Pick an Origin → Destination Pair

Choose one origin → destination pair from the warp route. Prefer a pair where both chains have transactions (i.e., both were modified), so you're testing the freshly configured path.

8b.2 Bypass ISM on Destination (Required for Self-Relay)

Forked mainnet chains have real multisig ISMs that require validator signatures. Since there are no validators on anvil forks, you must set a permissive ISM on the destination so self-relay can deliver the message.

# 1. Get the mailbox address for the destination chain from the forked registry
DEST_MAILBOX=$(cast call <warp-router-address> "mailbox()(address)" --rpc-url http://localhost:<DEST-PORT>)

# 2. Get the mailbox owner
MAILBOX_OWNER=$(cast call $DEST_MAILBOX "owner()(address)" --rpc-url http://localhost:<DEST-PORT>)

# 3. Get the current default ISM (to restore later if needed)
OLD_ISM=$(cast call $DEST_MAILBOX "defaultIsm()(address)" --rpc-url http://localhost:<DEST-PORT>)

# 4. Deploy Hyperlane's TestIsm.
# TestIsm returns Types.NULL from moduleType() and true from verify(bytes,bytes),
# so `hyperlane warp send --relay` uses null metadata instead of trying to derive
# multisig/routing metadata on the fork.
TEST_DEPLOYER_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
# Ensure Foundry dependencies are materialized in fresh worktrees/checkouts.
pnpm -C solidity deps:soldeer
TEST_ISM_ADDRESS=$(forge create \
  --root solidity \
  --broadcast \
  --rpc-url http://localhost:<DEST-PORT> \
  --private-key $TEST_DEPLOYER_KEY \
  contracts/test/TestIsm.sol:TestIsm \
  | awk '/Deployed to:/ {print $3}')

# Confirm the fork bypass ISM has the expected Hyperlane behavior:
# moduleType() == 6 (Types.NULL) and verify(...) == true.
cast call $TEST_ISM_ADDRESS "moduleType()(uint8)" --rpc-url http://localhost:<DEST-PORT>
cast call $TEST_ISM_ADDRESS "verify(bytes,bytes)(bool)" 0x 0x --rpc-url http://localhost:<DEST-PORT>

# 5. Impersonate mailbox owner and set the permissive ISM
cast rpc anvil_impersonateAccount $MAILBOX_OWNER --rpc-url http://localhost:<DEST-PORT>
BALANCE="0x56BC75E2D63100000"
cast rpc anvil_setBalance $MAILBOX_OWNER $BALANCE --rpc-url http://localhost:<DEST-PORT>
cast send $DEST_MAILBOX "setDefaultIsm(address)" $TEST_ISM_ADDRESS --from $MAILBOX_OWNER --rpc-url http://localhost:<DEST-PORT> --unlocked
cast rpc anvil_stopImpersonatingAccount $MAILBOX_OWNER --rpc-url http://localhost:<DEST-PORT>

NOTE: If the warp router has a custom ISM set (not using the mailbox default), you'll need to impersonate the router owner and call setInterchainSecurityModule(address) on the router instead.

8b.3 Fund the Test Sender
# Generate a test private key (or use a well-known anvil key)
TEST_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
TEST_SENDER="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"  # Anvil account 0

# Fund with native gas on origin
cast rpc anvil_setBalance $TEST_SENDER "0x56BC75E2D63100000" --rpc-url http://localhost:<ORIGIN-PORT>

# For collateral/ERC20 routes: mint or transfer tokens to the test sender
# Option A: If the token has a mint function (e.g., test tokens)
# cast send <token-address> "mint(address,uint256)" $TEST_SENDER 1000000000000000000 ...

# Option B: Impersonate a whale holder and transfer
# Find a top holder from the fork state, impersonate, and transfer
# cast rpc anvil_impersonateAccount <whale> --rpc-url http://localhost:<ORIGIN-PORT>
# cast send <token-address> "transfer(address,uint256)" $TEST_SENDER <amount> --from <whale> --rpc-url http://localhost:<ORIGIN-PORT> --unlocked

# Option C: For native token routes, setBalance is sufficient (already done above)

# Also fund on destination for self-relay gas
cast rpc anvil_setBalance $TEST_SENDER "0x56BC75E2D63100000" --rpc-url http://localhost:<DEST-PORT>
8b.4 Run the Transfer
HYP_KEY=$TEST_KEY \
hyperlane warp send \
  --registry <YOUR-ACTUAL-forked-registry-url-from-step-4> \
  --warp-route-id <warp-route-id> \
  --origin <origin-chain> \
  --destination <destination-chain> \
  --amount 1 \
  --relay \
  --skip-validation \
  --yes

Key flags:

  • --relay — Self-relay the message (no real relayer on forks)
  • --skip-validation — Skip balance/collateral checks that may fail on forks
  • --amount 1 — Use smallest practical amount (1 wei or 1 unit)
8b.5 Verify Result
  • Success: The CLI should log Transfer was self-relayed! and show the message ID
  • Failure: Check the error output. Common issues:
    • Ownable: caller is not the owner on ISM bypass → wrong mailbox owner or router has custom ISM
    • insufficient funds → test sender not funded with tokens (for collateral routes)
    • message already delivered → ISM bypass not working, retry with router-level ISM override
    • Timeout waiting for delivery → self-relay failed, check ISM was properly set

Capture output to /tmp/warp-transfer-test.log for the final report.

NOTE: This step is optional but strongly recommended. If it fails due to ISM complexity (e.g., routing ISMs, aggregation ISMs), document the failure and move on — the config check in Step 8 still validates the deployment.

Step 9: Interpret Results

Analyze the warp check output carefully:

CRITICAL: Focus on violations for chains that have transactions. Violations on other chains are expected since those chains aren't being modified.

Expected Violations (✅ These are OK)

1. Violations on chains WITHOUT transactions: ALWAYS EXPECTED

avalanche: # ← No transactions for this chain
  proxyAdmin:
    owner:
      EXPECTED: '0x...'
      ACTUAL: '0x...'

Why: Chains not in the transactions file are not being modified. Their violations show the gap between current and target state.

2. Ownership mismatches on chains WITH transactions: EXPECTED if transactions don't include ownership transfers

ethereum: # ← Has transactions, but ownership not transferred yet
  proxyAdmin:
    owner:
      EXPECTED: '0x...'
      ACTUAL: '0x...'

Why: The registry shows TARGET state after full deployment. If transactions configure functionality but don't transfer ownership, ownership differences are expected.

3. Fee configuration differences: MAY BE EXPECTED

ethereum:
  tokenFee:
    maxFee:
      ACTUAL: '115792...'
      EXPECTED: ''

Why: Transactions may SET fee values that aren't in the registry config yet.

Real Violations (❌ These are problems)

These indicate actual configuration issues on chains WITH transactions:

ethereum: # ← Has transactions but missing expected config
  remoteRouters:
    42161: # arbitrum domain
      ACTUAL: ''
      EXPECTED: '0x...'

Real problems to look for:

  • Missing ISM configurations that should have been set
  • Missing remote router enrollments that transactions should have added
  • Missing destination gas settings that transactions should have configured
  • Incorrect xERC20 limits (except for new chains showing 0.0)

Key: Only violations on chains that have transactions AND should have been fixed by those transactions are real problems.

Step 10: Generate Final Report

Create a comprehensive markdown report with the following sections:

Report Structure
# <WARP_ROUTE_ID> Validation Report

**Date:** <timestamp>
**Branch:** <registry-branch>
**Transaction File:** <filename>

## Executive Summary

[Overall pass/fail verdict]
[Key metrics: total transactions, chains modified, success rate]

## Port Detection (Dynamic)

[List all ports extracted from fork output]

- HTTP Registry: <actual-port>
- Forked Registry: <actual-port>
- Chain Ports: [chain: port mapping]

## Transaction Breakdown

[Table showing chains with transactions, counts, owners]

## Submission Results

[Detailed results of transaction submission]

- Split files created (if applicable)
- Submission success/failure counts
- Any errors encountered

## Warp Check Results

### Chains WITH Transactions

[Analysis of functional configuration]

- Routers: [status]
- Destination gas: [status]
- ISM: [status]
- Bridge allowances: [status]

### Chains WITHOUT Transactions

[List of chains not modified and their expected violations]

## Transfer Test Results

- **Origin → Destination**: <chain-a> → <chain-b>
- **Amount**: <amount>
- **Result**: ✅ Success / ❌ Failed (reason)
- **Message ID**: <id> (if successful)
- **ISM Bypass**: Yes (TestIsm deployed at <address> set on destination mailbox)

## Decoded Transactions (Heimdall Output)

```json
[Full decoded transaction output from /tmp/decoded-transactions.json]
```

Warp Check Output (Raw)

[Full warp check output from /tmp/warp-check.log]

Conclusion

[Final verdict with supporting rationale]


**Save report** to `/tmp/<route>-validation-report.md`

### Step 11: Cleanup

- Clean up **both** background processes:
  - Stop the `hyperlane warp fork` server
  - Stop the `http_registry` server
- **Switch registry branch back to main**: `git -C "$REGISTRY_PATH" checkout main`
- Confirm cleanup is complete

```bash
# Kill background processes
pkill -f "hyperlane warp fork"
pkill -f "http_registry"

# Switch back to main (use $REGISTRY_PATH variable)
git -C "$REGISTRY_PATH" checkout main

Common Issues & Solutions

"Ownable: caller is not the owner"

  • Cause: Using wrong owner address or single-owner strategy with multi-owner transactions
  • Solution: Always query actual owners (Step 5b) and split transactions if multiple owners detected (Step 5d)

"Insufficient funds for intrinsic transaction cost"

  • Cause: Impersonated accounts have no balance on forked chains
  • Solution: Use anvil_setBalance (Step 5g) for ALL unique owners

Multiple owners per chain causing submission failure

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
75
Forks
601
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
warp-route-check
Source
github.com/hyperlane-xyz/hyperlane-monorepo