diff --git a/.changeset/beige-sails-worry.md b/.changeset/beige-sails-worry.md new file mode 100644 index 0000000000..a49ba48448 --- /dev/null +++ b/.changeset/beige-sails-worry.md @@ -0,0 +1,2 @@ +--- +--- \ No newline at end of file diff --git a/contracts/.gas-snapshot b/contracts/.gas-snapshot index 8593776b30..54d322c3a7 100644 --- a/contracts/.gas-snapshot +++ b/contracts/.gas-snapshot @@ -1,11 +1,11 @@ -ExaAccountFactoryTest:testFuzz_createAccount_EOAOwners(uint256,address[63]) (runs: 256, μ: 5856150, ~: 5360703) +ExaAccountFactoryTest:testFuzz_createAccount_EOAOwners(uint256,address[63]) (runs: 256, μ: 5887874, ~: 5393451) ExaAccountFactoryTest:test_deploy_deploysToSameAddress() (gas: 15501257) -ExaPluginTest:testFork_claimAndVestEscrowedEXA_claimsAndVests() (gas: 38649064) -ExaPluginTest:testFork_collectCollateral_collects() (gas: 32148122) -ExaPluginTest:testFork_crossRepay_repays() (gas: 33970276) -ExaPluginTest:testFork_repay_whenFlashLoanerHasFees() (gas: 25585110) -ExaPluginTest:testFork_stakeEXA_stakes() (gas: 32727092) -ExaPluginTest:testFork_swap_swaps() (gas: 28088327) +ExaPluginTest:testFork_claimAndVestEscrowedEXA_claimsAndVests() (gas: 38666217) +ExaPluginTest:testFork_collectCollateral_collects() (gas: 32164167) +ExaPluginTest:testFork_crossRepay_repays() (gas: 33986321) +ExaPluginTest:testFork_repay_whenFlashLoanerHasFees() (gas: 25591707) +ExaPluginTest:testFork_stakeEXA_stakes() (gas: 32743609) +ExaPluginTest:testFork_swap_swaps() (gas: 28102911) ExaPluginTest:test_allowPlugin_emitsPluginAllowed() (gas: 58464) ExaPluginTest:test_allowPlugin_reverts_whenAddressZero() (gas: 32411) ExaPluginTest:test_allowPlugin_reverts_whenNotAdmin() (gas: 40964) @@ -32,12 +32,12 @@ ExaPluginTest:test_collectCredit_collects_whenHealthFactorHigherThanOne() (gas: ExaPluginTest:test_collectCredit_collects_whenProposalCausesInsufficientLiquidity() (gas: 1064982) ExaPluginTest:test_collectCredit_collects_whenProposalLeavesHealthFactorLowerThanOne() (gas: 1055379) ExaPluginTest:test_collectCredit_collects_withEnoughSlippage() (gas: 782246) -ExaPluginTest:test_collectCredit_collects_withPrevIssuerSignature() (gas: 986741) +ExaPluginTest:test_collectCredit_collects_withPrevIssuerSignature() (gas: 987065) ExaPluginTest:test_collectCredit_passes_whenProposalLeavesEnoughLiquidity() (gas: 1082579) ExaPluginTest:test_collectCredit_reverts_asNotKeeper() (gas: 342148) ExaPluginTest:test_collectCredit_reverts_whenDisagreement() (gas: 538817) ExaPluginTest:test_collectCredit_reverts_whenExpired() (gas: 352767) -ExaPluginTest:test_collectCredit_reverts_whenPrevSignatureNotValidAnymore() (gas: 483654) +ExaPluginTest:test_collectCredit_reverts_whenPrevSignatureNotValidAnymore() (gas: 483978) ExaPluginTest:test_collectCredit_reverts_whenReplay() (gas: 825827) ExaPluginTest:test_collectCredit_reverts_whenTimelocked() (gas: 334568) ExaPluginTest:test_collectCredit_toleratesTimeDrift() (gas: 798846) @@ -58,7 +58,7 @@ ExaPluginTest:test_collectInstallments_reverts_whenTimelocked() (gas: 336703) ExaPluginTest:test_collectInstallments_toleratesTimeDrift() (gas: 1161587) ExaPluginTest:test_collect_collects_whenProposalsLeaveNoLiquidity() (gas: 1783636) ExaPluginTest:test_collect_collects_whenTooMuchProposedDebt() (gas: 1923835) -ExaPluginTest:test_crossRepay_avoidsFrozenDeposit() (gas: 980825) +ExaPluginTest:test_crossRepay_avoidsFrozenDeposit() (gas: 981146) ExaPluginTest:test_crossRepay_avoidsZeroSharesDeposit() (gas: 1392815) ExaPluginTest:test_crossRepay_consumesProposal() (gas: 2204416) ExaPluginTest:test_crossRepay_repays() (gas: 2268206) @@ -202,6 +202,14 @@ ExaPreviewerTest:test_collect_reverts_whenProposalsLeaveNoLiquidity() (gas: 1437 ExaPreviewerTest:test_markets_returnsMarkets() (gas: 150447) ExaPreviewerTest:test_pendingProposals_returnsPendingProposals() (gas: 1984093) ExaPreviewerTest:test_utilizations_returns() (gas: 134922) +HypEXATest:test_handle_reverts_withoutBridgeRole() (gas: 136161) +HypEXATest:test_proposeBridgeRole_reverts_whenAlreadyGranted() (gas: 71218) +HypEXATest:test_proposeBridgeRole_reverts_whenRouterNotDeployed() (gas: 22344704) +HypEXATest:test_proposeBridgeRole_schedulesGrantOnTimelock() (gas: 1183670) +HypEXATest:test_roundTrip_opToBaseToOp() (gas: 1055313) +HypEXATest:test_roundTrip_opToPolygonToBaseToOp() (gas: 1562388) +HypEXATest:test_setupRouter_reverts_whenRouterNotDeployed() (gas: 22341787) +HypEXATest:test_transferRemote_reverts_withoutBridgeRole() (gas: 241639) IssuerCheckerTest:test_setIssuer_emits_IssuerSet() (gas: 82935) IssuerCheckerTest:test_setIssuer_reverts_whenNotAdmin() (gas: 43708) IssuerCheckerTest:test_setIssuer_reverts_whenZeroAddress() (gas: 35130) @@ -212,20 +220,20 @@ IssuerCheckerTest:test_setPrevIssuerWindow_emits_PrevIssuerWindowSet() (gas: 526 IssuerCheckerTest:test_setPrevIssuerWindow_reverts_whenNotAdmin() (gas: 45548) MockSwapperTest:test_swapExactAmountIn_swaps() (gas: 269414) MockSwapperTest:test_swapExactAmountOut_swaps() (gas: 269410) -RedeployerTest:test_deployEXA_deploysAtSameAddress_onBase() (gas: 54792391) -RedeployerTest:test_deployExaFactoryWithProxy_reverts_whenNotPrepared() (gas: 15868804) -RedeployerTest:test_deployExaFactoryWithProxy_succeeds_whenAlreadyUpgraded() (gas: 31111328) -RedeployerTest:test_deployExaFactory_deploysAtSameAddress_onEthereum() (gas: 260295770) -RedeployerTest:test_deployExaFactory_deploysAtSameAddress_onPolygon() (gas: 355860463) -RedeployerTest:test_deployExaFactory_deploysViaCreate3AtSameAddress_onPolygon() (gas: 34302845) -RedeployerTest:test_deployExaFactory_reverts_whenNotPrepared() (gas: 15892721) -RedeployerTest:test_deployExaFactory_succeeds_whenVersionAlreadyDeployed() (gas: 31658476) -RedeployerTest:test_prepare_reusesExistingDeps_onBase() (gas: 19385733) -RedeployerTest:test_prepare_reverts_whenAdminIsDeployer() (gas: 15897009) -RedeployerTest:test_prepare_succeeds_whenCalledTwice() (gas: 30601997) -RedeployerTest:test_recoversNativeETHOnPolygon() (gas: 34474337) -RedeployerTest:test_serialProxies_reverts_whenAttackerUpgradesProxy() (gas: 37486962) -RedeployerTest:test_serialProxies_reverts_whenTargetNonceTooLow() (gas: 43616725) +RedeployerTest:test_deployEXA_deploysAtSameAddress_onBase() (gas: 58860868) +RedeployerTest:test_deployExaFactoryWithProxy_reverts_whenNotPrepared() (gas: 19415727) +RedeployerTest:test_deployExaFactoryWithProxy_succeeds_whenAlreadyUpgraded() (gas: 34663902) +RedeployerTest:test_deployExaFactory_deploysAtSameAddress_onEthereum() (gas: 263843561) +RedeployerTest:test_deployExaFactory_deploysAtSameAddress_onPolygon() (gas: 359413573) +RedeployerTest:test_deployExaFactory_deploysViaCreate3AtSameAddress_onPolygon() (gas: 37855415) +RedeployerTest:test_deployExaFactory_reverts_whenNotPrepared() (gas: 19440012) +RedeployerTest:test_deployExaFactory_succeeds_whenVersionAlreadyDeployed() (gas: 35211436) +RedeployerTest:test_prepare_reusesExistingDeps_onBase() (gas: 22940779) +RedeployerTest:test_prepare_reverts_whenAdminIsDeployer() (gas: 19444569) +RedeployerTest:test_prepare_succeeds_whenCalledTwice() (gas: 34154925) +RedeployerTest:test_recoversNativeETHOnPolygon() (gas: 38027039) +RedeployerTest:test_serialProxies_reverts_whenAttackerUpgradesProxy() (gas: 42210375) +RedeployerTest:test_serialProxies_reverts_whenTargetNonceTooLow() (gas: 50708483) RefunderTest:test_refund_refunds() (gas: 263363) RefunderTest:test_refund_reverts_whenExpired() (gas: 88359) RefunderTest:test_refund_reverts_whenNotKeeper() (gas: 68861) diff --git a/contracts/deploy.json b/contracts/deploy.json index b920373dc6..b8a6950b6f 100644 --- a/contracts/deploy.json +++ b/contracts/deploy.json @@ -12,6 +12,10 @@ "11155420": "0xDb90CDB64CfF03f254e4015C4F705C3F3C834400", "default": "0xe61Bdef3FFF4C3CF7A07996DCB8802b5C85B665a" }, + "exactly": { + "10": "0xC0d6Bc5d052d1e74523AD79dD5A954276c9286D3", + "8453": "0x7A65824d74B0C20730B6eE4929ABcc41Cbe843Aa" + }, "keeper": { "31337": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "84532": "0xEbFa3f3306B2F62400D644714A20F6eF32c63C6B", @@ -34,6 +38,11 @@ "11155420": "0xe2D63e18Fb136ef84557f79052BFE8EFa90F69Bd", "default": "0xe61Bdef3FFF4C3CF7A07996DCB8802b5C85B665a" }, + "mailbox": { + "10": "0xd4C1905BB1D26BC93DAC913e13CaCC278CdCC80D", + "137": "0x5d934f4e2f797775e53561bB72aca21ba36B96BB", + "8453": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D" + }, "swapper": { "84532": "0xE4F8C9904a79EBd88Ff73ed3F86a3CFd1b489eDf", "11155420": "0x3E2D4b69C52932CB5b2a9Ee744CB585bb201c771", diff --git a/contracts/package.json b/contracts/package.json index eb3f58e681..6daa496516 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -23,10 +23,11 @@ "webauthn-sol": "base/webauthn-sol#v1.0.0" }, "devDependencies": { - "@exactly/protocol": "^0.2.22", + "@exactly/protocol": "exactly/protocol#5833408", "@openzeppelin/contracts-upgradeable-v4": "npm:@openzeppelin/contracts-upgradeable@^4.9.6", "@openzeppelin/contracts-upgradeable": "^5.6.1", "@openzeppelin/contracts-v4": "npm:@openzeppelin/contracts@^4.9.6", + "@hyperlane-xyz/core": "^11.0.0", "account-abstraction": "eth-infinitism/account-abstraction#v0.6.0", "forge-std": "foundry-rs/forge-std#v1.15.0", "fresh-crypto-lib": "rdubois-crypto/FreshCryptoLib#fd2a0e6e64609ade0b0c165e4d66de8106a70db6", diff --git a/contracts/remappings.txt b/contracts/remappings.txt index 91209330a0..5ffcccff86 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -9,10 +9,12 @@ webauthn-owner-plugin/=node_modules/webauthn-owner-plugin/src webauthn-sol/=node_modules/webauthn-sol/src @exactly/protocol/=node_modules/@exactly/protocol/contracts/ +node_modules/@exactly/protocol/:@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable-v4/ @openzeppelin/contracts-upgradeable-v4/=node_modules/@openzeppelin/contracts-upgradeable-v4/ @openzeppelin/contracts-v4/=node_modules/@openzeppelin/contracts-v4/ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts-v4/ +@hyperlane-xyz/=node_modules/@hyperlane-xyz/ account-abstraction/=node_modules/account-abstraction/contracts/ forge-std/=node_modules/forge-std/src/ solmate/=node_modules/solmate/ diff --git a/contracts/script/Redeployer.s.sol b/contracts/script/Redeployer.s.sol index f830d53c5f..a3c34e632c 100644 --- a/contracts/script/Redeployer.s.sol +++ b/contracts/script/Redeployer.s.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.0; import { TransparentUpgradeableProxy } from "@openzeppelin/contracts-v4/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { IAccessControl } from "openzeppelin-contracts/contracts/access/IAccessControl.sol"; +import { TimelockController } from "openzeppelin-contracts/contracts/governance/TimelockController.sol"; import { ERC1967Utils } from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol"; import { ProxyAdmin } from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import { @@ -16,6 +18,9 @@ import { ACCOUNT_IMPL, ENTRYPOINT } from "webauthn-owner-plugin/../script/Factor 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 { IAuditor, @@ -191,9 +196,59 @@ contract Redeployer is BaseScript { vm.startBroadcast(acct("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, (acct("exactly"))) + ); + 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"))); + } + + function deployRouter(address token) external returns (HypXERC20 router) { + address admin = acct("admin"); + router = HypXERC20(CREATE3_FACTORY.getDeployed(admin, keccak256(abi.encode("HypEXA")))); + if (address(router).code.length != 0) return router; + vm.startBroadcast(admin); + router = HypXERC20( + CREATE3_FACTORY.deploy( + keccak256(abi.encode("HypEXA")), + abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode( + address(new HypXERC20(token, 1, 1, acct("mailbox"))), + protocol("ProxyAdmin"), + abi.encodeCall(HypERC20Collateral.initialize, (address(0), address(0), admin)) + ) + ) + ) + ); + router.transferOwnership(acct("exactly")); vm.stopBroadcast(); } + function setupRouter(uint32 remoteDomain) external { + address router = CREATE3_FACTORY.getDeployed(acct("admin"), keccak256(abi.encode("HypEXA"))); + if (router.code.length == 0) revert RouterNotDeployed(); + vm.broadcast(acct("exactly")); + HypXERC20(router).enrollRemoteRouter(remoteDomain, bytes32(uint256(uint160(router)))); + } + + function proposeBridgeRole(address token, bytes32 salt) external { + address router = CREATE3_FACTORY.getDeployed(acct("admin"), keccak256(abi.encode("HypEXA"))); + if (router.code.length == 0) revert RouterNotDeployed(); + if (IAccessControl(token).hasRole(keccak256("BRIDGE_ROLE"), router)) revert AlreadyGranted(); + TimelockController timelock = TimelockController(payable(protocol("TimelockController"))); + uint256 delay = timelock.getMinDelay(); + vm.broadcast(acct("deployer")); + timelock.schedule( + token, 0, abi.encodeCall(IAccessControl.grantRole, (keccak256("BRIDGE_ROLE"), router)), bytes32(0), salt, delay + ); + } + /// @notice Upgrades a proxy to the cached ExaAccountFactory implementation. function deployExaFactory(address proxy) external { if (address(factory).code.length == 0) revert NotPrepared(); @@ -281,10 +336,12 @@ contract Redeployer is BaseScript { } error AdminIsDeployer(); +error AlreadyGranted(); error DummyNotDeployed(); error NonceNotFound(); error NotPrepared(); error ProxyAdminNotDeployed(); +error RouterNotDeployed(); error TargetNonceTooLow(); contract Dummy { } // solhint-disable-line no-empty-blocks diff --git a/contracts/test/HypEXA.t.sol b/contracts/test/HypEXA.t.sol new file mode 100644 index 0000000000..6dccf81a42 --- /dev/null +++ b/contracts/test/HypEXA.t.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import { IAccessControl } from "openzeppelin-contracts/contracts/access/IAccessControl.sol"; +import { TimelockController } from "openzeppelin-contracts/contracts/governance/TimelockController.sol"; +import { ERC1967Utils } from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import { ProxyAdmin } from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; +import { + ITransparentUpgradeableProxy +} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +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 { AlreadyGranted, Redeployer, RouterNotDeployed } 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; + Redeployer internal opRedeployer; + 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.proxyThrough(polygonRedeployer.findNonce(acct("deployer"), address(exa), 1000) + 1); + set("exactly", makeAddr("exactly")); // no exactly on polygon — test-only chain + set("ProxyAdmin", address(polygonRedeployer.proxyAdmin())); // no protocol deployment on polygon + polygonRedeployer.deployEXA(address(exa)); + polygonRouter = polygonRedeployer.deployRouter(address(exa)); + unset("ProxyAdmin"); + vm.prank(makeAddr("exactly")); + exa.grantRole(keccak256("BRIDGE_ROLE"), address(polygonRouter)); + polygonRedeployer.setupRouter(OP_DOMAIN); + polygonRedeployer.setupRouter(BASE_DOMAIN); + unset("exactly"); + + 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.proxyThrough(baseRedeployer.findNonce(acct("deployer"), address(exa), 1000) + 1); + baseRedeployer.deployEXA(address(exa)); + baseRouter = baseRedeployer.deployRouter(address(exa)); + vm.prank(acct("exactly")); + exa.grantRole(keccak256("BRIDGE_ROLE"), address(baseRouter)); + baseRedeployer.setupRouter(OP_DOMAIN); + baseRedeployer.setupRouter(POLYGON_DOMAIN); + + opFork = vm.createSelectFork("optimism", 147_967_000); + opMailbox = acct("mailbox"); + opRedeployer = new Redeployer(); + opRedeployer.setUp(); + opRedeployer.prepare(); + opRedeployer.deployEXAImpl(); + _upgradeEXA(address(exa), address(opRedeployer.exa())); + opRouter = opRedeployer.deployRouter(address(exa)); + vm.prank(acct("exactly")); + exa.grantRole(keccak256("BRIDGE_ROLE"), address(opRouter)); + opRedeployer.setupRouter(BASE_DOMAIN); + opRedeployer.setupRouter(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(acct("exactly")); + 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); + } + + function test_handle_reverts_withoutBridgeRole() external { + vm.selectFork(baseFork); + vm.prank(acct("exactly")); + 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)) + ); + } + + function test_setupRouter_reverts_whenRouterNotDeployed() external { + vm.createSelectFork("base", 42_380_001); + + Redeployer redeployer = new Redeployer(); + redeployer.setUp(); + + vm.expectRevert(RouterNotDeployed.selector); + redeployer.setupRouter(OP_DOMAIN); + } + + function test_proposeBridgeRole_reverts_whenRouterNotDeployed() external { + vm.createSelectFork("base", 42_380_001); + + Redeployer redeployer = new Redeployer(); + redeployer.setUp(); + + vm.expectRevert(RouterNotDeployed.selector); + redeployer.proposeBridgeRole(address(exa), keccak256("HypEXA.BRIDGE_ROLE")); + } + + function test_proposeBridgeRole_schedulesGrantOnTimelock() external { + vm.selectFork(opFork); + vm.prank(acct("exactly")); + exa.revokeRole(keccak256("BRIDGE_ROLE"), address(opRouter)); + + bytes32 salt = keccak256("HypEXA.BRIDGE_ROLE"); + opRedeployer.proposeBridgeRole(address(exa), salt); + + TimelockController timelock = TimelockController(payable(protocol("TimelockController"))); + bytes32 id = timelock.hashOperation( + address(exa), + 0, + abi.encodeCall(IAccessControl.grantRole, (keccak256("BRIDGE_ROLE"), address(opRouter))), + bytes32(0), + salt + ); + assertTrue(timelock.isOperationPending(id), "grant not scheduled"); + } + + function test_proposeBridgeRole_reverts_whenAlreadyGranted() external { + vm.selectFork(opFork); + vm.expectRevert(AlreadyGranted.selector); + opRedeployer.proposeBridgeRole(address(exa), keccak256("HypEXA.BRIDGE_ROLE")); + } + + // solhint-enable func-name-mixedcase + + function _upgradeEXA(address proxy, address implementation) internal { + ProxyAdmin p = ProxyAdmin(address(uint160(uint256(vm.load(proxy, ERC1967Utils.ADMIN_SLOT))))); + vm.prank(p.owner()); + p.upgradeAndCall( + ITransparentUpgradeableProxy(proxy), implementation, abi.encodeCall(EXA.initialize2, (acct("exactly"))) + ); + } +} diff --git a/contracts/test/Redeployer.t.sol b/contracts/test/Redeployer.t.sol index 6a1a1c1d1b..9b3aced4f0 100644 --- a/contracts/test/Redeployer.t.sol +++ b/contracts/test/Redeployer.t.sol @@ -46,8 +46,7 @@ contract RedeployerTest is ForkTest { EXA token = EXA(exaOP); assertEq(token.name(), "exactly"); assertEq(token.symbol(), "EXA"); - assertEq(token.totalSupply(), 10_000_000e18, "token should have same minted supply"); - assertEq(token.balanceOf(address(redeployer.proxyAdmin())), 10_000_000e18, "ProxyAdmin should have tokens"); + assertEq(token.totalSupply(), 0, "base should have zero supply"); assertEq(token.decimals(), 18, "token should have 18 decimals"); } diff --git a/cspell.json b/cspell.json index 54e7eee97a..4469467630 100644 --- a/cspell.json +++ b/cspell.json @@ -79,6 +79,7 @@ "hexlify", "hideable", "hono", + "hyperlane", "IBAN", "IERC", "indoc", @@ -190,6 +191,7 @@ "worklet", "worklets", "xcrun", + "XERC", "xhdpi", "xxhdpi", "xxxhdpi", diff --git a/package.json b/package.json index ed17636189..fe86c6d0a7 100644 --- a/package.json +++ b/package.json @@ -249,7 +249,7 @@ "typescript" ] }, - "neverBuiltDependencies": [], + "onlyBuiltDependencies": ["@exactly/protocol"], "peerDependencyRules": { "allowedVersions": { "@hono/mcp>hono-rate-limiter": "0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 442a3d2b4a..dd9b1a599b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -657,8 +657,11 @@ importers: version: https://codeload.github.com/base/webauthn-sol/tar.gz/619f20ab0f074fef41066ee4ab24849a913263b2 devDependencies: '@exactly/protocol': - specifier: ^0.2.22 - version: 0.2.22 + specifier: exactly/protocol#5833408 + version: https://codeload.github.com/exactly/protocol/tar.gz/5833408b2a8582f113adc07bbb302de6fdc5f7ff + '@hyperlane-xyz/core': + specifier: ^11.0.0 + version: 11.3.1(@ethersproject/abi@5.8.0)(@ethersproject/providers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@openzeppelin/contracts-upgradeable': specifier: ^5.6.1 version: 5.6.1(@openzeppelin/contracts@5.6.1) @@ -2848,6 +2851,11 @@ packages: resolution: {integrity: sha512-QqQTWoWSc+pZ830YpQ0KtAcagDuC65Ez2dDZBhkHy8Hks2VmHih5QjXnvHXS42Bytp+2p6wkPAXfkUewfsFPdQ==} engines: {node: '>=18'} + '@exactly/protocol@https://codeload.github.com/exactly/protocol/tar.gz/5833408b2a8582f113adc07bbb302de6fdc5f7ff': + resolution: {tarball: https://codeload.github.com/exactly/protocol/tar.gz/5833408b2a8582f113adc07bbb302de6fdc5f7ff} + version: 0.2.22 + engines: {node: '>=18'} + '@expo-google-fonts/material-symbols@0.4.27': resolution: {integrity: sha512-cnb3DZnWUWpezGFkJ8y4MT5f/lw6FcgDzeJzic+T+vpQHLHG1cg3SC3i1w1i8Bk4xKR4HPY3t9iIRNvtr5ml8A==} @@ -3163,6 +3171,14 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@hyperlane-xyz/core@11.3.1': + resolution: {integrity: sha512-PX/qoJnPXXnIkE/16o/OPXxbgyIV6RHWYlpuh4QIB6uMvJUK4lbNxSQnfo9bVlH7eKbaD3NL2ENV28053ig/SQ==} + engines: {node: '>=16'} + peerDependencies: + '@ethersproject/abi': '*' + '@ethersproject/providers': '*' + '@types/sinon-chai': '*' + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -15726,7 +15742,6 @@ snapshots: '@ethersproject/logger': 5.8.0 '@ethersproject/properties': 5.8.0 '@ethersproject/strings': 5.8.0 - optional: true '@ethersproject/abstract-provider@5.8.0': dependencies: @@ -15737,7 +15752,6 @@ snapshots: '@ethersproject/properties': 5.8.0 '@ethersproject/transactions': 5.8.0 '@ethersproject/web': 5.8.0 - optional: true '@ethersproject/abstract-signer@5.8.0': dependencies: @@ -15746,7 +15760,6 @@ snapshots: '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.8.0 '@ethersproject/properties': 5.8.0 - optional: true '@ethersproject/address@5.8.0': dependencies: @@ -15755,35 +15768,29 @@ snapshots: '@ethersproject/keccak256': 5.8.0 '@ethersproject/logger': 5.8.0 '@ethersproject/rlp': 5.8.0 - optional: true '@ethersproject/base64@5.8.0': dependencies: '@ethersproject/bytes': 5.8.0 - optional: true '@ethersproject/basex@5.8.0': dependencies: '@ethersproject/bytes': 5.8.0 '@ethersproject/properties': 5.8.0 - optional: true '@ethersproject/bignumber@5.8.0': dependencies: '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.8.0 bn.js: 5.2.3 - optional: true '@ethersproject/bytes@5.8.0': dependencies: '@ethersproject/logger': 5.8.0 - optional: true '@ethersproject/constants@5.8.0': dependencies: '@ethersproject/bignumber': 5.8.0 - optional: true '@ethersproject/contracts@5.8.0': dependencies: @@ -15810,7 +15817,6 @@ snapshots: '@ethersproject/logger': 5.8.0 '@ethersproject/properties': 5.8.0 '@ethersproject/strings': 5.8.0 - optional: true '@ethersproject/hdnode@5.8.0': dependencies: @@ -15849,15 +15855,12 @@ snapshots: dependencies: '@ethersproject/bytes': 5.8.0 js-sha3: 0.8.0 - optional: true - '@ethersproject/logger@5.8.0': - optional: true + '@ethersproject/logger@5.8.0': {} '@ethersproject/networks@5.8.0': dependencies: '@ethersproject/logger': 5.8.0 - optional: true '@ethersproject/pbkdf2@5.8.0': dependencies: @@ -15868,7 +15871,6 @@ snapshots: '@ethersproject/properties@5.8.0': dependencies: '@ethersproject/logger': 5.8.0 - optional: true '@ethersproject/providers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: @@ -15895,26 +15897,22 @@ snapshots: transitivePeerDependencies: - bufferutil - utf-8-validate - optional: true '@ethersproject/random@5.8.0': dependencies: '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.8.0 - optional: true '@ethersproject/rlp@5.8.0': dependencies: '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.8.0 - optional: true '@ethersproject/sha2@5.8.0': dependencies: '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.8.0 hash.js: 1.1.7 - optional: true '@ethersproject/signing-key@5.8.0': dependencies: @@ -15924,14 +15922,12 @@ snapshots: bn.js: 5.2.3 elliptic: 6.6.1 hash.js: 1.1.7 - optional: true '@ethersproject/strings@5.8.0': dependencies: '@ethersproject/bytes': 5.8.0 '@ethersproject/constants': 5.8.0 '@ethersproject/logger': 5.8.0 - optional: true '@ethersproject/transactions@5.8.0': dependencies: @@ -15944,7 +15940,6 @@ snapshots: '@ethersproject/properties': 5.8.0 '@ethersproject/rlp': 5.8.0 '@ethersproject/signing-key': 5.8.0 - optional: true '@ethersproject/units@5.8.0': dependencies: @@ -15979,7 +15974,6 @@ snapshots: '@ethersproject/logger': 5.8.0 '@ethersproject/properties': 5.8.0 '@ethersproject/strings': 5.8.0 - optional: true '@ethersproject/wordlists@5.8.0': dependencies: @@ -16001,6 +15995,15 @@ snapshots: solady: 0.1.26 solmate: '@rari-capital/solmate@https://codeload.github.com/transmissions11/solmate/tar.gz/eaa7041378f9a6c12f943de08a6c41b31a9870fc' + '@exactly/protocol@https://codeload.github.com/exactly/protocol/tar.gz/5833408b2a8582f113adc07bbb302de6fdc5f7ff': + dependencies: + '@openzeppelin/contracts': 5.6.1 + '@openzeppelin/contracts-upgradeable': 5.6.1(@openzeppelin/contracts@5.6.1) + '@openzeppelin/contracts-upgradeable-v4': '@openzeppelin/contracts-upgradeable@4.9.6' + '@openzeppelin/contracts-v4': '@openzeppelin/contracts@4.9.6' + solady: 0.1.26 + solmate: '@rari-capital/solmate@https://codeload.github.com/transmissions11/solmate/tar.gz/eaa7041378f9a6c12f943de08a6c41b31a9870fc' + '@expo-google-fonts/material-symbols@0.4.27': {} '@expo/cli@55.0.19(@expo/metro-runtime@55.0.7)(bufferutil@4.1.0)(expo-constants@55.0.9)(expo-font@55.0.4)(expo-router@55.0.8)(expo@55.0.9)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': @@ -16516,6 +16519,11 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@hyperlane-xyz/core@11.3.1(@ethersproject/abi@5.8.0)(@ethersproject/providers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + dependencies: + '@ethersproject/abi': 5.8.0 + '@ethersproject/providers': 5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@iconify/types@2.0.0': {} '@iconify/utils@3.1.0': @@ -18380,7 +18388,7 @@ snapshots: '@scure/bip32@1.7.0': dependencies: - '@noble/curves': 1.9.1 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 @@ -21443,11 +21451,9 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - bn.js@4.12.3: - optional: true + bn.js@4.12.3: {} - bn.js@5.2.3: - optional: true + bn.js@5.2.3: {} body-parser@2.2.2: dependencies: @@ -21494,8 +21500,7 @@ snapshots: dependencies: fill-range: 7.1.1 - brorand@1.1.0: - optional: true + brorand@1.1.0: {} brotli@1.3.3: dependencies: @@ -22503,7 +22508,6 @@ snapshots: inherits: 2.0.4 minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 - optional: true embedded-postgres@18.3.0-beta.16(patch_hash=cb5e37525b1810f2af136570b38d5e0cec4cc2455408896ed1943d27f3f61b38): dependencies: @@ -24357,7 +24361,6 @@ snapshots: dependencies: inherits: 2.0.4 minimalistic-assert: 1.0.1 - optional: true hasown@2.0.2: dependencies: @@ -24585,7 +24588,6 @@ snapshots: hash.js: 1.1.7 minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 - optional: true hoist-non-react-statics@3.3.2: dependencies: @@ -25169,8 +25171,7 @@ snapshots: js-cookie@3.0.1: {} - js-sha3@0.8.0: - optional: true + js-sha3@0.8.0: {} js-tokens@10.0.0: {} @@ -26654,11 +26655,9 @@ snapshots: mimic-response@4.0.0: {} - minimalistic-assert@1.0.1: - optional: true + minimalistic-assert@1.0.1: {} - minimalistic-crypto-utils@1.0.1: - optional: true + minimalistic-crypto-utils@1.0.1: {} minimatch@10.2.4: dependencies: @@ -29773,7 +29772,6 @@ snapshots: optionalDependencies: bufferutil: 4.1.0 utf-8-validate: 5.0.10 - optional: true ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: