feat: implement `OPL2ToL1Ism` contracts (#4319)
### Description Implementing the `OPL2ToL1Hook` and OPL2ToL1Ism` contracts for using the Optimism native bridge for sending messages from the L2 to L1. - Optimism needs double call encoding for the direct portal call because the portal itself calls the l1Messenger which then directs it to the ism. ### Drive-by changes None ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/1346 ### Backward compatibility Yes ### Testing Unit testing with mocks for OP contractspull/4335/head
parent
cc8624d687
commit
6f377f1743
@ -0,0 +1,5 @@ |
||||
--- |
||||
'@hyperlane-xyz/core': minor |
||||
--- |
||||
|
||||
Added hook/ism for using the Optimism native bridge for L2->L1 calls |
@ -0,0 +1,84 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
/*@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@ HYPERLANE @@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@*/ |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {AbstractPostDispatchHook, AbstractMessageIdAuthHook} from "./libs/AbstractMessageIdAuthHook.sol"; |
||||
import {StandardHookMetadata} from "./libs/StandardHookMetadata.sol"; |
||||
import {TypeCasts} from "../libs/TypeCasts.sol"; |
||||
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; |
||||
|
||||
// ============ External Imports ============ |
||||
import {ICrossDomainMessenger} from "../interfaces/optimism/ICrossDomainMessenger.sol"; |
||||
|
||||
/** |
||||
* @title OPL2ToL1Hook |
||||
* @notice Message hook to inform the OPL2ToL1Ism of messages published through |
||||
* the native Optimism bridge. |
||||
* @notice This works only for L2 -> L1 messages and has the 7 day delay as specified by the OptimismPortal contract. |
||||
*/ |
||||
contract OPL2ToL1Hook is AbstractMessageIdAuthHook { |
||||
using StandardHookMetadata for bytes; |
||||
|
||||
// ============ Constants ============ |
||||
|
||||
// precompile contract on L2 for sending messages to L1 |
||||
ICrossDomainMessenger public immutable l2Messenger; |
||||
// Immutable quote amount |
||||
uint32 public immutable GAS_QUOTE; |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
constructor( |
||||
address _mailbox, |
||||
uint32 _destinationDomain, |
||||
bytes32 _ism, |
||||
address _l2Messenger, |
||||
uint32 _gasQuote |
||||
) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) { |
||||
GAS_QUOTE = _gasQuote; |
||||
l2Messenger = ICrossDomainMessenger(_l2Messenger); |
||||
} |
||||
|
||||
/// @inheritdoc IPostDispatchHook |
||||
function hookType() external pure override returns (uint8) { |
||||
return uint8(IPostDispatchHook.Types.OP_L2_TO_L1); |
||||
} |
||||
|
||||
/// @inheritdoc AbstractPostDispatchHook |
||||
function _quoteDispatch( |
||||
bytes calldata, |
||||
bytes calldata |
||||
) internal view override returns (uint256) { |
||||
return GAS_QUOTE; |
||||
} |
||||
|
||||
// ============ Internal functions ============ |
||||
|
||||
/// @inheritdoc AbstractMessageIdAuthHook |
||||
function _sendMessageId( |
||||
bytes calldata metadata, |
||||
bytes memory payload |
||||
) internal override { |
||||
require( |
||||
msg.value >= metadata.msgValue(0) + GAS_QUOTE, |
||||
"OPL2ToL1Hook: insufficient msg.value" |
||||
); |
||||
l2Messenger.sendMessage{value: metadata.msgValue(0)}( |
||||
TypeCasts.bytes32ToAddress(ism), |
||||
payload, |
||||
GAS_QUOTE |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
pragma solidity ^0.8.0; |
||||
|
||||
// author: OP Labs |
||||
// copied from https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts-bedrock |
||||
interface IOptimismPortal { |
||||
/// @notice Struct representing a withdrawal transaction. |
||||
/// @custom:field nonce Nonce of the withdrawal transaction |
||||
/// @custom:field sender Address of the sender of the transaction. |
||||
/// @custom:field target Address of the recipient of the transaction. |
||||
/// @custom:field value Value to send to the recipient. |
||||
/// @custom:field gasLimit Gas limit of the transaction. |
||||
/// @custom:field data Data of the transaction. |
||||
struct WithdrawalTransaction { |
||||
uint256 nonce; |
||||
address sender; |
||||
address target; |
||||
uint256 value; |
||||
uint256 gasLimit; |
||||
bytes data; |
||||
} |
||||
|
||||
/// @notice Finalizes a withdrawal transaction. |
||||
/// @param _tx Withdrawal transaction to finalize. |
||||
function finalizeWithdrawalTransaction( |
||||
WithdrawalTransaction memory _tx |
||||
) external; |
||||
} |
@ -0,0 +1,109 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
/*@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@ HYPERLANE @@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@*/ |
||||
|
||||
// ============ Internal Imports ============ |
||||
|
||||
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; |
||||
import {TypeCasts} from "../../libs/TypeCasts.sol"; |
||||
import {Message} from "../../libs/Message.sol"; |
||||
import {OPL2ToL1Metadata} from "../../libs/OPL2ToL1Metadata.sol"; |
||||
import {AbstractMessageIdAuthorizedIsm} from "./AbstractMessageIdAuthorizedIsm.sol"; |
||||
|
||||
// ============ External Imports ============ |
||||
|
||||
import {ICrossDomainMessenger} from "../../interfaces/optimism/ICrossDomainMessenger.sol"; |
||||
import {IOptimismPortal} from "../../interfaces/optimism/IOptimismPortal.sol"; |
||||
import {CrossChainEnabledOptimism} from "@openzeppelin/contracts/crosschain/optimism/CrossChainEnabledOptimism.sol"; |
||||
import {Address} from "@openzeppelin/contracts/utils/Address.sol"; |
||||
|
||||
/** |
||||
* @title OPL2ToL1Ism |
||||
* @notice Uses the native Optimism bridge to verify interchain messages from L2 to L1. |
||||
*/ |
||||
contract OPL2ToL1Ism is |
||||
CrossChainEnabledOptimism, |
||||
AbstractMessageIdAuthorizedIsm |
||||
{ |
||||
using TypeCasts for address; |
||||
using Message for bytes; |
||||
using OPL2ToL1Metadata for bytes; |
||||
|
||||
// ============ Constants ============ |
||||
|
||||
// module type for the ISM |
||||
uint8 public constant moduleType = |
||||
uint8(IInterchainSecurityModule.Types.OP_L2_TO_L1); |
||||
// OptimismPortal contract on L1 to finalize withdrawal from L1 |
||||
IOptimismPortal public immutable portal; |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
constructor(address _messenger) CrossChainEnabledOptimism(_messenger) { |
||||
address _portal = ICrossDomainMessenger(_messenger).PORTAL(); |
||||
require( |
||||
Address.isContract(_portal), |
||||
"OPL2ToL1Ism: invalid OptimismPortal contract" |
||||
); |
||||
portal = IOptimismPortal(_portal); |
||||
} |
||||
|
||||
// ============ External Functions ============ |
||||
|
||||
/// @inheritdoc IInterchainSecurityModule |
||||
function verify( |
||||
bytes calldata metadata, |
||||
bytes calldata message |
||||
) external override returns (bool) { |
||||
bool verified = isVerified(message); |
||||
if (!verified) { |
||||
_verifyWithPortalCall(metadata, message); |
||||
} |
||||
releaseValueToRecipient(message); |
||||
return true; |
||||
} |
||||
|
||||
// ============ Internal function ============ |
||||
|
||||
/** |
||||
* @notice Verify message directly using the portal.finalizeWithdrawal function. |
||||
* @dev This is a fallback in case the message is not verified by the stateful verify function first. |
||||
*/ |
||||
function _verifyWithPortalCall( |
||||
bytes calldata metadata, |
||||
bytes calldata message |
||||
) internal { |
||||
require( |
||||
metadata.checkCalldataLength(), |
||||
"OPL2ToL1Ism: invalid data length" |
||||
); |
||||
require( |
||||
metadata.messageId() == message.id(), |
||||
"OPL2ToL1Ism: invalid message id" |
||||
); |
||||
|
||||
IOptimismPortal.WithdrawalTransaction memory withdrawal = abi.decode( |
||||
metadata, |
||||
(IOptimismPortal.WithdrawalTransaction) |
||||
); |
||||
// if the finalizeWithdrawalTransaction call is successful, the message is verified |
||||
portal.finalizeWithdrawalTransaction(withdrawal); |
||||
} |
||||
|
||||
/// @inheritdoc AbstractMessageIdAuthorizedIsm |
||||
function _isAuthorized() internal view override returns (bool) { |
||||
return |
||||
_crossChainSender() == TypeCasts.bytes32ToAddress(authorizedHook); |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
/** |
||||
* @title Hyperlane OPL2ToL1Metadata Library |
||||
* @notice Library for formatted metadata used by OPL2ToL1Ism |
||||
*/ |
||||
library OPL2ToL1Metadata { |
||||
// bottom offset to the start of message id in the metadata |
||||
uint256 private constant MESSAGE_ID_OFFSET = 88; |
||||
// from IOptimismPortal.WithdrawalTransaction |
||||
// Σ { |
||||
// nonce = 32 bytes |
||||
// PADDING + sender = 32 bytes |
||||
// PADDING + target = 32 bytes |
||||
// value = 32 bytes |
||||
// gasLimit = 32 bytes |
||||
// _data |
||||
// OFFSET = 32 bytes |
||||
// LENGTH = 32 bytes |
||||
// } = 252 bytes |
||||
uint256 private constant FIXED_METADATA_LENGTH = 252; |
||||
// metadata here is double encoded call relayMessage(..., verifyMessageId) |
||||
// Σ { |
||||
// _selector = 4 bytes |
||||
// _nonce = 32 bytes |
||||
// PADDING + _sender = 32 bytes |
||||
// PADDING + _target = 32 bytes |
||||
// _value = 32 bytes |
||||
// _minGasLimit = 32 bytes |
||||
// _data |
||||
// OFFSET = 32 bytes |
||||
// LENGTH = 32 bytes |
||||
// PADDING + verifyMessageId = 64 bytes |
||||
// } = 292 bytes |
||||
uint256 private constant MESSENGER_CALLDATA_LENGTH = 292; |
||||
|
||||
/** |
||||
* @notice Returns the message ID. |
||||
* @param _metadata OptimismPortal.WithdrawalTransaction encoded calldata |
||||
* @return ID of `_metadata` |
||||
*/ |
||||
function messageId( |
||||
bytes calldata _metadata |
||||
) internal pure returns (bytes32) { |
||||
uint256 metadataLength = _metadata.length; |
||||
return |
||||
bytes32( |
||||
_metadata[metadataLength - MESSAGE_ID_OFFSET:metadataLength - |
||||
MESSAGE_ID_OFFSET + |
||||
32] |
||||
); |
||||
} |
||||
|
||||
function checkCalldataLength( |
||||
bytes calldata _metadata |
||||
) internal pure returns (bool) { |
||||
return |
||||
_metadata.length == |
||||
MESSENGER_CALLDATA_LENGTH + FIXED_METADATA_LENGTH; |
||||
} |
||||
} |
@ -0,0 +1,61 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
pragma solidity ^0.8.0; |
||||
|
||||
import {CallLib} from "../middleware/libs/Call.sol"; |
||||
import {TypeCasts} from "../libs/TypeCasts.sol"; |
||||
import {ICrossDomainMessenger} from "../interfaces/optimism/ICrossDomainMessenger.sol"; |
||||
import {IOptimismPortal} from "../interfaces/optimism/IOptimismPortal.sol"; |
||||
|
||||
// for both L1 and L2 |
||||
contract MockOptimismMessenger is ICrossDomainMessenger { |
||||
address public xDomainMessageSender; |
||||
address public PORTAL; |
||||
|
||||
function sendMessage( |
||||
address _target, |
||||
bytes calldata _message, |
||||
uint32 _gasLimit |
||||
) external payable {} |
||||
|
||||
function relayMessage( |
||||
uint256 /*_nonce*/, |
||||
address /*_sender*/, |
||||
address _target, |
||||
uint256 _value, |
||||
uint256 /*_minGasLimit*/, |
||||
bytes calldata _message |
||||
) external payable { |
||||
CallLib.Call memory call = CallLib.Call( |
||||
TypeCasts.addressToBytes32(_target), |
||||
_value, |
||||
_message |
||||
); |
||||
CallLib.call(call); |
||||
} |
||||
|
||||
function OTHER_MESSENGER() external view returns (address) {} |
||||
|
||||
function setXDomainMessageSender(address _sender) external { |
||||
xDomainMessageSender = _sender; |
||||
} |
||||
|
||||
function setPORTAL(address _portal) external { |
||||
PORTAL = _portal; |
||||
} |
||||
} |
||||
|
||||
// mock deployment on L1 |
||||
contract MockOptimismPortal is IOptimismPortal { |
||||
error WithdrawalTransactionFailed(); |
||||
|
||||
function finalizeWithdrawalTransaction( |
||||
WithdrawalTransaction memory _tx |
||||
) external { |
||||
CallLib.Call memory call = CallLib.Call( |
||||
TypeCasts.addressToBytes32(_tx.target), |
||||
_tx.value, |
||||
_tx.data |
||||
); |
||||
CallLib.call(call); |
||||
} |
||||
} |
@ -0,0 +1,320 @@ |
||||
// 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"; |
||||
import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; |
||||
import {IOptimismPortal} from "../../contracts/interfaces/optimism/IOptimismPortal.sol"; |
||||
import {ICrossDomainMessenger} from "../../contracts/interfaces/optimism/ICrossDomainMessenger.sol"; |
||||
import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; |
||||
import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; |
||||
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"; |
||||
|
||||
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; |
||||
|
||||
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 { |
||||
// Optimism messenger mock setup |
||||
vm.etch( |
||||
L2_MESSENGER_ADDRESS, |
||||
address(new MockOptimismMessenger()).code |
||||
); |
||||
|
||||
testRecipient = new TestRecipient(); |
||||
|
||||
encodedMessage = _encodeTestMessage(); |
||||
messageId = Message.id(encodedMessage); |
||||
} |
||||
|
||||
function deployHook() public { |
||||
l2Mailbox = new TestMailbox(OPTIMISM_DOMAIN); |
||||
hook = new OPL2ToL1Hook( |
||||
address(l2Mailbox), |
||||
MAINNET_DOMAIN, |
||||
TypeCasts.addressToBytes32(address(ism)), |
||||
L2_MESSENGER_ADDRESS, |
||||
GAS_QUOTE |
||||
); |
||||
} |
||||
|
||||
function deployIsm() public { |
||||
l1Messenger = new MockOptimismMessenger(); |
||||
portal = new MockOptimismPortal(); |
||||
l1Messenger.setPORTAL(address(portal)); |
||||
|
||||
ism = new OPL2ToL1Ism(address(l1Messenger)); |
||||
} |
||||
|
||||
function deployAll() public { |
||||
deployIsm(); |
||||
deployHook(); |
||||
|
||||
l1Messenger.setXDomainMessageSender(address(hook)); |
||||
ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook))); |
||||
} |
||||
|
||||
function test_postDispatch() public { |
||||
deployAll(); |
||||
|
||||
bytes memory encodedHookData = abi.encodeCall( |
||||
AbstractMessageIdAuthorizedIsm.verifyMessageId, |
||||
(messageId) |
||||
); |
||||
|
||||
l2Mailbox.updateLatestDispatchedId(messageId); |
||||
|
||||
vm.expectCall( |
||||
L2_MESSENGER_ADDRESS, |
||||
abi.encodeCall( |
||||
ICrossDomainMessenger.sendMessage, |
||||
(address(ism), encodedHookData, 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 test_verify_revertsWhen_invalidIsm() public { |
||||
deployAll(); |
||||
|
||||
bytes memory encodedWithdrawalTx = _encodeFinalizeWithdrawalTx( |
||||
address(this), |
||||
0, |
||||
messageId |
||||
); |
||||
|
||||
vm.expectRevert(); // evmRevert in MockOptimismPortal |
||||
ism.verify(encodedWithdrawalTx, encodedMessage); |
||||
} |
||||
|
||||
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 |
||||
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, |
||||
incorrectMessageId |
||||
) |
||||
}); |
||||
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( |
||||
address _ism, |
||||
uint256 _value, |
||||
bytes32 _messageId |
||||
) internal view returns (bytes memory) { |
||||
bytes memory encodedHookData = abi.encodeCall( |
||||
AbstractMessageIdAuthorizedIsm.verifyMessageId, |
||||
(_messageId) |
||||
); |
||||
|
||||
return |
||||
abi.encodeCall( |
||||
ICrossDomainMessenger.relayMessage, |
||||
( |
||||
MOCK_NONCE, |
||||
address(hook), |
||||
_ism, |
||||
_value, |
||||
uint256(GAS_QUOTE), |
||||
encodedHookData |
||||
) |
||||
); |
||||
} |
||||
|
||||
function _encodeFinalizeWithdrawalTx( |
||||
address _ism, |
||||
uint256 _value, |
||||
bytes32 _messageId |
||||
) internal view returns (bytes memory) { |
||||
return |
||||
abi.encode( |
||||
MOCK_NONCE, |
||||
L2_MESSENGER_ADDRESS, |
||||
l1Messenger, |
||||
_value, |
||||
uint256(GAS_QUOTE), |
||||
_encodeMessengerCalldata(_ism, _value, _messageId) |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue