Post-deployment ownership transfer and registry PR for a warp route. Transfers ownership from the temporary deployer address to real owners, adds CoinGecko ID, commits, and opens a registry PR. Run after warp-deploy-init completes deployment.
Post-deployment ownership transfer and registry PR for a warp route. Transfers ownership from the temporary deployer address to real owners, adds CoinGecko ID, commits, and opens a registry PR. Run after warp-deploy-init completes deployment.
Warp Route Deploy — Update Owners & Finalize
You are transferring ownership of a newly deployed warp route and opening the registry PR.
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 machine-row + prose entry shape, and the surface-the-URL-as-proof hard gate. Use warp-deploy-update-owners 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 + warp route ID, (b) every [CONFIRM:] gate — before and after the response, (c) every ICA deployed (chain + resolved address), (d) the warp apply ownership transfer per chain (target owner + tx hash), (e) the comprehensive warp check verdict, (f) the registry PR URL, (g) skill exit (success or bail-out). Log smooth steps too — success data grounds the retrospective as much as failure data.
Input
The user provides (or you have from a prior /warp-deploy-init session):
Linear ticket URL or ID (e.g. https://linear.app/hyperlane/issue/ABC-123)
Registry path (defaults to $(pwd)/../hyperlane-registry)
If any of the above are missing, ask the user before proceeding.
Key Context (Prerequisite)
This skill runs warp apply to transfer ownership and needs a deployer key per protocol to sign the txs. It auto-loads produced by . If the artifact does not exist, invoke first.
~/.hyperlane/key-contexts/<ticket-id>.yaml
/warp-deploy-select-keys
/warp-deploy-select-keys <ticket-id>
For each unique protocol in the route, read keys.<protocol>.name and keys.<protocol>.source from the artifact. Expand <KEY_<PROTOCOL>_VALUE> placeholders in the commands below per the canonical key-value expansion legend in /warp-key-value-expansion. Display the resolved name + derived address from the artifact at every [CONFIRM:] gate so the human can spot a wrong-key foot-gun.
Reading the Linear Ticket
Fetch the ticket per /fetch-linear-ticket, then extract from the returned description:
Warp route ID (e.g. RISE/bsc-ethereum)
Chains involved
Real owner addresses per chain (if explicitly specified in the ticket)
Whether custom warp fee owners are specified (see below)
Determining Chain-Level Owners
For each chain in the route:
If the ticket explicitly specifies an owner address for that chain, use it.
For the ethereum chain, the owner is typically the Abacus Works Ethereum Safe (from the ticket).
Read this file to look up the ICA address for each chain where the tokenFee contracts are deployed. Use these as the fee owners unless the Linear ticket explicitly specifies different fee owner addresses. The ICA map is EVM-centric — for a non-EVM fee chain (e.g. an SVM fee contract), it won't have an entry, so take that chain's fee owner from the ticket (its native owner, e.g. a Squads address) rather than expecting an ICA.
If a chain's entry is commented out in warpFees.ts, the ICA has not been deployed yet — see Step 10a below for how to deploy it.
Step 10: Transfer Ownership to Real Owners
The deployed contracts are currently owned by the temporary deployer address. This step transfers ownership to the real owners from the ticket.
10a: Deploy Missing ICAs (if needed)
When to deploy ICAs: Check the ticket for the Ethereum Safe address used as the governance owner. Two scenarios:
Standard AW safe (e.g. 0x1234... matching an entry in aw.ts): Only deploy ICAs for chains that are commented out in aw.ts/warpFees.ts.
Custom/route-specific safe (a safe not in aw.ts, or the ticket says "ICA on all chains"): Deploy fresh ICAs from this safe for every non-ethereum chain in the route, regardless of what exists in aw.ts. The existing entries in aw.ts are from different safes and do not apply.
For each chain needing an ICA, deploy from typescript/infra:
Pass chains as space-separated values (NOT comma-separated — the script uses [array] type and rejects comma-separated input). You can pass all chains in one command. Each command prints the new ICA address on completion. Collect all new addresses before proceeding.
Signer disclosure — --deploy does NOT use the per-ticket selected key.get-owner-ica.ts --deploy takes no key argument; it calls config.getMultiProvider() and signs the ICA-deployment tx with the environment's shared Role.Deployer, not the protocol key from ~/.hyperlane/key-contexts/<ticket-id>.yaml that the rest of this skill uses. ICAs are permissionless (anyone can deploy one, and the derived address is independent of who deploys), so this is safe — but surface it at the [CONFIRM:] gate: state that the ICA deployment is signed by the shared mainnet3 deployer, not the selected key, and confirm that is acceptable before running --deploy. (Wiring the selected key into get-owner-ica.ts --deploy is tracked as follow-up code work.)
get-owner-ica.ts is EVM/Tron-only — it filters to Ethereum-protocol chains and silently drops Sealevel/Cosmos/Starknet/etc. with no warning row. Only pass EVM (or Tron) chains whose owner is an ICA; a non-EVM chain's owner comes from its native construct (Squads, etc.) per the ticket/artifact, not from this script. If you pass a non-EVM chain and get no address back, that is the silent drop — resolve that chain's owner another way rather than proceeding with a missing owner.
These new ICAs are route-specific and should NOT be added to aw.ts or warpFees.ts — they only belong in the deploy.yaml for this route.
10b: Confirm Real Owners
Show the user the real owner addresses for both chain-level owners and fee owners. Present them clearly per chain, e.g.:
If the ticket specifies custom fee owners, show those instead and label them accordingly.
Ask the user to confirm or provide corrections. End your message with this marker (this MUST be the very last thing in your message):
[CONFIRM: Owner addresses are correct]
Wait for confirmation before proceeding. If the user provides corrections, update your record of owner addresses accordingly.
10c: Update deploy.yaml with Real Owners
Update the deploy.yaml by replacing the deployer address with the correct real owner per chain. Parse the YAML structure and target specific keys only — never a global string replace. Update the chain-level owner, and inside any tokenFee / feeContracts blocks both the ownerand the beneficiary.
The beneficiary defaults to that block's target owner unless the ticket (§4) names a distinct one. Leaving it on the deployer means the fee keeps accruing to the deployer after the handoff — so it must not be silently skipped. Include it in the yaml edit so warp apply reconciles it alongside owner; surface any beneficiary that diverges from the owner for confirmation before applying. Confirm every owner and beneficiary landed in the Step 10e warp check; if apply did not move a beneficiary on a given protocol, flag it as a required manual follow-up rather than reporting the transfer complete.
Write the updated deploy.yaml back to the registry path. Show the user the diff (old → new owners).
10d: Build and Run Warp Apply
First, start the HTTP registry per /start-http-registrywith --writeMode (warp apply persists newly-deployed contract addresses back through the server). Note the port (typically 3333) and the background task ID — needed to stop the server after this step.
Assemble the warp apply command. Use only the HTTP registry — started with --writeMode so it handles both private RPC reads and writes. Expand <KEY_<PROTOCOL>_VALUE> per the artifact's source field (see the canonical key-value expansion legend in /warp-key-value-expansion). Supply one --key.<protocol> for every protocol in the route — don't assume ethereum, and don't drop one: a missing protocol key silently leaves that protocol's ownership transfers unsigned (e.g. omitting --key.tron leaves the Tron router + fee contracts still owned by the deployer while the run reports success).
# repeat --key.<protocol> per protocol in the route: ethereum / sealevel / cosmos / tron / starknet / radix / aleo
pnpm --silent -C typescript/cli hyperlane warp apply \
--registry http://localhost:<port> \
--key.<protocol> <KEY_<PROTOCOL>_VALUE> \
-w <WARP_ROUTE_ID>
Where <WARP_ROUTE_ID> is the stable route ID from init-route Step 7a (e.g. USDS/igra or USDS/ethereum-igra).
Ownership transfer is protocol-agnostic. The deploy.yaml lists the target owner per chain. warp apply emits a transferOwnership (or protocol equivalent) signed by the current owner — which is the deployer key on freshly-deployed routes. It does not matter what TYPE the new owner is (EOA, Safe, ICA, Squads multisig PDA, timelock, Turnkey wallet, Privy wallet, custom multisig, etc.) — the transfer just writes the address into the router's owner slot. No approval flow, no proposal step, no multisig signing is needed at this stage: only the current owner (deployer) signs. Any subsequent operation on the route will require the new owner to sign per whatever their type dictates.
xERC20 routes — router owner only. This transfer moves the router owner. If the route wraps an xERC20 token we also control, the underlying token owner and its ProxyAdmin are a separate contract and do NOT move via warp apply — transfer them with hyperlane xerc20 apply (see /warp-update §1c), setting the token's bridge limits before the ownership handoff. If the token is externally governed (Velo / customer) or non-Ownable, we don't own it — leave it and say so.
BUT — target-owner existence sanity check. Before running warp apply, verify per chain that the target owner address in the deploy.yaml is actually usable on chain. Transferring ownership to a nonexistent contract (typo, wrong chain, un-deployed multisig, wrong ICA derivation) permanently bricks the route. The check is protocol-specific but the pattern is uniform (the EVM/Tron code probe and the Sealevel existence probe are the primitives from /classify-onchain-owner; the Cosmos / other rows extend the same idea):
Protocol
Check
Interpretation
EVM / Tron
cast code <owner> --rpc-url <rpc> (or equivalent via multiProvider)
Null → address has never had state on this chain; non-null → account exists (may be executable / PDA / plain wallet)
Cosmos
Address decodes + bank.balance or auth.account query
Address exists in bank / auth module
Others
Per-protocol equivalent from the altvm SDK's provider
Existence signal
For each target owner in the deploy.yaml:
Run the per-protocol existence probe.
Cross-reference against the artifact context if /warp-deploy-validate-owners produced one — its owner-type classification tells you whether "no code" is expected (EOA) or a problem (missing multisig / ICA).
Decision:
Address has code / account state AND was classified as a contract-type owner (Safe / ICA / Squads / timelock / other multisig) → ✅ proceed
Address has NO code / no account state AND is expected to be an EOA (matches the deployer's own address for a self-owned staging route, or explicitly classified as EOA in validate-owners with allowSelfOwned: true) → ✅ proceed
Address has NO code / no account state AND is expected to be a contract → ❌ HALT. The multisig / ICA doesn't exist on this chain. Do NOT run warp apply.
Address has code but classification is unknown → surface to the user with the on-chain snippet + require an explicit [CONFIRM:] before proceeding
Show the sanity-check result per chain in a table BEFORE the warp apply CONFIRM gate. If any chain is ❌, halt the whole flow.
Show the user the exact command, then end your message with this marker (this MUST be the very last thing in your message):
[CONFIRM: Run warp apply to transfer ownership for <WARP_ROUTE_ID>]
If the user confirms, run it. Show the full output on completion.
On failure: stop the HTTP registry (Step 10e), show the error, and stop. Do not proceed to Step 11. Common issues:
Deployer key no longer has funds on the chain that failed → top up per protocol via /warp-deploy-fund-deployer and retry.
Owner address in the deploy.yaml doesn't exist on chain (typo, wrong chain, un-deployed contract) — should have been caught by the sanity check above; if not, correct the deploy.yaml and retry.
Deploy.yaml drift vs on-chain state → run hyperlane warp read to diff, correct the deploy.yaml, retry.
10e: Verify the Route with Comprehensive warp check
After warp apply completes, verify per the /warp-verify-onchain-config contract. This is Mode A — the deployer signed the transfer, so it's already on-chain — so run the canonical CLI verifier live against the deployed route. This is the gate before downstream steps (monitor deploy, registry PR): if warp check reports violations, the route isn't actually in the target state and the rest of the flow shouldn't proceed.
This is the comprehensive check — compares the on-chain state of every contract in the route against the target deploy.yaml. No violations = the deployment matches the config (ownership, ISM, hook, fee, rate-limit, all of it).
Additionally, IF any chain owner in the route is an ICA (per the resolution from /warp-deploy-validate-owners, or visible in the deploy.yaml owner fields), also run the ICA-aware variant:
--origin is the chain where the controlling Safe / EOA lives (typically ethereum).
--chains is the space-separated list of destination chains whose owners are ICAs derived from that origin.
--originOwner is the controlling Safe / EOA address on --origin — REQUIRED when the origin chain is NOT one of the chains in the route. If you omit it in that case, the CLI errors with Origin chain <name> does not have an owner configured. When the origin chain IS in the route (e.g. ethereum-collateral routes), the CLI infers --originOwner from the route's ethereum-leg owner field and you can omit the flag. Safest to always pass it explicitly.
This verifies each ICA address derives correctly from the configured controlling owner.
The warp check run may also emit transient warnings from public RPCs that the CLI falls back to — most commonly drpc.org returning HTTP 408 Request timeout on the free tier. These are harmless noise from the public free-tier endpoint and don't affect the check result. Ignore unless the run actually fails.
Show the user the full warp check output. If there are violations:
Surface each violation clearly (chain, field, actual vs expected).
Stop. Do not proceed to Step 11 / 12 (the monorepo register-route and monitor deploy that follow). The route isn't in the right state — investigate and re-apply before continuing.
10f: Stop the HTTP Registry
After warp check completes (or on any failure after Step 10d), stop the HTTP registry per /stop-http-registry (using the task ID noted in Step 10d). Always stop it — even on failure.
Step 11: Add CoinGecko ID and Finalize Config
YAML sort-order rules (apply to every edit in this step and any registry YAML edit in this skill): top-level chain entries AND keys within each entry must be in strict alphabetical order, per /registry-yaml-sort-policy — that skill carries the canonical config.yaml token-block key order (addressOrDenom, chainName, coinGeckoId, connections, decimals, logoURI, name, standard, symbol, tokenType). Insert each new field (e.g. coinGeckoId, logoURI) at its alphabetical position.
11a: Look Up CoinGecko ID
Search CoinGecko for the token symbol/name in the registry core-config.yaml.
If found, add coinGeckoId: <api-id> to each non-synthetic token entry in the config.yaml (i.e., collateral and native entries only — NOT synthetic entries). Insert at the alphabetical position in the token block (between chainName and connections). Do NOT append at the top or bottom — that fails the sort check.
If not found on CoinGecko, note this to the user and skip.
11b: Add Logo
Check if /warp-deploy-init-route already cached the logo locally at $REGISTRY_PATH/deployments/warp_routes/<TOKEN>/logo.svg (or logo.png) — that skill downloads the Linear-uploaded logo eagerly after fetching the ticket, so the JWT signed-URL doesn't expire by the time we get here.
If the local file exists (the happy path): proceed directly to step 3.
If the local file is missing (e.g. init-route was skipped or the cached file got purged): re-fetch the issue via mcp__plugin_linear_linear__get_issue to obtain a fresh signed URL, then immediately:
Use logo.svg if the upload is SVG; logo.png otherwise.
Add logoURI: /deployments/warp_routes/<TOKEN>/logo.svg (or /deployments/warp_routes/<TOKEN>/logo.png) to every token entry in config.yaml (all legs — synthetic and native). Insert at its alphabetical position — logoURI lands between decimals and name. The path is always the absolute path from the registry root.
If no logo is attached to the ticket and no local file exists, skip this step.
11b-check: Verify Sort Order Before Step 11c
Before showing the file for review, verify both sort invariants on the final config.yaml per /registry-yaml-sort-policy (visual scan against its canonical config.yaml key order, or pipe through yq). If either fails, fix the file before proceeding to 11c — the registry CI / CodeRabbit will otherwise block the PR.
11c: Show Final Config for Review
Show the user the complete final content of <chains>-config.yaml, then end your message with this marker (this MUST be the very last thing in your message):
[CONFIRM: Proceed with config.yaml as written]
Do not proceed to Step 12 until the user confirms.
Step 12: Commit, Push, and Open Registry PR
12a: Check Registry Git Status
cd$REGISTRY_PATH && git status
Show the user the list of changed/new files. There should be at minimum:
Push the branch and open a PR on GitHub. If git push fails with could not read Username for 'https://github.com', the sandbox has no push credentials — set GH_TOKEN and wire git to it (gh auth setup-git, or a credential helper) before retrying; gh pr create reads GH_TOKEN directly.
cd$REGISTRY_PATH
git push -u origin HEAD
gh pr create \
--base main \
--title "feat: add <WARP_ROUTE_ID> warp route" \
--body "$(cat <<'EOF'
## Summary
Adds the `<WARP_ROUTE_ID>` warp route.
| Field | Value |
| ----- | ----- |
| **Linear** | <linear-issue-url> |
| **Token** | <token-name> (<TOKEN>) |
| **Route type** | <e.g. native → synthetic, collateral → synthetic> |
| **Chains** | <chain1> (<type>), <chain2> (<type>), ... |
| **Decimals** | <decimals> |
| **Warp fee** | <fee in bps, or "none"> |
| **Owners** | <chain>: `<owner-address>`, ... |
### Contracts deployed
| Chain | Contract | Address |
| ----- | -------- | ------- |
| <chain> | <HypNative / HypSynthetic / HypERC20Collateral / ...> | `<address>` |
| ... | ... | ... |
### ICAs deployed
Only include this section if fresh ICAs were deployed during Step 10a. List each chain and address:
| Chain | ICA Address | Owner Safe |
| ----- | ----------- | ---------- |
| <chain> | `<ica-address>` | `<safe-address>` |
| ... | ... | ... |
If no ICAs were deployed (all owners were already known), omit this section entirely.
### Test transfers
Only include this section if warp send tests were run (`/warp-deploy-send-test`, Step 9 of the deploy chain). List each direction tested with its explorer link:
| From | To | Message ID | Status |
| ---- | -- | ---------- | ------ |
| <chain> | <chain> | [`<short-id>`](<explorer-link>) | ✅ |
| ... | ... | ... | ... |
If no test transfers were run, omit this section entirely.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
Fill in the PR body with real values from the deployment:
Contracts: list each deployed contract address from the config.yaml addressOrDenom fields
Owners: list per-chain real owner addresses from the final deploy.yaml
ICAs: list any ICAs deployed in Step 10a (omit section if none)
Test transfers: list message IDs and explorer links from the send test (/warp-deploy-send-test) run log (omit section if none)
Show the user the PR URL when done.
After showing the PR URL, tell the user:
Once this PR is merged, run /warp-deploy-register-route to add the warp route ID to the monorepo and update .registryrc.
Notes
The registry path is $(pwd)/../hyperlane-registry when run from the monorepo root. The hyperlane-registry repo is expected to be cloned at the same level as hyperlane-monorepo.
Always pass --registry http://localhost:<port> to CLI commands — the HTTP registry is started with --writeMode so it handles both private RPC reads and artifact writes.
If the ticket has links to token contracts on block explorers, use those addresses
Chain-level owner is typically an Abacus Works Safe address specified in the ticket
tokenFee block owner defaults to the Hyperlane ICA address from warpFees.ts unless the ticket says otherwise