Skip to content

🔨 contracts: support hyperlane on redeployer#860

Open
itofarina wants to merge 15 commits into
mainfrom
hyperlane
Open

🔨 contracts: support hyperlane on redeployer#860
itofarina wants to merge 15 commits into
mainfrom
hyperlane

Conversation

@itofarina

@itofarina itofarina commented Mar 3, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Added deployable, upgradeable EXA/HypEXA router tooling with remote enrollment, timelock-backed bridge-role proposals, and pausable modules plus rotation/refresh helpers.
  • Tests
    • Introduced fork-based multi-chain HypEXA integration coverage (round-trips, authorization, timelock scheduling, pausing/pauser rotation).
    • Updated gas snapshot baselines and adjusted one deploy-related assertion.
  • Chores
    • Expanded deployment account mappings, updated remappings/development dependencies, and refined build settings.
  • Style/Docs
    • Updated spellings and added a changelog fragment.

Open with Devin

@changeset-bot

changeset-bot Bot commented Mar 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0a4c9d8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Mar 3, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds Hyperlane/mailbox infrastructure (remappings, deploy accounts), updates dev dependencies and pnpm config, creates HypEXA script contract for router deployment/governance with aggregation hook/ISM composition and timelock-based BRIDGE_ROLE scheduling, updates Redeployer to deploy EXA implementation and run two-phase initialization (initialize + initialize2) with ProxyAdmin transfer, adds comprehensive forked multi-chain HypEXA tests covering round-trips, authorization, pausing, and rotation flows, refreshes gas snapshots, adjusts test expectations, and adds spellcheck words.

Changes

HypEXA and Redeployer integration

Layer / File(s) Summary
Chain/account and build wiring
contracts/deploy.json, contracts/package.json, contracts/remappings.txt, package.json, cspell.json
Adds deploy account mappings for exactly, pauser, and Hyperlane factories (mailbox, aggregation hook/ISM) across chains 10/137/8453. Pins @exactly/protocol to commit 5833408 and adds @hyperlane-xyz/core@^11.0.0. Adds Solidity remappings for both dependencies. Updates pnpm to use onlyBuiltDependencies for @exactly/protocol. Extends cspell words with hyperlane, interchain, XERC.
HypEXA script contract actions
contracts/script/HypEXA.s.sol
New HypEXA BaseScript with deployRouter (CREATE3/proxy-backed HypXERC20 with aggregation hook/ISM, remote enrollment, ownership transfer), proposeBridgeRole (timelock operation scheduling), rotateRouterPausable (pauser component replacement), refreshDefaults (mailbox default update), aggregation deployment helpers, pausable hook/ISM factories, and ownership validation with custom errors.
Redeployer EXA deployment flow
contracts/script/Redeployer.s.sol
Ensures EXA implementation is CREATE3-deployed and cached, upgrades proxy with EXA.initialize() call, performs second upgrade-and-call for EXA.initialize2(TimelockController), changes proxy admin to ProxyAdmin, and introduces deployEXAImpl() helper for cached implementation deployment.
Forked HypEXA integration tests
contracts/test/HypEXA.t.sol
New HypEXATest across Polygon/Base/Optimism forks: setup with Redeployer integration, two multi-hop round-trip tests verifying EXA burn/mint/supply restoration, authorization negatives (revoke BRIDGE_ROLE), timelock proposal scheduling and repeated-proposal revert, router composition validation (hook/ISM arrays/ownership), hook pausing/unpausing with revert assertions, ISM pausing with mailbox mock, Pauser integration, refreshDefaults with mailbox default mocking, pauser rotation restoring transfer and process flows, and internal _upgradeEXA helper.
Gas snapshots & test updates
contracts/.gas-snapshot, contracts/test/Redeployer.t.sol
Updates gas values for ExaPlugin and ExaAccountFactory tests, adds full HypEXATest snapshot entries (14 test cases), refreshes Redeployer snapshots across deploy/prepare/factory flows and Polygon paths, and changes test_deployEXA_deploysAtSameAddress_onBase assertion from minted supply to zero totalSupply().
Changeset and misc
.changeset/beige-sails-worry.md
Adds changeset file with YAML delimiters marking the feature for release notes.

Sequence Diagram

sequenceDiagram
    autonumber
    participant User as "User/Script"
    participant Redeployer as "Redeployer"
    participant HypEXA as "HypEXA"
    participant ProxyAdmin as "ProxyAdmin"
    participant EXA as "EXA Token"
    participant Timelock as "TimelockController"
    participant Router as "HypXERC20 Router"
    participant Mailbox as "Hyperlane Mailbox"

    User->>Redeployer: deployEXA(proxy)
    Redeployer->>Redeployer: deployEXAImpl() if needed
    Redeployer->>ProxyAdmin: upgradeAndCall(impl, initialize)
    ProxyAdmin->>EXA: initialize()
    Redeployer->>ProxyAdmin: upgradeAndCall(impl, initialize2(timelock))
    ProxyAdmin->>EXA: initialize2(timelock)
    Redeployer->>ProxyAdmin: upgradeAndCall(changeAdmin(proxyAdmin))
    
    User->>HypEXA: deployRouter(token, remoteDomains)
    HypEXA->>Router: CREATE3 proxy deploy + aggregation setup
    Router-->>HypEXA: router deployed
    
    User->>HypEXA: proposeBridgeRole(token, salt)
    HypEXA->>Timelock: schedule(grantRole(BRIDGE_ROLE, router))
    Timelock-->>HypEXA: operation pending
    
    User->>Router: transferRemote(amount, domain, payload)
    Router->>Mailbox: sendMessage(payload)
    Mailbox->>Router: process/handle -> mint if BRIDGE_ROLE granted
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • exactly/exa#807: Prior Redeployer implementation and deployEXA flow changes that this PR builds upon with new initialization steps and EXA implementation caching.

Suggested reviewers

  • cruzdanilo
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding Hyperlane support to the redeployer infrastructure, as evidenced by the new HypEXA.s.sol script, HypEXATest.t.sol tests, deploy.json configuration updates, and package.json dependency additions for Hyperlane.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hyperlane
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch hyperlane
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch hyperlane

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.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the project's interoperability by integrating Hyperlane, a cross-chain communication protocol. The changes enable the EXA token to be bridged and managed across different blockchain domains, expanding its utility and reach. This involved updating core dependencies, modifying deployment processes to include Hyperlane-specific components, and adding comprehensive tests to ensure the robustness of the new cross-chain capabilities.

Highlights

  • Hyperlane Integration: Integrated Hyperlane for cross-chain functionality, enabling the EXA token to be transferred and managed across multiple blockchain networks.
  • Deployment and Configuration Updates: Introduced new deployment scripts and configuration settings to facilitate the deployment and setup of Hyperlane routers and an Hyperlane-compatible EXA token.
  • New Cross-Chain Test Suite: Added a dedicated test suite to verify the end-to-end functionality of cross-chain EXA token transfers via Hyperlane, including round-trip scenarios and access control.
  • Dependency Management: Updated project dependencies to include the Hyperlane core library and adjusted existing dependency configurations for compatibility.
Changelog
  • contracts/.gas-snapshot
    • Updated gas consumption benchmarks for various ExaPluginTest and RedeployerTest functions.
  • contracts/deploy.json
    • Added Hyperlane mailbox addresses for Optimism (10), Polygon (137), and Base (8453) domains.
  • contracts/package.json
    • Updated the @exactly/protocol dependency to a specific commit hash.
    • Added the @hyperlane-xyz/core dependency for Hyperlane integration.
  • contracts/remappings.txt
    • Added a remapping for @exactly/protocol to @openzeppelin/contracts.
    • Configured a remapping for @hyperlane-xyz to its node_modules path.
  • contracts/script/Redeployer.s.sol
    • Imported HypERC20Collateral and HypXERC20 from the Hyperlane core library.
    • Modified deployEXA to include an initialize2 call for the EXA token.
    • Added deployEXAImpl to deploy the latest EXA implementation.
    • Implemented upgradeEXA to upgrade an existing EXA proxy.
    • Introduced deployRouter to deploy a HypXERC20 router for cross-chain transfers.
    • Added setupRouter to configure the Hyperlane router with remote domains and grant necessary roles.
  • contracts/test/HypEXA.t.sol
    • Added a new test file for HypEXA to validate cross-chain functionality.
    • Included tests for test_roundTrip_opToBaseToOp and test_roundTrip_opToPolygonToBaseToOp to verify token bridging.
    • Added tests to ensure transferRemote and handle revert without the BRIDGE_ROLE.
  • contracts/test/Redeployer.t.sol
    • Adjusted the assertion for totalSupply in test_deployEXA_deploysAtSameAddress_onBase to expect zero initial supply.
  • cspell.json
    • Added 'hyperlane' and 'XERC' to the spellcheck dictionary.
  • package.json
    • Changed neverBuiltDependencies to onlyBuiltDependencies for @exactly/protocol.
Activity
  • No specific activity (comments, reviews, or progress updates) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

gemini-code-assist[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@cruzdanilo cruzdanilo changed the title Hyperlane 🔨 contracts: support hyperlane on redeployer Mar 3, 2026
@sentry

sentry Bot commented Mar 3, 2026

Copy link
Copy Markdown

✅ All tests passed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
contracts/script/Redeployer.s.sol (3)

96-149: ⚠️ Potential issue | 🟠 Major

Run forge fmt on this file to unblock CI.

The pipeline is currently failing nx run @exactly/plugin:test:fmt due to formatting differences in this file.

As per coding guidelines **/*.sol: Follow Solhint rules strictly and use Forge fmt for code formatting.


113-121: ⚠️ Potential issue | 🟠 Major

Fail fast when EXA implementation is missing before upgradeEXA.

Line 120 upgrades to address(exa) without checking code presence, which defers failure to a less actionable downstream revert.

Proposed fix
 function upgradeEXA(address proxy) external {
   address admin = acct("admin");
+  if (address(exa).code.length == 0) revert EXAImplementationNotDeployed();
   ProxyAdmin p = ProxyAdmin(address(uint160(uint256(
       vm.load(proxy, bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1))
   ))));
   vm.broadcast(p.owner());
   p.upgradeAndCall(
     ITransparentUpgradeableProxy(proxy), address(exa), abi.encodeCall(EXA.initialize2, (admin))
   );
 }
@@
 error ProxyAdminNotDeployed();
 error TargetNonceTooLow();
+error EXAImplementationNotDeployed();

124-130: 🛠️ Refactor suggestion | 🟠 Major

Align CREATE3 salt derivation with token (or remove token from the API).

Line 129 hardcodes "HypEXA" even though the function accepts a token; that creates deterministic-slot collisions for multi-token use, and Line 145 resolves that same fixed slot.

Proposed refactor
 function deployRouter(address token) external returns (HypXERC20 router) {
@@
-        keccak256(abi.encode("HypEXA")),
+        keccak256(abi.encode("HypEXA", token)),
@@
 function setupRouter(address token, uint32 remoteDomain) external {
   address admin = acct("admin");
-  address router = CREATE3_FACTORY.getDeployed(admin, keccak256(abi.encode("HypEXA")));
+  address router = CREATE3_FACTORY.getDeployed(admin, keccak256(abi.encode("HypEXA", token)));

Also applies to: 145-145


ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between adceeef and 7727570.

📒 Files selected for processing (5)
  • .changeset/beige-sails-worry.md
  • contracts/.gas-snapshot
  • contracts/deploy.json
  • contracts/script/Redeployer.s.sol
  • contracts/test/HypEXA.t.sol

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
contracts/script/Redeployer.s.sol (3)

121-127: 🛠️ Refactor suggestion | 🟠 Major

deployRouter ignores token in the deterministic salt, making reuse collision-prone.

The function accepts token but always uses the fixed "HypEXA" slot. Reusing it for another token collides on the same CREATE3 address.

Proposed refactor
-        keccak256(abi.encode("HypEXA")),
+        keccak256(abi.encode("HypEXA", token)),
@@
-    address router = CREATE3_FACTORY.getDeployed(admin, keccak256(abi.encode("HypEXA")));
+    address router = CREATE3_FACTORY.getDeployed(admin, keccak256(abi.encode("HypEXA", token)));

Also applies to: 142-142


140-146: ⚠️ Potential issue | 🟠 Major

setupRouter should fail fast if the deterministic router address is not deployed.

getDeployed can resolve an address before code exists. Without a code-length guard, role/config steps can silently target an undeployed address path.

Proposed fix
 function setupRouter(address token, uint32 remoteDomain) external {
   address admin = acct("admin");
   address router = CREATE3_FACTORY.getDeployed(admin, keccak256(abi.encode("HypEXA")));
+  if (router.code.length == 0) revert RouterNotDeployed();
   vm.startBroadcast(admin);
   EXA(token).grantRole(keccak256("BRIDGE_ROLE"), router);
   HypXERC20(router).enrollRemoteRouter(remoteDomain, bytes32(uint256(uint160(router))));
   vm.stopBroadcast();
 }
@@
 error TargetNonceTooLow();
+error RouterNotDeployed();

113-119: ⚠️ Potential issue | 🟠 Major

upgradeEXA should guard against missing EXA implementation deployment.

The upgrade path uses address(exa) directly; adding an explicit code-length guard gives a clearer, earlier failure mode.

Proposed fix
 function upgradeEXA(address proxy) external {
   address admin = acct("admin");
+  if (address(exa).code.length == 0) revert EXAImplementationNotDeployed();
   ProxyAdmin p =
     ProxyAdmin(address(uint160(uint256(vm.load(proxy, bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1))))));
   vm.broadcast(p.owner());
   p.upgradeAndCall(ITransparentUpgradeableProxy(proxy), address(exa), abi.encodeCall(EXA.initialize2, (admin)));
 }
@@
 error TargetNonceTooLow();
+error EXAImplementationNotDeployed();

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7727570 and c7187e5.

📒 Files selected for processing (5)
  • .changeset/beige-sails-worry.md
  • contracts/.gas-snapshot
  • contracts/deploy.json
  • contracts/script/Redeployer.s.sol
  • contracts/test/HypEXA.t.sol

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@itofarina itofarina marked this pull request as ready for review April 15, 2026 19:27
@itofarina itofarina requested a review from cruzdanilo as a code owner April 15, 2026 19:27
devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment thread package.json
]
},
"neverBuiltDependencies": [],
"onlyBuiltDependencies": ["@exactly/protocol"],

@devin-ai-integration devin-ai-integration Bot Apr 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 onlyBuiltDependencies restricts install scripts to @exactly/protocol only

The change from "neverBuiltDependencies": [] (empty blocklist — all packages can run scripts) to "onlyBuiltDependencies": ["@exactly/protocol"] (allowlist — only @exactly/protocol runs scripts) is a significant behavioral shift. This blocks install/build scripts for ALL other dependencies including the newly added @hyperlane-xyz/core. This is likely intentional since @exactly/protocol changed from an npm published package (^0.2.22) to a git commit reference (exactly/protocol#5833408) that needs a build step, and @hyperlane-xyz/core is a Solidity library that typically doesn't need post-install scripts. However, if any future dependency requires build scripts, they would silently fail to run.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

sentry[bot]

This comment was marked as resolved.

sentry[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e13534f24a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread contracts/script/Redeployer.s.sol Outdated
Comment thread contracts/script/Redeployer.s.sol Outdated
Comment thread contracts/deploy.json Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a2dd3a36b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread contracts/script/Redeployer.s.sol Outdated
Comment thread contracts/script/Redeployer.s.sol

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fdcfcd61d2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread contracts/script/Redeployer.s.sol

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 093e8854-863a-41b2-8f11-96a8b94b1c4d

📥 Commits

Reviewing files that changed from the base of the PR and between 2798151 and fdcfcd6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • .changeset/beige-sails-worry.md
  • contracts/.gas-snapshot
  • contracts/deploy.json
  • contracts/package.json
  • contracts/remappings.txt
  • contracts/script/HypEXA.s.sol
  • contracts/script/Redeployer.s.sol
  • contracts/test/HypEXA.t.sol
  • contracts/test/Redeployer.t.sol
  • cspell.json
  • package.json

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 093e8854-863a-41b2-8f11-96a8b94b1c4d

📥 Commits

Reviewing files that changed from the base of the PR and between 2798151 and fdcfcd6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • .changeset/beige-sails-worry.md
  • contracts/.gas-snapshot
  • contracts/deploy.json
  • contracts/package.json
  • contracts/remappings.txt
  • contracts/script/HypEXA.s.sol
  • contracts/script/Redeployer.s.sol
  • contracts/test/HypEXA.t.sol
  • contracts/test/Redeployer.t.sol
  • cspell.json
  • package.json
🛑 Comments failed to post (3)
contracts/script/HypEXA.s.sol (2)

74-92: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Fragile ordering assumption: hooks[0] and modules[0] may not always be the "exactly" hook/ISM.

Lines 83 and 85 assume that hooks("")[0] and modules[0] correspond to the "exactly" hook/ISM based on the deployment order in _deployAggregations (lines 115-118, 121-124). If the aggregation order ever changes or if external factors reorder the returned arrays, this function will silently reuse the wrong hook/ISM, potentially breaking the rotation.

🛡️ Suggested mitigation

Consider one of these approaches:

Option 1: Store hook/ISM addresses on-chain (e.g., in a mapping or storage variable) during deployment to avoid relying on array index assumptions.

Option 2: Add explicit ownership or identifier checks to verify that hooks[0] and modules[0] are indeed owned by acct("exactly") before reusing them:

 function rotateRouterPausable() external returns (address hook, address ism) {
   address router = CREATE3_FACTORY.getDeployed(acct("admin"), keccak256(abi.encode("HypEXA")));
   if (router.code.length == 0) revert RouterNotDeployed();
   vm.startBroadcast(acct("exactly"));

   (address[] memory modules,) =
     IStaticAggregationIsm(address(HypXERC20(router).interchainSecurityModule())).modulesAndThreshold("");
+  address[] memory hooks = StaticAggregationHook(address(HypXERC20(router).hook())).hooks("");
+  // Verify ownership to ensure we're reusing the correct pausables
+  require(Ownable(hooks[0]).owner() == acct("exactly"), "hooks[0] not owned by exactly");
+  require(Ownable(modules[0]).owner() == acct("exactly"), "modules[0] not owned by exactly");
+  
   (hook, ism) = _deployAggregations(
-    StaticAggregationHook(address(HypXERC20(router).hook())).hooks("")[0],
+    hooks[0],
     _deployPausableHook(acct("pauser")),
     modules[0],
     _deployPausableIsm(acct("pauser"))
   );

94-108: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Same fragile ordering assumption: hooks[0], hooks[1], modules[0], modules[1] indices are not validated.

Line 103 assumes hooks[0] = exactly, hooks[1] = pauser, modules[0] = exactly, modules[1] = pauser. This mirrors the concern in rotateRouterPausable: if the aggregation factory returns arrays in a different order, or if a prior rotation changed the structure, this function will pass the wrong addresses to _deployAggregations, silently breaking the refresh.

🛡️ Suggested mitigation

Apply the same ownership-check pattern as recommended for rotateRouterPausable:

 function refreshDefaults() external returns (address hook, address ism) {
   address router = CREATE3_FACTORY.getDeployed(acct("admin"), keccak256(abi.encode("HypEXA")));
   if (router.code.length == 0) revert RouterNotDeployed();
   vm.startBroadcast(acct("exactly"));

   address[] memory hooks = StaticAggregationHook(address(HypXERC20(router).hook())).hooks("");
   (address[] memory modules,) =
     IStaticAggregationIsm(address(HypXERC20(router).interchainSecurityModule())).modulesAndThreshold("");
+  // Verify ownership to ensure correct hook/ISM pairing
+  require(Ownable(hooks[0]).owner() == acct("exactly"), "hooks[0] not owned by exactly");
+  require(Ownable(hooks[1]).owner() == acct("pauser"), "hooks[1] not owned by pauser");
+  require(Ownable(modules[0]).owner() == acct("exactly"), "modules[0] not owned by exactly");
+  require(Ownable(modules[1]).owner() == acct("pauser"), "modules[1] not owned by pauser");
+  
   (hook, ism) = _deployAggregations(hooks[0], hooks[1], modules[0], modules[1]);
   HypXERC20(router).setHook(hook);
   HypXERC20(router).setInterchainSecurityModule(ism);

   vm.stopBroadcast();
 }
cspell.json (1)

43-43: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move "ciphertext" before "clippy" to maintain alphabetical order.

The word list must be sorted alphabetically. "ciphertext" (c-i-p-h) comes before "clippy" (c-l-i-p) because 'i' < 'l'. As per coding guidelines, items should be added in sorted positions.

📝 Proposed fix
 "checkpointing",
 "checksummed",
+"ciphertext",
 "CLABE",
 "clippy",
-"ciphertext",
 "codegen",

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
contracts/deploy.json (1)

15-18: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Polygon support is only half-wired in this config.

contracts/script/HypEXA.s.sol resolves acct("exactly") and acct("pauser") on the active chain in deployRouter(), rotateRouterPausable(), and refreshDefaults(), but this file only defines those names for chains 10 and 8453. At the same time, the new hyperlaneMailbox, hyperlaneAggregationHookFactory, and hyperlaneAggregationIsmFactory entries now advertise local support on chain 137, so acct(...) will still fail when the script tries to read .accounts.exactly or .accounts.pauser on Polygon. Add the 137 owner mappings or drop the Polygon-local Hyperlane entries.

Also applies to: 19-33, 56-59

contracts/script/HypEXA.s.sol (2)

1-163: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Rename this new script file to kebab-case before more imports depend on it.

contracts/script/HypEXA.s.sol breaks the repository-wide file naming rule. Renaming it now is much cheaper than after more Solidity imports, script invocations, and docs start depending on this path.

As per coding guidelines, **/*: use kebab-case for files, directories, and anything else where you have freedom to choose.

Source: Coding guidelines


22-25: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make deployRouter() idempotent for remote enrollment.

Line 25 returns before the only enrollRemoteRouters() call at Lines 49-55. After the first CREATE3 deployment, rerunning this script with new remoteDomains silently does nothing, so newly added chains cannot be backfilled from this script even though this is the only entrypoint in the file that configures remote routers. Keep an "already deployed" branch that still enrolls missing domains, or split enrollment into a dedicated maintenance function.

Also applies to: 49-56

contracts/test/HypEXA.t.sol (1)

33-33: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Rename contracts/test/HypEXA.t.sol to kebab-case.

This new test file breaks the repo naming rule. Please rename it to a kebab-case path such as contracts/test/hyp-exa.t.sol and update any references accordingly.

As per coding guidelines, "use kebab-case for files, directories, and anything else where you have freedom to choose."

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e1e8e4de-829d-4aa7-ad24-adf7aa79153b

📥 Commits

Reviewing files that changed from the base of the PR and between f9af994 and 9e55785.

📒 Files selected for processing (3)
  • contracts/deploy.json
  • contracts/script/HypEXA.s.sol
  • contracts/test/HypEXA.t.sol

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c9d9da16-74e1-423c-baa4-829c25e83318

📥 Commits

Reviewing files that changed from the base of the PR and between 9e55785 and 0a4c9d8.

📒 Files selected for processing (1)
  • contracts/script/Redeployer.s.sol

Comment on lines +204 to +208
proxyAdmin.upgradeAndCall(
ITransparentUpgradeableProxy(proxy),
address(exa),
abi.encodeCall(TransparentUpgradeableProxy.changeAdmin, (protocol("ProxyAdmin")))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect local ProxyAdmin APIs:"
fd 'ProxyAdmin\.sol$' | while read -r f; do
  echo "== $f =="
  rg -n 'function\s+(upgradeAndCall|changeProxyAdmin|transferOwnership)\b' "$f" || true
done

echo
echo "Inspect local TransparentUpgradeableProxy admin APIs:"
fd 'TransparentUpgradeableProxy\.sol$' | while read -r f; do
  echo "== $f =="
  rg -n 'function\s+(changeAdmin|upgradeToAndCall)\b|_upgradeToAndCall|delegatecall|functionDelegateCall' "$f" || true
done

echo
echo "Check whether EXA implements the calldata being passed through upgradeAndCall:"
fd 'EXA\.sol$' | while read -r f; do
  echo "== $f =="
  rg -n 'function\s+changeAdmin\s*\(' "$f" || true
done

Repository: exactly/exa

Length of output: 221


🏁 Script executed:

fd -type f -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" | head -5

Repository: exactly/exa

Length of output: 226


🌐 Web query:

OpenZeppelin ProxyAdmin upgradeAndCall behavior v4 v5 admin handoff

💡 Result:

In OpenZeppelin Contracts v5.x, the ProxyAdmin contract and its upgradeAndCall function underwent architectural changes compared to v4.x to improve gas efficiency and security [1][2]. Behavior of upgradeAndCall in v5: In v5.0 and later, the ProxyAdmin contract exposes only the upgradeAndCall function for upgrading proxies [2][3]. The older upgrade function (which did not take data) has been removed [2]. If no specific function call is required during an upgrade, you must pass an empty byte string (0x) as the third argument [2]. Notably, because this version uses an immutable admin pattern, the UPGRADE_INTERFACE_VERSION is set to "5.0.0", making it impossible to invoke the receive function on the new implementation during an upgrade via this interface [2][3]. Admin Handoff and ProxyAdmin Reuse: A critical difference between v4 and v5 is the treatment of the ProxyAdmin address. In v5.0, the admin address on the TransparentUpgradeableProxy was made immutable—stored in bytecode rather than storage—to eliminate the high gas cost of reading the admin from storage during every proxy call [1]. Consequently: - Reuse of ProxyAdmin contracts is discouraged and effectively disabled for new deployments [4]. - Attempting to reuse a v4-style ProxyAdmin with v5 proxies will not work as expected because the v5 proxy expects an immutable admin [1][4]. - To handle admin management across multiple proxies in v5, you should deploy a dedicated ProxyAdmin for each proxy while using the same owner address (e.g., a MultiSig or DAO) across all of them [1]. - Alternatively, switching to UUPS (Universal Upgradeable Proxy Standard) proxies allows the upgrade logic to reside within the implementation contract, permitting a single admin to manage multiple proxies without needing separate ProxyAdmin instances [1]. In summary, the transition from v4 to v5 prioritizes gas efficiency by locking the admin at deployment [1]. While v4 allowed for flexible admin management, v5 standardizes the pattern where each TransparentUpgradeableProxy has its own ProxyAdmin, which is controlled by a common owner [1][4].

Citations:


🏁 Script executed:

cat -n contracts/script/Redeployer.s.sol | head -30

Repository: exactly/exa

Length of output: 1346


🏁 Script executed:

cat -n contracts/script/Redeployer.s.sol | sed -n '190,220p'

Repository: exactly/exa

Length of output: 1512


🏁 Script executed:

rg -n "proxyThrough|new.*ProxyAdmin|new.*TransparentUpgradeableProxy" contracts/script/Redeployer.s.sol -A 3

Repository: exactly/exa

Length of output: 519


🏁 Script executed:

fd 'EXA\.sol$' | head -1 | xargs -I {} cat {} | head -50

Repository: exactly/exa

Length of output: 37


🏁 Script executed:

rg -n "function\s+changeAdmin|function\s+initialize" contracts -A 2 | grep -A 2 "EXA\.sol"

Repository: exactly/exa

Length of output: 37


🌐 Web query:

OpenZeppelin v5 ProxyAdmin upgradeAndCall function signature implementation transparent proxy

💡 Result:

In OpenZeppelin v5, the ProxyAdmin contract provides the upgradeAndCall function to facilitate upgrading a TransparentUpgradeableProxy and executing a function on the new implementation in a single transaction [1][2]. The function signature for upgradeAndCall in ProxyAdmin.sol is as follows [2][3]: function upgradeAndCall( ITransparentUpgradeableProxy proxy, address implementation, bytes memory data) public payable virtual onlyOwner Implementation Details: 1. Function Logic: The ProxyAdmin contract acts as the administrative owner of the proxy. When upgradeAndCall is invoked, it calls the upgradeToAndCall function on the provided ITransparentUpgradeableProxy interface [2][4]. 2. Proxy Transparency: The TransparentUpgradeableProxy does not implement the upgradeability functions (like upgradeToAndCall) in its public ABI to prevent selector clashes [1][4]. Instead, it uses a custom dispatch mechanism within its _fallback function. When the proxy's admin (the ProxyAdmin) calls it, the proxy detects the call and routes it to an internal _dispatchUpgradeToAndCall function [1][4]. 3. Upgrade Interface Version: In v5, ProxyAdmin explicitly defines a constant string UPGRADE_INTERFACE_VERSION = "5.0.0" [2][5]. This indicates that only the upgradeAndCall(address,address,bytes) signature is supported [1]. Because of this, if you do not need to call a function during the upgrade, you must pass an empty byte string (hex"") as the third argument [1][2]. 4. Requirements: - The caller must be the owner of the ProxyAdmin [2]. - The ProxyAdmin must be the designated admin of the target proxy [2]. - If the data parameter is empty, the msg.value must be zero [2].

Citations:


🏁 Script executed:

cat -n contracts/script/Redeployer.s.sol | sed -n '160,185p'

Repository: exactly/exa

Length of output: 1218


🌐 Web query:

OpenZeppelin v5 ProxyAdmin changeProxyAdmin method signature

💡 Result:

In OpenZeppelin Contracts v5, the ProxyAdmin contract does not have a changeProxyAdmin method [1][2]. The ability to change the admin of a TransparentUpgradeableProxy was removed in v5 [3]. In this version, the admin address is stored in an immutable variable within the TransparentUpgradeableProxy contract, which is set during construction [4][5][3]. Consequently, it is no longer possible to change the admin address of a proxy contract after it has been deployed [4][3]. Transfer of administrative capabilities is instead handled exclusively through the ownership of the ProxyAdmin contract [6][3]. Because the ProxyAdmin inherits from OpenZeppelin's Ownable contract, you can manage who has the authority to perform upgrades by using the standard ownership transfer functions: - renounceOwnership [7] - transferOwnership(address newOwner) [7] By transferring the ownership of the ProxyAdmin instance to a new address, you effectively transfer the control over the upgrade process for all proxies managed by that specific ProxyAdmin [6][3].

Citations:


Do not route changeAdmin through upgradeAndCall.

The payload is executed as setup calldata on the EXA implementation during the proxy's delegatecall, not as a proxy admin call, causing the admin handoff to revert or silently fail and leave the proxy admin unchanged. Additionally, in OpenZeppelin v5 (which this code uses for ProxyAdmin), the proxy admin is immutable and cannot be changed after deployment—there is no changeProxyAdmin method.

To change the admin, either call the appropriate method directly on the proxy if using v4, or transfer ownership of the ProxyAdmin contract itself if you need to change who controls upgrades. Ensure version compatibility between TransparentUpgradeableProxy (v4) and ProxyAdmin (v5).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a4c9d83ee

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +204 to +208
proxyAdmin.upgradeAndCall(
ITransparentUpgradeableProxy(proxy),
address(exa),
abi.encodeCall(TransparentUpgradeableProxy.changeAdmin, (protocol("ProxyAdmin")))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Call changeAdmin on the proxy, not through upgradeAndCall

When deployEXA reaches this step for a fresh EXA proxy, ProxyAdmin.upgradeAndCall sends these bytes as an initializer delegatecall to the new EXA implementation, not as an admin call handled by the transparent proxy. The pinned EXA implementation has initialize/initialize2 but no changeAdmin(address), so this third upgrade reverts and the deployment never hands off to the protocol ProxyAdmin.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants