feat: support Arbitrum L2->L1 post dispatch hook (#3853)
### Description - Contract support for the enabling postDispatch hook with the Arbitrum nitro bridge from L2 to L1 - asynchronously via executeTransaction call to verifyMessageId first and then the relayer calling the verify message with no metadata (note: this supports msg.value) - synchronously via a single verify call which in turn calls executeTransaction on outbox to gets the message verified in the verifyMessageId (note: this doesn't support msg.value as ism.verify isn't payable) - Added a script for deploying the hook and ISM since the sdk doesn't support it yet. ### Drive-by changes - changing the type from "rate_limited_hook" to "RATE_LIMITED" to maintain consistency ### Related issues - fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2846 ### Backward compatibility Yes ### Testing Unit and e2e with arbitrumsepolia->sepoliapull/4093/head
parent
9cff8c2d3c
commit
f733379488
@ -0,0 +1,82 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
/*@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@ HYPERLANE @@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@*/ |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {AbstractPostDispatchHook} from "./libs/AbstractMessageIdAuthHook.sol"; |
||||
import {AbstractMessageIdAuthHook} from "./libs/AbstractMessageIdAuthHook.sol"; |
||||
import {Mailbox} from "../Mailbox.sol"; |
||||
import {StandardHookMetadata} from "./libs/StandardHookMetadata.sol"; |
||||
import {Message} from "../libs/Message.sol"; |
||||
import {TypeCasts} from "../libs/TypeCasts.sol"; |
||||
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; |
||||
import {MailboxClient} from "../client/MailboxClient.sol"; |
||||
|
||||
// ============ External Imports ============ |
||||
import {Address} from "@openzeppelin/contracts/utils/Address.sol"; |
||||
import {ArbSys} from "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; |
||||
|
||||
/** |
||||
* @title ArbL2ToL1Hook |
||||
* @notice Message hook to inform the ArbL2ToL1iSM of messages published through |
||||
* the native Arbitrum bridge. |
||||
* @notice This works only for L2 -> L1 messages and has the 7 day delay as specified by the ArbSys contract. |
||||
*/ |
||||
contract ArbL2ToL1Hook is AbstractMessageIdAuthHook { |
||||
using StandardHookMetadata for bytes; |
||||
|
||||
// ============ Constants ============ |
||||
|
||||
// precompile contract on L2 for sending messages to L1 |
||||
ArbSys public immutable arbSys; |
||||
// Immutable quote amount |
||||
uint256 public immutable GAS_QUOTE; |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
constructor( |
||||
address _mailbox, |
||||
uint32 _destinationDomain, |
||||
bytes32 _ism, |
||||
address _arbSys, |
||||
uint256 _gasQuote |
||||
) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) { |
||||
arbSys = ArbSys(_arbSys); |
||||
GAS_QUOTE = _gasQuote; |
||||
} |
||||
|
||||
function hookType() external pure override returns (uint8) { |
||||
return uint8(IPostDispatchHook.Types.ARB_L2_TO_L1); |
||||
} |
||||
|
||||
function _quoteDispatch( |
||||
bytes calldata, |
||||
bytes calldata |
||||
) internal view override returns (uint256) { |
||||
return GAS_QUOTE; |
||||
} |
||||
|
||||
// ============ Internal functions ============ |
||||
|
||||
/// @inheritdoc AbstractMessageIdAuthHook |
||||
function _sendMessageId( |
||||
bytes calldata metadata, |
||||
bytes memory payload |
||||
) internal override { |
||||
arbSys.sendTxToL1{value: metadata.msgValue(0)}( |
||||
TypeCasts.bytes32ToAddress(ism), |
||||
payload |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,147 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
/*@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@ HYPERLANE @@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@*/ |
||||
|
||||
// ============ Internal Imports ============ |
||||
|
||||
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; |
||||
import {TypeCasts} from "../../libs/TypeCasts.sol"; |
||||
import {Message} from "../../libs/Message.sol"; |
||||
import {AbstractMessageIdAuthorizedIsm} from "./AbstractMessageIdAuthorizedIsm.sol"; |
||||
|
||||
// ============ External Imports ============ |
||||
|
||||
import {IOutbox} from "@arbitrum/nitro-contracts/src/bridge/IOutbox.sol"; |
||||
import {CrossChainEnabledArbitrumL1} from "@openzeppelin/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol"; |
||||
import {Address} from "@openzeppelin/contracts/utils/Address.sol"; |
||||
|
||||
/** |
||||
* @title ArbL2ToL1Ism |
||||
* @notice Uses the native Arbitrum bridge to verify interchain messages from L2 to L1. |
||||
*/ |
||||
contract ArbL2ToL1Ism is |
||||
CrossChainEnabledArbitrumL1, |
||||
AbstractMessageIdAuthorizedIsm |
||||
{ |
||||
using Message for bytes; |
||||
// ============ Constants ============ |
||||
|
||||
// module type for the ISM |
||||
uint8 public constant moduleType = |
||||
uint8(IInterchainSecurityModule.Types.ARB_L2_TO_L1); |
||||
// arbitrum nitro contract on L1 to forward verification |
||||
IOutbox public arbOutbox; |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
constructor( |
||||
address _bridge, |
||||
address _outbox |
||||
) CrossChainEnabledArbitrumL1(_bridge) { |
||||
require( |
||||
Address.isContract(_bridge), |
||||
"ArbL2ToL1Ism: invalid Arbitrum Bridge" |
||||
); |
||||
arbOutbox = IOutbox(_outbox); |
||||
} |
||||
|
||||
// ============ External Functions ============ |
||||
|
||||
/// @inheritdoc IInterchainSecurityModule |
||||
function verify( |
||||
bytes calldata metadata, |
||||
bytes calldata message |
||||
) external override returns (bool) { |
||||
bool verified = isVerified(message); |
||||
if (verified) { |
||||
releaseValueToRecipient(message); |
||||
} |
||||
return verified || _verifyWithOutboxCall(metadata, message); |
||||
} |
||||
|
||||
// ============ Internal function ============ |
||||
|
||||
/** |
||||
* @notice Verify message directly using the arbOutbox.executeTransaction function. |
||||
* @dev This is a fallback in case the message is not verified by the stateful verify function first. |
||||
* @dev This function doesn't support msg.value as the ism.verify call doesn't support it either. |
||||
*/ |
||||
function _verifyWithOutboxCall( |
||||
bytes calldata metadata, |
||||
bytes calldata message |
||||
) internal returns (bool) { |
||||
( |
||||
bytes32[] memory proof, |
||||
uint256 index, |
||||
address l2Sender, |
||||
address to, |
||||
uint256 l2Block, |
||||
uint256 l1Block, |
||||
uint256 l2Timestamp, |
||||
bytes memory data |
||||
) = abi.decode( |
||||
metadata, |
||||
( |
||||
bytes32[], |
||||
uint256, |
||||
address, |
||||
address, |
||||
uint256, |
||||
uint256, |
||||
uint256, |
||||
bytes |
||||
) |
||||
); |
||||
|
||||
// check if the sender of the l2 message is the authorized hook |
||||
require( |
||||
l2Sender == TypeCasts.bytes32ToAddress(authorizedHook), |
||||
"ArbL2ToL1Ism: l2Sender != authorizedHook" |
||||
); |
||||
// this data is an abi encoded call of verifyMessageId(bytes32 messageId) |
||||
require(data.length == 36, "ArbL2ToL1Ism: invalid data length"); |
||||
bytes32 messageId = message.id(); |
||||
bytes32 convertedBytes; |
||||
assembly { |
||||
// data = 0x[4 bytes function signature][32 bytes messageId] |
||||
convertedBytes := mload(add(data, 36)) |
||||
} |
||||
// check if the parsed message id matches the message id of the message |
||||
require( |
||||
convertedBytes == messageId, |
||||
"ArbL2ToL1Ism: invalid message id" |
||||
); |
||||
|
||||
// value send to 0 |
||||
arbOutbox.executeTransaction( |
||||
proof, |
||||
index, |
||||
l2Sender, |
||||
to, |
||||
l2Block, |
||||
l1Block, |
||||
l2Timestamp, |
||||
0, |
||||
data |
||||
); |
||||
// the above bridge call will revert if the verifyMessageId call fails |
||||
return true; |
||||
} |
||||
|
||||
/// @inheritdoc AbstractMessageIdAuthorizedIsm |
||||
function _isAuthorized() internal view override returns (bool) { |
||||
return |
||||
_crossChainSender() == TypeCasts.bytes32ToAddress(authorizedHook); |
||||
} |
||||
} |
@ -1,6 +1,7 @@ |
||||
@openzeppelin=../node_modules/@openzeppelin |
||||
@layerzerolabs=../node_modules/@layerzerolabs |
||||
@arbitrum=../node_modules/@arbitrum |
||||
@eth-optimism=../node_modules/@eth-optimism |
||||
@layerzerolabs=../node_modules/@layerzerolabs |
||||
@openzeppelin=../node_modules/@openzeppelin |
||||
ds-test/=lib/forge-std/lib/ds-test/src/ |
||||
forge-std/=lib/forge-std/src/ |
||||
fx-portal/=lib/fx-portal/ |
||||
fx-portal/=lib/fx-portal/ |
||||
|
@ -0,0 +1,85 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
import "forge-std/Script.sol"; |
||||
|
||||
import {Mailbox} from "../../contracts/Mailbox.sol"; |
||||
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; |
||||
import {ArbL2ToL1Hook} from "../../contracts/hooks/ArbL2ToL1Hook.sol"; |
||||
import {ArbL2ToL1Ism} from "../../contracts/isms/hook/ArbL2ToL1Ism.sol"; |
||||
import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; |
||||
import {TestIsm} from "../../contracts/test/TestIsm.sol"; |
||||
|
||||
contract DeployArbHook is Script { |
||||
uint256 deployerPrivateKey; |
||||
|
||||
ArbL2ToL1Hook hook; |
||||
ArbL2ToL1Ism ism; |
||||
|
||||
uint32 constant L1_DOMAIN = 11155111; |
||||
address constant L1_MAILBOX = 0xfFAEF09B3cd11D9b20d1a19bECca54EEC2884766; |
||||
address constant L1_BRIDGE = 0x38f918D0E9F1b721EDaA41302E399fa1B79333a9; |
||||
address constant L1_OUTBOX = 0x65f07C7D521164a4d5DaC6eB8Fac8DA067A3B78F; |
||||
address constant L1_ISM = 0x096A1c034c7Ad113B6dB786b7BA852cB67025458; // placeholder |
||||
bytes32 TEST_RECIPIENT = |
||||
0x000000000000000000000000155b1cd2f7cbc58d403b9be341fab6cd77425175; // placeholder |
||||
|
||||
address constant ARBSYS = 0x0000000000000000000000000000000000000064; |
||||
address constant L2_MAILBOX = 0x598facE78a4302f11E3de0bee1894Da0b2Cb71F8; |
||||
address constant L2_HOOK = 0xd9d99AC1C645563576b8Df22cBebFC23FB60Ec73; // placeholder |
||||
|
||||
function deployIsm() external { |
||||
deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); |
||||
|
||||
vm.startBroadcast(deployerPrivateKey); |
||||
|
||||
ism = new ArbL2ToL1Ism(L1_BRIDGE, L1_OUTBOX); |
||||
|
||||
TestRecipient testRecipient = new TestRecipient(); |
||||
testRecipient.setInterchainSecurityModule(address(ism)); |
||||
|
||||
vm.stopBroadcast(); |
||||
} |
||||
|
||||
function deployHook() external { |
||||
deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); |
||||
|
||||
vm.startBroadcast(deployerPrivateKey); |
||||
|
||||
hook = new ArbL2ToL1Hook( |
||||
L2_MAILBOX, |
||||
L1_DOMAIN, |
||||
TypeCasts.addressToBytes32(L1_ISM), |
||||
ARBSYS, |
||||
200_000 // estimated gas amount used for verify |
||||
); |
||||
|
||||
vm.stopBroadcast(); |
||||
} |
||||
|
||||
function deployTestRecipient() external { |
||||
deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); |
||||
|
||||
vm.startBroadcast(deployerPrivateKey); |
||||
|
||||
TestIsm noopIsm = new TestIsm(); |
||||
noopIsm.setVerify(true); |
||||
TestRecipient testRecipient = new TestRecipient(); |
||||
testRecipient.setInterchainSecurityModule(address(noopIsm)); |
||||
|
||||
console.log("TestRecipient address: %s", address(testRecipient)); |
||||
|
||||
vm.stopBroadcast(); |
||||
} |
||||
|
||||
function setAuthorizedHook() external { |
||||
deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); |
||||
|
||||
vm.startBroadcast(deployerPrivateKey); |
||||
|
||||
ism = ArbL2ToL1Ism(L1_ISM); |
||||
ism.setAuthorizedHook(TypeCasts.addressToBytes32(L2_HOOK)); |
||||
|
||||
vm.stopBroadcast(); |
||||
} |
||||
} |
@ -0,0 +1,364 @@ |
||||
// SPDX-License-Identifier: MIT or Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import {Test} from "forge-std/Test.sol"; |
||||
|
||||
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; |
||||
import {MessageUtils} from "./IsmTestUtils.sol"; |
||||
import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; |
||||
import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; |
||||
import {Message} from "../../contracts/libs/Message.sol"; |
||||
import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; |
||||
import {ArbL2ToL1Hook} from "../../contracts/hooks/ArbL2ToL1Hook.sol"; |
||||
import {ArbL2ToL1Ism} from "../../contracts/isms/hook/ArbL2ToL1Ism.sol"; |
||||
import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; |
||||
|
||||
contract MockArbBridge { |
||||
error BridgeCallFailed(); |
||||
|
||||
address public activeOutbox; |
||||
address public l2ToL1Sender; |
||||
|
||||
constructor() { |
||||
activeOutbox = address(this); |
||||
} |
||||
|
||||
function setL2ToL1Sender(address _sender) external { |
||||
l2ToL1Sender = _sender; |
||||
} |
||||
|
||||
function executeTransaction( |
||||
bytes32[] calldata /*proof*/, |
||||
uint256 /*index*/, |
||||
address /*l2Sender*/, |
||||
address to, |
||||
uint256 /*l2Block*/, |
||||
uint256 /*l1Block*/, |
||||
uint256 /*timestamp*/, |
||||
uint256 value, |
||||
bytes calldata data |
||||
) external payable { |
||||
(bool success, bytes memory returndata) = to.call{value: value}(data); |
||||
if (!success) { |
||||
if (returndata.length > 0) { |
||||
// solhint-disable-next-line no-inline-assembly |
||||
assembly { |
||||
let returndata_size := mload(returndata) |
||||
revert(add(32, returndata), returndata_size) |
||||
} |
||||
} else { |
||||
revert BridgeCallFailed(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
contract MockArbSys { |
||||
function sendTxToL1( |
||||
address destination, |
||||
bytes calldata data |
||||
) external payable returns (uint256) {} |
||||
} |
||||
|
||||
contract ArbL2ToL1IsmTest is Test { |
||||
uint8 internal constant HYPERLANE_VERSION = 1; |
||||
uint32 internal constant MAINNET_DOMAIN = 1; |
||||
uint32 internal constant ARBITRUM_DOMAIN = 42161; |
||||
uint256 internal constant GAS_QUOTE = 120_000; |
||||
|
||||
uint256 internal constant MOCK_LEAF_INDEX = 40160; |
||||
uint256 internal constant MOCK_L2_BLOCK = 54220000; |
||||
uint256 internal constant MOCK_L1_BLOCK = 6098300; |
||||
|
||||
address internal constant L2_ARBSYS_ADDRESS = |
||||
0x0000000000000000000000000000000000000064; |
||||
|
||||
MockArbBridge internal arbBridge; |
||||
TestMailbox public l2Mailbox; |
||||
ArbL2ToL1Hook public hook; |
||||
ArbL2ToL1Ism public ism; |
||||
|
||||
TestRecipient internal testRecipient; |
||||
bytes internal testMessage = |
||||
abi.encodePacked("Hello from the other chain!"); |
||||
bytes internal encodedMessage; |
||||
bytes internal testMetadata = |
||||
StandardHookMetadata.overrideRefundAddress(address(this)); |
||||
bytes32 internal messageId; |
||||
|
||||
function setUp() public { |
||||
// Arbitrum bridge mock setup |
||||
vm.etch(L2_ARBSYS_ADDRESS, address(new MockArbSys()).code); |
||||
|
||||
testRecipient = new TestRecipient(); |
||||
|
||||
encodedMessage = _encodeTestMessage(); |
||||
messageId = Message.id(encodedMessage); |
||||
} |
||||
|
||||
/////////////////////////////////////////////////////////////////// |
||||
/// SETUP /// |
||||
/////////////////////////////////////////////////////////////////// |
||||
|
||||
function deployHook() public { |
||||
l2Mailbox = new TestMailbox(ARBITRUM_DOMAIN); |
||||
hook = new ArbL2ToL1Hook( |
||||
address(l2Mailbox), |
||||
MAINNET_DOMAIN, |
||||
TypeCasts.addressToBytes32(address(ism)), |
||||
L2_ARBSYS_ADDRESS, |
||||
GAS_QUOTE |
||||
); |
||||
} |
||||
|
||||
function deployIsm() public { |
||||
arbBridge = new MockArbBridge(); |
||||
|
||||
ism = new ArbL2ToL1Ism(address(arbBridge), address(arbBridge)); |
||||
} |
||||
|
||||
function deployAll() public { |
||||
deployIsm(); |
||||
deployHook(); |
||||
|
||||
ism.setAuthorizedHook(TypeCasts.addressToBytes32(address(hook))); |
||||
} |
||||
|
||||
function test_postDispatch() public { |
||||
deployAll(); |
||||
|
||||
bytes memory encodedHookData = abi.encodeCall( |
||||
AbstractMessageIdAuthorizedIsm.verifyMessageId, |
||||
(messageId) |
||||
); |
||||
|
||||
l2Mailbox.updateLatestDispatchedId(messageId); |
||||
|
||||
vm.expectCall( |
||||
L2_ARBSYS_ADDRESS, |
||||
abi.encodeCall( |
||||
MockArbSys.sendTxToL1, |
||||
(address(ism), encodedHookData) |
||||
) |
||||
); |
||||
hook.postDispatch(testMetadata, encodedMessage); |
||||
} |
||||
|
||||
function testFork_postDispatch_revertWhen_chainIDNotSupported() public { |
||||
deployAll(); |
||||
|
||||
bytes memory message = MessageUtils.formatMessage( |
||||
0, |
||||
uint32(0), |
||||
ARBITRUM_DOMAIN, |
||||
TypeCasts.addressToBytes32(address(this)), |
||||
2, // wrong domain |
||||
TypeCasts.addressToBytes32(address(testRecipient)), |
||||
testMessage |
||||
); |
||||
|
||||
l2Mailbox.updateLatestDispatchedId(Message.id(message)); |
||||
vm.expectRevert( |
||||
"AbstractMessageIdAuthHook: invalid destination domain" |
||||
); |
||||
hook.postDispatch(testMetadata, message); |
||||
} |
||||
|
||||
function test_postDispatch_revertWhen_notLastDispatchedMessage() public { |
||||
deployAll(); |
||||
|
||||
// vm.expectRevert( |
||||
// "AbstractMessageIdAuthHook: message not latest dispatched" |
||||
// ); |
||||
// hook.postDispatch(testMetadata, encodedMessage); |
||||
} |
||||
|
||||
function test_verify_outboxCall() public { |
||||
deployAll(); |
||||
|
||||
bytes memory encodedOutboxTxMetadata = _encodeOutboxTx( |
||||
address(hook), |
||||
address(ism), |
||||
messageId |
||||
); |
||||
|
||||
arbBridge.setL2ToL1Sender(address(hook)); |
||||
assertTrue(ism.verify(encodedOutboxTxMetadata, encodedMessage)); |
||||
} |
||||
|
||||
function test_verify_statefulVerify() public { |
||||
deployAll(); |
||||
|
||||
bytes memory encodedHookData = abi.encodeCall( |
||||
AbstractMessageIdAuthorizedIsm.verifyMessageId, |
||||
(messageId) |
||||
); |
||||
|
||||
arbBridge.setL2ToL1Sender(address(hook)); |
||||
arbBridge.executeTransaction{value: 1 ether}( |
||||
new bytes32[](0), |
||||
MOCK_LEAF_INDEX, |
||||
address(hook), |
||||
address(ism), |
||||
MOCK_L2_BLOCK, |
||||
MOCK_L1_BLOCK, |
||||
block.timestamp, |
||||
1 ether, |
||||
encodedHookData |
||||
); |
||||
|
||||
vm.etch(address(arbBridge), new bytes(0)); // this is a way to test that the arbBridge isn't called again |
||||
assertTrue(ism.verify(new bytes(0), encodedMessage)); |
||||
assertEq(address(testRecipient).balance, 1 ether); |
||||
} |
||||
|
||||
function test_verify_statefulAndOutbox() public { |
||||
deployAll(); |
||||
|
||||
bytes memory encodedHookData = abi.encodeCall( |
||||
AbstractMessageIdAuthorizedIsm.verifyMessageId, |
||||
(messageId) |
||||
); |
||||
|
||||
arbBridge.setL2ToL1Sender(address(hook)); |
||||
arbBridge.executeTransaction{value: 1 ether}( |
||||
new bytes32[](0), |
||||
MOCK_LEAF_INDEX, |
||||
address(hook), |
||||
address(ism), |
||||
MOCK_L2_BLOCK, |
||||
MOCK_L1_BLOCK, |
||||
block.timestamp, |
||||
1 ether, |
||||
encodedHookData |
||||
); |
||||
|
||||
bytes memory encodedOutboxTxMetadata = _encodeOutboxTx( |
||||
address(hook), |
||||
address(ism), |
||||
messageId |
||||
); |
||||
|
||||
vm.etch(address(arbBridge), new bytes(0)); // this is a way to test that the arbBridge isn't called again |
||||
assertTrue(ism.verify(encodedOutboxTxMetadata, encodedMessage)); |
||||
assertEq(address(testRecipient).balance, 1 ether); |
||||
} |
||||
|
||||
function test_verify_revertsWhen_noStatefulOrOutbox() public { |
||||
deployAll(); |
||||
|
||||
vm.expectRevert(); |
||||
ism.verify(new bytes(0), encodedMessage); |
||||
} |
||||
|
||||
function test_verify_revertsWhen_notAuthorizedHook() public { |
||||
deployAll(); |
||||
|
||||
bytes memory encodedOutboxTxMetadata = _encodeOutboxTx( |
||||
address(this), |
||||
address(ism), |
||||
messageId |
||||
); |
||||
|
||||
arbBridge.setL2ToL1Sender(address(hook)); |
||||
|
||||
vm.expectRevert("ArbL2ToL1Ism: l2Sender != authorizedHook"); |
||||
ism.verify(encodedOutboxTxMetadata, encodedMessage); |
||||
} |
||||
|
||||
function test_verify_revertsWhen_invalidIsm() public { |
||||
deployAll(); |
||||
|
||||
bytes memory encodedOutboxTxMetadata = _encodeOutboxTx( |
||||
address(hook), |
||||
address(this), |
||||
messageId |
||||
); |
||||
|
||||
arbBridge.setL2ToL1Sender(address(hook)); |
||||
|
||||
vm.expectRevert(); // BridgeCallFailed() |
||||
ism.verify(encodedOutboxTxMetadata, encodedMessage); |
||||
} |
||||
|
||||
function test_verify_revertsWhen_incorrectMessageId() public { |
||||
deployAll(); |
||||
|
||||
bytes32 incorrectMessageId = keccak256("incorrect message id"); |
||||
|
||||
bytes memory encodedOutboxTxMetadata = _encodeOutboxTx( |
||||
address(hook), |
||||
address(ism), |
||||
incorrectMessageId |
||||
); |
||||
|
||||
bytes memory encodedHookData = abi.encodeCall( |
||||
AbstractMessageIdAuthorizedIsm.verifyMessageId, |
||||
(incorrectMessageId) |
||||
); |
||||
|
||||
arbBridge.setL2ToL1Sender(address(hook)); |
||||
|
||||
// through outbox call |
||||
vm.expectRevert("ArbL2ToL1Ism: invalid message id"); |
||||
ism.verify(encodedOutboxTxMetadata, encodedMessage); |
||||
|
||||
// through statefulVerify |
||||
arbBridge.executeTransaction( |
||||
new bytes32[](0), |
||||
MOCK_LEAF_INDEX, |
||||
address(hook), |
||||
address(ism), |
||||
MOCK_L2_BLOCK, |
||||
MOCK_L1_BLOCK, |
||||
block.timestamp, |
||||
0, |
||||
encodedHookData |
||||
); |
||||
|
||||
vm.etch(address(arbBridge), new bytes(0)); // to stop the outbox route |
||||
vm.expectRevert(); |
||||
assertFalse(ism.verify(new bytes(0), encodedMessage)); |
||||
} |
||||
|
||||
// function test_verify_withMsgValue |
||||
|
||||
/* ============ helper functions ============ */ |
||||
|
||||
function _encodeOutboxTx( |
||||
address _hook, |
||||
address _ism, |
||||
bytes32 _messageId |
||||
) internal view returns (bytes memory) { |
||||
bytes memory encodedHookData = abi.encodeCall( |
||||
AbstractMessageIdAuthorizedIsm.verifyMessageId, |
||||
(_messageId) |
||||
); |
||||
|
||||
bytes32[] memory proof = new bytes32[](16); |
||||
return |
||||
abi.encode( |
||||
proof, |
||||
MOCK_LEAF_INDEX, |
||||
_hook, |
||||
_ism, |
||||
MOCK_L2_BLOCK, |
||||
MOCK_L1_BLOCK, |
||||
block.timestamp, |
||||
encodedHookData |
||||
); |
||||
} |
||||
|
||||
function _encodeTestMessage() internal view returns (bytes memory) { |
||||
return |
||||
MessageUtils.formatMessage( |
||||
HYPERLANE_VERSION, |
||||
uint32(0), |
||||
ARBITRUM_DOMAIN, |
||||
TypeCasts.addressToBytes32(address(this)), |
||||
MAINNET_DOMAIN, |
||||
TypeCasts.addressToBytes32(address(testRecipient)), |
||||
testMessage |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue