Skip to content

fix: moonpay bsc owner#8902

Open
troykessler wants to merge 3 commits into
mainfrom
fix/moonpay-bsc-owner
Open

fix: moonpay bsc owner#8902
troykessler wants to merge 3 commits into
mainfrom
fix/moonpay-bsc-owner

Conversation

@troykessler

@troykessler troykessler commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Description

Migrates ownership of the moonpay BSC USDC and USDT warp routes from the old BSC Safe (0x7bB2AD...) to the new ICA (awIcas.bsc = 0x269Af9...).

Three fixes were needed to make warp apply work correctly for this migration:

  1. HyperlaneIsmFactory: Added isInitialized guard before calling initialize() on FALLBACK_ROUTING ISMs (both standard and ZkSync paths). On retry, the contract was already initialized, causing a revert.

  2. EvmIsmModule: Added tryUpdateContainerIsm to update AGGREGATION and AMOUNT_ROUTING ISM sub-modules in-place when only owners change, avoiding a full redeploy of the ISM tree.

  3. multisend: Replaced static SAFE_NONCE_OVERRIDES map with automatic getNextNonce() lookup via the Safe API, which accounts for pending transactions and avoids nonce conflicts.

Drive-by changes

  • aw.ts: Uncommented/added bsc ICA entry.
  • getUSDCCitreaMoonpayWarpConfig.ts: BSC owner updated from awSafes.bscawIcas.bsc.
  • getUSDTCitreaMoonpayWarpConfig.ts: BSC owner updated from awSafes.bscawIcas.bsc.

Backward compatibility

Yes

Testing

Manual — ran warp apply for USDC/moonpay and USDT/moonpay on BSC end-to-end.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like an extraneous optimisation

Would prefer to focus the PRS on bsc changes and move sdk changes to a separate or for reviewing independently

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Activates the BSC entry in awIcas and switches the USDC Citrea/Moonpay warp config BSC owner from awSafes.bsc to awIcas.bsc. Replaces static SAFE_NONCE_OVERRIDES with dynamic runtime nonce fetching. In the SDK, adds in-place sub-module updating for AGGREGATION and AMOUNT_ROUTING container ISMs, and guards routing ISM initialization to prevent re-initialization on partial deployments.

Changes

Infra: BSC owner config and Safe nonce

Layer / File(s) Summary
BSC ICA activation and warp config owner
typescript/infra/config/environments/mainnet3/governance/ica/aw.ts, typescript/infra/config/environments/mainnet3/warp/configGetters/getUSDCCitreaMoonpayWarpConfig.ts
Uncomments and activates the bsc key in awIcas, then updates ownersByChain.bsc in the USDC Citrea/Moonpay warp config getter from awSafes.bsc to awIcas.bsc.
Dynamic Safe nonce fetching
typescript/infra/src/govern/multisend.ts
Removes the SAFE_NONCE_OVERRIDES static map and adds getNextNonce() which calls safeService with retries; both proposeIndividualTransactions and proposeMultiSendTransaction now derive their nonce at runtime from this helper.

SDK: ISM in-place updates and init guard

Layer / File(s) Summary
EvmIsmModule in-place container ISM sub-module updates
typescript/sdk/src/ism/EvmIsmModule.ts
Adds factory/utility/config imports, inserts an early in-place update path in update() for AGGREGATION and AMOUNT_ROUTING types, and implements tryUpdateContainerIsm which validates thresholds, reads on-chain sub-module addresses, recursively updates each sub-module, and returns transactions only when resulting addresses all match originals — falling back to full redeployment otherwise.
HyperlaneIsmFactory idempotent routing ISM init
typescript/sdk/src/ism/HyperlaneIsmFactory.ts
Imports isInitialized and guards the initialize(...) transaction in both the fallback routing ISM and ZkSync domain routing ISM deployment paths, skipping re-initialization when the contract at the deployed address is already initialized.

Possibly related PRs

  • hyperlane-xyz/hyperlane-monorepo#8722: Introduces getUSDCCitreaMoonpayWarpConfig.ts and the Citrea/Moonpay warp route getter that this PR directly modifies to switch bsc owner from awSafes.bsc to awIcas.bsc.
  • hyperlane-xyz/hyperlane-monorepo#8801: Also modifies awIcas in typescript/infra/config/environments/mainnet3/governance/ica/aw.ts, adding a nesa chain entry to the same ChainMap that this PR activates bsc in.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly captures the main change: fixing the BSC owner configuration for the MoonPay warp route, which aligns with the primary objective described in the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description comprehensively covers all required template sections with clear explanations of changes, fixes, and testing approach.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
.changeset/hot-bears-enjoy.md (1)

6-6: ⚡ Quick win

Consider expanding the changeset description for clarity.

The changeset is in past tense as required, but it's quite vague given the scope of these changes. Folk reading the changelog won't get much sense of what actually changed without context. Since this touches both infra (BSC owner config, dynamic nonce fetching) and SDK (ISM updates, routing ISM initialization), a bit more specificity would help.

For example, something like: "Fixed BSC owner configuration in Moonpay warp setup, replaced static Safe nonce overrides with dynamic fetching, and improved ISM in-place update handling with idempotent routing ISM initialization."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/hot-bears-enjoy.md at line 6, The changeset description at line 6
of the hot-bears-enjoy.md file is too vague and doesn't clearly communicate what
changed. Replace the current description "Fixed bsc owner and owner updates in
sdk" with a more detailed version that specifically mentions the three main
changes: the BSC owner configuration fix in Moonpay warp setup, the replacement
of static Safe nonce overrides with dynamic fetching, and the improved ISM
in-place update handling with idempotent routing ISM initialization. This will
help readers understand the scope and nature of the changes when they review the
changelog.

Source: Coding guidelines

typescript/infra/src/govern/multisend.ts (1)

103-108: 💤 Low value

Consider adding explicit radix to parseInt.

Now, I'm not one to get all worked up about the little things—got better things to do in me swamp—but using parseInt(nextNonce, 10) makes the intent crystal clear for anyone wandering through later. Works fine as-is, just a wee bit tidier with the radix spelled out.

 private async getNextNonce(): Promise<number> {
   const nextNonce = await retrySafeApi(() =>
     this.safeService.getNextNonce(this.safeAddress),
   );
-  return parseInt(nextNonce);
+  return parseInt(nextNonce, 10);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@typescript/infra/src/govern/multisend.ts` around lines 103 - 108, In the
getNextNonce method, add an explicit radix parameter to the parseInt call.
Change the parseInt invocation to include 10 as the second argument to
explicitly specify decimal parsing, making the intent clear that the value is
being parsed as a base-10 number rather than relying on implicit radix
detection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@typescript/sdk/src/ism/EvmIsmModule.ts`:
- Around line 564-570: The sorting logic in sortedTargetModules uses unsafe `as
any` casts to access the type property. Since IsmConfig is either a string
address or an object with a type field, extract the type directly from the
object after confirming it is not null without using the `as any` cast—simply
access the type property as (a as {type: string}).type or create a small helper
function that safely extracts the type from either a string or an object form of
IsmConfig.

---

Nitpick comments:
In @.changeset/hot-bears-enjoy.md:
- Line 6: The changeset description at line 6 of the hot-bears-enjoy.md file is
too vague and doesn't clearly communicate what changed. Replace the current
description "Fixed bsc owner and owner updates in sdk" with a more detailed
version that specifically mentions the three main changes: the BSC owner
configuration fix in Moonpay warp setup, the replacement of static Safe nonce
overrides with dynamic fetching, and the improved ISM in-place update handling
with idempotent routing ISM initialization. This will help readers understand
the scope and nature of the changes when they review the changelog.

In `@typescript/infra/src/govern/multisend.ts`:
- Around line 103-108: In the getNextNonce method, add an explicit radix
parameter to the parseInt call. Change the parseInt invocation to include 10 as
the second argument to explicitly specify decimal parsing, making the intent
clear that the value is being parsed as a base-10 number rather than relying on
implicit radix detection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 74591b40-05ef-494c-b040-db6dc1c1f7c1

📥 Commits

Reviewing files that changed from the base of the PR and between 5a40181 and 4b13285.

📒 Files selected for processing (6)
  • .changeset/hot-bears-enjoy.md
  • typescript/infra/config/environments/mainnet3/governance/ica/aw.ts
  • typescript/infra/config/environments/mainnet3/warp/configGetters/getUSDCCitreaMoonpayWarpConfig.ts
  • typescript/infra/src/govern/multisend.ts
  • typescript/sdk/src/ism/EvmIsmModule.ts
  • typescript/sdk/src/ism/HyperlaneIsmFactory.ts

Comment on lines +564 to +570
const sortedTargetModules = [...targetAgg.modules].sort((a, b) => {
const aType =
typeof a === 'object' && a !== null ? (a as any).type : String(a);
const bType =
typeof b === 'object' && b !== null ? (b as any).type : String(b);
return aType < bType ? -1 : aType > bType ? 1 : 0;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid as any cast when extracting type from IsmConfig.

This wee bit here does the job but tramples on the guidelines, aye? The type can be pulled out properly without conjuring up any. IsmConfig is either a string address or an object with a type field—no need to go off into the swamp with unsafe casts.

🧅 Proposed fix to extract type safely
       const sortedTargetModules = [...targetAgg.modules].sort((a, b) => {
-        const aType =
-          typeof a === 'object' && a !== null ? (a as any).type : String(a);
-        const bType =
-          typeof b === 'object' && b !== null ? (b as any).type : String(b);
+        const aType = typeof a === 'object' && a !== null ? a.type : String(a);
+        const bType = typeof b === 'object' && b !== null ? b.type : String(b);
         return aType < bType ? -1 : aType > bType ? 1 : 0;
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@typescript/sdk/src/ism/EvmIsmModule.ts` around lines 564 - 570, The sorting
logic in sortedTargetModules uses unsafe `as any` casts to access the type
property. Since IsmConfig is either a string address or an object with a type
field, extract the type directly from the object after confirming it is not null
without using the `as any` cast—simply access the type property as (a as {type:
string}).type or create a small helper function that safely extracts the type
from either a string or an object form of IsmConfig.

Source: Coding guidelines

@paulbalaji paulbalaji left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BSC/moonpay config fix looks clean and is ready to go: awIcas.bsc restored, USDC + USDT Citrea/Moonpay owners switched to it consistently, and the removed SAFE_NONCE_OVERRIDES map was empty on main so nothing active is lost. The dynamic getNextNonce() matches the Safe Kit's prior default behavior.

My main ask is to pull the tryUpdateContainerIsm change out of this PR so the config fix isn't held up. This is an area I've looked at before, and in-place container ISM updates are meaningfully more involved than the implementation here. The duplicate same-type pairing bug below is the clearest example, and the spread of follow-on issues/fixes surfaced in the stacked refactor (#8905) reinforces that it deserves its own PR with proper tests (duplicate same-type mutable children, preserved-address preflight) rather than riding along with a config change.

The isInitialized guard is worth keeping in this PR. It's low-risk and useful. Only the comment's rationale is a bit off (see inline). The nonce change is also fine to keep here.

Suggested split:

  • Keep here: BSC config, isInitialized guard, dynamic nonce.
  • Move to #8905 (or its own PR): tryUpdateContainerIsm + the update() early in-place path.

Non-blocking nits inline. Not requesting changes.

Comment on lines +561 to +574
onChainTyped.sort((a, b) =>
a.type < b.type ? -1 : a.type > b.type ? 1 : 0,
);
const sortedTargetModules = [...targetAgg.modules].sort((a, b) => {
const aType =
typeof a === 'object' && a !== null ? (a as any).type : String(a);
const bType =
typeof b === 'object' && b !== null ? (b as any).type : String(b);
return aType < bType ? -1 : aType > bType ? 1 : 0;
});
subModules = onChainTyped.map(({ addr }, i) => ({
address: addr,
targetConfig: sortedTargetModules[i],
}));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pairing-by-sorted-type is unsafe when sub-modules share a type. On-chain modules are sorted by derived .type and target modules by config .type, then zipped by index. If two children have the same IsmType (e.g. two MERKLE_ROOT_MULTISIG, or two routing modules), the comparator returns 0 and ordering is ambiguous, so an on-chain address can bind to the wrong target config. The eqAddress guard at line 611 does not catch this: a mutable child intentionally keeps its address after an in-place update, so a wrong-child mutation passes the check and silently misconfigures the aggregation.

Not triggered by the Citrea/Moonpay routes (their aggregations are [AMOUNT_ROUTING, FALLBACK_ROUTING], distinct types), but it's a latent landmine for any future aggregation with repeated types. Minimum fix: bail to redeploy (return null) when duplicate types are present. Better handled in the dedicated PR (#8905) with tests. Also (a as any).type / String(a) here is dead + a cast violation: at this point target modules are already fully derived objects with .type, and normalizeConfig already sorts modules by .type, so only the on-chain side needs sorting.

overrides,
),
);
// Guard against re-initialization when a previous run deployed this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth keeping the guard, but the rationale is slightly misleading. The fallback routing ISM is deployed via a fresh handleDeploy (new CREATE address each run), so on a re-run after a mid-deploy crash this lands on a brand-new uninitialized address and the guard never fires. So it doesn't actually make the fallback path idempotent across runs (the ZkSync path with a deterministic address is where it can hit). Suggest softening the comment to reflect that it's a defensive double-init guard rather than crash-recovery. The detection itself is correct (reads slot 0, OZ 4.9.6 _initialized == 1 or 255).

const nextNonce = await retrySafeApi(() =>
this.safeService.getNextNonce(this.safeAddress),
);
return parseInt(nextNonce);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: add explicit radix. safeService.getNextNonce returns a Promise<string>, so parseInt is the right call, but parseInt(nextNonce, 10) matches the convention already used in safe.ts.

"@hyperlane-xyz/sdk": patch
---

Fixed bsc owner and owner updates in sdk

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: a bit vague. Once the SDK change is split out, this becomes infra-only and could read closer to: "Migrated Moonpay BSC USDC/USDT warp route owners to the BSC ICA and replaced the static Safe nonce override map with dynamic next-nonce fetching."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

2 participants