Warp Route Deploy Init
SkillCloud & infraFirst step of deploying a new warp route (steps 1–9). Reads a Linear ticket to extract token details and chain configuration, looks up mailbox addresses from the local registry, generates the deploy.yaml, runs the deploy, and optionally runs a send test. Follow up with /warp-deploy-update-owners.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Warp Route Deploy Init skill
What this skill tells your AI
The instructions your AI receives, as published by hyperlane-xyz/hyperlane-monorepo in .claude/skills/warp-deploy-init-route/SKILL.md and read by ahel’s review.
You are generating the initial deploy.yaml for a new Hyperlane warp route deployment.
Step 0: Confirm the working environment
Before anything else, surface the checkout you're operating in so a stale-branch or wrong-directory run is visible up front:
git rev-parse --show-toplevel && git rev-parse --abbrev-ref HEAD
Skills load from whatever branch is checked out — running an older branch's copy silently follows outdated instructions. Confirm with the operator that this is the intended monorepo + branch before proceeding. (This is a visibility check; a hard "you must be on branch X" validation belongs at the harness level, since the skill can't know the intended branch on its own.)
PREREQUISITE (per-chain gate): confirmed sufficient deployer funds
Before running warp deploy, EVERY chain in the route must be in one of two states:
- Verified sufficient — the deployer's native balance (and, on collateral chains, ≥ 1 USD of the collateral token) has been checked via
/warp-deploy-fund-deployer— or an equivalent manual check appropriate to the chain's billing model — and reported ✅ OK for the route's shape (base-collateral vs cross-collateral vs cross-collateral + fee program each require different floors on non-EVM chains; see/warp-deploy-fund-deployerStep 5's shape table). - Verified short and topped up — a
⚠️ LOW/❌ EMPTYwas funded to ✅ OK before proceeding.
If you are unsure about a chain, treat it as unverified — run /warp-deploy-fund-deployer <ticket-id> and let it check. It is a preflight, not an unconditional funding action: chains already at ✅ OK are skipped, only shortfalls trigger transfers.
The gate is per-chain, not per-run. If a prior session already left ethereum at ✅ OK and nothing has changed since (no other deploys draining the key), skipping re-check on that chain is fine. Any chain that is unverified OR short must be resolved before Step 1.
Why: an under-funded chain fails mid-deploy after partial contract deployment on other chains, leaving orphaned artifacts that need manual cleanup before a retry. Fund-deployer's role is to catch shortfalls up front. The reactive text later in Step 8 ("insufficient gas → run /warp-deploy-fund-deployer first") is a defensive fallback for state that decayed between preflight and deploy — it is not a substitute for the preflight itself.
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-init-route 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, (b) every [CONFIRM:] gate — before showing it to the user AND after their response, (c) every command execution, with expected vs actual (gas amounts, tx hashes, deployed addresses, wall-clock times), (d) skill exit (success or bail-out). If any number, timing, or output diverges from what this skill's text predicts, log it — the diff is the input to the next skill revision. 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-3516orhttps://linear.app/hyperlane-xyz/issue/ENG-3516/...)
If the ticket is not provided, ask for it now.
Key Context (Prerequisite)
This skill needs deployer key(s) per protocol to sign the warp-deploy txs, and the matching deployer address per protocol to fill owner fields in the deploy.yaml. It auto-loads ~/.hyperlane/key-contexts/<ticket-id>.yaml produced by /warp-deploy-select-keys. If the artifact does not exist, invoke /warp-deploy-select-keys <ticket-id> first — do not ask the user for an env var name or a deployer address inline.
From the artifact, read per protocol:
keys.<protocol>.name— the GCP secret name (or env var name) for the signerkeys.<protocol>.address— the derived address used asownerin the deploy.yaml on all chains of that protocol
A pure-EVM route uses one ethereum key + one EVM owner address across all EVM chains. A cross-VM route uses one key + address per protocol. Real ownership is transferred later via /warp-deploy-update-owners — never use real Safe/ICA addresses in this step.
Step 1: Fetch the Linear Ticket
Fetch the ticket per /fetch-linear-ticket — it extracts the issue ID, fetches via the agent's Linear integration or the GraphQL API + LINEAR_API_KEY, halts if neither is configured, and shows the title + description. Extract the Step 2 fields from that returned description.
Step 2: Extract Warp Route Details
Parse the ticket description to extract the following. Read every value from the ticket itself — do not infer it by copying a similar-looking existing route's deploy.yaml. Existing production deploy.yamls are a reference for structural format only. The warp route ID, fee type, per-chain owners, and (for offchain-quoted fees) quote signers are ticket-specific and are the fields most often drafted wrong from a prior route — copy each verbatim from the ticket. Ask the user to clarify anything that is ambiguous or missing:
| Field | Description |
|---|---|
| Token name | Full name (e.g. RISE) |
| Token symbol | Symbol (e.g. RISE) |
| Warp route ID | The route's registry ID exactly as the ticket gives it (e.g. WBTC/staging). If the ticket states one, use it verbatim — do NOT synthesize a <TOKEN>/<chains> name. Only derive <TOKEN>/<chains-alphabetical> (Step 7a) when the ticket gives no explicit ID. |
| Decimals | Token decimals (e.g. 18) — use the reference table below for USDC; query on-chain if unsure |
| Collateral chain(s) | Chain(s) where the real token lives — may be multiple for multi-collateral routes |
| Collateral token address(es) | ERC-20 contract address per collateral chain — use the reference table below for USDC |
| Synthetic chains | Chains that get a synthetic (bridged) representation |
| Warp fee | Fee in basis points (bps) + direction (deposits / withdrawals) from the ticket's Warp Fee checkboxes |
| Fee type | The fee contract type the ticket specifies (LinearFee, OffchainQuotedLinearFee, …). Take it from the ticket — never default to whatever type a similar route happened to use. OffchainQuotedLinearFee additionally requires quoteSigners (below). |
| Fee owner | Address that receives fees — defaults to "Standard AW controlled ICA" per the ticket |
| Quote signers | OffchainQuotedLinearFee only: the EVM (hex) addresses authorized to sign off-chain quotes, from the ticket. Required whenever the fee type is offchain-quoted; omitting them ships a fee contract nobody can quote against. |
| Type overrides | Any chain that should be native instead of collateral/synthetic |
| Yield route type | If the ticket mentions yield/ERC4626/vault, determine the yield subtype (see below) |
| Daily Rate Limit | Optional amount (e.g. 200,000,000) — present in the structured Daily Rate Limit row on newer tickets. If present, the route adds a rate-limited hook on the synthetic chain (see Step 4). |
Validate the token symbol UNCONDITIONALLY and FIRST — before the eager logo download (below) or any other filesystem write, and regardless of whether the ticket gives an explicit route ID. <TOKEN> (the symbol) is used verbatim as the registry directory deployments/warp_routes/<TOKEN>/, as the logo path, and — when no explicit ID is given — as the base of the derived route ID; a symbol containing ../, whitespace, or shell metacharacters would escape the registry directory or inject into the curl/deploy commands well before Step 7 validates the derived ID. Require: a single path component matching ^[A-Za-z0-9._-]+$, neither . nor .., no whitespace, no shell metacharacters (; | & $ ` > < ( ) * ? \). Then build any path from it (e.g. the logo path or deploy.yaml path), resolve non-strictly (realpath -m), and assert the result stays under $REGISTRY_PATH/deployments/warp_routes/<TOKEN>/ specifically before writing. Halt with a clear error on any violation.
Validate an explicit Warp route ID immediately — fail fast, here, before any registry/filesystem work. It must be exactly <TOKEN>/<suffix>: exactly one /; each of the two components matches ^[A-Za-z0-9._-]+$ and is neither . nor .. (a bare ./.. component matches the charset but is still a traversal); no whitespace, no shell metacharacters (; | & $ ` > < ( ) * ? \). The <TOKEN> component must equal the ticket's token symbol (the canonical registry key), not an arbitrary string. Halt with a clear error otherwise — the id later becomes a registry filename and a --warp-route-id argument, so a hostile value is a path-traversal / command-injection vector. When you later build the deploy.yaml path, resolve it non-strictly (realpath -m, which canonicalizes without requiring the file to exist — for a new deploy the deploy.yaml isn't created yet, so a strict realpath would error) and assert it stays under $REGISTRY_PATH/deployments/warp_routes/<TOKEN>/ specifically — a bare deployments/warp_routes/ check doesn't prove it didn't escape the token directory. (Re-validated the same way in Step 7a.)
Yield routes: if the ticket mentions "yield", "ERC4626", "vault", "rebasing", "Aave", or the token is a known yield-bearing token (sUSDS, sDAI, etc.), it is a yield route. There are two subtypes:
| Ticket language | Collateral type | Synthetic type | Behavior |
|---|---|---|---|
| "owner yield" / "non-rebasing" | collateralVault | synthetic | Yield accrues to contract owner; owner calls sweep() to claim |
| "rebasing" / yield distributed to holders | collateralVaultRebase | syntheticRebase | Yield auto-distributes to all bridged token holders via exchange rate updates |
If the ticket says "owner yield", use collateralVault + synthetic. If ambiguous, ask the user.
For collateralVault routes — check if the collateral token already implements ERC4626:
Run this check on the collateral token address from the ticket:
cast call <collateral-token-address> "asset()(address)" --rpc-url <RPC_URL>
-
If
asset()returns a non-zero address → the token IS an ERC4626 vault. Use it directly astokenin the deploy.yaml. No vault deployment needed. -
If
asset()reverts or returns zero → the token is a plain ERC20. Warn the user:⚠️ The collateral token does not implement ERC4626. You must deploy an Aave ERC4626 vault wrapping it first using hyperlane-xyz/Aave-Vault, then replace
<VAULT_ADDRESS>in the deploy.yaml with the deployed vault address. The vault owner controls who cansweep()yield — confirm the yield beneficiary with product before deploying.Real example: WETH/incentiv vault
0xB1ea329f0B79d0b213957569594ca2a9dE637215= "Wrapped Aave Ethereum WETH" (waEthWETH), underlying = WETH0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2Use
<VAULT_ADDRESS>as a placeholder in the deploy.yaml until the vault is deployed.
collateralVaultRebase constraint: ALL destination chains MUST be syntheticRebase — you cannot mix syntheticRebase with synthetic in the same route. Each syntheticRebase chain requires a collateralChainName field pointing to the collateral chain.
Multi-collateral routes: when the ticket lists multiple collateral chains, each gets its own token address. All owner fields use the deployer address — real ICA/multisig addresses are set later in /warp-deploy-update-owners.
Rebalancing: if the ticket includes liquidity weights (e.g. 35% ethereum, 20% arb…), the route uses the rebalancer. Add allowedRebalancers and allowedRebalancingBridges to each collateral chain (not synthetic). Use the hardcoded values in the reference tables below — no need to search the registry. The weights themselves are NOT in the deploy.yaml — they go in typescript/infra/config/environments/mainnet3/balances/desiredRebalancerBalances.json in the monorepo. Flag this to the user as a separate step.
Daily Rate Limit: if the ticket's Daily Rate Limit row is set (e.g. 200,000,000), the route needs a rate-limited hook on the synthetic chain. Add the hook config to that chain's entry in deploy.yaml. The value is the daily rate limit cap in the token's smallest unit (i.e. apply × 10^decimals to the human-readable number from the ticket).
Ownership validation prerequisite: before generating the deploy.yaml in Step 4, the agent invokes /warp-deploy-validate-owners with the same Linear ticket as input. That skill produces a per-chain owner resolution table (ICA / Safe / Squads / EOA-rejected). The deploy.yaml in Step 4 uses the same deployer address for owner fields (real owner transfer happens later in /warp-deploy-update-owners), but the validation pass ensures the eventual owners are valid before any chain is touched. If /warp-deploy-validate-owners reports any ❌ row, abort — don't proceed to deploy against rejected owners.
Logo handling: the Linear ticket's SVG logo row links to a Linear upload URL with ?signature=…&exp=… JWT parameters that expire (typically ~5 minutes). To prevent 401s mid-flow on longer runs, download the logo eagerly right after fetching the ticket in Step 1 and cache it locally to <registry>/deployments/warp_routes/<TOKEN>/logo.<ext>:
# After mcp__plugin_linear_linear__get_issue returns, grab the SVG/PNG row's image URL
curl -sSL -o "$REGISTRY_PATH/deployments/warp_routes/<TOKEN>/logo.<ext>" "<signed-url>"
Use logo.svg if the upload is SVG; logo.png otherwise. The local file is then referenced by logoURI in <chain>-config.yaml later (/warp-deploy-update-owners Step 11b). Eagerly downloading prevents the signed URL from expiring before that later step needs to read it.
Reference: Known Token Addresses and Bridge Contracts
USDC (decimals: 6)
| Chain | Token Address |
|---|---|
| ethereum | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| arbitrum | 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 |
| base | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| optimism | 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85 |
| polygon | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 |
| avalanche | 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E |
| unichain | 0x078D782b760474a361dDA0AF3839290b0EF57AD6 |
| linea | 0x176211869cA2b568f2A7D4EE941E073a821EE1ff |
| worldchain | 0x79A02482A880bCE3F13e09Da970dC34db4CD24d1 |
| hyperevm | 0xb88339CB7199b77E23DB6E890353E22632Ba630f |
| ink | 0x2D270e6886d130D724215A266106e6832161EAEd |
Rebalancer
Single rebalancer address used across all multi-collateral routes:
0xa3948a15e1d0778a7d53268b651B2411AF198FE3
CCTP Bridge Addresses (per source chain)
Used in allowedRebalancingBridges on each collateral chain. The two addresses are the CCTP bridge contracts on that source chain — list them for every destination collateral chain in the route.
| Source Chain | Bridge Address 1 | Bridge Address 2 |
|---|---|---|
| ethereum | 0x8c8D831E1e879604b4B304a2c951B8AEe3aB3a23 | 0x7A576Bb5291567cfDbB4585B1911CF7C9891ea07 |
| arbitrum | 0x4c19c653a8419A475d9B6735511cB81C15b8d9b2 | 0xE086378F7f0afd5C3ff95E10B5e7806a0901b33f |
| base | 0x33e94B6D2ae697c16a750dB7c3d9443622C4405a | 0x31169ee5A8C0D680de74461d7B5394fFc7C3576B |
| optimism | 0x33e94B6D2ae697c16a750dB7c3d9443622C4405a | 0x4eFaacbf0D3d57b401Cb6B559e84b344448b0C30 |
| polygon | 0x33e94B6D2ae697c16a750dB7c3d9443622C4405a | 0x07d89DE0F7E18c9bcAAE81F44aee9CA02EBeE872 |
| avalanche | 0x33e94B6D2ae697c16a750dB7c3d9443622C4405a | 0xCB35d7730843F770625bE36A0E4228c17fDcBC09 |
| unichain | 0x33e94B6D2ae697c16a750dB7c3d9443622C4405a | 0xCB35d7730843F770625bE36A0E4228c17fDcBC09 |
| linea | 0x33e94B6D2ae697c16a750dB7c3d9443622C4405a | 0x89aAa89D36F995b41d929f2D29b8Ee7C9c8e54cA |
| worldchain | 0x33e94B6D2ae697c16a750dB7c3d9443622C4405a | 0x89aAa89D36F995b41d929f2D29b8Ee7C9c8e54cA |
| hyperevm | 0xDdf252a063f8c5C399B9ccDBbaDBA55225F53Da1 | 0xe10b7b030C75C80359841CB0ec892E233F03f145 |
| ink | 0x92dFEB6f7Daa532de0F3c75c2091e1607c6593b7 | 0x70CF23d09784fCA62be304c928BCA9F1801B1F21 |
Source: extracted from
deployments/warp_routes/USDC/eclipsemainnet-deploy.yaml(2025-04-02). If a chain is missing from this table, look it up in that file.
Step 3: Look Up Mailbox Addresses
For each chain (collateral + synthetics), read the mailbox address from the local registry:
REGISTRY_PATH="$(pwd)/../hyperlane-registry"
cat "$REGISTRY_PATH/chains/<chain>/addresses.yaml" | grep "^mailbox:"
If a chain is not found in the registry, warn the user — the chain may not have a Hyperlane deployment yet.
For Sealevel chains (solanamainnet, eclipsemainnet): also look up the IGP address from the monorepo's program-ids.json (NOT from addresses.yaml):
cat "rust/sealevel/environments/mainnet3/<chain>/core/program-ids.json" | python3 -c "import sys,json; print(json.load(sys.stdin)['igp_program_id'])"
Known values (verify against the file before using):
| Chain | igp_program_id |
|---|---|
| solanamainnet | BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv |
| eclipsemainnet | Hs7KVBU67nBnWhDPZkEFwWqrFMUfJbmY2DQ4gmCZfaZp |
Save this address — it is used as the hook field in Step 4.
Step 4: Generate deploy.yaml
Compose the deploy.yaml using the extracted details and mailbox addresses.
Canonical schema files — read these before authoring nested ISM / hook / fee configs. A deploy.yaml that fails Zod validation never reaches on-chain state — warp apply rejects it at parse time, but the resulting error output is voluminous; start from a correct shape:
- Per-chain router config (token type + ISM + hook + fee + proxyAdmin + remoteRouters + destinationGas):
typescript/sdk/src/token/types.ts—HypTokenRouterConfigSchemais the per-chain entry;HypTokenConfigis the token-type discriminated union (collateral, native, synthetic, xerc20, opL1/L2, cctp, everclear, depositAddress, crossCollateral, unknown). - ISMs:
typescript/sdk/src/ism/types.ts—IsmConfigSchemaunion, plus per-type schemas (PausableIsmConfigSchema,RateLimitedIsmConfigSchema,AggregationIsmConfigSchema,RoutingIsmConfigSchema, etc.). Threshold semantics:staticAggregationIsmwiththreshold = modules.lengthis AND across all modules;threshold: 1is OR. - Hooks:
typescript/sdk/src/hook/types.ts—HookConfigSchemaunion. NotedefaultHookis the sentinel that means "use mailbox default";fallbackRoutingHookis the standard pattern for "default hook on most chains, custom hook on a specific chain". - Fees:
typescript/sdk/src/fee/types.ts—TokenFeeConfigSchemadiscriminated union (LinearFee,OffchainQuotedLinearFee,RoutingFee,CrossCollateralRoutingFee, etc.). Thebpsfield onLinearFeeis immutable at the contract level so a bps edit redeploys the contract. - Shared mixins:
typescript/sdk/src/types.ts—OwnableSchema(owner+ optionalownerOverrides) andPausableSchema(Ownable +paused: boolean). Many ISM / hook configs extend these, soowneris required on more types than the schema name alone suggests.
Token-type ⇔ fee-wrapper coupling (mandatory pairing). The outer fee wrapper on a chain's tokenFee block is constrained by the chain's token type:
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-init-route- Source
- github.com/hyperlane-xyz/hyperlane-monorepo