Conversation
🦋 Changeset detectedLatest commit: 0a4c9d8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen 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 |
WalkthroughAdds 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. ChangesHypEXA and Redeployer integration
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
✨ Simplify code
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. Comment |
Summary of ChangesHello, 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
Changelog
Activity
Using Gemini Code AssistThe 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
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 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
|
|
✅ All tests passed. |
There was a problem hiding this comment.
♻️ Duplicate comments (3)
contracts/script/Redeployer.s.sol (3)
96-149:⚠️ Potential issue | 🟠 MajorRun
forge fmton this file to unblock CI.The pipeline is currently failing
nx run@exactly/plugin:test:fmtdue 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 | 🟠 MajorFail 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 | 🟠 MajorAlign CREATE3 salt derivation with
token(or removetokenfrom the API).Line 129 hardcodes
"HypEXA"even though the function accepts atoken; 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
📒 Files selected for processing (5)
.changeset/beige-sails-worry.mdcontracts/.gas-snapshotcontracts/deploy.jsoncontracts/script/Redeployer.s.solcontracts/test/HypEXA.t.sol
There was a problem hiding this comment.
♻️ Duplicate comments (3)
contracts/script/Redeployer.s.sol (3)
121-127: 🛠️ Refactor suggestion | 🟠 Major
deployRouterignorestokenin the deterministic salt, making reuse collision-prone.The function accepts
tokenbut 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
setupRoutershould fail fast if the deterministic router address is not deployed.
getDeployedcan 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
upgradeEXAshould 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
📒 Files selected for processing (5)
.changeset/beige-sails-worry.mdcontracts/.gas-snapshotcontracts/deploy.jsoncontracts/script/Redeployer.s.solcontracts/test/HypEXA.t.sol
| ] | ||
| }, | ||
| "neverBuiltDependencies": [], | ||
| "onlyBuiltDependencies": ["@exactly/protocol"], |
There was a problem hiding this comment.
🚩 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 093e8854-863a-41b2-8f11-96a8b94b1c4d
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
.changeset/beige-sails-worry.mdcontracts/.gas-snapshotcontracts/deploy.jsoncontracts/package.jsoncontracts/remappings.txtcontracts/script/HypEXA.s.solcontracts/script/Redeployer.s.solcontracts/test/HypEXA.t.solcontracts/test/Redeployer.t.solcspell.jsonpackage.json
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
.changeset/beige-sails-worry.mdcontracts/.gas-snapshotcontracts/deploy.jsoncontracts/package.jsoncontracts/remappings.txtcontracts/script/HypEXA.s.solcontracts/script/Redeployer.s.solcontracts/test/HypEXA.t.solcontracts/test/Redeployer.t.solcspell.jsonpackage.json
🛑 Comments failed to post (3)
contracts/script/HypEXA.s.sol (2)
74-92:
⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftFragile ordering assumption:
hooks[0]andmodules[0]may not always be the "exactly" hook/ISM.Lines 83 and 85 assume that
hooks("")[0]andmodules[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]andmodules[0]are indeed owned byacct("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 liftSame 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 inrotateRouterPausable: 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 winMove
"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
There was a problem hiding this comment.
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 winPolygon support is only half-wired in this config.
contracts/script/HypEXA.s.solresolvesacct("exactly")andacct("pauser")on the active chain indeployRouter(),rotateRouterPausable(), andrefreshDefaults(), but this file only defines those names for chains10and8453. At the same time, the newhyperlaneMailbox,hyperlaneAggregationHookFactory, andhyperlaneAggregationIsmFactoryentries now advertise local support on chain137, soacct(...)will still fail when the script tries to read.accounts.exactlyor.accounts.pauseron Polygon. Add the137owner 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 winRename this new script file to kebab-case before more imports depend on it.
contracts/script/HypEXA.s.solbreaks 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 liftMake
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 newremoteDomainssilently 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 winRename
contracts/test/HypEXA.t.solto 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.soland 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
📒 Files selected for processing (3)
contracts/deploy.jsoncontracts/script/HypEXA.s.solcontracts/test/HypEXA.t.sol
| proxyAdmin.upgradeAndCall( | ||
| ITransparentUpgradeableProxy(proxy), | ||
| address(exa), | ||
| abi.encodeCall(TransparentUpgradeableProxy.changeAdmin, (protocol("ProxyAdmin"))) | ||
| ); |
There was a problem hiding this comment.
🧩 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
doneRepository: exactly/exa
Length of output: 221
🏁 Script executed:
fd -type f -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" | head -5Repository: 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:
- 1: Make TransparentUpgradeableProxy work with existing ProxyAdmins OpenZeppelin/openzeppelin-contracts#5855
- 2: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.6.1/contracts/proxy/transparent/ProxyAdmin.sol
- 3: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/transparent/ProxyAdmin.sol
- 4: https://docs.openzeppelin.com/upgrades-plugins
🏁 Script executed:
cat -n contracts/script/Redeployer.s.sol | head -30Repository: 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 3Repository: exactly/exa
Length of output: 519
🏁 Script executed:
fd 'EXA\.sol$' | head -1 | xargs -I {} cat {} | head -50Repository: 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:
- 1: https://docs.openzeppelin.com/contracts/5.x/api/proxy
- 2: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.6.1/contracts/proxy/transparent/ProxyAdmin.sol
- 3: https://raw.githubusercontent.com/OpenZeppelin/docs/refs/heads/main/content/contracts/5.x/api/proxy.mdx
- 4: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.6.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol
- 5: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/transparent/ProxyAdmin.sol
🏁 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:
- 1: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/proxy/transparent/ProxyAdmin.sol
- 2: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.6.1/contracts/proxy/transparent/ProxyAdmin.sol
- 3: https://docs.openzeppelin.com/contracts/5.x/changelog
- 4: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v5.6/contracts/proxy/transparent/TransparentUpgradeableProxy.sol
- 5: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.6.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol
- 6: https://docs.openzeppelin.com/contracts/5.x/api/proxy
- 7: https://raw.githubusercontent.com/OpenZeppelin/docs/refs/heads/main/content/contracts/5.x/api/proxy.mdx
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).
There was a problem hiding this comment.
💡 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".
| proxyAdmin.upgradeAndCall( | ||
| ITransparentUpgradeableProxy(proxy), | ||
| address(exa), | ||
| abi.encodeCall(TransparentUpgradeableProxy.changeAdmin, (protocol("ProxyAdmin"))) | ||
| ); |
There was a problem hiding this comment.
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 👍 / 👎.
Summary by CodeRabbit