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
pull/4542/head
Kunal Arora 2 months ago committed by GitHub
parent f36beb6831
commit c032b8ba27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 260
      solidity/test/isms/ArbL2ToL1Ism.t.sol
  2. 152
      solidity/test/isms/ERC5164ISM.t.sol
  3. 268
      solidity/test/isms/ExternalBridgeTest.sol
  4. 238
      solidity/test/isms/OPL2ToL1Ism.t.sol
  5. 529
      solidity/test/isms/OPStackIsm.t.sol

@ -1,8 +1,6 @@
// SPDX-License-Identifier: MIT or Apache-2.0 // SPDX-License-Identifier: MIT or Apache-2.0
pragma solidity ^0.8.13; pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {MessageUtils} from "./IsmTestUtils.sol"; import {MessageUtils} from "./IsmTestUtils.sol";
import {TestMailbox} from "../../contracts/test/TestMailbox.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 {ArbL2ToL1Ism} from "../../contracts/isms/hook/ArbL2ToL1Ism.sol";
import {MockArbBridge, MockArbSys} from "../../contracts/mock/MockArbBridge.sol"; import {MockArbBridge, MockArbSys} from "../../contracts/mock/MockArbBridge.sol";
import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
import {ExternalBridgeTest} from "./ExternalBridgeTest.sol";
contract ArbL2ToL1IsmTest is Test { contract ArbL2ToL1IsmTest is ExternalBridgeTest {
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_LEAF_INDEX = 40160;
uint256 internal constant MOCK_L2_BLOCK = 54220000; uint256 internal constant MOCK_L2_BLOCK = 54220000;
uint256 internal constant MOCK_L1_BLOCK = 6098300; uint256 internal constant MOCK_L1_BLOCK = 6098300;
@ -28,26 +22,14 @@ contract ArbL2ToL1IsmTest is Test {
0x0000000000000000000000000000000000000064; 0x0000000000000000000000000000000000000064;
MockArbBridge internal arbBridge; MockArbBridge internal arbBridge;
TestMailbox public l2Mailbox;
ArbL2ToL1Hook public hook;
ArbL2ToL1Ism public ism;
TestRecipient internal testRecipient; function setUp() public override {
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 // Arbitrum bridge mock setup
GAS_QUOTE = 120_000;
vm.etch(L2_ARBSYS_ADDRESS, address(new MockArbSys()).code); vm.etch(L2_ARBSYS_ADDRESS, address(new MockArbSys()).code);
testRecipient = new TestRecipient(); deployAll();
super.setUp();
encodedMessage = _encodeTestMessage();
messageId = Message.id(encodedMessage);
} }
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
@ -55,10 +37,10 @@ contract ArbL2ToL1IsmTest is Test {
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
function deployHook() public { function deployHook() public {
l2Mailbox = new TestMailbox(ARBITRUM_DOMAIN); originMailbox = new TestMailbox(ORIGIN_DOMAIN);
hook = new ArbL2ToL1Hook( hook = new ArbL2ToL1Hook(
address(l2Mailbox), address(originMailbox),
MAINNET_DOMAIN, DESTINATION_DOMAIN,
TypeCasts.addressToBytes32(address(ism)), TypeCasts.addressToBytes32(address(ism)),
L2_ARBSYS_ADDRESS, L2_ARBSYS_ADDRESS,
GAS_QUOTE GAS_QUOTE
@ -67,7 +49,6 @@ contract ArbL2ToL1IsmTest is Test {
function deployIsm() public { function deployIsm() public {
arbBridge = new MockArbBridge(); arbBridge = new MockArbBridge();
ism = new ArbL2ToL1Ism(address(arbBridge)); ism = new ArbL2ToL1Ism(address(arbBridge));
} }
@ -75,196 +56,39 @@ contract ArbL2ToL1IsmTest is Test {
deployIsm(); deployIsm();
deployHook(); deployHook();
arbBridge.setL2ToL1Sender(address(hook));
ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook))); ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook)));
} }
function test_postDispatch() public { /* ============ helper functions ============ */
deployAll();
bytes memory encodedHookData = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
(messageId)
);
l2Mailbox.updateLatestDispatchedId(messageId);
function _expectOriginExternalBridgeCall(
bytes memory _encodedHookData
) internal override {
vm.expectCall( vm.expectCall(
L2_ARBSYS_ADDRESS, L2_ARBSYS_ADDRESS,
abi.encodeCall( abi.encodeCall(
MockArbSys.sendTxToL1, 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 { function _encodeExternalDestinationBridgeCall(
deployAll(); address _from,
address _to,
bytes memory encodedHookData = abi.encodeCall( uint256 _msgValue,
AbstractMessageIdAuthorizedIsm.verifyMessageId, bytes32 _messageId
(messageId) ) internal override returns (bytes memory) {
); vm.deal(address(arbBridge), _msgValue);
return _encodeOutboxTx(_from, _to, _messageId, _msgValue);
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 { function _externalBridgeDestinationCall(
deployAll(); bytes memory _encodedHookData,
uint256 _msgValue
bytes memory encodedHookData = abi.encodeCall( ) internal override {
AbstractMessageIdAuthorizedIsm.verifyMessageId, vm.deal(address(arbBridge), _msgValue);
(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
arbBridge.executeTransaction( arbBridge.executeTransaction(
new bytes32[](0), new bytes32[](0),
MOCK_LEAF_INDEX, MOCK_LEAF_INDEX,
@ -273,16 +97,17 @@ contract ArbL2ToL1IsmTest is Test {
MOCK_L2_BLOCK, MOCK_L2_BLOCK,
MOCK_L1_BLOCK, MOCK_L1_BLOCK,
block.timestamp, block.timestamp,
0, _msgValue,
encodedHookData _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( function _encodeOutboxTx(
address _hook, address _hook,
@ -309,17 +134,4 @@ contract ArbL2ToL1IsmTest is Test {
encodedHookData 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
);
}
} }

@ -1,8 +1,6 @@
// SPDX-License-Identifier: MIT or Apache-2.0 // SPDX-License-Identifier: MIT or Apache-2.0
pragma solidity ^0.8.13; pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {LibBit} from "../../contracts/libs/LibBit.sol"; import {LibBit} from "../../contracts/libs/LibBit.sol";
import {Message} from "../../contracts/libs/Message.sol"; import {Message} from "../../contracts/libs/Message.sol";
import {MessageUtils} from "./IsmTestUtils.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 {TestMailbox} from "../../contracts/test/TestMailbox.sol";
import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
import {MockMessageDispatcher, MockMessageExecutor} from "../../contracts/mock/MockERC5164.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 LibBit for uint256;
using TypeCasts for address; using TypeCasts for address;
using Message for bytes; using Message for bytes;
@ -27,23 +26,8 @@ contract ERC5164IsmTest is Test {
IMessageDispatcher internal dispatcher; IMessageDispatcher internal dispatcher;
MockMessageExecutor internal executor; 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); address internal alice = address(0x1);
// req for most tests
bytes encodedMessage = _encodeTestMessage(0, address(testRecipient));
bytes32 messageId = encodedMessage.id();
event MessageDispatched( event MessageDispatched(
bytes32 indexed messageId, bytes32 indexed messageId,
address indexed from, address indexed from,
@ -56,19 +40,19 @@ contract ERC5164IsmTest is Test {
/// SETUP /// /// SETUP ///
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
function setUp() public { function setUp() public override {
dispatcher = new MockMessageDispatcher(); dispatcher = new MockMessageDispatcher();
executor = new MockMessageExecutor(); executor = new MockMessageExecutor();
testRecipient = new TestRecipient(); originMailbox = new TestMailbox(ORIGIN_DOMAIN);
originMailbox = new TestMailbox(TEST1_DOMAIN);
ism = new ERC5164Ism(address(executor)); ism = new ERC5164Ism(address(executor));
hook = new ERC5164Hook( hook = new ERC5164Hook(
address(originMailbox), address(originMailbox),
TEST2_DOMAIN, DESTINATION_DOMAIN,
address(ism).addressToBytes32(), address(ism).addressToBytes32(),
address(dispatcher) address(dispatcher)
); );
ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook))); ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook)));
super.setUp();
} }
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
@ -100,7 +84,7 @@ contract ERC5164IsmTest is Test {
vm.expectRevert("AbstractMessageIdAuthHook: invalid ISM"); vm.expectRevert("AbstractMessageIdAuthHook: invalid ISM");
hook = new ERC5164Hook( hook = new ERC5164Hook(
address(originMailbox), address(originMailbox),
TEST2_DOMAIN, DESTINATION_DOMAIN,
address(0).addressToBytes32(), address(0).addressToBytes32(),
address(dispatcher) address(dispatcher)
); );
@ -108,125 +92,83 @@ contract ERC5164IsmTest is Test {
vm.expectRevert("ERC5164Hook: invalid dispatcher"); vm.expectRevert("ERC5164Hook: invalid dispatcher");
hook = new ERC5164Hook( hook = new ERC5164Hook(
address(originMailbox), address(originMailbox),
TEST2_DOMAIN, DESTINATION_DOMAIN,
address(ism).addressToBytes32(), address(ism).addressToBytes32(),
address(0) address(0)
); );
} }
function test_postDispatch() public { function testTypes() public view {
bytes memory encodedHookData = abi.encodeCall( assertEq(hook.hookType(), uint8(IPostDispatchHook.Types.ID_AUTH_ISM));
AbstractMessageIdAuthorizedIsm.verifyMessageId, assertEq(ism.moduleType(), uint8(IInterchainSecurityModule.Types.NULL));
(messageId) }
);
originMailbox.updateLatestDispatchedId(messageId);
// 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)); vm.expectEmit(false, true, true, true, address(dispatcher));
emit MessageDispatched( emit MessageDispatched(
messageId, messageId,
address(hook), address(hook),
TEST2_DOMAIN, DESTINATION_DOMAIN,
address(ism), 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 { function test_verify_revertWhen_invalidMetadata() public override {
encodedMessage = MessageUtils.formatMessage( assertFalse(ism.verify(new bytes(0), encodedMessage));
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_postDispatch_RevertWhen_msgValueNotAllowed() public payable { function test_postDispatch_revertWhen_msgValueNotAllowed() public payable {
originMailbox.updateLatestDispatchedId(messageId); originMailbox.updateLatestDispatchedId(messageId);
vm.expectRevert("ERC5164Hook: no value allowed"); vm.expectRevert("ERC5164Hook: no value allowed");
hook.postDispatch{value: 1}(bytes(""), encodedMessage); hook.postDispatch{value: 1}(bytes(""), encodedMessage);
} }
/* ============ ISM.verifyMessageId ============ */ // override to omit direct external bridge call
function test_verify_revertsWhen_notAuthorizedHook() public override {
function test_verifyMessageId() public { vm.prank(alice);
vm.startPrank(address(executor));
ism.verifyMessageId(messageId);
assertTrue(ism.verifiedMessages(messageId).isBitSet(255));
vm.stopPrank();
}
function test_verifyMessageId_RevertWhen_NotAuthorized() public {
vm.startPrank(alice);
// needs to be called by the authorized hook contract on Ethereum
vm.expectRevert( vm.expectRevert(
"AbstractMessageIdAuthorizedIsm: sender is not the hook" "AbstractMessageIdAuthorizedIsm: sender is not the hook"
); );
ism.verifyMessageId(messageId); ism.verifyMessageId(messageId);
assertFalse(ism.isVerified(encodedMessage));
vm.stopPrank();
} }
/* ============ ISM.verify ============ */ // SKIP - duplicate of test_verify_revertWhen_invalidMetadata
function test_verify_revertsWhen_incorrectMessageId() public override {}
function test_verify() public { function test_verify_revertsWhen_invalidIsm() public override {}
vm.startPrank(address(executor));
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); function test_verify_msgValue_externalBridgeCall() public override {}
assertTrue(verified);
vm.stopPrank(); function test_verify_valueAlreadyClaimed(uint256) public override {}
}
function test_verify_RevertWhen_InvalidMessage() public { /* ============ helper functions ============ */
vm.startPrank(address(executor));
function _externalBridgeDestinationCall(
bytes memory _encodedHookData,
uint256 _msgValue
) internal override {
vm.prank(address(executor));
ism.verifyMessageId(messageId); 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 _encodeExternalDestinationBridgeCall(
address _from,
function _encodeTestMessage( address _to,
uint32 _msgCount, uint256 _msgValue,
address _receipient bytes32 _messageId
) internal view returns (bytes memory) { ) internal override returns (bytes memory) {
return if (_from == address(hook)) {
MessageUtils.formatMessage( vm.prank(address(executor));
VERSION, ism.verifyMessageId{value: _msgValue}(messageId);
_msgCount, }
TEST1_DOMAIN,
TypeCasts.addressToBytes32(address(this)),
TEST2_DOMAIN,
TypeCasts.addressToBytes32(_receipient),
testMessage
);
} }
} }

@ -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) {}
}

@ -1,8 +1,6 @@
// SPDX-License-Identifier: MIT or Apache-2.0 // SPDX-License-Identifier: MIT or Apache-2.0
pragma solidity ^0.8.13; pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {Message} from "../../contracts/libs/Message.sol"; import {Message} from "../../contracts/libs/Message.sol";
import {MessageUtils} from "./IsmTestUtils.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 {MockOptimismMessenger, MockOptimismPortal} from "../../contracts/mock/MockOptimism.sol";
import {OPL2ToL1Hook} from "../../contracts/hooks/OPL2ToL1Hook.sol"; import {OPL2ToL1Hook} from "../../contracts/hooks/OPL2ToL1Hook.sol";
import {OPL2ToL1Ism} from "../../contracts/isms/hook/OPL2ToL1Ism.sol"; import {OPL2ToL1Ism} from "../../contracts/isms/hook/OPL2ToL1Ism.sol";
import {ExternalBridgeTest} from "./ExternalBridgeTest.sol";
contract OPL2ToL1IsmTest is Test { contract OPL2ToL1IsmTest is ExternalBridgeTest {
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;
address internal constant L2_MESSENGER_ADDRESS = address internal constant L2_MESSENGER_ADDRESS =
0x4200000000000000000000000000000000000007; 0x4200000000000000000000000000000000000007;
uint256 internal constant MOCK_NONCE = 0; 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; MockOptimismPortal internal portal;
MockOptimismMessenger internal l1Messenger; MockOptimismMessenger internal l1Messenger;
OPL2ToL1Hook public hook;
OPL2ToL1Ism public ism;
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
/// SETUP /// /// SETUP ///
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
function setUp() public { function setUp() public override {
// Optimism messenger mock setup // Optimism messenger mock setup
GAS_QUOTE = 120_000;
vm.etch( vm.etch(
L2_MESSENGER_ADDRESS, L2_MESSENGER_ADDRESS,
address(new MockOptimismMessenger()).code address(new MockOptimismMessenger()).code
); );
testRecipient = new TestRecipient(); deployAll();
super.setUp();
encodedMessage = _encodeTestMessage();
messageId = Message.id(encodedMessage);
} }
function deployHook() public { function deployHook() public {
l2Mailbox = new TestMailbox(OPTIMISM_DOMAIN); originMailbox = new TestMailbox(ORIGIN_DOMAIN);
hook = new OPL2ToL1Hook( hook = new OPL2ToL1Hook(
address(l2Mailbox), address(originMailbox),
MAINNET_DOMAIN, DESTINATION_DOMAIN,
TypeCasts.addressToBytes32(address(ism)), TypeCasts.addressToBytes32(address(ism)),
L2_MESSENGER_ADDRESS, L2_MESSENGER_ADDRESS,
GAS_QUOTE uint32(GAS_QUOTE)
); );
} }
@ -85,197 +67,58 @@ contract OPL2ToL1IsmTest is Test {
ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook))); ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook)));
} }
function test_postDispatch() public { /* ============ helper functions ============ */
deployAll();
bytes memory encodedHookData = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
(messageId)
);
l2Mailbox.updateLatestDispatchedId(messageId);
function _expectOriginExternalBridgeCall(
bytes memory _encodedHookData
) internal override {
vm.expectCall( vm.expectCall(
L2_MESSENGER_ADDRESS, L2_MESSENGER_ADDRESS,
abi.encodeCall( abi.encodeCall(
ICrossDomainMessenger.sendMessage, 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 { function _encodeExternalDestinationBridgeCall(
deployAll(); address,
/*_from*/
vm.expectRevert(); address _to,
ism.verify(new bytes(0), encodedMessage); 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 { function _setExternalOriginSender(
deployAll(); address _sender
) internal override returns (bytes memory) {
bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx( l1Messenger.setXDomainMessageSender(_sender);
address(this), return "AbstractMessageIdAuthorizedIsm: sender is not the hook";
0,
messageId
);
vm.expectRevert(); // evmRevert in MockOptimismPortal
ism.verify(encodedWithdrawalTx, encodedMessage);
} }
function test_verify_revertsWhen_incorrectMessageId() public { function _externalBridgeDestinationCall(
deployAll(); bytes memory,
/*_encodedHookData*/
bytes32 incorrectMessageId = keccak256("incorrect message id"); uint256 _msgValue
) internal override {
bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx( vm.deal(address(portal), _msgValue);
address(this),
0,
incorrectMessageId
);
// through portal call
vm.expectRevert("OPL2ToL1Ism: invalid message id");
ism.verify(encodedWithdrawalTx, encodedMessage);
// through statefulVerify
IOptimismPortal.WithdrawalTransaction IOptimismPortal.WithdrawalTransaction
memory withdrawal = IOptimismPortal.WithdrawalTransaction({ memory withdrawal = IOptimismPortal.WithdrawalTransaction({
nonce: MOCK_NONCE, nonce: MOCK_NONCE,
sender: L2_MESSENGER_ADDRESS, sender: L2_MESSENGER_ADDRESS,
target: address(l1Messenger), target: address(l1Messenger),
value: 0, value: _msgValue,
gasLimit: uint256(GAS_QUOTE), gasLimit: uint256(GAS_QUOTE),
data: _encodeMessengerCalldata( data: _encodeMessengerCalldata(
address(ism), address(ism),
0, _msgValue,
incorrectMessageId messageId
) )
}); });
portal.finalizeWithdrawalTransaction(withdrawal); 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( function _encodeMessengerCalldata(
@ -283,10 +126,7 @@ contract OPL2ToL1IsmTest is Test {
uint256 _value, uint256 _value,
bytes32 _messageId bytes32 _messageId
) internal view returns (bytes memory) { ) internal view returns (bytes memory) {
bytes memory encodedHookData = abi.encodeCall( bytes memory encodedHookData = _encodeHookData(_messageId);
AbstractMessageIdAuthorizedIsm.verifyMessageId,
(_messageId)
);
return return
abi.encodeCall( abi.encodeCall(

@ -1,13 +1,14 @@
// SPDX-License-Identifier: MIT or Apache-2.0 // SPDX-License-Identifier: MIT or Apache-2.0
pragma solidity ^0.8.13; 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 {LibBit} from "../../contracts/libs/LibBit.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.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 {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol";
import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol";
import {MockOptimismMessenger} from "../../contracts/mock/MockOptimism.sol";
import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; import {TestMailbox} from "../../contracts/test/TestMailbox.sol";
import {Message} from "../../contracts/libs/Message.sol"; import {Message} from "../../contracts/libs/Message.sol";
import {MessageUtils} from "./IsmTestUtils.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 {NotCrossChainCall} from "@openzeppelin/contracts/crosschain/errors.sol";
import {AddressAliasHelper} from "@eth-optimism/contracts/standards/AddressAliasHelper.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 LibBit for uint256;
using TypeCasts for address; using TypeCasts for address;
using MessageUtils for bytes; using MessageUtils for bytes;
uint256 internal mainnetFork;
uint256 internal optimismFork;
address internal constant L1_MESSENGER_ADDRESS = address internal constant L1_MESSENGER_ADDRESS =
0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1; 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1;
address internal constant L1_CANNONICAL_CHAIN = address internal constant L1_CANNONICAL_CHAIN =
@ -36,36 +35,12 @@ contract OPStackIsmTest is Test {
0x4200000000000000000000000000000000000007; 0x4200000000000000000000000000000000000007;
uint8 internal constant OPTIMISM_VERSION = 0; uint8 internal constant OPTIMISM_VERSION = 0;
uint8 internal constant HYPERLANE_VERSION = 1;
uint256 internal constant DEFAULT_GAS_LIMIT = 1_920_000; uint256 internal constant DEFAULT_GAS_LIMIT = 1_920_000;
address internal alice = address(0x1); address internal alice = address(0x1);
ICrossDomainMessenger internal l1Messenger; MockOptimismMessenger internal l1Messenger;
IL2CrossDomainMessenger internal l2Messenger; MockOptimismMessenger 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
);
event RelayedMessage(bytes32 indexed msgHash); event RelayedMessage(bytes32 indexed msgHash);
@ -73,469 +48,145 @@ contract OPStackIsmTest is Test {
event ReceivedMessage(bytes32 indexed messageId); event ReceivedMessage(bytes32 indexed messageId);
function setUp() public { function setUp() public override {
// block numbers to fork from, chain data is cached to ../../forge-cache/ GAS_QUOTE = 0;
mainnetFork = vm.createFork(vm.rpcUrl("mainnet"), 18_992_500);
optimismFork = vm.createFork(vm.rpcUrl("optimism"), 114_696_811);
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(); deployAll();
messageId = Message.id(encodedMessage); super.setUp();
} }
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
/// SETUP /// /// SETUP ///
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
function deployOptimismHook() public { function deployHook() public {
vm.selectFork(mainnetFork); originMailbox = new TestMailbox(ORIGIN_DOMAIN);
hook = new OPStackHook(
l1Messenger = ICrossDomainMessenger(L1_MESSENGER_ADDRESS); address(originMailbox),
l1Mailbox = new TestMailbox(MAINNET_DOMAIN); DESTINATION_DOMAIN,
TypeCasts.addressToBytes32(address(ism)),
opHook = new OPStackHook(
address(l1Mailbox),
OPTIMISM_DOMAIN,
TypeCasts.addressToBytes32(address(opISM)),
L1_MESSENGER_ADDRESS L1_MESSENGER_ADDRESS
); );
vm.makePersistent(address(opHook));
} }
function deployOPStackIsm() public { function deployIsm() public {
vm.selectFork(optimismFork); ism = new OPStackIsm(L2_MESSENGER_ADDRESS);
l2Messenger = IL2CrossDomainMessenger(L2_MESSENGER_ADDRESS);
opISM = new OPStackIsm(L2_MESSENGER_ADDRESS);
vm.makePersistent(address(opISM));
} }
function deployAll() public { function deployAll() public {
deployOPStackIsm(); deployIsm();
deployOptimismHook(); deployHook();
vm.selectFork(optimismFork); ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook)));
l2Messenger.setXDomainMessageSender(address(hook));
opISM.setAuthorizedHook(TypeCasts.addressToBytes32(address(opHook)));
// for sending value
vm.deal(
AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS),
2 ** 255
);
} }
/////////////////////////////////////////////////////////////////// function test_verify_revertWhen_invalidMetadata() public override {
/// FORK TESTS /// assertFalse(ism.verify(new bytes(0), encodedMessage));
///////////////////////////////////////////////////////////////////
/* ============ hook.quoteDispatch ============ */
function testFork_quoteDispatch() public {
deployAll();
vm.selectFork(mainnetFork);
assertEq(opHook.quoteDispatch(testMetadata, encodedMessage), 0);
} }
/* ============ hook.postDispatch ============ */ function test_verify_revertsWhen_incorrectMessageId() public override {
bytes32 incorrectMessageId = keccak256("incorrect message id");
function testFork_postDispatch() public { _externalBridgeDestinationCall(_encodeHookData(incorrectMessageId), 0);
deployAll(); assertFalse(ism.isVerified(testMessage));
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);
} }
function testFork_postDispatch_RevertWhen_ChainIDNotSupported() public { /* ============ helper functions ============ */
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
);
l1Mailbox.updateLatestDispatchedId(Message.id(message)); function _expectOriginExternalBridgeCall(
vm.expectRevert( bytes memory _encodedHookData
"AbstractMessageIdAuthHook: invalid destination domain" ) 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 { function _externalBridgeDestinationCall(
deployAll(); bytes memory _encodedHookData,
uint256 _msgValue
vm.selectFork(mainnetFork); ) internal override {
vm.deal(L2_MESSENGER_ADDRESS, _msgValue);
vm.deal(address(this), uint256(2 ** 255 + 1)); l2Messenger.relayMessage(
bytes memory excessValueMetadata = StandardHookMetadata 0,
.overrideMsgValue(uint256(2 ** 255 + 1)); address(hook),
address(ism),
l1Mailbox.updateLatestDispatchedId(messageId); _msgValue,
vm.expectRevert( uint32(GAS_QUOTE),
"AbstractMessageIdAuthHook: msgValue must be less than 2 ** 255" _encodedHookData
); );
opHook.postDispatch(excessValueMetadata, encodedMessage);
} }
function testFork_postDispatch_RevertWhen_NotLastDispatchedMessage() function _encodeExternalDestinationBridgeCall(
public address _from,
{ address _to,
deployAll(); uint256 _msgValue,
bytes32 _messageId
vm.selectFork(mainnetFork); ) internal pure override returns (bytes memory) {
return new bytes(0);
vm.expectRevert(
"AbstractMessageIdAuthHook: message not latest dispatched"
);
opHook.postDispatch(testMetadata, encodedMessage);
} }
/* ============ ISM.verifyMessageId ============ */ // SKIP - no external bridge call
function test_verifyMessageId_externalBridgeCall() public override {}
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);
bytes32 versionedHash = hashCrossDomainMessageV1( function test_verify_msgValue_externalBridgeCall() public override {}
versionedNonce,
address(opHook),
address(opISM),
0,
DEFAULT_GAS_LIMIT,
encodedHookData
);
vm.startPrank(
AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS)
);
vm.expectEmit(true, false, false, false, address(opISM)); function test_verify_revertsWhen_invalidIsm() public override {}
emit ReceivedMessage(messageId);
vm.expectEmit(true, false, false, false, L2_MESSENGER_ADDRESS); /* ============ ISM.verifyMessageId ============ */
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);
function test_verify_revertsWhen_notAuthorizedHook() public override {
// needs to be called by the canonical messenger on Optimism // needs to be called by the canonical messenger on Optimism
vm.expectRevert(NotCrossChainCall.selector); vm.expectRevert(NotCrossChainCall.selector);
opISM.verifyMessageId(messageId); ism.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);
vm.startPrank(L2_MESSENGER_ADDRESS); vm.startPrank(L2_MESSENGER_ADDRESS);
_setExternalOriginSender(address(this));
// needs to be called by the authorized hook contract on Ethereum // needs to be called by the authorized hook contract on Ethereum
vm.expectRevert( vm.expectRevert(
"AbstractMessageIdAuthorizedIsm: sender is not the hook" "AbstractMessageIdAuthorizedIsm: sender is not the hook"
); );
opISM.verifyMessageId(messageId); ism.verifyMessageId(messageId);
} }
/* ============ ISM.verify ============ */ function _setExternalOriginSender(
address _sender
function testFork_verify() public { ) internal override returns (bytes memory) {
deployAll(); l2Messenger.setXDomainMessageSender(_sender);
return "";
vm.selectFork(optimismFork);
orchestrateRelayMessage(0, messageId);
bool verified = opISM.verify(new bytes(0), encodedMessage);
assertTrue(verified);
} }
/// forge-config: default.fuzz.runs = 10 /* ============ ISM.verify ============ */
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();
function test_verify_tooMuchValue() public {
uint256 _msgValue = 2 ** 255 + 1; uint256 _msgValue = 2 ** 255 + 1;
vm.expectEmit(false, false, false, false, address(l2Messenger)); vm.expectRevert(
emit FailedRelayedMessage(messageId); "AbstractMessageIdAuthorizedIsm: msg.value must be less than 2^255"
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
); );
bool verified = opISM.verify(new bytes(0), invalidMessage); _externalBridgeDestinationCall(_encodeHookData(messageId), _msgValue);
assertFalse(verified);
}
// invalid messageID in postDispatch assertFalse(ism.isVerified(encodedMessage));
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);
bool verified = opISM.verify(new bytes(0), encodedMessage); assertEq(address(ism).balance, 0);
assertFalse(verified); assertEq(address(testRecipient).balance, 0);
} }
/* ============ helper functions ============ */ /* ============ 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
);
}
} }

Loading…
Cancel
Save