Adding Optimism Hook and dispatch with metadata (#2580)

Use mailbox callback to authenticate messages in hooks where necessary (#2609)

Co-authored-by: Kunal Arora <55632507+aroralanuk@users.noreply.github.com>
pull/2736/head
Kunal Arora 1 year ago committed by Yorke Rhodes
parent 46f5311a95
commit 0e10306d4b
No known key found for this signature in database
GPG Key ID: 9EEACF1DA75C5627
  1. 59
      .github/workflows/node.yml
  2. 265
      solidity/contracts/Mailbox.sol
  3. 185
      solidity/contracts/MailboxV3.sol
  4. 14
      solidity/contracts/client/MailboxClient.sol
  5. 15
      solidity/contracts/hooks/AbstractHook.sol
  6. 88
      solidity/contracts/hooks/AbstractMessageIdAuthHook.sol
  7. 55
      solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol
  8. 37
      solidity/contracts/hooks/DomainRoutingHook.sol
  9. 95
      solidity/contracts/hooks/ERC5164/ERC5164MessageHook.sol
  10. 52
      solidity/contracts/hooks/ERC5164Hook.sol
  11. 16
      solidity/contracts/hooks/MerkleTreeHook.sol
  12. 70
      solidity/contracts/hooks/OPStackHook.sol
  13. 98
      solidity/contracts/hooks/OptimismMessageHook.sol
  14. 12
      solidity/contracts/hooks/PausableHook.sol
  15. 67
      solidity/contracts/interfaces/IMailbox.sol
  16. 55
      solidity/contracts/interfaces/IMailboxV3.sol
  17. 0
      solidity/contracts/interfaces/hooks/IMessageDispatcher.sol
  18. 9
      solidity/contracts/interfaces/hooks/IMessageHook.sol
  19. 4
      solidity/contracts/interfaces/hooks/IPostDispatchHook.sol
  20. 20
      solidity/contracts/interfaces/optimism/ICrossDomainMessenger.sol
  21. 42
      solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol
  22. 33
      solidity/contracts/isms/hook/ERC5164ISM.sol
  23. 55
      solidity/contracts/isms/hook/OPStackISM.sol
  24. 17
      solidity/contracts/libs/hooks/OPStackHookMetadata.sol
  25. 2
      solidity/contracts/mock/MockERC5164.sol
  26. 30
      solidity/contracts/test/TestMailbox.sol
  27. 16
      solidity/contracts/test/TestPostDispatchHook.sol
  28. 10
      solidity/hardhat.config.ts
  29. 71
      solidity/test/hooks/FallbackDomainRoutingHook.t.sol
  30. 71
      solidity/test/isms/ERC5164ISM.t.sol
  31. 2
      solidity/test/isms/IsmTestUtils.sol
  32. 22
      solidity/test/isms/MultisigIsm.t.sol
  33. 34
      solidity/test/isms/OPStackIsm.t.sol

@ -3,10 +3,9 @@ name: node
on:
# Triggers the workflow on push or pull request against main
push:
branches: [main]
branches: [v3]
pull_request:
branches: [main]
branches: [v3]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@ -67,8 +66,8 @@ jobs:
path: ./*
key: ${{ github.sha }}
- name: build
run: yarn build
- name: core build
run: yarn workspace @hyperlane-xyz/core build
lint-prettier:
runs-on: ubuntu-latest
@ -102,9 +101,6 @@ jobs:
with:
submodules: recursive
- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
- uses: actions/cache@v3
with:
path: ./*
@ -119,30 +115,29 @@ jobs:
- name: token
run: yarn workspace @hyperlane-xyz/hyperlane-token run test
- name: infra
run: yarn workspace @hyperlane-xyz/infra run test
test-env:
runs-on: ubuntu-latest
needs: [yarn-build]
strategy:
fail-fast: false
matrix:
environment: [testnet3, mainnet2]
module: [ism, core, igp, ica, helloworld]
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}
- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
- name: Test ${{ matrix.environment }} ${{ matrix.module }} deployment (check, deploy, govern, check again)
run: cd typescript/infra && ./fork.sh ${{ matrix.environment }} ${{ matrix.module }}
# - name: infra
# run: yarn workspace @hyperlane-xyz/infra run test
# test-env:
# runs-on: ubuntu-latest
# needs: [yarn-build]
# strategy:
# matrix:
# environment: [testnet3, mainnet2]
# module: [ism, core, igp, ica, helloworld]
# steps:
# - uses: actions/checkout@v3
# - uses: actions/cache@v3
# with:
# path: ./*
# key: ${{ github.sha }}
# - name: Install Foundry
# uses: onbjerg/foundry-toolchain@v1
# - name: Test ${{ matrix.environment }} ${{ matrix.module }} deployment (check, deploy, govern, check again)
# run: cd typescript/infra && ./fork.sh ${{ matrix.environment }} ${{ matrix.module }}
test-sol:
env:

@ -3,51 +3,51 @@ pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {Versioned} from "./upgrade/Versioned.sol";
import {MerkleLib} from "./libs/Merkle.sol";
import {Message} from "./libs/Message.sol";
import {TypeCasts} from "./libs/TypeCasts.sol";
import {IMessageRecipient} from "./interfaces/IMessageRecipient.sol";
import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "./interfaces/IInterchainSecurityModule.sol";
import {IPostDispatchHook} from "./interfaces/hooks/IPostDispatchHook.sol";
import {IMessageRecipient} from "./interfaces/IMessageRecipientV3.sol";
import {IMailbox} from "./interfaces/IMailbox.sol";
import {PausableReentrancyGuardUpgradeable} from "./PausableReentrancyGuard.sol";
// ============ External Imports ============
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract Mailbox is
IMailbox,
OwnableUpgradeable,
PausableReentrancyGuardUpgradeable,
Versioned
{
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract Mailbox is IMailbox, Versioned, Ownable {
// ============ Libraries ============
using MerkleLib for MerkleLib.Tree;
using Message for bytes;
using TypeCasts for bytes32;
using TypeCasts for address;
// ============ Constants ============
// Maximum bytes per message = 2 KiB (somewhat arbitrarily set to begin)
uint256 public constant MAX_MESSAGE_BODY_BYTES = 2 * 2**10;
// Domain of chain on which the contract is deployed
uint32 public immutable localDomain;
// ============ Public Storage ============
// A monotonically increasing nonce for outbound unique message IDs.
uint32 public nonce;
// The latest dispatched message ID used for auth in post-dispatch hooks.
bytes32 public latestDispatchedId;
// The default ISM, used if the recipient fails to specify one.
IInterchainSecurityModule public defaultIsm;
// An incremental merkle tree used to store outbound message IDs.
MerkleLib.Tree public tree;
// Mapping of message ID to whether or not that message has been delivered.
mapping(bytes32 => bool) public delivered;
// ============ Upgrade Gap ============
// The default post dispatch hook, used for post processing of dispatched messages.
IPostDispatchHook public defaultHook;
// gap for upgrade safety
uint256[47] private __GAP;
// Mapping of message ID to delivery context that processed the message.
struct Delivery {
// address sender;
IInterchainSecurityModule ism;
// uint48 value?
// uint48 timestamp?
}
mapping(bytes32 => Delivery) internal deliveries;
// ============ Events ============
@ -58,31 +58,16 @@ contract Mailbox is
event DefaultIsmSet(address indexed module);
/**
* @notice Emitted when Mailbox is paused
*/
event Paused();
/**
* @notice Emitted when Mailbox is unpaused
* @notice Emitted when the default hook is updated
* @param hook The new default hook
*/
event Unpaused();
event DefaultHookSet(address indexed hook);
// ============ Constructor ============
constructor(uint32 _localDomain) {
constructor(uint32 _localDomain, address _owner) {
localDomain = _localDomain;
}
// ============ Initializers ============
function initialize(address _owner, address _defaultIsm)
external
initializer
{
__PausableReentrancyGuard_init();
__Ownable_init();
transferOwnership(_owner);
_setDefaultIsm(_defaultIsm);
_transferOwnership(_owner);
}
// ============ External Functions ============
@ -92,7 +77,15 @@ contract Mailbox is
* @param _module The new default ISM. Must be a contract.
*/
function setDefaultIsm(address _module) external onlyOwner {
_setDefaultIsm(_module);
require(Address.isContract(_module), "!contract");
defaultIsm = IInterchainSecurityModule(_module);
emit DefaultIsmSet(_module);
}
function setDefaultHook(address _hook) external onlyOwner {
require(Address.isContract(_hook), "!contract");
defaultHook = IPostDispatchHook(_hook);
emit DefaultHookSet(_hook);
}
/**
@ -106,30 +99,78 @@ contract Mailbox is
uint32 _destinationDomain,
bytes32 _recipientAddress,
bytes calldata _messageBody
) external override notPaused returns (bytes32) {
require(_messageBody.length <= MAX_MESSAGE_BODY_BYTES, "msg too long");
) external payable override returns (bytes32) {
return
dispatch(
_destinationDomain,
_recipientAddress,
_messageBody,
defaultHook,
_messageBody[0:0]
);
}
/**
* @notice Dispatches a message to the destination domain & recipient.
* @param destinationDomain Domain of destination chain
* @param recipientAddress Address of recipient on destination chain as bytes32
* @param messageBody Raw bytes content of message body
* @param hookMetadata Metadata used by the post dispatch hook
* @return The message ID inserted into the Mailbox's merkle tree
*/
function dispatch(
uint32 destinationDomain,
bytes32 recipientAddress,
bytes calldata messageBody,
bytes calldata hookMetadata
) external payable override returns (bytes32) {
return
dispatch(
destinationDomain,
recipientAddress,
messageBody,
defaultHook,
hookMetadata
);
}
function dispatch(
uint32 destinationDomain,
bytes32 recipientAddress,
bytes calldata messageBody,
IPostDispatchHook hook,
bytes calldata metadata
) public payable returns (bytes32) {
/// CHECKS ///
// Format the message into packed bytes.
bytes memory _message = Message.formatMessage(
bytes memory message = Message.formatMessage(
VERSION,
count(),
nonce,
localDomain,
msg.sender.addressToBytes32(),
_destinationDomain,
_recipientAddress,
_messageBody
destinationDomain,
recipientAddress,
messageBody
);
bytes32 id = message.id();
// Insert the message ID into the merkle tree.
bytes32 _id = _message.id();
tree.insert(_id);
emit Dispatch(
msg.sender,
_destinationDomain,
_recipientAddress,
_message
);
emit DispatchId(_id);
return _id;
/// EFFECTS ///
nonce += 1;
latestDispatchedId = id;
emit DispatchId(id);
emit Dispatch(message);
/// INTERACTIONS ///
hook.postDispatch{value: msg.value}(metadata, message);
return id;
}
function delivered(bytes32 _id) public view override returns (bool) {
return address(deliveries[_id].ism) != address(0);
}
/**
@ -140,83 +181,51 @@ contract Mailbox is
*/
function process(bytes calldata _metadata, bytes calldata _message)
external
payable
override
nonReentrantAndNotPaused
{
/// CHECKS ///
// Check that the message was intended for this mailbox.
require(_message.version() == VERSION, "!version");
require(_message.destination() == localDomain, "!destination");
require(_message.version() == VERSION, "bad version");
require(
_message.destination() == localDomain,
"unexpected destination"
);
// Check that the message hasn't already been delivered.
bytes32 _id = _message.id();
require(delivered[_id] == false, "delivered");
delivered[_id] = true;
// Verify the message via the ISM.
IInterchainSecurityModule _ism = IInterchainSecurityModule(
recipientIsm(_message.recipientAddress())
);
require(_ism.verify(_metadata, _message), "!module");
require(delivered(_id) == false, "already delivered");
// Deliver the message to the recipient.
uint32 origin = _message.origin();
bytes32 sender = _message.sender();
// Get the recipient's ISM.
address recipient = _message.recipientAddress();
IMessageRecipient(recipient).handle(origin, sender, _message.body());
emit Process(origin, sender, recipient);
emit ProcessId(_id);
}
IInterchainSecurityModule ism = recipientIsm(recipient);
// ============ Public Functions ============
/**
* @notice Calculates and returns tree's current root
*/
function root() public view returns (bytes32) {
return tree.root();
}
/// EFFECTS ///
/**
* @notice Returns the number of inserted leaves in the tree
*/
function count() public view returns (uint32) {
// count cannot exceed 2**TREE_DEPTH, see MerkleLib.sol
return uint32(tree.count);
}
deliveries[_id] = Delivery({
ism: ism
// sender: msg.sender
// value: uint48(msg.value),
// timestamp: uint48(block.number)
});
emit Process(_message);
emit ProcessId(_id);
/**
* @notice Returns a checkpoint representing the current merkle tree.
* @return root The root of the Mailbox's merkle tree.
* @return index The index of the last element in the tree.
*/
function latestCheckpoint() external view returns (bytes32, uint32) {
return (root(), count() - 1);
}
/// INTERACTIONS ///
/**
* @notice Pauses mailbox and prevents further dispatch/process calls
* @dev Only `owner` can pause the mailbox.
*/
function pause() external onlyOwner {
_pause();
emit Paused();
}
// Verify the message via the ISM.
require(ism.verify(_metadata, _message), "verification failed");
/**
* @notice Unpauses mailbox and allows for message processing.
* @dev Only `owner` can unpause the mailbox.
*/
function unpause() external onlyOwner {
_unpause();
emit Unpaused();
// Deliver the message to the recipient.
IMessageRecipient(recipient).handle{value: msg.value}(
_message.origin(),
_message.sender(),
_message.body()
);
}
/**
* @notice Returns whether mailbox is paused.
*/
function isPaused() external view returns (bool) {
return _isPaused();
}
// ============ Public Functions ============
/**
* @notice Returns the ISM to use for the recipient, defaulting to the
@ -245,16 +254,4 @@ contract Mailbox is
} catch {}
return defaultIsm;
}
// ============ Internal Functions ============
/**
* @notice Sets the default ISM for the Mailbox.
* @param _module The new default ISM. Must be a contract.
*/
function _setDefaultIsm(address _module) internal {
require(Address.isContract(_module), "!contract");
defaultIsm = IInterchainSecurityModule(_module);
emit DefaultIsmSet(_module);
}
}

@ -1,185 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {Versioned} from "./upgrade/Versioned.sol";
import {Message} from "./libs/Message.sol";
import {TypeCasts} from "./libs/TypeCasts.sol";
import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "./interfaces/IInterchainSecurityModule.sol";
import {IPostDispatchHook} from "./interfaces/hooks/IPostDispatchHook.sol";
import {IMessageRecipient} from "./interfaces/IMessageRecipientV3.sol";
import {IMailboxV3} from "./interfaces/IMailboxV3.sol";
// ============ External Imports ============
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract MailboxV3 is IMailboxV3, Versioned, Ownable {
// ============ Libraries ============
using Message for bytes;
using TypeCasts for bytes32;
using TypeCasts for address;
// ============ Constants ============
// Domain of chain on which the contract is deployed
uint32 public immutable localDomain;
// ============ Public Storage ============
// A monotonically increasing nonce for outbound unique message IDs.
uint32 public nonce;
// The default ISM, used if the recipient fails to specify one.
IInterchainSecurityModule public defaultIsm;
// The default post dispatch hook, used for post processing of dispatched messages.
IPostDispatchHook public defaultHook;
// Mapping of message ID to whether or not that message has been delivered.
mapping(bytes32 => bool) public delivered;
// ============ Events ============
/**
* @notice Emitted when the default ISM is updated
* @param module The new default ISM
*/
event DefaultIsmSet(address indexed module);
/**
* @notice Emitted when the default hook is updated
* @param hook The new default hook
*/
event DefaultHookSet(address indexed hook);
// ============ Constructor ============
constructor(uint32 _localDomain, address _owner) {
localDomain = _localDomain;
_transferOwnership(_owner);
}
// ============ External Functions ============
/**
* @notice Sets the default ISM for the Mailbox.
* @param _module The new default ISM. Must be a contract.
*/
function setDefaultIsm(address _module) external onlyOwner {
require(Address.isContract(_module), "!contract");
defaultIsm = IInterchainSecurityModule(_module);
emit DefaultIsmSet(_module);
}
function setDefaultHook(address _hook) external onlyOwner {
require(Address.isContract(_hook), "!contract");
defaultHook = IPostDispatchHook(_hook);
emit DefaultHookSet(_hook);
}
/**
* @notice Dispatches a message to the destination domain & recipient.
* @param _destinationDomain Domain of destination chain
* @param _recipientAddress Address of recipient on destination chain as bytes32
* @param _messageBody Raw bytes content of message body
* @return The message ID inserted into the Mailbox's merkle tree
*/
function dispatch(
uint32 _destinationDomain,
bytes32 _recipientAddress,
bytes calldata _messageBody
) external payable override returns (bytes32) {
// Format the message into packed bytes.
bytes memory _message = Message.formatMessage(
VERSION,
nonce,
localDomain,
msg.sender.addressToBytes32(),
_destinationDomain,
_recipientAddress,
_messageBody
);
// effects
nonce += 1;
bytes32 _id = _message.id();
emit DispatchId(_id);
emit Dispatch(_message);
// interactions
defaultHook.postDispatch{value: msg.value}(_message);
return _id;
}
/**
* @notice Attempts to deliver `_message` to its recipient. Verifies
* `_message` via the recipient's ISM using the provided `_metadata`.
* @param _metadata Metadata used by the ISM to verify `_message`.
* @param _message Formatted Hyperlane message (refer to Message.sol).
*/
function process(bytes calldata _metadata, bytes calldata _message)
external
payable
override
{
// Check that the message was intended for this mailbox.
require(_message.version() == VERSION, "!version");
require(_message.destination() == localDomain, "!destination");
// Check that the message hasn't already been delivered.
bytes32 _id = _message.id();
require(delivered[_id] == false, "delivered");
address recipient = _message.recipientAddress();
// Verify the message via the ISM.
IInterchainSecurityModule _ism = IInterchainSecurityModule(
recipientIsm(recipient)
);
require(_ism.verify(_metadata, _message), "!module");
// effects
delivered[_id] = true;
emit Process(_message);
emit ProcessId(_id);
// Deliver the message to the recipient. (interactions)
IMessageRecipient(recipient).handle{value: msg.value}(
_message.origin(),
_message.sender(),
_message.body()
);
}
// ============ Public Functions ============
/**
* @notice Returns the ISM to use for the recipient, defaulting to the
* default ISM if none is specified.
* @param _recipient The message recipient whose ISM should be returned.
* @return The ISM to use for `_recipient`.
*/
function recipientIsm(address _recipient)
public
view
returns (IInterchainSecurityModule)
{
// Use a default interchainSecurityModule if one is not specified by the
// recipient.
// This is useful for backwards compatibility and for convenience as
// recipients are not mandated to specify an ISM.
try
ISpecifiesInterchainSecurityModule(_recipient)
.interchainSecurityModule()
returns (IInterchainSecurityModule _val) {
// If the recipient specifies a zero address, use the default ISM.
if (address(_val) != address(0)) {
return _val;
}
// solhint-disable-next-line no-empty-blocks
} catch {}
return defaultIsm;
}
}

@ -3,15 +3,18 @@ pragma solidity >=0.6.11;
// ============ Internal Imports ============
import {IMailbox} from "../interfaces/IMailbox.sol";
import {Message} from "../libs/Message.sol";
// ============ External Imports ============
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
abstract contract MailboxClient {
using Message for bytes;
IMailbox immutable mailbox;
constructor(address _mailbox) {
require(Address.isContract(_mailbox), "!contract");
require(Address.isContract(_mailbox), "MailboxClient: invalid mailbox");
mailbox = IMailbox(_mailbox);
}
@ -21,7 +24,14 @@ abstract contract MailboxClient {
* @notice Only accept messages from an Hyperlane Mailbox contract
*/
modifier onlyMailbox() {
require(msg.sender == address(mailbox), "!mailbox");
require(
msg.sender == address(mailbox),
"MailboxClient: sender not mailbox"
);
_;
}
function isLatestDispatched(bytes32 id) internal view returns (bool) {
return mailbox.latestDispatchedId() == id;
}
}

@ -1,15 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {MailboxClient} from "../client/MailboxClient.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
abstract contract AbstractHook is MailboxClient, IPostDispatchHook {
constructor(address mailbox) MailboxClient(mailbox) {}
function postDispatch(bytes calldata message) external payable onlyMailbox {
_postDispatch(message);
}
function _postDispatch(bytes calldata message) internal virtual;
}

@ -0,0 +1,88 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
// ============ Internal Imports ============
import {AbstractMessageIdAuthorizedIsm} from "../isms/hook/AbstractMessageIdAuthorizedIsm.sol";
import {TypeCasts} from "../libs/TypeCasts.sol";
import {Message} from "../libs/Message.sol";
import {OPStackHookMetadata} from "../libs/hooks/OPStackHookMetadata.sol";
import {MailboxClient} from "../client/MailboxClient.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
// ============ External Imports ============
import {ICrossDomainMessenger} from "../interfaces/optimism/ICrossDomainMessenger.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title AbstractMessageIdAuthHook
* @notice Message hook to inform an Abstract Message ID ISM of messages published through
* the native OPStack bridge.
* @dev V3 WIP
*/
abstract contract AbstractMessageIdAuthHook is
IPostDispatchHook,
MailboxClient
{
using Message for bytes;
// ============ Constants ============
// address for ISM to verify messages
address public immutable ism;
// Domain of chain on which the ISM is deployed
uint32 public immutable destinationDomain;
// ============ Constructor ============
constructor(
address mailbox,
uint32 _destinationDomain,
address _ism
) MailboxClient(mailbox) {
require(_ism != address(0), "invalid ISM");
require(_destinationDomain != 0, "invalid destination domain");
ism = _ism;
destinationDomain = _destinationDomain;
}
/**
* @notice Hook to inform the optimism ISM of messages published through.
* metadata The metadata for the hook caller
* @param message The message being dispatched
*/
function postDispatch(bytes calldata metadata, bytes calldata message)
external
payable
override
{
bytes32 id = message.id();
require(isLatestDispatched(id), "message not latest dispatched");
require(
message.destination() == destinationDomain,
"invalid destination domain"
);
// TODO: handle msg.value?
bytes memory payload = abi.encodeCall(
AbstractMessageIdAuthorizedIsm.verifyMessageId,
id
);
_sendMessageId(metadata, payload);
}
function _sendMessageId(bytes calldata metadata, bytes memory payload)
internal
virtual;
}

@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
import {Message} from "../libs/Message.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
import {IMailbox} from "../interfaces/IMailbox.sol";
contract ConfigFallbackDomainRoutingHook is IPostDispatchHook {
using Message for bytes;
IMailbox public immutable mailbox;
/// @notice message sender => destination => recipient => hook
mapping(address => mapping(uint32 => mapping(bytes32 => IPostDispatchHook)))
public customHooks;
constructor(address _mailbox) {
mailbox = IMailbox(_mailbox);
}
function postDispatch(bytes calldata metadata, bytes calldata message)
public
payable
override
{
IPostDispatchHook configuredHook = customHooks[message.senderAddress()][
message.destination()
][message.recipient()];
if (address(configuredHook) == address(0)) {
configuredHook = mailbox.defaultHook();
}
configuredHook.postDispatch{value: msg.value}(metadata, message);
}
function setHook(
uint32 destinationDomain,
bytes32 recipient,
IPostDispatchHook hook
) external {
customHooks[msg.sender][destinationDomain][recipient] = hook;
}
}

@ -1,27 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {AbstractHook} from "./AbstractHook.sol";
// ============ Internal Imports ============
import {Message} from "../libs/Message.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
contract DomainRoutingHook is AbstractHook, Ownable {
// ============ External Imports ============
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract DomainRoutingHook is IPostDispatchHook, Ownable {
using Message for bytes;
struct HookConfig {
uint32 destination;
address hook;
}
mapping(uint32 => IPostDispatchHook) public hooks;
constructor(address _mailbox, address _owner) AbstractHook(_mailbox) {
constructor(address _owner) {
_transferOwnership(_owner);
}
function setHook(uint32 destination, address hook) external onlyOwner {
function setHook(uint32 destination, address hook) public onlyOwner {
hooks[destination] = IPostDispatchHook(hook);
}
function _postDispatch(bytes calldata message) internal override {
hooks[message.destination()].postDispatch{value: msg.value}(message);
function setHooks(HookConfig[] calldata configs) external onlyOwner {
for (uint256 i = 0; i < configs.length; i++) {
setHook(configs[i].destination, configs[i].hook);
}
}
function postDispatch(bytes calldata metadata, bytes calldata message)
external
payable
virtual
override
{
hooks[message.destination()].postDispatch{value: msg.value}(
metadata,
message
);
}
}

@ -1,95 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
// ============ Internal Imports ============
import {TypeCasts} from "../../libs/TypeCasts.sol";
import {IMessageHook} from "../../interfaces/hooks/IMessageHook.sol";
import {IMessageDispatcher} from "./interfaces/IMessageDispatcher.sol";
import {ERC5164ISM} from "../../isms/hook/ERC5164ISM.sol";
// ============ External Imports ============
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title 5164MessageHook
* @notice Message hook to inform the 5164 ISM of messages published through
* any of the 5164 adapters.
*/
contract ERC5164MessageHook is IMessageHook {
using TypeCasts for address;
// ============ Constants ============
// Domain of chain on which the ERC5164ISM is deployed
uint32 public immutable destinationDomain;
// Dispatcher used to send messages
IMessageDispatcher public immutable dispatcher;
// address for ERC5164ISM to verify messages
address public immutable ism;
// ============ Constructor ============
constructor(
uint32 _destinationDomain,
address _dispatcher,
address _ism
) {
require(
_destinationDomain != 0,
"ERC5164Hook: invalid destination domain"
);
require(_ism != address(0), "ERC5164Hook: invalid ISM");
destinationDomain = _destinationDomain;
require(
Address.isContract(_dispatcher),
"ERC5164Hook: invalid dispatcher"
);
dispatcher = IMessageDispatcher(_dispatcher);
ism = _ism;
}
// ============ External Functions ============
/**
* @notice Hook to inform the ERC5164ISM of messages published through.
* @dev anyone can call this function, that's why we need to send msg.sender
* @param _destinationDomain The destination domain of the message.
* @param _messageId The message ID.
* @return gasOverhead The gas overhead for the function call on destination.
*/
function postDispatch(uint32 _destinationDomain, bytes32 _messageId)
public
payable
override
returns (uint256)
{
require(msg.value == 0, "ERC5164Hook: no value allowed");
require(
_destinationDomain == destinationDomain,
"ERC5164Hook: invalid destination domain"
);
bytes memory _payload = abi.encodeCall(
ERC5164ISM.verifyMessageId,
(msg.sender.addressToBytes32(), _messageId)
);
dispatcher.dispatchMessage(_destinationDomain, ism, _payload);
// EIP-5164 doesn't specify a gas overhead
return 0;
}
}

@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
// ============ Internal Imports ============
import {TypeCasts} from "../libs/TypeCasts.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
import {IMessageDispatcher} from "../interfaces/hooks/IMessageDispatcher.sol";
import {AbstractMessageIdAuthHook} from "./AbstractMessageIdAuthHook.sol";
// ============ External Imports ============
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title 5164MessageHook
* @notice Message hook to inform the 5164 ISM of messages published through
* any of the 5164 adapters.
*/
contract ERC5164Hook is AbstractMessageIdAuthHook {
IMessageDispatcher immutable dispatcher;
constructor(
address _mailbox,
uint32 _destinationDomain,
address _ism,
address _dispatcher
) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) {
require(
Address.isContract(_dispatcher),
"ERC5164Hook: invalid dispatcher"
);
dispatcher = IMessageDispatcher(_dispatcher);
}
function _sendMessageId(
bytes calldata, /* metadata */
bytes memory payload
) internal override {
dispatcher.dispatchMessage(destinationDomain, ism, payload);
}
}

@ -1,18 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {AbstractHook} from "./AbstractHook.sol";
import {MerkleLib, TREE_DEPTH} from "../libs/Merkle.sol";
import {Message} from "../libs/Message.sol";
import {MailboxClient} from "../client/MailboxClient.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
contract MerkleTreeHook is AbstractHook {
contract MerkleTreeHook is IPostDispatchHook, MailboxClient {
using Message for bytes;
using MerkleLib for MerkleLib.Tree;
// An incremental merkle tree used to store outbound message IDs.
MerkleLib.Tree internal _tree;
constructor(address _mailbox) AbstractHook(_mailbox) {}
constructor(address _mailbox) MailboxClient(_mailbox) {}
function count() public view returns (uint32) {
return uint32(_tree.count);
@ -34,7 +35,12 @@ contract MerkleTreeHook is AbstractHook {
return (root(), count() - 1);
}
function _postDispatch(bytes calldata message) internal override {
_tree.insert(message.id());
function postDispatch(
bytes calldata, /*metadata*/
bytes calldata message
) external payable override {
bytes32 id = message.id();
require(isLatestDispatched(id), "message not dispatching");
_tree.insert(id);
}
}

@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
// ============ Internal Imports ============
import {AbstractMessageIdAuthHook} from "./AbstractMessageIdAuthHook.sol";
import {TypeCasts} from "../libs/TypeCasts.sol";
import {Message} from "../libs/Message.sol";
import {OPStackHookMetadata} from "../libs/hooks/OPStackHookMetadata.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
// ============ External Imports ============
import {ICrossDomainMessenger} from "../interfaces/optimism/ICrossDomainMessenger.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title OPStackHook
* @notice Message hook to inform the Optimism ISM of messages published through
* the native OPStack bridge.
* @dev V3 WIP
*/
contract OPStackHook is AbstractMessageIdAuthHook {
using OPStackHookMetadata for bytes;
// ============ Constants ============
ICrossDomainMessenger public immutable l1Messenger;
// Gas limit for sending messages to L2
// First 1.92e6 gas is provided by Optimism, see more here:
// https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions
uint32 internal constant GAS_LIMIT = 1_920_000;
// ============ Constructor ============
constructor(
address _mailbox,
uint32 _destinationDomain,
address _ism,
address _messenger
) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) {
require(
Address.isContract(_messenger),
"ERC5164Hook: invalid dispatcher"
);
l1Messenger = ICrossDomainMessenger(_messenger);
}
function _sendMessageId(bytes calldata metadata, bytes memory payload)
internal
override
{
l1Messenger.sendMessage{value: metadata.msgValue()}(
ism,
payload,
GAS_LIMIT
);
}
}

@ -1,98 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
// ============ Internal Imports ============
import {IMessageHook} from "../interfaces/hooks/IMessageHook.sol";
import {OptimismISM} from "../isms/hook/OptimismISM.sol";
import {TypeCasts} from "../libs/TypeCasts.sol";
// ============ External Imports ============
import {ICrossDomainMessenger} from "@eth-optimism/contracts/libraries/bridge/ICrossDomainMessenger.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title OptimismMessageHook
* @notice Message hook to inform the Optimism ISM of messages published through
* the native Optimism bridge.
*/
contract OptimismMessageHook is IMessageHook {
using TypeCasts for address;
// ============ Constants ============
// Domain of chain on which the optimism ISM is deployed
uint32 public immutable destinationDomain;
// Messenger used to send messages from L1 -> L2
ICrossDomainMessenger public immutable l1Messenger;
// address for Optimism ISM to verify messages
address public immutable ism;
// Gas limit for sending messages to L2
// First 1.92e6 gas is provided by Optimism, see more here:
// https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions
uint32 internal constant GAS_LIMIT = 1_920_000;
// ============ Constructor ============
constructor(
uint32 _destinationDomain,
address _messenger,
address _ism
) {
require(
_destinationDomain != 0,
"OptimismHook: invalid destination domain"
);
require(_ism != address(0), "OptimismHook: invalid ISM");
destinationDomain = _destinationDomain;
require(
Address.isContract(_messenger),
"OptimismHook: invalid messenger"
);
l1Messenger = ICrossDomainMessenger(_messenger);
ism = _ism;
}
// ============ External Functions ============
/**
* @notice Hook to inform the optimism ISM of messages published through.
* @dev anyone can call this function, that's why we need to send msg.sender
* @param _destination The destination domain of the message.
* @param _messageId The message ID.
* @return gasOverhead The gas overhead for the function call on L2.
*/
function postDispatch(uint32 _destination, bytes32 _messageId)
public
payable
override
returns (uint256)
{
require(msg.value == 0, "OptimismHook: no value allowed");
require(
_destination == destinationDomain,
"OptimismHook: invalid destination domain"
);
bytes memory _payload = abi.encodeCall(
OptimismISM.verifyMessageId,
(msg.sender.addressToBytes32(), _messageId)
);
l1Messenger.sendMessage(ism, _payload, GAS_LIMIT);
// calling the verifyMessageId function is ~25k gas but we get 1.92m gas from Optimism
return 0;
}
}

@ -1,17 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {AbstractHook} from "./AbstractHook.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
contract PausableHook is AbstractHook, Ownable, Pausable {
constructor(address _mailbox) AbstractHook(_mailbox) {}
function _postDispatch(bytes calldata message)
internal
override
contract PausableHook is IPostDispatchHook, Ownable, Pausable {
function postDispatch(bytes calldata metadata, bytes calldata message)
external
payable
whenNotPaused
{}

@ -2,22 +2,15 @@
pragma solidity >=0.8.0;
import {IInterchainSecurityModule} from "./IInterchainSecurityModule.sol";
import {IPostDispatchHook} from "./hooks/IPostDispatchHook.sol";
interface IMailbox {
// ============ Events ============
/**
* @notice Emitted when a new message is dispatched via Hyperlane
* @param sender The address that dispatched the message
* @param destination The destination domain of the message
* @param recipient The message recipient address on `destination`
* @param message Raw bytes of message
*/
event Dispatch(
address indexed sender,
uint32 indexed destination,
bytes32 indexed recipient,
bytes message
);
event Dispatch(bytes message);
/**
* @notice Emitted when a new message is dispatched via Hyperlane
@ -26,22 +19,16 @@ interface IMailbox {
event DispatchId(bytes32 indexed messageId);
/**
* @notice Emitted when a Hyperlane message is processed
* @param messageId The unique message identifier
* @notice Emitted when a Hyperlane message is delivered
* @param message Raw bytes of message
*/
event ProcessId(bytes32 indexed messageId);
event Process(bytes message);
/**
* @notice Emitted when a Hyperlane message is delivered
* @param origin The origin domain of the message
* @param sender The message sender address on `origin`
* @param recipient The address that handled the message
* @notice Emitted when a Hyperlane message is processed
* @param messageId The unique message identifier
*/
event Process(
uint32 indexed origin,
bytes32 indexed sender,
address indexed recipient
);
event ProcessId(bytes32 indexed messageId);
function localDomain() external view returns (uint32);
@ -49,23 +36,37 @@ interface IMailbox {
function defaultIsm() external view returns (IInterchainSecurityModule);
function dispatch(
uint32 _destinationDomain,
bytes32 _recipientAddress,
bytes calldata _messageBody
) external returns (bytes32);
function defaultHook() external view returns (IPostDispatchHook);
function process(bytes calldata _metadata, bytes calldata _message)
external;
function latestDispatchedId() external view returns (bytes32);
function count() external view returns (uint32);
function dispatch(
uint32 destinationDomain,
bytes32 recipientAddress,
bytes calldata messageBody
) external payable returns (bytes32 messageId);
function root() external view returns (bytes32);
function dispatch(
uint32 destinationDomain,
bytes32 recipientAddress,
bytes calldata body,
bytes calldata defaultHookMetadata
) external payable returns (bytes32 messageId);
function latestCheckpoint() external view returns (bytes32, uint32);
function dispatch(
uint32 destinationDomain,
bytes32 recipientAddress,
bytes calldata body,
IPostDispatchHook customHook,
bytes calldata customHookMetadata
) external payable returns (bytes32 messageId);
function process(bytes calldata metadata, bytes calldata message)
external
payable;
function recipientIsm(address _recipient)
function recipientIsm(address recipient)
external
view
returns (IInterchainSecurityModule);
returns (IInterchainSecurityModule module);
}

@ -1,55 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
import {IInterchainSecurityModule} from "./IInterchainSecurityModule.sol";
import {IPostDispatchHook} from "./hooks/IPostDispatchHook.sol";
interface IMailboxV3 {
// ============ Events ============
/**
* @notice Emitted when a new message is dispatched via Hyperlane
* @param message Raw bytes of message
*/
event Dispatch(bytes message);
/**
* @notice Emitted when a new message is dispatched via Hyperlane
* @param messageId The unique message identifier
*/
event DispatchId(bytes32 indexed messageId);
/**
* @notice Emitted when a Hyperlane message is delivered
* @param message Raw bytes of message
*/
event Process(bytes message);
/**
* @notice Emitted when a Hyperlane message is processed
* @param messageId The unique message identifier
*/
event ProcessId(bytes32 indexed messageId);
function localDomain() external view returns (uint32);
function delivered(bytes32 messageId) external view returns (bool);
function defaultIsm() external view returns (IInterchainSecurityModule);
function defaultHook() external view returns (IPostDispatchHook);
function dispatch(
uint32 _destinationDomain,
bytes32 _recipientAddress,
bytes calldata _messageBody
) external payable returns (bytes32);
function process(bytes calldata _metadata, bytes calldata _message)
external
payable;
function recipientIsm(address _recipient)
external
view
returns (IInterchainSecurityModule);
}

@ -1,9 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
interface IMessageHook {
function postDispatch(uint32 _destination, bytes32 _messageId)
external
payable
returns (uint256);
}

@ -2,5 +2,7 @@
pragma solidity >=0.8.0;
interface IPostDispatchHook {
function postDispatch(bytes calldata message) external payable;
function postDispatch(bytes calldata metadata, bytes calldata message)
external
payable;
}

@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
/**
* @title ICrossDomainMessenger interface for bedrock update
* @dev eth-optimism's version uses strict 0.8.15 which we don't want to restrict to
*/
interface ICrossDomainMessenger {
/**
* Sends a cross domain message to the target messenger.
* @param _target Target contract address.
* @param _message Message to send to the target.
* @param _gasLimit Gas limit for the provided message.
*/
function sendMessage(
address _target,
bytes calldata _message,
uint32 _gasLimit
) external payable;
}

@ -29,17 +29,33 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini
* @notice Uses the native bridges to verify interchain messages.
* @dev In the future, the hook might be moved inside the Mailbox which doesn't require storage mappings for senders.
* for more details see https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2381
* @dev V3 WIP
*/
abstract contract AbstractHookISM is IInterchainSecurityModule, Initializable {
abstract contract AbstractMessageIdAuthorizedIsm is
IInterchainSecurityModule,
Initializable
{
// ============ Public Storage ============
// Maps messageId to whether or not the sender attested to that message ID on the origin chain
// @dev anyone can send an untrusted messageId, so need to check for that while verifying
mapping(bytes32 => mapping(bytes32 => bool)) public verifiedMessageIds;
mapping(bytes32 => bool) public verifiedMessageIds;
// Address for Hook on L1 responsible for sending message via the Optimism bridge
address public authorizedHook;
// ============ Events ============
event ReceivedMessage(bytes32 indexed sender, bytes32 indexed messageId);
event ReceivedMessage(bytes32 indexed messageId);
// ============ Initializer ============
function setAuthorizedHook(address _hook) external initializer {
require(
_hook != address(0),
"AbstractNativeISM: invalid authorized hook"
);
authorizedHook = _hook;
}
// ============ External Functions ============
@ -52,8 +68,24 @@ abstract contract AbstractHookISM is IInterchainSecurityModule, Initializable {
bytes calldata _message
) external view returns (bool) {
bytes32 _messageId = Message.id(_message);
bytes32 _messageSender = Message.sender(_message);
return verifiedMessageIds[_messageId][_messageSender];
return verifiedMessageIds[_messageId];
}
/**
* @notice Receive a message from the L2 messenger.
* @dev Only callable by the L2 messenger.
* @param _messageId Hyperlane ID for the message.
*/
function verifyMessageId(bytes32 _messageId) external {
require(
_isAuthorized(),
"AbstractMessageIdAuthorizedIsm: sender is not the hook"
);
verifiedMessageIds[_messageId] = true;
emit ReceivedMessage(_messageId);
}
function _isAuthorized() internal view virtual returns (bool);
}

@ -16,10 +16,9 @@ pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol";
import {ERC5164MessageHook} from "../../hooks/ERC5164/ERC5164MessageHook.sol";
import {Message} from "../../libs/Message.sol";
import {TypeCasts} from "../../libs/TypeCasts.sol";
import {AbstractHookISM} from "./AbstractHookISM.sol";
import {AbstractMessageIdAuthorizedIsm} from "./AbstractMessageIdAuthorizedIsm.sol";
// ============ External Imports ============
@ -29,7 +28,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
* @title ERC5164ISM
* @notice Uses the generic eip-5164 standard to verify interchain messages.
*/
contract ERC5164ISM is AbstractHookISM {
contract ERC5164ISM is AbstractMessageIdAuthorizedIsm {
// ============ Constants ============
uint8 public constant moduleType =
@ -37,19 +36,6 @@ contract ERC5164ISM is AbstractHookISM {
// corresponding 5164 executor address
address public immutable executor;
// ============ Modifiers ============
/**
* @notice Check if sender is authorized to message `verifyMessageId`.
*/
modifier isAuthorized() {
require(
msg.sender == executor,
"ERC5164ISM: sender is not the executor"
);
_;
}
// ============ Constructor ============
constructor(address _executor) {
@ -57,19 +43,10 @@ contract ERC5164ISM is AbstractHookISM {
executor = _executor;
}
// ============ External Functions ============
/**
* @notice Receive a message from the executor.
* @param _sender Left-padded address of the sender.
* @param _messageId Hyperlane ID for the message.
* @notice Check if sender is authorized to message `verifyMessageId`.
*/
function verifyMessageId(bytes32 _sender, bytes32 _messageId)
external
isAuthorized
{
verifiedMessageIds[_messageId][_sender] = true;
emit ReceivedMessage(_sender, _messageId);
function _isAuthorized() internal view override returns (bool) {
return msg.sender == executor;
}
}

@ -16,10 +16,9 @@ pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol";
import {OptimismMessageHook} from "../../hooks/OptimismMessageHook.sol";
import {Message} from "../../libs/Message.sol";
import {TypeCasts} from "../../libs/TypeCasts.sol";
import {AbstractHookISM} from "./AbstractHookISM.sol";
import {AbstractMessageIdAuthorizedIsm} from "./AbstractMessageIdAuthorizedIsm.sol";
import {CrossChainEnabledOptimism} from "./crossChainEnabled/optimism/CrossChainEnabledOptimism.sol";
// ============ External Imports ============
@ -28,64 +27,34 @@ import {ICrossDomainMessenger} from "@eth-optimism/contracts/libraries/bridge/IC
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title OptimismISM
* @title OPStackIsm
* @notice Uses the native Optimism bridge to verify interchain messages.
* @dev V3 WIP
*/
contract OptimismISM is CrossChainEnabledOptimism, AbstractHookISM {
contract OPStackIsm is
CrossChainEnabledOptimism,
AbstractMessageIdAuthorizedIsm
{
// ============ Constants ============
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.NULL);
// ============ Public Storage ============
// Address for Hook on L1 responsible for sending message via the Optimism bridge
// @dev check https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2381 for updates to native
address public l1Hook;
// ============ Modifiers ============
/**
* @notice Check if sender is authorized to message `verifyMessageId`.
*/
modifier isAuthorized() {
require(
_crossChainSender() == l1Hook,
"OptimismISM: sender is not the hook"
);
_;
}
// ============ Constructor ============
constructor(address _l2Messenger) CrossChainEnabledOptimism(_l2Messenger) {
require(
Address.isContract(_l2Messenger),
"OptimismISM: invalid L2Messenger"
"OPStackIsm: invalid L2Messenger"
);
}
// ============ Initializer ============
function setOptimismHook(address _l1Hook) external initializer {
require(_l1Hook != address(0), "OptimismISM: invalid l1Hook");
l1Hook = _l1Hook;
}
// ============ External Functions ============
// ============ Internal function ============
/**
* @notice Receive a message from the L2 messenger.
* @dev Only callable by the L2 messenger.
* @param _sender Left-padded address of the sender.
* @param _messageId Hyperlane ID for the message.
* @notice Check if sender is authorized to message `verifyMessageId`.
*/
function verifyMessageId(bytes32 _sender, bytes32 _messageId)
external
isAuthorized
{
verifiedMessageIds[_messageId][_sender] = true;
emit ReceivedMessage(_sender, _messageId);
function _isAuthorized() internal view override returns (bool) {
return _crossChainSender() == authorizedHook;
}
}

@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/**
* Format of metadata:
*
* [0:32] Msg value to be sent to L2
*/
library OPStackHookMetadata {
function msgValue(bytes calldata _metadata)
internal
pure
returns (uint256)
{
return uint256(bytes32(_metadata[0:32]));
}
}

@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT or Apache-2.0
pragma solidity ^0.8.13;
import {IMessageDispatcher} from "../hooks/ERC5164/interfaces/IMessageDispatcher.sol";
import {IMessageDispatcher} from "../interfaces/IMessageDispatcher.sol";
contract MockMessageDispatcher is IMessageDispatcher {
function dispatchMessage(

@ -9,23 +9,23 @@ import {IMessageRecipient} from "../interfaces/IMessageRecipient.sol";
contract TestMailbox is Mailbox {
using TypeCasts for bytes32;
constructor(uint32 _localDomain) Mailbox(_localDomain) {} // solhint-disable-line no-empty-blocks
constructor(uint32 _localDomain) Mailbox(_localDomain, msg.sender) {} // solhint-disable-line no-empty-blocks
function proof() external view returns (bytes32[32] memory) {
bytes32[32] memory _zeroes = MerkleLib.zeroHashes();
uint256 _index = tree.count - 1;
bytes32[32] memory _proof;
// function proof() external view returns (bytes32[32] memory) {
// bytes32[32] memory _zeroes = MerkleLib.zeroHashes();
// uint256 _index = tree.count - 1;
// bytes32[32] memory _proof;
for (uint256 i = 0; i < 32; i++) {
uint256 _ithBit = (_index >> i) & 0x01;
if (_ithBit == 1) {
_proof[i] = tree.branch[i];
} else {
_proof[i] = _zeroes[i];
}
}
return _proof;
}
// for (uint256 i = 0; i < 32; i++) {
// uint256 _ithBit = (_index >> i) & 0x01;
// if (_ithBit == 1) {
// _proof[i] = tree.branch[i];
// } else {
// _proof[i] = _zeroes[i];
// }
// }
// return _proof;
// }
function testHandle(
uint32 _origin,

@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
contract TestPostDispatchHook is IPostDispatchHook {
event PostDispatchHookCalled();
function postDispatch(
bytes calldata, /*metadata*/
bytes calldata /*message*/
) external payable override {
// test - emit event
emit PostDispatchHookCalled();
}
}

@ -9,7 +9,15 @@ import 'solidity-coverage';
*/
module.exports = {
solidity: {
version: '0.8.17',
compilers: [
{
version: '0.8.17',
},
{
// for @eth-optimism
version: '0.8.15',
},
],
settings: {
optimizer: {
enabled: true,

@ -0,0 +1,71 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {MessageUtils} from "../isms/IsmTestUtils.sol";
import {Mailbox} from "../../contracts/Mailbox.sol";
import {FallbackDomainRoutingHook} from "../../contracts/hooks/FallbackDomainRoutingHook.sol";
import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol";
import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
contract FallbackDomainRoutingHookTest is Test {
using TypeCasts for address;
FallbackDomainRoutingHook internal fallbackHook;
TestPostDispatchHook internal configuredTestHook;
TestPostDispatchHook internal mailboxDefaultHook;
TestRecipient internal testRecipient;
Mailbox internal mailbox;
uint32 internal constant TEST_ORIGIN_DOMAIN = 1;
uint32 internal constant TEST_DESTINATION_DOMAIN = 2;
bytes internal testMessage;
event PostDispatchHookCalled();
function setUp() public {
mailbox = new Mailbox(TEST_ORIGIN_DOMAIN, address(this));
configuredTestHook = new TestPostDispatchHook();
mailboxDefaultHook = new TestPostDispatchHook();
testRecipient = new TestRecipient();
fallbackHook = new FallbackDomainRoutingHook(
address(mailbox),
address(this)
);
testMessage = _encodeTestMessage();
mailbox.setDefaultHook(address(mailboxDefaultHook));
}
function test_postDispatchHook_configured() public payable {
fallbackHook.setHook(
TEST_DESTINATION_DOMAIN,
address(configuredTestHook)
);
vm.expectEmit(false, false, false, false, address(configuredTestHook));
emit PostDispatchHookCalled();
fallbackHook.postDispatch{value: msg.value}("", testMessage);
}
function test_postDispatch_default() public payable {
vm.expectEmit(false, false, false, false, address(mailboxDefaultHook));
emit PostDispatchHookCalled();
fallbackHook.postDispatch{value: msg.value}("", testMessage);
}
function _encodeTestMessage() internal view returns (bytes memory) {
return
MessageUtils.formatMessage(
uint8(0), // version
uint32(1), // nonce
TEST_ORIGIN_DOMAIN,
address(this).addressToBytes32(),
TEST_DESTINATION_DOMAIN,
address(testRecipient).addressToBytes32(),
abi.encodePacked("Hello from the other chain!")
);
}
}

@ -6,19 +6,21 @@ import {Test} from "forge-std/Test.sol";
import {Message} from "../../contracts/libs/Message.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {IMessageDispatcher} from "../../contracts/hooks/ERC5164/interfaces/IMessageDispatcher.sol";
import {ERC5164MessageHook} from "../../contracts/hooks/ERC5164/ERC5164MessageHook.sol";
import {IMessageDispatcher} from "../../contracts/interfaces/IMessageDispatcher.sol";
import {ERC5164Hook} from "../../contracts/hooks/ERC5164Hook.sol";
import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol";
import {ERC5164ISM} from "../../contracts/isms/hook/ERC5164ISM.sol";
import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
import {MockMessageDispatcher, MockMessageExecutor} from "../../contracts/mock/MockERC5164.sol";
contract ERC5164ISMTest is Test {
using TypeCasts for address;
using Message for bytes;
IMessageDispatcher internal dispatcher;
MockMessageExecutor internal executor;
ERC5164MessageHook internal hook;
ERC5164Hook internal hook;
ERC5164ISM internal ism;
TestRecipient internal testRecipient;
@ -32,7 +34,7 @@ contract ERC5164ISMTest is Test {
// req for most tests
bytes encodedMessage = _encodeTestMessage(0, address(testRecipient));
bytes32 messageId = Message.id(encodedMessage);
bytes32 messageId = encodedMessage.id();
event MessageDispatched(
bytes32 indexed messageId,
@ -54,10 +56,12 @@ contract ERC5164ISMTest is Test {
function deployContracts() public {
ism = new ERC5164ISM(address(executor));
hook = new ERC5164MessageHook(
address mailbox = address(0); // TODO: check?
hook = new ERC5164Hook(
mailbox,
TEST2_DOMAIN,
address(dispatcher),
address(ism)
address(ism),
address(dispatcher)
);
}
@ -69,16 +73,35 @@ contract ERC5164ISMTest is Test {
vm.expectRevert("ERC5164ISM: invalid executor");
ism = new ERC5164ISM(alice);
vm.expectRevert("ERC5164Hook: invalid destination domain");
hook = new ERC5164MessageHook(0, address(dispatcher), address(ism));
vm.expectRevert("MailboxClient: invalid mailbox");
hook = new ERC5164Hook(
address(0),
0,
address(ism),
address(dispatcher)
);
vm.expectRevert("ERC5164Hook: invalid dispatcher");
hook = new ERC5164MessageHook(TEST2_DOMAIN, alice, address(ism));
vm.expectRevert("ERC5164Hook: invalid destination domain");
hook = new ERC5164Hook(
address(dispatcher),
0,
address(ism),
address(dispatcher)
);
vm.expectRevert("ERC5164Hook: invalid ISM");
hook = new ERC5164MessageHook(
hook = new ERC5164Hook(
address(dispatcher),
TEST2_DOMAIN,
address(0),
address(dispatcher)
);
vm.expectRevert("ERC5164Hook: invalid dispatcher");
hook = new ERC5164Hook(
address(dispatcher),
TEST2_DOMAIN,
address(ism),
address(0)
);
}
@ -87,8 +110,8 @@ contract ERC5164ISMTest is Test {
deployContracts();
bytes memory encodedHookData = abi.encodeCall(
ERC5164ISM.verifyMessageId,
(address(this).addressToBytes32(), messageId)
AbstractMessageIdAuthorizedIsm.verifyMessageId,
(messageId)
);
// note: not checking for messageId since this is implementation dependent on each vendor
@ -101,14 +124,16 @@ contract ERC5164ISMTest is Test {
encodedHookData
);
hook.postDispatch(TEST2_DOMAIN, messageId);
hook.postDispatch(bytes(""), encodedMessage);
}
function test_postDispatch_RevertWhen_ChainIDNotSupported() public {
deployContracts();
encodedMessage = _encodeTestMessage(0, address(this));
vm.expectRevert("ERC5164Hook: invalid destination domain");
hook.postDispatch(3, messageId);
hook.postDispatch(bytes(""), encodedMessage);
}
/* ============ ISM.verifyMessageId ============ */
@ -118,10 +143,8 @@ contract ERC5164ISMTest is Test {
vm.startPrank(address(executor));
ism.verifyMessageId(address(this).addressToBytes32(), messageId);
assertTrue(
ism.verifiedMessageIds(messageId, address(this).addressToBytes32())
);
ism.verifyMessageId(messageId);
assertTrue(ism.verifiedMessageIds(messageId));
vm.stopPrank();
}
@ -133,7 +156,7 @@ contract ERC5164ISMTest is Test {
// needs to be called by the authorized hook contract on Ethereum
vm.expectRevert("ERC5164ISM: sender is not the executor");
ism.verifyMessageId(alice.addressToBytes32(), messageId);
ism.verifyMessageId(messageId);
vm.stopPrank();
}
@ -145,7 +168,7 @@ contract ERC5164ISMTest is Test {
vm.startPrank(address(executor));
ism.verifyMessageId(address(this).addressToBytes32(), messageId);
ism.verifyMessageId(messageId);
bool verified = ism.verify(new bytes(0), encodedMessage);
assertTrue(verified);
@ -158,7 +181,7 @@ contract ERC5164ISMTest is Test {
vm.startPrank(address(executor));
ism.verifyMessageId(address(this).addressToBytes32(), messageId);
ism.verifyMessageId(messageId);
bytes memory invalidMessage = _encodeTestMessage(0, address(this));
bool verified = ism.verify(new bytes(0), invalidMessage);
@ -172,7 +195,7 @@ contract ERC5164ISMTest is Test {
vm.startPrank(address(executor));
ism.verifyMessageId(alice.addressToBytes32(), messageId);
ism.verifyMessageId(messageId);
bool verified = ism.verify(new bytes(0), encodedMessage);
assertFalse(verified);

@ -17,7 +17,7 @@ library MessageUtils {
uint32 _destinationDomain,
bytes32 _recipient,
bytes memory _messageBody
) private pure returns (bytes memory) {
) public pure returns (bytes memory) {
return
abi.encodePacked(
_version,

@ -10,15 +10,18 @@ import {MerkleRootMultisigIsmMetadata} from "../../contracts/libs/isms/MerkleRoo
import {CheckpointLib} from "../../contracts/libs/CheckpointLib.sol";
import {StaticMOfNAddressSetFactory} from "../../contracts/libs/StaticMOfNAddressSetFactory.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {MerkleTreeHook} from "../../contracts/hooks/MerkleTreeHook.sol";
import {Message} from "../../contracts/libs/Message.sol";
import {MOfNTestUtils} from "./IsmTestUtils.sol";
/// @notice since we removed merkle tree from the mailbox, we need to include the MerkleTreeHook in the test
abstract contract AbstractMultisigIsmTest is Test {
using Message for bytes;
uint32 constant ORIGIN = 11;
StaticMOfNAddressSetFactory factory;
IMultisigIsm ism;
MerkleTreeHook merkleTreeHook;
TestMailbox mailbox;
function metadataPrefix(bytes memory message)
@ -37,8 +40,9 @@ abstract contract AbstractMultisigIsmTest is Test {
uint256[] memory keys = addValidators(m, n, seed);
uint256[] memory signers = MOfNTestUtils.choose(m, keys, seed);
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox));
bytes32 checkpointRoot = mailbox.root();
uint32 checkpointIndex = uint32(mailbox.count() - 1);
// bytes
bytes32 checkpointRoot = merkleTreeHook.root();
uint32 checkpointIndex = uint32(merkleTreeHook.count() - 1);
bytes32 messageId = message.id();
bytes32 digest = CheckpointLib.digest(
domain,
@ -79,7 +83,7 @@ abstract contract AbstractMultisigIsmTest is Test {
uint8 version = mailbox.VERSION();
uint32 origin = mailbox.localDomain();
bytes32 sender = TypeCasts.addressToBytes32(address(this));
uint32 nonce = mailbox.count();
uint32 nonce = mailbox.nonce();
mailbox.dispatch(destination, recipient, body);
bytes memory message = Message.formatMessage(
version,
@ -131,7 +135,9 @@ contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest {
function setUp() public {
mailbox = new TestMailbox(ORIGIN);
merkleTreeHook = new MerkleTreeHook(address(mailbox));
factory = new StaticMerkleRootMultisigIsmFactory();
mailbox.setDefaultHook(address(merkleTreeHook));
}
function metadataPrefix(bytes memory message)
@ -140,14 +146,14 @@ contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest {
override
returns (bytes memory)
{
uint32 checkpointIndex = uint32(mailbox.count() - 1);
uint32 checkpointIndex = uint32(merkleTreeHook.count() - 1);
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox));
return
abi.encodePacked(
mailboxAsBytes32,
checkpointIndex,
message.id(),
mailbox.proof()
message.id()
// mailbox.proof()
);
}
}
@ -157,7 +163,9 @@ contract MessageIdMultisigIsmTest is AbstractMultisigIsmTest {
function setUp() public {
mailbox = new TestMailbox(ORIGIN);
merkleTreeHook = new MerkleTreeHook(address(mailbox));
factory = new StaticMessageIdMultisigIsmFactory();
mailbox.setDefaultHook(address(merkleTreeHook));
}
function metadataPrefix(bytes memory)
@ -167,6 +175,6 @@ contract MessageIdMultisigIsmTest is AbstractMultisigIsmTest {
returns (bytes memory)
{
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox));
return abi.encodePacked(mailboxAsBytes32, mailbox.root());
return abi.encodePacked(mailboxAsBytes32, merkleTreeHook.root());
}
}

@ -7,8 +7,8 @@ import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {Mailbox} from "../../contracts/Mailbox.sol";
import {Message} from "../../contracts/libs/Message.sol";
import {TestMultisigIsm} from "../../contracts/test/TestMultisigIsm.sol";
import {OptimismISM} from "../../contracts/isms/hook/OptimismISM.sol";
import {OptimismMessageHook} from "../../contracts/hooks/OptimismMessageHook.sol";
import {OPStackIsm} from "../../contracts/isms/hook/OPStackIsm.sol";
import {OPStackHook} from "../../contracts/hooks/OPStackHook.sol";
import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
import {NotCrossChainCall} from "../../contracts/isms/hook/crossChainEnabled/errors.sol";
@ -20,7 +20,7 @@ import {L2CrossDomainMessenger} from "@eth-optimism/contracts-bedrock/contracts/
import {Encoding} from "@eth-optimism/contracts-bedrock/contracts/libraries/Encoding.sol";
import {Hashing} from "@eth-optimism/contracts-bedrock/contracts/libraries/Hashing.sol";
contract OptimismISMTest is Test {
contract OPStackIsmTest is Test {
using TypeCasts for address;
uint256 internal mainnetFork;
@ -40,8 +40,8 @@ contract OptimismISMTest is Test {
ICrossDomainMessenger internal l1Messenger;
L2CrossDomainMessenger internal l2Messenger;
OptimismISM internal opISM;
OptimismMessageHook internal opHook;
OPStackIsmTest internal opISM;
OPStackHook internal opHook;
TestRecipient internal testRecipient;
bytes internal testMessage =
@ -84,7 +84,7 @@ contract OptimismISMTest is Test {
l1Messenger = ICrossDomainMessenger(L1_MESSENGER_ADDRESS);
opHook = new OptimismMessageHook(
opHook = new OPStackHook(
OPTIMISM_DOMAIN,
L1_MESSENGER_ADDRESS,
address(opISM)
@ -93,17 +93,17 @@ contract OptimismISMTest is Test {
vm.makePersistent(address(opHook));
}
function deployOptimismISM() public {
function deployOPStackIsm() public {
vm.selectFork(optimismFork);
l2Messenger = L2CrossDomainMessenger(L2_MESSENGER_ADDRESS);
opISM = new OptimismISM(L2_MESSENGER_ADDRESS);
opISM = new OPStackIsm(L2_MESSENGER_ADDRESS);
vm.makePersistent(address(opISM));
}
function deployAll() public {
deployOptimismISM();
deployOPStackIsm();
deployOptimismHook();
vm.selectFork(optimismFork);
@ -128,7 +128,7 @@ contract OptimismISMTest is Test {
vm.selectFork(mainnetFork);
bytes memory encodedHookData = abi.encodeCall(
OptimismISM.verifyMessageId,
OPStackIsm.verifyMessageId,
(address(this).addressToBytes32(), messageId)
);
@ -164,7 +164,7 @@ contract OptimismISMTest is Test {
vm.selectFork(optimismFork);
bytes memory encodedHookData = abi.encodeCall(
OptimismISM.verifyMessageId,
OPStackIsm.verifyMessageId,
(address(this).addressToBytes32(), messageId)
);
@ -222,7 +222,7 @@ contract OptimismISMTest is Test {
// vm.selectFork(optimismFork);
// bytes memory encodedHookData = abi.encodeCall(
// OptimismISM.verifyMessageId,
// OPStackIsm.verifyMessageId,
// (address(this), messageId)
// );
@ -267,7 +267,7 @@ contract OptimismISMTest is Test {
vm.startPrank(L2_MESSENGER_ADDRESS);
// needs to be called by the authorized hook contract on Ethereum
vm.expectRevert("OptimismISM: sender is not the hook");
vm.expectRevert("OPStackIsm: sender is not the hook");
opISM.verifyMessageId(address(opHook).addressToBytes32(), messageId);
}
@ -279,7 +279,7 @@ contract OptimismISMTest is Test {
vm.selectFork(optimismFork);
bytes memory encodedHookData = abi.encodeCall(
OptimismISM.verifyMessageId,
OPStackIsm.verifyMessageId,
(address(this).addressToBytes32(), messageId)
);
@ -312,7 +312,7 @@ contract OptimismISMTest is Test {
vm.selectFork(optimismFork);
bytes memory encodedHookData = abi.encodeCall(
OptimismISM.verifyMessageId,
OPStackIsm.verifyMessageId,
(address(this).addressToBytes32(), messageId)
);
@ -349,7 +349,7 @@ contract OptimismISMTest is Test {
bytes32 _messageId = Message.id(invalidMessage);
bytes memory encodedHookData = abi.encodeCall(
OptimismISM.verifyMessageId,
OPStackIsm.verifyMessageId,
(address(this).addressToBytes32(), _messageId)
);
@ -381,7 +381,7 @@ contract OptimismISMTest is Test {
vm.selectFork(optimismFork);
bytes memory encodedHookData = abi.encodeCall(
OptimismISM.verifyMessageId,
OPStackIsm.verifyMessageId,
(alice.addressToBytes32(), messageId)
);
Loading…
Cancel
Save