Warp Route Deploy Preflight Check

SkillCloud & infra

Pre-flight gas and balance check before deploying a warp route. Reads a Linear ticket, checks deployer wallet native gas balances per chain (warn if <$10), and checks collateral token balance (need ~$1 for testing).

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 Deploy Preflight Check skill

What this skill tells your AI

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

You are checking whether a deployer wallet has sufficient funds (gas + collateral tokens) before deploying a new warp route.

Run Log (mandatory)

Maintain the durable, per-ticket run log per /warp-run-log — that skill owns the storage contract (Linear-document-by-title primary, single-writer discipline, local-file fallback), the chain | protocol | shape | floor | actual | verdict machine-row + prose entry shape, and the surface-the-URL-as-proof hard gate. Use warp-deploy-fund-deployer as the skill name in each prose entry, and do not report this skill complete until the run-log URL has been surfaced.

Log at least: (a) skill entry with the ticket ID + deployer address, (b) every [CONFIRM:] gate — before showing it to the user AND after their response, (c) every balance-check result per chain (expected floor vs actual balance, in native token units + USD), (d) every funding-command execution (amount, tx hash, wall-clock), (e) skill exit (success or bail-out). Once the deploy runs and the actual on-chain consumption is known, append a post-hoc row per chain so the next floor revision can compare floor to reality. Log smooth steps too — success data grounds the retrospective as much as failure data.

Input

The user provides:

  • Linear ticket URL or ID (required, e.g. ENG-3516)
  • Deployer address (optional — auto-loaded from key-context if absent; see below)

Key Context (Prerequisite)

This skill funds the deployer address(es) the warp-deploy chain will use. It auto-loads ~/.hyperlane/key-contexts/<ticket-id>.yaml produced by /warp-deploy-select-keys. If the artifact does not exist and the user did not provide an explicit deployer address, invoke /warp-deploy-select-keys <ticket-id> first.

For each protocol resolved in the artifact, the recipient address for funding on that protocol's chains is keys.<protocol>.address. A pure-EVM route funds one EVM address across all EVM chains; a cross-VM route funds the protocol-matching address per chain (e.g. EVM address on EVM chains, SVM address on Solana, etc.). If the user supplied an explicit deployer address that does NOT match any address in the artifact, surface this discrepancy before proceeding.


Step 1: Fetch the Linear Ticket

Fetch the ticket per /fetch-linear-ticket. Read the token, chains, and "Did they send funds" details from the returned description.


Step 2: Extract Chains and Token Details

Parse the ticket to extract:

FieldDescription
Token symbole.g. USDC, RISE
Collateral chain(s)Chain(s) where the real token lives, with token address
Synthetic chain(s)Chains getting a synthetic (bridged) representation
Native chain(s)Chains where the native gas token is bridged (no token address)

Classify each chain as collateral, synthetic, or native.


Step 3: Get Registry Path and RPC URLs

REGISTRY_PATH="${HYPERLANE_REGISTRY:-$(pwd)/../hyperlane-registry}"
echo "Registry: $REGISTRY_PATH"

For each chain in the warp route, get the RPC URL from the registry:

# Get the first rpcUrl for a chain
cat "$REGISTRY_PATH/chains/<chain>/metadata.yaml" | grep -A2 "rpcUrls:" | head -3

Extract the http URL from the first entry under rpcUrls. Also extract nativeCurrency.symbol and nativeCurrency.decimals for display, and the coingeckoId field for price lookups.

If a chain is not in the registry, warn the user and skip that chain.


Step 4: Fetch Current Gas Price and Compute Required Balance Per Chain

The CLI uses this exact formula to determine the minimum native balance needed for a warp deploy (from typescript/cli/src/consts.ts and typescript/cli/src/utils/balances.ts):

required_wei = currentGasPrice_wei × WARP_DEPLOY_GAS (30,000,000)
required_native = required_wei / 10^decimals

For each EVM chain, fetch the current gas price via JSON-RPC:

curl -s -X POST -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":1}' \
  <RPC_URL>
# Returns hex gas price in wei, e.g. "0x3B9ACA00" = 1 gwei

Then compute:

WARP_DEPLOY_GAS = 30_000_000
gas_price_wei   = int(hex_result, 16)
required_wei    = gas_price_wei * WARP_DEPLOY_GAS
required_native = required_wei / 10**18   # (adjust decimals for non-18-decimal chains)

Add a 2× safety buffer on top of this: gas prices can spike between check time and deploy time, and a multi-collateral route deploys more contracts than a standard route.

