Skip to content

Commit

Permalink
feat: support Arbitrum L2->L1 post dispatch hook (#3853)
Browse files Browse the repository at this point in the history
### 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 #2846

### Backward compatibility

Yes

### Testing

Unit and e2e with arbitrumsepolia->sepolia
  • Loading branch information
aroralanuk authored Jul 3, 2024
1 parent 9cff8c2 commit f733379
Show file tree
Hide file tree
Showing 13 changed files with 904 additions and 26 deletions.
82 changes: 82 additions & 0 deletions solidity/contracts/hooks/ArbL2ToL1Hook.sol
Original file line number Diff line number Diff line change
@@ -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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ abstract contract AbstractMessageIdAuthHook is
}

/// @inheritdoc IPostDispatchHook
function hookType() external pure returns (uint8) {
function hookType() external pure virtual returns (uint8) {
return uint8(IPostDispatchHook.Types.ID_AUTH_ISM);
}

Expand Down
3 changes: 2 additions & 1 deletion solidity/contracts/hooks/warp-route/RateLimitedHook.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {MailboxClient} from "contracts/client/MailboxClient.sol";
import {IPostDispatchHook} from "contracts/interfaces/hooks/IPostDispatchHook.sol";
import {Message} from "contracts/libs/Message.sol";
Expand All @@ -26,7 +27,7 @@ contract RateLimitedHook is IPostDispatchHook, MailboxClient, RateLimited {

/// @inheritdoc IPostDispatchHook
function hookType() external pure returns (uint8) {
return uint8(IPostDispatchHook.Types.Rate_Limited_Hook);
return uint8(IPostDispatchHook.Types.RATE_LIMITED);
}

/// @inheritdoc IPostDispatchHook
Expand Down
3 changes: 2 additions & 1 deletion solidity/contracts/interfaces/IInterchainSecurityModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ interface IInterchainSecurityModule {
MERKLE_ROOT_MULTISIG,
MESSAGE_ID_MULTISIG,
NULL, // used with relayer carrying no metadata
CCIP_READ
CCIP_READ,
ARB_L2_TO_L1
}

/**
Expand Down
3 changes: 2 additions & 1 deletion solidity/contracts/interfaces/hooks/IPostDispatchHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ interface IPostDispatchHook {
PAUSABLE,
PROTOCOL_FEE,
LAYER_ZERO_V1,
Rate_Limited_Hook
RATE_LIMITED,
ARB_L2_TO_L1
}

/**
Expand Down
50 changes: 35 additions & 15 deletions solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,26 +72,41 @@ abstract contract AbstractMessageIdAuthorizedIsm is
*/
function verify(
bytes calldata,
/*_metadata*/
/*metadata*/
bytes calldata message
) external returns (bool) {
bytes32 messageId = message.id();
) external virtual returns (bool) {
bool verified = isVerified(message);
if (verified) {
releaseValueToRecipient(message);
}
return verified;
}

// check for the first bit (used for verification)
bool verified = verifiedMessages[messageId].isBitSet(
// ============ Public Functions ============

/**
* @notice Release the value to the recipient if the message is verified.
* @param message Message to release value for.
*/
function releaseValueToRecipient(bytes calldata message) public {
bytes32 messageId = message.id();
uint256 _msgValue = verifiedMessages[messageId].clearBit(
VERIFIED_MASK_INDEX
);
// rest 255 bits contains the msg.value passed from the hook
if (verified) {
uint256 _msgValue = verifiedMessages[messageId].clearBit(
VERIFIED_MASK_INDEX
);
if (_msgValue > 0) {
verifiedMessages[messageId] -= _msgValue;
payable(message.recipientAddress()).sendValue(_msgValue);
}
if (_msgValue > 0) {
verifiedMessages[messageId] -= _msgValue;
payable(message.recipientAddress()).sendValue(_msgValue);
}
return verified;
}

/**
* @notice Check if a message is verified through verifyMessageId first.
* @param message Message to check.
*/
function isVerified(bytes calldata message) public view returns (bool) {
bytes32 messageId = message.id();
// check for the first bit (used for verification)
return verifiedMessages[messageId].isBitSet(VERIFIED_MASK_INDEX);
}

/**
Expand All @@ -113,5 +128,10 @@ abstract contract AbstractMessageIdAuthorizedIsm is
emit ReceivedMessage(messageId);
}

// ============ Internal Functions ============

/**
* @notice Check if sender is authorized to message `verifyMessageId`.
*/
function _isAuthorized() internal view virtual returns (bool);
}
147 changes: 147 additions & 0 deletions solidity/contracts/isms/hook/ArbL2ToL1Ism.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions solidity/contracts/test/TestRecipient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ contract TestRecipient is
function setInterchainSecurityModule(address _ism) external onlyOwner {
interchainSecurityModule = IInterchainSecurityModule(_ism);
}

receive() external payable {}
}
1 change: 1 addition & 0 deletions solidity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"description": "Core solidity contracts for Hyperlane",
"version": "4.0.0",
"dependencies": {
"@arbitrum/nitro-contracts": "^1.2.1",
"@eth-optimism/contracts": "^0.6.0",
"@hyperlane-xyz/utils": "4.0.0",
"@layerzerolabs/lz-evm-oapp-v2": "2.0.2",
Expand Down
7 changes: 4 additions & 3 deletions solidity/remappings.txt
Original file line number Diff line number Diff line change
@@ -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/
Loading

0 comments on commit f733379

Please sign in to comment.