Foundation skill for warp-route updates. Reads the current on-chain config of a warp route via `hyperlane warp read`, enumerates every artifact (router, ProxyAdmin, ISM, hook, fee contracts) per chain, classifies each artifact's owner (auto-detects Safe / ICA / EOA; asks the user for everything else — Turnkey, Privy, MPC, custom multisig, timelock, etc.), and persists a per-artifact owner table that downstream update skills consume to know which signing path each batch needs. Read-only — no signing, no on-chain mutations.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Foundation skill for warp-route updates. Reads the current on-chain config of a warp route via `hyperlane warp read`, enumerates every artifact (router, ProxyAdmin, ISM, hook, fee contracts) per chain, classifies each artifact's owner (auto-detects Safe / ICA / EOA; asks the user for everything else — Turnkey, Privy, MPC, custom multisig, timelock, etc.), and persists a per-artifact owner table that downstream update skills consume to know which signing path each batch needs. Read-only — no signing, no on-chain mutations.
Warp Route Update — Resolve Artifacts
You are enumerating every artifact contract in an existing warp route and resolving the current on-chain owner of each. Downstream update skills (/warp-update, /warp-update-extend, future /propose-warp-txs-heimdall) consume this resolution to group their tx-proposal batches by signer.
Why this is a separate skill
Different artifacts in the same warp route can be owned by different addresses. The router on arbitrum might be owned by an ICA derived from the AW ethereum Safe, while the warp-fee contracts on the same chain are owned by an ICA from the Foundation's warpFees Safe, while the ProxyAdmin might be owned by something else entirely. An update that changes a fee parameter needs to propose to the warpFees-Safe signers; an update that changes the router's ISM needs to propose to the AW-Safe signers. Without per-artifact owner enumeration, the update flow can't route tx batches correctly.
Input
Linear ticket ID (required, e.g. AW-123) — namespaces the resolved artifact-context file.
Warp route ID (required, e.g. ETH/arbitrum-base) — the route to resolve artifacts for.
If either is missing, ask the user.
Step 1: Start the HTTP Registry
Invoke /start-http-registry (no extra flags — this skill is read-only and doesn't need ). The HTTP registry serves the central chain-metadata config so hits private/pinned RPCs instead of rate-limited public ones. Stop it at the end of the skill per .
warp read is a read-only command and does NOT require a key. The output is a YAML document with the deploy.yaml shape — one block per chain, with on-chain values populated for owner, interchainSecurityModule, hook, proxyAdmin, and any tokenFee.feeContracts block. Capture the full output for the next steps.
Expected non-fatal warnings: warp read emits Failed to get configured rebalancers for token … / Failed to get allowed rebalancer bridges for token … errors on routes that don't have a rebalancer configured (most non-multi-collateral routes). The errors are noisy but non-fatal — the read completes and the YAML is still produced. Ignore them unless the YAML is empty or ❌ Warp route config read successfully: is missing from the output.
If warp read fails on a specific chain (real RPC error, missing chain in registry, contract reverted in a way that aborts the read), surface the per-chain error and continue with the chains that succeeded — partial resolution is more useful than nothing. Mark the failed chain's row as ❌ READ_FAILED in the final table.
Step 3: Extract Artifacts Per Chain
From the warp read output, build a flat list of (chain, artifactType, address, ownerAddress) tuples. The artifact types and where they appear in the YAML:
Top-level artifacts (always present per chain)
Artifact type
Source field in warp read output
Shape
Owner from warp read?
router
top-level chain block (the warp-token contract address is the route entry) + <chain>.owner
flat
✅ (the chain .owner)
proxyAdmin
<chain>.proxyAdmin.{address, owner}
nested
✅
fee:routing
<chain>.tokenFee.{address, owner} (synthetic side only)
nested
✅
fee:linear
each <chain>.tokenFee.feeContracts.<other-chain>.{address, owner, bps}
nested
✅
xERC20 / xERC20-lockbox routes — the underlying token is a SEPARATE artifact
When a chain's token standard is xERC20 or xERC20-lockbox (router types like HypVSXERC20, EvmHypXERC20Lockbox, …), the underlying xERC20 token is a distinct contract from the warp router, with its OWN owner and its OWN ProxyAdmin. warp read reports the router's owner, not the token's — enumerate two extra artifacts:
Artifact type
How to resolve the address
Owner controls
xerc20:token
lockbox route → lockbox.XERC20(); otherwise the router's wrapped/collateral-token field
setBufferCap / setRateLimitPerSecond / addBridge / removeBridge — NOT the router
xerc20:proxyAdmin
the token's own ProxyAdmin (distinct from the router's proxyAdmin)
token-implementation upgrades
Owner-classification caveats for these two (they are NOT the router owner):
The token owner is frequently externally governed (Velo / customer / third party) and may be entirely out of our control — classify it, but flag when we can't operate it.
The token may be non-Ownable (EIP-7281 doesn't mandate Ownable — it can be AccessControl or custom, with no owner()). If owner() reverts, record owner: null, type: non-ownable, and ASK THE USER — do not throw.
ISM and hook artifacts (flat-or-nested — walk the tree)
interchainSecurityModule and hook are NOT always flat addresses. They can be either:
A flat string — typically 0x0000000000000000000000000000000000000000 ("use mailbox default", skip as artifact) or a non-zero address for an existing custom contract whose internals aren't surfaced by warp read.
A nested object — recent routes use complex ISM trees (e.g. staticAggregationIsm of rateLimitedIsm + pausableIsm + defaultFallbackRoutingIsm) or hook trees (e.g. amountRoutingHook). Each sub-component is its own deployed contract with its own address, type, and (sometimes) owner.
Algorithm for each ISM / hook field:
If typeof field === 'string':
0x0…0 (zero address) → uses mailbox default. Skip — do NOT add a row.
Non-zero address → custom contract. Record as ONE artifact row with type: <unknown-or-from-context>, address: <field-value>. Resolve owner via cast call <addr> "owner()(address)". If the contract doesn't implement owner(), mark owner: null + type: immutable.
If field is an object:
Walk the tree depth-first. At each node that has an owner field, record it as a separate artifact (with address, type, owner, and any type-specific fields like maxCapacity / paused). Aggregation containers themselves (e.g. staticAggregationIsm) typically have NO owner — they're constructor-immutable; do not record them as actionable artifacts, but DO record each of their constituent sub-modules.
Containers to descend into:
modules: [...] — array of sub-modules (aggregation ISMs)
any other field whose value is itself an object with type set
At each leaf node with an owner, record a row like { chain, type: <node.type>, address: <node.address>, owner: <node.owner>, details: { ...type-specific fields... } }.
That single interchainSecurityModule field produces THREE artifact rows in the resolution table — one per leaf sub-ISM with an owner.
cast call fallback for any missing owner field
If a nested-shape artifact's owner is unexpectedly missing (rare; might happen if the warp read schema changes or for non-standard contracts), fall back to (EVM/Tron only):
Always prefer the warp read value when present — it's the canonical source. On a non-EVM artifact with a missing owner, read it via that protocol's native query or ask the user — cast won't work.
Step 4: Classify Each Owner
Owner classification is open-set — the auto-detection rules only cover a few shapes; for everything else the user supplies the type. Do NOT assume contract-not-Safe is necessarily an ICA. Real-world owners include Hyperlane ICAs, Gnosis Safes, Squads multisigs, Turnkey wallets, Privy embedded wallets, other MPC wallet contracts, custom non-Gnosis multisigs, timelocks, and one-off bespoke owners.
For each unique owner address, run the following sequence.
4.0: Probe Shortcut — Match Against Known Hyperlane Governance Maps
Most Hyperlane-owned warp routes have owners that already live in the canonical governance config in typescript/infra/config/environments/mainnet3/governance/. The Safe / ICA / timelock / ProxyAdmin maps there are the source of truth for those addresses across all chains, organized by governance group (AW / Foundation warpFees / Regular / Irregular / oUSDT). Use them as a probe shortcut — a direct match means you can skip the on-chain cast and ICA-derive calls in 4a–4c and resolve the type from the map directly.
Read the lookup helpers in typescript/infra/config/environments/mainnet3/governance/utils.ts (getGovernanceSafes, getGovernanceIcas, getSafesByGovernanceForChain, getGovernanceTimelocks, getWarpFeeOwner) and the per-group address maps in safe/, ica/, timelock/, proxy-admin/. For each owner address being classified:
Match in safe/<group>.ts[chain] → type: Safe. Skip 4b's VERSION() probe.
Match in ica/<group>.ts[chain] → type: ICA, origin: ethereum, controllingOwner: <safe-from-the-same-group-on-ethereum>. Skip 4c's ICA derivation — the map already encodes the deterministic result. Still run 4c.1 to classify the controller (it'll usually match safe/<group>.ts[ethereum] and short-circuit too).
Match in timelock/<group>.ts[chain] → type: Timelock, and record the underlying owner from the same group's Safe map as the resolution target.
Match in proxy-admin/<group>.ts[chain] → informational annotation that the artifact's ProxyAdmin is the canonical one; the ProxyAdmin's own owner is classified separately.
If no map matches, fall through to 4a–4d for on-chain detection. The governance maps are a cache of the most common Hyperlane-controlled cases, not exhaustive — customer routes, ad-hoc owners, third-party-controlled addresses, and any non-Hyperlane warp route won't be there. Don't assume a non-match means "wrong" — it just means the address isn't in this monorepo's governance config.
You MAY add a free-form governance label (e.g. details: "matched awSafes[arbitrum]") to the artifact's owner record when a map matches, to make the resolution table more readable. The schema does NOT require a structured governanceType field — many warp routes aren't governance-controlled at all.
4.1 Route by owner protocol (before the EVM probes)
Steps 4a–4c use EVM tooling (cast code, Safe VERSION(), and the read-only get-owner-ica.ts derive) and apply only to EVM owners (and Tron, which is EVM-address-compatible). Determine the owner chain's protocol first:
EVM / Tron → run 4a–4d.
SVM (Solana base58) → skip to 4e.
Cosmos (bech32) / Starknet (felt) / Radix / Aleo → do NOT run the EVM cast / VERSION() probes against a non-hex address (they error or mislead); skip straight to 4d and ask the user to classify, recording the owner's protocol. Native auto-classification for these is deferred.
The governance-map shortcut (4.0) is protocol-agnostic — a direct address match resolves the type for any protocol and skips the probes entirely.
4a. Is the owner an EOA?
Run the EVM code probe from /classify-onchain-owner against the owner. If it classifies as a plain EOA or an EIP-7702-delegated EOA: EOAs are red flags as long-term production owners — surface as ❌ EOA and stop the skill with a clear error. Do not persist the artifact context with an EOA owner; the route is in a corrupt state that needs human investigation first. Any other bytecode → contract; proceed to 4b.
4b. Auto-detect: Gnosis Safe
Run the Safe probe (VERSION()) from /classify-onchain-owner against the owner. A version string (e.g. "1.3.0") → classify as type: Safe, record the version; done. Reverts or returns empty → not a Safe; proceed to 4c.
4c. Auto-detect: Hyperlane ICA (only if ethereum is in the route)
If the route has an ethereum leg, attempt one ICA derivation using the ethereum leg's owner as the candidate controlling owner:
This is read-only — get-owner-ica.ts without --deploy only calls InterchainAccount.getAccount (a view) and prints the deterministic ICA address derived from (ethereum, ethereum-leg-owner, ICA router on <this-chain>, ISM); it sends no tx. Do NOT use hyperlane ica deploy here — that CLI command has no read-only mode and would deploy an ICA (spend gas, create a contract) when probing a non-ICA owner whose derived account is absent (see /classify-onchain-owner).
If the derived address matches the on-chain owner: classify as type: ICA, origin: ethereum, controllingOwner: <ethereum-leg-owner>. Then proceed to 4c.1 to classify the controller itself.
If the derived address does not match OR ethereum isn't in the route: fall through to 4d. The owner could still be an ICA controlled from a different chain, OR something else entirely.
4c.1. Classify the controller (always — when the owner is an ICA)
Recording controllingOwner: <address> is not enough. Downstream /warp-update Step 4a needs to know what kind of account the controller is — Safe, EOA, another ICA, Squads, MPC wallet, etc. — to pick the right internalSubmitter for the interchainAccount strategy.
First, check the governance maps again (from 4.0) against the controller address on its origin chain — most controllers in Hyperlane-owned routes are Safes that already live in safe/<type>.ts. A direct match yields controllerType: Safe + controllerGovernanceType: <type> without any on-chain probe.
If no governance map matches, apply the code + Safe probes from /classify-onchain-owner against the controllingOwner on its origin chain. Auto-detectable outcomes:
cast code returned bytecode AND VERSION() returned a Gnosis Safe version string → controllerType: Safe. This is the most common case for production warp routes (AW Safe / Foundation Safe / customer Safes control most ICAs).
cast code returned 0x → controllerType: EOA. Common for test routes, contributor EOAs, or one-off setups.
If the controller is a contract but neither a Safe nor anything else the skill can determine on its own, halt and ASK THE USER inline — do not record Unknown and defer to downstream. Open-set classification belongs here, in the foundation skill, not buried in a downstream propose failure that's hard to recover from. The controller could be another ICA (recursive — re-apply 4c against it), a Squads multisig (on SVM origins), a Turnkey wallet, a Privy embedded wallet, an MPC wallet, a custom multisig, a timelock, or something bespoke. Don't guess.
Halt message shape:
"Controller of ICA <owner-address> on <this-chain> is <controllingOwner> on <origin-chain> — contract, not a Gnosis Safe. I can't classify it autonomously. What is it? Options: another ICA (supply its origin chain + controllingOwner), Squads multisig PDA, Turnkey wallet, Privy embedded wallet, MPC wallet (other), custom multisig, timelock (supply its underlying owner), other — supply a short description. The classification gates downstream submitter choice; I need an answer before persisting."
[CONFIRM: Classify controller <controllingOwner> as <user-supplied-type>]
If the user says it's another ICA, recursively apply 4c against the new controller (using the user-supplied origin + controllingOwner). Recursion stops at an EOA, a Safe, or a non-ICA classification.
Print the resolved controller classification on every ICA artifact — even (especially) when it's the common controller=Safe case. Making the type explicit is what prevents the unconscious "ICA controllers are always Safes" prior from re-emerging in the other direction either:
"arbitrum router owner 0x645F…d875 resolves to ICA(ethereum, controllingOwner=0x3965…C5b6); controller is Safe. Downstream interchainAccount strategy uses internalSubmitter: gnosisSafeTxBuilder targeting the controlling Safe on ethereum."
"base proxyAdmin owner 0xC92c…651c resolves to ICA(ethereum, controllingOwner=0x3f13…0913); controller is EOA. Downstream interchainAccount strategy must use internalSubmitter: jsonRpc or file — NOT gnosisSafeTxBuilder."
If the same controllingOwner is referenced by multiple ICAs in this route, classify it once and reuse; still print the controller line for every artifact that resolves through it.
4d. Ask the user — open-set classification
When auto-detection didn't match a Safe or a route-ethereum-derived ICA, ask the user to classify the owner. Surface the context and let them pick — don't pre-pick. End your message with one [CONFIRM:] per unclassified owner:
Owner address: <owner-address> on <chain>
Status: contract (has code), not a Gnosis Safe, not derivable as ICA from this route's ethereum leg.
What type of owner is this? Possible types:
- ICA (Hyperlane Interchain Account) controlled from a different origin chain — also supply origin chain + controlling owner address on origin
- Squads (Solana multisig PDA) — supply the multisig PDA's role if multi-vault
- Turnkey wallet — supply the Turnkey org / wallet ID for reference
- Privy embedded wallet — supply the Privy project ID / wallet reference for reference
- MPC wallet (other) — supply the provider name + any wallet ID
- Custom multisig (not Gnosis) — supply the multisig type if known
- Timelock — supply the underlying owner (where the queued ops resolve to)
- Other — supply a short description
- Unknown — cannot identify; downstream proposals touching artifacts owned by this address will halt
[CONFIRM: Classify <owner-address> as <user-supplied-type>]
Note:[CONFIRM: ...] is a Haggis-specific harness primitive — Haggis renders it as an inline approve/reject button. In other Claude Code contexts it is just text.
If the user supplies type: ICA with a different origin, verify the derivation by re-running the read-only get-owner-ica.ts derive (no --deploy) with the user-supplied --ownerChain + --owner. If the derived address still doesn't match, the user-supplied origin is wrong — surface the mismatch and re-prompt.
For all other types (Turnkey / Privy / MPC / custom-multisig / timelock / other), the skill records the classification without further verification. The signing-path mapping is downstream's responsibility (most non-Safe / non-ICA / non-Squads types map to file submitter + manual hand-off; downstream skills decide).
4e. SVM owners
For SVM owners (Solana base58 pubkeys), do NOT default to type: Squads. Reading the account (getAccountInfo) only proves it exists — it does not distinguish a Squads multisig PDA from a plain wallet or another program-owned account, and stamping Squads on assumption would route proposals to the wrong signer. On-chain Squads detection is Phase-2 wiring, so until then leave the type unclassified (notes: SVM owner classification not yet automated — Squads vs wallet vs program-owned undetermined) and require the user to confirm the actual type via 4d before any downstream routing. If unconfirmed, treat it as a manual hand-off (never auto-route to the Squads signer).
Step 5: Drift Detection
Compare the on-chain owner returned by warp read against the owner field in the registry's deploy.yaml for the same artifact:
ticket:<ticket-id>warpRouteId:<WARP_ROUTE_ID>resolvedAt:'<ISO-8601 timestamp>'artifacts:-chain:arbitrumtype:routeraddress:'0xA2cB89330E057a2cF76C6945aEAD22631c4df061'owner:address:'0x645FE06507C8a188494d3E755B248a8dbF3bd875'type:ICA# Safe | ICA | Squads | Turnkey | Privy | MPC | CustomMultisig | Timelock | Other | Unknownorigin:ethereum# for ICA onlycontrollingOwner:'0x3965AC3D295641E452E0ea896a086A9cD7C6C5b6'# for ICA only — the controller on the origin chain (e.g. AW Safe on ethereum)controllerType:Safe# for ICA only — Safe | EOA | ICA | Squads | Turnkey | Privy | MPC | CustomMultisig | Timelock | Other. Gates downstream `interchainAccount.internalSubmitter` choice.details:null# free-form notes (e.g. Turnkey org ID, governance label if matched against a known map, etc.)drift:null# or { expected: '0x...', actual: '0x...', severity: 'warning' }-chain:arbitrumtype:fee:routingaddress:'0x...'owner:address:'0xICAfromFoundationSafe...'type:ICAorigin:ethereumcontrollingOwner:'0x8Ff4c563f26db00e65bD93d9f662A51c304C09b0'# Foundation warpFees Safedrift:null-chain:basetype:proxyAdminaddress:'0x...'owner:address:'0xTurnkeyOwner...'type:Turnkeydetails:'Turnkey org abc-123, wallet xyz-456'# user-supplied at 4ddrift:nullgroupedByOwner:-owner:'0x645FE06507C8a188494d3E755B248a8dbF3bd875'type:ICAorigin:ethereumcontrollingOwner:'0x3965AC3D295641E452E0ea896a086A9cD7C6C5b6'controllerType:Safeartifacts:- { chain:arbitrum, type:router }
- { chain:arbitrum, type:proxyAdmin }
-owner:'0xTurnkeyOwner...'type:Turnkeydetails:'Turnkey org abc-123, wallet xyz-456'artifacts:- { chain:base, type:proxyAdmin }
drift:-chain:baseartifact:routerexpected:'0x645FE06507C8a188494d3E755B248a8dbF3bd875'actual:'0x999...'severity:warning
The groupedByOwner block is the most useful downstream shape — it's what propose skills consume to construct one tx batch per signing path.
Step 8: Stop the HTTP Registry
Stop the background task started in Step 1 per /stop-http-registry.
Step 9: Hand Off to Downstream
Tell the user:
Artifact context resolved and saved to ~/.hyperlane/update-context/<ticket-id>.yaml. Downstream warp-update skills (/warp-update, /warp-update-extend, /propose-warp-txs-heimdall) consume this file to know which owners need to sign for the diff being applied. Run those next; they auto-load this artifact.
For owners with types that don't yet have a native submitter path (Turnkey / Privy / MPC / custom multisig / timelock / Other / Unknown), downstream propose flows will fall back to the file submitter and emit TX JSON for manual hand-off to the owner's signing tooling.
Notes
Read-only skill. Nothing on chain is mutated. Step 4c derives the ICA address with get-owner-ica.ts (no --deploy), which sends no tx. It must NOT use hyperlane ica deploy — that command deploys-if-absent and would mutate chain state when probing a non-ICA owner.
Open-set classification. Contract-but-not-Safe is NOT assumed to be an ICA. Auto-detection covers the cheap cases (EOA reject, Safe via VERSION(), ICA via route-ethereum derivation). Everything else the user classifies. Owner types intentionally include non-multisig wallet shapes (Turnkey, Privy, MPC) because real customer routes use them.
Drift is a warning, not a halt. This skill records drift but doesn't decide what to do about it; downstream update skills surface drift to the human in their CONFIRM gates and let the human decide whether to proceed.
EOA owner halts the skill. An EOA on the production owner slot is a corruption signal and needs human investigation before any update runs.
SVM owners are never auto-classified as Squads. Account existence (getAccountInfo) cannot distinguish a Squads PDA from a wallet or another program-owned account, so SVM owners are left unclassified and the user confirms the actual type at 4d (else manual hand-off) — see Step 4e. On-chain Squads detection is Phase-2 wiring.
Idempotent. Re-running for the same ticket-id overwrites the artifact context. Useful when route state changed between runs (e.g. ownership transferred mid-flow).