From c032b8ba2757657b272d6df3e8173e2fd7f4b596 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Sat, 21 Sep 2024 01:40:46 +0530 Subject: [PATCH] chore: refactor MessagIdHook/ISM tests into a common `ExternalBridgeTest` contract (#4490) ### Description We currently have 6 different versions of ISMs derived from AbstractMessageIdAuthorizedIsm: - OPL2ToL1 - ArbL2ToL1 - OPStack (L1->L2) - ERC5164 - LayerZero - PolygonPOS But all of them have their own custom test suite right now, with widely overlapping testing areas. For standardizing the tests, I've created a base test contract `ExternalBridgeTest` that handles the common tests. This has a few benefits: - harder to miss tests for known pitfalls like msg.value or asynchronicity assumption for verifyMessageId - less code duplication with each additional Ism I have moved OPL2ToL1, ArbL2ToL1, OPStack, and ERC5164 to the new base test, and I'll forgo the rest for now because of the time sensitivity of audit remediations. I've created an issue tracking the remaining work here: https://github.com/hyperlane-xyz/issues/issues/1384 ### Drive-by changes None ### Related issues Related to https://github.com/chainlight-io/2024-08-hyperlane/issues/3 ### Backward compatibility Yes ### Testing Unit --- solidity/test/isms/ArbL2ToL1Ism.t.sol | 260 ++--------- solidity/test/isms/ERC5164ISM.t.sol | 152 ++----- solidity/test/isms/ExternalBridgeTest.sol | 268 +++++++++++ solidity/test/isms/OPL2ToL1Ism.t.sol | 238 ++-------- solidity/test/isms/OPStackIsm.t.sol | 529 ++++------------------ 5 files changed, 480 insertions(+), 967 deletions(-) create mode 100644 solidity/test/isms/ExternalBridgeTest.sol diff --git a/solidity/test/isms/ArbL2ToL1Ism.t.sol b/solidity/test/isms/ArbL2ToL1Ism.t.sol index f34c1a74e..a251cfda4 100644 --- a/solidity/test/isms/ArbL2ToL1Ism.t.sol +++ b/solidity/test/isms/ArbL2ToL1Ism.t.sol @@ -1,8 +1,6 @@ // 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"; @@ -13,13 +11,9 @@ import {ArbL2ToL1Hook} from "../../contracts/hooks/ArbL2ToL1Hook.sol"; import {ArbL2ToL1Ism} from "../../contracts/isms/hook/ArbL2ToL1Ism.sol"; import {MockArbBridge, MockArbSys} from "../../contracts/mock/MockArbBridge.sol"; import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; +import {ExternalBridgeTest} from "./ExternalBridgeTest.sol"; -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; - +contract ArbL2ToL1IsmTest is ExternalBridgeTest { uint256 internal constant MOCK_LEAF_INDEX = 40160; uint256 internal constant MOCK_L2_BLOCK = 54220000; uint256 internal constant MOCK_L1_BLOCK = 6098300; @@ -28,26 +22,14 @@ contract ArbL2ToL1IsmTest is Test { 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 { + function setUp() public override { // Arbitrum bridge mock setup + GAS_QUOTE = 120_000; vm.etch(L2_ARBSYS_ADDRESS, address(new MockArbSys()).code); - testRecipient = new TestRecipient(); - - encodedMessage = _encodeTestMessage(); - messageId = Message.id(encodedMessage); + deployAll(); + super.setUp(); } /////////////////////////////////////////////////////////////////// @@ -55,10 +37,10 @@ contract ArbL2ToL1IsmTest is Test { /////////////////////////////////////////////////////////////////// function deployHook() public { - l2Mailbox = new TestMailbox(ARBITRUM_DOMAIN); + originMailbox = new TestMailbox(ORIGIN_DOMAIN); hook = new ArbL2ToL1Hook( - address(l2Mailbox), - MAINNET_DOMAIN, + address(originMailbox), + DESTINATION_DOMAIN, TypeCasts.addressToBytes32(address(ism)), L2_ARBSYS_ADDRESS, GAS_QUOTE @@ -67,7 +49,6 @@ contract ArbL2ToL1IsmTest is Test { function deployIsm() public { arbBridge = new MockArbBridge(); - ism = new ArbL2ToL1Ism(address(arbBridge)); } @@ -75,196 +56,39 @@ contract ArbL2ToL1IsmTest is Test { deployIsm(); deployHook(); + arbBridge.setL2ToL1Sender(address(hook)); ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook))); } - function test_postDispatch() public { - deployAll(); - - bytes memory encodedHookData = abi.encodeCall( - AbstractMessageIdAuthorizedIsm.verifyMessageId, - (messageId) - ); - - l2Mailbox.updateLatestDispatchedId(messageId); + /* ============ helper functions ============ */ + function _expectOriginExternalBridgeCall( + bytes memory _encodedHookData + ) internal override { vm.expectCall( L2_ARBSYS_ADDRESS, abi.encodeCall( MockArbSys.sendTxToL1, - (address(ism), encodedHookData) + (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, - 1 ether - ); - - vm.deal(address(arbBridge), 1 ether); - arbBridge.setL2ToL1Sender(address(hook)); - assertTrue(ism.verify(encodedOutboxTxMetadata, encodedMessage)); - assertEq(address(testRecipient).balance, 1 ether); } - 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 _encodeExternalDestinationBridgeCall( + address _from, + address _to, + uint256 _msgValue, + bytes32 _messageId + ) internal override returns (bytes memory) { + vm.deal(address(arbBridge), _msgValue); + return _encodeOutboxTx(_from, _to, _messageId, _msgValue); } - 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, - 1 ether - ); - - 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, - 0 - ); - - arbBridge.setL2ToL1Sender(address(this)); - - 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, - 0 - ); - - 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, - 0 - ); - - 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 + function _externalBridgeDestinationCall( + bytes memory _encodedHookData, + uint256 _msgValue + ) internal override { + vm.deal(address(arbBridge), _msgValue); arbBridge.executeTransaction( new bytes32[](0), MOCK_LEAF_INDEX, @@ -273,16 +97,17 @@ contract ArbL2ToL1IsmTest is Test { MOCK_L2_BLOCK, MOCK_L1_BLOCK, block.timestamp, - 0, - encodedHookData + _msgValue, + _encodedHookData ); - - vm.etch(address(arbBridge), new bytes(0)); // to stop the outbox route - vm.expectRevert(); - assertFalse(ism.verify(new bytes(0), encodedMessage)); } - /* ============ helper functions ============ */ + function _setExternalOriginSender( + address _sender + ) internal override returns (bytes memory unauthorizedHookErrorMsg) { + arbBridge.setL2ToL1Sender(_sender); + return "ArbL2ToL1Ism: l2Sender != authorizedHook"; + } function _encodeOutboxTx( address _hook, @@ -309,17 +134,4 @@ contract ArbL2ToL1IsmTest is Test { 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/solidity/test/isms/ERC5164ISM.t.sol b/solidity/test/isms/ERC5164ISM.t.sol index c94052e25..75c79fb82 100644 --- a/solidity/test/isms/ERC5164ISM.t.sol +++ b/solidity/test/isms/ERC5164ISM.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: MIT or Apache-2.0 pragma solidity ^0.8.13; -import {Test} from "forge-std/Test.sol"; - import {LibBit} from "../../contracts/libs/LibBit.sol"; import {Message} from "../../contracts/libs/Message.sol"; import {MessageUtils} from "./IsmTestUtils.sol"; @@ -17,8 +15,9 @@ import {ERC5164Ism} from "../../contracts/isms/hook/ERC5164Ism.sol"; import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; import {MockMessageDispatcher, MockMessageExecutor} from "../../contracts/mock/MockERC5164.sol"; +import {ExternalBridgeTest} from "./ExternalBridgeTest.sol"; -contract ERC5164IsmTest is Test { +contract ERC5164IsmTest is ExternalBridgeTest { using LibBit for uint256; using TypeCasts for address; using Message for bytes; @@ -27,23 +26,8 @@ contract ERC5164IsmTest is Test { IMessageDispatcher internal dispatcher; MockMessageExecutor internal executor; - ERC5164Hook internal hook; - ERC5164Ism internal ism; - TestMailbox internal originMailbox; - TestRecipient internal testRecipient; - - uint32 internal constant TEST1_DOMAIN = 1; - uint32 internal constant TEST2_DOMAIN = 2; - - uint8 internal constant VERSION = 0; - bytes internal testMessage = - abi.encodePacked("Hello from the other chain!"); address internal alice = address(0x1); - // req for most tests - bytes encodedMessage = _encodeTestMessage(0, address(testRecipient)); - bytes32 messageId = encodedMessage.id(); - event MessageDispatched( bytes32 indexed messageId, address indexed from, @@ -56,19 +40,19 @@ contract ERC5164IsmTest is Test { /// SETUP /// /////////////////////////////////////////////////////////////////// - function setUp() public { + function setUp() public override { dispatcher = new MockMessageDispatcher(); executor = new MockMessageExecutor(); - testRecipient = new TestRecipient(); - originMailbox = new TestMailbox(TEST1_DOMAIN); + originMailbox = new TestMailbox(ORIGIN_DOMAIN); ism = new ERC5164Ism(address(executor)); hook = new ERC5164Hook( address(originMailbox), - TEST2_DOMAIN, + DESTINATION_DOMAIN, address(ism).addressToBytes32(), address(dispatcher) ); ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook))); + super.setUp(); } /////////////////////////////////////////////////////////////////// @@ -100,7 +84,7 @@ contract ERC5164IsmTest is Test { vm.expectRevert("AbstractMessageIdAuthHook: invalid ISM"); hook = new ERC5164Hook( address(originMailbox), - TEST2_DOMAIN, + DESTINATION_DOMAIN, address(0).addressToBytes32(), address(dispatcher) ); @@ -108,125 +92,83 @@ contract ERC5164IsmTest is Test { vm.expectRevert("ERC5164Hook: invalid dispatcher"); hook = new ERC5164Hook( address(originMailbox), - TEST2_DOMAIN, + DESTINATION_DOMAIN, address(ism).addressToBytes32(), address(0) ); } - function test_postDispatch() public { - bytes memory encodedHookData = abi.encodeCall( - AbstractMessageIdAuthorizedIsm.verifyMessageId, - (messageId) - ); - originMailbox.updateLatestDispatchedId(messageId); + function testTypes() public view { + assertEq(hook.hookType(), uint8(IPostDispatchHook.Types.ID_AUTH_ISM)); + assertEq(ism.moduleType(), uint8(IInterchainSecurityModule.Types.NULL)); + } - // note: not checking for messageId since this is implementation dependent on each vendor + function _expectOriginExternalBridgeCall( + bytes memory _encodedHookData + ) internal override { vm.expectEmit(false, true, true, true, address(dispatcher)); emit MessageDispatched( messageId, address(hook), - TEST2_DOMAIN, + DESTINATION_DOMAIN, address(ism), - encodedHookData + _encodedHookData ); - - hook.postDispatch(bytes(""), encodedMessage); - } - - function testTypes() public { - assertEq(hook.hookType(), uint8(IPostDispatchHook.Types.ID_AUTH_ISM)); - assertEq(ism.moduleType(), uint8(IInterchainSecurityModule.Types.NULL)); } - function test_postDispatch_RevertWhen_ChainIDNotSupported() public { - encodedMessage = MessageUtils.formatMessage( - VERSION, - 0, - TEST1_DOMAIN, - TypeCasts.addressToBytes32(address(this)), - 3, // unsupported chain id - TypeCasts.addressToBytes32(address(testRecipient)), - testMessage - ); - originMailbox.updateLatestDispatchedId(Message.id(encodedMessage)); - - vm.expectRevert( - "AbstractMessageIdAuthHook: invalid destination domain" - ); - hook.postDispatch(bytes(""), encodedMessage); + function test_verify_revertWhen_invalidMetadata() public override { + assertFalse(ism.verify(new bytes(0), encodedMessage)); } - function test_postDispatch_RevertWhen_msgValueNotAllowed() public payable { + function test_postDispatch_revertWhen_msgValueNotAllowed() public payable { originMailbox.updateLatestDispatchedId(messageId); vm.expectRevert("ERC5164Hook: no value allowed"); hook.postDispatch{value: 1}(bytes(""), encodedMessage); } - /* ============ ISM.verifyMessageId ============ */ - - function test_verifyMessageId() public { - vm.startPrank(address(executor)); - - ism.verifyMessageId(messageId); - assertTrue(ism.verifiedMessages(messageId).isBitSet(255)); - - vm.stopPrank(); - } - - function test_verifyMessageId_RevertWhen_NotAuthorized() public { - vm.startPrank(alice); + // override to omit direct external bridge call + function test_verify_revertsWhen_notAuthorizedHook() public override { + vm.prank(alice); - // needs to be called by the authorized hook contract on Ethereum vm.expectRevert( "AbstractMessageIdAuthorizedIsm: sender is not the hook" ); ism.verifyMessageId(messageId); - - vm.stopPrank(); + assertFalse(ism.isVerified(encodedMessage)); } - /* ============ ISM.verify ============ */ + // SKIP - duplicate of test_verify_revertWhen_invalidMetadata + function test_verify_revertsWhen_incorrectMessageId() public override {} - function test_verify() public { - vm.startPrank(address(executor)); + function test_verify_revertsWhen_invalidIsm() public override {} - ism.verifyMessageId(messageId); + // SKIP - 5164 ism does not support msg.value + function test_verify_msgValue_asyncCall() public override {} - bool verified = ism.verify(new bytes(0), encodedMessage); - assertTrue(verified); + function test_verify_msgValue_externalBridgeCall() public override {} - vm.stopPrank(); - } + function test_verify_valueAlreadyClaimed(uint256) public override {} - function test_verify_RevertWhen_InvalidMessage() public { - vm.startPrank(address(executor)); + /* ============ helper functions ============ */ + function _externalBridgeDestinationCall( + bytes memory _encodedHookData, + uint256 _msgValue + ) internal override { + vm.prank(address(executor)); ism.verifyMessageId(messageId); - - bytes memory invalidMessage = _encodeTestMessage(0, address(this)); - bool verified = ism.verify(new bytes(0), invalidMessage); - assertFalse(verified); - - vm.stopPrank(); } - /* ============ helper functions ============ */ - - function _encodeTestMessage( - uint32 _msgCount, - address _receipient - ) internal view returns (bytes memory) { - return - MessageUtils.formatMessage( - VERSION, - _msgCount, - TEST1_DOMAIN, - TypeCasts.addressToBytes32(address(this)), - TEST2_DOMAIN, - TypeCasts.addressToBytes32(_receipient), - testMessage - ); + function _encodeExternalDestinationBridgeCall( + address _from, + address _to, + uint256 _msgValue, + bytes32 _messageId + ) internal override returns (bytes memory) { + if (_from == address(hook)) { + vm.prank(address(executor)); + ism.verifyMessageId{value: _msgValue}(messageId); + } } } diff --git a/solidity/test/isms/ExternalBridgeTest.sol b/solidity/test/isms/ExternalBridgeTest.sol new file mode 100644 index 000000000..8db043fcf --- /dev/null +++ b/solidity/test/isms/ExternalBridgeTest.sol @@ -0,0 +1,268 @@ +// 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 {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; +import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; +import {Message} from "../../contracts/libs/Message.sol"; +import {MessageUtils} from "./IsmTestUtils.sol"; +import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; +import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; +import {AbstractMessageIdAuthHook} from "../../contracts/hooks/libs/AbstractMessageIdAuthHook.sol"; + +abstract contract ExternalBridgeTest is Test { + using TypeCasts for address; + using MessageUtils for bytes; + + uint8 internal constant HYPERLANE_VERSION = 1; + uint32 internal constant ORIGIN_DOMAIN = 1; + uint32 internal constant DESTINATION_DOMAIN = 2; + uint256 internal constant MAX_MSG_VALUE = 2 ** 255 - 1; + uint256 internal GAS_QUOTE; + + TestMailbox internal originMailbox; + TestRecipient internal testRecipient; + + AbstractMessageIdAuthHook internal hook; + AbstractMessageIdAuthorizedIsm internal ism; + + bytes internal testMessage = + abi.encodePacked("Hello from the other chain!"); + bytes internal testMetadata = + StandardHookMetadata.overrideRefundAddress(address(this)); + bytes internal encodedMessage; + bytes32 internal messageId; + + function setUp() public virtual { + testRecipient = new TestRecipient(); + encodedMessage = _encodeTestMessage(); + messageId = Message.id(encodedMessage); + } + + /* ============ hook.quoteDispatch ============ */ + + function test_quoteDispatch() public view { + assertEq(hook.quoteDispatch(testMetadata, encodedMessage), GAS_QUOTE); + } + + /* ============ Hook.postDispatch ============ */ + + function test_postDispatch() public { + bytes memory encodedHookData = _encodeHookData(messageId); + originMailbox.updateLatestDispatchedId(messageId); + _expectOriginExternalBridgeCall(encodedHookData); + + hook.postDispatch{value: GAS_QUOTE}(testMetadata, encodedMessage); + } + + function test_postDispatch_revertWhen_chainIDNotSupported() public { + bytes memory message = originMailbox.buildOutboundMessage( + 3, + TypeCasts.addressToBytes32(address(this)), + testMessage + ); + + originMailbox.updateLatestDispatchedId(Message.id(message)); + vm.expectRevert( + "AbstractMessageIdAuthHook: invalid destination domain" + ); + hook.postDispatch(testMetadata, message); + } + + function test_postDispatch_revertWhen_notLastDispatchedMessage() public { + vm.expectRevert( + "AbstractMessageIdAuthHook: message not latest dispatched" + ); + hook.postDispatch(testMetadata, encodedMessage); + } + + function test_postDispatch_revertWhen_tooMuchValue() public { + vm.deal(address(this), uint256(MAX_MSG_VALUE + 1)); + bytes memory excessValueMetadata = StandardHookMetadata + .overrideMsgValue(uint256(2 ** 255 + 1)); + + originMailbox.updateLatestDispatchedId(messageId); + vm.expectRevert( + "AbstractMessageIdAuthHook: msgValue must be less than 2 ** 255" + ); + hook.postDispatch(excessValueMetadata, encodedMessage); + } + + /* ============ ISM.verifyMessageId ============ */ + + function test_verifyMessageId_asyncCall() public { + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) + ); + _externalBridgeDestinationCall(encodedHookData, 0); + + assertTrue(ism.isVerified(encodedMessage)); + } + + function test_verifyMessageId_externalBridgeCall() public virtual { + bytes memory externalCalldata = _encodeExternalDestinationBridgeCall( + address(hook), + address(ism), + 0, + messageId + ); + + assertTrue(ism.verify(externalCalldata, encodedMessage)); + assertTrue(ism.isVerified(encodedMessage)); + } + + /* ============ ISM.verify ============ */ + + function test_verify_revertWhen_invalidMetadata() public virtual { + vm.expectRevert(); + assertFalse(ism.verify(new bytes(0), encodedMessage)); + } + + function test_verify_msgValue_asyncCall() public virtual { + bytes memory encodedHookData = _encodeHookData(messageId); + _externalBridgeDestinationCall(encodedHookData, 1 ether); + + assertTrue(ism.verify(new bytes(0), encodedMessage)); + assertEq(address(testRecipient).balance, 1 ether); + } + + function test_verify_msgValue_externalBridgeCall() public virtual { + bytes memory externalCalldata = _encodeExternalDestinationBridgeCall( + address(hook), + address(ism), + 1 ether, + messageId + ); + ism.verify(externalCalldata, encodedMessage); + assertEq(address(testRecipient).balance, 1 ether); + } + + function test_verify_revertsWhen_invalidIsm() public virtual { + bytes memory externalCalldata = _encodeExternalDestinationBridgeCall( + address(hook), + address(this), + 0, + messageId + ); + + vm.expectRevert(); + assertFalse(ism.verify(externalCalldata, encodedMessage)); + } + + function test_verify_revertsWhen_notAuthorizedHook() public virtual { + bytes memory unauthorizedHookErrorMsg = _setExternalOriginSender( + address(this) + ); + + bytes memory externalCalldata = _encodeExternalDestinationBridgeCall( + address(this), + address(ism), + 0, + messageId + ); + + // external call + vm.expectRevert(unauthorizedHookErrorMsg); + assertFalse(ism.verify(externalCalldata, encodedMessage)); + + // async call vm.expectRevert(NotCrossChainCall.selector); + vm.expectRevert(); + _externalBridgeDestinationCall(externalCalldata, 0); + assertFalse(ism.isVerified(encodedMessage)); + } + + function test_verify_revertsWhen_incorrectMessageId() public virtual { + bytes32 incorrectMessageId = keccak256("incorrect message id"); + bytes memory externalCalldata = _encodeExternalDestinationBridgeCall( + address(hook), + address(ism), + 0, + incorrectMessageId + ); + + // external call + vm.expectRevert(); + assertFalse(ism.verify(externalCalldata, encodedMessage)); + + // async call - native bridges might have try catch block to prevent revert + try + this.externalBridgeDestinationCallWrapper( + _encodeHookData(incorrectMessageId), + 0 + ) + {} catch {} + assertFalse(ism.isVerified(testMessage)); + } + + /// forge-config: default.fuzz.runs = 10 + function test_verify_valueAlreadyClaimed(uint256 _msgValue) public virtual { + _msgValue = bound(_msgValue, 0, MAX_MSG_VALUE); + _externalBridgeDestinationCall(_encodeHookData(messageId), _msgValue); + + bool verified = ism.verify(new bytes(0), encodedMessage); + assertTrue(verified); + assertEq(address(ism).balance, 0); + assertEq(address(testRecipient).balance, _msgValue); + + // send more value to the ISM + vm.deal(address(ism), _msgValue); + + // verified still true + verified = ism.verify(new bytes(0), encodedMessage); + assertTrue(verified); + // value which was already sent + assertEq(address(ism).balance, _msgValue); + assertEq(address(testRecipient).balance, _msgValue); + } + + /* ============ helper functions ============ */ + + function _encodeTestMessage() internal view returns (bytes memory) { + return + originMailbox.buildOutboundMessage( + DESTINATION_DOMAIN, + TypeCasts.addressToBytes32(address(testRecipient)), + testMessage + ); + } + + function _encodeHookData( + bytes32 _messageId + ) internal pure returns (bytes memory) { + return + abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (_messageId) + ); + } + + // wrapper function needed for _externalBridgeDestinationCall because try catch cannot call an internal function + function externalBridgeDestinationCallWrapper( + bytes memory _encodedHookData, + uint256 _msgValue + ) external { + _externalBridgeDestinationCall(_encodedHookData, _msgValue); + } + + function _expectOriginExternalBridgeCall( + bytes memory _encodedHookData + ) internal virtual; + + function _externalBridgeDestinationCall( + bytes memory _encodedHookData, + uint256 _msgValue + ) internal virtual; + + function _encodeExternalDestinationBridgeCall( + address _from, + address _to, + uint256 _msgValue, + bytes32 _messageId + ) internal virtual returns (bytes memory); + + function _setExternalOriginSender( + address _sender + ) internal virtual returns (bytes memory) {} +} diff --git a/solidity/test/isms/OPL2ToL1Ism.t.sol b/solidity/test/isms/OPL2ToL1Ism.t.sol index 9466a7c93..d829e0211 100644 --- a/solidity/test/isms/OPL2ToL1Ism.t.sol +++ b/solidity/test/isms/OPL2ToL1Ism.t.sol @@ -1,8 +1,6 @@ // 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 {Message} from "../../contracts/libs/Message.sol"; import {MessageUtils} from "./IsmTestUtils.sol"; @@ -15,57 +13,41 @@ import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; import {MockOptimismMessenger, MockOptimismPortal} from "../../contracts/mock/MockOptimism.sol"; import {OPL2ToL1Hook} from "../../contracts/hooks/OPL2ToL1Hook.sol"; import {OPL2ToL1Ism} from "../../contracts/isms/hook/OPL2ToL1Ism.sol"; +import {ExternalBridgeTest} from "./ExternalBridgeTest.sol"; -contract OPL2ToL1IsmTest is Test { - uint8 internal constant HYPERLANE_VERSION = 1; - uint32 internal constant MAINNET_DOMAIN = 1; - uint32 internal constant OPTIMISM_DOMAIN = 10; - uint32 internal constant GAS_QUOTE = 120_000; - +contract OPL2ToL1IsmTest is ExternalBridgeTest { address internal constant L2_MESSENGER_ADDRESS = 0x4200000000000000000000000000000000000007; uint256 internal constant MOCK_NONCE = 0; - TestMailbox public l2Mailbox; - 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; - MockOptimismPortal internal portal; MockOptimismMessenger internal l1Messenger; - OPL2ToL1Hook public hook; - OPL2ToL1Ism public ism; /////////////////////////////////////////////////////////////////// /// SETUP /// /////////////////////////////////////////////////////////////////// - function setUp() public { + function setUp() public override { // Optimism messenger mock setup + GAS_QUOTE = 120_000; vm.etch( L2_MESSENGER_ADDRESS, address(new MockOptimismMessenger()).code ); - testRecipient = new TestRecipient(); - - encodedMessage = _encodeTestMessage(); - messageId = Message.id(encodedMessage); + deployAll(); + super.setUp(); } function deployHook() public { - l2Mailbox = new TestMailbox(OPTIMISM_DOMAIN); + originMailbox = new TestMailbox(ORIGIN_DOMAIN); hook = new OPL2ToL1Hook( - address(l2Mailbox), - MAINNET_DOMAIN, + address(originMailbox), + DESTINATION_DOMAIN, TypeCasts.addressToBytes32(address(ism)), L2_MESSENGER_ADDRESS, - GAS_QUOTE + uint32(GAS_QUOTE) ); } @@ -85,197 +67,58 @@ contract OPL2ToL1IsmTest is Test { ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook))); } - function test_postDispatch() public { - deployAll(); - - bytes memory encodedHookData = abi.encodeCall( - AbstractMessageIdAuthorizedIsm.verifyMessageId, - (messageId) - ); - - l2Mailbox.updateLatestDispatchedId(messageId); + /* ============ helper functions ============ */ + function _expectOriginExternalBridgeCall( + bytes memory _encodedHookData + ) internal override { vm.expectCall( L2_MESSENGER_ADDRESS, abi.encodeCall( ICrossDomainMessenger.sendMessage, - (address(ism), encodedHookData, GAS_QUOTE) + (address(ism), _encodedHookData, uint32(GAS_QUOTE)) ) ); - - hook.postDispatch{value: GAS_QUOTE}(testMetadata, encodedMessage); - } - - function testFork_postDispatch_revertWhen_chainIDNotSupported() public { - deployAll(); - - bytes memory message = MessageUtils.formatMessage( - 0, - uint32(0), - OPTIMISM_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_directWithdrawalCall() public { - deployAll(); - - bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx( - address(ism), - 0, - messageId - ); - - assertTrue(ism.verify(encodedWithdrawalTx, encodedMessage)); - } - - function test_verify_directWithdrawalCall_revertsWhen_invalidSender() - public - { - deployAll(); - l1Messenger.setXDomainMessageSender(address(this)); - - bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx( - address(ism), - 0, - messageId - ); - - vm.expectRevert(); // evmRevert in MockOptimismPortal - ism.verify(encodedWithdrawalTx, encodedMessage); - } - - function test_verify_statefulVerify() public { - deployAll(); - - vm.deal(address(portal), 1 ether); - IOptimismPortal.WithdrawalTransaction - memory withdrawal = IOptimismPortal.WithdrawalTransaction({ - nonce: MOCK_NONCE, - sender: L2_MESSENGER_ADDRESS, - target: address(l1Messenger), - value: 1 ether, - gasLimit: uint256(GAS_QUOTE), - data: _encodeMessengerCalldata(address(ism), 1 ether, messageId) - }); - portal.finalizeWithdrawalTransaction(withdrawal); - - vm.etch(address(portal), new bytes(0)); // this is a way to test that the portal isn't called again - assertTrue(ism.verify(new bytes(0), encodedMessage)); - assertEq(address(testRecipient).balance, 1 ether); // testing msg.value - } - - function test_verify_statefulAndDirectWithdrawal() public { - deployAll(); - - IOptimismPortal.WithdrawalTransaction - memory withdrawal = IOptimismPortal.WithdrawalTransaction({ - nonce: MOCK_NONCE, - sender: L2_MESSENGER_ADDRESS, - target: address(l1Messenger), - value: 0, - gasLimit: uint256(GAS_QUOTE), - data: _encodeMessengerCalldata(address(ism), 0, messageId) - }); - portal.finalizeWithdrawalTransaction(withdrawal); - - bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx( - address(ism), - 0, - messageId - ); - - vm.etch(address(portal), new bytes(0)); // this is a way to test that the portal isn't called again - assertTrue(ism.verify(encodedWithdrawalTx, encodedMessage)); } - function test_verify_revertsWhen_noStatefulAndDirectWithdrawal() public { - deployAll(); - - vm.expectRevert(); - ism.verify(new bytes(0), encodedMessage); + function _encodeExternalDestinationBridgeCall( + address, + /*_from*/ + address _to, + uint256 _msgValue, + bytes32 _messageId + ) internal override returns (bytes memory) { + vm.deal(address(portal), _msgValue); + return _encodeFinalizeWithdrawalTx(_to, _msgValue, _messageId); } - function test_verify_revertsWhen_invalidIsm() public { - deployAll(); - - bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx( - address(this), - 0, - messageId - ); - - vm.expectRevert(); // evmRevert in MockOptimismPortal - ism.verify(encodedWithdrawalTx, encodedMessage); + function _setExternalOriginSender( + address _sender + ) internal override returns (bytes memory) { + l1Messenger.setXDomainMessageSender(_sender); + return "AbstractMessageIdAuthorizedIsm: sender is not the hook"; } - function test_verify_revertsWhen_incorrectMessageId() public { - deployAll(); - - bytes32 incorrectMessageId = keccak256("incorrect message id"); - - bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx( - address(this), - 0, - incorrectMessageId - ); - - // through portal call - vm.expectRevert("OPL2ToL1Ism: invalid message id"); - ism.verify(encodedWithdrawalTx, encodedMessage); - - // through statefulVerify + function _externalBridgeDestinationCall( + bytes memory, + /*_encodedHookData*/ + uint256 _msgValue + ) internal override { + vm.deal(address(portal), _msgValue); IOptimismPortal.WithdrawalTransaction memory withdrawal = IOptimismPortal.WithdrawalTransaction({ nonce: MOCK_NONCE, sender: L2_MESSENGER_ADDRESS, target: address(l1Messenger), - value: 0, + value: _msgValue, gasLimit: uint256(GAS_QUOTE), data: _encodeMessengerCalldata( address(ism), - 0, - incorrectMessageId + _msgValue, + messageId ) }); portal.finalizeWithdrawalTransaction(withdrawal); - - vm.etch(address(portal), new bytes(0)); // to stop the portal route - vm.expectRevert(); // evmRevert() - assertFalse(ism.verify(new bytes(0), encodedMessage)); - } - - /* ============ helper functions ============ */ - - function _encodeTestMessage() internal view returns (bytes memory) { - return - MessageUtils.formatMessage( - HYPERLANE_VERSION, - uint32(0), - OPTIMISM_DOMAIN, - TypeCasts.addressToBytes32(address(this)), - MAINNET_DOMAIN, - TypeCasts.addressToBytes32(address(testRecipient)), - testMessage - ); } function _encodeMessengerCalldata( @@ -283,10 +126,7 @@ contract OPL2ToL1IsmTest is Test { uint256 _value, bytes32 _messageId ) internal view returns (bytes memory) { - bytes memory encodedHookData = abi.encodeCall( - AbstractMessageIdAuthorizedIsm.verifyMessageId, - (_messageId) - ); + bytes memory encodedHookData = _encodeHookData(_messageId); return abi.encodeCall( diff --git a/solidity/test/isms/OPStackIsm.t.sol b/solidity/test/isms/OPStackIsm.t.sol index 9862717ad..45c818ec3 100644 --- a/solidity/test/isms/OPStackIsm.t.sol +++ b/solidity/test/isms/OPStackIsm.t.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT or Apache-2.0 pragma solidity ^0.8.13; -import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; import {LibBit} from "../../contracts/libs/LibBit.sol"; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; +import {MockOptimismMessenger} from "../../contracts/mock/MockOptimism.sol"; import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; import {Message} from "../../contracts/libs/Message.sol"; import {MessageUtils} from "./IsmTestUtils.sol"; @@ -18,16 +19,14 @@ import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; import {NotCrossChainCall} from "@openzeppelin/contracts/crosschain/errors.sol"; import {AddressAliasHelper} from "@eth-optimism/contracts/standards/AddressAliasHelper.sol"; -import {ICrossDomainMessenger, IL2CrossDomainMessenger} from "../../contracts/interfaces/optimism/ICrossDomainMessenger.sol"; +import {ICrossDomainMessenger} from "../../contracts/interfaces/optimism/ICrossDomainMessenger.sol"; +import {ExternalBridgeTest} from "./ExternalBridgeTest.sol"; -contract OPStackIsmTest is Test { +contract OPStackIsmTest is ExternalBridgeTest { using LibBit for uint256; using TypeCasts for address; using MessageUtils for bytes; - uint256 internal mainnetFork; - uint256 internal optimismFork; - address internal constant L1_MESSENGER_ADDRESS = 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1; address internal constant L1_CANNONICAL_CHAIN = @@ -36,36 +35,12 @@ contract OPStackIsmTest is Test { 0x4200000000000000000000000000000000000007; uint8 internal constant OPTIMISM_VERSION = 0; - uint8 internal constant HYPERLANE_VERSION = 1; uint256 internal constant DEFAULT_GAS_LIMIT = 1_920_000; address internal alice = address(0x1); - ICrossDomainMessenger internal l1Messenger; - IL2CrossDomainMessenger internal l2Messenger; - TestMailbox internal l1Mailbox; - OPStackIsm internal opISM; - OPStackHook internal opHook; - - TestRecipient internal testRecipient; - bytes internal testMessage = - abi.encodePacked("Hello from the other chain!"); - bytes internal testMetadata = - StandardHookMetadata.overrideRefundAddress(address(this)); - - bytes internal encodedMessage; - bytes32 internal messageId; - - uint32 internal constant MAINNET_DOMAIN = 1; - uint32 internal constant OPTIMISM_DOMAIN = 10; - - event SentMessage( - address indexed target, - address sender, - bytes message, - uint256 messageNonce, - uint256 gasLimit - ); + MockOptimismMessenger internal l1Messenger; + MockOptimismMessenger internal l2Messenger; event RelayedMessage(bytes32 indexed msgHash); @@ -73,469 +48,145 @@ contract OPStackIsmTest is Test { event ReceivedMessage(bytes32 indexed messageId); - function setUp() public { - // block numbers to fork from, chain data is cached to ../../forge-cache/ - mainnetFork = vm.createFork(vm.rpcUrl("mainnet"), 18_992_500); - optimismFork = vm.createFork(vm.rpcUrl("optimism"), 114_696_811); + function setUp() public override { + GAS_QUOTE = 0; - testRecipient = new TestRecipient(); + vm.etch( + L1_MESSENGER_ADDRESS, + address(new MockOptimismMessenger()).code + ); + vm.etch( + L2_MESSENGER_ADDRESS, + address(new MockOptimismMessenger()).code + ); + l1Messenger = MockOptimismMessenger(L1_MESSENGER_ADDRESS); + l2Messenger = MockOptimismMessenger(L2_MESSENGER_ADDRESS); - encodedMessage = _encodeTestMessage(); - messageId = Message.id(encodedMessage); + deployAll(); + super.setUp(); } /////////////////////////////////////////////////////////////////// /// SETUP /// /////////////////////////////////////////////////////////////////// - function deployOptimismHook() public { - vm.selectFork(mainnetFork); - - l1Messenger = ICrossDomainMessenger(L1_MESSENGER_ADDRESS); - l1Mailbox = new TestMailbox(MAINNET_DOMAIN); - - opHook = new OPStackHook( - address(l1Mailbox), - OPTIMISM_DOMAIN, - TypeCasts.addressToBytes32(address(opISM)), + function deployHook() public { + originMailbox = new TestMailbox(ORIGIN_DOMAIN); + hook = new OPStackHook( + address(originMailbox), + DESTINATION_DOMAIN, + TypeCasts.addressToBytes32(address(ism)), L1_MESSENGER_ADDRESS ); - - vm.makePersistent(address(opHook)); } - function deployOPStackIsm() public { - vm.selectFork(optimismFork); - - l2Messenger = IL2CrossDomainMessenger(L2_MESSENGER_ADDRESS); - opISM = new OPStackIsm(L2_MESSENGER_ADDRESS); - - vm.makePersistent(address(opISM)); + function deployIsm() public { + ism = new OPStackIsm(L2_MESSENGER_ADDRESS); } function deployAll() public { - deployOPStackIsm(); - deployOptimismHook(); + deployIsm(); + deployHook(); - vm.selectFork(optimismFork); - - opISM.setAuthorizedHook(TypeCasts.addressToBytes32(address(opHook))); - // for sending value - vm.deal( - AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS), - 2 ** 255 - ); + ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook))); + l2Messenger.setXDomainMessageSender(address(hook)); } - /////////////////////////////////////////////////////////////////// - /// FORK TESTS /// - /////////////////////////////////////////////////////////////////// - - /* ============ hook.quoteDispatch ============ */ - - function testFork_quoteDispatch() public { - deployAll(); - - vm.selectFork(mainnetFork); - - assertEq(opHook.quoteDispatch(testMetadata, encodedMessage), 0); + function test_verify_revertWhen_invalidMetadata() public override { + assertFalse(ism.verify(new bytes(0), encodedMessage)); } - /* ============ hook.postDispatch ============ */ + function test_verify_revertsWhen_incorrectMessageId() public override { + bytes32 incorrectMessageId = keccak256("incorrect message id"); - function testFork_postDispatch() public { - deployAll(); - - vm.selectFork(mainnetFork); - - bytes memory encodedHookData = abi.encodeCall( - AbstractMessageIdAuthorizedIsm.verifyMessageId, - (messageId) - ); - - uint40 testNonce = 123; - l1Mailbox.updateLatestDispatchedId(messageId); - - vm.expectEmit(true, true, true, false, L1_MESSENGER_ADDRESS); - emit SentMessage( - address(opISM), - address(opHook), - encodedHookData, - testNonce, - DEFAULT_GAS_LIMIT - ); - opHook.postDispatch(testMetadata, encodedMessage); + _externalBridgeDestinationCall(_encodeHookData(incorrectMessageId), 0); + assertFalse(ism.isVerified(testMessage)); } - function testFork_postDispatch_RevertWhen_ChainIDNotSupported() public { - deployAll(); - - vm.selectFork(mainnetFork); - - bytes memory message = MessageUtils.formatMessage( - OPTIMISM_VERSION, - uint32(0), - MAINNET_DOMAIN, - TypeCasts.addressToBytes32(address(this)), - 11, // wrong domain - TypeCasts.addressToBytes32(address(testRecipient)), - testMessage - ); + /* ============ helper functions ============ */ - l1Mailbox.updateLatestDispatchedId(Message.id(message)); - vm.expectRevert( - "AbstractMessageIdAuthHook: invalid destination domain" + function _expectOriginExternalBridgeCall( + bytes memory _encodedHookData + ) internal override { + vm.expectCall( + L1_MESSENGER_ADDRESS, + abi.encodeCall( + ICrossDomainMessenger.sendMessage, + (address(ism), _encodedHookData, uint32(DEFAULT_GAS_LIMIT)) + ) ); - opHook.postDispatch(testMetadata, message); } - function testFork_postDispatch_RevertWhen_TooMuchValue() public { - deployAll(); - - vm.selectFork(mainnetFork); - - vm.deal(address(this), uint256(2 ** 255 + 1)); - bytes memory excessValueMetadata = StandardHookMetadata - .overrideMsgValue(uint256(2 ** 255 + 1)); - - l1Mailbox.updateLatestDispatchedId(messageId); - vm.expectRevert( - "AbstractMessageIdAuthHook: msgValue must be less than 2 ** 255" + function _externalBridgeDestinationCall( + bytes memory _encodedHookData, + uint256 _msgValue + ) internal override { + vm.deal(L2_MESSENGER_ADDRESS, _msgValue); + l2Messenger.relayMessage( + 0, + address(hook), + address(ism), + _msgValue, + uint32(GAS_QUOTE), + _encodedHookData ); - opHook.postDispatch(excessValueMetadata, encodedMessage); } - function testFork_postDispatch_RevertWhen_NotLastDispatchedMessage() - public - { - deployAll(); - - vm.selectFork(mainnetFork); - - vm.expectRevert( - "AbstractMessageIdAuthHook: message not latest dispatched" - ); - opHook.postDispatch(testMetadata, encodedMessage); + function _encodeExternalDestinationBridgeCall( + address _from, + address _to, + uint256 _msgValue, + bytes32 _messageId + ) internal pure override returns (bytes memory) { + return new bytes(0); } - /* ============ ISM.verifyMessageId ============ */ - - function testFork_verifyMessageId() public { - deployAll(); - - vm.selectFork(optimismFork); - - bytes memory encodedHookData = abi.encodeCall( - AbstractMessageIdAuthorizedIsm.verifyMessageId, - (messageId) - ); - - (uint240 nonce, uint16 version) = decodeVersionedNonce( - l2Messenger.messageNonce() - ); - uint256 versionedNonce = encodeVersionedNonce(nonce + 1, version); + // SKIP - no external bridge call + function test_verifyMessageId_externalBridgeCall() public override {} - bytes32 versionedHash = hashCrossDomainMessageV1( - versionedNonce, - address(opHook), - address(opISM), - 0, - DEFAULT_GAS_LIMIT, - encodedHookData - ); - - vm.startPrank( - AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS) - ); + function test_verify_msgValue_externalBridgeCall() public override {} - vm.expectEmit(true, false, false, false, address(opISM)); - emit ReceivedMessage(messageId); + function test_verify_revertsWhen_invalidIsm() public override {} - vm.expectEmit(true, false, false, false, L2_MESSENGER_ADDRESS); - emit RelayedMessage(versionedHash); - - l2Messenger.relayMessage( - versionedNonce, - address(opHook), - address(opISM), - 0, - DEFAULT_GAS_LIMIT, - encodedHookData - ); - - assertTrue(opISM.verifiedMessages(messageId).isBitSet(255)); - vm.stopPrank(); - } - - function testFork_verifyMessageId_RevertWhen_NotAuthorized() public { - deployAll(); - - vm.selectFork(optimismFork); + /* ============ ISM.verifyMessageId ============ */ + function test_verify_revertsWhen_notAuthorizedHook() public override { // needs to be called by the canonical messenger on Optimism vm.expectRevert(NotCrossChainCall.selector); - opISM.verifyMessageId(messageId); - - // set the xDomainMessageSender storage slot as alice - bytes32 key = bytes32(uint256(204)); - bytes32 value = TypeCasts.addressToBytes32(alice); - vm.store(address(l2Messenger), key, value); + ism.verifyMessageId(messageId); vm.startPrank(L2_MESSENGER_ADDRESS); + _setExternalOriginSender(address(this)); // needs to be called by the authorized hook contract on Ethereum vm.expectRevert( "AbstractMessageIdAuthorizedIsm: sender is not the hook" ); - opISM.verifyMessageId(messageId); + ism.verifyMessageId(messageId); } - /* ============ ISM.verify ============ */ - - function testFork_verify() public { - deployAll(); - - vm.selectFork(optimismFork); - - orchestrateRelayMessage(0, messageId); - - bool verified = opISM.verify(new bytes(0), encodedMessage); - assertTrue(verified); + function _setExternalOriginSender( + address _sender + ) internal override returns (bytes memory) { + l2Messenger.setXDomainMessageSender(_sender); + return ""; } - /// forge-config: default.fuzz.runs = 10 - function testFork_verify_WithValue(uint256 _msgValue) public { - _msgValue = bound(_msgValue, 0, 2 ** 254); - deployAll(); - - orchestrateRelayMessage(_msgValue, messageId); - - bool verified = opISM.verify(new bytes(0), encodedMessage); - assertTrue(verified); - - assertEq(address(opISM).balance, 0); - assertEq(address(testRecipient).balance, _msgValue); - } - - /// forge-config: default.fuzz.runs = 10 - function testFork_verify_valueAlreadyClaimed(uint256 _msgValue) public { - _msgValue = bound(_msgValue, 0, 2 ** 254); - deployAll(); - - orchestrateRelayMessage(_msgValue, messageId); - - bool verified = opISM.verify(new bytes(0), encodedMessage); - assertTrue(verified); - - assertEq(address(opISM).balance, 0); - assertEq(address(testRecipient).balance, _msgValue); - - // send more value to the ISM - vm.deal(address(opISM), _msgValue); - - verified = opISM.verify(new bytes(0), encodedMessage); - // verified still true - assertTrue(verified); - - assertEq(address(opISM).balance, _msgValue); - // value which was already sent - assertEq(address(testRecipient).balance, _msgValue); - } - - function testFork_verify_tooMuchValue() public { - deployAll(); + /* ============ ISM.verify ============ */ + function test_verify_tooMuchValue() public { uint256 _msgValue = 2 ** 255 + 1; - vm.expectEmit(false, false, false, false, address(l2Messenger)); - emit FailedRelayedMessage(messageId); - orchestrateRelayMessage(_msgValue, messageId); - - bool verified = opISM.verify(new bytes(0), encodedMessage); - assertFalse(verified); - - assertEq(address(opISM).balance, 0); - assertEq(address(testRecipient).balance, 0); - } - - // sending over invalid message - function testFork_verify_RevertWhen_HyperlaneInvalidMessage() public { - deployAll(); - - orchestrateRelayMessage(0, messageId); - - bytes memory invalidMessage = MessageUtils.formatMessage( - HYPERLANE_VERSION, - uint8(0), - MAINNET_DOMAIN, - TypeCasts.addressToBytes32(address(this)), - OPTIMISM_DOMAIN, - TypeCasts.addressToBytes32(address(this)), // wrong recipient - testMessage + vm.expectRevert( + "AbstractMessageIdAuthorizedIsm: msg.value must be less than 2^255" ); - bool verified = opISM.verify(new bytes(0), invalidMessage); - assertFalse(verified); - } + _externalBridgeDestinationCall(_encodeHookData(messageId), _msgValue); - // invalid messageID in postDispatch - function testFork_verify_RevertWhen_InvalidOptimismMessageID() public { - deployAll(); - vm.selectFork(optimismFork); - - bytes memory invalidMessage = MessageUtils.formatMessage( - HYPERLANE_VERSION, - uint8(0), - MAINNET_DOMAIN, - TypeCasts.addressToBytes32(address(this)), - OPTIMISM_DOMAIN, - TypeCasts.addressToBytes32(address(this)), - testMessage - ); - bytes32 _messageId = Message.id(invalidMessage); - orchestrateRelayMessage(0, _messageId); + assertFalse(ism.isVerified(encodedMessage)); - bool verified = opISM.verify(new bytes(0), encodedMessage); - assertFalse(verified); + assertEq(address(ism).balance, 0); + assertEq(address(testRecipient).balance, 0); } /* ============ helper functions ============ */ - - function _encodeTestMessage() internal view returns (bytes memory) { - return - MessageUtils.formatMessage( - HYPERLANE_VERSION, - uint32(0), - MAINNET_DOMAIN, - TypeCasts.addressToBytes32(address(this)), - OPTIMISM_DOMAIN, - TypeCasts.addressToBytes32(address(testRecipient)), - testMessage - ); - } - - /// @dev from eth-optimism/contracts-bedrock/contracts/libraries/Hashing.sol - /// @notice Hashes a cross domain message based on the V1 (current) encoding. - /// @param _nonce Message nonce. - /// @param _sender Address of the sender of the message. - /// @param _target Address of the target of the message. - /// @param _value ETH value to send to the target. - /// @param _gasLimit Gas limit to use for the message. - /// @param _data Data to send with the message. - /// @return Hashed cross domain message. - function hashCrossDomainMessageV1( - uint256 _nonce, - address _sender, - address _target, - uint256 _value, - uint256 _gasLimit, - bytes memory _data - ) internal pure returns (bytes32) { - return - keccak256( - encodeCrossDomainMessageV1( - _nonce, - _sender, - _target, - _value, - _gasLimit, - _data - ) - ); - } - - /// @dev from eth-optimism/contracts-bedrock/contracts/libraries/Encoding.sol - /// @notice Encodes a cross domain message based on the V1 (current) encoding. - /// @param _nonce Message nonce. - /// @param _sender Address of the sender of the message. - /// @param _target Address of the target of the message. - /// @param _value ETH value to send to the target. - /// @param _gasLimit Gas limit to use for the message. - /// @param _data Data to send with the message. - /// @return Encoded cross domain message. - function encodeCrossDomainMessageV1( - uint256 _nonce, - address _sender, - address _target, - uint256 _value, - uint256 _gasLimit, - bytes memory _data - ) internal pure returns (bytes memory) { - return - abi.encodeWithSignature( - "relayMessage(uint256,address,address,uint256,uint256,bytes)", - _nonce, - _sender, - _target, - _value, - _gasLimit, - _data - ); - } - - /// @dev from eth-optimism/contracts-bedrock/contracts/libraries/Encoding.sol - /// @notice Adds a version number into the first two bytes of a message nonce. - /// @param _nonce Message nonce to encode into. - /// @param _version Version number to encode into the message nonce. - /// @return Message nonce with version encoded into the first two bytes. - function encodeVersionedNonce( - uint240 _nonce, - uint16 _version - ) internal pure returns (uint256) { - uint256 nonce; - assembly { - nonce := or(shl(240, _version), _nonce) - } - return nonce; - } - - /// @dev from eth-optimism/contracts-bedrock/contracts/libraries/Encoding.sol - /// @notice Pulls the version out of a version-encoded nonce. - /// @param _nonce Message nonce with version encoded into the first two bytes. - /// @return Nonce without encoded version. - /// @return Version of the message. - function decodeVersionedNonce( - uint256 _nonce - ) internal pure returns (uint240, uint16) { - uint240 nonce; - uint16 version; - assembly { - nonce := and( - _nonce, - 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - ) - version := shr(240, _nonce) - } - return (nonce, version); - } - - function orchestrateRelayMessage( - uint256 _msgValue, - bytes32 _messageId - ) internal { - vm.selectFork(optimismFork); - - bytes memory encodedHookData = abi.encodeCall( - AbstractMessageIdAuthorizedIsm.verifyMessageId, - (_messageId) - ); - - (uint240 nonce, uint16 version) = decodeVersionedNonce( - l2Messenger.messageNonce() - ); - uint256 versionedNonce = encodeVersionedNonce(nonce + 1, version); - - vm.deal( - AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS), - 2 ** 256 - 1 - ); - vm.prank(AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS)); - l2Messenger.relayMessage{value: _msgValue}( - versionedNonce, - address(opHook), - address(opISM), - _msgValue, - DEFAULT_GAS_LIMIT, - encodedHookData - ); - } }