-
Notifications
You must be signed in to change notification settings - Fork 2
🔨 contracts: support hyperlane on redeployer #860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4ad3919
ef6f789
04fef7b
f3c7401
df40d74
a778f0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,9 @@ import { WebauthnOwnerPlugin } from "webauthn-owner-plugin/WebauthnOwnerPlugin.s | |
|
|
||
| import { EXA } from "@exactly/protocol/periphery/EXA.sol"; | ||
|
|
||
| import { HypERC20Collateral } from "@hyperlane-xyz/core/contracts/token/HypERC20Collateral.sol"; | ||
| import { HypXERC20 } from "@hyperlane-xyz/core/contracts/token/extensions/HypXERC20.sol"; | ||
|
|
||
| import { ExaAccountFactory } from "../src/ExaAccountFactory.sol"; | ||
| import { | ||
| ExaPlugin, | ||
|
|
@@ -90,9 +93,56 @@ contract Redeployer is BaseScript { | |
|
|
||
| /// @notice Deploys EXA token and upgrades the proxy to it. | ||
| function deployEXA(address proxy) external { | ||
| vm.startBroadcast(acct("admin")); | ||
| address admin = acct("admin"); | ||
| vm.startBroadcast(admin); | ||
| exa = EXA(CREATE3_FACTORY.deploy(keccak256(abi.encode("EXA")), vm.getCode("EXA.sol:EXA"))); | ||
| proxyAdmin.upgradeAndCall(ITransparentUpgradeableProxy(proxy), address(exa), abi.encodeCall(EXA.initialize, ())); | ||
| proxyAdmin.upgradeAndCall( | ||
| ITransparentUpgradeableProxy(proxy), address(exa), abi.encodeCall(EXA.initialize2, (admin)) | ||
| ); | ||
itofarina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| vm.stopBroadcast(); | ||
| } | ||
|
|
||
| /// @notice Deploys the latest EXA implementation via CREATE3. | ||
| function deployEXAImpl() external { | ||
| vm.broadcast(acct("admin")); | ||
| exa = EXA(CREATE3_FACTORY.deploy(keccak256(abi.encode("EXA")), vm.getCode("EXA.sol:EXA"))); | ||
| } | ||
|
|
||
| /// @notice Upgrades an existing EXA proxy to the latest implementation. | ||
| function upgradeEXA(address proxy) external { | ||
| address admin = acct("admin"); | ||
| 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))); | ||
| } | ||
|
|
||
| function deployRouter(address token) external returns (HypXERC20 router) { | ||
| address admin = acct("admin"); | ||
| vm.startBroadcast(admin); | ||
| router = HypXERC20( | ||
| CREATE3_FACTORY.deploy( | ||
| keccak256(abi.encode("HypEXA")), | ||
| abi.encodePacked( | ||
itofarina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| type(TransparentUpgradeableProxy).creationCode, | ||
| abi.encode( | ||
| address(new HypXERC20(token, 1, acct("mailbox"))), | ||
| address(proxyAdmin), | ||
| abi.encodeCall(HypERC20Collateral.initialize, (address(0), address(0), admin)) | ||
itofarina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
|
Comment on lines
+121
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Where proxyAdmin deployment is currently validated:"
rg -nP 'address\(proxyAdmin\)\.code\.length' contracts/script/Redeployer.s.sol
echo
echo "run() guard context:"
sed -n '71,75p' contracts/script/Redeployer.s.sol
echo
echo "deployRouter() context:"
sed -n '121,138p' contracts/script/Redeployer.s.solRepository: exactly/exa Length of output: 1088 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Full deployRouter() function signature:"
sed -n '121,121p' contracts/script/Redeployer.s.sol
echo
echo "Check for other callers of deployRouter in the file:"
rg -n 'deployRouter' contracts/script/Redeployer.s.sol
echo
echo "Check if proxyAdmin is set before deployRouter calls:"
sed -n '70,85p' contracts/script/Redeployer.s.solRepository: exactly/exa Length of output: 1056 Add The 🛠️ Proposed fix function deployRouter(address token) external returns (HypXERC20 router) {
address admin = acct("admin");
+ if (address(proxyAdmin).code.length == 0) revert ProxyAdminNotDeployed();
vm.startBroadcast(admin);
router = HypXERC20(
CREATE3_FACTORY.deploy(
keccak256(abi.encode("HypEXA")), |
||
| ) | ||
| ) | ||
| ); | ||
| vm.stopBroadcast(); | ||
| } | ||
|
|
||
| function setupRouter(address token, uint32 remoteDomain) external { | ||
| address admin = acct("admin"); | ||
| address router = CREATE3_FACTORY.getDeployed(admin, keccak256(abi.encode("HypEXA"))); | ||
| vm.startBroadcast(admin); | ||
| if (!EXA(token).hasRole(keccak256("BRIDGE_ROLE"), router)) EXA(token).grantRole(keccak256("BRIDGE_ROLE"), router); | ||
| HypXERC20(router).enrollRemoteRouter(remoteDomain, bytes32(uint256(uint160(router)))); | ||
| vm.stopBroadcast(); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0 | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import { EXA } from "@exactly/protocol/periphery/EXA.sol"; | ||
| import { TypeCasts } from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol"; | ||
| import { HypXERC20 } from "@hyperlane-xyz/core/contracts/token/extensions/HypXERC20.sol"; | ||
|
|
||
| import { Redeployer } from "../script/Redeployer.s.sol"; | ||
| import { ForkTest } from "./Fork.t.sol"; | ||
|
|
||
| contract HypEXATest is ForkTest { | ||
| using TypeCasts for address; | ||
|
|
||
| uint256 internal opFork; | ||
| uint256 internal baseFork; | ||
| uint256 internal polygonFork; | ||
| HypXERC20 internal opRouter; | ||
| HypXERC20 internal baseRouter; | ||
| HypXERC20 internal polygonRouter; | ||
| address internal admin; | ||
| address internal opMailbox; | ||
| address internal baseMailbox; | ||
| address internal polygonMailbox; | ||
| EXA internal exa = EXA(0x1e925De1c68ef83bD98eE3E130eF14a50309C01B); | ||
| address internal exaHolder = 0x92024C4bDa9DA602b711B9AbB610d072018eb58b; | ||
|
|
||
| uint32 internal constant OP_DOMAIN = 10; | ||
| uint32 internal constant BASE_DOMAIN = 8453; | ||
| uint32 internal constant POLYGON_DOMAIN = 137; | ||
|
|
||
| function setUp() external { | ||
| polygonFork = vm.createSelectFork("polygon", 83_700_000); | ||
| polygonMailbox = acct("mailbox"); | ||
| Redeployer polygonRedeployer = new Redeployer(); | ||
| polygonRedeployer.setUp(); | ||
| if (address(polygonRedeployer.proxyAdmin()).code.length == 0) polygonRedeployer.prepare(); | ||
| polygonRedeployer.run(polygonRedeployer.findNonce(acct("deployer"), address(exa), 1000) + 1); | ||
| polygonRedeployer.deployEXA(address(exa)); | ||
| polygonRouter = polygonRedeployer.deployRouter(address(exa)); | ||
| polygonRedeployer.setupRouter(address(exa), OP_DOMAIN); | ||
| polygonRedeployer.setupRouter(address(exa), BASE_DOMAIN); | ||
|
|
||
| baseFork = vm.createSelectFork("base", 42_380_000); | ||
| baseMailbox = acct("mailbox"); | ||
| Redeployer baseRedeployer = new Redeployer(); | ||
| baseRedeployer.setUp(); | ||
| if (address(baseRedeployer.proxyAdmin()).code.length == 0) baseRedeployer.prepare(); | ||
| baseRedeployer.run(baseRedeployer.findNonce(acct("deployer"), address(exa), 1000) + 1); | ||
| baseRedeployer.deployEXA(address(exa)); | ||
| baseRouter = baseRedeployer.deployRouter(address(exa)); | ||
| baseRedeployer.setupRouter(address(exa), OP_DOMAIN); | ||
| baseRedeployer.setupRouter(address(exa), POLYGON_DOMAIN); | ||
|
|
||
| opFork = vm.createSelectFork("optimism", 147_967_000); | ||
| opMailbox = acct("mailbox"); | ||
| admin = acct("admin"); | ||
| Redeployer opRedeployer = new Redeployer(); | ||
| opRedeployer.setUp(); | ||
| opRedeployer.deployEXAImpl(); | ||
| opRedeployer.upgradeEXA(address(exa)); | ||
| opRouter = opRedeployer.deployRouter(address(exa)); | ||
| opRedeployer.setupRouter(address(exa), BASE_DOMAIN); | ||
| opRedeployer.setupRouter(address(exa), POLYGON_DOMAIN); | ||
| } | ||
|
|
||
| // solhint-disable func-name-mixedcase | ||
|
|
||
| function test_roundTrip_opToBaseToOp() external { | ||
| address receiver = makeAddr("receiver"); | ||
| uint256 amount = 100e18; | ||
| uint256 opSupply = exa.totalSupply(); | ||
|
|
||
| uint256 fee = opRouter.quoteGasPayment(BASE_DOMAIN); | ||
| vm.deal(exaHolder, fee); | ||
| vm.prank(exaHolder); | ||
| opRouter.transferRemote{ value: fee }(BASE_DOMAIN, exaHolder.addressToBytes32(), amount); | ||
| assertEq(exa.totalSupply(), opSupply - amount, "op didn't burn"); | ||
|
|
||
| vm.selectFork(baseFork); | ||
| vm.prank(baseMailbox); | ||
| baseRouter.handle( | ||
| OP_DOMAIN, address(opRouter).addressToBytes32(), abi.encodePacked(exaHolder.addressToBytes32(), amount) | ||
| ); | ||
| assertEq(exa.balanceOf(exaHolder), amount, "base didn't credit holder"); | ||
| assertEq(exa.totalSupply(), amount, "base didn't mint"); | ||
|
|
||
| fee = baseRouter.quoteGasPayment(OP_DOMAIN); | ||
| vm.deal(exaHolder, fee); | ||
| vm.prank(exaHolder); | ||
| baseRouter.transferRemote{ value: fee }(OP_DOMAIN, receiver.addressToBytes32(), amount); | ||
| assertEq(exa.totalSupply(), 0, "base didn't burn"); | ||
|
|
||
| vm.selectFork(opFork); | ||
| vm.prank(opMailbox); | ||
| opRouter.handle( | ||
| BASE_DOMAIN, address(baseRouter).addressToBytes32(), abi.encodePacked(receiver.addressToBytes32(), amount) | ||
| ); | ||
| assertEq(exa.balanceOf(receiver), amount, "op didn't credit receiver"); | ||
| assertEq(exa.totalSupply(), opSupply, "op didn't restore supply"); | ||
| } | ||
|
|
||
| function test_roundTrip_opToPolygonToBaseToOp() external { | ||
| uint256 amount = 100e18; | ||
| uint256 opSupply = exa.totalSupply(); | ||
|
|
||
| uint256 fee = opRouter.quoteGasPayment(POLYGON_DOMAIN); | ||
| vm.deal(exaHolder, fee); | ||
| vm.prank(exaHolder); | ||
| opRouter.transferRemote{ value: fee }(POLYGON_DOMAIN, exaHolder.addressToBytes32(), amount); | ||
| assertEq(exa.totalSupply(), opSupply - amount, "op didn't burn"); | ||
|
|
||
| vm.selectFork(polygonFork); | ||
| uint256 polygonSupply = exa.totalSupply(); | ||
| vm.prank(polygonMailbox); | ||
| polygonRouter.handle( | ||
| OP_DOMAIN, address(opRouter).addressToBytes32(), abi.encodePacked(exaHolder.addressToBytes32(), amount) | ||
| ); | ||
| assertEq(exa.totalSupply(), polygonSupply + amount, "polygon didn't mint"); | ||
|
|
||
| fee = polygonRouter.quoteGasPayment(BASE_DOMAIN); | ||
| vm.deal(exaHolder, fee); | ||
| vm.prank(exaHolder); | ||
| polygonRouter.transferRemote{ value: fee }(BASE_DOMAIN, exaHolder.addressToBytes32(), amount); | ||
| assertEq(exa.totalSupply(), polygonSupply, "polygon didn't burn"); | ||
|
|
||
| vm.selectFork(baseFork); | ||
| uint256 baseSupply = exa.totalSupply(); | ||
| vm.prank(baseMailbox); | ||
| baseRouter.handle( | ||
| POLYGON_DOMAIN, address(polygonRouter).addressToBytes32(), abi.encodePacked(exaHolder.addressToBytes32(), amount) | ||
| ); | ||
| assertEq(exa.totalSupply(), baseSupply + amount, "base didn't mint"); | ||
|
|
||
| fee = baseRouter.quoteGasPayment(OP_DOMAIN); | ||
| vm.deal(exaHolder, fee); | ||
| vm.prank(exaHolder); | ||
| baseRouter.transferRemote{ value: fee }(OP_DOMAIN, exaHolder.addressToBytes32(), amount); | ||
| assertEq(exa.totalSupply(), baseSupply, "base didn't burn"); | ||
|
|
||
| vm.selectFork(opFork); | ||
| vm.prank(opMailbox); | ||
| opRouter.handle( | ||
| BASE_DOMAIN, address(baseRouter).addressToBytes32(), abi.encodePacked(exaHolder.addressToBytes32(), amount) | ||
| ); | ||
| assertEq(exa.totalSupply(), opSupply, "op didn't restore supply"); | ||
| } | ||
|
|
||
| function test_transferRemote_reverts_withoutBridgeRole() external { | ||
| vm.prank(admin); | ||
| exa.revokeRole(keccak256("BRIDGE_ROLE"), address(opRouter)); | ||
|
|
||
| uint256 fee = opRouter.quoteGasPayment(BASE_DOMAIN); | ||
| vm.deal(exaHolder, fee); | ||
| vm.prank(exaHolder); | ||
| vm.expectRevert(); | ||
| opRouter.transferRemote{ value: fee }(BASE_DOMAIN, makeAddr("receiver").addressToBytes32(), 100e18); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| function test_handle_reverts_withoutBridgeRole() external { | ||
| vm.selectFork(baseFork); | ||
| vm.prank(admin); | ||
| exa.revokeRole(keccak256("BRIDGE_ROLE"), address(baseRouter)); | ||
|
|
||
| vm.prank(baseMailbox); | ||
| vm.expectRevert(); | ||
| baseRouter.handle( | ||
| OP_DOMAIN, | ||
| address(opRouter).addressToBytes32(), | ||
| abi.encodePacked(makeAddr("receiver").addressToBytes32(), uint256(100e18)) | ||
| ); | ||
| } | ||
|
|
||
| // solhint-enable func-name-mixedcase | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding a mailbox
default(or explicit unsupported-chain guard).The
acct("mailbox")path falls back to.accounts.mailbox.defaultwhen a chain key is absent; without it, unsupported chains fail with a generic JSON key error instead of a clearer deployment-time failure.