required_with_buffer = required_native * 2

This floor is a deliberate conservative CEILING, not a cost estimate. WARP_DEPLOY_GAS = 30_000_000 is the SDK's upper-bound constant; a typical single-chain EVM warp deploy actually burns ~3–5M gas, so 30M × 2× over-provisions the real single-attempt cost by roughly 5–10×. That is intentional — an out-of-gas failure mid-deploy is far worse than leaving unused native in the deployer key (the excess is not lost; it stays available for later deploys). Consequences to keep in mind: the balance check may report ⚠️ LOW and fund a chain that already had enough for the actual burn, and the >10 USD warning below may trip on the ceiling even when the true cost is under $10. Present the number to the operator as a conservative ceiling, never as "this deploy will cost X". The precise per-route-shape figure comes from getMinGasForWarpDeploy(config) on IProtocolProvider (PR #9075) — once that lands, consume it instead of this flat ceiling. Until then, always log the actual post-deploy burn next to this floor (see the Run Log) so the estimate can be tightened.

Fetch native token USD prices via CoinGecko to display USD equivalents (for informational purposes and to trigger the >10 USD warning):

curl -s "https://api.coingecko.com/api/v3/simple/price?ids=<id1>,<id2>&vs_currencies=usd"

Common native token CoinGecko IDs:

ChainCoinGecko ID
ethereumethereum
arbitrumethereum
baseethereum
optimismethereum
polygonpolygon-ecosystem-token
bscbinancecoin
avalancheavalanche-2
solanasolana
celocelo
gnosisxdai

If CoinGecko fails for a chain, do not silently fall back to 0 USD — that would bypass the 10 USD warning and the funding script's MAX_FUNDING_AMOUNT_IN_USD safety bound. Instead, the agent tries alternative price venues in order before escalating to the user:

  1. CoinMarketCaphttps://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/latest (requires CMC_API_KEY).
  2. Binance public APIhttps://api.binance.com/api/v3/ticker/price?symbol=<SYMBOL>USDT for tokens with active Binance markets.
  3. Uniswap on-chain quote — query the relevant Uniswap V3 pool's slot0 against a stablecoin pair (USDC / USDT) on the same chain; useful for tokens with liquidity but no CEX listing.
  4. Only if all of the above fail: surface the missing price to the user and ask them to supply a manual override via the funding script's --price flag (see Step 8).

When an alternative venue produces a price, log which venue and the value (so the operator can audit). The fund-wallet script's MAX_FUNDING_AMOUNT_IN_USD safety bound stays in force regardless of which source produced the price.

⚠️ Warning threshold: If the required amount (with buffer) exceeds 10 USD, warn the user explicitly before running funding commands. On gas-market chains this is the conservative ceiling described above, so it may exceed $10 even when the true deploy cost is well under it — frame the warning as "conservative ceiling ≈ $X (actual typically far lower)", not as a firm cost, and still surface it so the operator can sanity-check an unusually high number (e.g. a genuine gas spike).


Step 5: Check Deployer Native Gas Balance Per Chain

For each chain in the warp route, resolve the deployer's per-protocol address from the key-context artifact (keys.<protocol>.address) and check the native balance against the chain's required floor for this route's shape.

Cost models — and why "trust the CLI check" is not enough

Chains bill for a warp-deploy in different ways, and the CLI's shared preflight (nativeBalancesAreSufficient at typescript/cli/src/utils/balances.ts) only handles the linear one:

  • EVM (and any chain with a non-null gasPrice in the registry) — linear gas market. Cost = gasPrice × units. The CLI computes this correctly.
  • Sealevel (Solana, Eclipse) — rent-exempt reserve. Cost = the sum of rent-exempt lamports locked in the accounts the deploy creates (program data, token PDA, ATA payer, fee program if present). Not a gasPrice × units product; SVM has gasPrice: null in the registry.
  • Tron — energy + bandwidth. Cost = TRX burned for bandwidth (broadcast) and energy (contract execution). Not a gasPrice × units product; Tron has gasPrice: null in the registry.

nativeBalancesAreSufficient short-circuits on any chain with gasPrice: null (if (!gasPrice) return; at balances.ts:79-81) — it reports the chain as "OK" without checking anything. An under-funded SVM or Tron leg sails through the CLI preflight and fails mid-deploy.

Do NOT treat a silent CLI pass on a null-gasPrice chain as ✅ OK. For those chains, use the per-protocol / per-route-shape floor tables below and check the balance manually.

