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
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
);
}
}

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

@ -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
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(

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

Loading…
Cancel
Save