diff --git a/solidity/contracts/hooks/ArbL2ToL1Hook.sol b/solidity/contracts/hooks/ArbL2ToL1Hook.sol new file mode 100644 index 0000000000..488dabc1ee --- /dev/null +++ b/solidity/contracts/hooks/ArbL2ToL1Hook.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ +import {AbstractPostDispatchHook} from "./libs/AbstractMessageIdAuthHook.sol"; +import {AbstractMessageIdAuthHook} from "./libs/AbstractMessageIdAuthHook.sol"; +import {Mailbox} from "../Mailbox.sol"; +import {StandardHookMetadata} from "./libs/StandardHookMetadata.sol"; +import {Message} from "../libs/Message.sol"; +import {TypeCasts} from "../libs/TypeCasts.sol"; +import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; +import {MailboxClient} from "../client/MailboxClient.sol"; + +// ============ External Imports ============ +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {ArbSys} from "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; + +/** + * @title ArbL2ToL1Hook + * @notice Message hook to inform the ArbL2ToL1iSM of messages published through + * the native Arbitrum bridge. + * @notice This works only for L2 -> L1 messages and has the 7 day delay as specified by the ArbSys contract. + */ +contract ArbL2ToL1Hook is AbstractMessageIdAuthHook { + using StandardHookMetadata for bytes; + + // ============ Constants ============ + + // precompile contract on L2 for sending messages to L1 + ArbSys public immutable arbSys; + // Immutable quote amount + uint256 public immutable GAS_QUOTE; + + // ============ Constructor ============ + + constructor( + address _mailbox, + uint32 _destinationDomain, + bytes32 _ism, + address _arbSys, + uint256 _gasQuote + ) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) { + arbSys = ArbSys(_arbSys); + GAS_QUOTE = _gasQuote; + } + + function hookType() external pure override returns (uint8) { + return uint8(IPostDispatchHook.Types.ARB_L2_TO_L1); + } + + function _quoteDispatch( + bytes calldata, + bytes calldata + ) internal view override returns (uint256) { + return GAS_QUOTE; + } + + // ============ Internal functions ============ + + /// @inheritdoc AbstractMessageIdAuthHook + function _sendMessageId( + bytes calldata metadata, + bytes memory payload + ) internal override { + arbSys.sendTxToL1{value: metadata.msgValue(0)}( + TypeCasts.bytes32ToAddress(ism), + payload + ); + } +} diff --git a/solidity/contracts/hooks/libs/AbstractMessageIdAuthHook.sol b/solidity/contracts/hooks/libs/AbstractMessageIdAuthHook.sol index cd0f665838..a9e3a41e94 100644 --- a/solidity/contracts/hooks/libs/AbstractMessageIdAuthHook.sol +++ b/solidity/contracts/hooks/libs/AbstractMessageIdAuthHook.sol @@ -58,7 +58,7 @@ abstract contract AbstractMessageIdAuthHook is } /// @inheritdoc IPostDispatchHook - function hookType() external pure returns (uint8) { + function hookType() external pure virtual returns (uint8) { return uint8(IPostDispatchHook.Types.ID_AUTH_ISM); } diff --git a/solidity/contracts/hooks/warp-route/RateLimitedHook.sol b/solidity/contracts/hooks/warp-route/RateLimitedHook.sol index 0937f841be..dd46577502 100644 --- a/solidity/contracts/hooks/warp-route/RateLimitedHook.sol +++ b/solidity/contracts/hooks/warp-route/RateLimitedHook.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; + import {MailboxClient} from "contracts/client/MailboxClient.sol"; import {IPostDispatchHook} from "contracts/interfaces/hooks/IPostDispatchHook.sol"; import {Message} from "contracts/libs/Message.sol"; @@ -26,7 +27,7 @@ contract RateLimitedHook is IPostDispatchHook, MailboxClient, RateLimited { /// @inheritdoc IPostDispatchHook function hookType() external pure returns (uint8) { - return uint8(IPostDispatchHook.Types.Rate_Limited_Hook); + return uint8(IPostDispatchHook.Types.RATE_LIMITED); } /// @inheritdoc IPostDispatchHook diff --git a/solidity/contracts/interfaces/IInterchainSecurityModule.sol b/solidity/contracts/interfaces/IInterchainSecurityModule.sol index d4e6c30c61..d98327e745 100644 --- a/solidity/contracts/interfaces/IInterchainSecurityModule.sol +++ b/solidity/contracts/interfaces/IInterchainSecurityModule.sol @@ -10,7 +10,8 @@ interface IInterchainSecurityModule { MERKLE_ROOT_MULTISIG, MESSAGE_ID_MULTISIG, NULL, // used with relayer carrying no metadata - CCIP_READ + CCIP_READ, + ARB_L2_TO_L1 } /** diff --git a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol index 0efa08ab39..a74951b344 100644 --- a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol +++ b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol @@ -25,7 +25,8 @@ interface IPostDispatchHook { PAUSABLE, PROTOCOL_FEE, LAYER_ZERO_V1, - Rate_Limited_Hook + RATE_LIMITED, + ARB_L2_TO_L1 } /** diff --git a/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol b/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol index 4aa0b9bc92..a8b8baee6c 100644 --- a/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol +++ b/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol @@ -72,26 +72,41 @@ abstract contract AbstractMessageIdAuthorizedIsm is */ function verify( bytes calldata, - /*_metadata*/ + /*metadata*/ bytes calldata message - ) external returns (bool) { - bytes32 messageId = message.id(); + ) external virtual returns (bool) { + bool verified = isVerified(message); + if (verified) { + releaseValueToRecipient(message); + } + return verified; + } - // check for the first bit (used for verification) - bool verified = verifiedMessages[messageId].isBitSet( + // ============ Public Functions ============ + + /** + * @notice Release the value to the recipient if the message is verified. + * @param message Message to release value for. + */ + function releaseValueToRecipient(bytes calldata message) public { + bytes32 messageId = message.id(); + uint256 _msgValue = verifiedMessages[messageId].clearBit( VERIFIED_MASK_INDEX ); - // rest 255 bits contains the msg.value passed from the hook - if (verified) { - uint256 _msgValue = verifiedMessages[messageId].clearBit( - VERIFIED_MASK_INDEX - ); - if (_msgValue > 0) { - verifiedMessages[messageId] -= _msgValue; - payable(message.recipientAddress()).sendValue(_msgValue); - } + if (_msgValue > 0) { + verifiedMessages[messageId] -= _msgValue; + payable(message.recipientAddress()).sendValue(_msgValue); } - return verified; + } + + /** + * @notice Check if a message is verified through verifyMessageId first. + * @param message Message to check. + */ + function isVerified(bytes calldata message) public view returns (bool) { + bytes32 messageId = message.id(); + // check for the first bit (used for verification) + return verifiedMessages[messageId].isBitSet(VERIFIED_MASK_INDEX); } /** @@ -113,5 +128,10 @@ abstract contract AbstractMessageIdAuthorizedIsm is emit ReceivedMessage(messageId); } + // ============ Internal Functions ============ + + /** + * @notice Check if sender is authorized to message `verifyMessageId`. + */ function _isAuthorized() internal view virtual returns (bool); } diff --git a/solidity/contracts/isms/hook/ArbL2ToL1Ism.sol b/solidity/contracts/isms/hook/ArbL2ToL1Ism.sol new file mode 100644 index 0000000000..baba9e7bf6 --- /dev/null +++ b/solidity/contracts/isms/hook/ArbL2ToL1Ism.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ + +import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; +import {TypeCasts} from "../../libs/TypeCasts.sol"; +import {Message} from "../../libs/Message.sol"; +import {AbstractMessageIdAuthorizedIsm} from "./AbstractMessageIdAuthorizedIsm.sol"; + +// ============ External Imports ============ + +import {IOutbox} from "@arbitrum/nitro-contracts/src/bridge/IOutbox.sol"; +import {CrossChainEnabledArbitrumL1} from "@openzeppelin/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title ArbL2ToL1Ism + * @notice Uses the native Arbitrum bridge to verify interchain messages from L2 to L1. + */ +contract ArbL2ToL1Ism is + CrossChainEnabledArbitrumL1, + AbstractMessageIdAuthorizedIsm +{ + using Message for bytes; + // ============ Constants ============ + + // module type for the ISM + uint8 public constant moduleType = + uint8(IInterchainSecurityModule.Types.ARB_L2_TO_L1); + // arbitrum nitro contract on L1 to forward verification + IOutbox public arbOutbox; + + // ============ Constructor ============ + + constructor( + address _bridge, + address _outbox + ) CrossChainEnabledArbitrumL1(_bridge) { + require( + Address.isContract(_bridge), + "ArbL2ToL1Ism: invalid Arbitrum Bridge" + ); + arbOutbox = IOutbox(_outbox); + } + + // ============ External Functions ============ + + /// @inheritdoc IInterchainSecurityModule + function verify( + bytes calldata metadata, + bytes calldata message + ) external override returns (bool) { + bool verified = isVerified(message); + if (verified) { + releaseValueToRecipient(message); + } + return verified || _verifyWithOutboxCall(metadata, message); + } + + // ============ Internal function ============ + + /** + * @notice Verify message directly using the arbOutbox.executeTransaction function. + * @dev This is a fallback in case the message is not verified by the stateful verify function first. + * @dev This function doesn't support msg.value as the ism.verify call doesn't support it either. + */ + function _verifyWithOutboxCall( + bytes calldata metadata, + bytes calldata message + ) internal returns (bool) { + ( + bytes32[] memory proof, + uint256 index, + address l2Sender, + address to, + uint256 l2Block, + uint256 l1Block, + uint256 l2Timestamp, + bytes memory data + ) = abi.decode( + metadata, + ( + bytes32[], + uint256, + address, + address, + uint256, + uint256, + uint256, + bytes + ) + ); + + // check if the sender of the l2 message is the authorized hook + require( + l2Sender == TypeCasts.bytes32ToAddress(authorizedHook), + "ArbL2ToL1Ism: l2Sender != authorizedHook" + ); + // this data is an abi encoded call of verifyMessageId(bytes32 messageId) + require(data.length == 36, "ArbL2ToL1Ism: invalid data length"); + bytes32 messageId = message.id(); + bytes32 convertedBytes; + assembly { + // data = 0x[4 bytes function signature][32 bytes messageId] + convertedBytes := mload(add(data, 36)) + } + // check if the parsed message id matches the message id of the message + require( + convertedBytes == messageId, + "ArbL2ToL1Ism: invalid message id" + ); + + // value send to 0 + arbOutbox.executeTransaction( + proof, + index, + l2Sender, + to, + l2Block, + l1Block, + l2Timestamp, + 0, + data + ); + // the above bridge call will revert if the verifyMessageId call fails + return true; + } + + /// @inheritdoc AbstractMessageIdAuthorizedIsm + function _isAuthorized() internal view override returns (bool) { + return + _crossChainSender() == TypeCasts.bytes32ToAddress(authorizedHook); + } +} diff --git a/solidity/contracts/test/TestRecipient.sol b/solidity/contracts/test/TestRecipient.sol index 3b3b3e3afb..1d4020dd6f 100644 --- a/solidity/contracts/test/TestRecipient.sol +++ b/solidity/contracts/test/TestRecipient.sol @@ -46,4 +46,6 @@ contract TestRecipient is function setInterchainSecurityModule(address _ism) external onlyOwner { interchainSecurityModule = IInterchainSecurityModule(_ism); } + + receive() external payable {} } diff --git a/solidity/package.json b/solidity/package.json index 347dcc6631..cdbe62fb88 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -3,6 +3,7 @@ "description": "Core solidity contracts for Hyperlane", "version": "4.0.0", "dependencies": { + "@arbitrum/nitro-contracts": "^1.2.1", "@eth-optimism/contracts": "^0.6.0", "@hyperlane-xyz/utils": "4.0.0", "@layerzerolabs/lz-evm-oapp-v2": "2.0.2", diff --git a/solidity/remappings.txt b/solidity/remappings.txt index e85474338a..49ae22c339 100644 --- a/solidity/remappings.txt +++ b/solidity/remappings.txt @@ -1,6 +1,7 @@ -@openzeppelin=../node_modules/@openzeppelin -@layerzerolabs=../node_modules/@layerzerolabs +@arbitrum=../node_modules/@arbitrum @eth-optimism=../node_modules/@eth-optimism +@layerzerolabs=../node_modules/@layerzerolabs +@openzeppelin=../node_modules/@openzeppelin ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ -fx-portal/=lib/fx-portal/ \ No newline at end of file +fx-portal/=lib/fx-portal/ diff --git a/solidity/script/DeployArbHook.s.sol b/solidity/script/DeployArbHook.s.sol new file mode 100644 index 0000000000..ad9f42d77a --- /dev/null +++ b/solidity/script/DeployArbHook.s.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import "forge-std/Script.sol"; + +import {Mailbox} from "../../contracts/Mailbox.sol"; +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; +import {ArbL2ToL1Hook} from "../../contracts/hooks/ArbL2ToL1Hook.sol"; +import {ArbL2ToL1Ism} from "../../contracts/isms/hook/ArbL2ToL1Ism.sol"; +import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; +import {TestIsm} from "../../contracts/test/TestIsm.sol"; + +contract DeployArbHook is Script { + uint256 deployerPrivateKey; + + ArbL2ToL1Hook hook; + ArbL2ToL1Ism ism; + + uint32 constant L1_DOMAIN = 11155111; + address constant L1_MAILBOX = 0xfFAEF09B3cd11D9b20d1a19bECca54EEC2884766; + address constant L1_BRIDGE = 0x38f918D0E9F1b721EDaA41302E399fa1B79333a9; + address constant L1_OUTBOX = 0x65f07C7D521164a4d5DaC6eB8Fac8DA067A3B78F; + address constant L1_ISM = 0x096A1c034c7Ad113B6dB786b7BA852cB67025458; // placeholder + bytes32 TEST_RECIPIENT = + 0x000000000000000000000000155b1cd2f7cbc58d403b9be341fab6cd77425175; // placeholder + + address constant ARBSYS = 0x0000000000000000000000000000000000000064; + address constant L2_MAILBOX = 0x598facE78a4302f11E3de0bee1894Da0b2Cb71F8; + address constant L2_HOOK = 0xd9d99AC1C645563576b8Df22cBebFC23FB60Ec73; // placeholder + + function deployIsm() external { + deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + + vm.startBroadcast(deployerPrivateKey); + + ism = new ArbL2ToL1Ism(L1_BRIDGE, L1_OUTBOX); + + TestRecipient testRecipient = new TestRecipient(); + testRecipient.setInterchainSecurityModule(address(ism)); + + vm.stopBroadcast(); + } + + function deployHook() external { + deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + + vm.startBroadcast(deployerPrivateKey); + + hook = new ArbL2ToL1Hook( + L2_MAILBOX, + L1_DOMAIN, + TypeCasts.addressToBytes32(L1_ISM), + ARBSYS, + 200_000 // estimated gas amount used for verify + ); + + vm.stopBroadcast(); + } + + function deployTestRecipient() external { + deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + + vm.startBroadcast(deployerPrivateKey); + + TestIsm noopIsm = new TestIsm(); + noopIsm.setVerify(true); + TestRecipient testRecipient = new TestRecipient(); + testRecipient.setInterchainSecurityModule(address(noopIsm)); + + console.log("TestRecipient address: %s", address(testRecipient)); + + vm.stopBroadcast(); + } + + function setAuthorizedHook() external { + deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + + vm.startBroadcast(deployerPrivateKey); + + ism = ArbL2ToL1Ism(L1_ISM); + ism.setAuthorizedHook(TypeCasts.addressToBytes32(L2_HOOK)); + + vm.stopBroadcast(); + } +} diff --git a/solidity/test/isms/ArbL2ToL1Ism.t.sol b/solidity/test/isms/ArbL2ToL1Ism.t.sol new file mode 100644 index 0000000000..40d77bff6b --- /dev/null +++ b/solidity/test/isms/ArbL2ToL1Ism.t.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: MIT or Apache-2.0 +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; + +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; +import {MessageUtils} from "./IsmTestUtils.sol"; +import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; +import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; +import {Message} from "../../contracts/libs/Message.sol"; +import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; +import {ArbL2ToL1Hook} from "../../contracts/hooks/ArbL2ToL1Hook.sol"; +import {ArbL2ToL1Ism} from "../../contracts/isms/hook/ArbL2ToL1Ism.sol"; +import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; + +contract MockArbBridge { + error BridgeCallFailed(); + + address public activeOutbox; + address public l2ToL1Sender; + + constructor() { + activeOutbox = address(this); + } + + function setL2ToL1Sender(address _sender) external { + l2ToL1Sender = _sender; + } + + function executeTransaction( + bytes32[] calldata /*proof*/, + uint256 /*index*/, + address /*l2Sender*/, + address to, + uint256 /*l2Block*/, + uint256 /*l1Block*/, + uint256 /*timestamp*/, + uint256 value, + bytes calldata data + ) external payable { + (bool success, bytes memory returndata) = to.call{value: value}(data); + if (!success) { + if (returndata.length > 0) { + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert BridgeCallFailed(); + } + } + } +} + +contract MockArbSys { + function sendTxToL1( + address destination, + bytes calldata data + ) external payable returns (uint256) {} +} + +contract ArbL2ToL1IsmTest is Test { + uint8 internal constant HYPERLANE_VERSION = 1; + uint32 internal constant MAINNET_DOMAIN = 1; + uint32 internal constant ARBITRUM_DOMAIN = 42161; + uint256 internal constant GAS_QUOTE = 120_000; + + uint256 internal constant MOCK_LEAF_INDEX = 40160; + uint256 internal constant MOCK_L2_BLOCK = 54220000; + uint256 internal constant MOCK_L1_BLOCK = 6098300; + + address internal constant L2_ARBSYS_ADDRESS = + 0x0000000000000000000000000000000000000064; + + MockArbBridge internal arbBridge; + TestMailbox public l2Mailbox; + ArbL2ToL1Hook public hook; + ArbL2ToL1Ism public ism; + + TestRecipient internal testRecipient; + bytes internal testMessage = + abi.encodePacked("Hello from the other chain!"); + bytes internal encodedMessage; + bytes internal testMetadata = + StandardHookMetadata.overrideRefundAddress(address(this)); + bytes32 internal messageId; + + function setUp() public { + // Arbitrum bridge mock setup + vm.etch(L2_ARBSYS_ADDRESS, address(new MockArbSys()).code); + + testRecipient = new TestRecipient(); + + encodedMessage = _encodeTestMessage(); + messageId = Message.id(encodedMessage); + } + + /////////////////////////////////////////////////////////////////// + /// SETUP /// + /////////////////////////////////////////////////////////////////// + + function deployHook() public { + l2Mailbox = new TestMailbox(ARBITRUM_DOMAIN); + hook = new ArbL2ToL1Hook( + address(l2Mailbox), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(ism)), + L2_ARBSYS_ADDRESS, + GAS_QUOTE + ); + } + + function deployIsm() public { + arbBridge = new MockArbBridge(); + + ism = new ArbL2ToL1Ism(address(arbBridge), address(arbBridge)); + } + + function deployAll() public { + deployIsm(); + deployHook(); + + ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook))); + } + + function test_postDispatch() public { + deployAll(); + + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) + ); + + l2Mailbox.updateLatestDispatchedId(messageId); + + vm.expectCall( + L2_ARBSYS_ADDRESS, + abi.encodeCall( + MockArbSys.sendTxToL1, + (address(ism), encodedHookData) + ) + ); + hook.postDispatch(testMetadata, encodedMessage); + } + + function testFork_postDispatch_revertWhen_chainIDNotSupported() public { + deployAll(); + + bytes memory message = MessageUtils.formatMessage( + 0, + uint32(0), + ARBITRUM_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + 2, // wrong domain + TypeCasts.addressToBytes32(address(testRecipient)), + testMessage + ); + + l2Mailbox.updateLatestDispatchedId(Message.id(message)); + vm.expectRevert( + "AbstractMessageIdAuthHook: invalid destination domain" + ); + hook.postDispatch(testMetadata, message); + } + + function test_postDispatch_revertWhen_notLastDispatchedMessage() public { + deployAll(); + + // vm.expectRevert( + // "AbstractMessageIdAuthHook: message not latest dispatched" + // ); + // hook.postDispatch(testMetadata, encodedMessage); + } + + function test_verify_outboxCall() public { + deployAll(); + + bytes memory encodedOutboxTxMetadata = _encodeOutboxTx( + address(hook), + address(ism), + messageId + ); + + arbBridge.setL2ToL1Sender(address(hook)); + assertTrue(ism.verify(encodedOutboxTxMetadata, encodedMessage)); + } + + function test_verify_statefulVerify() public { + deployAll(); + + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) + ); + + arbBridge.setL2ToL1Sender(address(hook)); + arbBridge.executeTransaction{value: 1 ether}( + new bytes32[](0), + MOCK_LEAF_INDEX, + address(hook), + address(ism), + MOCK_L2_BLOCK, + MOCK_L1_BLOCK, + block.timestamp, + 1 ether, + encodedHookData + ); + + vm.etch(address(arbBridge), new bytes(0)); // this is a way to test that the arbBridge isn't called again + assertTrue(ism.verify(new bytes(0), encodedMessage)); + assertEq(address(testRecipient).balance, 1 ether); + } + + function test_verify_statefulAndOutbox() public { + deployAll(); + + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) + ); + + arbBridge.setL2ToL1Sender(address(hook)); + arbBridge.executeTransaction{value: 1 ether}( + new bytes32[](0), + MOCK_LEAF_INDEX, + address(hook), + address(ism), + MOCK_L2_BLOCK, + MOCK_L1_BLOCK, + block.timestamp, + 1 ether, + encodedHookData + ); + + bytes memory encodedOutboxTxMetadata = _encodeOutboxTx( + address(hook), + address(ism), + messageId + ); + + vm.etch(address(arbBridge), new bytes(0)); // this is a way to test that the arbBridge isn't called again + assertTrue(ism.verify(encodedOutboxTxMetadata, encodedMessage)); + assertEq(address(testRecipient).balance, 1 ether); + } + + function test_verify_revertsWhen_noStatefulOrOutbox() public { + deployAll(); + + vm.expectRevert(); + ism.verify(new bytes(0), encodedMessage); + } + + function test_verify_revertsWhen_notAuthorizedHook() public { + deployAll(); + + bytes memory encodedOutboxTxMetadata = _encodeOutboxTx( + address(this), + address(ism), + messageId + ); + + arbBridge.setL2ToL1Sender(address(hook)); + + vm.expectRevert("ArbL2ToL1Ism: l2Sender != authorizedHook"); + ism.verify(encodedOutboxTxMetadata, encodedMessage); + } + + function test_verify_revertsWhen_invalidIsm() public { + deployAll(); + + bytes memory encodedOutboxTxMetadata = _encodeOutboxTx( + address(hook), + address(this), + messageId + ); + + arbBridge.setL2ToL1Sender(address(hook)); + + vm.expectRevert(); // BridgeCallFailed() + ism.verify(encodedOutboxTxMetadata, encodedMessage); + } + + function test_verify_revertsWhen_incorrectMessageId() public { + deployAll(); + + bytes32 incorrectMessageId = keccak256("incorrect message id"); + + bytes memory encodedOutboxTxMetadata = _encodeOutboxTx( + address(hook), + address(ism), + incorrectMessageId + ); + + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (incorrectMessageId) + ); + + arbBridge.setL2ToL1Sender(address(hook)); + + // through outbox call + vm.expectRevert("ArbL2ToL1Ism: invalid message id"); + ism.verify(encodedOutboxTxMetadata, encodedMessage); + + // through statefulVerify + arbBridge.executeTransaction( + new bytes32[](0), + MOCK_LEAF_INDEX, + address(hook), + address(ism), + MOCK_L2_BLOCK, + MOCK_L1_BLOCK, + block.timestamp, + 0, + encodedHookData + ); + + vm.etch(address(arbBridge), new bytes(0)); // to stop the outbox route + vm.expectRevert(); + assertFalse(ism.verify(new bytes(0), encodedMessage)); + } + + // function test_verify_withMsgValue + + /* ============ helper functions ============ */ + + function _encodeOutboxTx( + address _hook, + address _ism, + bytes32 _messageId + ) internal view returns (bytes memory) { + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (_messageId) + ); + + bytes32[] memory proof = new bytes32[](16); + return + abi.encode( + proof, + MOCK_LEAF_INDEX, + _hook, + _ism, + MOCK_L2_BLOCK, + MOCK_L1_BLOCK, + block.timestamp, + encodedHookData + ); + } + + function _encodeTestMessage() internal view returns (bytes memory) { + return + MessageUtils.formatMessage( + HYPERLANE_VERSION, + uint32(0), + ARBITRUM_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(testRecipient)), + testMessage + ); + } +} diff --git a/yarn.lock b/yarn.lock index 80adf046c8..1ac6d807b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,6 +29,18 @@ __metadata: languageName: node linkType: hard +"@arbitrum/nitro-contracts@npm:^1.2.1": + version: 1.2.1 + resolution: "@arbitrum/nitro-contracts@npm:1.2.1" + dependencies: + "@offchainlabs/upgrade-executor": "npm:1.1.0-beta.0" + "@openzeppelin/contracts": "npm:4.5.0" + "@openzeppelin/contracts-upgradeable": "npm:4.5.2" + patch-package: "npm:^6.4.7" + checksum: b8e682e85a6cb45757427d8d24a59752e4e69167d8347ddf36bb299a64a892d9d847bd11ee8d4c6b61b62688e83657b3a1691a1d1dfb924006b39caa64ec2df1 + languageName: node + linkType: hard + "@arbitrum/sdk@npm:^3.0.0": version: 3.0.0 resolution: "@arbitrum/sdk@npm:3.0.0" @@ -5734,6 +5746,7 @@ __metadata: version: 0.0.0-use.local resolution: "@hyperlane-xyz/core@workspace:solidity" dependencies: + "@arbitrum/nitro-contracts": "npm:^1.2.1" "@eth-optimism/contracts": "npm:^0.6.0" "@hyperlane-xyz/utils": "npm:4.0.0" "@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2" @@ -7554,6 +7567,16 @@ __metadata: languageName: node linkType: hard +"@offchainlabs/upgrade-executor@npm:1.1.0-beta.0": + version: 1.1.0-beta.0 + resolution: "@offchainlabs/upgrade-executor@npm:1.1.0-beta.0" + dependencies: + "@openzeppelin/contracts": "npm:4.7.3" + "@openzeppelin/contracts-upgradeable": "npm:4.7.3" + checksum: a8cd0cc24103cc42021c452220005efde535ba3596ec2ba5eb6dc299d1f3291c38a3d859621d7983bd7c43c80606d6e7d906e1081a1e499455ddea7ba64ab355 + languageName: node + linkType: hard + "@openzeppelin-3/contracts@npm:@openzeppelin/contracts@^3.4.2-solc-0.7": version: 3.4.2 resolution: "@openzeppelin/contracts@npm:3.4.2" @@ -7568,6 +7591,20 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts-upgradeable@npm:4.5.2": + version: 4.5.2 + resolution: "@openzeppelin/contracts-upgradeable@npm:4.5.2" + checksum: 5e246da7a44bb982a312ebf79978735712140692d46273566e490159b98b9041ca72cc08c3d05172137a389be4caad5afc001480bc5557f3d47162f4626e3723 + languageName: node + linkType: hard + +"@openzeppelin/contracts-upgradeable@npm:4.7.3": + version: 4.7.3 + resolution: "@openzeppelin/contracts-upgradeable@npm:4.7.3" + checksum: 7c72ffeca867478b5aa8e8c7adb3d1ce114cfdc797ed4f3cd074788cf4da25d620ffffd624ac7e9d1223eecffeea9f7b79200ff70dc464cc828c470ccd12ddf1 + languageName: node + linkType: hard + "@openzeppelin/contracts-upgradeable@npm:^4.6.0": version: 4.9.5 resolution: "@openzeppelin/contracts-upgradeable@npm:4.9.5" @@ -7589,6 +7626,20 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts@npm:4.5.0": + version: 4.5.0 + resolution: "@openzeppelin/contracts@npm:4.5.0" + checksum: 8bfa1733732420331728cedd7f1f5f4e4ae0700b32c9e5def19b2d42dbb0b246709e8e22abd457e8269d743012ff2aed4e3f100a942f45d9507cb78d5dbd435b + languageName: node + linkType: hard + +"@openzeppelin/contracts@npm:4.7.3": + version: 4.7.3 + resolution: "@openzeppelin/contracts@npm:4.7.3" + checksum: 3d16ed8943938373ecc331c2ab83c3e8d0d89aed0c2a109aaa61ca6524b4c31cb5a81185c6f93ce9ee2dda685a4328fd85bd217929ae598f4be813d5d4cd1b78 + languageName: node + linkType: hard + "@openzeppelin/contracts@npm:^4.2.0": version: 4.9.6 resolution: "@openzeppelin/contracts@npm:4.9.6" @@ -10752,6 +10803,13 @@ __metadata: languageName: node linkType: hard +"@yarnpkg/lockfile@npm:^1.1.0": + version: 1.1.0 + resolution: "@yarnpkg/lockfile@npm:1.1.0" + checksum: cd19e1114aaf10a05126aeea8833ef4ca8af8a46e88e12884f8359d19333fd19711036dbc2698dbe937f81f037070cf9a8da45c2e8c6ca19cafd7d15659094ed + languageName: node + linkType: hard + "JSONStream@npm:^1.3.5": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" @@ -13154,6 +13212,19 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^6.0.5": + version: 6.0.5 + resolution: "cross-spawn@npm:6.0.5" + dependencies: + nice-try: "npm:^1.0.4" + path-key: "npm:^2.0.1" + semver: "npm:^5.5.0" + shebang-command: "npm:^1.2.0" + which: "npm:^1.2.9" + checksum: f07e643b4875f26adffcd7f13bc68d9dff20cf395f8ed6f43a23f3ee24fc3a80a870a32b246fd074e514c8fd7da5f978ac6a7668346eec57aa87bac89c1ed3a1 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -15211,6 +15282,15 @@ __metadata: languageName: node linkType: hard +"find-yarn-workspace-root@npm:^2.0.0": + version: 2.0.0 + resolution: "find-yarn-workspace-root@npm:2.0.0" + dependencies: + micromatch: "npm:^4.0.2" + checksum: 7fa7942849eef4d5385ee96a0a9a5a9afe885836fd72ed6a4280312a38690afea275e7d09b343fe97daf0412d833f8ac4b78c17fc756386d9ebebf0759d707a7 + languageName: node + linkType: hard + "flat-cache@npm:^3.0.4": version: 3.0.4 resolution: "flat-cache@npm:3.0.4" @@ -15446,7 +15526,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^9.1.0": +"fs-extra@npm:^9.0.0, fs-extra@npm:^9.1.0": version: 9.1.0 resolution: "fs-extra@npm:9.1.0" dependencies: @@ -16181,7 +16261,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -17182,6 +17262,17 @@ __metadata: languageName: node linkType: hard +"is-ci@npm:^2.0.0": + version: 2.0.0 + resolution: "is-ci@npm:2.0.0" + dependencies: + ci-info: "npm:^2.0.0" + bin: + is-ci: bin.js + checksum: 77b869057510f3efa439bbb36e9be429d53b3f51abd4776eeea79ab3b221337fe1753d1e50058a9e2c650d38246108beffb15ccfd443929d77748d8c0cc90144 + languageName: node + linkType: hard + "is-ci@npm:^3.0.1": version: 3.0.1 resolution: "is-ci@npm:3.0.1" @@ -17220,6 +17311,15 @@ __metadata: languageName: node linkType: hard +"is-docker@npm:^2.0.0": + version: 2.2.1 + resolution: "is-docker@npm:2.2.1" + bin: + is-docker: cli.js + checksum: 3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56 + languageName: node + linkType: hard + "is-docker@npm:^3.0.0": version: 3.0.0 resolution: "is-docker@npm:3.0.0" @@ -17507,6 +17607,15 @@ __metadata: languageName: node linkType: hard +"is-wsl@npm:^2.1.1": + version: 2.2.0 + resolution: "is-wsl@npm:2.2.0" + dependencies: + is-docker: "npm:^2.0.0" + checksum: 20849846ae414997d290b75e16868e5261e86ff5047f104027026fd61d8b5a9b0b3ade16239f35e1a067b3c7cc02f70183cb661010ed16f4b6c7c93dad1b19d8 + languageName: node + linkType: hard + "is-wsl@npm:^3.1.0": version: 3.1.0 resolution: "is-wsl@npm:3.1.0" @@ -18494,6 +18603,15 @@ __metadata: languageName: node linkType: hard +"klaw-sync@npm:^6.0.0": + version: 6.0.0 + resolution: "klaw-sync@npm:6.0.0" + dependencies: + graceful-fs: "npm:^4.1.11" + checksum: 0da397f8961313c3ef8f79fb63af9002cde5a8fb2aeb1a37351feff0dd6006129c790400c3f5c3b4e757bedcabb13d21ec0a5eaef5a593d59515d4f2c291e475 + languageName: node + linkType: hard + "klaw@npm:^1.0.0": version: 1.3.1 resolution: "klaw@npm:1.3.1" @@ -20113,6 +20231,13 @@ __metadata: languageName: node linkType: hard +"nice-try@npm:^1.0.4": + version: 1.0.5 + resolution: "nice-try@npm:1.0.5" + checksum: 0b4af3b5bb5d86c289f7a026303d192a7eb4417231fe47245c460baeabae7277bcd8fd9c728fb6bd62c30b3e15cd6620373e2cf33353b095d8b403d3e8a15aff + languageName: node + linkType: hard + "nise@npm:^5.1.1": version: 5.1.1 resolution: "nise@npm:5.1.1" @@ -20680,6 +20805,16 @@ __metadata: languageName: node linkType: hard +"open@npm:^7.4.2": + version: 7.4.2 + resolution: "open@npm:7.4.2" + dependencies: + is-docker: "npm:^2.0.0" + is-wsl: "npm:^2.1.1" + checksum: 4fc02ed3368dcd5d7247ad3566433ea2695b0713b041ebc0eeb2f0f9e5d4e29fc2068f5cdd500976b3464e77fe8b61662b1b059c73233ccc601fe8b16d6c1cd6 + languageName: node + linkType: hard + "optionator@npm:^0.8.1": version: 0.8.3 resolution: "optionator@npm:0.8.3" @@ -20934,6 +21069,30 @@ __metadata: languageName: node linkType: hard +"patch-package@npm:^6.4.7": + version: 6.5.1 + resolution: "patch-package@npm:6.5.1" + dependencies: + "@yarnpkg/lockfile": "npm:^1.1.0" + chalk: "npm:^4.1.2" + cross-spawn: "npm:^6.0.5" + find-yarn-workspace-root: "npm:^2.0.0" + fs-extra: "npm:^9.0.0" + is-ci: "npm:^2.0.0" + klaw-sync: "npm:^6.0.0" + minimist: "npm:^1.2.6" + open: "npm:^7.4.2" + rimraf: "npm:^2.6.3" + semver: "npm:^5.6.0" + slash: "npm:^2.0.0" + tmp: "npm:^0.0.33" + yaml: "npm:^1.10.2" + bin: + patch-package: index.js + checksum: e15b3848f008da2cc659abd6d84dfeab6ed25a999ba25692071c13409f198dad28b6e451ecfebc2139a0847ad8e608575d6724bcc887c56169df8a733b849e79 + languageName: node + linkType: hard + "path-browserify@npm:^1.0.0": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" @@ -20962,6 +21121,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^2.0.1": + version: 2.0.1 + resolution: "path-key@npm:2.0.1" + checksum: 6e654864e34386a2a8e6bf72cf664dcabb76574dd54013add770b374384d438aca95f4357bb26935b514a4e4c2c9b19e191f2200b282422a76ee038b9258c5e7 + languageName: node + linkType: hard + "path-key@npm:^3.0.0, path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -22446,7 +22612,7 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^2.2.8": +"rimraf@npm:^2.2.8, rimraf@npm:^2.6.3": version: 2.7.1 resolution: "rimraf@npm:2.7.1" dependencies: @@ -22693,7 +22859,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5": +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.6.0": version: 5.7.2 resolution: "semver@npm:5.7.2" bin: @@ -23026,6 +23192,13 @@ __metadata: languageName: node linkType: hard +"slash@npm:^2.0.0": + version: 2.0.0 + resolution: "slash@npm:2.0.0" + checksum: 512d4350735375bd11647233cb0e2f93beca6f53441015eea241fe784d8068281c3987fbaa93e7ef1c38df68d9c60013045c92837423c69115297d6169aa85e6 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0"