For gas-market chains (EVM, and any non-EVM chain with a non-null gasPrice)

Use the CLI's formula (matches nativeBalancesAreSufficient):

  • requiredUnits = protocolClient.getMinGas().WARP_DEPLOY_GAS
  • requiredNative = requiredUnits × gasPricegasPrice from provider.getGasPrice() (EVM) or multiProvider.getChainMetadata(chain).gasPrice.amount (altvm)
  • actualBalance = provider.getBalance(address) (EVM) or signer.getBalance({ address, denom }) (altvm)
  • Apply a 2× safety buffer on requiredNative — gas can spike between preflight and deploy

Interfaces: getProtocolProvider(protocol).getMinGas() per altvm SDK (typescript/<protocol>-sdk/src/clients/protocol.ts); IProvider.getBalance from typescript/provider-sdk/src/altvm.ts.

For null-gasPrice chains (SVM, Tron)

The flat getMinGas().WARP_DEPLOY_GAS constant on these SDKs is calibrated for a vanilla synthetic-or-collateral router. It's silently wrong for cross-collateral + fee routes: on SVM those add per-program rent-exempt reserves that a single flat number can't express; on Tron the extra energy/bandwidth needed for the additional contracts likewise isn't captured. Use the shape-aware floors below, matched to what the ticket says the route will look like on this chain.

Determining a chain's route shape from the ticket (before deploy.yaml exists). "Shape" here is the on-chain token type deployed on this chain — that is what sets the floor — NOT the route's topology. Do not confuse a multi-collateral route (more than one collateral chain) with the crossCollateral token type: multi-collateral is simply several independent plain collateral routers, whereas crossCollateral is a distinct token type (collateral↔collateral swaps backed by shared cross-collateral routers) with a higher per-chain floor. They are orthogonal — a route can have many collateral chains and none of them be crossCollateral.

  • Read each chain's token type from its per-chain designation in the ticket (collateral / synthetic / native / crossCollateral). Several collateral chains on their own → each is a plain collateral router (base floor), not crossCollateral.
  • Classify a chain as crossCollateral only when the ticket explicitly calls for cross-collateral / collateral-swap behavior on it — never merely because the route lists multiple collateral chains.
  • Ticket has a Warp Fee row set → each chain quoting a fee has a fee program.
  • Ticket calls for a custom ISM or hook beyond the mailbox default → each such chain deploys an extra program.

SVM (solanamainnet, eclipsemainnet) — required floor in SOL:

Route shape (this chain)FloorNotes
Base collateral / synthetic router, no fee2.6 SOLProgram + token PDA + ATA payer rent. Empirically validated on prior deploys.
crossCollateral router, no fee≥3.7 SOLComposed: base 2.6 + cross-collateral extra 1.1. Not yet empirically validated — bias high and log the actual.
synthetic / collateral router + fee program on this chain≥5.1 SOLComposed: base 2.6 + fee program 2.5. Not yet empirically validated — bias high and log the actual.
crossCollateral router + fee program on this chain6.5 SOLComposed constants sum to 6.2 (2.6 + 1.1 + 2.5); observed cost exceeds that — use 6.5 until the actual is measured with more precision.
Any of the above + custom ISM deployment on this chain+?Not yet quantified — capture the actual during deploy and log it.
Any of the above + custom hook deployment on this chain+?Not yet quantified — capture the actual during deploy and log it.

Tron (tron) — required floor in TRX:

Route shape (this chain)FloorNotes
Base collateral / synthetic router, no fee1000 TRXValidated empirically on prior Tron warp-route deploys
Cross-collateral router, or router + fee program1500 TRXConservative; actual not yet measured — capture during deploy and log it

Tron bills in energy + bandwidth, not gasPrice × units, so no formula generalizes; the table is the source of truth for this skill.

Manual balance checks for null-gasPrice chains:

# SVM
solana balance <address> --url <mainnet-rpc>

# Tron (via TronGrid)
curl -s "https://api.trongrid.io/v1/accounts/<T-address>" | jq '.data[0].balance / 1e6'

Reporting

Per chain, emit one of:

  • OK — actual ≥ floor (gas-market chains include the 2× buffer; null-gasPrice chains use the table floors as-is — they already include headroom)
  • ⚠️ LOW — 0 < actual < floor (fund + continue)
  • EMPTY — actual = 0 (fund + flag)

For null-gasPrice chains, ALWAYS state the applied shape and floor explicitly — the reader must not confuse this with the CLI's silent pass:

Chain: ethereum (evm)  [gas-market]
  WARP_DEPLOY_GAS: 30_000_000 × 0.13 gwei → 0.0078 ETH (2× buffer) = ~16.09 USD
  Balance:         0.002 ETH (~4.13 USD)
  ⚠️  SHORT: fund 0.006 ETH (~12.38 USD more)

Chain: solanamainnet (svm)  [null-gasPrice — CLI preflight does NOT apply]
  Route shape on this chain: crossCollateral router + fee program  (example — actual shape comes from the ticket)
  Applied floor:  6.5 SOL (composed 2.6 + 1.1 + 2.5 = 6.2, observed slightly higher; log the actual once known)
  Balance:        0.3 SOL
  ⚠️  SHORT: fund 6.2 SOL more

Chain: tron (tron)  [null-gasPrice — CLI preflight does NOT apply]
  Route shape on this chain: cross-collateral + fee  (example — actual shape comes from the ticket)
  Applied floor:  1500 TRX  (conservative; capture the deploy's actual consumption during the run and log it)
  Balance:        0 TRX
  ❌  EMPTY: fund 1500 TRX

⚠️ Warning threshold: if any chain's required amount (gas-market chains: with buffer; null-gasPrice chains: floor) exceeds 10 USD, warn explicitly before running funding commands.


Step 6: Check Collateral Token Balance (If Applicable)

For each collateral chain, check if the deployer holds the collateral token. For testing, 1 USD worth is sufficient.

Step 6a: Detect ERC4626 Vault Tokens

Before checking balances, determine whether the collateral token is an ERC4626 vault. Call asset() on the token contract:

cast call <TOKEN_ADDRESS> "asset()(address)" --rpc-url <RPC_URL>
  • If the call succeeds and returns a non-zero address, the token is an ERC4626 vault. Use the returned address as the source token for funding — the deployer needs the underlying asset, not the vault share.
  • If the call reverts or returns zero, the token is a standard ERC20 — proceed as normal.

When an ERC4626 is detected, note it clearly:

Chain: ethereum (collateral)
  Token: wsETH (0xAbc...) — ERC4626 vault
  Underlying asset: wstETH (0xDef...)
  → Funding will be requested in wstETH, not wsETH

For the rest of Step 6, replace TOKEN_ADDRESS with the underlying asset address when an ERC4626 is detected.

Step 6b: Check Balance

# Get raw balance (returns token units in smallest denomination)
cast call <TOKEN_ADDRESS> "balanceOf(address)(uint256)" <DEPLOYER_ADDRESS> --rpc-url <RPC_URL>

Get token decimals:

cast call <TOKEN_ADDRESS> "decimals()(uint8)" --rpc-url <RPC_URL>

Convert: balance / 10^decimals = human-readable amount.

On Tron: cast / eth_call against the public trongrid JSON-RPC endpoint is unsupported and the balanceOf / decimals calls above will fail. Read Tron contract state via TronGrid's HTTP API (triggerconstantcontract for balanceOf; the /v1/accounts endpoint for native TRX, as in Step 5), or point --rpc-url at a keyed Tron RPC (e.g. Alchemy) that does support eth_call. Resolve the keyed endpoint through the CI=false HTTP-registry path, never the public default.

For price of the collateral token, use CoinGecko contract address lookup:

curl -s "https://api.coingecko.com/api/v3/simple/token_price/<platform>?contract_addresses=<TOKEN_ADDRESS>&vs_currencies=usd"

Where <platform> is the CoinGecko platform ID for the chain (e.g. ethereum, arbitrum-one, base, polygon-pos, binance-smart-chain).

If CoinGecko has no price for the collateral token, do not invent one with hardcoded fallbacks — those silently bypass the MAX_FUNDING_AMOUNT_IN_USD safety bound. The agent uses the same alternative-venue chain as Step 4: CoinMarketCap → Binance → Uniswap on-chain quote → only as a last resort, ask the user for a manual --price <usd> override on fund-wallet-from-deployer-key.ts (see Step 8). Always log which venue produced the price.

Threshold: 1 USD worth of collateral token (or its underlying asset if ERC4626)

Report:

  • OK — holds >= 1 USD of collateral token (or underlying asset)
  • ⚠️ LOW — holds > 0 but < 1 USD (may be enough if price is just unavailable)
  • NONE — zero balance — must acquire some for testing

When insufficient:

Chain: ethereum (collateral)
  Token: USDC (0xA0b8...)
  Balance: 0 USDC
  ❌  Need at least 1 USD of USDC for testing. Request from faucet or transfer a small amount.

For ERC4626 vaults with insufficient underlying asset:

Chain: ethereum (collateral)
  Token: wsETH (0xAbc...) — ERC4626 vault → underlying: wstETH (0xDef...)
  Balance: 0 wstETH
  ❌  Need at least 1 USD of wstETH (underlying asset) for testing.

Step 7: Summary Report

Print a clear summary table:

## Preflight Check: <TOKEN_SYMBOL> Warp Route
Deployer: <ADDRESS>

### Gas Balance Check (per-chain route-shape floor; $10 is a warning threshold, not a minimum)

| Chain     | Type        | Balance          | USD Value | Status |
|-----------|-------------|------------------|-----------|--------|
| ethereum  | collateral  | 0.01 ETH         | 22.00 USD | ✅ OK  |
| arbitrum  | synthetic   | 0.002 ETH        | 4.40 USD  | ⚠️ LOW |
| base      | synthetic   | 0 ETH            | 0.00 USD  | ❌ EMPTY |

### Collateral Token Check (min 1 USD for testing)

| Chain     | Token | Balance    | USD Value | Status |
|-----------|-------|------------|-----------|--------|
| ethereum  | USDC  | 5.00 USDC  | 5.00 USD  | ✅ OK  |

If all balances are sufficient, say ✅ All chains funded — ready to deploy! and stop.

If any chain needs funding, proceed to Step 8.


Step 8: Generate and Run Funding Commands

For every chain with insufficient gas or collateral, generate the exact funding commands using the deployer funding script.

The script must be run from the typescript/infra directory, and every invocation MUST be prefixed with CI=false (scoped to the single command — do NOT export CI=false globally). Without this, getRegistryForEnvironment on Haggis workers and other CI-flagged environments silently loads public rate-limited RPCs (e.g. Tron's trongrid.io at 3 rps → 429 during broadcasts) instead of the private GCP-stored keyed endpoints. See the CI-mode section in [[reference-haggis-sandbox]] for the full explanation.

cd typescript/infra

Gas funding (native token — no -t or -s flag)

CI=false pnpm tsx scripts/funding/fund-wallet-from-deployer-key.ts \
  --recipient <DEPLOYER_ADDRESS> \
  --amount <AMOUNT> \
  -e mainnet3 \
  -c <CHAIN>

Collateral token funding by symbol (preferred for known tokens like USDC)

CI=false pnpm tsx scripts/funding/fund-wallet-from-deployer-key.ts \
  --recipient <DEPLOYER_ADDRESS> \
  --amount <AMOUNT> \
  -e mainnet3 \
  -c <CHAIN> \
  -s <SYMBOL>

Collateral token funding by contract address (for unknown/custom tokens)

CI=false pnpm tsx scripts/funding/fund-wallet-from-deployer-key.ts \
  --recipient <DEPLOYER_ADDRESS> \
  --amount <AMOUNT> \
  -e mainnet3 \
  -c <CHAIN> \
  -t <TOKEN_ADDRESS>

For tokens not on CoinGecko (new launches, testnet tokens, internal tokens): the agent should first exhaust the alternative price venues per Step 4 / Step 6 (CoinMarketCap → Binance → Uniswap on-chain quote). Only when all of those fail should the agent ask the user for a manual --price <usd-per-unit> override. Example, passing a manual price of $0.50 as a last resort:

CI=false pnpm tsx scripts/funding/fund-wallet-from-deployer-key.ts \
  --recipient <DEPLOYER_ADDRESS> \
  --amount <AMOUNT> \
  -e mainnet3 \
  -c <CHAIN> \
  -t <TOKEN_ADDRESS> \
  --price 0.5

The script rejects --price 0 and any non-positive / non-finite value — the safety bound (MAX_FUNDING_AMOUNT_IN_USD) requires a positive price. Without --price (or with an invalid value), the script hard-fails when CoinGecko has no price for the token. Whenever the agent supplies a price discovered from an alternative venue (CoinMarketCap / Binance / Uniswap), it should pass that via --price.

ERC4626 vaults: use the underlying asset address (from asset()) as <TOKEN_ADDRESS>, not the vault token address. The deployer needs the underlying asset to test the route; the vault share is not what gets transferred during bridging tests.

Amount calculation

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-deploy-fund-deployer
Source
github.com/hyperlane-xyz/hyperlane-monorepo