From 45d2df6145f3b8e932143c1aa4b5922ac99d6115 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 25 Jul 2023 16:22:02 -0400 Subject: [PATCH 01/27] Init immutable mailbox v3 and native hooks --- solidity/contracts/MailboxV3.sol | 184 ++++++++++++++++++ solidity/contracts/client/MailboxClient.sol | 27 +++ solidity/contracts/hooks/AbstractHook.sol | 15 ++ .../contracts/hooks/DomainRoutingHook.sol | 27 +++ solidity/contracts/hooks/MerkleTreeHook.sol | 40 ++++ solidity/contracts/hooks/PausableHook.sol | 25 +++ solidity/contracts/interfaces/IMailboxV3.sol | 68 +++++++ .../interfaces/IMessageDispatcher.sol | 30 +++ .../interfaces/hooks/IPostDispatchHook.sol | 6 + solidity/contracts/libs/Merkle.sol | 3 +- 10 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 solidity/contracts/MailboxV3.sol create mode 100644 solidity/contracts/client/MailboxClient.sol create mode 100644 solidity/contracts/hooks/AbstractHook.sol create mode 100644 solidity/contracts/hooks/DomainRoutingHook.sol create mode 100644 solidity/contracts/hooks/MerkleTreeHook.sol create mode 100644 solidity/contracts/hooks/PausableHook.sol create mode 100644 solidity/contracts/interfaces/IMailboxV3.sol create mode 100644 solidity/contracts/interfaces/IMessageDispatcher.sol create mode 100644 solidity/contracts/interfaces/hooks/IPostDispatchHook.sol diff --git a/solidity/contracts/MailboxV3.sol b/solidity/contracts/MailboxV3.sol new file mode 100644 index 0000000000..8b52a16f12 --- /dev/null +++ b/solidity/contracts/MailboxV3.sol @@ -0,0 +1,184 @@ +// 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 {IMessageRecipient} from "./interfaces/IMessageRecipient.sol"; +import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "./interfaces/IInterchainSecurityModule.sol"; +import {IPostDispatchHook} from "./interfaces/hooks/IPostDispatchHook.sol"; +import {IMailboxV3} from "./interfaces/IMailboxV3.sol"; + +// ============ External Imports ============ +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract MailboxV3 is IMailboxV3, Versioned, ReentrancyGuard, 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 override returns (bytes32) { + // Format the message into packed bytes. + bytes memory _message = Message.formatMessage( + VERSION, + nonce, + localDomain, + msg.sender.addressToBytes32(), + _destinationDomain, + _recipientAddress, + _messageBody + ); + + nonce += 1; + + // Insert the message ID into the merkle tree. + bytes32 _id = _message.id(); + emit Dispatch( + msg.sender, + _destinationDomain, + _recipientAddress, + _message + ); + emit DispatchId(_id); + 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 + override + nonReentrant + { + // 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"); + delivered[_id] = true; + + // Verify the message via the ISM. + IInterchainSecurityModule _ism = IInterchainSecurityModule( + recipientIsm(_message.recipientAddress()) + ); + require(_ism.verify(_metadata, _message), "!module"); + + // Deliver the message to the recipient. + uint32 origin = _message.origin(); + bytes32 sender = _message.sender(); + address recipient = _message.recipientAddress(); + IMessageRecipient(recipient).handle(origin, sender, _message.body()); + emit Process(origin, sender, recipient); + emit ProcessId(_id); + } + + // ============ 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; + } +} diff --git a/solidity/contracts/client/MailboxClient.sol b/solidity/contracts/client/MailboxClient.sol new file mode 100644 index 0000000000..d644fd9aef --- /dev/null +++ b/solidity/contracts/client/MailboxClient.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +// ============ Internal Imports ============ +import {IMailbox} from "../interfaces/IMailbox.sol"; + +// ============ External Imports ============ +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +abstract contract MailboxClient { + IMailbox immutable mailbox; + + constructor(address _mailbox) { + require(Address.isContract(_mailbox), "!contract"); + mailbox = IMailbox(_mailbox); + } + + // ============ Modifiers ============ + + /** + * @notice Only accept messages from an Hyperlane Mailbox contract + */ + modifier onlyMailbox() { + require(msg.sender == address(mailbox), "!mailbox"); + _; + } +} diff --git a/solidity/contracts/hooks/AbstractHook.sol b/solidity/contracts/hooks/AbstractHook.sol new file mode 100644 index 0000000000..7b03d44450 --- /dev/null +++ b/solidity/contracts/hooks/AbstractHook.sol @@ -0,0 +1,15 @@ +// 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; +} diff --git a/solidity/contracts/hooks/DomainRoutingHook.sol b/solidity/contracts/hooks/DomainRoutingHook.sol new file mode 100644 index 0000000000..a2d71c1f87 --- /dev/null +++ b/solidity/contracts/hooks/DomainRoutingHook.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {AbstractHook} from "./AbstractHook.sol"; +import {Message} from "../libs/Message.sol"; + +import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; + +contract DomainRoutingHook is AbstractHook, Ownable { + using Message for bytes; + + mapping(uint32 => IPostDispatchHook) public hooks; + + constructor(address _mailbox, address _owner) AbstractHook(_mailbox) { + _transferOwnership(_owner); + } + + function setHook(uint32 destination, address hook) external onlyOwner { + hooks[destination] = IPostDispatchHook(hook); + } + + function _postDispatch(bytes calldata message) internal override { + hooks[message.destination()].postDispatch{value: msg.value}(message); + } +} diff --git a/solidity/contracts/hooks/MerkleTreeHook.sol b/solidity/contracts/hooks/MerkleTreeHook.sol new file mode 100644 index 0000000000..19b2367a0e --- /dev/null +++ b/solidity/contracts/hooks/MerkleTreeHook.sol @@ -0,0 +1,40 @@ +// 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"; + +contract MerkleTreeHook is AbstractHook { + 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) {} + + function count() public view returns (uint32) { + return uint32(_tree.count); + } + + function root() public view returns (bytes32) { + return _tree.root(); + } + + function branch() public view returns (bytes32[TREE_DEPTH] memory) { + return _tree.branch; + } + + function tree() public view returns (MerkleLib.Tree memory) { + return _tree; + } + + function latestCheckpoint() external view returns (bytes32, uint32) { + return (root(), count() - 1); + } + + function _postDispatch(bytes calldata message) internal override { + _tree.insert(message.id()); + } +} diff --git a/solidity/contracts/hooks/PausableHook.sol b/solidity/contracts/hooks/PausableHook.sol new file mode 100644 index 0000000000..b3feba3165 --- /dev/null +++ b/solidity/contracts/hooks/PausableHook.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {AbstractHook} from "./AbstractHook.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 + whenNotPaused + {} + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } +} diff --git a/solidity/contracts/interfaces/IMailboxV3.sol b/solidity/contracts/interfaces/IMailboxV3.sol new file mode 100644 index 0000000000..a4c2c9e8d7 --- /dev/null +++ b/solidity/contracts/interfaces/IMailboxV3.sol @@ -0,0 +1,68 @@ +// 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 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 + ); + + /** + * @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 processed + * @param messageId The unique message identifier + */ + event ProcessId(bytes32 indexed messageId); + + /** + * @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 + */ + event Process( + uint32 indexed origin, + bytes32 indexed sender, + address indexed recipient + ); + + 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 returns (bytes32); + + function process(bytes calldata _metadata, bytes calldata _message) + external; + + function recipientIsm(address _recipient) + external + view + returns (IInterchainSecurityModule); +} diff --git a/solidity/contracts/interfaces/IMessageDispatcher.sol b/solidity/contracts/interfaces/IMessageDispatcher.sol new file mode 100644 index 0000000000..b9058687ff --- /dev/null +++ b/solidity/contracts/interfaces/IMessageDispatcher.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/** + * @title ERC-5164: Cross-Chain Execution Standard + * @dev See https://eips.ethereum.org/EIPS/eip-5164 + */ +interface IMessageDispatcher { + /** + * @notice Emitted when a message has successfully been dispatched to the executor chain. + * @param messageId ID uniquely identifying the message + * @param from Address that dispatched the message + * @param toChainId ID of the chain receiving the message + * @param to Address that will receive the message + * @param data Data that was dispatched + */ + event MessageDispatched( + bytes32 indexed messageId, + address indexed from, + uint256 indexed toChainId, + address to, + bytes data + ); + + function dispatchMessage( + uint256 toChainId, + address to, + bytes calldata data + ) external returns (bytes32); +} diff --git a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol new file mode 100644 index 0000000000..c6daf084d4 --- /dev/null +++ b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +interface IPostDispatchHook { + function postDispatch(bytes calldata message) external payable; +} diff --git a/solidity/contracts/libs/Merkle.sol b/solidity/contracts/libs/Merkle.sol index 688c9c0dba..8c8819bc26 100644 --- a/solidity/contracts/libs/Merkle.sol +++ b/solidity/contracts/libs/Merkle.sol @@ -3,13 +3,14 @@ pragma solidity >=0.6.11; // work based on eth2 deposit contract, which is used under CC0-1.0 +uint256 constant TREE_DEPTH = 32; + /** * @title MerkleLib * @author Celo Labs Inc. * @notice An incremental merkle tree modeled on the eth2 deposit contract. **/ library MerkleLib { - uint256 internal constant TREE_DEPTH = 32; uint256 internal constant MAX_LEAVES = 2**TREE_DEPTH - 1; /** From df69886ab508e2c7db8df622dfa1d40a77621362 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 25 Jul 2023 16:51:06 -0400 Subject: [PATCH 02/27] Simplify mailbox and make interfaces payable --- solidity/contracts/MailboxV3.sol | 46 +++++++++---------- solidity/contracts/interfaces/IMailboxV3.sol | 37 +++------------ .../interfaces/IMessageRecipientV3.sol | 10 ++++ 3 files changed, 38 insertions(+), 55 deletions(-) create mode 100644 solidity/contracts/interfaces/IMessageRecipientV3.sol diff --git a/solidity/contracts/MailboxV3.sol b/solidity/contracts/MailboxV3.sol index 8b52a16f12..da68500b7f 100644 --- a/solidity/contracts/MailboxV3.sol +++ b/solidity/contracts/MailboxV3.sol @@ -5,17 +5,16 @@ pragma solidity >=0.8.0; import {Versioned} from "./upgrade/Versioned.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 {IMailboxV3} from "./interfaces/IMailboxV3.sol"; // ============ External Imports ============ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -contract MailboxV3 is IMailboxV3, Versioned, ReentrancyGuard, Ownable { +contract MailboxV3 is IMailboxV3, Versioned, Ownable { // ============ Libraries ============ using Message for bytes; @@ -91,7 +90,7 @@ contract MailboxV3 is IMailboxV3, Versioned, ReentrancyGuard, Ownable { uint32 _destinationDomain, bytes32 _recipientAddress, bytes calldata _messageBody - ) external override returns (bytes32) { + ) external payable override returns (bytes32) { // Format the message into packed bytes. bytes memory _message = Message.formatMessage( VERSION, @@ -103,18 +102,13 @@ contract MailboxV3 is IMailboxV3, Versioned, ReentrancyGuard, Ownable { _messageBody ); + // effects nonce += 1; + emit Dispatch(_message); - // Insert the message ID into the merkle tree. - bytes32 _id = _message.id(); - emit Dispatch( - msg.sender, - _destinationDomain, - _recipientAddress, - _message - ); - emit DispatchId(_id); - return _id; + // interactions + defaultHook.postDispatch{value: msg.value}(_message); + return _message.id(); } /** @@ -125,8 +119,8 @@ contract MailboxV3 is IMailboxV3, Versioned, ReentrancyGuard, Ownable { */ function process(bytes calldata _metadata, bytes calldata _message) external + payable override - nonReentrant { // Check that the message was intended for this mailbox. require(_message.version() == VERSION, "!version"); @@ -135,21 +129,25 @@ contract MailboxV3 is IMailboxV3, Versioned, ReentrancyGuard, Ownable { // Check that the message hasn't already been delivered. bytes32 _id = _message.id(); require(delivered[_id] == false, "delivered"); - delivered[_id] = true; + + address recipient = _message.recipientAddress(); // Verify the message via the ISM. IInterchainSecurityModule _ism = IInterchainSecurityModule( - recipientIsm(_message.recipientAddress()) + recipientIsm(recipient) ); require(_ism.verify(_metadata, _message), "!module"); - // Deliver the message to the recipient. - uint32 origin = _message.origin(); - bytes32 sender = _message.sender(); - address recipient = _message.recipientAddress(); - IMessageRecipient(recipient).handle(origin, sender, _message.body()); - emit Process(origin, sender, recipient); - emit ProcessId(_id); + // effects + delivered[_id] = true; + emit Process(_message); + + // Deliver the message to the recipient. (interactions) + IMessageRecipient(recipient).handle{value: msg.value}( + _message.origin(), + _message.sender(), + _message.body() + ); } // ============ Public Functions ============ diff --git a/solidity/contracts/interfaces/IMailboxV3.sol b/solidity/contracts/interfaces/IMailboxV3.sol index a4c2c9e8d7..9e9eda22a4 100644 --- a/solidity/contracts/interfaces/IMailboxV3.sol +++ b/solidity/contracts/interfaces/IMailboxV3.sol @@ -8,41 +8,15 @@ interface IMailboxV3 { // ============ 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 - ); - - /** - * @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 processed - * @param messageId The unique message identifier - */ - event ProcessId(bytes32 indexed messageId); + event Dispatch(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 + * @param message Raw bytes of message */ - event Process( - uint32 indexed origin, - bytes32 indexed sender, - address indexed recipient - ); + event Process(bytes message); function localDomain() external view returns (uint32); @@ -56,10 +30,11 @@ interface IMailboxV3 { uint32 _destinationDomain, bytes32 _recipientAddress, bytes calldata _messageBody - ) external returns (bytes32); + ) external payable returns (bytes32); function process(bytes calldata _metadata, bytes calldata _message) - external; + external + payable; function recipientIsm(address _recipient) external diff --git a/solidity/contracts/interfaces/IMessageRecipientV3.sol b/solidity/contracts/interfaces/IMessageRecipientV3.sol new file mode 100644 index 0000000000..187194b6d1 --- /dev/null +++ b/solidity/contracts/interfaces/IMessageRecipientV3.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +interface IMessageRecipient { + function handle( + uint32 _origin, + bytes32 _sender, + bytes calldata _message + ) external payable; +} From 9ef2da351d34d99047eae2f60e05fb6312604311 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 25 Jul 2023 16:53:22 -0400 Subject: [PATCH 03/27] Update version constant --- solidity/contracts/upgrade/Versioned.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/contracts/upgrade/Versioned.sol b/solidity/contracts/upgrade/Versioned.sol index 2b857bbb7e..c018b2dc6b 100644 --- a/solidity/contracts/upgrade/Versioned.sol +++ b/solidity/contracts/upgrade/Versioned.sol @@ -6,5 +6,5 @@ pragma solidity >=0.6.11; * @notice Version getter for contracts **/ contract Versioned { - uint8 public constant VERSION = 0; + uint8 public constant VERSION = 3; } From a78be691763a85b648870993f2f3514d03487688 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 25 Jul 2023 17:04:41 -0400 Subject: [PATCH 04/27] Keep message ID events --- solidity/contracts/MailboxV3.sol | 5 ++++- solidity/contracts/interfaces/IMailboxV3.sol | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/solidity/contracts/MailboxV3.sol b/solidity/contracts/MailboxV3.sol index da68500b7f..54bbb24a30 100644 --- a/solidity/contracts/MailboxV3.sol +++ b/solidity/contracts/MailboxV3.sol @@ -104,11 +104,13 @@ contract MailboxV3 is IMailboxV3, Versioned, Ownable { // effects nonce += 1; + bytes32 _id = _message.id(); + emit DispatchId(_id); emit Dispatch(_message); // interactions defaultHook.postDispatch{value: msg.value}(_message); - return _message.id(); + return _id; } /** @@ -141,6 +143,7 @@ contract MailboxV3 is IMailboxV3, Versioned, Ownable { // effects delivered[_id] = true; emit Process(_message); + emit ProcessId(_id); // Deliver the message to the recipient. (interactions) IMessageRecipient(recipient).handle{value: msg.value}( diff --git a/solidity/contracts/interfaces/IMailboxV3.sol b/solidity/contracts/interfaces/IMailboxV3.sol index 9e9eda22a4..198e812c23 100644 --- a/solidity/contracts/interfaces/IMailboxV3.sol +++ b/solidity/contracts/interfaces/IMailboxV3.sol @@ -12,12 +12,24 @@ interface IMailboxV3 { */ 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); From f56c9a4001e8362de31c217e6e57c2bc7b3432b3 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:38:37 -0400 Subject: [PATCH 05/27] Kunal/v3 hooks (#2580) ### Description - Adding Optimism Hook for MailboxV3 ### Drive-by changes - dispatch function with hookMetadata ### Related issues - https://github.com/hyperlane-xyz/issues/issues/513 ### Backward compatibility No ### Testing None --- .github/workflows/node.yml | 3 +- solidity/contracts/MailboxV3.sol | 78 ++++++++++---- solidity/contracts/hooks/AbstractHook.sol | 12 ++- .../contracts/hooks/DomainRoutingHook.sol | 10 +- solidity/contracts/hooks/MerkleTreeHook.sol | 5 +- solidity/contracts/hooks/OPStackHook.sol | 101 ++++++++++++++++++ solidity/contracts/hooks/PausableHook.sol | 2 +- solidity/contracts/interfaces/IMailboxV3.sol | 17 ++- .../interfaces/hooks/IPostDispatchHook.sol | 4 +- .../optimism/ICrossDomainMessenger.sol | 20 ++++ .../hook/AbstractMessageIdAuthorizedIsm.sol | 91 ++++++++++++++++ solidity/contracts/isms/hook/OPStackISM.sol | 61 +++++++++++ .../libs/hooks/OPStackHookMetadata.sol | 17 +++ solidity/hardhat.config.ts | 10 +- 14 files changed, 396 insertions(+), 35 deletions(-) create mode 100644 solidity/contracts/hooks/OPStackHook.sol create mode 100644 solidity/contracts/interfaces/optimism/ICrossDomainMessenger.sol create mode 100644 solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol create mode 100644 solidity/contracts/isms/hook/OPStackISM.sol create mode 100644 solidity/contracts/libs/hooks/OPStackHookMetadata.sol diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 92474d72ad..837a4d1337 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -5,8 +5,7 @@ on: push: branches: [main] pull_request: - branches: [main] - + branches: [v3] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/solidity/contracts/MailboxV3.sol b/solidity/contracts/MailboxV3.sol index 54bbb24a30..9537da4e27 100644 --- a/solidity/contracts/MailboxV3.sol +++ b/solidity/contracts/MailboxV3.sol @@ -91,26 +91,36 @@ contract MailboxV3 is IMailboxV3, Versioned, Ownable { 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); + return + _dispatch( + _destinationDomain, + _recipientAddress, + _messageBody, + bytes("") + ); + } - // interactions - defaultHook.postDispatch{value: msg.value}(_message); - return _id; + /** + * @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, + hookMetadata + ); } /** @@ -182,4 +192,34 @@ contract MailboxV3 is IMailboxV3, Versioned, Ownable { } catch {} return defaultIsm; } + + // ============ Internal Functions ============ + + function _dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody, + bytes memory hookMetadata + ) internal 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}(hookMetadata, message); + return id; + } } diff --git a/solidity/contracts/hooks/AbstractHook.sol b/solidity/contracts/hooks/AbstractHook.sol index 7b03d44450..ca47546bfa 100644 --- a/solidity/contracts/hooks/AbstractHook.sol +++ b/solidity/contracts/hooks/AbstractHook.sol @@ -7,9 +7,15 @@ 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 metadata, bytes calldata message) + external + payable + onlyMailbox + { + _postDispatch(metadata, message); } - function _postDispatch(bytes calldata message) internal virtual; + function _postDispatch(bytes calldata metadata, bytes calldata message) + internal + virtual; } diff --git a/solidity/contracts/hooks/DomainRoutingHook.sol b/solidity/contracts/hooks/DomainRoutingHook.sol index a2d71c1f87..e57bde7715 100644 --- a/solidity/contracts/hooks/DomainRoutingHook.sol +++ b/solidity/contracts/hooks/DomainRoutingHook.sol @@ -21,7 +21,13 @@ contract DomainRoutingHook is AbstractHook, Ownable { hooks[destination] = IPostDispatchHook(hook); } - function _postDispatch(bytes calldata message) internal override { - hooks[message.destination()].postDispatch{value: msg.value}(message); + function _postDispatch(bytes calldata metadata, bytes calldata message) + internal + override + { + hooks[message.destination()].postDispatch{value: msg.value}( + metadata, + message + ); } } diff --git a/solidity/contracts/hooks/MerkleTreeHook.sol b/solidity/contracts/hooks/MerkleTreeHook.sol index 19b2367a0e..a230e24797 100644 --- a/solidity/contracts/hooks/MerkleTreeHook.sol +++ b/solidity/contracts/hooks/MerkleTreeHook.sol @@ -34,7 +34,10 @@ contract MerkleTreeHook is AbstractHook { return (root(), count() - 1); } - function _postDispatch(bytes calldata message) internal override { + function _postDispatch( + bytes calldata, /*metadata*/ + bytes calldata message + ) internal override { _tree.insert(message.id()); } } diff --git a/solidity/contracts/hooks/OPStackHook.sol b/solidity/contracts/hooks/OPStackHook.sol new file mode 100644 index 0000000000..711f8d7174 --- /dev/null +++ b/solidity/contracts/hooks/OPStackHook.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ +import {AbstractHook} from "./AbstractHook.sol"; +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"; + +// ============ 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 AbstractHook { + using Message for bytes; + using OPStackHookMetadata for bytes; + using TypeCasts for address; + + // ============ Constants ============ + + // Domain of chain on which the OPStack ISM is deployed + uint32 public immutable destinationDomain; + // Messenger used to send messages from L1 -> L2 + ICrossDomainMessenger public immutable l1Messenger; + // address for OPStack 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( + address _mailbox, + uint32 _destinationDomain, + address _messenger, + address _ism + ) AbstractHook(_mailbox) { + require( + _destinationDomain != 0, + "OPStackHook: invalid destination domain" + ); + require(_ism != address(0), "OPStackHook: invalid ISM"); + destinationDomain = _destinationDomain; + + require( + Address.isContract(_messenger), + "OPStackHook: invalid messenger" + ); + l1Messenger = ICrossDomainMessenger(_messenger); + ism = _ism; + } + + // ============ External Functions ============ + + /** + * @notice Hook to inform the optimism ISM of messages published through. + * metadata The metadata for the hook caller (unused) + * @param message The message being dispatched + */ + function _postDispatch(bytes calldata metadata, bytes calldata message) + internal + override + { + bytes32 messageId = message.id(); + uint256 msgValue = metadata.msgValue(); + + require( + message.destination() == destinationDomain, + "OPStackHook: invalid destination domain" + ); + + bytes memory payload = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) + ); + + // send the rest of the val + l1Messenger.sendMessage{value: msgValue}(ism, payload, GAS_LIMIT); + } +} diff --git a/solidity/contracts/hooks/PausableHook.sol b/solidity/contracts/hooks/PausableHook.sol index b3feba3165..1f31b98ceb 100644 --- a/solidity/contracts/hooks/PausableHook.sol +++ b/solidity/contracts/hooks/PausableHook.sol @@ -9,7 +9,7 @@ import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; contract PausableHook is AbstractHook, Ownable, Pausable { constructor(address _mailbox) AbstractHook(_mailbox) {} - function _postDispatch(bytes calldata message) + function _postDispatch(bytes calldata metadata, bytes calldata message) internal override whenNotPaused diff --git a/solidity/contracts/interfaces/IMailboxV3.sol b/solidity/contracts/interfaces/IMailboxV3.sol index 198e812c23..cc489c2965 100644 --- a/solidity/contracts/interfaces/IMailboxV3.sol +++ b/solidity/contracts/interfaces/IMailboxV3.sol @@ -39,16 +39,23 @@ interface IMailboxV3 { function defaultHook() external view returns (IPostDispatchHook); function dispatch( - uint32 _destinationDomain, - bytes32 _recipientAddress, - bytes calldata _messageBody + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody ) external payable returns (bytes32); - function process(bytes calldata _metadata, bytes calldata _message) + function dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata body, + bytes calldata hookMetadata + ) external payable returns (bytes32); + + function process(bytes calldata metadata, bytes calldata message) external payable; - function recipientIsm(address _recipient) + function recipientIsm(address recipient) external view returns (IInterchainSecurityModule); diff --git a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol index c6daf084d4..737268a486 100644 --- a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol +++ b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol @@ -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; } diff --git a/solidity/contracts/interfaces/optimism/ICrossDomainMessenger.sol b/solidity/contracts/interfaces/optimism/ICrossDomainMessenger.sol new file mode 100644 index 0000000000..19bcbf55e5 --- /dev/null +++ b/solidity/contracts/interfaces/optimism/ICrossDomainMessenger.sol @@ -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; +} diff --git a/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol b/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol new file mode 100644 index 0000000000..5cec2f31ba --- /dev/null +++ b/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ + +import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; +import {Message} from "../../libs/Message.sol"; +import {TypeCasts} from "../../libs/TypeCasts.sol"; + +// ============ External Imports ============ + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @title ArbtractNativeISM + * @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 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 => bool) public verifiedMessageIds; + // Address for Hook on L1 responsible for sending message via the Optimism bridge + address public authorizedHook; + + // ============ Events ============ + + event ReceivedMessage(bytes32 indexed messageId); + + // ============ Initializer ============ + + function setAuthorizedHook(address _hook) external initializer { + require( + _hook != address(0), + "AbstractNativeISM: invalid authorized hook" + ); + authorizedHook = _hook; + } + + // ============ External Functions ============ + + /** + * @notice Verify a message was received by ISM. + * @param _message Message to verify. + */ + function verify( + bytes calldata, /*_metadata*/ + bytes calldata _message + ) external view returns (bool) { + bytes32 _messageId = Message.id(_message); + + 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 virtual { + require( + _isAuthorized(), + "AbstractMessageIdAuthorizedIsm: sender is not the hook" + ); + + verifiedMessageIds[_messageId] = true; + emit ReceivedMessage(_messageId); + } + + function _isAuthorized() internal view virtual returns (bool); +} diff --git a/solidity/contracts/isms/hook/OPStackISM.sol b/solidity/contracts/isms/hook/OPStackISM.sol new file mode 100644 index 0000000000..4ec60235b1 --- /dev/null +++ b/solidity/contracts/isms/hook/OPStackISM.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ 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 {AbstractMessageIdAuthorizedIsm} from "./AbstractMessageIdAuthorizedIsm.sol"; +import {CrossChainEnabledOptimism} from "./crossChainEnabled/optimism/CrossChainEnabledOptimism.sol"; + +// ============ External Imports ============ + +import {ICrossDomainMessenger} from "@eth-optimism/contracts/libraries/bridge/ICrossDomainMessenger.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title OptimismISM + * @notice Uses the native Optimism bridge to verify interchain messages. + * @dev V3 WIP + */ +contract OPStackIsm is + CrossChainEnabledOptimism, + AbstractMessageIdAuthorizedIsm +{ + // ============ Constants ============ + + uint8 public constant moduleType = + uint8(IInterchainSecurityModule.Types.NULL); + + // ============ Constructor ============ + + constructor(address _l2Messenger) CrossChainEnabledOptimism(_l2Messenger) { + require( + Address.isContract(_l2Messenger), + "OPStackIsm: invalid L2Messenger" + ); + } + + // ============ Internal function ============ + + /** + * @notice Check if sender is authorized to message `verifyMessageId`. + */ + function _isAuthorized() internal view override returns (bool) { + return _crossChainSender() == authorizedHook; + } +} diff --git a/solidity/contracts/libs/hooks/OPStackHookMetadata.sol b/solidity/contracts/libs/hooks/OPStackHookMetadata.sol new file mode 100644 index 0000000000..b598032500 --- /dev/null +++ b/solidity/contracts/libs/hooks/OPStackHookMetadata.sol @@ -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])); + } +} diff --git a/solidity/hardhat.config.ts b/solidity/hardhat.config.ts index 7e3d03441f..518ee335aa 100644 --- a/solidity/hardhat.config.ts +++ b/solidity/hardhat.config.ts @@ -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, From fba00e1390c9dc523d57d8bd32d8e0cf57922da0 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Wed, 2 Aug 2023 19:24:05 -0400 Subject: [PATCH 06/27] Add msg.sender to delivery mapping --- solidity/contracts/MailboxV3.sol | 111 ++++++++++++++++++------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/solidity/contracts/MailboxV3.sol b/solidity/contracts/MailboxV3.sol index 9537da4e27..00972b4eb0 100644 --- a/solidity/contracts/MailboxV3.sol +++ b/solidity/contracts/MailboxV3.sol @@ -37,8 +37,17 @@ contract MailboxV3 is IMailboxV3, Versioned, Ownable { // 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; + // Mapping of message ID to Delivery struct + struct Delivery { + address sender; + // uint48 timestamp; + // uint48 gasUsed? + } + mapping(bytes32 => Delivery) internal deliveries; + + function delivered(bytes32 messageId) public view override returns (bool) { + return deliveries[messageId].sender != address(0x0); + } // ============ Events ============ @@ -91,12 +100,13 @@ contract MailboxV3 is IMailboxV3, Versioned, Ownable { bytes32 _recipientAddress, bytes calldata _messageBody ) external payable override returns (bytes32) { + bytes calldata _defaultHookMetadata = _messageBody[0:0]; // empty calldata bytes return - _dispatch( + dispatch( _destinationDomain, _recipientAddress, _messageBody, - bytes("") + _defaultHookMetadata ); } @@ -105,24 +115,62 @@ contract MailboxV3 is IMailboxV3, Versioned, Ownable { * @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 + * @param defaultHookMetadata 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) { + bytes calldata defaultHookMetadata + ) public payable override returns (bytes32) { return - _dispatch( + dispatch( destinationDomain, recipientAddress, messageBody, - hookMetadata + defaultHook, + defaultHookMetadata ); } + /** + * @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, + IPostDispatchHook hook, + bytes calldata hookMetadata + ) public payable 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 + hook.postDispatch{value: msg.value}(hookMetadata, message); + return id; + } + /** * @notice Attempts to deliver `_message` to its recipient. Verifies * `_message` via the recipient's ISM using the provided `_metadata`. @@ -140,22 +188,23 @@ contract MailboxV3 is IMailboxV3, Versioned, Ownable { // Check that the message hasn't already been delivered. bytes32 _id = _message.id(); - require(delivered[_id] == false, "delivered"); + require(delivered(_id) == false, "delivered"); address recipient = _message.recipientAddress(); + // effects + deliveries[_id].sender = msg.sender; + emit Process(_message); + emit ProcessId(_id); + + // interactions // 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) + // Deliver the message to the recipient. IMessageRecipient(recipient).handle{value: msg.value}( _message.origin(), _message.sender(), @@ -192,34 +241,4 @@ contract MailboxV3 is IMailboxV3, Versioned, Ownable { } catch {} return defaultIsm; } - - // ============ Internal Functions ============ - - function _dispatch( - uint32 destinationDomain, - bytes32 recipientAddress, - bytes calldata messageBody, - bytes memory hookMetadata - ) internal 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}(hookMetadata, message); - return id; - } } From 0f3784897a01f77ce272be01c4e0fb1cc7b3f239 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Thu, 3 Aug 2023 13:03:54 -0400 Subject: [PATCH 07/27] Begin merge --- solidity/contracts/Mailbox.sol | 227 +++++++--------- solidity/contracts/MailboxV3.sol | 244 ------------------ ...MessageHook.sol => AbstractBridgeHook.sol} | 65 ++--- solidity/contracts/hooks/AbstractHook.sol | 7 +- .../hooks/AbstractMessageIdAuthHook.sol | 86 ++++++ .../hooks/ERC5164/ERC5164MessageHook.sol | 95 ------- solidity/contracts/hooks/ERC5164Hook.sol | 52 ++++ solidity/contracts/hooks/OPStackHook.sol | 57 +--- solidity/contracts/interfaces/IMailbox.sol | 65 +++-- solidity/contracts/interfaces/IMailboxV3.sol | 62 ----- .../hooks}/IMessageDispatcher.sol | 0 .../interfaces/hooks/IMessageHook.sol | 9 - .../interfaces/hooks/IPostDispatchHook.sol | 3 +- .../contracts/isms/hook/AbstractHookISM.sol | 59 ----- solidity/contracts/isms/hook/ERC5164ISM.sol | 33 +-- solidity/contracts/isms/hook/OPStackISM.sol | 1 - solidity/contracts/isms/hook/OptimismISM.sol | 91 ------- solidity/contracts/mock/MockERC5164.sol | 2 +- solidity/test/isms/ERC5164ISM.t.sol | 17 +- .../{OptimismISM.t.sol => OPStackIsm.t.sol} | 10 +- 20 files changed, 341 insertions(+), 844 deletions(-) delete mode 100644 solidity/contracts/MailboxV3.sol rename solidity/contracts/hooks/{OptimismMessageHook.sol => AbstractBridgeHook.sol} (52%) create mode 100644 solidity/contracts/hooks/AbstractMessageIdAuthHook.sol delete mode 100644 solidity/contracts/hooks/ERC5164/ERC5164MessageHook.sol create mode 100644 solidity/contracts/hooks/ERC5164Hook.sol delete mode 100644 solidity/contracts/interfaces/IMailboxV3.sol rename solidity/contracts/{hooks/ERC5164/interfaces => interfaces/hooks}/IMessageDispatcher.sol (100%) delete mode 100644 solidity/contracts/interfaces/hooks/IMessageHook.sol delete mode 100644 solidity/contracts/isms/hook/AbstractHookISM.sol delete mode 100644 solidity/contracts/isms/hook/OptimismISM.sol rename solidity/test/isms/{OptimismISM.t.sol => OPStackIsm.t.sol} (98%) diff --git a/solidity/contracts/Mailbox.sol b/solidity/contracts/Mailbox.sol index b6b5ab5a33..bcf077b51d 100644 --- a/solidity/contracts/Mailbox.sol +++ b/solidity/contracts/Mailbox.sol @@ -3,51 +3,42 @@ 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 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 whether or not that message has been delivered. + mapping(bytes32 => bool) public delivered; // ============ Events ============ @@ -58,31 +49,16 @@ contract Mailbox is event DefaultIsmSet(address indexed module); /** - * @notice Emitted when Mailbox is paused + * @notice Emitted when the default hook is updated + * @param hook The new default hook */ - event Paused(); - - /** - * @notice Emitted when Mailbox is unpaused - */ - 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 +68,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 +90,37 @@ 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"); - // Format the message into packed bytes. - bytes memory _message = Message.formatMessage( - VERSION, - count(), - localDomain, - msg.sender.addressToBytes32(), - _destinationDomain, - _recipientAddress, - _messageBody - ); + ) external payable override returns (bytes32) { + return + _dispatch( + _destinationDomain, + _recipientAddress, + _messageBody, + bytes("") + ); + } - // 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; + /** + * @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, + hookMetadata + ); } /** @@ -140,8 +131,8 @@ contract Mailbox is */ function process(bytes calldata _metadata, bytes calldata _message) external + payable override - nonReentrantAndNotPaused { // Check that the message was intended for this mailbox. require(_message.version() == VERSION, "!version"); @@ -150,73 +141,29 @@ contract Mailbox is // Check that the message hasn't already been delivered. bytes32 _id = _message.id(); require(delivered[_id] == false, "delivered"); - delivered[_id] = true; + + address recipient = _message.recipientAddress(); // Verify the message via the ISM. IInterchainSecurityModule _ism = IInterchainSecurityModule( - recipientIsm(_message.recipientAddress()) + recipientIsm(recipient) ); require(_ism.verify(_metadata, _message), "!module"); - // Deliver the message to the recipient. - uint32 origin = _message.origin(); - bytes32 sender = _message.sender(); - address recipient = _message.recipientAddress(); - IMessageRecipient(recipient).handle(origin, sender, _message.body()); - emit Process(origin, sender, recipient); + // effects + delivered[_id] = true; + emit Process(_message); emit ProcessId(_id); - } - // ============ Public Functions ============ - - /** - * @notice Calculates and returns tree's current root - */ - function root() public view returns (bytes32) { - return tree.root(); - } - - /** - * @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); - } - - /** - * @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); - } - - /** - * @notice Pauses mailbox and prevents further dispatch/process calls - * @dev Only `owner` can pause the mailbox. - */ - function pause() external onlyOwner { - _pause(); - emit Paused(); - } - - /** - * @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. (interactions) + 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 @@ -248,13 +195,31 @@ contract Mailbox is // ============ 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); + function _dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody, + bytes memory hookMetadata + ) internal 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}(hookMetadata, message); + return id; } } diff --git a/solidity/contracts/MailboxV3.sol b/solidity/contracts/MailboxV3.sol deleted file mode 100644 index 00972b4eb0..0000000000 --- a/solidity/contracts/MailboxV3.sol +++ /dev/null @@ -1,244 +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 Delivery struct - struct Delivery { - address sender; - // uint48 timestamp; - // uint48 gasUsed? - } - mapping(bytes32 => Delivery) internal deliveries; - - function delivered(bytes32 messageId) public view override returns (bool) { - return deliveries[messageId].sender != address(0x0); - } - - // ============ 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) { - bytes calldata _defaultHookMetadata = _messageBody[0:0]; // empty calldata bytes - return - dispatch( - _destinationDomain, - _recipientAddress, - _messageBody, - _defaultHookMetadata - ); - } - - /** - * @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 defaultHookMetadata 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 defaultHookMetadata - ) public payable override returns (bytes32) { - return - dispatch( - destinationDomain, - recipientAddress, - messageBody, - defaultHook, - defaultHookMetadata - ); - } - - /** - * @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, - IPostDispatchHook hook, - bytes calldata hookMetadata - ) public payable 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 - hook.postDispatch{value: msg.value}(hookMetadata, 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(); - - // effects - deliveries[_id].sender = msg.sender; - emit Process(_message); - emit ProcessId(_id); - - // interactions - // Verify the message via the ISM. - IInterchainSecurityModule _ism = IInterchainSecurityModule( - recipientIsm(recipient) - ); - require(_ism.verify(_metadata, _message), "!module"); - - // Deliver the message to the recipient. - 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; - } -} diff --git a/solidity/contracts/hooks/OptimismMessageHook.sol b/solidity/contracts/hooks/AbstractBridgeHook.sol similarity index 52% rename from solidity/contracts/hooks/OptimismMessageHook.sol rename to solidity/contracts/hooks/AbstractBridgeHook.sol index 527353539d..53cb7a31b7 100644 --- a/solidity/contracts/hooks/OptimismMessageHook.sol +++ b/solidity/contracts/hooks/AbstractBridgeHook.sol @@ -14,28 +14,34 @@ pragma solidity >=0.8.0; @@@@@@@@@ @@@@@@@@*/ // ============ Internal Imports ============ -import {IMessageHook} from "../interfaces/hooks/IMessageHook.sol"; -import {OptimismISM} from "../isms/hook/OptimismISM.sol"; +import {AbstractHook} from "./AbstractHook.sol"; +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"; // ============ External Imports ============ -import {ICrossDomainMessenger} from "@eth-optimism/contracts/libraries/bridge/ICrossDomainMessenger.sol"; +import {ICrossDomainMessenger} from "../interfaces/optimism/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. + * @title AbstractBridgeHook + * @notice Message hook to inform an Abstract Message ID ISM of messages published through + * the native OPStack bridge. + * @dev V3 WIP */ -contract OptimismMessageHook is IMessageHook { +abstract contract AbstractBridgeHook is AbstractHook { + using Message for bytes; + using OPStackHookMetadata for bytes; using TypeCasts for address; + // ============ Constants ============ - // Domain of chain on which the optimism ISM is deployed + // Domain of chain on which the OPStack 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 for OPStack 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: @@ -45,20 +51,21 @@ contract OptimismMessageHook is IMessageHook { // ============ Constructor ============ constructor( + address _mailbox, uint32 _destinationDomain, address _messenger, address _ism - ) { + ) AbstractHook(_mailbox) { require( _destinationDomain != 0, - "OptimismHook: invalid destination domain" + "OPStackHook: invalid destination domain" ); - require(_ism != address(0), "OptimismHook: invalid ISM"); + require(_ism != address(0), "OPStackHook: invalid ISM"); destinationDomain = _destinationDomain; require( Address.isContract(_messenger), - "OptimismHook: invalid messenger" + "OPStackHook: invalid messenger" ); l1Messenger = ICrossDomainMessenger(_messenger); ism = _ism; @@ -68,31 +75,27 @@ contract OptimismMessageHook is IMessageHook { /** * @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. + * metadata The metadata for the hook caller (unused) + * @param message The message being dispatched */ - function postDispatch(uint32 _destination, bytes32 _messageId) - public - payable + function _postDispatch(bytes calldata metadata, bytes calldata message) + internal override - returns (uint256) { - require(msg.value == 0, "OptimismHook: no value allowed"); + bytes32 messageId = message.id(); + uint256 msgValue = metadata.msgValue(); + require( - _destination == destinationDomain, - "OptimismHook: invalid destination domain" + message.destination() == destinationDomain, + "OPStackHook: invalid destination domain" ); - bytes memory _payload = abi.encodeCall( - OptimismISM.verifyMessageId, - (msg.sender.addressToBytes32(), _messageId) + bytes memory payload = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) ); - l1Messenger.sendMessage(ism, _payload, GAS_LIMIT); - - // calling the verifyMessageId function is ~25k gas but we get 1.92m gas from Optimism - return 0; + // send the rest of the val + l1Messenger.sendMessage{value: msgValue}(ism, payload, GAS_LIMIT); } } diff --git a/solidity/contracts/hooks/AbstractHook.sol b/solidity/contracts/hooks/AbstractHook.sol index ca47546bfa..673599754c 100644 --- a/solidity/contracts/hooks/AbstractHook.sol +++ b/solidity/contracts/hooks/AbstractHook.sol @@ -10,12 +10,15 @@ abstract contract AbstractHook is MailboxClient, IPostDispatchHook { function postDispatch(bytes calldata metadata, bytes calldata message) external payable + override onlyMailbox + returns (IPostDispatchHook) { - _postDispatch(metadata, message); + return _postDispatch(metadata, message); } function _postDispatch(bytes calldata metadata, bytes calldata message) internal - virtual; + virtual + returns (IPostDispatchHook); } diff --git a/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol b/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol new file mode 100644 index 0000000000..f2d6cb1e44 --- /dev/null +++ b/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ +import {AbstractHook} from "./AbstractHook.sol"; +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 {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 AbstractHook { + 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 + ) AbstractHook(_mailbox) { + require(_ism != address(0), "invalid ISM"); + require(_destinationDomain != 0, "invalid destination domain"); + ism = _ism; + destinationDomain = _destinationDomain; + } + + function _sendMessageId(bytes calldata metadata, bytes memory payload) + internal + virtual; + + /** + * @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) + internal + override + returns (IPostDispatchHook) + { + require( + message.destination() == destinationDomain, + "invalid destination domain" + ); + + bytes memory payload = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + message.id() + ); + _sendMessageId(metadata, payload); + + // no next post-dispatch hook + // TODO: consider configuring? + return IPostDispatchHook(address(0)); + } +} diff --git a/solidity/contracts/hooks/ERC5164/ERC5164MessageHook.sol b/solidity/contracts/hooks/ERC5164/ERC5164MessageHook.sol deleted file mode 100644 index 5cb2dbf95b..0000000000 --- a/solidity/contracts/hooks/ERC5164/ERC5164MessageHook.sol +++ /dev/null @@ -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; - } -} diff --git a/solidity/contracts/hooks/ERC5164Hook.sol b/solidity/contracts/hooks/ERC5164Hook.sol new file mode 100644 index 0000000000..805f58d99a --- /dev/null +++ b/solidity/contracts/hooks/ERC5164Hook.sol @@ -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); + } +} diff --git a/solidity/contracts/hooks/OPStackHook.sol b/solidity/contracts/hooks/OPStackHook.sol index 711f8d7174..0e70c42dd4 100644 --- a/solidity/contracts/hooks/OPStackHook.sol +++ b/solidity/contracts/hooks/OPStackHook.sol @@ -14,11 +14,11 @@ pragma solidity >=0.8.0; @@@@@@@@@ @@@@@@@@*/ // ============ Internal Imports ============ -import {AbstractHook} from "./AbstractHook.sol"; -import {AbstractMessageIdAuthorizedIsm} from "../isms/hook/AbstractMessageIdAuthorizedIsm.sol"; +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"; @@ -30,19 +30,13 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; * the native OPStack bridge. * @dev V3 WIP */ -contract OPStackHook is AbstractHook { - using Message for bytes; +contract OPStackHook is AbstractMessageIdAuthHook { using OPStackHookMetadata for bytes; - using TypeCasts for address; // ============ Constants ============ - // Domain of chain on which the OPStack ISM is deployed - uint32 public immutable destinationDomain; - // Messenger used to send messages from L1 -> L2 ICrossDomainMessenger public immutable l1Messenger; - // address for OPStack 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 @@ -53,49 +47,24 @@ contract OPStackHook is AbstractHook { constructor( address _mailbox, uint32 _destinationDomain, - address _messenger, - address _ism - ) AbstractHook(_mailbox) { - require( - _destinationDomain != 0, - "OPStackHook: invalid destination domain" - ); - require(_ism != address(0), "OPStackHook: invalid ISM"); - destinationDomain = _destinationDomain; - + address _ism, + address _messenger + ) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) { require( Address.isContract(_messenger), - "OPStackHook: invalid messenger" + "ERC5164Hook: invalid dispatcher" ); l1Messenger = ICrossDomainMessenger(_messenger); - ism = _ism; } - // ============ External Functions ============ - - /** - * @notice Hook to inform the optimism ISM of messages published through. - * metadata The metadata for the hook caller (unused) - * @param message The message being dispatched - */ - function _postDispatch(bytes calldata metadata, bytes calldata message) + function _sendMessageId(bytes calldata metadata, bytes memory payload) internal override { - bytes32 messageId = message.id(); - uint256 msgValue = metadata.msgValue(); - - require( - message.destination() == destinationDomain, - "OPStackHook: invalid destination domain" + l1Messenger.sendMessage{value: metadata.msgValue()}( + ism, + payload, + GAS_LIMIT ); - - bytes memory payload = abi.encodeCall( - AbstractMessageIdAuthorizedIsm.verifyMessageId, - (messageId) - ); - - // send the rest of the val - l1Messenger.sendMessage{value: msgValue}(ism, payload, GAS_LIMIT); } } diff --git a/solidity/contracts/interfaces/IMailbox.sol b/solidity/contracts/interfaces/IMailbox.sol index 0a6dd3c2bf..1581a0ab0c 100644 --- a/solidity/contracts/interfaces/IMailbox.sol +++ b/solidity/contracts/interfaces/IMailbox.sol @@ -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,35 @@ 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 dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody + ) external payable returns (bytes32 messageId); - function count() external view returns (uint32); + function dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata body, + bytes calldata defaultHookMetadata + ) external payable returns (bytes32 messageId); - function root() external view returns (bytes32); + function dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata body, + IPostDispatchHook customHook, + bytes calldata customHookMetadata + ) external payable returns (bytes32 messageId); - function latestCheckpoint() external view returns (bytes32, uint32); + 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); } diff --git a/solidity/contracts/interfaces/IMailboxV3.sol b/solidity/contracts/interfaces/IMailboxV3.sol deleted file mode 100644 index cc489c2965..0000000000 --- a/solidity/contracts/interfaces/IMailboxV3.sol +++ /dev/null @@ -1,62 +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 dispatch( - uint32 destinationDomain, - bytes32 recipientAddress, - bytes calldata body, - bytes calldata hookMetadata - ) external payable returns (bytes32); - - function process(bytes calldata metadata, bytes calldata message) - external - payable; - - function recipientIsm(address recipient) - external - view - returns (IInterchainSecurityModule); -} diff --git a/solidity/contracts/hooks/ERC5164/interfaces/IMessageDispatcher.sol b/solidity/contracts/interfaces/hooks/IMessageDispatcher.sol similarity index 100% rename from solidity/contracts/hooks/ERC5164/interfaces/IMessageDispatcher.sol rename to solidity/contracts/interfaces/hooks/IMessageDispatcher.sol diff --git a/solidity/contracts/interfaces/hooks/IMessageHook.sol b/solidity/contracts/interfaces/hooks/IMessageHook.sol deleted file mode 100644 index 67e0740bcc..0000000000 --- a/solidity/contracts/interfaces/hooks/IMessageHook.sol +++ /dev/null @@ -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); -} diff --git a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol index 737268a486..cdcc123d2c 100644 --- a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol +++ b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol @@ -4,5 +4,6 @@ pragma solidity >=0.8.0; interface IPostDispatchHook { function postDispatch(bytes calldata metadata, bytes calldata message) external - payable; + payable + returns (IPostDispatchHook next); } diff --git a/solidity/contracts/isms/hook/AbstractHookISM.sol b/solidity/contracts/isms/hook/AbstractHookISM.sol deleted file mode 100644 index 99b327d945..0000000000 --- a/solidity/contracts/isms/hook/AbstractHookISM.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; - -/*@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@ HYPERLANE @@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ -@@@@@@@@@ @@@@@@@@*/ - -// ============ Internal Imports ============ - -import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; -import {Message} from "../../libs/Message.sol"; -import {TypeCasts} from "../../libs/TypeCasts.sol"; - -// ============ External Imports ============ - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - -/** - * @title ArbtractNativeISM - * @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 - */ -abstract contract AbstractHookISM 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; - - // ============ Events ============ - - event ReceivedMessage(bytes32 indexed sender, bytes32 indexed messageId); - - // ============ External Functions ============ - - /** - * @notice Verify a message was received by ISM. - * @param _message Message to verify. - */ - function verify( - bytes calldata, /*_metadata*/ - bytes calldata _message - ) external view returns (bool) { - bytes32 _messageId = Message.id(_message); - bytes32 _messageSender = Message.sender(_message); - - return verifiedMessageIds[_messageId][_messageSender]; - } -} diff --git a/solidity/contracts/isms/hook/ERC5164ISM.sol b/solidity/contracts/isms/hook/ERC5164ISM.sol index a4f232f73f..604cd7839b 100644 --- a/solidity/contracts/isms/hook/ERC5164ISM.sol +++ b/solidity/contracts/isms/hook/ERC5164ISM.sol @@ -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; } } diff --git a/solidity/contracts/isms/hook/OPStackISM.sol b/solidity/contracts/isms/hook/OPStackISM.sol index 4ec60235b1..772a7f99ec 100644 --- a/solidity/contracts/isms/hook/OPStackISM.sol +++ b/solidity/contracts/isms/hook/OPStackISM.sol @@ -16,7 +16,6 @@ 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 {AbstractMessageIdAuthorizedIsm} from "./AbstractMessageIdAuthorizedIsm.sol"; diff --git a/solidity/contracts/isms/hook/OptimismISM.sol b/solidity/contracts/isms/hook/OptimismISM.sol deleted file mode 100644 index 1007450cfe..0000000000 --- a/solidity/contracts/isms/hook/OptimismISM.sol +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; - -/*@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@ HYPERLANE @@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ -@@@@@@@@@ @@@@@@@@*/ - -// ============ 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 {CrossChainEnabledOptimism} from "./crossChainEnabled/optimism/CrossChainEnabledOptimism.sol"; - -// ============ External Imports ============ - -import {ICrossDomainMessenger} from "@eth-optimism/contracts/libraries/bridge/ICrossDomainMessenger.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - -/** - * @title OptimismISM - * @notice Uses the native Optimism bridge to verify interchain messages. - */ -contract OptimismISM is CrossChainEnabledOptimism, AbstractHookISM { - // ============ 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" - ); - } - - // ============ Initializer ============ - - function setOptimismHook(address _l1Hook) external initializer { - require(_l1Hook != address(0), "OptimismISM: invalid l1Hook"); - l1Hook = _l1Hook; - } - - // ============ External Functions ============ - - /** - * @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. - */ - function verifyMessageId(bytes32 _sender, bytes32 _messageId) - external - isAuthorized - { - verifiedMessageIds[_messageId][_sender] = true; - - emit ReceivedMessage(_sender, _messageId); - } -} diff --git a/solidity/contracts/mock/MockERC5164.sol b/solidity/contracts/mock/MockERC5164.sol index 5c18e6b67d..4175c43092 100644 --- a/solidity/contracts/mock/MockERC5164.sol +++ b/solidity/contracts/mock/MockERC5164.sol @@ -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( diff --git a/solidity/test/isms/ERC5164ISM.t.sol b/solidity/test/isms/ERC5164ISM.t.sol index fdbdffeffe..4f5ffac0ed 100644 --- a/solidity/test/isms/ERC5164ISM.t.sol +++ b/solidity/test/isms/ERC5164ISM.t.sol @@ -6,19 +6,20 @@ 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 {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 +33,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 +55,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) ); } diff --git a/solidity/test/isms/OptimismISM.t.sol b/solidity/test/isms/OPStackIsm.t.sol similarity index 98% rename from solidity/test/isms/OptimismISM.t.sol rename to solidity/test/isms/OPStackIsm.t.sol index 2df8517eb2..18ca554a1b 100644 --- a/solidity/test/isms/OptimismISM.t.sol +++ b/solidity/test/isms/OPStackIsm.t.sol @@ -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 = From 8cabf55b9430969ad2285c1e2d8dd9859f55124f Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Thu, 3 Aug 2023 13:16:17 -0400 Subject: [PATCH 08/27] More mailbox changes --- solidity/contracts/Mailbox.sol | 90 +++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/solidity/contracts/Mailbox.sol b/solidity/contracts/Mailbox.sol index bcf077b51d..eda3acc40c 100644 --- a/solidity/contracts/Mailbox.sol +++ b/solidity/contracts/Mailbox.sol @@ -37,8 +37,13 @@ contract Mailbox is IMailbox, Versioned, Ownable { // 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; + // Mapping of message ID to sender of the call that processed the message. + struct Delivery { + address sender; + // uint48 value? + // uint48 timestamp? + } + mapping(bytes32 => Delivery) internal deliveries; // ============ Events ============ @@ -92,7 +97,7 @@ contract Mailbox is IMailbox, Versioned, Ownable { bytes calldata _messageBody ) external payable override returns (bytes32) { return - _dispatch( + dispatch( _destinationDomain, _recipientAddress, _messageBody, @@ -115,14 +120,48 @@ contract Mailbox is IMailbox, Versioned, Ownable { bytes calldata hookMetadata ) external payable override returns (bytes32) { return - _dispatch( + dispatch( destinationDomain, recipientAddress, messageBody, + defaultHook, hookMetadata ); } + function dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody, + IPostDispatchHook hook, + bytes calldata metadata + ) public 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 + hook.postDispatch{value: msg.value}(metadata, message); + return id; + } + + function delivered(bytes32 _id) public view override returns (bool) { + return deliveries[_id].sender != address(0); + } + /** * @notice Attempts to deliver `_message` to its recipient. Verifies * `_message` via the recipient's ISM using the provided `_metadata`. @@ -135,12 +174,15 @@ contract Mailbox is IMailbox, Versioned, Ownable { override { // 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"); + require(delivered(_id) == false, "already delivered"); address recipient = _message.recipientAddress(); @@ -148,10 +190,10 @@ contract Mailbox is IMailbox, Versioned, Ownable { IInterchainSecurityModule _ism = IInterchainSecurityModule( recipientIsm(recipient) ); - require(_ism.verify(_metadata, _message), "!module"); + require(_ism.verify(_metadata, _message), "verification failed"); // effects - delivered[_id] = true; + deliveries[_id] = Delivery({sender: msg.sender}); emit Process(_message); emit ProcessId(_id); @@ -192,34 +234,4 @@ contract Mailbox is IMailbox, Versioned, Ownable { } catch {} return defaultIsm; } - - // ============ Internal Functions ============ - - function _dispatch( - uint32 destinationDomain, - bytes32 recipientAddress, - bytes calldata messageBody, - bytes memory hookMetadata - ) internal 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}(hookMetadata, message); - return id; - } } From bccc5f67cdd707b7da81d20112aea6df25d12f4d Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Mon, 14 Aug 2023 18:26:04 -0400 Subject: [PATCH 09/27] Only run core contracts CI --- .github/workflows/node.yml | 144 ++++++++++++++++++------------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 837a4d1337..85d26b7646 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -63,78 +63,78 @@ jobs: path: ./* key: ${{ github.sha }} - - name: build - run: yarn build - - lint-prettier: - runs-on: ubuntu-latest - needs: [yarn-install] - steps: - - uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: | - **/node_modules - .yarn/cache - key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} - - - name: lint - run: yarn lint - - - name: prettier - run: | - yarn prettier - CHANGES=$(git status -s) - if [[ ! -z $CHANGES ]]; then - echo "Changes found: $CHANGES" - exit 1 - fi - - test-ts: - runs-on: ubuntu-latest - needs: [yarn-build] - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - uses: actions/cache@v3 - with: - path: ./* - key: ${{ github.sha }} - - - name: sdk - run: yarn workspace @hyperlane-xyz/sdk run test - - - name: helloworld - run: yarn workspace @hyperlane-xyz/helloworld run test - - - 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: - 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: core build + run: yarn workspace @hyperlane-xyz/core build + + # lint-prettier: + # runs-on: ubuntu-latest + # needs: [yarn-install] + # steps: + # - uses: actions/checkout@v3 + # - uses: actions/cache@v3 + # with: + # path: | + # **/node_modules + # .yarn/cache + # key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} + + # - name: lint + # run: yarn lint + + # - name: prettier + # run: | + # yarn prettier + # CHANGES=$(git status -s) + # if [[ ! -z $CHANGES ]]; then + # echo "Changes found: $CHANGES" + # exit 1 + # fi + + # test-ts: + # runs-on: ubuntu-latest + # needs: [yarn-build] + # steps: + # - uses: actions/checkout@v3 + # with: + # submodules: recursive + + # - uses: actions/cache@v3 + # with: + # path: ./* + # key: ${{ github.sha }} + + # - name: sdk + # run: yarn workspace @hyperlane-xyz/sdk run test + + # - name: helloworld + # run: yarn workspace @hyperlane-xyz/helloworld run test + + # - 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: + # 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: From 96d8f79d99a0d3c4261f53c8513cc6c372d015f3 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Mon, 14 Aug 2023 18:58:31 -0400 Subject: [PATCH 10/27] Use mailbox callback to authenticate messages in hooks where necessary (#2609) Co-authored-by: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> --- solidity/contracts/Mailbox.sol | 52 ++++++--- solidity/contracts/client/MailboxClient.sol | 14 ++- .../contracts/hooks/AbstractBridgeHook.sol | 101 ------------------ solidity/contracts/hooks/AbstractHook.sol | 24 ----- .../hooks/AbstractMessageIdAuthHook.sol | 34 +++--- .../hooks/ConfigFallbackDomainRoutingHook.sol | 55 ++++++++++ .../contracts/hooks/DomainRoutingHook.sol | 31 ++++-- solidity/contracts/hooks/MerkleTreeHook.sol | 15 +-- solidity/contracts/hooks/PausableHook.sol | 12 +-- solidity/contracts/interfaces/IMailbox.sol | 2 + .../interfaces/hooks/IPostDispatchHook.sol | 3 +- .../hook/AbstractMessageIdAuthorizedIsm.sol | 2 +- solidity/contracts/isms/hook/OPStackISM.sol | 2 +- solidity/contracts/test/TestMailbox.sol | 30 +++--- .../contracts/test/TestPostDispatchHook.sol | 16 +++ .../hooks/FallbackDomainRoutingHook.t.sol | 71 ++++++++++++ solidity/test/isms/ERC5164ISM.t.sol | 54 +++++++--- solidity/test/isms/IsmTestUtils.sol | 2 +- solidity/test/isms/MultisigIsm.t.sol | 22 ++-- solidity/test/isms/OPStackIsm.t.sol | 24 ++--- 20 files changed, 329 insertions(+), 237 deletions(-) delete mode 100644 solidity/contracts/hooks/AbstractBridgeHook.sol delete mode 100644 solidity/contracts/hooks/AbstractHook.sol create mode 100644 solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol create mode 100644 solidity/contracts/test/TestPostDispatchHook.sol create mode 100644 solidity/test/hooks/FallbackDomainRoutingHook.t.sol diff --git a/solidity/contracts/Mailbox.sol b/solidity/contracts/Mailbox.sol index eda3acc40c..ce1ff62eb7 100644 --- a/solidity/contracts/Mailbox.sol +++ b/solidity/contracts/Mailbox.sol @@ -31,15 +31,19 @@ contract Mailbox is IMailbox, Versioned, Ownable { // 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; // The default post dispatch hook, used for post processing of dispatched messages. IPostDispatchHook public defaultHook; - // Mapping of message ID to sender of the call that processed the message. + // Mapping of message ID to delivery context that processed the message. struct Delivery { - address sender; + // address sender; + IInterchainSecurityModule ism; // uint48 value? // uint48 timestamp? } @@ -101,7 +105,8 @@ contract Mailbox is IMailbox, Versioned, Ownable { _destinationDomain, _recipientAddress, _messageBody, - bytes("") + defaultHook, + _messageBody[0:0] ); } @@ -135,7 +140,9 @@ contract Mailbox is IMailbox, Versioned, Ownable { bytes calldata messageBody, IPostDispatchHook hook, bytes calldata metadata - ) public returns (bytes32) { + ) public payable returns (bytes32) { + /// CHECKS /// + // Format the message into packed bytes. bytes memory message = Message.formatMessage( VERSION, @@ -146,20 +153,24 @@ contract Mailbox is IMailbox, Versioned, Ownable { recipientAddress, messageBody ); + bytes32 id = message.id(); + + /// EFFECTS /// - // effects nonce += 1; - bytes32 id = message.id(); + latestDispatchedId = id; emit DispatchId(id); emit Dispatch(message); - // interactions + /// INTERACTIONS /// + hook.postDispatch{value: msg.value}(metadata, message); + return id; } function delivered(bytes32 _id) public view override returns (bool) { - return deliveries[_id].sender != address(0); + return address(deliveries[_id].ism) != address(0); } /** @@ -173,6 +184,8 @@ contract Mailbox is IMailbox, Versioned, Ownable { payable override { + /// CHECKS /// + // Check that the message was intended for this mailbox. require(_message.version() == VERSION, "bad version"); require( @@ -184,20 +197,27 @@ contract Mailbox is IMailbox, Versioned, Ownable { bytes32 _id = _message.id(); require(delivered(_id) == false, "already delivered"); + // Get the recipient's ISM. address recipient = _message.recipientAddress(); + IInterchainSecurityModule ism = recipientIsm(recipient); - // Verify the message via the ISM. - IInterchainSecurityModule _ism = IInterchainSecurityModule( - recipientIsm(recipient) - ); - require(_ism.verify(_metadata, _message), "verification failed"); + /// EFFECTS /// - // effects - deliveries[_id] = Delivery({sender: msg.sender}); + deliveries[_id] = Delivery({ + ism: ism + // sender: msg.sender + // value: uint48(msg.value), + // timestamp: uint48(block.number) + }); emit Process(_message); emit ProcessId(_id); - // Deliver the message to the recipient. (interactions) + /// INTERACTIONS /// + + // Verify the message via the ISM. + require(ism.verify(_metadata, _message), "verification failed"); + + // Deliver the message to the recipient. IMessageRecipient(recipient).handle{value: msg.value}( _message.origin(), _message.sender(), diff --git a/solidity/contracts/client/MailboxClient.sol b/solidity/contracts/client/MailboxClient.sol index d644fd9aef..2b60d4de03 100644 --- a/solidity/contracts/client/MailboxClient.sol +++ b/solidity/contracts/client/MailboxClient.sol @@ -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; + } } diff --git a/solidity/contracts/hooks/AbstractBridgeHook.sol b/solidity/contracts/hooks/AbstractBridgeHook.sol deleted file mode 100644 index 53cb7a31b7..0000000000 --- a/solidity/contracts/hooks/AbstractBridgeHook.sol +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; - -/*@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@ HYPERLANE @@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ -@@@@@@@@@ @@@@@@@@*/ - -// ============ Internal Imports ============ -import {AbstractHook} from "./AbstractHook.sol"; -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"; - -// ============ External Imports ============ -import {ICrossDomainMessenger} from "../interfaces/optimism/ICrossDomainMessenger.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - -/** - * @title AbstractBridgeHook - * @notice Message hook to inform an Abstract Message ID ISM of messages published through - * the native OPStack bridge. - * @dev V3 WIP - */ -abstract contract AbstractBridgeHook is AbstractHook { - using Message for bytes; - using OPStackHookMetadata for bytes; - using TypeCasts for address; - - // ============ Constants ============ - - // Domain of chain on which the OPStack ISM is deployed - uint32 public immutable destinationDomain; - // Messenger used to send messages from L1 -> L2 - ICrossDomainMessenger public immutable l1Messenger; - // address for OPStack 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( - address _mailbox, - uint32 _destinationDomain, - address _messenger, - address _ism - ) AbstractHook(_mailbox) { - require( - _destinationDomain != 0, - "OPStackHook: invalid destination domain" - ); - require(_ism != address(0), "OPStackHook: invalid ISM"); - destinationDomain = _destinationDomain; - - require( - Address.isContract(_messenger), - "OPStackHook: invalid messenger" - ); - l1Messenger = ICrossDomainMessenger(_messenger); - ism = _ism; - } - - // ============ External Functions ============ - - /** - * @notice Hook to inform the optimism ISM of messages published through. - * metadata The metadata for the hook caller (unused) - * @param message The message being dispatched - */ - function _postDispatch(bytes calldata metadata, bytes calldata message) - internal - override - { - bytes32 messageId = message.id(); - uint256 msgValue = metadata.msgValue(); - - require( - message.destination() == destinationDomain, - "OPStackHook: invalid destination domain" - ); - - bytes memory payload = abi.encodeCall( - AbstractMessageIdAuthorizedIsm.verifyMessageId, - (messageId) - ); - - // send the rest of the val - l1Messenger.sendMessage{value: msgValue}(ism, payload, GAS_LIMIT); - } -} diff --git a/solidity/contracts/hooks/AbstractHook.sol b/solidity/contracts/hooks/AbstractHook.sol deleted file mode 100644 index 673599754c..0000000000 --- a/solidity/contracts/hooks/AbstractHook.sol +++ /dev/null @@ -1,24 +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 metadata, bytes calldata message) - external - payable - override - onlyMailbox - returns (IPostDispatchHook) - { - return _postDispatch(metadata, message); - } - - function _postDispatch(bytes calldata metadata, bytes calldata message) - internal - virtual - returns (IPostDispatchHook); -} diff --git a/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol b/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol index f2d6cb1e44..a5c3031309 100644 --- a/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol +++ b/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol @@ -14,11 +14,11 @@ pragma solidity >=0.8.0; @@@@@@@@@ @@@@@@@@*/ // ============ Internal Imports ============ -import {AbstractHook} from "./AbstractHook.sol"; 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 ============ @@ -31,7 +31,10 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; * the native OPStack bridge. * @dev V3 WIP */ -abstract contract AbstractMessageIdAuthHook is AbstractHook { +abstract contract AbstractMessageIdAuthHook is + IPostDispatchHook, + MailboxClient +{ using Message for bytes; // ============ Constants ============ @@ -44,43 +47,42 @@ abstract contract AbstractMessageIdAuthHook is AbstractHook { // ============ Constructor ============ constructor( - address _mailbox, + address mailbox, uint32 _destinationDomain, address _ism - ) AbstractHook(_mailbox) { + ) MailboxClient(mailbox) { require(_ism != address(0), "invalid ISM"); require(_destinationDomain != 0, "invalid destination domain"); ism = _ism; destinationDomain = _destinationDomain; } - function _sendMessageId(bytes calldata metadata, bytes memory payload) - internal - virtual; - /** * @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) - internal + function postDispatch(bytes calldata metadata, bytes calldata message) + external + payable override - returns (IPostDispatchHook) { + 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, - message.id() + id ); _sendMessageId(metadata, payload); - - // no next post-dispatch hook - // TODO: consider configuring? - return IPostDispatchHook(address(0)); } + + function _sendMessageId(bytes calldata metadata, bytes memory payload) + internal + virtual; } diff --git a/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol b/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol new file mode 100644 index 0000000000..41dd3c1299 --- /dev/null +++ b/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol @@ -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; + } +} diff --git a/solidity/contracts/hooks/DomainRoutingHook.sol b/solidity/contracts/hooks/DomainRoutingHook.sol index e57bde7715..b57fd9cc83 100644 --- a/solidity/contracts/hooks/DomainRoutingHook.sol +++ b/solidity/contracts/hooks/DomainRoutingHook.sol @@ -1,28 +1,41 @@ // 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 metadata, bytes calldata message) - internal + 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}( diff --git a/solidity/contracts/hooks/MerkleTreeHook.sol b/solidity/contracts/hooks/MerkleTreeHook.sol index a230e24797..c4c61f3850 100644 --- a/solidity/contracts/hooks/MerkleTreeHook.sol +++ b/solidity/contracts/hooks/MerkleTreeHook.sol @@ -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,10 +35,12 @@ contract MerkleTreeHook is AbstractHook { return (root(), count() - 1); } - function _postDispatch( + function postDispatch( bytes calldata, /*metadata*/ bytes calldata message - ) internal override { - _tree.insert(message.id()); + ) external payable override { + bytes32 id = message.id(); + require(isLatestDispatched(id), "message not dispatching"); + _tree.insert(id); } } diff --git a/solidity/contracts/hooks/PausableHook.sol b/solidity/contracts/hooks/PausableHook.sol index 1f31b98ceb..99835fa488 100644 --- a/solidity/contracts/hooks/PausableHook.sol +++ b/solidity/contracts/hooks/PausableHook.sol @@ -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 metadata, bytes calldata message) - internal - override +contract PausableHook is IPostDispatchHook, Ownable, Pausable { + function postDispatch(bytes calldata metadata, bytes calldata message) + external + payable whenNotPaused {} diff --git a/solidity/contracts/interfaces/IMailbox.sol b/solidity/contracts/interfaces/IMailbox.sol index 1581a0ab0c..89958881f5 100644 --- a/solidity/contracts/interfaces/IMailbox.sol +++ b/solidity/contracts/interfaces/IMailbox.sol @@ -38,6 +38,8 @@ interface IMailbox { function defaultHook() external view returns (IPostDispatchHook); + function latestDispatchedId() external view returns (bytes32); + function dispatch( uint32 destinationDomain, bytes32 recipientAddress, diff --git a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol index cdcc123d2c..737268a486 100644 --- a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol +++ b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol @@ -4,6 +4,5 @@ pragma solidity >=0.8.0; interface IPostDispatchHook { function postDispatch(bytes calldata metadata, bytes calldata message) external - payable - returns (IPostDispatchHook next); + payable; } diff --git a/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol b/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol index 5cec2f31ba..916e392480 100644 --- a/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol +++ b/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol @@ -77,7 +77,7 @@ abstract contract AbstractMessageIdAuthorizedIsm is * @dev Only callable by the L2 messenger. * @param _messageId Hyperlane ID for the message. */ - function verifyMessageId(bytes32 _messageId) external virtual { + function verifyMessageId(bytes32 _messageId) external { require( _isAuthorized(), "AbstractMessageIdAuthorizedIsm: sender is not the hook" diff --git a/solidity/contracts/isms/hook/OPStackISM.sol b/solidity/contracts/isms/hook/OPStackISM.sol index 772a7f99ec..9cee32fc54 100644 --- a/solidity/contracts/isms/hook/OPStackISM.sol +++ b/solidity/contracts/isms/hook/OPStackISM.sol @@ -27,7 +27,7 @@ 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 */ diff --git a/solidity/contracts/test/TestMailbox.sol b/solidity/contracts/test/TestMailbox.sol index 6678590d25..83361e1670 100644 --- a/solidity/contracts/test/TestMailbox.sol +++ b/solidity/contracts/test/TestMailbox.sol @@ -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, diff --git a/solidity/contracts/test/TestPostDispatchHook.sol b/solidity/contracts/test/TestPostDispatchHook.sol new file mode 100644 index 0000000000..82febe3ccc --- /dev/null +++ b/solidity/contracts/test/TestPostDispatchHook.sol @@ -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(); + } +} diff --git a/solidity/test/hooks/FallbackDomainRoutingHook.t.sol b/solidity/test/hooks/FallbackDomainRoutingHook.t.sol new file mode 100644 index 0000000000..147b2431de --- /dev/null +++ b/solidity/test/hooks/FallbackDomainRoutingHook.t.sol @@ -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!") + ); + } +} diff --git a/solidity/test/isms/ERC5164ISM.t.sol b/solidity/test/isms/ERC5164ISM.t.sol index 4f5ffac0ed..bad87d1dde 100644 --- a/solidity/test/isms/ERC5164ISM.t.sol +++ b/solidity/test/isms/ERC5164ISM.t.sol @@ -8,6 +8,7 @@ import {TypeCasts} from "../../contracts/libs/TypeCasts.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"; @@ -72,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) ); } @@ -90,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 @@ -104,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 ============ */ @@ -121,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(); } @@ -136,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(); } @@ -148,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); @@ -161,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); @@ -175,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); diff --git a/solidity/test/isms/IsmTestUtils.sol b/solidity/test/isms/IsmTestUtils.sol index b7b71ceb61..d9a3d2daf7 100644 --- a/solidity/test/isms/IsmTestUtils.sol +++ b/solidity/test/isms/IsmTestUtils.sol @@ -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, diff --git a/solidity/test/isms/MultisigIsm.t.sol b/solidity/test/isms/MultisigIsm.t.sol index 961b5ab723..40e42f461e 100644 --- a/solidity/test/isms/MultisigIsm.t.sol +++ b/solidity/test/isms/MultisigIsm.t.sol @@ -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()); } } diff --git a/solidity/test/isms/OPStackIsm.t.sol b/solidity/test/isms/OPStackIsm.t.sol index 18ca554a1b..157dd61350 100644 --- a/solidity/test/isms/OPStackIsm.t.sol +++ b/solidity/test/isms/OPStackIsm.t.sol @@ -84,7 +84,7 @@ contract OPStackIsmTest 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 OPStackIsmTest 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 OPStackIsmTest is Test { vm.selectFork(mainnetFork); bytes memory encodedHookData = abi.encodeCall( - OptimismISM.verifyMessageId, + OPStackIsm.verifyMessageId, (address(this).addressToBytes32(), messageId) ); @@ -164,7 +164,7 @@ contract OPStackIsmTest is Test { vm.selectFork(optimismFork); bytes memory encodedHookData = abi.encodeCall( - OptimismISM.verifyMessageId, + OPStackIsm.verifyMessageId, (address(this).addressToBytes32(), messageId) ); @@ -222,7 +222,7 @@ contract OPStackIsmTest is Test { // vm.selectFork(optimismFork); // bytes memory encodedHookData = abi.encodeCall( - // OptimismISM.verifyMessageId, + // OPStackIsm.verifyMessageId, // (address(this), messageId) // ); @@ -267,7 +267,7 @@ contract OPStackIsmTest 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 OPStackIsmTest is Test { vm.selectFork(optimismFork); bytes memory encodedHookData = abi.encodeCall( - OptimismISM.verifyMessageId, + OPStackIsm.verifyMessageId, (address(this).addressToBytes32(), messageId) ); @@ -312,7 +312,7 @@ contract OPStackIsmTest is Test { vm.selectFork(optimismFork); bytes memory encodedHookData = abi.encodeCall( - OptimismISM.verifyMessageId, + OPStackIsm.verifyMessageId, (address(this).addressToBytes32(), messageId) ); @@ -349,7 +349,7 @@ contract OPStackIsmTest 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 OPStackIsmTest is Test { vm.selectFork(optimismFork); bytes memory encodedHookData = abi.encodeCall( - OptimismISM.verifyMessageId, + OPStackIsm.verifyMessageId, (alice.addressToBytes32(), messageId) ); From b0085408b78663503f8702139fd97cc33f588160 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 15 Aug 2023 11:14:47 -0400 Subject: [PATCH 11/27] Modifying IGP to be a hook (#2638) ### Description - IGP as a standalone hook, implementing postDispatch to call payForGas directly - Setting a DEFAULT_GAS_USAGE if metadata not specified and message.senderAddress() as refund address if not specified. ### Drive-by changes - None ### Related issues Fixes https://github.com/hyperlane-xyz/issues/issues/511 ### Backward compatibility Yes, same interface as the previous IGP but for Mailbox V3 ### Testing Unit Tests --------- Co-authored-by: Yorke Rhodes --- solidity/contracts/Mailbox.sol | 2 - .../hooks/AbstractMessageIdAuthHook.sol | 6 +- solidity/contracts/hooks/DefaultHook.sol | 60 +++++++++++++ .../contracts/hooks/DomainRoutingHook.sol | 2 +- .../contracts/igps/InterchainGasPaymaster.sol | 89 +++++++++++++------ solidity/contracts/libs/hooks/IGPMetadata.sol | 71 +++++++++++++++ solidity/contracts/test/TestMailbox.sol | 4 + .../test/igps/InterchainGasPaymaster.t.sol | 86 ++++++++++++++++++ 8 files changed, 284 insertions(+), 36 deletions(-) create mode 100644 solidity/contracts/hooks/DefaultHook.sol create mode 100644 solidity/contracts/libs/hooks/IGPMetadata.sol diff --git a/solidity/contracts/Mailbox.sol b/solidity/contracts/Mailbox.sol index ce1ff62eb7..f0b44723cc 100644 --- a/solidity/contracts/Mailbox.sol +++ b/solidity/contracts/Mailbox.sol @@ -30,8 +30,6 @@ contract Mailbox is IMailbox, Versioned, Ownable { // 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. diff --git a/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol b/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol index a5c3031309..56328051c8 100644 --- a/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol +++ b/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol @@ -21,14 +21,10 @@ 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. + * a third-party bridge. * @dev V3 WIP */ abstract contract AbstractMessageIdAuthHook is diff --git a/solidity/contracts/hooks/DefaultHook.sol b/solidity/contracts/hooks/DefaultHook.sol new file mode 100644 index 0000000000..dca3d55e79 --- /dev/null +++ b/solidity/contracts/hooks/DefaultHook.sol @@ -0,0 +1,60 @@ +// 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 {DomainRoutingHook} from "./DomainRoutingHook.sol"; + +contract ConfigurableDomainRoutingHook is DomainRoutingHook { + using Message for bytes; + + /// @notice mapping of destination domain and recipient to custom hook + mapping(bytes32 => address) public customHooks; + + constructor(address mailbox, address owner) DomainRoutingHook(owner) {} + + function postDispatch(bytes calldata metadata, bytes calldata message) + public + payable + override + { + bytes32 hookKey = keccak256( + abi.encodePacked(message.destination(), message.recipient()) + ); + + address customHookPreset = customHooks[hookKey]; + if (customHookPreset != address(0)) { + IPostDispatchHook(customHookPreset).postDispatch{value: msg.value}( + metadata, + message + ); + } else { + super.postDispatch(metadata, message); + } + } + + // TODO: need to restrict sender + function configCustomHook( + uint32 destinationDomain, + bytes32 recipient, + address hook + ) external { + bytes32 hookKey = keccak256( + abi.encodePacked(destinationDomain, recipient) + ); + require(customHooks[hookKey] == address(0), "hook already set"); + customHooks[hookKey] = hook; + } +} diff --git a/solidity/contracts/hooks/DomainRoutingHook.sol b/solidity/contracts/hooks/DomainRoutingHook.sol index b57fd9cc83..c74cebb735 100644 --- a/solidity/contracts/hooks/DomainRoutingHook.sol +++ b/solidity/contracts/hooks/DomainRoutingHook.sol @@ -33,7 +33,7 @@ contract DomainRoutingHook is IPostDispatchHook, Ownable { } function postDispatch(bytes calldata metadata, bytes calldata message) - external + public payable virtual override diff --git a/solidity/contracts/igps/InterchainGasPaymaster.sol b/solidity/contracts/igps/InterchainGasPaymaster.sol index 6418659e60..e951f4ba87 100644 --- a/solidity/contracts/igps/InterchainGasPaymaster.sol +++ b/solidity/contracts/igps/InterchainGasPaymaster.sol @@ -2,9 +2,13 @@ pragma solidity >=0.8.0; // ============ Internal Imports ============ +import {Message} from "../libs/Message.sol"; +import {IGPMetadata} from "../libs/hooks/IGPMetadata.sol"; import {IGasOracle} from "../interfaces/IGasOracle.sol"; import {IInterchainGasPaymaster} from "../interfaces/IInterchainGasPaymaster.sol"; +import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; // ============ External Imports ============ +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; /** @@ -14,13 +18,19 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own */ contract InterchainGasPaymaster is IInterchainGasPaymaster, + IPostDispatchHook, IGasOracle, OwnableUpgradeable { + using Address for address payable; + using Message for bytes; + using IGPMetadata for bytes; // ============ Constants ============ /// @notice The scale of gas oracle token exchange rates. uint256 internal constant TOKEN_EXCHANGE_RATE_SCALE = 1e10; + /// @notice default for user call if metadata not provided + uint256 internal immutable DEFAULT_GAS_USAGE = 69_420; // ============ Public Storage ============ @@ -66,36 +76,27 @@ contract InterchainGasPaymaster is } /** - * @notice Deposits msg.value as a payment for the relaying of a message - * to its destination chain. - * @dev Overpayment will result in a refund of native tokens to the _refundAddress. - * Callers should be aware that this may present reentrancy issues. - * @param _messageId The ID of the message to pay for. - * @param _destinationDomain The domain of the message's destination chain. - * @param _gasAmount The amount of destination gas to pay for. - * @param _refundAddress The address to refund any overpayment to. + * @notice pay for gas as a hook + * @param metadata The metadata as gasConfig. + * @param message The message to pay for. */ - function payForGas( - bytes32 _messageId, - uint32 _destinationDomain, - uint256 _gasAmount, - address _refundAddress - ) external payable override { - uint256 _requiredPayment = quoteGasPayment( - _destinationDomain, - _gasAmount - ); - require( - msg.value >= _requiredPayment, - "insufficient interchain gas payment" - ); - uint256 _overpayment = msg.value - _requiredPayment; - if (_overpayment > 0) { - (bool _success, ) = _refundAddress.call{value: _overpayment}(""); - require(_success, "Interchain gas payment refund failed"); + function postDispatch(bytes calldata metadata, bytes calldata message) + external + payable + override + { + uint256 gasLimit; + address refundAddress; + if (metadata.length == 0) { + gasLimit = DEFAULT_GAS_USAGE; + refundAddress = message.senderAddress(); + } else { + gasLimit = metadata.gasLimit(); + refundAddress = metadata.refundAddress(); + if (refundAddress == address(0)) + refundAddress = message.senderAddress(); } - - emit GasPayment(_messageId, _gasAmount, _requiredPayment); + payForGas(message.id(), message.destination(), gasLimit, refundAddress); } /** @@ -132,6 +133,38 @@ contract InterchainGasPaymaster is // ============ Public Functions ============ + /** + * @notice Deposits msg.value as a payment for the relaying of a message + * to its destination chain. + * @dev Overpayment will result in a refund of native tokens to the _refundAddress. + * Callers should be aware that this may present reentrancy issues. + * @param _messageId The ID of the message to pay for. + * @param _destinationDomain The domain of the message's destination chain. + * @param _gasAmount The amount of destination gas to pay for. + * @param _refundAddress The address to refund any overpayment to. + */ + function payForGas( + bytes32 _messageId, + uint32 _destinationDomain, + uint256 _gasAmount, + address _refundAddress + ) public payable override { + uint256 _requiredPayment = quoteGasPayment( + _destinationDomain, + _gasAmount + ); + require( + msg.value >= _requiredPayment, + "insufficient interchain gas payment" + ); + uint256 _overpayment = msg.value - _requiredPayment; + if (_overpayment > 0) { + payable(_refundAddress).sendValue(_overpayment); + } + + emit GasPayment(_messageId, _gasAmount, _requiredPayment); + } + /** * @notice Quotes the amount of native tokens to pay for interchain gas. * @param _destinationDomain The domain of the message's destination chain. diff --git a/solidity/contracts/libs/hooks/IGPMetadata.sol b/solidity/contracts/libs/hooks/IGPMetadata.sol new file mode 100644 index 0000000000..0bac899f94 --- /dev/null +++ b/solidity/contracts/libs/hooks/IGPMetadata.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +/** + * Format of metadata: + * + * [0:32] Gas limit for message + * [32:52] Refund address for message + */ +library IGPMetadata { + uint8 private constant GAS_LIMIT_OFFSET = 0; + uint8 private constant REFUND_ADDRESS_OFFSET = 32; + + /** + * @notice Returns the specified gas limit for the message. + * @param _metadata ABI encoded IGP hook metadata. + * @return Gas limit for the message as uint256. + */ + function gasLimit(bytes calldata _metadata) + internal + pure + returns (uint256) + { + return + uint256(bytes32(_metadata[GAS_LIMIT_OFFSET:GAS_LIMIT_OFFSET + 32])); + } + + /** + * @notice Returns the specified refund address for the message. + * @param _metadata ABI encoded IGP hook metadata. + * @return Refund address for the message as address. + */ + function refundAddress(bytes calldata _metadata) + internal + pure + returns (address) + { + return + address( + bytes20( + _metadata[REFUND_ADDRESS_OFFSET:REFUND_ADDRESS_OFFSET + 20] + ) + ); + } + + /** + * @notice Formats the specified gas limit and refund address into IGP hook metadata. + * @param _gasLimit Gas limit for the message. + * @param _refundAddress Refund address for the message. + * @return ABI encoded IGP hook metadata. + */ + function formatMetadata(uint256 _gasLimit, address _refundAddress) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(bytes32(_gasLimit), bytes20(_refundAddress)); + } +} diff --git a/solidity/contracts/test/TestMailbox.sol b/solidity/contracts/test/TestMailbox.sol index 83361e1670..4eda8ffaff 100644 --- a/solidity/contracts/test/TestMailbox.sol +++ b/solidity/contracts/test/TestMailbox.sol @@ -39,4 +39,8 @@ contract TestMailbox is Mailbox { _body ); } + + function updateLatestDispatchedId(bytes32 _id) external { + latestDispatchedId = _id; + } } diff --git a/solidity/test/igps/InterchainGasPaymaster.t.sol b/solidity/test/igps/InterchainGasPaymaster.t.sol index fa7a2b6b11..7b9a8c4ffa 100644 --- a/solidity/test/igps/InterchainGasPaymaster.t.sol +++ b/solidity/test/igps/InterchainGasPaymaster.t.sol @@ -2,18 +2,29 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; + +import {IGPMetadata} from "../../contracts/libs/hooks/IGPMetadata.sol"; +import {Message} from "../../contracts/libs/Message.sol"; +import {MessageUtils} from "../isms/IsmTestUtils.sol"; +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {InterchainGasPaymaster} from "../../contracts/igps/InterchainGasPaymaster.sol"; import {StorageGasOracle} from "../../contracts/igps/gas-oracles/StorageGasOracle.sol"; import {IGasOracle} from "../../contracts/interfaces/IGasOracle.sol"; contract InterchainGasPaymasterTest is Test { + using IGPMetadata for bytes; + using TypeCasts for address; + using MessageUtils for bytes; + InterchainGasPaymaster igp; StorageGasOracle oracle; address constant beneficiary = address(0x444444); + uint32 constant testOriginDomain = 22222; uint32 constant testDestinationDomain = 11111; uint256 constant testGasAmount = 300000; + bytes constant testMessage = "hello world"; bytes32 constant testMessageId = 0x6ae9a99190641b9ed0c07143340612dde0e9cb7deaa5fe07597858ae9ba5fd7f; address constant testRefundAddress = address(0xc0ffee); @@ -48,6 +59,66 @@ contract InterchainGasPaymasterTest is Test { igp.initialize(address(this), beneficiary); } + // ============ postDispatch ============ + + function testPostDispatch_defaultGasLimit() public { + setRemoteGasData( + testDestinationDomain, + 1 * 1e10, // 1.0 exchange rate (remote token has exact same value as local) + 1 // 1 wei gas price + ); + + uint256 _igpBalanceBefore = address(igp).balance; + uint256 _refundAddressBalanceBefore = address(this).balance; + uint256 _quote = igp.quoteGasPayment(testDestinationDomain, 69_420); + + uint256 _overpayment = 21000; + bytes memory message = _encodeTestMessage(); + + igp.postDispatch{value: _quote + _overpayment}("", message); + + uint256 _igpBalanceAfter = address(igp).balance; + uint256 _refundAddressBalanceAfter = address(this).balance; + assertEq(_igpBalanceAfter - _igpBalanceBefore, _quote); + assertEq( + _refundAddressBalanceBefore - _refundAddressBalanceAfter, + _quote + ); + } + + function testPostDispatch_customWithMetadata() public { + setRemoteGasData( + testDestinationDomain, + 1 * 1e10, // 1.0 exchange rate (remote token has exact same value as local) + 1 // 1 wei gas price + ); + + uint256 _igpBalanceBefore = address(igp).balance; + uint256 _refundAddressBalanceBefore = testRefundAddress.balance; + uint256 _quote = igp.quoteGasPayment( + testDestinationDomain, + testGasAmount + ); + + uint256 _overpayment = 25000; + bytes memory metadata = IGPMetadata.formatMetadata( + uint256(testGasAmount), // gas limit + testRefundAddress // refund address + ); + bytes memory message = _encodeTestMessage(); + + igp.postDispatch{value: _quote + _overpayment}(metadata, message); + + uint256 _igpBalanceAfter = address(igp).balance; + uint256 _refundAddressBalanceAfter = testRefundAddress.balance; + + assertEq(_igpBalanceAfter - _igpBalanceBefore, _quote); + assertEq( + _refundAddressBalanceAfter - _refundAddressBalanceBefore, + _overpayment + ); + } + // ============ payForGas ============ function testPayForGas() public { @@ -290,4 +361,19 @@ contract InterchainGasPaymasterTest is Test { }) ); } + + function _encodeTestMessage() internal view returns (bytes memory) { + return + MessageUtils.formatMessage( + uint8(0), + uint32(0), + testOriginDomain, + TypeCasts.addressToBytes32(address(this)), + testDestinationDomain, + TypeCasts.addressToBytes32(address(0x1)), + testMessage + ); + } + + receive() external payable {} } From 816cc407841e42363c30f5fce73428c28ea8682b Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 15 Aug 2023 11:16:39 -0400 Subject: [PATCH 12/27] Converting the OP stack hooks to transient storage version (#2632) ### Description - Updated the OP Stack tests for the Mailbox V3 transient storage version - Allowing OPStackHook to send msg.value at the time of message delivery (uses bit masking) - Added LibBit library, will be useful for all the auth hooks which can send `msg.value` ### Drive-by changes - None ### Related issues - Fixes breaking OP Stack tests for https://github.com/hyperlane-xyz/issues/issues/513 - Also fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2410 ### Backward compatibility No ### Testing Unit tests --------- Co-authored-by: Yorke Rhodes --- .github/workflows/node.yml | 48 ++-- solidity/contracts/Mailbox.sol | 1 + .../hooks/AbstractMessageIdAuthHook.sol | 16 +- solidity/contracts/hooks/OPStackHook.sol | 13 +- .../hook/AbstractMessageIdAuthorizedIsm.sol | 44 ++-- solidity/contracts/libs/LibBit.sol | 29 +++ solidity/test/isms/ERC5164ISM.t.sol | 2 +- solidity/test/isms/OPStackIsm.t.sol | 222 ++++++++++-------- solidity/test/lib/LibBit.t.sol | 34 +++ 9 files changed, 267 insertions(+), 142 deletions(-) create mode 100644 solidity/contracts/libs/LibBit.sol create mode 100644 solidity/test/lib/LibBit.t.sol diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 85d26b7646..9db7a0a9e5 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -38,33 +38,33 @@ jobs: exit 1 fi - yarn-build: - runs-on: ubuntu-latest - needs: [yarn-install] - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - uses: actions/setup-node@v3 - with: - node-version: 18 + # yarn-build: + # runs-on: ubuntu-latest + # needs: [yarn-install] + # steps: + # - uses: actions/checkout@v3 + # with: + # submodules: recursive + # - uses: actions/setup-node@v3 + # with: + # node-version: 18 - - name: yarn-cache - uses: actions/cache@v3 - with: - path: | - **/node_modules - .yarn/cache - key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} + # - name: yarn-cache + # uses: actions/cache@v3 + # with: + # path: | + # **/node_modules + # .yarn/cache + # key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} - - name: build-cache - uses: actions/cache@v3 - with: - path: ./* - key: ${{ github.sha }} + # - name: build-cache + # uses: actions/cache@v3 + # with: + # path: ./* + # key: ${{ github.sha }} - - name: core build - run: yarn workspace @hyperlane-xyz/core build + # - name: core build + # run: yarn workspace @hyperlane-xyz/core build # lint-prettier: # runs-on: ubuntu-latest diff --git a/solidity/contracts/Mailbox.sol b/solidity/contracts/Mailbox.sol index f0b44723cc..01d9ad529e 100644 --- a/solidity/contracts/Mailbox.sol +++ b/solidity/contracts/Mailbox.sol @@ -30,6 +30,7 @@ contract Mailbox is IMailbox, Versioned, Ownable { // 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. diff --git a/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol b/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol index 56328051c8..afc8ee415a 100644 --- a/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol +++ b/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol @@ -64,13 +64,14 @@ abstract contract AbstractMessageIdAuthHook is override { bytes32 id = message.id(); - require(isLatestDispatched(id), "message not latest dispatched"); + require( + isLatestDispatched(id), + "AbstractMessageIdAuthHook: message not latest dispatched" + ); require( message.destination() == destinationDomain, - "invalid destination domain" + "AbstractMessageIdAuthHook: invalid destination domain" ); - // TODO: handle msg.value? - bytes memory payload = abi.encodeCall( AbstractMessageIdAuthorizedIsm.verifyMessageId, id @@ -78,6 +79,13 @@ abstract contract AbstractMessageIdAuthHook is _sendMessageId(metadata, payload); } + // ============ Internal functions ============ + + /** + * @notice Send a message to the ISM. + * @param metadata The metadata for the hook caller + * @param payload The payload for call to the ISM + */ function _sendMessageId(bytes calldata metadata, bytes memory payload) internal virtual; diff --git a/solidity/contracts/hooks/OPStackHook.sol b/solidity/contracts/hooks/OPStackHook.sol index 0e70c42dd4..7b4c036bc1 100644 --- a/solidity/contracts/hooks/OPStackHook.sol +++ b/solidity/contracts/hooks/OPStackHook.sol @@ -26,15 +26,15 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; /** * @title OPStackHook - * @notice Message hook to inform the Optimism ISM of messages published through + * @notice Message hook to inform the OPStackISM of messages published through * the native OPStack bridge. - * @dev V3 WIP */ contract OPStackHook is AbstractMessageIdAuthHook { using OPStackHookMetadata for bytes; // ============ Constants ============ + /// @notice messenger contract specified by the rollup ICrossDomainMessenger public immutable l1Messenger; // Gas limit for sending messages to L2 @@ -52,15 +52,22 @@ contract OPStackHook is AbstractMessageIdAuthHook { ) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) { require( Address.isContract(_messenger), - "ERC5164Hook: invalid dispatcher" + "OPStackHook: invalid messenger" ); l1Messenger = ICrossDomainMessenger(_messenger); } + // ============ Internal functions ============ + + /// @inheritdoc AbstractMessageIdAuthHook function _sendMessageId(bytes calldata metadata, bytes memory payload) internal override { + require( + metadata.msgValue() < 2**255, + "OPStackHook: msgValue must less than 2 ** 255" + ); l1Messenger.sendMessage{value: metadata.msgValue()}( ism, payload, diff --git a/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol b/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol index 916e392480..145a337501 100644 --- a/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol +++ b/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol @@ -16,11 +16,13 @@ pragma solidity >=0.8.0; // ============ Internal Imports ============ import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; +import {LibBit} from "../../libs/LibBit.sol"; import {Message} from "../../libs/Message.sol"; import {TypeCasts} from "../../libs/TypeCasts.sol"; // ============ External Imports ============ +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; @@ -35,16 +37,25 @@ abstract contract AbstractMessageIdAuthorizedIsm is IInterchainSecurityModule, Initializable { + using Address for address payable; + using LibBit for uint256; + using Message for bytes; // ============ 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 => bool) public verifiedMessageIds; - // Address for Hook on L1 responsible for sending message via the Optimism bridge + /// @notice Maps messageId to whether or not the message has been verified + /// first bit is boolean for verification + /// rest of bits is the amount to send to the recipient + /// @dev bc of the bit packing, we can only send up to 2^255 wei + mapping(bytes32 => uint256) public verifiedMessages; + /// @notice Index of verification bit in verifiedMessages + uint256 public constant MASK_INDEX = 255; + /// @notice Address for Hook on L1 responsible for sending message via the Optimism bridge address public authorizedHook; // ============ Events ============ + /// @notice Emitted when a message is received from the external bridge + /// Might be useful for debugging for the scraper event ReceivedMessage(bytes32 indexed messageId); // ============ Initializer ============ @@ -61,30 +72,35 @@ abstract contract AbstractMessageIdAuthorizedIsm is /** * @notice Verify a message was received by ISM. - * @param _message Message to verify. + * @param message Message to verify. */ function verify( bytes calldata, /*_metadata*/ - bytes calldata _message - ) external view returns (bool) { - bytes32 _messageId = Message.id(_message); - - return verifiedMessageIds[_messageId]; + bytes calldata message + ) external returns (bool) { + bytes32 messageId = message.id(); + + bool verified = verifiedMessages[messageId].isBitSet(MASK_INDEX); + if (verified) + payable(message.recipientAddress()).sendValue( + verifiedMessages[messageId].clearBit(MASK_INDEX) + ); + return verified; } /** * @notice Receive a message from the L2 messenger. * @dev Only callable by the L2 messenger. - * @param _messageId Hyperlane ID for the message. + * @param messageId Hyperlane Id of the message. */ - function verifyMessageId(bytes32 _messageId) external { + function verifyMessageId(bytes32 messageId) external payable { require( _isAuthorized(), "AbstractMessageIdAuthorizedIsm: sender is not the hook" ); - verifiedMessageIds[_messageId] = true; - emit ReceivedMessage(_messageId); + verifiedMessages[messageId] = msg.value.setBit(MASK_INDEX); + emit ReceivedMessage(messageId); } function _isAuthorized() internal view virtual returns (bool); diff --git a/solidity/contracts/libs/LibBit.sol b/solidity/contracts/libs/LibBit.sol new file mode 100644 index 0000000000..a22a47a1e2 --- /dev/null +++ b/solidity/contracts/libs/LibBit.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/// @notice Library for bit shifting and masking +library LibBit { + function setBit(uint256 _value, uint256 _index) + internal + pure + returns (uint256) + { + return _value | (1 << _index); + } + + function clearBit(uint256 _value, uint256 _index) + internal + pure + returns (uint256) + { + return _value & ~(1 << _index); + } + + function isBitSet(uint256 _value, uint256 _index) + internal + pure + returns (bool) + { + return (_value >> _index) & 1 == 1; + } +} diff --git a/solidity/test/isms/ERC5164ISM.t.sol b/solidity/test/isms/ERC5164ISM.t.sol index bad87d1dde..012b6b6749 100644 --- a/solidity/test/isms/ERC5164ISM.t.sol +++ b/solidity/test/isms/ERC5164ISM.t.sol @@ -144,7 +144,7 @@ contract ERC5164ISMTest is Test { vm.startPrank(address(executor)); ism.verifyMessageId(messageId); - assertTrue(ism.verifiedMessageIds(messageId)); + assertTrue(ism.verifiedMessages(messageId)); vm.stopPrank(); } diff --git a/solidity/test/isms/OPStackIsm.t.sol b/solidity/test/isms/OPStackIsm.t.sol index 157dd61350..f5823cd851 100644 --- a/solidity/test/isms/OPStackIsm.t.sol +++ b/solidity/test/isms/OPStackIsm.t.sol @@ -3,9 +3,12 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; +import {LibBit} from "../../contracts/libs/LibBit.sol"; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; -import {Mailbox} from "../../contracts/Mailbox.sol"; +import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; +import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; import {Message} from "../../contracts/libs/Message.sol"; +import {MessageUtils} from "./IsmTestUtils.sol"; import {TestMultisigIsm} from "../../contracts/test/TestMultisigIsm.sol"; import {OPStackIsm} from "../../contracts/isms/hook/OPStackIsm.sol"; import {OPStackHook} from "../../contracts/hooks/OPStackHook.sol"; @@ -21,7 +24,9 @@ import {Encoding} from "@eth-optimism/contracts-bedrock/contracts/libraries/Enco import {Hashing} from "@eth-optimism/contracts-bedrock/contracts/libraries/Hashing.sol"; contract OPStackIsmTest is Test { + using LibBit for uint256; using TypeCasts for address; + using MessageUtils for bytes; uint256 internal mainnetFork; uint256 internal optimismFork; @@ -40,15 +45,17 @@ contract OPStackIsmTest is Test { ICrossDomainMessenger internal l1Messenger; L2CrossDomainMessenger internal l2Messenger; - OPStackIsmTest internal opISM; + TestMailbox internal l1Mailbox; + OPStackIsm internal opISM; OPStackHook internal opHook; TestRecipient internal testRecipient; bytes internal testMessage = abi.encodePacked("Hello from the other chain!"); + bytes internal testMetadata = abi.encodePacked(uint256(0)); - bytes encodedMessage = _encodeTestMessage(0, address(testRecipient)); - bytes32 messageId = Message.id(encodedMessage); + bytes internal encodedMessage; + bytes32 internal messageId; uint32 internal constant MAINNET_DOMAIN = 1; uint32 internal constant OPTIMISM_DOMAIN = 10; @@ -65,7 +72,7 @@ contract OPStackIsmTest is Test { event FailedRelayedMessage(bytes32 indexed msgHash); - event ReceivedMessage(bytes32 indexed sender, bytes32 indexed messageId); + event ReceivedMessage(bytes32 indexed messageId); function setUp() public { // block numbers to fork from, chain data is cached to ../../forge-cache/ @@ -73,6 +80,9 @@ contract OPStackIsmTest is Test { optimismFork = vm.createFork(vm.rpcUrl("optimism"), 106_233_774); testRecipient = new TestRecipient(); + + encodedMessage = _encodeTestMessage(); + messageId = Message.id(encodedMessage); } /////////////////////////////////////////////////////////////////// @@ -83,11 +93,13 @@ contract OPStackIsmTest is Test { vm.selectFork(mainnetFork); l1Messenger = ICrossDomainMessenger(L1_MESSENGER_ADDRESS); + l1Mailbox = new TestMailbox(MAINNET_DOMAIN); opHook = new OPStackHook( + address(l1Mailbox), OPTIMISM_DOMAIN, - L1_MESSENGER_ADDRESS, - address(opISM) + address(opISM), + L1_MESSENGER_ADDRESS ); vm.makePersistent(address(opHook)); @@ -108,11 +120,11 @@ contract OPStackIsmTest is Test { vm.selectFork(optimismFork); - opISM.setOptimismHook(address(opHook)); + opISM.setAuthorizedHook(address(opHook)); // for sending value vm.deal( AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS), - 1e18 + 2**255 ); } @@ -128,13 +140,15 @@ contract OPStackIsmTest is Test { vm.selectFork(mainnetFork); bytes memory encodedHookData = abi.encodeCall( - OPStackIsm.verifyMessageId, - (address(this).addressToBytes32(), messageId) + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) ); uint40 nonce = ICanonicalTransactionChain(L1_CANNONICAL_CHAIN) .getQueueLength(); + l1Mailbox.updateLatestDispatchedId(messageId); + vm.expectEmit(true, true, true, false, L1_MESSENGER_ADDRESS); emit SentMessage( address(opISM), @@ -143,8 +157,7 @@ contract OPStackIsmTest is Test { nonce, DEFAULT_GAS_LIMIT ); - - opHook.postDispatch(OPTIMISM_DOMAIN, messageId); + opHook.postDispatch(testMetadata, encodedMessage); } function testFork_postDispatch_RevertWhen_ChainIDNotSupported() public { @@ -152,8 +165,49 @@ contract OPStackIsmTest is Test { vm.selectFork(mainnetFork); - vm.expectRevert("OptimismHook: invalid destination domain"); - opHook.postDispatch(11, messageId); + bytes memory message = MessageUtils.formatMessage( + VERSION, + uint32(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + 11, // wrong domain + TypeCasts.addressToBytes32(address(testRecipient)), + testMessage + ); + + l1Mailbox.updateLatestDispatchedId(Message.id(message)); + vm.expectRevert( + "AbstractMessageIdAuthHook: invalid destination domain" + ); + opHook.postDispatch(testMetadata, message); + } + + function testFork_postDispatch_RevertWhen_TooMuchValue() public { + deployAll(); + + vm.selectFork(mainnetFork); + + vm.deal(address(this), uint256(2**255 + 1)); + bytes memory excessValueMetadata = abi.encodePacked( + uint256(2**255 + 1) + ); + + l1Mailbox.updateLatestDispatchedId(messageId); + vm.expectRevert("OPStackHook: msgValue must less than 2 ** 255"); + opHook.postDispatch(excessValueMetadata, encodedMessage); + } + + function testFork_postDispatch_RevertWhen_NotLastDispatchedMessage() + public + { + deployAll(); + + vm.selectFork(mainnetFork); + + vm.expectRevert( + "AbstractMessageIdAuthHook: message not latest dispatched" + ); + opHook.postDispatch(testMetadata, encodedMessage); } /* ============ ISM.verifyMessageId ============ */ @@ -164,8 +218,8 @@ contract OPStackIsmTest is Test { vm.selectFork(optimismFork); bytes memory encodedHookData = abi.encodeCall( - OPStackIsm.verifyMessageId, - (address(this).addressToBytes32(), messageId) + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) ); (uint240 nonce, uint16 verison) = Encoding.decodeVersionedNonce( @@ -189,8 +243,8 @@ contract OPStackIsmTest is Test { AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS) ); - vm.expectEmit(true, true, false, false, address(opISM)); - emit ReceivedMessage(address(this).addressToBytes32(), messageId); + vm.expectEmit(true, false, false, false, address(opISM)); + emit ReceivedMessage(messageId); vm.expectEmit(true, false, false, false, L2_MESSENGER_ADDRESS); emit RelayedMessage(versionedHash); @@ -204,52 +258,10 @@ contract OPStackIsmTest is Test { encodedHookData ); - assertTrue( - opISM.verifiedMessageIds( - messageId, - address(this).addressToBytes32() - ) - ); - + assertTrue(opISM.verifiedMessages(messageId).isBitSet(255)); vm.stopPrank(); } - // will get included in https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2410 - // function testverifyMessageId_WithValue() public { - // // this would fail - // deployAll(); - - // vm.selectFork(optimismFork); - - // bytes memory encodedHookData = abi.encodeCall( - // OPStackIsm.verifyMessageId, - // (address(this), messageId) - // ); - - // (uint240 nonce, uint16 verison) = - // Encoding.decodeVersionedNonce(l2Messenger.messageNonce()); - // uint256 versionedNonce = Encoding.encodeVersionedNonce(nonce + 1, verison); - - // vm.startPrank( - // AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS) - // ); - - // l2Messenger.relayMessage{value: 1e18} ( - // versionedNonce, - // address(opHook), - // address(opISM), - // 1e18, - // DEFAULT_GAS_LIMIT, - // encodedHookData - // ); - - // assertEq(opISM.verifiedMessageIds(messageId, address(this)), true); - // assertEq(AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS), 0); - // assertEq(address(this).balance, 1e18); - - // vm.stopPrank(); - // } - function testFork_verifyMessageId_RevertWhen_NotAuthorized() public { deployAll(); @@ -257,7 +269,7 @@ contract OPStackIsmTest is Test { // needs to be called by the cannonical messenger on Optimism vm.expectRevert(NotCrossChainCall.selector); - opISM.verifyMessageId(address(opHook).addressToBytes32(), messageId); + opISM.verifyMessageId(messageId); // set the xDomainMessageSender storage slot as alice bytes32 key = bytes32(uint256(204)); @@ -267,8 +279,10 @@ contract OPStackIsmTest is Test { vm.startPrank(L2_MESSENGER_ADDRESS); // needs to be called by the authorized hook contract on Ethereum - vm.expectRevert("OPStackIsm: sender is not the hook"); - opISM.verifyMessageId(address(opHook).addressToBytes32(), messageId); + vm.expectRevert( + "AbstractMessageIdAuthorizedIsm: sender is not the hook" + ); + opISM.verifyMessageId(messageId); } /* ============ ISM.verify ============ */ @@ -279,8 +293,8 @@ contract OPStackIsmTest is Test { vm.selectFork(optimismFork); bytes memory encodedHookData = abi.encodeCall( - OPStackIsm.verifyMessageId, - (address(this).addressToBytes32(), messageId) + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) ); (uint240 nonce, uint16 verison) = Encoding.decodeVersionedNonce( @@ -305,15 +319,16 @@ contract OPStackIsmTest is Test { assertTrue(verified); } - // sending over invalid message - function testFork_verify_RevertWhen_HyperlaneInvalidMessage() public { + /// forge-config: default.fuzz.runs = 10 + function testFork_verify_WithValue(uint256 _msgValue) public { + _msgValue = bound(_msgValue, 0, 2**254); deployAll(); vm.selectFork(optimismFork); bytes memory encodedHookData = abi.encodeCall( - OPStackIsm.verifyMessageId, - (address(this).addressToBytes32(), messageId) + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) ); (uint240 nonce, uint16 verison) = Encoding.decodeVersionedNonce( @@ -325,32 +340,31 @@ contract OPStackIsmTest is Test { ); vm.prank(AddressAliasHelper.applyL1ToL2Alias(L1_MESSENGER_ADDRESS)); - l2Messenger.relayMessage( + l2Messenger.relayMessage{value: _msgValue}( versionedNonce, address(opHook), address(opISM), - 0, + _msgValue, DEFAULT_GAS_LIMIT, encodedHookData ); - bytes memory invalidMessage = _encodeTestMessage(0, address(this)); - bool verified = opISM.verify(new bytes(0), invalidMessage); - assertFalse(verified); + bool verified = opISM.verify(new bytes(0), encodedMessage); + assertTrue(verified); + + assertEq(address(opISM).balance, 0); + assertEq(address(testRecipient).balance, _msgValue); } - // invalid messageID in postDispatch - function testFork_verify_RevertWhen_InvalidOptimismMessageID() public { + // sending over invalid message + function testFork_verify_RevertWhen_HyperlaneInvalidMessage() public { deployAll(); vm.selectFork(optimismFork); - bytes memory invalidMessage = _encodeTestMessage(0, address(this)); - bytes32 _messageId = Message.id(invalidMessage); - bytes memory encodedHookData = abi.encodeCall( - OPStackIsm.verifyMessageId, - (address(this).addressToBytes32(), _messageId) + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) ); (uint240 nonce, uint16 verison) = Encoding.decodeVersionedNonce( @@ -371,18 +385,38 @@ contract OPStackIsmTest is Test { encodedHookData ); - bool verified = opISM.verify(new bytes(0), encodedMessage); + bytes memory invalidMessage = MessageUtils.formatMessage( + VERSION, + uint8(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + OPTIMISM_DOMAIN, + TypeCasts.addressToBytes32(address(this)), // wrong recipient + testMessage + ); + bool verified = opISM.verify(new bytes(0), invalidMessage); assertFalse(verified); } - function testFork_verify_RevertWhen_InvalidSender() public { + // invalid messageID in postDispatch + function testFork_verify_RevertWhen_InvalidOptimismMessageID() public { deployAll(); - vm.selectFork(optimismFork); + bytes memory invalidMessage = MessageUtils.formatMessage( + VERSION, + uint8(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + OPTIMISM_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + testMessage + ); + bytes32 _messageId = Message.id(invalidMessage); + bytes memory encodedHookData = abi.encodeCall( - OPStackIsm.verifyMessageId, - (alice.addressToBytes32(), messageId) + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (_messageId) ); (uint240 nonce, uint16 verison) = Encoding.decodeVersionedNonce( @@ -409,19 +443,15 @@ contract OPStackIsmTest is Test { /* ============ helper functions ============ */ - function _encodeTestMessage(uint32 _msgCount, address _receipient) - internal - view - returns (bytes memory) - { + function _encodeTestMessage() internal view returns (bytes memory) { return - abi.encodePacked( + MessageUtils.formatMessage( VERSION, - _msgCount, + uint32(0), MAINNET_DOMAIN, TypeCasts.addressToBytes32(address(this)), OPTIMISM_DOMAIN, - TypeCasts.addressToBytes32(_receipient), + TypeCasts.addressToBytes32(address(testRecipient)), testMessage ); } diff --git a/solidity/test/lib/LibBit.t.sol b/solidity/test/lib/LibBit.t.sol new file mode 100644 index 0000000000..54b7af257b --- /dev/null +++ b/solidity/test/lib/LibBit.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT or Apache-2.0 +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; + +import {LibBit} from "../../contracts/libs/LibBit.sol"; + +contract LibBitTest is Test { + using LibBit for uint256; + + uint256 testValue; + uint256 MAX_INT = + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + + function setUp() public { + testValue = 0; + } + + function testSetBit(uint8 index) public { + testValue = testValue.setBit(index); + assertEq(testValue, 2**index); + } + + function testClearBit(uint8 index) public { + testValue = MAX_INT; + testValue = testValue.clearBit(index); + assertEq(testValue + 2**index, MAX_INT); + } + + function testIsBitSet(uint8 index) public { + testValue = 2**index; + assertTrue(testValue.isBitSet(index)); + } +} From a574665e385695b89e572aa8beee89aedc1097c4 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 15 Aug 2023 12:10:49 -0400 Subject: [PATCH 13/27] Remove e2e and add yarn build to CI --- .github/workflows/e2e.yml | 1 + .github/workflows/node.yml | 48 +++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 24c5e8d45d..41d62b326a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -4,6 +4,7 @@ on: push: branches: [main] pull_request: + branches: [main] workflow_dispatch: concurrency: diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 9db7a0a9e5..85d26b7646 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -38,33 +38,33 @@ jobs: exit 1 fi - # yarn-build: - # runs-on: ubuntu-latest - # needs: [yarn-install] - # steps: - # - uses: actions/checkout@v3 - # with: - # submodules: recursive - # - uses: actions/setup-node@v3 - # with: - # node-version: 18 + yarn-build: + runs-on: ubuntu-latest + needs: [yarn-install] + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - uses: actions/setup-node@v3 + with: + node-version: 18 - # - name: yarn-cache - # uses: actions/cache@v3 - # with: - # path: | - # **/node_modules - # .yarn/cache - # key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} + - name: yarn-cache + uses: actions/cache@v3 + with: + path: | + **/node_modules + .yarn/cache + key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} - # - name: build-cache - # uses: actions/cache@v3 - # with: - # path: ./* - # key: ${{ github.sha }} + - name: build-cache + uses: actions/cache@v3 + with: + path: ./* + key: ${{ github.sha }} - # - name: core build - # run: yarn workspace @hyperlane-xyz/core build + - name: core build + run: yarn workspace @hyperlane-xyz/core build # lint-prettier: # runs-on: ubuntu-latest From abc8b796ba471a88f411d231b2f999239904d703 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 15 Aug 2023 12:15:27 -0400 Subject: [PATCH 14/27] Adding protocol fees (#2640) ### Description - Adding protocol fee as a hook ### Drive-by changes - None ### Related issues V3 ### Backward compatibility Yes ### Testing Fuzz tests --------- Co-authored-by: Yorke Rhodes --- .../contracts/hooks/StaticProtocolFee.sol | 125 +++++++++++++++ .../hooks/FallbackDomainRoutingHook.t.sol | 12 +- solidity/test/hooks/StaticProtocolFee.t.sol | 146 ++++++++++++++++++ 3 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 solidity/contracts/hooks/StaticProtocolFee.sol create mode 100644 solidity/test/hooks/StaticProtocolFee.t.sol diff --git a/solidity/contracts/hooks/StaticProtocolFee.sol b/solidity/contracts/hooks/StaticProtocolFee.sol new file mode 100644 index 0000000000..deb3a75afc --- /dev/null +++ b/solidity/contracts/hooks/StaticProtocolFee.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ +import {Message} from "../libs/Message.sol"; +import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; +// ============ External Imports ============ +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title StaticProtocolFee + * @notice Collects a static protocol fee from the sender. + * @dev V3 WIP + */ +contract StaticProtocolFee is IPostDispatchHook, Ownable { + using Address for address payable; + using Message for bytes; + + // ============ Constants ============ + + /// @notice The maximum protocol fee that can be set. + uint256 public immutable MAX_PROTOCOL_FEE; + + // ============ Public Storage ============ + + /// @notice The current protocol fee. + uint256 public protocolFee; + /// @notice The beneficiary of protocol fees. + address public beneficiary; + + // ============ Constructor ============ + + constructor( + uint256 _maxProtocolFee, + uint256 _protocolFee, + address _beneficiary, + address _owner + ) { + MAX_PROTOCOL_FEE = _maxProtocolFee; + _setProtocolFee(_protocolFee); + _setBeneficiary(_beneficiary); + _transferOwnership(_owner); + } + + // ============ External Functions ============ + + /** + * @notice Collects the protocol fee from the sender. + */ + function postDispatch(bytes calldata, bytes calldata message) + external + payable + override + { + require( + msg.value >= protocolFee, + "StaticProtocolFee: insufficient protocol fee" + ); + + uint256 refund = msg.value - protocolFee; + if (refund > 0) payable(message.senderAddress()).sendValue(refund); + } + + /** + * @notice Sets the protocol fee. + * @param _protocolFee The new protocol fee. + */ + function setProtocolFee(uint256 _protocolFee) external onlyOwner { + _setProtocolFee(_protocolFee); + } + + /** + * @notice Sets the beneficiary of protocol fees. + * @param _beneficiary The new beneficiary. + */ + function setBeneficiary(address _beneficiary) external onlyOwner { + _setBeneficiary(_beneficiary); + } + + /** + * @notice Collects protocol fees from the contract. + */ + function collectProtocolFees() external { + payable(beneficiary).sendValue(address(this).balance); + } + + // ============ Internal Functions ============ + + /** + * @notice Sets the protocol fee. + * @param _protocolFee The new protocol fee. + */ + function _setProtocolFee(uint256 _protocolFee) internal { + require( + _protocolFee <= MAX_PROTOCOL_FEE, + "StaticProtocolFee: exceeds max protocol fee" + ); + protocolFee = _protocolFee; + } + + /** + * @notice Sets the beneficiary of protocol fees. + * @param _beneficiary The new beneficiary. + */ + function _setBeneficiary(address _beneficiary) internal { + require( + _beneficiary != address(0), + "StaticProtocolFee: invalid beneficiary" + ); + beneficiary = _beneficiary; + } +} diff --git a/solidity/test/hooks/FallbackDomainRoutingHook.t.sol b/solidity/test/hooks/FallbackDomainRoutingHook.t.sol index 147b2431de..1e87e65180 100644 --- a/solidity/test/hooks/FallbackDomainRoutingHook.t.sol +++ b/solidity/test/hooks/FallbackDomainRoutingHook.t.sol @@ -6,13 +6,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 {ConfigFallbackDomainRoutingHook} from "../../contracts/hooks/ConfigFallbackDomainRoutingHook.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; + ConfigFallbackDomainRoutingHook internal fallbackHook; TestPostDispatchHook internal configuredTestHook; TestPostDispatchHook internal mailboxDefaultHook; TestRecipient internal testRecipient; @@ -29,10 +29,7 @@ contract FallbackDomainRoutingHookTest is Test { configuredTestHook = new TestPostDispatchHook(); mailboxDefaultHook = new TestPostDispatchHook(); testRecipient = new TestRecipient(); - fallbackHook = new FallbackDomainRoutingHook( - address(mailbox), - address(this) - ); + fallbackHook = new ConfigFallbackDomainRoutingHook(address(mailbox)); testMessage = _encodeTestMessage(); mailbox.setDefaultHook(address(mailboxDefaultHook)); } @@ -40,7 +37,8 @@ contract FallbackDomainRoutingHookTest is Test { function test_postDispatchHook_configured() public payable { fallbackHook.setHook( TEST_DESTINATION_DOMAIN, - address(configuredTestHook) + address(testRecipient).addressToBytes32(), + configuredTestHook ); vm.expectEmit(false, false, false, false, address(configuredTestHook)); diff --git a/solidity/test/hooks/StaticProtocolFee.t.sol b/solidity/test/hooks/StaticProtocolFee.t.sol new file mode 100644 index 0000000000..6428bf1728 --- /dev/null +++ b/solidity/test/hooks/StaticProtocolFee.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: 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 "../isms/IsmTestUtils.sol"; + +import {StaticProtocolFee} from "../../contracts/hooks/StaticProtocolFee.sol"; + +contract StaticProtocolFeeTest is Test { + using TypeCasts for address; + StaticProtocolFee internal fees; + + address internal alice = address(0x1); // alice the user + address internal bob = address(0x2); // bob the beneficiary + address internal charlie = address(0x3); // charlie the crock + + uint32 internal constant TEST_ORIGIN_DOMAIN = 1; + uint32 internal constant TEST_DESTINATION_DOMAIN = 2; + + bytes internal testMessage; + + function setUp() public { + fees = new StaticProtocolFee(1e16, 1e15, bob, address(this)); + + testMessage = _encodeTestMessage(); + } + + function testConstructor() public { + assertEq(fees.protocolFee(), 1e15); + } + + function testSetProtocolFee(uint256 fee) public { + fee = bound(fee, 0, fees.MAX_PROTOCOL_FEE()); + fees.setProtocolFee(fee); + assertEq(fees.protocolFee(), fee); + } + + function testSetProtocolFee_revertsWhen_notOwner() public { + uint256 fee = 1e17; + + vm.prank(charlie); + vm.expectRevert("Ownable: caller is not the owner"); + fees.setProtocolFee(fee); + + assertEq(fees.protocolFee(), 1e15); + } + + function testSetProtocolFee_revertWhen_exceedsMax(uint256 fee) public { + fee = bound( + fee, + fees.MAX_PROTOCOL_FEE() + 1, + 10 * fees.MAX_PROTOCOL_FEE() + ); + + vm.expectRevert("StaticProtocolFee: exceeds max protocol fee"); + fees.setProtocolFee(fee); + + assertEq(fees.protocolFee(), 1e15); + } + + function testSetBeneficiary_revertWhen_notOwner() public { + vm.prank(charlie); + + vm.expectRevert("Ownable: caller is not the owner"); + fees.setBeneficiary(charlie); + assertEq(fees.beneficiary(), bob); + } + + function testFuzz_postDispatch_inusfficientFees( + uint256 feeRequired, + uint256 feeSent + ) public { + feeRequired = bound(feeRequired, 1, fees.MAX_PROTOCOL_FEE()); + // bound feeSent to be less than feeRequired + feeSent = bound(feeSent, 0, feeRequired - 1); + vm.deal(alice, feeSent); + + fees.setProtocolFee(feeRequired); + + uint256 balanceBefore = alice.balance; + + vm.prank(alice); + vm.expectRevert("StaticProtocolFee: insufficient protocol fee"); + fees.postDispatch{value: feeSent}("", ""); + + assertEq(alice.balance, balanceBefore); + } + + function testFuzz_postDispatch_sufficientFees( + uint256 feeRequired, + uint256 feeSent + ) public { + feeRequired = bound(feeRequired, 1, fees.MAX_PROTOCOL_FEE()); + feeSent = bound(feeSent, feeRequired, 10 * feeRequired); + vm.deal(alice, feeSent); + + fees.setProtocolFee(feeRequired); + uint256 aliceBalanceBefore = alice.balance; + vm.prank(alice); + + fees.postDispatch{value: feeSent}("", testMessage); + + assertEq(alice.balance, aliceBalanceBefore - feeRequired); + } + + function testFuzz_collectProtocolFee( + uint256 feeRequired, + uint256 dispatchCalls + ) public { + feeRequired = bound(feeRequired, 1, fees.MAX_PROTOCOL_FEE()); + // no of postDispatch calls to be made + dispatchCalls = bound(dispatchCalls, 1, 1000); + vm.deal(alice, feeRequired * dispatchCalls); + + fees.setProtocolFee(feeRequired); + + uint256 balanceBefore = bob.balance; + + for (uint256 i = 0; i < dispatchCalls; i++) { + vm.prank(alice); + fees.postDispatch{value: feeRequired}("", ""); + } + + fees.collectProtocolFees(); + + assertEq(bob.balance, balanceBefore + feeRequired * dispatchCalls); + } + + // ============ Helper Functions ============ + + function _encodeTestMessage() internal view returns (bytes memory) { + return + MessageUtils.formatMessage( + uint8(0), + uint32(1), + TEST_ORIGIN_DOMAIN, + alice.addressToBytes32(), + TEST_DESTINATION_DOMAIN, + alice.addressToBytes32(), + abi.encodePacked("Hello World") + ); + } + + receive() external payable {} +} From 2e678689c712227cd6b54d920cb81349aff9e52d Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Thu, 17 Aug 2023 14:08:04 -0400 Subject: [PATCH 15/27] Fix forge tests post V3 (#2661) ### Description - fixes GasRouter expectRevert message - added TestMerkleRootHook for proof() and fixes MerkleRootMultisig test - fixes ERC5164 tests post v3 changes ### Drive-by changes - added contract name to mailbox and abstractHook error messages - removed unnecessary tests for "message too large" - downgrade slither in actions to 0.3.0 because of their recent "missing inheritance" bug on main ### Related issues - V3 ### Backward compatibility Yes ### Testing Unit tests --- .github/workflows/node.yml | 2 +- solidity/contracts/Mailbox.sol | 22 +++-- .../hooks/AbstractMessageIdAuthHook.sol | 7 +- solidity/contracts/hooks/ERC5164Hook.sol | 1 + .../interfaces/IMessageDispatcher.sol | 30 ------- .../hook/AbstractMessageIdAuthorizedIsm.sol | 2 +- .../hook/{OPStackISM.sol => OPStackIsm.sol} | 0 solidity/contracts/mock/MockERC5164.sol | 2 +- solidity/contracts/test/TestMailbox.sol | 20 +---- .../contracts/test/TestMerkleTreeHook.sol | 25 ++++++ solidity/slither.config.json | 3 +- solidity/test/GasRouter.t.sol | 4 +- .../test/hyperlaneConnectionClient.test.ts | 4 +- solidity/test/isms/ERC5164ISM.t.sol | 65 +++++++++------ solidity/test/isms/MultisigIsm.t.sol | 15 ++-- solidity/test/isms/OPStackIsm.t.sol | 2 +- solidity/test/isms/legacyMultisigIsm.test.ts | 18 +++-- solidity/test/lib/mailboxes.ts | 22 +++-- solidity/test/mailbox.test.ts | 80 +++++-------------- solidity/test/router.test.ts | 10 ++- 20 files changed, 165 insertions(+), 169 deletions(-) delete mode 100644 solidity/contracts/interfaces/IMessageDispatcher.sol rename solidity/contracts/isms/hook/{OPStackISM.sol => OPStackIsm.sol} (100%) create mode 100644 solidity/contracts/test/TestMerkleTreeHook.sol diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 85d26b7646..3da77e514d 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -174,7 +174,7 @@ jobs: run: yarn workspace @hyperlane-xyz/core run test - name: Run Slither - uses: crytic/slither-action@main + uses: crytic/slither-action@v0.3.0 id: slither with: target: 'solidity/' diff --git a/solidity/contracts/Mailbox.sol b/solidity/contracts/Mailbox.sol index 01d9ad529e..69ce9b093f 100644 --- a/solidity/contracts/Mailbox.sol +++ b/solidity/contracts/Mailbox.sol @@ -76,13 +76,17 @@ contract Mailbox is IMailbox, Versioned, Ownable { * @param _module The new default ISM. Must be a contract. */ function setDefaultIsm(address _module) external onlyOwner { - require(Address.isContract(_module), "!contract"); + require(Address.isContract(_module), "Mailbox: !contract"); defaultIsm = IInterchainSecurityModule(_module); emit DefaultIsmSet(_module); } + /** + * @notice Sets the default post dispatch hook for the Mailbox. + * @param _hook The new default post dispatch hook. Must be a contract. + */ function setDefaultHook(address _hook) external onlyOwner { - require(Address.isContract(_hook), "!contract"); + require(Address.isContract(_hook), "Mailbox: !contract"); defaultHook = IPostDispatchHook(_hook); emit DefaultHookSet(_hook); } @@ -158,8 +162,9 @@ contract Mailbox is IMailbox, Versioned, Ownable { nonce += 1; latestDispatchedId = id; - emit DispatchId(id); + emit Dispatch(message); + emit DispatchId(id); /// INTERACTIONS /// @@ -186,15 +191,15 @@ contract Mailbox is IMailbox, Versioned, Ownable { /// CHECKS /// // Check that the message was intended for this mailbox. - require(_message.version() == VERSION, "bad version"); + require(_message.version() == VERSION, "Mailbox: bad version"); require( _message.destination() == localDomain, - "unexpected destination" + "Mailbox: unexpected destination" ); // Check that the message hasn't already been delivered. bytes32 _id = _message.id(); - require(delivered(_id) == false, "already delivered"); + require(delivered(_id) == false, "Mailbox: already delivered"); // Get the recipient's ISM. address recipient = _message.recipientAddress(); @@ -214,7 +219,10 @@ contract Mailbox is IMailbox, Versioned, Ownable { /// INTERACTIONS /// // Verify the message via the ISM. - require(ism.verify(_metadata, _message), "verification failed"); + require( + ism.verify(_metadata, _message), + "Mailbox: verification failed" + ); // Deliver the message to the recipient. IMessageRecipient(recipient).handle{value: msg.value}( diff --git a/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol b/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol index afc8ee415a..2b01ba5ad2 100644 --- a/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol +++ b/solidity/contracts/hooks/AbstractMessageIdAuthHook.sol @@ -47,8 +47,11 @@ abstract contract AbstractMessageIdAuthHook is uint32 _destinationDomain, address _ism ) MailboxClient(mailbox) { - require(_ism != address(0), "invalid ISM"); - require(_destinationDomain != 0, "invalid destination domain"); + require(_ism != address(0), "AbstractMessageIdAuthHook: invalid ISM"); + require( + _destinationDomain != 0, + "AbstractMessageIdAuthHook: invalid destination domain" + ); ism = _ism; destinationDomain = _destinationDomain; } diff --git a/solidity/contracts/hooks/ERC5164Hook.sol b/solidity/contracts/hooks/ERC5164Hook.sol index 805f58d99a..b401140d2b 100644 --- a/solidity/contracts/hooks/ERC5164Hook.sol +++ b/solidity/contracts/hooks/ERC5164Hook.sol @@ -47,6 +47,7 @@ contract ERC5164Hook is AbstractMessageIdAuthHook { bytes calldata, /* metadata */ bytes memory payload ) internal override { + require(msg.value == 0, "ERC5164Hook: no value allowed"); dispatcher.dispatchMessage(destinationDomain, ism, payload); } } diff --git a/solidity/contracts/interfaces/IMessageDispatcher.sol b/solidity/contracts/interfaces/IMessageDispatcher.sol deleted file mode 100644 index b9058687ff..0000000000 --- a/solidity/contracts/interfaces/IMessageDispatcher.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; - -/** - * @title ERC-5164: Cross-Chain Execution Standard - * @dev See https://eips.ethereum.org/EIPS/eip-5164 - */ -interface IMessageDispatcher { - /** - * @notice Emitted when a message has successfully been dispatched to the executor chain. - * @param messageId ID uniquely identifying the message - * @param from Address that dispatched the message - * @param toChainId ID of the chain receiving the message - * @param to Address that will receive the message - * @param data Data that was dispatched - */ - event MessageDispatched( - bytes32 indexed messageId, - address indexed from, - uint256 indexed toChainId, - address to, - bytes data - ); - - function dispatchMessage( - uint256 toChainId, - address to, - bytes calldata data - ) external returns (bytes32); -} diff --git a/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol b/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol index 145a337501..bdc08c3426 100644 --- a/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol +++ b/solidity/contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol @@ -93,7 +93,7 @@ abstract contract AbstractMessageIdAuthorizedIsm is * @dev Only callable by the L2 messenger. * @param messageId Hyperlane Id of the message. */ - function verifyMessageId(bytes32 messageId) external payable { + function verifyMessageId(bytes32 messageId) external payable virtual { require( _isAuthorized(), "AbstractMessageIdAuthorizedIsm: sender is not the hook" diff --git a/solidity/contracts/isms/hook/OPStackISM.sol b/solidity/contracts/isms/hook/OPStackIsm.sol similarity index 100% rename from solidity/contracts/isms/hook/OPStackISM.sol rename to solidity/contracts/isms/hook/OPStackIsm.sol diff --git a/solidity/contracts/mock/MockERC5164.sol b/solidity/contracts/mock/MockERC5164.sol index 4175c43092..6263e5d1b8 100644 --- a/solidity/contracts/mock/MockERC5164.sol +++ b/solidity/contracts/mock/MockERC5164.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT or Apache-2.0 pragma solidity ^0.8.13; -import {IMessageDispatcher} from "../interfaces/IMessageDispatcher.sol"; +import {IMessageDispatcher} from "../interfaces/hooks/IMessageDispatcher.sol"; contract MockMessageDispatcher is IMessageDispatcher { function dispatchMessage( diff --git a/solidity/contracts/test/TestMailbox.sol b/solidity/contracts/test/TestMailbox.sol index 4eda8ffaff..af5204dfcf 100644 --- a/solidity/contracts/test/TestMailbox.sol +++ b/solidity/contracts/test/TestMailbox.sol @@ -9,23 +9,9 @@ import {IMessageRecipient} from "../interfaces/IMessageRecipient.sol"; contract TestMailbox is Mailbox { using TypeCasts for bytes32; - 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; - - // 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; - // } + constructor(uint32 _localDomain, address _owner) + Mailbox(_localDomain, _owner) + {} // solhint-disable-line no-empty-blocks function testHandle( uint32 _origin, diff --git a/solidity/contracts/test/TestMerkleTreeHook.sol b/solidity/contracts/test/TestMerkleTreeHook.sol new file mode 100644 index 0000000000..291d37d146 --- /dev/null +++ b/solidity/contracts/test/TestMerkleTreeHook.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {MerkleLib} from "../libs/Merkle.sol"; +import {MerkleTreeHook} from "../hooks/MerkleTreeHook.sol"; + +contract TestMerkleTreeHook is MerkleTreeHook { + constructor(address _mailbox) MerkleTreeHook(_mailbox) {} + + 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; + } +} diff --git a/solidity/slither.config.json b/solidity/slither.config.json index 777c5e893c..d92e194767 100644 --- a/solidity/slither.config.json +++ b/solidity/slither.config.json @@ -1,5 +1,6 @@ { "filter_paths": "lib|node_modules|test|Mock*|Test*", "compile_force_framework": "foundry", - "exclude_informational": true + "exclude_informational": true, + "no_fail": true } diff --git a/solidity/test/GasRouter.t.sol b/solidity/test/GasRouter.t.sol index 276395445e..4146c8a70b 100644 --- a/solidity/test/GasRouter.t.sol +++ b/solidity/test/GasRouter.t.sol @@ -111,7 +111,9 @@ contract GasRouterTest is Test { vm.deal(address(this), requiredPayment + 1); passRefund = false; - vm.expectRevert("Interchain gas payment refund failed"); + vm.expectRevert( + "Address: unable to send value, recipient may have reverted" + ); originRouter.dispatchWithGas{value: requiredPayment + 1}( remoteDomain, "" diff --git a/solidity/test/hyperlaneConnectionClient.test.ts b/solidity/test/hyperlaneConnectionClient.test.ts index a5423fb803..e4ef4ab097 100644 --- a/solidity/test/hyperlaneConnectionClient.test.ts +++ b/solidity/test/hyperlaneConnectionClient.test.ts @@ -27,8 +27,8 @@ describe('HyperlaneConnectionClient', async () => { beforeEach(async () => { const mailboxFactory = new Mailbox__factory(signer); const domain = 1000; - mailbox = await mailboxFactory.deploy(domain); - newMailbox = await mailboxFactory.deploy(domain); + mailbox = await mailboxFactory.deploy(domain, signer.address); + newMailbox = await mailboxFactory.deploy(domain, signer.address); const connectionClientFactory = new TestHyperlaneConnectionClient__factory( signer, diff --git a/solidity/test/isms/ERC5164ISM.t.sol b/solidity/test/isms/ERC5164ISM.t.sol index 012b6b6749..fad9df6479 100644 --- a/solidity/test/isms/ERC5164ISM.t.sol +++ b/solidity/test/isms/ERC5164ISM.t.sol @@ -3,25 +3,31 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; +import {LibBit} from "../../contracts/libs/LibBit.sol"; import {Message} from "../../contracts/libs/Message.sol"; +import {MessageUtils} from "./IsmTestUtils.sol"; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; -import {IMessageDispatcher} from "../../contracts/interfaces/IMessageDispatcher.sol"; +import {IMessageDispatcher} from "../../contracts/interfaces/hooks/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 {TestMailbox} from "../../contracts/test/TestMailbox.sol"; import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; import {MockMessageDispatcher, MockMessageExecutor} from "../../contracts/mock/MockERC5164.sol"; contract ERC5164ISMTest is Test { + using LibBit for uint256; using TypeCasts for address; using Message for bytes; + using MessageUtils for bytes; IMessageDispatcher internal dispatcher; MockMessageExecutor internal executor; ERC5164Hook internal hook; ERC5164ISM internal ism; + TestMailbox internal srcMailbox; TestRecipient internal testRecipient; uint32 internal constant TEST1_DOMAIN = 1; @@ -55,14 +61,15 @@ contract ERC5164ISMTest is Test { } function deployContracts() public { + srcMailbox = new TestMailbox(TEST1_DOMAIN, address(this)); ism = new ERC5164ISM(address(executor)); - address mailbox = address(0); // TODO: check? hook = new ERC5164Hook( - mailbox, + address(srcMailbox), TEST2_DOMAIN, address(ism), address(dispatcher) ); + ism.setAuthorizedHook(address(hook)); } /////////////////////////////////////////////////////////////////// @@ -81,7 +88,9 @@ contract ERC5164ISMTest is Test { address(dispatcher) ); - vm.expectRevert("ERC5164Hook: invalid destination domain"); + vm.expectRevert( + "AbstractMessageIdAuthHook: invalid destination domain" + ); hook = new ERC5164Hook( address(dispatcher), 0, @@ -89,7 +98,7 @@ contract ERC5164ISMTest is Test { address(dispatcher) ); - vm.expectRevert("ERC5164Hook: invalid ISM"); + vm.expectRevert("AbstractMessageIdAuthHook: invalid ISM"); hook = new ERC5164Hook( address(dispatcher), TEST2_DOMAIN, @@ -113,6 +122,7 @@ contract ERC5164ISMTest is Test { AbstractMessageIdAuthorizedIsm.verifyMessageId, (messageId) ); + srcMailbox.updateLatestDispatchedId(messageId); // note: not checking for messageId since this is implementation dependent on each vendor vm.expectEmit(false, true, true, true, address(dispatcher)); @@ -130,12 +140,32 @@ contract ERC5164ISMTest is Test { function test_postDispatch_RevertWhen_ChainIDNotSupported() public { deployContracts(); - encodedMessage = _encodeTestMessage(0, address(this)); + encodedMessage = MessageUtils.formatMessage( + VERSION, + 0, + TEST1_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + 3, // unsupported chain id + TypeCasts.addressToBytes32(address(testRecipient)), + testMessage + ); + srcMailbox.updateLatestDispatchedId(Message.id(encodedMessage)); - vm.expectRevert("ERC5164Hook: invalid destination domain"); + vm.expectRevert( + "AbstractMessageIdAuthHook: invalid destination domain" + ); hook.postDispatch(bytes(""), encodedMessage); } + function test_postDispatch_RevertWhen_msgValueNotAllowed() public payable { + deployContracts(); + + srcMailbox.updateLatestDispatchedId(messageId); + + vm.expectRevert("ERC5164Hook: no value allowed"); + hook.postDispatch{value: 1}(bytes(""), encodedMessage); + } + /* ============ ISM.verifyMessageId ============ */ function test_verifyMessageId() public { @@ -144,7 +174,7 @@ contract ERC5164ISMTest is Test { vm.startPrank(address(executor)); ism.verifyMessageId(messageId); - assertTrue(ism.verifiedMessages(messageId)); + assertTrue(ism.verifiedMessages(messageId).isBitSet(255)); vm.stopPrank(); } @@ -155,7 +185,9 @@ contract ERC5164ISMTest is Test { vm.startPrank(alice); // needs to be called by the authorized hook contract on Ethereum - vm.expectRevert("ERC5164ISM: sender is not the executor"); + vm.expectRevert( + "AbstractMessageIdAuthorizedIsm: sender is not the hook" + ); ism.verifyMessageId(messageId); vm.stopPrank(); @@ -190,19 +222,6 @@ contract ERC5164ISMTest is Test { vm.stopPrank(); } - function test_verify_RevertWhen_InvalidSender() public { - deployContracts(); - - vm.startPrank(address(executor)); - - ism.verifyMessageId(messageId); - - bool verified = ism.verify(new bytes(0), encodedMessage); - assertFalse(verified); - - vm.stopPrank(); - } - /* ============ helper functions ============ */ function _encodeTestMessage(uint32 _msgCount, address _receipient) @@ -211,7 +230,7 @@ contract ERC5164ISMTest is Test { returns (bytes memory) { return - abi.encodePacked( + MessageUtils.formatMessage( VERSION, _msgCount, TEST1_DOMAIN, diff --git a/solidity/test/isms/MultisigIsm.t.sol b/solidity/test/isms/MultisigIsm.t.sol index 40e42f461e..63f1309e80 100644 --- a/solidity/test/isms/MultisigIsm.t.sol +++ b/solidity/test/isms/MultisigIsm.t.sol @@ -11,6 +11,7 @@ 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 {TestMerkleTreeHook} from "../../contracts/test/TestMerkleTreeHook.sol"; import {Message} from "../../contracts/libs/Message.sol"; import {MOfNTestUtils} from "./IsmTestUtils.sol"; @@ -21,7 +22,7 @@ abstract contract AbstractMultisigIsmTest is Test { uint32 constant ORIGIN = 11; StaticMOfNAddressSetFactory factory; IMultisigIsm ism; - MerkleTreeHook merkleTreeHook; + TestMerkleTreeHook internal merkleTreeHook; TestMailbox mailbox; function metadataPrefix(bytes memory message) @@ -134,8 +135,8 @@ contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest { using Message for bytes; function setUp() public { - mailbox = new TestMailbox(ORIGIN); - merkleTreeHook = new MerkleTreeHook(address(mailbox)); + mailbox = new TestMailbox(ORIGIN, address(this)); + merkleTreeHook = new TestMerkleTreeHook(address(mailbox)); factory = new StaticMerkleRootMultisigIsmFactory(); mailbox.setDefaultHook(address(merkleTreeHook)); } @@ -152,8 +153,8 @@ contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest { abi.encodePacked( mailboxAsBytes32, checkpointIndex, - message.id() - // mailbox.proof() + message.id(), + merkleTreeHook.proof() ); } } @@ -162,8 +163,8 @@ contract MessageIdMultisigIsmTest is AbstractMultisigIsmTest { using Message for bytes; function setUp() public { - mailbox = new TestMailbox(ORIGIN); - merkleTreeHook = new MerkleTreeHook(address(mailbox)); + mailbox = new TestMailbox(ORIGIN, address(this)); + merkleTreeHook = new TestMerkleTreeHook(address(mailbox)); factory = new StaticMessageIdMultisigIsmFactory(); mailbox.setDefaultHook(address(merkleTreeHook)); } diff --git a/solidity/test/isms/OPStackIsm.t.sol b/solidity/test/isms/OPStackIsm.t.sol index f5823cd851..882599c94e 100644 --- a/solidity/test/isms/OPStackIsm.t.sol +++ b/solidity/test/isms/OPStackIsm.t.sol @@ -93,7 +93,7 @@ contract OPStackIsmTest is Test { vm.selectFork(mainnetFork); l1Messenger = ICrossDomainMessenger(L1_MESSENGER_ADDRESS); - l1Mailbox = new TestMailbox(MAINNET_DOMAIN); + l1Mailbox = new TestMailbox(MAINNET_DOMAIN, address(this)); opHook = new OPStackHook( address(l1Mailbox), diff --git a/solidity/test/isms/legacyMultisigIsm.test.ts b/solidity/test/isms/legacyMultisigIsm.test.ts index 66ca90eeb6..4032b7b18e 100644 --- a/solidity/test/isms/legacyMultisigIsm.test.ts +++ b/solidity/test/isms/legacyMultisigIsm.test.ts @@ -12,6 +12,8 @@ import { TestLegacyMultisigIsm__factory, TestMailbox, TestMailbox__factory, + TestMerkleTreeHook, + TestMerkleTreeHook__factory, TestRecipient__factory, } from '../../types'; import { @@ -27,6 +29,7 @@ const DESTINATION_DOMAIN = 4321; describe('LegacyMultisigIsm', async () => { let multisigIsm: TestLegacyMultisigIsm, mailbox: TestMailbox, + defaultHook: TestMerkleTreeHook, signer: SignerWithAddress, nonOwner: SignerWithAddress, validators: Validator[]; @@ -35,7 +38,10 @@ describe('LegacyMultisigIsm', async () => { const signers = await ethers.getSigners(); [signer, nonOwner] = signers; const mailboxFactory = new TestMailbox__factory(signer); - mailbox = await mailboxFactory.deploy(ORIGIN_DOMAIN); + mailbox = await mailboxFactory.deploy(ORIGIN_DOMAIN, signer.address); + const defaultHookFactory = new TestMerkleTreeHook__factory(signer); + defaultHook = await defaultHookFactory.deploy(mailbox.address); + await mailbox.setDefaultHook(defaultHook.address); validators = await Promise.all( signers .filter((_, i) => i > 1) @@ -383,6 +389,7 @@ describe('LegacyMultisigIsm', async () => { ({ message, metadata } = await dispatchMessageAndReturnMetadata( mailbox, + defaultHook, multisigIsm, DESTINATION_DOMAIN, recipient, @@ -399,8 +406,9 @@ describe('LegacyMultisigIsm', async () => { const mailboxFactory = new TestMailbox__factory(signer); const destinationMailbox = await mailboxFactory.deploy( DESTINATION_DOMAIN, + signer.address, ); - await destinationMailbox.initialize(signer.address, multisigIsm.address); + await destinationMailbox.setDefaultIsm(multisigIsm.address); await destinationMailbox.process(metadata, message); }); @@ -522,12 +530,14 @@ describe('LegacyMultisigIsm', async () => { await multisigIsm.setThreshold(ORIGIN_DOMAIN, threshold); + // TODO: fix const maxBodySize = await mailbox.MAX_MESSAGE_BODY_BYTES(); // The max body is used to estimate an upper bound on gas usage. const maxBody = '0x' + 'AA'.repeat(maxBodySize.toNumber()); ({ message, metadata } = await dispatchMessageAndReturnMetadata( mailbox, + defaultHook, multisigIsm, DESTINATION_DOMAIN, recipient, @@ -540,11 +550,9 @@ describe('LegacyMultisigIsm', async () => { const mailboxFactory = new TestMailbox__factory(signer); const destinationMailbox = await mailboxFactory.deploy( DESTINATION_DOMAIN, - ); - await destinationMailbox.initialize( signer.address, - multisigIsm.address, ); + await destinationMailbox.setDefaultIsm(multisigIsm.address); const gas = await destinationMailbox.estimateGas.process( metadata, message, diff --git a/solidity/test/lib/mailboxes.ts b/solidity/test/lib/mailboxes.ts index c1fe24f1b5..89a6bb1665 100644 --- a/solidity/test/lib/mailboxes.ts +++ b/solidity/test/lib/mailboxes.ts @@ -3,7 +3,11 @@ import { ethers } from 'ethers'; import { Validator, types, utils } from '@hyperlane-xyz/utils'; -import { LegacyMultisigIsm, TestMailbox } from '../../types'; +import { + LegacyMultisigIsm, + TestMailbox, + TestMerkleTreeHook, +} from '../../types'; import { DispatchEvent } from '../../types/contracts/Mailbox'; export type MessageAndProof = { @@ -23,7 +27,7 @@ export const dispatchMessage = async ( messageStr: string, utf8 = true, ) => { - const tx = await mailbox.dispatch( + const tx = await mailbox['dispatch(uint32,bytes32,bytes)']( destination, recipient, utf8 ? ethers.utils.toUtf8Bytes(messageStr) : messageStr, @@ -36,12 +40,13 @@ export const dispatchMessage = async ( export const dispatchMessageAndReturnProof = async ( mailbox: TestMailbox, + merkleHook: TestMerkleTreeHook, destination: number, recipient: string, messageStr: string, utf8 = true, ): Promise => { - const nonce = await mailbox.count(); + const nonce = await mailbox.nonce(); const { message } = await dispatchMessage( mailbox, destination, @@ -50,7 +55,8 @@ export const dispatchMessageAndReturnProof = async ( utf8, ); const messageId = utils.messageId(message); - const proof = await mailbox.proof(); + + const proof = await merkleHook.proof(); return { proof: { branch: proof, @@ -79,6 +85,7 @@ export async function signCheckpoint( export async function dispatchMessageAndReturnMetadata( mailbox: TestMailbox, + merkleHook: TestMerkleTreeHook, multisigIsm: LegacyMultisigIsm, destination: number, recipient: string, @@ -89,15 +96,16 @@ export async function dispatchMessageAndReturnMetadata( ): Promise { // Checkpoint indices are 0 indexed, so we pull the count before // we dispatch the message. - const index = await mailbox.count(); + const index = await mailbox.nonce(); const proofAndMessage = await dispatchMessageAndReturnProof( mailbox, + merkleHook, destination, recipient, messageStr, utf8, ); - const root = await mailbox.root(); + const root = await merkleHook.root(); const signatures = await signCheckpoint( root, index, @@ -138,7 +146,7 @@ export const inferMessageValues = async ( const body = utils.ensure0x( Buffer.from(ethers.utils.toUtf8Bytes(messageStr)).toString('hex'), ); - const nonce = await mailbox.count(); + const nonce = await mailbox.nonce(); const localDomain = await mailbox.localDomain(); const message = utils.formatMessage( version ?? (await mailbox.VERSION()), diff --git a/solidity/test/mailbox.test.ts b/solidity/test/mailbox.test.ts index 71957f08f2..9d31a74063 100644 --- a/solidity/test/mailbox.test.ts +++ b/solidity/test/mailbox.test.ts @@ -12,6 +12,8 @@ import { BadRecipient6__factory, TestMailbox, TestMailbox__factory, + TestMerkleTreeHook, + TestMerkleTreeHook__factory, TestMultisigIsm, TestMultisigIsm__factory, TestRecipient, @@ -26,6 +28,7 @@ const ONLY_OWNER_REVERT_MSG = 'Ownable: caller is not the owner'; describe('Mailbox', async () => { let mailbox: TestMailbox, + defaultHook: TestMerkleTreeHook, module: TestMultisigIsm, signer: SignerWithAddress, nonOwner: SignerWithAddress; @@ -35,25 +38,23 @@ describe('Mailbox', async () => { const moduleFactory = new TestMultisigIsm__factory(signer); module = await moduleFactory.deploy(); const mailboxFactory = new TestMailbox__factory(signer); - mailbox = await mailboxFactory.deploy(originDomain); - await mailbox.initialize(signer.address, module.address); + mailbox = await mailboxFactory.deploy(originDomain, signer.address); + const defaultHookFactory = new TestMerkleTreeHook__factory(signer); + defaultHook = await defaultHookFactory.deploy(mailbox.address); + await mailbox.setDefaultIsm(module.address); + await mailbox.setDefaultHook(defaultHook.address); }); describe('#initialize', () => { it('Sets the owner', async () => { const mailboxFactory = new TestMailbox__factory(signer); - mailbox = await mailboxFactory.deploy(originDomain); + mailbox = await mailboxFactory.deploy(originDomain, nonOwner.address); const expectedOwner = nonOwner.address; - await mailbox.initialize(expectedOwner, module.address); + + await mailbox.connect(nonOwner).setDefaultIsm(module.address); const owner = await mailbox.owner(); expect(owner).equals(expectedOwner); }); - - it('Cannot be initialized twice', async () => { - await expect( - mailbox.initialize(signer.address, module.address), - ).to.be.revertedWith('Initializable: contract is already initialized'); - }); }); describe('#dispatch', () => { @@ -69,22 +70,13 @@ describe('Mailbox', async () => { )); }); - it('Does not dispatch too large messages', async () => { - const longMessage = `0x${Buffer.alloc(3000).toString('hex')}`; - await expect( - mailbox.dispatch( - destDomain, - utils.addressToBytes32(recipient.address), - longMessage, - ), - ).to.be.revertedWith('msg too long'); - }); - it('Dispatches a message', async () => { // Send message with signer address as msg.sender const recipientBytes = utils.addressToBytes32(recipient.address); await expect( - mailbox.connect(signer).dispatch(destDomain, recipientBytes, body), + mailbox + .connect(signer) + ['dispatch(uint32,bytes32,bytes)'](destDomain, recipientBytes, body), ) .to.emit(mailbox, 'Dispatch') .withArgs(signer.address, destDomain, recipientBytes, message) @@ -95,7 +87,7 @@ describe('Mailbox', async () => { it('Returns the id of the dispatched message', async () => { const actualId = await mailbox .connect(signer) - .callStatic.dispatch( + .callStatic['dispatch(uint32,bytes32,bytes)']( destDomain, utils.addressToBytes32(recipient.address), body, @@ -175,7 +167,7 @@ describe('Mailbox', async () => { it('Fails to process message when rejected by module', async () => { await module.setAccept(false); await expect(mailbox.process('0x', message)).to.be.revertedWith( - '!module', + 'verification failed', ); }); @@ -208,7 +200,7 @@ describe('Mailbox', async () => { )); await expect(mailbox.process('0x', message)).to.be.revertedWith( - '!destination', + 'Mailbox: unexpected destination', ); }); @@ -223,7 +215,7 @@ describe('Mailbox', async () => { version + 1, )); await expect(mailbox.process('0x', message)).to.be.revertedWith( - '!version', + 'Mailbox: bad version', ); }); @@ -261,42 +253,8 @@ describe('Mailbox', async () => { it('Reverts if the provided ISM is not a contract', async () => { await expect(mailbox.setDefaultIsm(signer.address)).to.be.revertedWith( - '!contract', - ); - }); - }); - - describe('#pause', () => { - it('should revert on non-owner', async () => { - await expect(mailbox.connect(nonOwner).pause()).to.be.revertedWith( - ONLY_OWNER_REVERT_MSG, - ); - await expect(mailbox.connect(nonOwner).unpause()).to.be.revertedWith( - ONLY_OWNER_REVERT_MSG, + 'Mailbox: !contract', ); }); - - it('should emit events', async () => { - await expect(mailbox.pause()).to.emit(mailbox, 'Paused'); - await expect(mailbox.unpause()).to.emit(mailbox, 'Unpaused'); - }); - - it('should prevent dispatch and process', async () => { - await mailbox.pause(); - await expect( - mailbox.dispatch( - destDomain, - utils.addressToBytes32(nonOwner.address), - '0x', - ), - ).to.be.revertedWith('paused'); - await expect(mailbox.process('0x', '0x')).to.be.revertedWith('paused'); - }); - - it('isPaused should be true', async () => { - await mailbox.pause(); - const paused = await mailbox.isPaused(); - expect(paused); - }); }); }); diff --git a/solidity/test/router.test.ts b/solidity/test/router.test.ts index a75fe2e0b2..7154e77110 100644 --- a/solidity/test/router.test.ts +++ b/solidity/test/router.test.ts @@ -11,6 +11,8 @@ import { TestInterchainGasPaymaster__factory, TestMailbox, TestMailbox__factory, + TestMerkleTreeHook, + TestMerkleTreeHook__factory, TestMultisigIsm__factory, TestRouter, TestRouter__factory, @@ -35,6 +37,7 @@ interface GasPaymentParams { describe('Router', async () => { let router: TestRouter, mailbox: TestMailbox, + defaultHook: TestMerkleTreeHook, igp: TestInterchainGasPaymaster, signer: SignerWithAddress, nonOwner: SignerWithAddress; @@ -45,7 +48,10 @@ describe('Router', async () => { beforeEach(async () => { const mailboxFactory = new TestMailbox__factory(signer); - mailbox = await mailboxFactory.deploy(origin); + mailbox = await mailboxFactory.deploy(origin, signer.address); + const defaultHookFactory = new TestMerkleTreeHook__factory(signer); + defaultHook = await defaultHookFactory.deploy(mailbox.address); + await mailbox.setDefaultHook(defaultHook.address); igp = await new TestInterchainGasPaymaster__factory(signer).deploy( signer.address, ); @@ -88,7 +94,7 @@ describe('Router', async () => { await router.initialize(mailbox.address, igp.address); const ism = await new TestMultisigIsm__factory(signer).deploy(); await ism.setAccept(true); - await mailbox.initialize(signer.address, ism.address); + await mailbox.setDefaultIsm(ism.address); }); it('accepts message from enrolled mailbox and router', async () => { From 73239547a7a9778863f60763a209d5cf6622ecbc Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Thu, 17 Aug 2023 15:01:54 -0400 Subject: [PATCH 16/27] Add `quoteDispatch` to `IPostDispatchHook` (#2660) ### Description - `quoteDispatch` added to `IPostDispatchHook` interface and the relevant hooks: - StaticProtocolFee - OPStackHook - InterchainGasPaymaster - MerkleTreeHook - PausableHook (no tests) - DomainRoutingHook (no tests) - ConfigFallbackDomainRoutingHook - ConfigurableDomainRoutingHook (no tests) ### Drive-by changes - `expectEmit` -> `expectCall` ### Related issues - Quote in V3 ### Backward compatibility Yes ### Testing Uint tests --- .../hooks/ConfigFallbackDomainRoutingHook.sol | 34 ++++++++++++---- .../contracts/hooks/DomainRoutingHook.sol | 22 +++++++++- solidity/contracts/hooks/ERC5164Hook.sol | 9 +++++ solidity/contracts/hooks/MerkleTreeHook.sol | 7 ++++ solidity/contracts/hooks/OPStackHook.sol | 12 ++++++ solidity/contracts/hooks/PausableHook.sol | 10 +++++ .../contracts/hooks/StaticProtocolFee.sol | 10 +++++ .../contracts/igps/InterchainGasPaymaster.sol | 40 ++++++++++++++----- .../interfaces/hooks/IPostDispatchHook.sol | 16 ++++++++ solidity/contracts/libs/hooks/IGPMetadata.sol | 15 +++++-- .../contracts/test/TestPostDispatchHook.sol | 12 ++++-- .../hooks/FallbackDomainRoutingHook.t.sol | 39 +++++++++++++++--- solidity/test/hooks/StaticProtocolFee.t.sol | 4 ++ .../test/igps/InterchainGasPaymaster.t.sol | 34 +++++++++++++++- solidity/test/isms/OPStackIsm.t.sol | 10 +++++ solidity/test/mailbox.test.ts | 2 +- 16 files changed, 242 insertions(+), 34 deletions(-) diff --git a/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol b/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol index 41dd3c1299..d61ae991d0 100644 --- a/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol +++ b/solidity/contracts/hooks/ConfigFallbackDomainRoutingHook.sol @@ -35,14 +35,18 @@ contract ConfigFallbackDomainRoutingHook is IPostDispatchHook { payable override { - IPostDispatchHook configuredHook = customHooks[message.senderAddress()][ - message.destination() - ][message.recipient()]; - if (address(configuredHook) == address(0)) { - configuredHook = mailbox.defaultHook(); - } + _getConfiguredHook(message).postDispatch{value: msg.value}( + metadata, + message + ); + } - configuredHook.postDispatch{value: msg.value}(metadata, message); + function quoteDispatch(bytes calldata metadata, bytes calldata message) + public + view + returns (uint256) + { + return _getConfiguredHook(message).quoteDispatch(metadata, message); } function setHook( @@ -52,4 +56,20 @@ contract ConfigFallbackDomainRoutingHook is IPostDispatchHook { ) external { customHooks[msg.sender][destinationDomain][recipient] = hook; } + + // ============ Internal Functions ============ + + function _getConfiguredHook(bytes calldata message) + internal + view + returns (IPostDispatchHook) + { + IPostDispatchHook configuredHook = customHooks[message.senderAddress()][ + message.destination() + ][message.recipient()]; + if (address(configuredHook) == address(0)) { + configuredHook = mailbox.defaultHook(); + } + return configuredHook; + } } diff --git a/solidity/contracts/hooks/DomainRoutingHook.sol b/solidity/contracts/hooks/DomainRoutingHook.sol index c74cebb735..54f558dc57 100644 --- a/solidity/contracts/hooks/DomainRoutingHook.sol +++ b/solidity/contracts/hooks/DomainRoutingHook.sol @@ -38,9 +38,29 @@ contract DomainRoutingHook is IPostDispatchHook, Ownable { virtual override { - hooks[message.destination()].postDispatch{value: msg.value}( + _getConfiguredHook(message).postDispatch{value: msg.value}( metadata, message ); } + + function quoteDispatch(bytes calldata metadata, bytes calldata message) + public + view + virtual + override + returns (uint256) + { + return _getConfiguredHook(message).quoteDispatch(metadata, message); + } + + // ============ Internal Functions ============ + + function _getConfiguredHook(bytes calldata message) + internal + view + returns (IPostDispatchHook) + { + return hooks[message.destination()]; + } } diff --git a/solidity/contracts/hooks/ERC5164Hook.sol b/solidity/contracts/hooks/ERC5164Hook.sol index b401140d2b..f853487661 100644 --- a/solidity/contracts/hooks/ERC5164Hook.sol +++ b/solidity/contracts/hooks/ERC5164Hook.sol @@ -43,6 +43,15 @@ contract ERC5164Hook is AbstractMessageIdAuthHook { dispatcher = IMessageDispatcher(_dispatcher); } + function quoteDispatch(bytes calldata, bytes calldata) + external + pure + override + returns (uint256) + { + revert("not implemented"); + } + function _sendMessageId( bytes calldata, /* metadata */ bytes memory payload diff --git a/solidity/contracts/hooks/MerkleTreeHook.sol b/solidity/contracts/hooks/MerkleTreeHook.sol index c4c61f3850..bd301d1a4c 100644 --- a/solidity/contracts/hooks/MerkleTreeHook.sol +++ b/solidity/contracts/hooks/MerkleTreeHook.sol @@ -43,4 +43,11 @@ contract MerkleTreeHook is IPostDispatchHook, MailboxClient { require(isLatestDispatched(id), "message not dispatching"); _tree.insert(id); } + + function quoteDispatch( + bytes calldata, /*metadata*/ + bytes calldata /*message*/ + ) external pure override returns (uint256) { + return 0; + } } diff --git a/solidity/contracts/hooks/OPStackHook.sol b/solidity/contracts/hooks/OPStackHook.sol index 7b4c036bc1..4c8b82fa5e 100644 --- a/solidity/contracts/hooks/OPStackHook.sol +++ b/solidity/contracts/hooks/OPStackHook.sol @@ -57,6 +57,18 @@ contract OPStackHook is AbstractMessageIdAuthHook { l1Messenger = ICrossDomainMessenger(_messenger); } + // ============ External functions ============ + + /// @inheritdoc IPostDispatchHook + function quoteDispatch(bytes calldata, bytes calldata) + external + pure + override + returns (uint256) + { + return 0; // gas subsidized by the L2 + } + // ============ Internal functions ============ /// @inheritdoc AbstractMessageIdAuthHook diff --git a/solidity/contracts/hooks/PausableHook.sol b/solidity/contracts/hooks/PausableHook.sol index 99835fa488..c28ad46d50 100644 --- a/solidity/contracts/hooks/PausableHook.sol +++ b/solidity/contracts/hooks/PausableHook.sol @@ -13,6 +13,16 @@ contract PausableHook is IPostDispatchHook, Ownable, Pausable { whenNotPaused {} + /// @inheritdoc IPostDispatchHook + function quoteDispatch(bytes calldata, bytes calldata) + external + pure + override + returns (uint256) + { + return 0; + } + function pause() external onlyOwner { _pause(); } diff --git a/solidity/contracts/hooks/StaticProtocolFee.sol b/solidity/contracts/hooks/StaticProtocolFee.sol index deb3a75afc..75b5b0d2fb 100644 --- a/solidity/contracts/hooks/StaticProtocolFee.sol +++ b/solidity/contracts/hooks/StaticProtocolFee.sol @@ -74,6 +74,16 @@ contract StaticProtocolFee is IPostDispatchHook, Ownable { if (refund > 0) payable(message.senderAddress()).sendValue(refund); } + /// @inheritdoc IPostDispatchHook + function quoteDispatch(bytes calldata, bytes calldata) + external + view + override + returns (uint256) + { + return protocolFee; + } + /** * @notice Sets the protocol fee. * @param _protocolFee The new protocol fee. diff --git a/solidity/contracts/igps/InterchainGasPaymaster.sol b/solidity/contracts/igps/InterchainGasPaymaster.sol index e951f4ba87..97778b7e5b 100644 --- a/solidity/contracts/igps/InterchainGasPaymaster.sol +++ b/solidity/contracts/igps/InterchainGasPaymaster.sol @@ -1,6 +1,18 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + // ============ Internal Imports ============ import {Message} from "../libs/Message.sol"; import {IGPMetadata} from "../libs/hooks/IGPMetadata.sol"; @@ -85,20 +97,26 @@ contract InterchainGasPaymaster is payable override { - uint256 gasLimit; - address refundAddress; - if (metadata.length == 0) { - gasLimit = DEFAULT_GAS_USAGE; - refundAddress = message.senderAddress(); - } else { - gasLimit = metadata.gasLimit(); - refundAddress = metadata.refundAddress(); - if (refundAddress == address(0)) - refundAddress = message.senderAddress(); - } + uint256 gasLimit = metadata.gasLimit(DEFAULT_GAS_USAGE); + address refundAddress = metadata.refundAddress(message.senderAddress()); payForGas(message.id(), message.destination(), gasLimit, refundAddress); } + /** + * @notice Quote gas payment for a hook call. + * @param metadata The metadata as gasConfig. + * @param message The message to pay for. + */ + function quoteDispatch(bytes calldata metadata, bytes calldata message) + external + view + override + returns (uint256) + { + uint256 gasLimit = metadata.gasLimit(DEFAULT_GAS_USAGE); + return quoteGasPayment(message.destination(), gasLimit); + } + /** * @notice Transfers the entire native token balance to the beneficiary. * @dev The beneficiary must be able to receive native tokens. diff --git a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol index 737268a486..7b86e98920 100644 --- a/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol +++ b/solidity/contracts/interfaces/hooks/IPostDispatchHook.sol @@ -2,7 +2,23 @@ pragma solidity >=0.8.0; interface IPostDispatchHook { + /** + * @notice Post action afte a message is dispatched via the Mailbox + * @param metadata The metadata required for the hook + * @param message The message passed from the Mailbox.dispatch() call + */ function postDispatch(bytes calldata metadata, bytes calldata message) external payable; + + /** + * @notice Estimate the amount of gas consumed by the postDispatch call + * @param metadata The metadata required for the hook + * @param message The message passed from the Mailbox.dispatch() call + * @return Gas quote for the postDispatch call + */ + function quoteDispatch(bytes calldata metadata, bytes calldata message) + external + view + returns (uint256); } diff --git a/solidity/contracts/libs/hooks/IGPMetadata.sol b/solidity/contracts/libs/hooks/IGPMetadata.sol index 0bac899f94..24ad99e940 100644 --- a/solidity/contracts/libs/hooks/IGPMetadata.sol +++ b/solidity/contracts/libs/hooks/IGPMetadata.sol @@ -28,11 +28,12 @@ library IGPMetadata { * @param _metadata ABI encoded IGP hook metadata. * @return Gas limit for the message as uint256. */ - function gasLimit(bytes calldata _metadata) + function gasLimit(bytes calldata _metadata, uint256 _default) internal pure returns (uint256) { + if (_metadata.length < GAS_LIMIT_OFFSET + 32) return _default; return uint256(bytes32(_metadata[GAS_LIMIT_OFFSET:GAS_LIMIT_OFFSET + 32])); } @@ -42,17 +43,23 @@ library IGPMetadata { * @param _metadata ABI encoded IGP hook metadata. * @return Refund address for the message as address. */ - function refundAddress(bytes calldata _metadata) + function refundAddress(bytes calldata _metadata, address _default) internal pure returns (address) { - return - address( + address _refundAddress; + if (_metadata.length < REFUND_ADDRESS_OFFSET + 20) { + _refundAddress = _default; + } else { + _refundAddress = address( bytes20( _metadata[REFUND_ADDRESS_OFFSET:REFUND_ADDRESS_OFFSET + 20] ) ); + if (_refundAddress == address(0)) _refundAddress = _default; + } + return _refundAddress; } /** diff --git a/solidity/contracts/test/TestPostDispatchHook.sol b/solidity/contracts/test/TestPostDispatchHook.sol index 82febe3ccc..019cd8abf7 100644 --- a/solidity/contracts/test/TestPostDispatchHook.sol +++ b/solidity/contracts/test/TestPostDispatchHook.sol @@ -4,13 +4,19 @@ pragma solidity >=0.8.0; import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; contract TestPostDispatchHook is IPostDispatchHook { - event PostDispatchHookCalled(); + uint256 public mockGasQuote = 25000; function postDispatch( bytes calldata, /*metadata*/ bytes calldata /*message*/ ) external payable override { - // test - emit event - emit PostDispatchHookCalled(); + // test - empty + } + + function quoteDispatch( + bytes calldata, /*metadata*/ + bytes calldata /*message*/ + ) external view override returns (uint256) { + return mockGasQuote; } } diff --git a/solidity/test/hooks/FallbackDomainRoutingHook.t.sol b/solidity/test/hooks/FallbackDomainRoutingHook.t.sol index 1e87e65180..c1a1301ac4 100644 --- a/solidity/test/hooks/FallbackDomainRoutingHook.t.sol +++ b/solidity/test/hooks/FallbackDomainRoutingHook.t.sol @@ -34,22 +34,51 @@ contract FallbackDomainRoutingHookTest is Test { mailbox.setDefaultHook(address(mailboxDefaultHook)); } - function test_postDispatchHook_configured() public payable { + /* ============ hook.quoteDispatch ============ */ + + function test_quoteDispatchHook_configured() public { fallbackHook.setHook( TEST_DESTINATION_DOMAIN, address(testRecipient).addressToBytes32(), configuredTestHook ); - vm.expectEmit(false, false, false, false, address(configuredTestHook)); - emit PostDispatchHookCalled(); + vm.expectCall( + address(configuredTestHook), + abi.encodeCall(configuredTestHook.quoteDispatch, ("", testMessage)) + ); + assertEq(fallbackHook.quoteDispatch("", testMessage), 25000); + } + function test_quoteDispatch_default() public payable { + vm.expectCall( + address(mailboxDefaultHook), + abi.encodeCall(mailboxDefaultHook.quoteDispatch, ("", testMessage)) + ); + fallbackHook.quoteDispatch("", testMessage); + } + + /* ============ hook.postDispatch ============ */ + + function test_postDispatchHook_configured() public payable { + fallbackHook.setHook( + TEST_DESTINATION_DOMAIN, + address(testRecipient).addressToBytes32(), + configuredTestHook + ); + + vm.expectCall( + address(configuredTestHook), + abi.encodeCall(configuredTestHook.postDispatch, ("", testMessage)) + ); fallbackHook.postDispatch{value: msg.value}("", testMessage); } function test_postDispatch_default() public payable { - vm.expectEmit(false, false, false, false, address(mailboxDefaultHook)); - emit PostDispatchHookCalled(); + vm.expectCall( + address(mailboxDefaultHook), + abi.encodeCall(mailboxDefaultHook.postDispatch, ("", testMessage)) + ); fallbackHook.postDispatch{value: msg.value}("", testMessage); } diff --git a/solidity/test/hooks/StaticProtocolFee.t.sol b/solidity/test/hooks/StaticProtocolFee.t.sol index 6428bf1728..25d9347153 100644 --- a/solidity/test/hooks/StaticProtocolFee.t.sol +++ b/solidity/test/hooks/StaticProtocolFee.t.sol @@ -67,6 +67,10 @@ contract StaticProtocolFeeTest is Test { assertEq(fees.beneficiary(), bob); } + function testQuoteDispatch() public { + assertEq(fees.quoteDispatch("", testMessage), 1e15); + } + function testFuzz_postDispatch_inusfficientFees( uint256 feeRequired, uint256 feeSent diff --git a/solidity/test/igps/InterchainGasPaymaster.t.sol b/solidity/test/igps/InterchainGasPaymaster.t.sol index 7b9a8c4ffa..536edd291f 100644 --- a/solidity/test/igps/InterchainGasPaymaster.t.sol +++ b/solidity/test/igps/InterchainGasPaymaster.t.sol @@ -28,6 +28,7 @@ contract InterchainGasPaymasterTest is Test { bytes32 constant testMessageId = 0x6ae9a99190641b9ed0c07143340612dde0e9cb7deaa5fe07597858ae9ba5fd7f; address constant testRefundAddress = address(0xc0ffee); + bytes testEncodedMessage; event GasPayment( bytes32 indexed messageId, @@ -44,6 +45,8 @@ contract InterchainGasPaymasterTest is Test { igp.initialize(address(this), beneficiary); oracle = new StorageGasOracle(); setGasOracle(testDestinationDomain, address(oracle)); + + testEncodedMessage = _encodeTestMessage(); } // ============ constructor ============ @@ -59,6 +62,34 @@ contract InterchainGasPaymasterTest is Test { igp.initialize(address(this), beneficiary); } + // ============ quoteDispatch ============ + + function testQuoteDispatch_defaultGasLimit() public { + setRemoteGasData( + testDestinationDomain, + 1 * 1e10, // 1.0 exchange rate (remote token has exact same value as local) + 150 // 1 wei gas price + ); + + // 150 * 69_420 = 10_413_000 + assertEq(igp.quoteDispatch("", testEncodedMessage), 10_413_000); + } + + function testQuoteDispatch_customWithMetadata() public { + setRemoteGasData( + testDestinationDomain, + 1 * 1e10, // 1.0 exchange rate (remote token has exact same value as local) + 150 // 1 wei gas price + ); + + bytes memory metadata = IGPMetadata.formatMetadata( + uint256(testGasAmount), // gas limit + testRefundAddress // refund address + ); + // 150 * 300_000 = 45_000_000 + assertEq(igp.quoteDispatch(metadata, testEncodedMessage), 45_000_000); + } + // ============ postDispatch ============ function testPostDispatch_defaultGasLimit() public { @@ -73,9 +104,8 @@ contract InterchainGasPaymasterTest is Test { uint256 _quote = igp.quoteGasPayment(testDestinationDomain, 69_420); uint256 _overpayment = 21000; - bytes memory message = _encodeTestMessage(); - igp.postDispatch{value: _quote + _overpayment}("", message); + igp.postDispatch{value: _quote + _overpayment}("", testEncodedMessage); uint256 _igpBalanceAfter = address(igp).balance; uint256 _refundAddressBalanceAfter = address(this).balance; diff --git a/solidity/test/isms/OPStackIsm.t.sol b/solidity/test/isms/OPStackIsm.t.sol index 882599c94e..dae29a6516 100644 --- a/solidity/test/isms/OPStackIsm.t.sol +++ b/solidity/test/isms/OPStackIsm.t.sol @@ -132,6 +132,16 @@ contract OPStackIsmTest is Test { /// FORK TESTS /// /////////////////////////////////////////////////////////////////// + /* ============ hook.quoteDispatch ============ */ + + function testFork_quoteDispatch() public { + deployAll(); + + vm.selectFork(mainnetFork); + + assertEq(opHook.quoteDispatch(testMetadata, encodedMessage), 0); + } + /* ============ hook.postDispatch ============ */ function testFork_postDispatch() public { diff --git a/solidity/test/mailbox.test.ts b/solidity/test/mailbox.test.ts index 9d31a74063..273943ccc0 100644 --- a/solidity/test/mailbox.test.ts +++ b/solidity/test/mailbox.test.ts @@ -79,7 +79,7 @@ describe('Mailbox', async () => { ['dispatch(uint32,bytes32,bytes)'](destDomain, recipientBytes, body), ) .to.emit(mailbox, 'Dispatch') - .withArgs(signer.address, destDomain, recipientBytes, message) + .withArgs(message) .to.emit(mailbox, 'DispatchId') .withArgs(utils.messageId(message)); }); From a77613150096c5581c30c224c14f392a1445edf5 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Fri, 18 Aug 2023 12:29:59 -0400 Subject: [PATCH 17/27] Add deployed block numbers to indexable contracts (#2672) Fixes #2004 --- solidity/contracts/Indexed.sol | 10 +++ solidity/contracts/Mailbox.sol | 3 +- .../contracts/PausableReentrancyGuard.sol | 69 ------------------- solidity/contracts/hooks/MerkleTreeHook.sol | 3 +- .../contracts/igps/InterchainGasPaymaster.sol | 3 + solidity/test/PausableReentrancyGuard.t.sol | 60 ---------------- .../test/igps/InterchainGasPaymaster.t.sol | 7 ++ solidity/test/mailbox.test.ts | 15 +++- 8 files changed, 38 insertions(+), 132 deletions(-) create mode 100644 solidity/contracts/Indexed.sol delete mode 100644 solidity/contracts/PausableReentrancyGuard.sol delete mode 100644 solidity/test/PausableReentrancyGuard.t.sol diff --git a/solidity/contracts/Indexed.sol b/solidity/contracts/Indexed.sol new file mode 100644 index 0000000000..25a017abf7 --- /dev/null +++ b/solidity/contracts/Indexed.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +contract Indexed { + uint256 public immutable deployedBlock; + + constructor() { + deployedBlock = block.number; + } +} diff --git a/solidity/contracts/Mailbox.sol b/solidity/contracts/Mailbox.sol index 69ce9b093f..a02ced561d 100644 --- a/solidity/contracts/Mailbox.sol +++ b/solidity/contracts/Mailbox.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.0; // ============ Internal Imports ============ import {Versioned} from "./upgrade/Versioned.sol"; +import {Indexed} from "./Indexed.sol"; import {Message} from "./libs/Message.sol"; import {TypeCasts} from "./libs/TypeCasts.sol"; import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "./interfaces/IInterchainSecurityModule.sol"; @@ -14,7 +15,7 @@ import {IMailbox} from "./interfaces/IMailbox.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -contract Mailbox is IMailbox, Versioned, Ownable { +contract Mailbox is IMailbox, Indexed, Versioned, Ownable { // ============ Libraries ============ using Message for bytes; diff --git a/solidity/contracts/PausableReentrancyGuard.sol b/solidity/contracts/PausableReentrancyGuard.sol deleted file mode 100644 index a079f47752..0000000000 --- a/solidity/contracts/PausableReentrancyGuard.sol +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; - -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - -// adapted from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -abstract contract PausableReentrancyGuardUpgradeable is Initializable { - uint256 private constant _ENTERED = 0; - uint256 private constant _NOT_ENTERED = 1; - uint256 private constant _PAUSED = 2; - - uint256 private _status; - - /** - * @dev MUST be called for `nonReentrant` to not always revert - */ - function __PausableReentrancyGuard_init() internal onlyInitializing { - _status = _NOT_ENTERED; - } - - function _isPaused() internal view returns (bool) { - return _status == _PAUSED; - } - - function _pause() internal notPaused { - _status = _PAUSED; - } - - function _unpause() internal { - require(_isPaused(), "!paused"); - _status = _NOT_ENTERED; - } - - /** - * @dev Prevents a contract from being entered when paused. - */ - modifier notPaused() { - require(!_isPaused(), "paused"); - _; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and making it call a - * `private` function that does the actual work. - */ - modifier nonReentrantAndNotPaused() { - // status must have been initialized - require(_status == _NOT_ENTERED, "reentrant call (or paused)"); - - // Any calls to nonReentrant after this point will fail - _status = _ENTERED; - - _; - - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _status = _NOT_ENTERED; - } - - /** - * @dev This empty reserved space is put in place to allow future versions to add new - * variables without shifting down storage in the inheritance chain. - * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps - */ - uint256[49] private __gap; -} diff --git a/solidity/contracts/hooks/MerkleTreeHook.sol b/solidity/contracts/hooks/MerkleTreeHook.sol index bd301d1a4c..b83df831de 100644 --- a/solidity/contracts/hooks/MerkleTreeHook.sol +++ b/solidity/contracts/hooks/MerkleTreeHook.sol @@ -4,9 +4,10 @@ pragma solidity >=0.8.0; import {MerkleLib, TREE_DEPTH} from "../libs/Merkle.sol"; import {Message} from "../libs/Message.sol"; import {MailboxClient} from "../client/MailboxClient.sol"; +import {Indexed} from "../Indexed.sol"; import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; -contract MerkleTreeHook is IPostDispatchHook, MailboxClient { +contract MerkleTreeHook is IPostDispatchHook, MailboxClient, Indexed { using Message for bytes; using MerkleLib for MerkleLib.Tree; diff --git a/solidity/contracts/igps/InterchainGasPaymaster.sol b/solidity/contracts/igps/InterchainGasPaymaster.sol index 97778b7e5b..81c19a293a 100644 --- a/solidity/contracts/igps/InterchainGasPaymaster.sol +++ b/solidity/contracts/igps/InterchainGasPaymaster.sol @@ -19,6 +19,8 @@ import {IGPMetadata} from "../libs/hooks/IGPMetadata.sol"; import {IGasOracle} from "../interfaces/IGasOracle.sol"; import {IInterchainGasPaymaster} from "../interfaces/IInterchainGasPaymaster.sol"; import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; +import {Indexed} from "../Indexed.sol"; + // ============ External Imports ============ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -32,6 +34,7 @@ contract InterchainGasPaymaster is IInterchainGasPaymaster, IPostDispatchHook, IGasOracle, + Indexed, OwnableUpgradeable { using Address for address payable; diff --git a/solidity/test/PausableReentrancyGuard.t.sol b/solidity/test/PausableReentrancyGuard.t.sol deleted file mode 100644 index add79ac63f..0000000000 --- a/solidity/test/PausableReentrancyGuard.t.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import {PausableReentrancyGuardUpgradeable} from "../contracts/PausableReentrancyGuard.sol"; - -contract MockPausableReentrancyGuard is PausableReentrancyGuardUpgradeable { - constructor() initializer { - __PausableReentrancyGuard_init(); - } - - function pause() external { - _pause(); - } - - function unpause() external { - _unpause(); - } - - function isPaused() external view returns (bool) { - return _isPaused(); - } - - function f1() public nonReentrantAndNotPaused {} - - function f2() external nonReentrantAndNotPaused { - f1(); - } - - function f3() external notPaused {} -} - -contract PausableReentrancyGuardTest is Test { - MockPausableReentrancyGuard mprg; - - function setUp() public { - mprg = new MockPausableReentrancyGuard(); - } - - function testPause() public { - mprg.f3(); - mprg.pause(); - vm.expectRevert("paused"); - mprg.f3(); - mprg.unpause(); - mprg.f3(); - } - - function testNonreentrant() public { - mprg.f1(); - vm.expectRevert("reentrant call (or paused)"); - mprg.f2(); - } - - function testNonreentrantNotPaused() public { - mprg.pause(); - vm.expectRevert("reentrant call (or paused)"); - mprg.f1(); - } -} diff --git a/solidity/test/igps/InterchainGasPaymaster.t.sol b/solidity/test/igps/InterchainGasPaymaster.t.sol index 536edd291f..cbcf80ca28 100644 --- a/solidity/test/igps/InterchainGasPaymaster.t.sol +++ b/solidity/test/igps/InterchainGasPaymaster.t.sol @@ -30,6 +30,8 @@ contract InterchainGasPaymasterTest is Test { address constant testRefundAddress = address(0xc0ffee); bytes testEncodedMessage; + uint256 blockNumber; + event GasPayment( bytes32 indexed messageId, uint256 gasAmount, @@ -41,6 +43,7 @@ contract InterchainGasPaymasterTest is Test { event BeneficiarySet(address beneficiary); function setUp() public { + blockNumber = block.number; igp = new InterchainGasPaymaster(); igp.initialize(address(this), beneficiary); oracle = new StorageGasOracle(); @@ -55,6 +58,10 @@ contract InterchainGasPaymasterTest is Test { assertEq(igp.beneficiary(), beneficiary); } + function testConstructorSetsDeployedBlock() public { + assertEq(igp.deployedBlock(), blockNumber); + } + // ============ initialize ============ function testInitializeRevertsIfCalledTwice() public { diff --git a/solidity/test/mailbox.test.ts b/solidity/test/mailbox.test.ts index 273943ccc0..e1ace94c4f 100644 --- a/solidity/test/mailbox.test.ts +++ b/solidity/test/mailbox.test.ts @@ -1,5 +1,6 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; +import { BigNumber } from 'ethers'; import { ethers } from 'hardhat'; import { utils } from '@hyperlane-xyz/utils'; @@ -31,7 +32,8 @@ describe('Mailbox', async () => { defaultHook: TestMerkleTreeHook, module: TestMultisigIsm, signer: SignerWithAddress, - nonOwner: SignerWithAddress; + nonOwner: SignerWithAddress, + beforeBlock: number; beforeEach(async () => { [signer, nonOwner] = await ethers.getSigners(); @@ -39,12 +41,23 @@ describe('Mailbox', async () => { module = await moduleFactory.deploy(); const mailboxFactory = new TestMailbox__factory(signer); mailbox = await mailboxFactory.deploy(originDomain, signer.address); + beforeBlock = mailbox.deployTransaction.blockNumber!; const defaultHookFactory = new TestMerkleTreeHook__factory(signer); defaultHook = await defaultHookFactory.deploy(mailbox.address); await mailbox.setDefaultIsm(module.address); await mailbox.setDefaultHook(defaultHook.address); }); + it('#deployedBlock', async () => { + const block = await mailbox.deployedBlock(); + expect(block).to.equal(beforeBlock); + }); + + it('#VERSION', async () => { + const version = await mailbox.VERSION(); + expect(version).to.equal(3); + }); + describe('#initialize', () => { it('Sets the owner', async () => { const mailboxFactory = new TestMailbox__factory(signer); From 0c3f1ed4f6bee58272c4f0e1525b190bff0902f0 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Wed, 30 Aug 2023 08:47:51 -0700 Subject: [PATCH 18/27] Add aggregation hook for V3 (#2667) ### Description - AggregationHook for v3 ### Drive-by changes None ### Related issues v3 https://github.com/hyperlane-xyz/issues/issues/514 ### Backward compatibility Yes ### Testing Always be fuzzing --- .../aggregation/StaticAggregationHook.sol | 61 +++++++++++++ .../StaticAggregationHookFactory.sol | 16 ++++ .../libs/StaticNAddressSetFactory.sol | 86 +++++++++++++++++++ solidity/test/hooks/AggregationHook.t.sol | 81 +++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 solidity/contracts/hooks/aggregation/StaticAggregationHook.sol create mode 100644 solidity/contracts/hooks/aggregation/StaticAggregationHookFactory.sol create mode 100644 solidity/contracts/libs/StaticNAddressSetFactory.sol create mode 100644 solidity/test/hooks/AggregationHook.t.sol diff --git a/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol b/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol new file mode 100644 index 0000000000..f90d2b4c0f --- /dev/null +++ b/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol"; +import {MetaProxy} from "../../libs/MetaProxy.sol"; + +contract StaticAggregationHook is IPostDispatchHook { + function postDispatch(bytes calldata metadata, bytes calldata message) + external + payable + override + { + address[] memory _hooks = hooks(message); + uint256 count = _hooks.length; + for (uint256 i = 0; i < count; i++) { + uint256 quote = IPostDispatchHook(_hooks[i]).quoteDispatch( + metadata, + message + ); + + IPostDispatchHook(_hooks[i]).postDispatch{value: quote}( + metadata, + message + ); + } + } + + function quoteDispatch(bytes calldata metadata, bytes calldata message) + external + view + override + returns (uint256) + { + address[] memory _hooks = hooks(message); + uint256 count = _hooks.length; + uint256 total = 0; + for (uint256 i = 0; i < count; i++) { + total += IPostDispatchHook(_hooks[i]).quoteDispatch( + metadata, + message + ); + } + return total; + } + + function hooks(bytes calldata) public pure returns (address[] memory) { + return abi.decode(MetaProxy.metadata(), (address[])); + } +} diff --git a/solidity/contracts/hooks/aggregation/StaticAggregationHookFactory.sol b/solidity/contracts/hooks/aggregation/StaticAggregationHookFactory.sol new file mode 100644 index 0000000000..5cb694c662 --- /dev/null +++ b/solidity/contracts/hooks/aggregation/StaticAggregationHookFactory.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; +// ============ Internal Imports ============ +import {StaticAggregationHook} from "./StaticAggregationHook.sol"; +import {StaticNAddressSetFactory} from "../../libs/StaticNAddressSetFactory.sol"; + +contract StaticAggregationHookFactory is StaticNAddressSetFactory { + function _deployImplementation() + internal + virtual + override + returns (address) + { + return address(new StaticAggregationHook()); + } +} diff --git a/solidity/contracts/libs/StaticNAddressSetFactory.sol b/solidity/contracts/libs/StaticNAddressSetFactory.sol new file mode 100644 index 0000000000..14cb94ac4a --- /dev/null +++ b/solidity/contracts/libs/StaticNAddressSetFactory.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; +// ============ External Imports ============ +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +// ============ Internal Imports ============ +import {MetaProxy} from "./MetaProxy.sol"; + +abstract contract StaticNAddressSetFactory { + // ============ Immutables ============ + address private immutable _implementation; + + // ============ Constructor ============ + + constructor() { + _implementation = _deployImplementation(); + } + + function _deployImplementation() internal virtual returns (address); + + /** + * @notice Deploys a StaticNAddressSet contract address for the given + * values + * @dev Consider sorting addresses to ensure contract reuse + * @param _values An array of addresses + * @return set The contract address representing this StaticNAddressSet + */ + function deploy(address[] calldata _values) external returns (address) { + (bytes32 _salt, bytes memory _bytecode) = _saltAndBytecode(_values); + address _set = _getAddress(_salt, _bytecode); + if (!Address.isContract(_set)) { + _set = Create2.deploy(0, _salt, _bytecode); + } + return _set; + } + + /** + * @notice Returns the StaticNAddressSet contract address for the given + * values + * @dev Consider sorting addresses to ensure contract reuse + * @param _values An array of addresses + * @return set The contract address representing this StaticNAddressSet + */ + function getAddress(address[] calldata _values) + external + view + returns (address) + { + (bytes32 _salt, bytes memory _bytecode) = _saltAndBytecode(_values); + return _getAddress(_salt, _bytecode); + } + + /** + * @notice Returns the StaticNAddressSet contract address for the given + * values + * @param _salt The salt used in Create2 + * @param _bytecode The metaproxy bytecode used in Create2 + * @return set The contract address representing this StaticNAddressSet + */ + function _getAddress(bytes32 _salt, bytes memory _bytecode) + private + view + returns (address) + { + bytes32 _bytecodeHash = keccak256(_bytecode); + return Create2.computeAddress(_salt, _bytecodeHash); + } + + /** + * @notice Returns the create2 salt and bytecode for the given values + * @param _values An array of addresses + * @return _salt The salt used in Create2 + * @return _bytecode The metaproxy bytecode used in Create2 + */ + function _saltAndBytecode(address[] calldata _values) + private + view + returns (bytes32, bytes memory) + { + bytes memory _metadata = abi.encode(_values); + bytes memory _bytecode = MetaProxy.bytecode(_implementation, _metadata); + bytes32 _salt = keccak256(_metadata); + return (_salt, _bytecode); + } +} diff --git a/solidity/test/hooks/AggregationHook.t.sol b/solidity/test/hooks/AggregationHook.t.sol new file mode 100644 index 0000000000..c9afc8100e --- /dev/null +++ b/solidity/test/hooks/AggregationHook.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; + +import {StaticAggregationHook} from "../../contracts/hooks/aggregation/StaticAggregationHook.sol"; +import {StaticAggregationHookFactory} from "../../contracts/hooks/aggregation/StaticAggregationHookFactory.sol"; +import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; + +contract AggregationHookTest is Test { + StaticAggregationHookFactory internal factory; + StaticAggregationHook internal hook; + + function setUp() public { + factory = new StaticAggregationHookFactory(); + } + + function deployHooks(uint8 n) internal returns (address[] memory) { + address[] memory hooks = new address[](n); + for (uint8 i = 0; i < n; i++) { + TestPostDispatchHook subHook = new TestPostDispatchHook(); + hooks[i] = address(subHook); + } + hook = StaticAggregationHook(factory.deploy(hooks)); + return hooks; + } + + function testPostDispatch(uint8 _hooks) public { + address[] memory hooksDeployed = deployHooks(_hooks); + uint256 _msgValue = hooksDeployed.length * 25000; + + bytes memory message = abi.encodePacked("hello world"); + for (uint256 i = 0; i < hooksDeployed.length; i++) { + vm.expectCall( + hooksDeployed[i], + 25000, + abi.encodeCall( + TestPostDispatchHook(hooksDeployed[i]).postDispatch, + ("", "hello world") + ) + ); + } + hook.postDispatch{value: _msgValue}("", message); + } + + function testPostDispatch_reverts_outOfFund(uint8 _hooks, uint8 k) public { + address[] memory hooksDeployed = deployHooks(_hooks); + vm.assume(k < hooksDeployed.length); + uint256 _msgValue = uint256(k) * 25000; + + bytes memory message = abi.encodePacked("hello world"); + for (uint256 i = 0; i < k; i++) { + vm.expectCall( + hooksDeployed[i], + 25000, + abi.encodeCall( + TestPostDispatchHook(hooksDeployed[i]).postDispatch, + ("", "hello world") + ) + ); + } + vm.expectRevert(); // outOfFund + hook.postDispatch{value: _msgValue}("", message); + } + + function testQuoteDispatch(uint8 _hooks) public { + address[] memory hooksDeployed = deployHooks(_hooks); + uint256 _msgValue = hooksDeployed.length * 25000; + + bytes memory message = abi.encodePacked("hello world"); + uint256 totalQuote = hook.quoteDispatch("", message); + + assertEq(totalQuote, _msgValue); + } + + function testMetadata(uint8 _hooks) public { + address[] memory expectedHooks = deployHooks(_hooks); + address[] memory actualHook = hook.hooks(""); + assertEq(actualHook, expectedHooks); + } +} From 27cd6c96b5a96e600fce1989dbf1654a36c48bef Mon Sep 17 00:00:00 2001 From: Kunal Arora Date: Sun, 3 Sep 2023 15:28:30 -0400 Subject: [PATCH 19/27] add to event and struct --- rust/Cargo.lock | 13 ------------- rust/hyperlane-core/src/types/mod.rs | 7 +++++++ solidity/contracts/igps/InterchainGasPaymaster.sol | 7 ++++++- .../interfaces/IInterchainGasPaymaster.sol | 2 ++ solidity/test/igps/InterchainGasPaymaster.t.sol | 8 +++++++- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d24082f19a..f5316c94a6 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -9193,13 +9193,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" dependencies = [ "base64 0.21.2", - "flate2", "log", "once_cell", - "rustls 0.21.2", - "rustls-webpki", "url", - "webpki-roots 0.23.1", ] [[package]] @@ -9505,15 +9501,6 @@ dependencies = [ "webpki 0.22.0", ] -[[package]] -name = "webpki-roots" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" -dependencies = [ - "rustls-webpki", -] - [[package]] name = "which" version = "4.4.0" diff --git a/rust/hyperlane-core/src/types/mod.rs b/rust/hyperlane-core/src/types/mod.rs index b502a4b50b..5d58ff22dc 100644 --- a/rust/hyperlane-core/src/types/mod.rs +++ b/rust/hyperlane-core/src/types/mod.rs @@ -108,6 +108,8 @@ impl From for ethers_core::types::Signature { pub struct InterchainGasPayment { /// Id of the message pub message_id: H256, + /// Destination domain paid for. + pub destination: u32, /// Amount of native tokens paid. pub payment: U256, /// Amount of destination gas paid for. @@ -133,8 +135,13 @@ impl Add for InterchainGasPayment { self.message_id, rhs.message_id, "Cannot add interchain gas payments for different messages" ); + assert_eq!( + self.destination, rhs.destination, + "Cannot add interchain gas payments for different destinations" + ); Self { message_id: self.message_id, + destination: self.destination, payment: self.payment + rhs.payment, gas_amount: self.gas_amount + rhs.gas_amount, } diff --git a/solidity/contracts/igps/InterchainGasPaymaster.sol b/solidity/contracts/igps/InterchainGasPaymaster.sol index 81c19a293a..18e92af06a 100644 --- a/solidity/contracts/igps/InterchainGasPaymaster.sol +++ b/solidity/contracts/igps/InterchainGasPaymaster.sol @@ -183,7 +183,12 @@ contract InterchainGasPaymaster is payable(_refundAddress).sendValue(_overpayment); } - emit GasPayment(_messageId, _gasAmount, _requiredPayment); + emit GasPayment( + _messageId, + _destinationDomain, + _gasAmount, + _requiredPayment + ); } /** diff --git a/solidity/contracts/interfaces/IInterchainGasPaymaster.sol b/solidity/contracts/interfaces/IInterchainGasPaymaster.sol index aa34fda5f5..a58e069fa2 100644 --- a/solidity/contracts/interfaces/IInterchainGasPaymaster.sol +++ b/solidity/contracts/interfaces/IInterchainGasPaymaster.sol @@ -10,11 +10,13 @@ interface IInterchainGasPaymaster { /** * @notice Emitted when a payment is made for a message's gas costs. * @param messageId The ID of the message to pay for. + * @param destinationDomain The domain of the destination chain. * @param gasAmount The amount of destination gas paid for. * @param payment The amount of native tokens paid. */ event GasPayment( bytes32 indexed messageId, + uint32 indexed destinationDomain, uint256 gasAmount, uint256 payment ); diff --git a/solidity/test/igps/InterchainGasPaymaster.t.sol b/solidity/test/igps/InterchainGasPaymaster.t.sol index cbcf80ca28..4b0594fba5 100644 --- a/solidity/test/igps/InterchainGasPaymaster.t.sol +++ b/solidity/test/igps/InterchainGasPaymaster.t.sol @@ -34,6 +34,7 @@ contract InterchainGasPaymasterTest is Test { event GasPayment( bytes32 indexed messageId, + uint32 indexed destinationDomain, uint256 gasAmount, uint256 payment ); @@ -176,7 +177,12 @@ contract InterchainGasPaymasterTest is Test { uint256 _overpayment = 54321; vm.expectEmit(true, true, false, true); - emit GasPayment(testMessageId, testGasAmount, _quote); + emit GasPayment( + testMessageId, + testDestinationDomain, + testGasAmount, + _quote + ); igp.payForGas{value: _quote + _overpayment}( testMessageId, testDestinationDomain, From dcabfb0e0828ffefb57866128a74e9ed5152cd53 Mon Sep 17 00:00:00 2001 From: Kunal Arora Date: Sun, 3 Sep 2023 21:40:27 -0400 Subject: [PATCH 20/27] add abi and test --- .../agents/relayer/src/msg/gas_payment/mod.rs | 57 ++++ .../src/msg/gas_payment/policies/minimum.rs | 2 + .../src/msg/gas_payment/policies/none.rs | 1 + .../policies/on_chain_fee_quoting.rs | 1 + .../abis/IInterchainGasPaymaster.abi.json | 279 +++++++++++++++++- .../hyperlane-ethereum/src/interchain_gas.rs | 1 + .../src/db/rocks/storage_types.rs | 5 + 7 files changed, 345 insertions(+), 1 deletion(-) diff --git a/rust/agents/relayer/src/msg/gas_payment/mod.rs b/rust/agents/relayer/src/msg/gas_payment/mod.rs index 4b5a66b9c6..4eb999145d 100644 --- a/rust/agents/relayer/src/msg/gas_payment/mod.rs +++ b/rust/agents/relayer/src/msg/gas_payment/mod.rs @@ -97,6 +97,17 @@ impl GasPaymentEnforcer { ?whitelist, "Message matched whitelist for policy" ); + + if current_payment.destination != message.destination { + trace!( + msg=%message, + ?policy, + ?current_payment, + ?current_expenditure, + "Message destination did not match current payment destination" + ); + continue; + } debug!( msg=%message, ?policy, @@ -209,6 +220,52 @@ mod test { .await; } + #[tokio::test] + async fn test_check_destination() { + #[allow(unused_must_use)] + test_utils::run_test_db(|db| async move { + let correct_destination_msg = HyperlaneMessage::default(); + let incorrect_destination_msg = HyperlaneMessage { + destination: 123, + ..HyperlaneMessage::default() + }; + + let hyperlane_db = HyperlaneRocksDB::new( + &HyperlaneDomain::new_test_domain("test_check_destination"), + db, + ); + + let enforcer = GasPaymentEnforcer::new( + // Require a payment + vec![GasPaymentEnforcementConf { + policy: GasPaymentEnforcementPolicy::None, + matching_list: MatchingList::default(), + }], + hyperlane_db, + ); + + /// Ensure if the message has the correct destination, it meets the requirement + assert!(enforcer + .message_meets_gas_payment_requirement( + &correct_destination_msg, + &TxCostEstimate::default(), + ) + .await + .unwrap() + .is_some()); + /// Ensure if the message has the incorrect destination, it does not meet the requirement + assert!(enforcer + .message_meets_gas_payment_requirement( + &incorrect_destination_msg, + &TxCostEstimate::default(), + ) + .await + .unwrap() + .is_none()); + }) + .await; + } + #[tokio::test] async fn test_non_empty_matching_list() { test_utils::run_test_db(|db| async move { diff --git a/rust/agents/relayer/src/msg/gas_payment/policies/minimum.rs b/rust/agents/relayer/src/msg/gas_payment/policies/minimum.rs index ae9c44fde0..23f329d09a 100644 --- a/rust/agents/relayer/src/msg/gas_payment/policies/minimum.rs +++ b/rust/agents/relayer/src/msg/gas_payment/policies/minimum.rs @@ -41,6 +41,7 @@ async fn test_gas_payment_policy_minimum() { // If the payment is less than the minimum, returns false let current_payment = InterchainGasPayment { message_id: H256::zero(), + destination: message.destination, payment: U256::from(999u32), gas_amount: U256::zero(), }; @@ -70,6 +71,7 @@ async fn test_gas_payment_policy_minimum() { // If the payment is at least the minimum, returns false let current_payment = InterchainGasPayment { message_id: H256::zero(), + destination: message.destination, payment: U256::from(1000u32), gas_amount: U256::zero(), }; diff --git a/rust/agents/relayer/src/msg/gas_payment/policies/none.rs b/rust/agents/relayer/src/msg/gas_payment/policies/none.rs index 8d634b5cfe..9ce88a39c7 100644 --- a/rust/agents/relayer/src/msg/gas_payment/policies/none.rs +++ b/rust/agents/relayer/src/msg/gas_payment/policies/none.rs @@ -33,6 +33,7 @@ async fn test_gas_payment_policy_none() { let current_payment = InterchainGasPayment { message_id: H256::zero(), + destination: message.destination, payment: U256::zero(), gas_amount: U256::zero(), }; diff --git a/rust/agents/relayer/src/msg/gas_payment/policies/on_chain_fee_quoting.rs b/rust/agents/relayer/src/msg/gas_payment/policies/on_chain_fee_quoting.rs index 6674ca830e..cdc017defa 100644 --- a/rust/agents/relayer/src/msg/gas_payment/policies/on_chain_fee_quoting.rs +++ b/rust/agents/relayer/src/msg/gas_payment/policies/on_chain_fee_quoting.rs @@ -70,6 +70,7 @@ mod test { fn current_payment(gas_amount: impl Into) -> InterchainGasPayment { InterchainGasPayment { message_id: H256::zero(), + destination: 0, payment: U256::zero(), gas_amount: gas_amount.into(), } diff --git a/rust/chains/hyperlane-ethereum/abis/IInterchainGasPaymaster.abi.json b/rust/chains/hyperlane-ethereum/abis/IInterchainGasPaymaster.abi.json index 3a197135d7..c084af1f68 100644 --- a/rust/chains/hyperlane-ethereum/abis/IInterchainGasPaymaster.abi.json +++ b/rust/chains/hyperlane-ethereum/abis/IInterchainGasPaymaster.abi.json @@ -1,4 +1,36 @@ [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "beneficiary", + "type": "address" + } + ], + "name": "BeneficiarySet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "remoteDomain", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "address", + "name": "gasOracle", + "type": "address" + } + ], + "name": "GasOracleSet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -8,6 +40,12 @@ "name": "messageId", "type": "bytes32" }, + { + "indexed": true, + "internalType": "uint32", + "name": "destinationDomain", + "type": "uint32" + }, { "indexed": false, "internalType": "uint256", @@ -24,6 +62,145 @@ "name": "GasPayment", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "beneficiary", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "deployedBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "name": "gasOracles", + "outputs": [ + { + "internalType": "contract IGasOracle", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_destinationDomain", + "type": "uint32" + } + ], + "name": "getExchangeRateAndGasPrice", + "outputs": [ + { + "internalType": "uint128", + "name": "tokenExchangeRate", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "gasPrice", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "address", + "name": "_beneficiary", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -52,6 +229,48 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "message", + "type": "bytes" + } + ], + "name": "postDispatch", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "message", + "type": "bytes" + } + ], + "name": "quoteDispatch", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -75,5 +294,63 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_beneficiary", + "type": "address" + } + ], + "name": "setBeneficiary", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint32", + "name": "remoteDomain", + "type": "uint32" + }, + { + "internalType": "address", + "name": "gasOracle", + "type": "address" + } + ], + "internalType": "struct InterchainGasPaymaster.GasOracleConfig[]", + "name": "_configs", + "type": "tuple[]" + } + ], + "name": "setGasOracles", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } -] +] \ No newline at end of file diff --git a/rust/chains/hyperlane-ethereum/src/interchain_gas.rs b/rust/chains/hyperlane-ethereum/src/interchain_gas.rs index 7ac5ee87b7..77ed88c5b6 100644 --- a/rust/chains/hyperlane-ethereum/src/interchain_gas.rs +++ b/rust/chains/hyperlane-ethereum/src/interchain_gas.rs @@ -109,6 +109,7 @@ where ( InterchainGasPayment { message_id: H256::from(log.message_id), + destination: log.destination_domain.into(), payment: log.payment.into(), gas_amount: log.gas_amount.into(), }, diff --git a/rust/hyperlane-base/src/db/rocks/storage_types.rs b/rust/hyperlane-base/src/db/rocks/storage_types.rs index 2e5a1d8aa6..31965493bd 100644 --- a/rust/hyperlane-base/src/db/rocks/storage_types.rs +++ b/rust/hyperlane-base/src/db/rocks/storage_types.rs @@ -9,6 +9,7 @@ use hyperlane_core::{ /// the key. #[derive(Debug, Copy, Clone)] pub(super) struct InterchainGasPaymentData { + pub destination: u32, pub payment: U256, pub gas_amount: U256, } @@ -24,6 +25,7 @@ pub(super) struct InterchainGasExpenditureData { impl Default for InterchainGasPaymentData { fn default() -> Self { Self { + destination: 0, payment: U256::zero(), gas_amount: U256::zero(), } @@ -34,6 +36,7 @@ impl InterchainGasPaymentData { pub fn complete(self, message_id: H256) -> InterchainGasPayment { InterchainGasPayment { message_id, + destination: self.destination, payment: self.payment, gas_amount: self.gas_amount, } @@ -43,6 +46,7 @@ impl InterchainGasPaymentData { impl From for InterchainGasPaymentData { fn from(p: InterchainGasPayment) -> Self { Self { + destination: p.destination, payment: p.payment, gas_amount: p.gas_amount, } @@ -65,6 +69,7 @@ impl Decode for InterchainGasPaymentData { Self: Sized, { Ok(Self { + destination: u32::read_from(reader)?, payment: U256::read_from(reader)?, gas_amount: U256::read_from(reader)?, }) From 60b2fc3619061871534bab65d9aff5cde0b96f81 Mon Sep 17 00:00:00 2001 From: -f Date: Tue, 5 Sep 2023 10:06:53 -0400 Subject: [PATCH 21/27] fix ci errors Signed-off-by: -f --- rust/chains/hyperlane-ethereum/src/interchain_gas.rs | 2 +- solidity/test/router.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/chains/hyperlane-ethereum/src/interchain_gas.rs b/rust/chains/hyperlane-ethereum/src/interchain_gas.rs index 77ed88c5b6..815e8b70f5 100644 --- a/rust/chains/hyperlane-ethereum/src/interchain_gas.rs +++ b/rust/chains/hyperlane-ethereum/src/interchain_gas.rs @@ -109,7 +109,7 @@ where ( InterchainGasPayment { message_id: H256::from(log.message_id), - destination: log.destination_domain.into(), + destination: log.destination_domain, payment: log.payment.into(), gas_amount: log.gas_amount.into(), }, diff --git a/solidity/test/router.test.ts b/solidity/test/router.test.ts index 7154e77110..7cd37afef0 100644 --- a/solidity/test/router.test.ts +++ b/solidity/test/router.test.ts @@ -225,6 +225,7 @@ describe('Router', async () => { .emit(igp, 'GasPayment') .withArgs( id, + destination, testGasPaymentParams.gasAmount, testGasPaymentParams.payment, ); From 115ef53ec068bac87ae3c07e51cad60c0b929042 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Fri, 8 Sep 2023 14:22:08 -0400 Subject: [PATCH 22/27] Add forge test for Mailbox (#2713) --- solidity/contracts/GasRouter.sol | 10 +- solidity/contracts/Mailbox.sol | 306 +++++++++--- solidity/contracts/Router.sol | 40 +- solidity/contracts/hooks/MerkleTreeHook.sol | 7 +- solidity/contracts/interfaces/IMailbox.sol | 53 ++- .../interfaces/IMessageRecipient.sol | 2 +- .../interfaces/IMessageRecipientV3.sol | 10 - .../mock/MockHyperlaneEnvironment.sol | 4 +- solidity/contracts/mock/MockMailbox.sol | 11 +- .../contracts/test/LightTestRecipient.sol | 2 +- .../test/TestInterchainGasPaymaster.sol | 5 +- solidity/contracts/test/TestIsm.sol | 18 + solidity/contracts/test/TestMailbox.sol | 33 +- .../contracts/test/TestMerkleTreeHook.sol | 6 + .../contracts/test/TestPostDispatchHook.sol | 10 +- solidity/contracts/test/TestRecipient.sol | 5 +- solidity/contracts/test/TestRouter.sol | 12 +- solidity/contracts/test/TestSendReceiver.sol | 2 +- .../test/bad-recipient/BadRecipient1.sol | 2 +- .../test/bad-recipient/BadRecipient3.sol | 2 +- .../test/bad-recipient/BadRecipient5.sol | 2 +- .../test/bad-recipient/BadRecipient6.sol | 2 +- solidity/test/GasRouter.t.sol | 44 +- solidity/test/Mailbox.t.sol | 445 ++++++++++++++++++ .../hooks/FallbackDomainRoutingHook.t.sol | 28 +- .../test/hyperlaneConnectionClient.test.ts | 7 +- solidity/test/igps/OverheadIgp.t.sol | 2 +- solidity/test/isms/ERC5164ISM.t.sol | 2 +- solidity/test/isms/MultisigIsm.t.sol | 15 +- solidity/test/isms/OPStackIsm.t.sol | 2 +- solidity/test/isms/legacyMultisigIsm.test.ts | 34 +- solidity/test/mailbox.test.ts | 273 ----------- solidity/test/mockMailbox.test.ts | 2 +- solidity/test/router.test.ts | 164 +++---- 34 files changed, 1003 insertions(+), 559 deletions(-) delete mode 100644 solidity/contracts/interfaces/IMessageRecipientV3.sol create mode 100644 solidity/contracts/test/TestIsm.sol create mode 100644 solidity/test/Mailbox.t.sol delete mode 100644 solidity/test/mailbox.test.ts diff --git a/solidity/contracts/GasRouter.sol b/solidity/contracts/GasRouter.sol index 6eaee5a80b..c6c802cda5 100644 --- a/solidity/contracts/GasRouter.sol +++ b/solidity/contracts/GasRouter.sol @@ -2,6 +2,7 @@ pragma solidity >=0.6.11; import {Router} from "./Router.sol"; +import {IGPMetadata} from "./libs/hooks/IGPMetadata.sol"; abstract contract GasRouter is Router { // ============ Mutable Storage ============ @@ -45,9 +46,14 @@ abstract contract GasRouter is Router { returns (uint256 _gasPayment) { return - interchainGasPaymaster.quoteGasPayment( + mailbox.quoteDispatch( _destinationDomain, - destinationGas[_destinationDomain] + _mustHaveRemoteRouter(_destinationDomain), + "", + IGPMetadata.formatMetadata( + destinationGas[_destinationDomain], + address(this) + ) ); } diff --git a/solidity/contracts/Mailbox.sol b/solidity/contracts/Mailbox.sol index a02ced561d..4dbf57328c 100644 --- a/solidity/contracts/Mailbox.sol +++ b/solidity/contracts/Mailbox.sol @@ -8,14 +8,14 @@ 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 {IMessageRecipient} from "./interfaces/IMessageRecipient.sol"; import {IMailbox} from "./interfaces/IMailbox.sol"; // ============ External Imports ============ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -contract Mailbox is IMailbox, Indexed, Versioned, Ownable { +contract Mailbox is IMailbox, Indexed, Versioned, OwnableUpgradeable { // ============ Libraries ============ using Message for bytes; @@ -31,21 +31,23 @@ contract Mailbox is IMailbox, Indexed, Versioned, Ownable { // 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; - // The default post dispatch hook, used for post processing of dispatched messages. + // The default post dispatch hook, used for post processing of opting-in dispatches. IPostDispatchHook public defaultHook; + // The required post dispatch hook, used for post processing of ALL dispatches. + IPostDispatchHook public requiredHook; + // Mapping of message ID to delivery context that processed the message. struct Delivery { - // address sender; - IInterchainSecurityModule ism; - // uint48 value? - // uint48 timestamp? + address processor; + uint48 timestamp; } mapping(bytes32 => Delivery) internal deliveries; @@ -63,35 +65,32 @@ contract Mailbox is IMailbox, Indexed, Versioned, Ownable { */ 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. + * @notice Emitted when the required hook is updated + * @param hook The new required hook */ - function setDefaultIsm(address _module) external onlyOwner { - require(Address.isContract(_module), "Mailbox: !contract"); - defaultIsm = IInterchainSecurityModule(_module); - emit DefaultIsmSet(_module); + event RequiredHookSet(address indexed hook); + + // ============ Constructor ============ + constructor(uint32 _localDomain) { + localDomain = _localDomain; } - /** - * @notice Sets the default post dispatch hook for the Mailbox. - * @param _hook The new default post dispatch hook. Must be a contract. - */ - function setDefaultHook(address _hook) external onlyOwner { - require(Address.isContract(_hook), "Mailbox: !contract"); - defaultHook = IPostDispatchHook(_hook); - emit DefaultHookSet(_hook); + // ============ Initializers ============ + function initialize( + address _owner, + address _defaultIsm, + address _defaultHook, + address _requiredHook + ) external initializer { + __Ownable_init(); + setDefaultIsm(_defaultIsm); + setDefaultHook(_defaultHook); + setRequiredHook(_requiredHook); + transferOwnership(_owner); } + // ============ External Functions ============ /** * @notice Dispatches a message to the destination domain & recipient. * @param _destinationDomain Domain of destination chain @@ -109,8 +108,8 @@ contract Mailbox is IMailbox, Indexed, Versioned, Ownable { _destinationDomain, _recipientAddress, _messageBody, - defaultHook, - _messageBody[0:0] + _messageBody[0:0], + defaultHook ); } @@ -133,49 +132,55 @@ contract Mailbox is IMailbox, Indexed, Versioned, Ownable { destinationDomain, recipientAddress, messageBody, - defaultHook, - hookMetadata + hookMetadata, + defaultHook ); } - function dispatch( + /** + * @notice Computes quote for dispatching 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 fee The payment required to dispatch the message + */ + function quoteDispatch( 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( - VERSION, - nonce, - localDomain, - msg.sender.addressToBytes32(), - destinationDomain, - recipientAddress, - messageBody - ); - bytes32 id = message.id(); - - /// EFFECTS /// - - nonce += 1; - latestDispatchedId = id; - - emit Dispatch(message); - emit DispatchId(id); - - /// INTERACTIONS /// - - hook.postDispatch{value: msg.value}(metadata, message); - - return id; + bytes calldata messageBody + ) external view returns (uint256 fee) { + return + quoteDispatch( + destinationDomain, + recipientAddress, + messageBody, + messageBody[0:0], + defaultHook + ); } - function delivered(bytes32 _id) public view override returns (bool) { - return address(deliveries[_id].ism) != address(0); + /** + * @notice Computes quote for dispatching 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 defaultHookMetadata Metadata used by the default post dispatch hook + * @return fee The payment required to dispatch the message + */ + function quoteDispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody, + bytes calldata defaultHookMetadata + ) external view returns (uint256 fee) { + return + quoteDispatch( + destinationDomain, + recipientAddress, + messageBody, + defaultHookMetadata, + defaultHook + ); } /** @@ -209,20 +214,18 @@ contract Mailbox is IMailbox, Indexed, Versioned, Ownable { /// EFFECTS /// deliveries[_id] = Delivery({ - ism: ism - // sender: msg.sender - // value: uint48(msg.value), - // timestamp: uint48(block.number) + processor: msg.sender, + timestamp: uint48(block.timestamp) }); - emit Process(_message); + emit Process(_message.origin(), _message.sender(), recipient); emit ProcessId(_id); /// INTERACTIONS /// - // Verify the message via the ISM. + // Verify the message via the interchain security module. require( ism.verify(_metadata, _message), - "Mailbox: verification failed" + "Mailbox: ISM verification failed" ); // Deliver the message to the recipient. @@ -233,8 +236,141 @@ contract Mailbox is IMailbox, Indexed, Versioned, Ownable { ); } + /** + * @notice Returns the account that processed the message. + * @param _id The message ID to check. + * @return The account that processed the message. + */ + function processor(bytes32 _id) external view returns (address) { + return deliveries[_id].processor; + } + + /** + * @notice Returns the account that processed the message. + * @param _id The message ID to check. + * @return The account that processed the message. + */ + function processedAt(bytes32 _id) external view returns (uint48) { + return deliveries[_id].timestamp; + } + // ============ Public Functions ============ + /** + * @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 metadata Metadata used by the post dispatch hook + * @param hook Custom hook to use instead of the default + * @return The message ID inserted into the Mailbox's merkle tree + */ + function dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody, + bytes calldata metadata, + IPostDispatchHook hook + ) public payable returns (bytes32) { + /// CHECKS /// + + // Format the message into packed bytes. + bytes memory message = _buildMessage( + destinationDomain, + recipientAddress, + messageBody + ); + bytes32 id = message.id(); + + /// EFFECTS /// + + latestDispatchedId = id; + nonce += 1; + emit Dispatch(msg.sender, destinationDomain, recipientAddress, message); + emit DispatchId(id); + + /// INTERACTIONS /// + uint256 requiredValue = requiredHook.quoteDispatch(metadata, message); + requiredHook.postDispatch{value: requiredValue}(metadata, message); + hook.postDispatch{value: msg.value - requiredValue}(metadata, message); + + return id; + } + + /** + * @notice Computes quote for dispatching 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 metadata Metadata used by the post dispatch hook + * @param hook Custom hook to use instead of the default + * @return fee The payment required to dispatch the message + */ + function quoteDispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody, + bytes calldata metadata, + IPostDispatchHook hook + ) public view returns (uint256 fee) { + bytes memory message = _buildMessage( + destinationDomain, + recipientAddress, + messageBody + ); + return + requiredHook.quoteDispatch(metadata, message) + + hook.quoteDispatch(metadata, message); + } + + /** + * @notice Returns true if the message has been processed. + * @param _id The message ID to check. + * @return True if the message has been delivered. + */ + function delivered(bytes32 _id) public view override returns (bool) { + return deliveries[_id].timestamp > 0; + } + + /** + * @notice Sets the default ISM for the Mailbox. + * @param _module The new default ISM. Must be a contract. + */ + function setDefaultIsm(address _module) public onlyOwner { + require( + Address.isContract(_module), + "Mailbox: default ISM not contract" + ); + defaultIsm = IInterchainSecurityModule(_module); + emit DefaultIsmSet(_module); + } + + /** + * @notice Sets the default post dispatch hook for the Mailbox. + * @param _hook The new default post dispatch hook. Must be a contract. + */ + function setDefaultHook(address _hook) public onlyOwner { + require( + Address.isContract(_hook), + "Mailbox: default hook not contract" + ); + defaultHook = IPostDispatchHook(_hook); + emit DefaultHookSet(_hook); + } + + /** + * @notice Sets the required post dispatch hook for the Mailbox. + * @param _hook The new default post dispatch hook. Must be a contract. + */ + function setRequiredHook(address _hook) public onlyOwner { + require( + Address.isContract(_hook), + "Mailbox: required hook not contract" + ); + requiredHook = IPostDispatchHook(_hook); + emit RequiredHookSet(_hook); + } + /** * @notice Returns the ISM to use for the recipient, defaulting to the * default ISM if none is specified. @@ -262,4 +398,22 @@ contract Mailbox is IMailbox, Indexed, Versioned, Ownable { } catch {} return defaultIsm; } + + // ============ Internal Functions ============ + function _buildMessage( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody + ) internal view returns (bytes memory) { + return + Message.formatMessage( + VERSION, + nonce, + localDomain, + msg.sender.addressToBytes32(), + destinationDomain, + recipientAddress, + messageBody + ); + } } diff --git a/solidity/contracts/Router.sol b/solidity/contracts/Router.sol index be8418bf18..7eeb096fd6 100644 --- a/solidity/contracts/Router.sol +++ b/solidity/contracts/Router.sol @@ -7,6 +7,7 @@ import {IInterchainGasPaymaster} from "./interfaces/IInterchainGasPaymaster.sol" import {IMessageRecipient} from "./interfaces/IMessageRecipient.sol"; import {IMailbox} from "./interfaces/IMailbox.sol"; import {EnumerableMapExtended} from "./libs/EnumerableMapExtended.sol"; +import {IGPMetadata} from "./libs/hooks/IGPMetadata.sol"; abstract contract Router is HyperlaneConnectionClient, IMessageRecipient { using EnumerableMapExtended for EnumerableMapExtended.UintToBytes32Map; @@ -126,7 +127,14 @@ abstract contract Router is HyperlaneConnectionClient, IMessageRecipient { uint32 _origin, bytes32 _sender, bytes calldata _message - ) external virtual override onlyMailbox onlyRemoteRouter(_origin, _sender) { + ) + external + payable + virtual + override + onlyMailbox + onlyRemoteRouter(_origin, _sender) + { _handle(_origin, _sender, _message); } @@ -193,31 +201,31 @@ abstract contract Router is HyperlaneConnectionClient, IMessageRecipient { uint256 _gasPayment, address _gasPaymentRefundAddress ) internal returns (bytes32 _messageId) { - _messageId = _dispatch(_destinationDomain, _messageBody); - // Call the IGP even if the gas payment is zero. This is to support on-chain - // fee quoting in IGPs, which should always revert if gas payment is insufficient. - interchainGasPaymaster.payForGas{value: _gasPayment}( - _messageId, - _destinationDomain, + // Ensure that destination chain has an enrolled router. + bytes32 _router = _mustHaveRemoteRouter(_destinationDomain); + bytes memory metadata = IGPMetadata.formatMetadata( _gasAmount, _gasPaymentRefundAddress ); + _messageId = mailbox.dispatch{value: _gasPayment}( + _destinationDomain, + _router, + _messageBody, + metadata + ); } - /** - * @notice Dispatches a message to an enrolled router via the provided Mailbox. - * @dev Does not pay interchain gas. - * @dev Reverts if there is no enrolled router for _destinationDomain. - * @param _destinationDomain The domain of the chain to which to send the message. - * @param _messageBody Raw bytes content of message. - */ function _dispatch(uint32 _destinationDomain, bytes memory _messageBody) internal virtual returns (bytes32) { - // Ensure that destination chain has an enrolled router. bytes32 _router = _mustHaveRemoteRouter(_destinationDomain); - return mailbox.dispatch(_destinationDomain, _router, _messageBody); + return + mailbox.dispatch{value: msg.value}( + _destinationDomain, + _router, + _messageBody + ); } } diff --git a/solidity/contracts/hooks/MerkleTreeHook.sol b/solidity/contracts/hooks/MerkleTreeHook.sol index b83df831de..0e320a92e2 100644 --- a/solidity/contracts/hooks/MerkleTreeHook.sol +++ b/solidity/contracts/hooks/MerkleTreeHook.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {MerkleLib, TREE_DEPTH} from "../libs/Merkle.sol"; +import {MerkleLib} from "../libs/Merkle.sol"; import {Message} from "../libs/Message.sol"; import {MailboxClient} from "../client/MailboxClient.sol"; import {Indexed} from "../Indexed.sol"; @@ -24,10 +24,6 @@ contract MerkleTreeHook is IPostDispatchHook, MailboxClient, Indexed { return _tree.root(); } - function branch() public view returns (bytes32[TREE_DEPTH] memory) { - return _tree.branch; - } - function tree() public view returns (MerkleLib.Tree memory) { return _tree; } @@ -40,6 +36,7 @@ contract MerkleTreeHook is IPostDispatchHook, MailboxClient, Indexed { bytes calldata, /*metadata*/ bytes calldata message ) external payable override { + require(msg.value == 0, "MerkleTreeHook: no value expected"); bytes32 id = message.id(); require(isLatestDispatched(id), "message not dispatching"); _tree.insert(id); diff --git a/solidity/contracts/interfaces/IMailbox.sol b/solidity/contracts/interfaces/IMailbox.sol index 89958881f5..019392d020 100644 --- a/solidity/contracts/interfaces/IMailbox.sol +++ b/solidity/contracts/interfaces/IMailbox.sol @@ -8,9 +8,17 @@ 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(bytes message); + event Dispatch( + address indexed sender, + uint32 indexed destination, + bytes32 indexed recipient, + bytes message + ); /** * @notice Emitted when a new message is dispatched via Hyperlane @@ -18,18 +26,24 @@ interface IMailbox { */ 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); + /** + * @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 + */ + event Process( + uint32 indexed origin, + bytes32 indexed sender, + address indexed recipient + ); + function localDomain() external view returns (uint32); function delivered(bytes32 messageId) external view returns (bool); @@ -46,6 +60,12 @@ interface IMailbox { bytes calldata messageBody ) external payable returns (bytes32 messageId); + function quoteDispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody + ) external view returns (uint256 fee); + function dispatch( uint32 destinationDomain, bytes32 recipientAddress, @@ -53,14 +73,29 @@ interface IMailbox { bytes calldata defaultHookMetadata ) external payable returns (bytes32 messageId); + function quoteDispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody, + bytes calldata defaultHookMetadata + ) external view returns (uint256 fee); + function dispatch( uint32 destinationDomain, bytes32 recipientAddress, bytes calldata body, - IPostDispatchHook customHook, - bytes calldata customHookMetadata + bytes calldata customHookMetadata, + IPostDispatchHook customHook ) external payable returns (bytes32 messageId); + function quoteDispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody, + bytes calldata customHookMetadata, + IPostDispatchHook customHook + ) external view returns (uint256 fee); + function process(bytes calldata metadata, bytes calldata message) external payable; diff --git a/solidity/contracts/interfaces/IMessageRecipient.sol b/solidity/contracts/interfaces/IMessageRecipient.sol index 02487430ed..187194b6d1 100644 --- a/solidity/contracts/interfaces/IMessageRecipient.sol +++ b/solidity/contracts/interfaces/IMessageRecipient.sol @@ -6,5 +6,5 @@ interface IMessageRecipient { uint32 _origin, bytes32 _sender, bytes calldata _message - ) external; + ) external payable; } diff --git a/solidity/contracts/interfaces/IMessageRecipientV3.sol b/solidity/contracts/interfaces/IMessageRecipientV3.sol deleted file mode 100644 index 187194b6d1..0000000000 --- a/solidity/contracts/interfaces/IMessageRecipientV3.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.6.11; - -interface IMessageRecipient { - function handle( - uint32 _origin, - bytes32 _sender, - bytes calldata _message - ) external payable; -} diff --git a/solidity/contracts/mock/MockHyperlaneEnvironment.sol b/solidity/contracts/mock/MockHyperlaneEnvironment.sol index d87a91e367..c15dfcbeb3 100644 --- a/solidity/contracts/mock/MockHyperlaneEnvironment.sol +++ b/solidity/contracts/mock/MockHyperlaneEnvironment.sol @@ -27,8 +27,8 @@ contract MockHyperlaneEnvironment { originMailbox.addRemoteMailbox(_destinationDomain, destinationMailbox); destinationMailbox.addRemoteMailbox(_originDomain, originMailbox); - igps[originDomain] = new TestInterchainGasPaymaster(address(this)); - igps[destinationDomain] = new TestInterchainGasPaymaster(address(this)); + igps[originDomain] = new TestInterchainGasPaymaster(); + igps[destinationDomain] = new TestInterchainGasPaymaster(); isms[originDomain] = new TestMultisigIsm(); isms[destinationDomain] = new TestMultisigIsm(); diff --git a/solidity/contracts/mock/MockMailbox.sol b/solidity/contracts/mock/MockMailbox.sol index 3e7f4b129c..4ffef452de 100644 --- a/solidity/contracts/mock/MockMailbox.sol +++ b/solidity/contracts/mock/MockMailbox.sol @@ -46,7 +46,7 @@ contract MockMailbox is Versioned { uint32 _destinationDomain, bytes32 _recipientAddress, bytes calldata _messageBody - ) external returns (bytes32) { + ) public payable returns (bytes32) { require(_messageBody.length <= MAX_MESSAGE_BODY_BYTES, "msg too long"); MockMailbox _destinationMailbox = remoteMailboxes[_destinationDomain]; require( @@ -64,6 +64,15 @@ contract MockMailbox is Versioned { return bytes32(0); } + function dispatch( + uint32 _destinationDomain, + bytes32 _recipientAddress, + bytes calldata _messageBody, + bytes calldata /*_metadata*/ + ) external payable returns (bytes32) { + return dispatch(_destinationDomain, _recipientAddress, _messageBody); + } + function addInboundMessage( uint32 _nonce, uint32 _origin, diff --git a/solidity/contracts/test/LightTestRecipient.sol b/solidity/contracts/test/LightTestRecipient.sol index ca21f9ffbf..c62a21f884 100644 --- a/solidity/contracts/test/LightTestRecipient.sol +++ b/solidity/contracts/test/LightTestRecipient.sol @@ -9,7 +9,7 @@ contract LightTestRecipient is TestRecipient { uint32 _origin, bytes32 _sender, bytes calldata _data - ) external override { + ) external payable override { // do nothing } } diff --git a/solidity/contracts/test/TestInterchainGasPaymaster.sol b/solidity/contracts/test/TestInterchainGasPaymaster.sol index 1bc7e3b999..d503f57560 100644 --- a/solidity/contracts/test/TestInterchainGasPaymaster.sol +++ b/solidity/contracts/test/TestInterchainGasPaymaster.sol @@ -7,9 +7,8 @@ import {InterchainGasPaymaster} from "../igps/InterchainGasPaymaster.sol"; contract TestInterchainGasPaymaster is InterchainGasPaymaster { uint256 public constant gasPrice = 10; - // Ensure the same constructor interface as the inherited InterchainGasPaymaster - constructor(address _beneficiary) { - initialize(msg.sender, _beneficiary); + constructor() { + initialize(msg.sender, msg.sender); } function quoteGasPayment(uint32, uint256 gasAmount) diff --git a/solidity/contracts/test/TestIsm.sol b/solidity/contracts/test/TestIsm.sol new file mode 100644 index 0000000000..b3bc00d014 --- /dev/null +++ b/solidity/contracts/test/TestIsm.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +import {IInterchainSecurityModule} from "../interfaces/IInterchainSecurityModule.sol"; + +contract TestIsm is IInterchainSecurityModule { + uint8 public moduleType = uint8(Types.NULL); + + bool verifyResult = true; + + function setVerify(bool _verify) public { + verifyResult = _verify; + } + + function verify(bytes calldata, bytes calldata) public view returns (bool) { + return verifyResult; + } +} diff --git a/solidity/contracts/test/TestMailbox.sol b/solidity/contracts/test/TestMailbox.sol index af5204dfcf..0f8725e543 100644 --- a/solidity/contracts/test/TestMailbox.sol +++ b/solidity/contracts/test/TestMailbox.sol @@ -3,15 +3,16 @@ pragma solidity >=0.8.0; import {Mailbox} from "../Mailbox.sol"; import {TypeCasts} from "../libs/TypeCasts.sol"; +import {Message} from "../libs/Message.sol"; import {MerkleLib} from "../libs/Merkle.sol"; import {IMessageRecipient} from "../interfaces/IMessageRecipient.sol"; contract TestMailbox is Mailbox { using TypeCasts for bytes32; - constructor(uint32 _localDomain, address _owner) - Mailbox(_localDomain, _owner) - {} // solhint-disable-line no-empty-blocks + constructor(uint32 _localDomain) Mailbox(_localDomain) { + _transferOwnership(msg.sender); + } function testHandle( uint32 _origin, @@ -26,6 +27,32 @@ contract TestMailbox is Mailbox { ); } + function buildOutboundMessage( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata body + ) external view returns (bytes memory) { + return _buildMessage(destinationDomain, recipientAddress, body); + } + + function buildInboundMessage( + uint32 originDomain, + bytes32 recipientAddress, + bytes32 senderAddress, + bytes calldata body + ) external view returns (bytes memory) { + return + Message.formatMessage( + VERSION, + nonce, + originDomain, + senderAddress, + localDomain, + recipientAddress, + body + ); + } + function updateLatestDispatchedId(bytes32 _id) external { latestDispatchedId = _id; } diff --git a/solidity/contracts/test/TestMerkleTreeHook.sol b/solidity/contracts/test/TestMerkleTreeHook.sol index 291d37d146..c0f8176113 100644 --- a/solidity/contracts/test/TestMerkleTreeHook.sol +++ b/solidity/contracts/test/TestMerkleTreeHook.sol @@ -5,6 +5,8 @@ import {MerkleLib} from "../libs/Merkle.sol"; import {MerkleTreeHook} from "../hooks/MerkleTreeHook.sol"; contract TestMerkleTreeHook is MerkleTreeHook { + using MerkleLib for MerkleLib.Tree; + constructor(address _mailbox) MerkleTreeHook(_mailbox) {} function proof() external view returns (bytes32[32] memory) { @@ -22,4 +24,8 @@ contract TestMerkleTreeHook is MerkleTreeHook { } return _proof; } + + function insert(bytes32 _id) external { + _tree.insert(_id); + } } diff --git a/solidity/contracts/test/TestPostDispatchHook.sol b/solidity/contracts/test/TestPostDispatchHook.sol index 019cd8abf7..09bdaa67e8 100644 --- a/solidity/contracts/test/TestPostDispatchHook.sol +++ b/solidity/contracts/test/TestPostDispatchHook.sol @@ -4,19 +4,23 @@ pragma solidity >=0.8.0; import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; contract TestPostDispatchHook is IPostDispatchHook { - uint256 public mockGasQuote = 25000; + uint256 public fee = 25000; function postDispatch( bytes calldata, /*metadata*/ bytes calldata /*message*/ - ) external payable override { + ) external payable { // test - empty } + function setFee(uint256 _fee) external { + fee = _fee; + } + function quoteDispatch( bytes calldata, /*metadata*/ bytes calldata /*message*/ ) external view override returns (uint256) { - return mockGasQuote; + return fee; } } diff --git a/solidity/contracts/test/TestRecipient.sol b/solidity/contracts/test/TestRecipient.sol index e09c53505d..3b3b3e3afb 100644 --- a/solidity/contracts/test/TestRecipient.sol +++ b/solidity/contracts/test/TestRecipient.sol @@ -21,6 +21,7 @@ contract TestRecipient is event ReceivedMessage( uint32 indexed origin, bytes32 indexed sender, + uint256 indexed value, string message ); @@ -30,8 +31,8 @@ contract TestRecipient is uint32 _origin, bytes32 _sender, bytes calldata _data - ) external virtual override { - emit ReceivedMessage(_origin, _sender, string(_data)); + ) external payable virtual override { + emit ReceivedMessage(_origin, _sender, msg.value, string(_data)); lastSender = _sender; lastData = _data; } diff --git a/solidity/contracts/test/TestRouter.sol b/solidity/contracts/test/TestRouter.sol index eab94b21c9..8178e10f69 100644 --- a/solidity/contracts/test/TestRouter.sol +++ b/solidity/contracts/test/TestRouter.sol @@ -6,14 +6,8 @@ import "../Router.sol"; contract TestRouter is Router { event InitializeOverload(); - function initialize(address _mailbox, address _interchainGasPaymaster) - external - initializer - { - __HyperlaneConnectionClient_initialize( - _mailbox, - _interchainGasPaymaster - ); + function initialize(address _mailbox) external initializer { + __Router_initialize(_mailbox); emit InitializeOverload(); } @@ -39,7 +33,7 @@ contract TestRouter is Router { return _mustHaveRemoteRouter(_domain); } - function dispatch(uint32 _destination, bytes memory _msg) external { + function dispatch(uint32 _destination, bytes memory _msg) external payable { _dispatch(_destination, _msg); } diff --git a/solidity/contracts/test/TestSendReceiver.sol b/solidity/contracts/test/TestSendReceiver.sol index f921121aa5..de59021c61 100644 --- a/solidity/contracts/test/TestSendReceiver.sol +++ b/solidity/contracts/test/TestSendReceiver.sol @@ -58,7 +58,7 @@ contract TestSendReceiver is IMessageRecipient { uint32, bytes32, bytes calldata - ) external override { + ) external payable override { bytes32 blockHash = previousBlockHash(); bool isBlockHashEndIn0 = uint256(blockHash) % 16 == 0; require(!isBlockHashEndIn0, "block hash ends in 0"); diff --git a/solidity/contracts/test/bad-recipient/BadRecipient1.sol b/solidity/contracts/test/bad-recipient/BadRecipient1.sol index 62e5bfba9e..e89af8e78c 100644 --- a/solidity/contracts/test/bad-recipient/BadRecipient1.sol +++ b/solidity/contracts/test/bad-recipient/BadRecipient1.sol @@ -8,7 +8,7 @@ contract BadRecipient1 is IMessageRecipient { uint32, bytes32, bytes calldata - ) external pure override { + ) external payable override { assembly { revert(0, 0) } diff --git a/solidity/contracts/test/bad-recipient/BadRecipient3.sol b/solidity/contracts/test/bad-recipient/BadRecipient3.sol index 35fba6b32f..91638fb777 100644 --- a/solidity/contracts/test/bad-recipient/BadRecipient3.sol +++ b/solidity/contracts/test/bad-recipient/BadRecipient3.sol @@ -8,7 +8,7 @@ contract BadRecipient3 is IMessageRecipient { uint32, bytes32, bytes calldata - ) external pure override { + ) external payable override { assembly { mstore(0, 0xabcdef) revert(0, 32) diff --git a/solidity/contracts/test/bad-recipient/BadRecipient5.sol b/solidity/contracts/test/bad-recipient/BadRecipient5.sol index 382c2a5be3..2101907ad7 100644 --- a/solidity/contracts/test/bad-recipient/BadRecipient5.sol +++ b/solidity/contracts/test/bad-recipient/BadRecipient5.sol @@ -8,7 +8,7 @@ contract BadRecipient5 is IMessageRecipient { uint32, bytes32, bytes calldata - ) external pure override { + ) external payable override { require(false, "no can do"); } } diff --git a/solidity/contracts/test/bad-recipient/BadRecipient6.sol b/solidity/contracts/test/bad-recipient/BadRecipient6.sol index fad3c98ec7..f6e6ce9015 100644 --- a/solidity/contracts/test/bad-recipient/BadRecipient6.sol +++ b/solidity/contracts/test/bad-recipient/BadRecipient6.sol @@ -8,7 +8,7 @@ contract BadRecipient6 is IMessageRecipient { uint32, bytes32, bytes calldata - ) external pure override { + ) external payable override { require(false); // solhint-disable-line reason-string } } diff --git a/solidity/test/GasRouter.t.sol b/solidity/test/GasRouter.t.sol index 4146c8a70b..13cb751aa4 100644 --- a/solidity/test/GasRouter.t.sol +++ b/solidity/test/GasRouter.t.sol @@ -4,36 +4,54 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../contracts/mock/MockHyperlaneEnvironment.sol"; import "../contracts/test/TestGasRouter.sol"; +import "../contracts/test/TestMailbox.sol"; +import "../contracts/test/TestIsm.sol"; +import "../contracts/test/TestInterchainGasPaymaster.sol"; +import "../contracts/test/TestMerkleTreeHook.sol"; contract GasRouterTest is Test { event DestinationGasSet(uint32 indexed domain, uint256 gas); - MockHyperlaneEnvironment environment; - uint32 originDomain = 1; uint32 remoteDomain = 2; uint256 gasPrice; // The gas price used in IGP.quoteGasPayment + TestMailbox originMailbox; + TestMailbox remoteMailbox; + TestGasRouter originRouter; TestGasRouter remoteRouter; function setUp() public { - environment = new MockHyperlaneEnvironment(originDomain, remoteDomain); + originMailbox = new TestMailbox(originDomain); + TestIsm ism = new TestIsm(); + TestInterchainGasPaymaster igp = new TestInterchainGasPaymaster(); + TestMerkleTreeHook _requiredHook = new TestMerkleTreeHook( + address(originMailbox) + ); + originMailbox.initialize( + address(this), + address(ism), + address(igp), + address(_requiredHook) + ); + remoteMailbox = new TestMailbox(remoteDomain); + remoteMailbox.initialize( + address(this), + address(ism), + address(igp), + address(_requiredHook) + ); + // Same for origin and remote - gasPrice = environment.igps(originDomain).gasPrice(); + gasPrice = igp.gasPrice(); originRouter = new TestGasRouter(); remoteRouter = new TestGasRouter(); - originRouter.initialize( - address(environment.mailboxes(originDomain)), - address(environment.igps(originDomain)) - ); - remoteRouter.initialize( - address(environment.mailboxes(remoteDomain)), - address(environment.igps(remoteDomain)) - ); + originRouter.initialize(address(originMailbox)); + remoteRouter.initialize(address(remoteMailbox)); originRouter.enrollRemoteRouter( remoteDomain, @@ -107,7 +125,7 @@ contract GasRouterTest is Test { assertEq(refund, 1); // Reset the IGP balance to avoid a balance overflow - vm.deal(address(environment.igps(originDomain)), 0); + vm.deal(address(originMailbox.defaultHook()), 0); vm.deal(address(this), requiredPayment + 1); passRefund = false; diff --git a/solidity/test/Mailbox.t.sol b/solidity/test/Mailbox.t.sol new file mode 100644 index 0000000000..a4e4525e50 --- /dev/null +++ b/solidity/test/Mailbox.t.sol @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "../contracts/test/TestMailbox.sol"; +import "../contracts/upgrade/Versioned.sol"; +import "../contracts/test/TestPostDispatchHook.sol"; +import "../contracts/test/TestIsm.sol"; +import "../contracts/test/TestRecipient.sol"; +import "../contracts/hooks/MerkleTreeHook.sol"; + +import {TypeCasts} from "../contracts/libs/TypeCasts.sol"; + +contract MailboxTest is Test, Versioned { + using TypeCasts for address; + using Message for bytes; + + uint32 localDomain = 1; + uint32 remoteDomain = 2; + TestMailbox mailbox; + + MerkleTreeHook merkleHook; + + TestPostDispatchHook defaultHook; + TestPostDispatchHook overrideHook; + TestPostDispatchHook requiredHook; + + TestIsm defaultIsm; + TestRecipient recipient; + bytes32 recipientb32; + + address owner; + + function setUp() public { + mailbox = new TestMailbox(localDomain); + recipient = new TestRecipient(); + recipientb32 = address(recipient).addressToBytes32(); + defaultHook = new TestPostDispatchHook(); + merkleHook = new MerkleTreeHook(address(mailbox)); + requiredHook = new TestPostDispatchHook(); + overrideHook = new TestPostDispatchHook(); + defaultIsm = new TestIsm(); + + owner = msg.sender; + mailbox.initialize( + owner, + address(defaultIsm), + address(defaultHook), + address(requiredHook) + ); + } + + function test_localDomain() public { + assertEq(mailbox.localDomain(), localDomain); + } + + function test_initialize() public { + assertEq(mailbox.owner(), owner); + assertEq(address(mailbox.defaultIsm()), address(defaultIsm)); + assertEq(address(mailbox.defaultHook()), address(defaultHook)); + assertEq(address(mailbox.requiredHook()), address(requiredHook)); + } + + function test_initialize_revertsWhenCalledTwice() public { + vm.expectRevert("Initializable: contract is already initialized"); + mailbox.initialize( + owner, + address(defaultIsm), + address(defaultHook), + address(requiredHook) + ); + } + + function test_recipientIsm() public { + IInterchainSecurityModule ism = mailbox.recipientIsm( + address(recipient) + ); + assertEq(address(mailbox.defaultIsm()), address(ism)); + TestIsm newIsm = new TestIsm(); + recipient.setInterchainSecurityModule(address(newIsm)); + ism = mailbox.recipientIsm(address(recipient)); + assertEq(address(ism), address(newIsm)); + } + + event DefaultIsmSet(address indexed module); + + function test_setDefaultIsm() public { + TestIsm newIsm = new TestIsm(); + + // prank owner + vm.startPrank(owner); + vm.expectEmit(true, false, false, false, address(mailbox)); + emit DefaultIsmSet(address(newIsm)); + mailbox.setDefaultIsm(address(newIsm)); + assertEq(address(mailbox.defaultIsm()), address(newIsm)); + + vm.expectRevert("Mailbox: default ISM not contract"); + mailbox.setDefaultIsm(owner); + vm.stopPrank(); + + vm.expectRevert("Ownable: caller is not the owner"); + mailbox.setDefaultIsm(address(newIsm)); + } + + event DefaultHookSet(address indexed module); + + function test_setDefaultHook() public { + TestPostDispatchHook newHook = new TestPostDispatchHook(); + + // prank owner + vm.startPrank(owner); + vm.expectEmit(true, false, false, false, address(mailbox)); + emit DefaultHookSet(address(newHook)); + mailbox.setDefaultHook(address(newHook)); + assertEq(address(mailbox.defaultHook()), address(newHook)); + + vm.expectRevert("Mailbox: default hook not contract"); + mailbox.setDefaultHook(owner); + vm.stopPrank(); + + vm.expectRevert("Ownable: caller is not the owner"); + mailbox.setDefaultHook(address(newHook)); + } + + event RequiredHookSet(address indexed module); + + function test_setRequiredHook() public { + TestPostDispatchHook newHook = new TestPostDispatchHook(); + + // prank owner + vm.startPrank(owner); + vm.expectEmit(true, false, false, false, address(mailbox)); + emit RequiredHookSet(address(newHook)); + mailbox.setRequiredHook(address(newHook)); + assertEq(address(mailbox.requiredHook()), address(newHook)); + + vm.expectRevert("Mailbox: required hook not contract"); + mailbox.setRequiredHook(owner); + vm.stopPrank(); + + vm.expectRevert("Ownable: caller is not the owner"); + mailbox.setRequiredHook(address(newHook)); + } + + function expectHookQuote( + IPostDispatchHook hook, + bytes calldata metadata, + bytes memory message + ) internal { + vm.expectCall( + address(hook), + abi.encodeCall(IPostDispatchHook.quoteDispatch, (metadata, message)) + ); + } + + function expectHookPost( + IPostDispatchHook hook, + bytes calldata metadata, + bytes memory message, + uint256 value + ) internal { + vm.expectCall( + address(hook), + value, + abi.encodeCall(IPostDispatchHook.postDispatch, (metadata, message)) + ); + } + + function test_quoteDispatch( + uint256 requiredFee, + uint256 defaultFee, + uint256 overrideFee, + bytes calldata body, + bytes calldata metadata + ) public { + vm.assume( + requiredFee < type(uint128).max && + defaultFee < type(uint128).max && + overrideFee < type(uint128).max + ); + defaultHook.setFee(defaultFee); + requiredHook.setFee(requiredFee); + overrideHook.setFee(overrideFee); + + bytes memory message = mailbox.buildOutboundMessage( + remoteDomain, + recipientb32, + body + ); + bytes calldata defaultMetadata = metadata[0:0]; + + expectHookQuote(requiredHook, defaultMetadata, message); + expectHookQuote(defaultHook, defaultMetadata, message); + uint256 quote = mailbox.quoteDispatch( + remoteDomain, + address(recipient).addressToBytes32(), + body + ); + assertEq(quote, defaultFee + requiredFee); + + expectHookQuote(requiredHook, metadata, message); + expectHookQuote(defaultHook, metadata, message); + quote = mailbox.quoteDispatch( + remoteDomain, + address(recipient).addressToBytes32(), + body, + metadata + ); + assertEq(quote, defaultFee + requiredFee); + + expectHookQuote(requiredHook, metadata, message); + expectHookQuote(overrideHook, metadata, message); + quote = mailbox.quoteDispatch( + remoteDomain, + address(recipient).addressToBytes32(), + body, + metadata, + overrideHook + ); + assertEq(quote, overrideFee + requiredFee); + } + + event Dispatch( + address indexed sender, + uint32 indexed destination, + bytes32 indexed recipient, + bytes message + ); + + event DispatchId(bytes32 indexed messageId); + + function expectDispatch( + TestPostDispatchHook firstHook, + TestPostDispatchHook hook, + bytes calldata metadata, + bytes calldata body + ) internal { + bytes memory message = mailbox.buildOutboundMessage( + remoteDomain, + recipientb32, + body + ); + expectHookQuote(firstHook, metadata, message); + expectHookPost(firstHook, metadata, message, firstHook.fee()); + expectHookPost(hook, metadata, message, hook.fee()); + vm.expectEmit(true, true, true, true, address(mailbox)); + emit Dispatch(address(this), remoteDomain, recipientb32, message); + vm.expectEmit(true, false, false, false, address(mailbox)); + emit DispatchId(message.id()); + } + + function test_dispatch( + uint8 n, + bytes calldata body, + bytes calldata metadata + ) public { + bytes calldata defaultMetadata = metadata[0:0]; + uint256 quote; + uint32 nonce; + bytes32 id; + + for (uint256 i = 0; i < n; i += 3) { + nonce = mailbox.nonce(); + assertEq(nonce, i); + + // default hook and no metadata + quote = mailbox.quoteDispatch(remoteDomain, recipientb32, body); + expectDispatch(requiredHook, defaultHook, defaultMetadata, body); + id = mailbox.dispatch{value: quote}( + remoteDomain, + recipientb32, + body + ); + assertEq(mailbox.latestDispatchedId(), id); + nonce = mailbox.nonce(); + assertEq(nonce, i + 1); + + // default hook with metadata + quote = mailbox.quoteDispatch( + remoteDomain, + recipientb32, + body, + metadata + ); + expectDispatch(requiredHook, defaultHook, metadata, body); + id = mailbox.dispatch{value: quote}( + remoteDomain, + recipientb32, + body, + metadata + ); + assertEq(mailbox.latestDispatchedId(), id); + nonce = mailbox.nonce(); + assertEq(nonce, i + 2); + + // override default hook with metadata + quote = mailbox.quoteDispatch( + remoteDomain, + recipientb32, + body, + metadata, + overrideHook + ); + expectDispatch(requiredHook, overrideHook, metadata, body); + id = mailbox.dispatch{value: quote}( + remoteDomain, + recipientb32, + body, + metadata, + overrideHook + ); + assertEq(mailbox.latestDispatchedId(), id); + nonce = mailbox.nonce(); + assertEq(nonce, i + 3); + } + } + + // for instrumenting gas costs of merkleHook.postDispatch after several insertions + function test_100dispatch_withMerkleTreeHook(bytes calldata body) public { + uint256 quote = mailbox.quoteDispatch( + remoteDomain, + recipientb32, + body, + body[0:0], + merkleHook + ); + for (uint256 i = 0; i < 100; i++) { + mailbox.dispatch{value: quote}( + remoteDomain, + recipientb32, + body, + body[0:0], + merkleHook + ); + } + } + + event ProcessId(bytes32 indexed messageId); + + event Process( + uint32 indexed origin, + bytes32 indexed sender, + address indexed recipient + ); + + function expectProcess( + bytes calldata metadata, + bytes memory message, + bytes calldata body, + uint256 value + ) internal { + bytes32 sender = msg.sender.addressToBytes32(); + IInterchainSecurityModule ism = mailbox.recipientIsm( + address(recipient) + ); + vm.expectEmit(true, true, true, false, address(mailbox)); + emit Process(remoteDomain, sender, address(recipient)); + vm.expectEmit(true, false, false, false, address(mailbox)); + emit ProcessId(message.id()); + vm.expectCall( + address(ism), + abi.encodeCall(ism.verify, (metadata, message)) + ); + vm.expectCall( + address(recipient), + value, + abi.encodeCall(recipient.handle, (remoteDomain, sender, body)) + ); + } + + function test_process( + bytes calldata body, + bytes calldata metadata, + uint256 value + ) public { + vm.assume(value < address(this).balance); + bytes memory message = mailbox.buildInboundMessage( + remoteDomain, + recipientb32, + msg.sender.addressToBytes32(), + body + ); + bytes32 id = keccak256(message); + assertEq(mailbox.delivered(id), false); + expectProcess(metadata, message, body, value); + mailbox.process{value: value}(metadata, message); + assertEq(mailbox.delivered(id), true); + assertEq(mailbox.processor(id), address(this)); + assertEq(mailbox.processedAt(id), uint48(block.timestamp)); + } + + function test_process_revertsWhenAlreadyDelivered() public { + bytes memory message = mailbox.buildInboundMessage( + remoteDomain, + recipientb32, + address(this).addressToBytes32(), + "0x" + ); + mailbox.process("", message); + vm.expectRevert("Mailbox: already delivered"); + mailbox.process("", message); + } + + function test_process_revertsWhenBadVersion(bytes calldata body) public { + bytes memory message = Message.formatMessage( + VERSION + 1, + 0, + localDomain, + address(this).addressToBytes32(), + remoteDomain, + recipientb32, + body + ); + vm.expectRevert("Mailbox: bad version"); + mailbox.process("", message); + } + + function test_process_revertsWhenBadDestination(bytes calldata body) + public + { + bytes memory message = Message.formatMessage( + VERSION, + 0, + remoteDomain, + address(this).addressToBytes32(), + remoteDomain, + recipientb32, + body + ); + vm.expectRevert("Mailbox: unexpected destination"); + mailbox.process("", message); + } + + function test_process_revertsWhenISMFails(bytes calldata body) public { + bytes memory message = mailbox.buildInboundMessage( + remoteDomain, + recipientb32, + msg.sender.addressToBytes32(), + body + ); + defaultIsm.setVerify(false); + vm.expectRevert("Mailbox: ISM verification failed"); + mailbox.process("", message); + } +} diff --git a/solidity/test/hooks/FallbackDomainRoutingHook.t.sol b/solidity/test/hooks/FallbackDomainRoutingHook.t.sol index c1a1301ac4..7af0fdad6c 100644 --- a/solidity/test/hooks/FallbackDomainRoutingHook.t.sol +++ b/solidity/test/hooks/FallbackDomainRoutingHook.t.sol @@ -5,7 +5,7 @@ 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 {TestMailbox} from "../../contracts/test/TestMailbox.sol"; import {ConfigFallbackDomainRoutingHook} from "../../contracts/hooks/ConfigFallbackDomainRoutingHook.sol"; import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; @@ -13,10 +13,10 @@ import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; contract FallbackDomainRoutingHookTest is Test { using TypeCasts for address; ConfigFallbackDomainRoutingHook internal fallbackHook; - TestPostDispatchHook internal configuredTestHook; + TestPostDispatchHook internal configuredTestPostDispatchHook; TestPostDispatchHook internal mailboxDefaultHook; TestRecipient internal testRecipient; - Mailbox internal mailbox; + TestMailbox internal mailbox; uint32 internal constant TEST_ORIGIN_DOMAIN = 1; uint32 internal constant TEST_DESTINATION_DOMAIN = 2; @@ -25,8 +25,8 @@ contract FallbackDomainRoutingHookTest is Test { event PostDispatchHookCalled(); function setUp() public { - mailbox = new Mailbox(TEST_ORIGIN_DOMAIN, address(this)); - configuredTestHook = new TestPostDispatchHook(); + mailbox = new TestMailbox(TEST_ORIGIN_DOMAIN); + configuredTestPostDispatchHook = new TestPostDispatchHook(); mailboxDefaultHook = new TestPostDispatchHook(); testRecipient = new TestRecipient(); fallbackHook = new ConfigFallbackDomainRoutingHook(address(mailbox)); @@ -40,12 +40,15 @@ contract FallbackDomainRoutingHookTest is Test { fallbackHook.setHook( TEST_DESTINATION_DOMAIN, address(testRecipient).addressToBytes32(), - configuredTestHook + configuredTestPostDispatchHook ); vm.expectCall( - address(configuredTestHook), - abi.encodeCall(configuredTestHook.quoteDispatch, ("", testMessage)) + address(configuredTestPostDispatchHook), + abi.encodeCall( + configuredTestPostDispatchHook.quoteDispatch, + ("", testMessage) + ) ); assertEq(fallbackHook.quoteDispatch("", testMessage), 25000); } @@ -64,12 +67,15 @@ contract FallbackDomainRoutingHookTest is Test { fallbackHook.setHook( TEST_DESTINATION_DOMAIN, address(testRecipient).addressToBytes32(), - configuredTestHook + configuredTestPostDispatchHook ); vm.expectCall( - address(configuredTestHook), - abi.encodeCall(configuredTestHook.postDispatch, ("", testMessage)) + address(configuredTestPostDispatchHook), + abi.encodeCall( + configuredTestPostDispatchHook.postDispatch, + ("", testMessage) + ) ); fallbackHook.postDispatch{value: msg.value}("", testMessage); } diff --git a/solidity/test/hyperlaneConnectionClient.test.ts b/solidity/test/hyperlaneConnectionClient.test.ts index e4ef4ab097..e037a5ed48 100644 --- a/solidity/test/hyperlaneConnectionClient.test.ts +++ b/solidity/test/hyperlaneConnectionClient.test.ts @@ -27,8 +27,9 @@ describe('HyperlaneConnectionClient', async () => { beforeEach(async () => { const mailboxFactory = new Mailbox__factory(signer); const domain = 1000; - mailbox = await mailboxFactory.deploy(domain, signer.address); - newMailbox = await mailboxFactory.deploy(domain, signer.address); + // TODO: fix + mailbox = await mailboxFactory.deploy(domain); + newMailbox = await mailboxFactory.deploy(domain); const connectionClientFactory = new TestHyperlaneConnectionClient__factory( signer, @@ -63,7 +64,7 @@ describe('HyperlaneConnectionClient', async () => { before(async () => { const paymasterFactory = new TestInterchainGasPaymaster__factory(signer); - newPaymaster = await paymasterFactory.deploy(signer.address); + newPaymaster = await paymasterFactory.deploy(); }); it('Allows owner to set the interchainGasPaymaster', async () => { diff --git a/solidity/test/igps/OverheadIgp.t.sol b/solidity/test/igps/OverheadIgp.t.sol index e01500adf9..0e2b3ab62d 100644 --- a/solidity/test/igps/OverheadIgp.t.sol +++ b/solidity/test/igps/OverheadIgp.t.sol @@ -24,7 +24,7 @@ contract OverheadIgpTest is Test { event DestinationGasOverheadSet(uint32 indexed domain, uint256 gasOverhead); function setUp() public { - innerIgp = new TestInterchainGasPaymaster(address(this)); + innerIgp = new TestInterchainGasPaymaster(); igp = new OverheadIgp(address(innerIgp)); } diff --git a/solidity/test/isms/ERC5164ISM.t.sol b/solidity/test/isms/ERC5164ISM.t.sol index fad9df6479..e932ea68f4 100644 --- a/solidity/test/isms/ERC5164ISM.t.sol +++ b/solidity/test/isms/ERC5164ISM.t.sol @@ -61,7 +61,7 @@ contract ERC5164ISMTest is Test { } function deployContracts() public { - srcMailbox = new TestMailbox(TEST1_DOMAIN, address(this)); + srcMailbox = new TestMailbox(TEST1_DOMAIN); ism = new ERC5164ISM(address(executor)); hook = new ERC5164Hook( address(srcMailbox), diff --git a/solidity/test/isms/MultisigIsm.t.sol b/solidity/test/isms/MultisigIsm.t.sol index 63f1309e80..f7a852eeef 100644 --- a/solidity/test/isms/MultisigIsm.t.sol +++ b/solidity/test/isms/MultisigIsm.t.sol @@ -12,6 +12,7 @@ import {StaticMOfNAddressSetFactory} from "../../contracts/libs/StaticMOfNAddres import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {MerkleTreeHook} from "../../contracts/hooks/MerkleTreeHook.sol"; import {TestMerkleTreeHook} from "../../contracts/test/TestMerkleTreeHook.sol"; +import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; import {Message} from "../../contracts/libs/Message.sol"; import {MOfNTestUtils} from "./IsmTestUtils.sol"; @@ -23,6 +24,7 @@ abstract contract AbstractMultisigIsmTest is Test { StaticMOfNAddressSetFactory factory; IMultisigIsm ism; TestMerkleTreeHook internal merkleTreeHook; + TestPostDispatchHook internal noopHook; TestMailbox mailbox; function metadataPrefix(bytes memory message) @@ -84,8 +86,7 @@ abstract contract AbstractMultisigIsmTest is Test { uint8 version = mailbox.VERSION(); uint32 origin = mailbox.localDomain(); bytes32 sender = TypeCasts.addressToBytes32(address(this)); - uint32 nonce = mailbox.nonce(); - mailbox.dispatch(destination, recipient, body); + uint32 nonce = merkleTreeHook.count(); bytes memory message = Message.formatMessage( version, nonce, @@ -95,6 +96,7 @@ abstract contract AbstractMultisigIsmTest is Test { recipient, body ); + merkleTreeHook.insert(message.id()); return message; } @@ -135,10 +137,12 @@ contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest { using Message for bytes; function setUp() public { - mailbox = new TestMailbox(ORIGIN, address(this)); + mailbox = new TestMailbox(ORIGIN); merkleTreeHook = new TestMerkleTreeHook(address(mailbox)); + noopHook = new TestPostDispatchHook(); factory = new StaticMerkleRootMultisigIsmFactory(); mailbox.setDefaultHook(address(merkleTreeHook)); + mailbox.setRequiredHook(address(noopHook)); } function metadataPrefix(bytes memory message) @@ -163,10 +167,13 @@ contract MessageIdMultisigIsmTest is AbstractMultisigIsmTest { using Message for bytes; function setUp() public { - mailbox = new TestMailbox(ORIGIN, address(this)); + mailbox = new TestMailbox(ORIGIN); merkleTreeHook = new TestMerkleTreeHook(address(mailbox)); + noopHook = new TestPostDispatchHook(); + factory = new StaticMessageIdMultisigIsmFactory(); mailbox.setDefaultHook(address(merkleTreeHook)); + mailbox.setRequiredHook(address(noopHook)); } function metadataPrefix(bytes memory) diff --git a/solidity/test/isms/OPStackIsm.t.sol b/solidity/test/isms/OPStackIsm.t.sol index dae29a6516..79945200bd 100644 --- a/solidity/test/isms/OPStackIsm.t.sol +++ b/solidity/test/isms/OPStackIsm.t.sol @@ -93,7 +93,7 @@ contract OPStackIsmTest is Test { vm.selectFork(mainnetFork); l1Messenger = ICrossDomainMessenger(L1_MESSENGER_ADDRESS); - l1Mailbox = new TestMailbox(MAINNET_DOMAIN, address(this)); + l1Mailbox = new TestMailbox(MAINNET_DOMAIN); opHook = new OPStackHook( address(l1Mailbox), diff --git a/solidity/test/isms/legacyMultisigIsm.test.ts b/solidity/test/isms/legacyMultisigIsm.test.ts index 4032b7b18e..d45f332b63 100644 --- a/solidity/test/isms/legacyMultisigIsm.test.ts +++ b/solidity/test/isms/legacyMultisigIsm.test.ts @@ -8,12 +8,15 @@ import { Validator, types, utils } from '@hyperlane-xyz/utils'; import domainHashTestCases from '../../../vectors/domainHash.json'; import { LightTestRecipient__factory, + TestIsm__factory, TestLegacyMultisigIsm, TestLegacyMultisigIsm__factory, TestMailbox, TestMailbox__factory, TestMerkleTreeHook, TestMerkleTreeHook__factory, + TestPostDispatchHook, + TestPostDispatchHook__factory, TestRecipient__factory, } from '../../types'; import { @@ -30,6 +33,7 @@ describe('LegacyMultisigIsm', async () => { let multisigIsm: TestLegacyMultisigIsm, mailbox: TestMailbox, defaultHook: TestMerkleTreeHook, + requiredHook: TestPostDispatchHook, signer: SignerWithAddress, nonOwner: SignerWithAddress, validators: Validator[]; @@ -38,10 +42,18 @@ describe('LegacyMultisigIsm', async () => { const signers = await ethers.getSigners(); [signer, nonOwner] = signers; const mailboxFactory = new TestMailbox__factory(signer); - mailbox = await mailboxFactory.deploy(ORIGIN_DOMAIN, signer.address); + mailbox = await mailboxFactory.deploy(ORIGIN_DOMAIN); const defaultHookFactory = new TestMerkleTreeHook__factory(signer); defaultHook = await defaultHookFactory.deploy(mailbox.address); - await mailbox.setDefaultHook(defaultHook.address); + requiredHook = await new TestPostDispatchHook__factory(signer).deploy(); + await requiredHook.setFee(0); + const testIsm = await new TestIsm__factory(signer).deploy(); + mailbox.initialize( + signer.address, + testIsm.address, + defaultHook.address, + requiredHook.address, + ); validators = await Promise.all( signers .filter((_, i) => i > 1) @@ -52,6 +64,7 @@ describe('LegacyMultisigIsm', async () => { beforeEach(async () => { const multisigIsmFactory = new TestLegacyMultisigIsm__factory(signer); multisigIsm = await multisigIsmFactory.deploy(); + await mailbox.setDefaultIsm(multisigIsm.address); }); describe('#constructor', () => { @@ -406,9 +419,13 @@ describe('LegacyMultisigIsm', async () => { const mailboxFactory = new TestMailbox__factory(signer); const destinationMailbox = await mailboxFactory.deploy( DESTINATION_DOMAIN, + ); + await destinationMailbox.initialize( signer.address, + multisigIsm.address, + defaultHook.address, + requiredHook.address, ); - await destinationMailbox.setDefaultIsm(multisigIsm.address); await destinationMailbox.process(metadata, message); }); @@ -530,10 +547,9 @@ describe('LegacyMultisigIsm', async () => { await multisigIsm.setThreshold(ORIGIN_DOMAIN, threshold); - // TODO: fix - const maxBodySize = await mailbox.MAX_MESSAGE_BODY_BYTES(); + const maxBodySize = 2 ** 16 - 1; // The max body is used to estimate an upper bound on gas usage. - const maxBody = '0x' + 'AA'.repeat(maxBodySize.toNumber()); + const maxBody = '0x' + 'AA'.repeat(maxBodySize); ({ message, metadata } = await dispatchMessageAndReturnMetadata( mailbox, @@ -550,9 +566,13 @@ describe('LegacyMultisigIsm', async () => { const mailboxFactory = new TestMailbox__factory(signer); const destinationMailbox = await mailboxFactory.deploy( DESTINATION_DOMAIN, + ); + await destinationMailbox.initialize( signer.address, + multisigIsm.address, + defaultHook.address, + requiredHook.address, ); - await destinationMailbox.setDefaultIsm(multisigIsm.address); const gas = await destinationMailbox.estimateGas.process( metadata, message, diff --git a/solidity/test/mailbox.test.ts b/solidity/test/mailbox.test.ts deleted file mode 100644 index e1ace94c4f..0000000000 --- a/solidity/test/mailbox.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { expect } from 'chai'; -import { BigNumber } from 'ethers'; -import { ethers } from 'hardhat'; - -import { utils } from '@hyperlane-xyz/utils'; - -import { - BadRecipient1__factory, - BadRecipient2__factory, - BadRecipient3__factory, - BadRecipient5__factory, - BadRecipient6__factory, - TestMailbox, - TestMailbox__factory, - TestMerkleTreeHook, - TestMerkleTreeHook__factory, - TestMultisigIsm, - TestMultisigIsm__factory, - TestRecipient, - TestRecipient__factory, -} from '../types'; - -import { inferMessageValues } from './lib/mailboxes'; - -const originDomain = 1000; -const destDomain = 2000; -const ONLY_OWNER_REVERT_MSG = 'Ownable: caller is not the owner'; - -describe('Mailbox', async () => { - let mailbox: TestMailbox, - defaultHook: TestMerkleTreeHook, - module: TestMultisigIsm, - signer: SignerWithAddress, - nonOwner: SignerWithAddress, - beforeBlock: number; - - beforeEach(async () => { - [signer, nonOwner] = await ethers.getSigners(); - const moduleFactory = new TestMultisigIsm__factory(signer); - module = await moduleFactory.deploy(); - const mailboxFactory = new TestMailbox__factory(signer); - mailbox = await mailboxFactory.deploy(originDomain, signer.address); - beforeBlock = mailbox.deployTransaction.blockNumber!; - const defaultHookFactory = new TestMerkleTreeHook__factory(signer); - defaultHook = await defaultHookFactory.deploy(mailbox.address); - await mailbox.setDefaultIsm(module.address); - await mailbox.setDefaultHook(defaultHook.address); - }); - - it('#deployedBlock', async () => { - const block = await mailbox.deployedBlock(); - expect(block).to.equal(beforeBlock); - }); - - it('#VERSION', async () => { - const version = await mailbox.VERSION(); - expect(version).to.equal(3); - }); - - describe('#initialize', () => { - it('Sets the owner', async () => { - const mailboxFactory = new TestMailbox__factory(signer); - mailbox = await mailboxFactory.deploy(originDomain, nonOwner.address); - const expectedOwner = nonOwner.address; - - await mailbox.connect(nonOwner).setDefaultIsm(module.address); - const owner = await mailbox.owner(); - expect(owner).equals(expectedOwner); - }); - }); - - describe('#dispatch', () => { - let recipient: SignerWithAddress, message: string, id: string, body: string; - before(async () => { - [, recipient] = await ethers.getSigners(); - ({ message, id, body } = await inferMessageValues( - mailbox, - signer.address, - destDomain, - recipient.address, - 'message', - )); - }); - - it('Dispatches a message', async () => { - // Send message with signer address as msg.sender - const recipientBytes = utils.addressToBytes32(recipient.address); - await expect( - mailbox - .connect(signer) - ['dispatch(uint32,bytes32,bytes)'](destDomain, recipientBytes, body), - ) - .to.emit(mailbox, 'Dispatch') - .withArgs(message) - .to.emit(mailbox, 'DispatchId') - .withArgs(utils.messageId(message)); - }); - - it('Returns the id of the dispatched message', async () => { - const actualId = await mailbox - .connect(signer) - .callStatic['dispatch(uint32,bytes32,bytes)']( - destDomain, - utils.addressToBytes32(recipient.address), - body, - ); - - expect(actualId).equals(id); - }); - }); - - describe('#recipientIsm', () => { - let recipient: TestRecipient; - beforeEach(async () => { - const recipientF = new TestRecipient__factory(signer); - recipient = await recipientF.deploy(); - }); - - it('Returns the default module when unspecified', async () => { - expect(await mailbox.recipientIsm(recipient.address)).to.equal( - await mailbox.defaultIsm(), - ); - }); - - it('Returns the recipient module when specified', async () => { - const recipientIsm = mailbox.address; - await recipient.setInterchainSecurityModule(recipientIsm); - expect(await mailbox.recipientIsm(recipient.address)).to.equal( - recipientIsm, - ); - }); - }); - - describe('#process', () => { - const badRecipientFactories = [ - BadRecipient1__factory, - BadRecipient2__factory, - BadRecipient3__factory, - BadRecipient5__factory, - BadRecipient6__factory, - ]; - let message: string, id: string, recipient: string; - - beforeEach(async () => { - await module.setAccept(true); - const recipientF = new TestRecipient__factory(signer); - recipient = utils.addressToBytes32((await recipientF.deploy()).address); - ({ message, id } = await inferMessageValues( - mailbox, - signer.address, - originDomain, - recipient, - 'message', - )); - }); - - it('processes a message', async () => { - await expect(mailbox.process('0x', message)) - .to.emit(mailbox, 'Process') - .withArgs( - originDomain, - utils.addressToBytes32(signer.address), - recipient, - ) - .to.emit(mailbox, 'ProcessId') - .withArgs(id); - expect(await mailbox.delivered(id)).to.be.true; - }); - - it('Rejects an already-processed message', async () => { - await expect(mailbox.process('0x', message)).to.emit(mailbox, 'Process'); - - // Try to process message again - await expect(mailbox.process('0x', message)).to.be.revertedWith( - 'delivered', - ); - }); - - it('Fails to process message when rejected by module', async () => { - await module.setAccept(false); - await expect(mailbox.process('0x', message)).to.be.revertedWith( - 'verification failed', - ); - }); - - for (let i = 0; i < badRecipientFactories.length; i++) { - it(`Fails to process a message for a badly implemented recipient (${ - i + 1 - })`, async () => { - const factory = new badRecipientFactories[i](signer); - const badRecipient = await factory.deploy(); - - ({ message } = await inferMessageValues( - mailbox, - signer.address, - originDomain, - badRecipient.address, - 'message', - )); - await expect(mailbox.process('0x', message)).to.be.reverted; - }); - } - - // TODO: Fails to process with wrong version.. - it('Fails to process message with wrong destination Domain', async () => { - ({ message } = await inferMessageValues( - mailbox, - signer.address, - originDomain + 1, - recipient, - 'message', - )); - - await expect(mailbox.process('0x', message)).to.be.revertedWith( - 'Mailbox: unexpected destination', - ); - }); - - it('Fails to process message with wrong version', async () => { - const version = await mailbox.VERSION(); - ({ message } = await inferMessageValues( - mailbox, - signer.address, - originDomain, - recipient, - 'message', - version + 1, - )); - await expect(mailbox.process('0x', message)).to.be.revertedWith( - 'Mailbox: bad version', - ); - }); - - it('Fails to process message sent to a non-existent contract address', async () => { - ({ message } = await inferMessageValues( - mailbox, - signer.address, - originDomain, - '0x1234567890123456789012345678901234567890', // non-existent contract address - 'message', - )); - await expect(mailbox.process('0x', message)).to.be.reverted; - }); - }); - - describe('#setDefaultIsm', async () => { - let newIsm: TestMultisigIsm; - before(async () => { - const moduleFactory = new TestMultisigIsm__factory(signer); - newIsm = await moduleFactory.deploy(); - }); - - it('Allows owner to update the default ISM', async () => { - await expect(mailbox.setDefaultIsm(newIsm.address)) - .to.emit(mailbox, 'DefaultIsmSet') - .withArgs(newIsm.address); - expect(await mailbox.defaultIsm()).to.equal(newIsm.address); - }); - - it('Does not allow non-owner to update the default ISM', async () => { - await expect( - mailbox.connect(nonOwner).setDefaultIsm(newIsm.address), - ).to.be.revertedWith(ONLY_OWNER_REVERT_MSG); - }); - - it('Reverts if the provided ISM is not a contract', async () => { - await expect(mailbox.setDefaultIsm(signer.address)).to.be.revertedWith( - 'Mailbox: !contract', - ); - }); - }); -}); diff --git a/solidity/test/mockMailbox.test.ts b/solidity/test/mockMailbox.test.ts index a5c435a4a2..03a77e2680 100644 --- a/solidity/test/mockMailbox.test.ts +++ b/solidity/test/mockMailbox.test.ts @@ -23,7 +23,7 @@ describe('MockMailbox', function () { const body = ethers.utils.toUtf8Bytes('This is a test message'); - await originMailbox.dispatch( + await originMailbox['dispatch(uint32,bytes32,bytes)']( DESTINATION_DOMAIN, utils.addressToBytes32(recipient.address), body, diff --git a/solidity/test/router.test.ts b/solidity/test/router.test.ts index 7154e77110..f47c15e983 100644 --- a/solidity/test/router.test.ts +++ b/solidity/test/router.test.ts @@ -9,17 +9,14 @@ import { utils } from '@hyperlane-xyz/utils'; import { TestInterchainGasPaymaster, TestInterchainGasPaymaster__factory, + TestIsm__factory, TestMailbox, TestMailbox__factory, - TestMerkleTreeHook, TestMerkleTreeHook__factory, - TestMultisigIsm__factory, TestRouter, TestRouter__factory, } from '../types'; -import { inferMessageValues } from './lib/mailboxes'; - const ONLY_OWNER_REVERT_MSG = 'Ownable: caller is not the owner'; const origin = 1; const destination = 2; @@ -34,10 +31,10 @@ interface GasPaymentParams { refundAddress: string; } -describe('Router', async () => { +// TODO: update for v3 +describe.skip('Router', async () => { let router: TestRouter, mailbox: TestMailbox, - defaultHook: TestMerkleTreeHook, igp: TestInterchainGasPaymaster, signer: SignerWithAddress, nonOwner: SignerWithAddress; @@ -48,53 +45,45 @@ describe('Router', async () => { beforeEach(async () => { const mailboxFactory = new TestMailbox__factory(signer); - mailbox = await mailboxFactory.deploy(origin, signer.address); - const defaultHookFactory = new TestMerkleTreeHook__factory(signer); - defaultHook = await defaultHookFactory.deploy(mailbox.address); - await mailbox.setDefaultHook(defaultHook.address); + mailbox = await mailboxFactory.deploy(origin); igp = await new TestInterchainGasPaymaster__factory(signer).deploy( + nonOwner.address, + ); + const requiredHook = await new TestMerkleTreeHook__factory(signer).deploy( + mailbox.address, + ); + const defaultIsm = await new TestIsm__factory(signer).deploy(); + await mailbox.initialize( signer.address, + defaultIsm.address, + igp.address, + requiredHook.address, ); router = await new TestRouter__factory(signer).deploy(); }); describe('#initialize', () => { it('should set the mailbox', async () => { - await router.initialize(mailbox.address, igp.address); + await router.initialize(mailbox.address); expect(await router.mailbox()).to.equal(mailbox.address); }); - it('should set the IGP', async () => { - await router.initialize(mailbox.address, igp.address); - expect(await router.interchainGasPaymaster()).to.equal(igp.address); - }); - it('should transfer owner to deployer', async () => { - await router.initialize(mailbox.address, igp.address); + await router.initialize(mailbox.address); expect(await router.owner()).to.equal(signer.address); }); - it('should use overloaded initialize', async () => { - await expect(router.initialize(mailbox.address, igp.address)).to.emit( - router, - 'InitializeOverload', - ); - }); - it('cannot be initialized twice', async () => { - await router.initialize(mailbox.address, igp.address); - await expect( - router.initialize(mailbox.address, igp.address), - ).to.be.revertedWith('Initializable: contract is already initialized'); + await router.initialize(mailbox.address); + await expect(router.initialize(mailbox.address)).to.be.revertedWith( + 'Initializable: contract is already initialized', + ); }); }); describe('when initialized', () => { beforeEach(async () => { - await router.initialize(mailbox.address, igp.address); - const ism = await new TestMultisigIsm__factory(signer).deploy(); - await ism.setAccept(true); - await mailbox.setDefaultIsm(ism.address); + await router.initialize(mailbox.address); }); it('accepts message from enrolled mailbox and router', async () => { @@ -170,6 +159,8 @@ describe('Router', async () => { }); describe('dispatch functions', () => { + let payment: BigNumberish; + beforeEach(async () => { // Enroll a remote router on the destination domain. // The address is arbitrary because no messages will actually be processed. @@ -177,90 +168,71 @@ describe('Router', async () => { destination, utils.addressToBytes32(nonOwner.address), ); + const recipient = utils.addressToBytes32(router.address); + payment = await mailbox.quoteDispatch(destination, recipient, body); }); - // Helper for testing different variations of dispatch functions - const runDispatchFunctionTests = async ( - dispatchFunction: ( - destinationDomain: number, - gasPaymentParams: GasPaymentParams, - ) => Promise, - expectGasPayment: boolean, - ) => { - // Allows a Chai Assertion to be programmatically negated - const expectAssertion = ( - assertion: Chai.Assertion, - expected: boolean, - ) => { - return expected ? assertion : assertion.not; - }; - - const testGasPaymentParams: GasPaymentParams = { - gasAmount: 4321, - payment: 43210, - refundAddress: '0xc0ffee0000000000000000000000000000000000', - }; - + describe('#dispatch', () => { it('dispatches a message', async () => { await expect( - dispatchFunction(destination, testGasPaymentParams), + router.dispatch(destination, body, { value: payment }), ).to.emit(mailbox, 'Dispatch'); }); - it(`${ - expectGasPayment ? 'pays' : 'does not pay' - } interchain gas`, async () => { - const { id } = await inferMessageValues( - mailbox, - router.address, - destination, - await router.routers(destination), - '', - ); - const assertion = expectAssertion( - expect(dispatchFunction(destination, testGasPaymentParams)).to, - expectGasPayment, - ); - await assertion - .emit(igp, 'GasPayment') - .withArgs( - id, - testGasPaymentParams.gasAmount, - testGasPaymentParams.payment, - ); + it('reverts on insufficient payment', async () => { + await expect( + router.dispatch(destination, body, { value: payment.sub(1) }), + ).to.be.revertedWith('insufficient interchain gas payment'); }); it('reverts when dispatching a message to an unenrolled remote router', async () => { await expect( - dispatchFunction(destinationWithoutRouter, testGasPaymentParams), + router.dispatch(destinationWithoutRouter, body), ).to.be.revertedWith( `No router enrolled for domain. Did you specify the right domain ID?`, ); }); - }; - - describe('#dispatch', () => { - runDispatchFunctionTests( - (destinationDomain) => router.dispatch(destinationDomain, '0x'), - false, - ); }); describe('#dispatchWithGas', () => { - runDispatchFunctionTests( - (destinationDomain, gasPaymentParams) => + const testGasPaymentParams = { + gasAmount: 4321, + payment: 43210, + refundAddress: '0xc0ffee0000000000000000000000000000000000', + }; + + it('dispatches a message', async () => { + await expect( router.dispatchWithGas( - destinationDomain, - '0x', - gasPaymentParams.gasAmount, - gasPaymentParams.payment, - gasPaymentParams.refundAddress, - { - value: gasPaymentParams.payment, - }, + destination, + body, + testGasPaymentParams.gasAmount, + testGasPaymentParams.payment, + testGasPaymentParams.refundAddress, + { value: testGasPaymentParams.payment }, ), - true, - ); + ).to.emit(mailbox, 'Dispatch'); + }); + + it('uses custom igp metadata', async () => { + const tx = await router.dispatchWithGas( + destination, + body, + testGasPaymentParams.gasAmount, + testGasPaymentParams.payment, + testGasPaymentParams.refundAddress, + { value: testGasPaymentParams.payment }, + ); + + const messageId = await mailbox.latestDispatchedId(); + const required = await igp.quoteGasPayment( + destination, + testGasPaymentParams.gasAmount, + ); + expect(tx) + .to.emit(igp, 'GasPayment') + .withArgs(messageId, testGasPaymentParams.gasAmount, required); + }); }); }); }); From 7dd2190848a219f31642f4f9e05d6b57416bb294 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Wed, 13 Sep 2023 00:57:18 -0400 Subject: [PATCH 23/27] Warp route changes for v3 (#2721) ### Description - V3 compatible ERC20, ERC721 and all their extensions ### Drive-by changes - Painstakingly migrating hardhat tests to foundry tests for all variants ### Related issues - fixes https://github.com/hyperlane-xyz/issues/issues/577 ### Backward compatibility Yes ### Testing whole lotta unit tests and poor man's version of integration tests --------- Signed-off-by: -f Co-authored-by: Yorke Rhodes --- .gitmodules | 3 + README.md | 4 +- typescript/token/contracts/HypERC20.sol | 18 +- .../token/contracts/HypERC20Collateral.sol | 18 +- typescript/token/contracts/HypERC721.sol | 19 +- .../token/contracts/HypERC721Collateral.sol | 22 +- typescript/token/contracts/HypNative.sol | 18 +- .../extensions/HypERC721URIStorage.sol | 9 + .../token/contracts/libs/TokenRouter.sol | 7 + typescript/token/lib/forge-std | 1 + typescript/token/package.json | 6 +- typescript/token/src/deploy.ts | 11 +- typescript/token/test/HypERC20.t.sol | 369 ++++++++++++++++++ typescript/token/test/HypERC721.t.sol | 369 ++++++++++++++++++ typescript/token/test/erc20.test.ts | 291 -------------- typescript/token/test/erc721.test.ts | 324 --------------- 16 files changed, 818 insertions(+), 671 deletions(-) create mode 160000 typescript/token/lib/forge-std create mode 100644 typescript/token/test/HypERC20.t.sol create mode 100644 typescript/token/test/HypERC721.t.sol delete mode 100644 typescript/token/test/erc20.test.ts delete mode 100644 typescript/token/test/erc721.test.ts diff --git a/.gitmodules b/.gitmodules index 3077b9e20f..a8fce55006 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "solidity/lib/forge-std"] path = solidity/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "typescript/token/lib/forge-std"] + path = typescript/token/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md index 68830a86db..36566fc960 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ ## Versioning -Note this is the branch for Hyperlane v2. +Note this is the branch for Hyperlane v3. + +V2 is on the main branch but is eventually being phased out. V1 has since been deprecated in favor of V2, but if you are looking for code relating to the existing V1 deployments of the `testnet2` or `mainnet` environments, refer to the [v1](https://github.com/hyperlane-xyz/hyperlane-monorepo/tree/v1) branch. diff --git a/typescript/token/contracts/HypERC20.sol b/typescript/token/contracts/HypERC20.sol index 4801592775..8b765f5724 100644 --- a/typescript/token/contracts/HypERC20.sol +++ b/typescript/token/contracts/HypERC20.sol @@ -20,23 +20,18 @@ contract HypERC20 is ERC20Upgradeable, TokenRouter { /** * @notice Initializes the Hyperlane router, ERC20 metadata, and mints initial supply to deployer. * @param _mailbox The address of the mailbox contract. - * @param _interchainGasPaymaster The address of the interchain gas paymaster contract. * @param _totalSupply The initial supply of the token. * @param _name The name of the token. * @param _symbol The symbol of the token. */ function initialize( address _mailbox, - address _interchainGasPaymaster, uint256 _totalSupply, string memory _name, string memory _symbol ) external initializer { - // transfers ownership to `msg.sender` - __HyperlaneConnectionClient_initialize( - _mailbox, - _interchainGasPaymaster - ); + // initialize router + __Router_initialize(_mailbox); // Initialize ERC20 metadata __ERC20_init(_name, _symbol); @@ -47,6 +42,15 @@ contract HypERC20 is ERC20Upgradeable, TokenRouter { return _decimals; } + function balanceOf(address _account) + public + view + override(TokenRouter, ERC20Upgradeable) + returns (uint256) + { + return ERC20Upgradeable.balanceOf(_account); + } + /** * @dev Burns `_amount` of token from `msg.sender` balance. * @inheritdoc TokenRouter diff --git a/typescript/token/contracts/HypERC20Collateral.sol b/typescript/token/contracts/HypERC20Collateral.sol index 50c9889bc9..2e9cb4966c 100644 --- a/typescript/token/contracts/HypERC20Collateral.sol +++ b/typescript/token/contracts/HypERC20Collateral.sol @@ -27,19 +27,17 @@ contract HypERC20Collateral is TokenRouter { /** * @notice Initializes the Hyperlane router. * @param _mailbox The address of the mailbox contract. - * @param _interchainGasPaymaster The address of the interchain gas paymaster contract. */ - function initialize(address _mailbox, address _interchainGasPaymaster) - external - initializer - { - __HyperlaneConnectionClient_initialize( - _mailbox, - _interchainGasPaymaster - ); + function initialize(address _mailbox) external initializer { + __Router_initialize(_mailbox); } - function balanceOf(address _account) external view returns (uint256) { + function balanceOf(address _account) + external + view + override + returns (uint256) + { return wrappedToken.balanceOf(_account); } diff --git a/typescript/token/contracts/HypERC721.sol b/typescript/token/contracts/HypERC721.sol index 0effbcaaaa..24aa971cdf 100644 --- a/typescript/token/contracts/HypERC721.sol +++ b/typescript/token/contracts/HypERC721.sol @@ -3,6 +3,8 @@ pragma solidity >=0.8.0; import {TokenRouter} from "./libs/TokenRouter.sol"; +import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; import {ERC721EnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; /** @@ -13,23 +15,18 @@ contract HypERC721 is ERC721EnumerableUpgradeable, TokenRouter { /** * @notice Initializes the Hyperlane router, ERC721 metadata, and mints initial supply to deployer. * @param _mailbox The address of the mailbox contract. - * @param _interchainGasPaymaster The address of the interchain gas paymaster contract. * @param _mintAmount The amount of NFTs to mint to `msg.sender`. * @param _name The name of the token. * @param _symbol The symbol of the token. */ function initialize( address _mailbox, - address _interchainGasPaymaster, uint256 _mintAmount, string memory _name, string memory _symbol ) external initializer { // transfers ownership to `msg.sender` - __HyperlaneConnectionClient_initialize( - _mailbox, - _interchainGasPaymaster - ); + __Router_initialize(_mailbox); __ERC721_init(_name, _symbol); for (uint256 i = 0; i < _mintAmount; i++) { @@ -37,6 +34,16 @@ contract HypERC721 is ERC721EnumerableUpgradeable, TokenRouter { } } + function balanceOf(address _account) + public + view + virtual + override(TokenRouter, ERC721Upgradeable, IERC721Upgradeable) + returns (uint256) + { + return ERC721Upgradeable.balanceOf(_account); + } + /** * @dev Asserts `msg.sender` is owner and burns `_tokenId`. * @inheritdoc TokenRouter diff --git a/typescript/token/contracts/HypERC721Collateral.sol b/typescript/token/contracts/HypERC721Collateral.sol index a3f5a61601..c9d876c5e0 100644 --- a/typescript/token/contracts/HypERC721Collateral.sol +++ b/typescript/token/contracts/HypERC721Collateral.sol @@ -28,19 +28,21 @@ contract HypERC721Collateral is TokenRouter { /** * @notice Initializes the Hyperlane router. * @param _mailbox The address of the mailbox contract. - * @param _interchainGasPaymaster The address of the interchain gas paymaster contract. */ - function initialize(address _mailbox, address _interchainGasPaymaster) - external - initializer - { - __HyperlaneConnectionClient_initialize( - _mailbox, - _interchainGasPaymaster - ); + function initialize(address _mailbox) external initializer { + __Router_initialize(_mailbox); } - function balanceOf(address _account) external view returns (uint256) { + /** + * @dev Returns the balance of `_account` for `wrappedToken`. + * @inheritdoc TokenRouter + */ + function balanceOf(address _account) + external + view + override + returns (uint256) + { return IERC721(wrappedToken).balanceOf(_account); } diff --git a/typescript/token/contracts/HypNative.sol b/typescript/token/contracts/HypNative.sol index e4291fef72..121d5a269a 100644 --- a/typescript/token/contracts/HypNative.sol +++ b/typescript/token/contracts/HypNative.sol @@ -14,17 +14,10 @@ contract HypNative is TokenRouter { /** * @notice Initializes the Hyperlane router, ERC20 metadata, and mints initial supply to deployer. * @param _mailbox The address of the mailbox contract. - * @param _interchainGasPaymaster The address of the interchain gas paymaster contract. */ - function initialize(address _mailbox, address _interchainGasPaymaster) - external - initializer - { + function initialize(address _mailbox) external initializer { // transfers ownership to `msg.sender` - __HyperlaneConnectionClient_initialize( - _mailbox, - _interchainGasPaymaster - ); + __Router_initialize(_mailbox); } /** @@ -47,7 +40,12 @@ contract HypNative is TokenRouter { emit SentTransferRemote(_destination, _recipient, _amount); } - function balanceOf(address _account) external view returns (uint256) { + function balanceOf(address _account) + external + view + override + returns (uint256) + { return _account.balance; } diff --git a/typescript/token/contracts/extensions/HypERC721URIStorage.sol b/typescript/token/contracts/extensions/HypERC721URIStorage.sol index c921a00297..7c45b7b36c 100644 --- a/typescript/token/contracts/extensions/HypERC721URIStorage.sol +++ b/typescript/token/contracts/extensions/HypERC721URIStorage.sol @@ -12,6 +12,15 @@ import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC72 * @author Abacus Works */ contract HypERC721URIStorage is HypERC721, ERC721URIStorageUpgradeable { + function balanceOf(address account) + public + view + override(HypERC721, ERC721Upgradeable) + returns (uint256) + { + return HypERC721.balanceOf(account); + } + /** * @return _tokenURI The URI of `_tokenId`. * @inheritdoc HypERC721 diff --git a/typescript/token/contracts/libs/TokenRouter.sol b/typescript/token/contracts/libs/TokenRouter.sol index c072d88b71..8b100ecfe5 100644 --- a/typescript/token/contracts/libs/TokenRouter.sol +++ b/typescript/token/contracts/libs/TokenRouter.sol @@ -72,6 +72,13 @@ abstract contract TokenRouter is GasRouter { virtual returns (bytes memory metadata); + /** + * @notice Returns the balance of `account` on this token router. + * @param account The address to query the balance of. + * @return The balance of `account`. + */ + function balanceOf(address account) external virtual returns (uint256); + /** * @dev Mints tokens to recipient when router receives transfer message. * @dev Emits `ReceivedTransferRemote` event on the destination chain. diff --git a/typescript/token/lib/forge-std b/typescript/token/lib/forge-std new file mode 160000 index 0000000000..1d9650e951 --- /dev/null +++ b/typescript/token/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 1d9650e951204a0ddce9ff89c32f1997984cef4d diff --git a/typescript/token/package.json b/typescript/token/package.json index 3c6c070ed1..eff1a69f29 100644 --- a/typescript/token/package.json +++ b/typescript/token/package.json @@ -54,11 +54,11 @@ "scripts": { "clean": "hardhat clean && rm -rf dist cache src/types && forge clean", "docs": "forge doc", - "build": "hardhat compile && tsc", - "coverage": "hardhat coverage", + "build": "forge build", + "coverage": "forge coverage --report lcov", "lint": "solhint contracts/**/*.sol && eslint . --ext .ts", "prettier": "prettier --write ./contracts ./test", - "test": "hardhat test ./test/*.test.ts", + "test": "forge test -vvv", "deploy-warp-route": "DEBUG=* ts-node scripts/deploy" }, "types": "dist/index.d.ts" diff --git a/typescript/token/src/deploy.ts b/typescript/token/src/deploy.ts index 7b2daf1867..3f996b2e41 100644 --- a/typescript/token/src/deploy.ts +++ b/typescript/token/src/deploy.ts @@ -128,10 +128,7 @@ export class HypERC20Deployer extends GasRouterDeployer< 'HypERC20Collateral', [config.token], ); - await this.multiProvider.handleTx( - chain, - router.initialize(config.mailbox, config.interchainGasPaymaster), - ); + await this.multiProvider.handleTx(chain, router.initialize(config.mailbox)); return router; } @@ -145,10 +142,7 @@ export class HypERC20Deployer extends GasRouterDeployer< 'HypNative', [], ); - await this.multiProvider.handleTx( - chain, - router.initialize(config.mailbox, config.interchainGasPaymaster), - ); + await this.multiProvider.handleTx(chain, router.initialize(config.mailbox)); return router; } @@ -166,7 +160,6 @@ export class HypERC20Deployer extends GasRouterDeployer< chain, router.initialize( config.mailbox, - config.interchainGasPaymaster, config.totalSupply, config.name, config.symbol, diff --git a/typescript/token/test/HypERC20.t.sol b/typescript/token/test/HypERC20.t.sol new file mode 100644 index 0000000000..ffd34cc92b --- /dev/null +++ b/typescript/token/test/HypERC20.t.sol @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.13; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +import "forge-std/Test.sol"; + +import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol"; +import {TestMailbox} from "@hyperlane-xyz/core/contracts/test/TestMailbox.sol"; +import {TestPostDispatchHook} from "@hyperlane-xyz/core/contracts/test/TestPostDispatchHook.sol"; +import {TestInterchainGasPaymaster} from "@hyperlane-xyz/core/contracts/test/TestInterchainGasPaymaster.sol"; +import {GasRouter} from "@hyperlane-xyz/core/contracts/GasRouter.sol"; + +import {ERC20Test} from "../contracts/test/ERC20Test.sol"; +import {HypERC20} from "../contracts/HypERC20.sol"; +import {HypERC20Collateral} from "../contracts/HypERC20Collateral.sol"; +import {HypNative} from "../contracts/HypNative.sol"; +import {TokenRouter} from "../contracts/libs/TokenRouter.sol"; + +abstract contract HypTokenTest is Test { + using TypeCasts for address; + uint32 internal constant ORIGIN = 11; + uint32 internal constant DESTINATION = 12; + uint8 internal constant DECIMALS = 18; + uint256 internal constant TOTAL_SUPPLY = 1_000_000e18; + uint256 internal REQUIRED_VALUE; // initialized in setUp + uint256 internal constant GAS_LIMIT = 10_000; + uint256 internal IGP_GAS_PRICE; // initialized in test + uint256 internal constant TRANSFER_AMT = 100e18; + string internal constant NAME = "HyperlaneInu"; + string internal constant SYMBOL = "HYP"; + address internal constant ALICE = address(0x1); + address internal constant BOB = address(0x2); + + ERC20Test internal primaryToken; + TokenRouter internal localToken; + HypERC20 internal remoteToken; + TestMailbox internal localMailbox; + TestMailbox internal remoteMailbox; + TestPostDispatchHook internal noopHook; + TestInterchainGasPaymaster internal igp; + + event SentTransferRemote( + uint32 indexed destination, + bytes32 indexed recipient, + uint256 amount + ); + + event ReceivedTransferRemote( + uint32 indexed origin, + bytes32 indexed recipient, + uint256 amount + ); + + function setUp() public virtual { + localMailbox = new TestMailbox(ORIGIN); + remoteMailbox = new TestMailbox(DESTINATION); + + primaryToken = new ERC20Test(NAME, SYMBOL, TOTAL_SUPPLY); + + noopHook = new TestPostDispatchHook(); + localMailbox.setDefaultHook(address(noopHook)); + localMailbox.setRequiredHook(address(noopHook)); + + REQUIRED_VALUE = noopHook.quoteDispatch("", ""); + + remoteToken = new HypERC20(DECIMALS); + remoteToken.initialize( + address(remoteMailbox), + TOTAL_SUPPLY, + NAME, + SYMBOL + ); + remoteToken.enrollRemoteRouter( + ORIGIN, + address(localToken).addressToBytes32() + ); + igp = new TestInterchainGasPaymaster(); + vm.deal(ALICE, 125000); + } + + function _enrollRemoteTokenRouter() internal { + remoteToken.enrollRemoteRouter( + ORIGIN, + address(localToken).addressToBytes32() + ); + } + + function _expectRemoteBalance(address _user, uint256 _balance) internal { + assertEq(remoteToken.balanceOf(_user), _balance); + } + + function _processTransfers(address _recipient, uint256 _amount) internal { + vm.prank(address(remoteMailbox)); + remoteToken.handle( + ORIGIN, + address(localToken).addressToBytes32(), + abi.encodePacked(_recipient.addressToBytes32(), _amount) + ); + } + + function _setCustomGasConfig() internal { + localMailbox.setDefaultHook(address(igp)); + IGP_GAS_PRICE = igp.gasPrice(); + + TokenRouter.GasRouterConfig[] + memory config = new TokenRouter.GasRouterConfig[](1); + config[0] = GasRouter.GasRouterConfig({ + domain: DESTINATION, + gas: GAS_LIMIT + }); + localToken.setDestinationGas(config); + } + + function _performRemoteTransfer(uint256 _msgValue, uint256 _amount) + internal + { + vm.prank(ALICE); + localToken.transferRemote{value: _msgValue}( + DESTINATION, + BOB.addressToBytes32(), + _amount + ); + + vm.expectEmit(true, true, false, true); + emit ReceivedTransferRemote(ORIGIN, BOB.addressToBytes32(), _amount); + _processTransfers(BOB, _amount); + + assertEq(remoteToken.balanceOf(BOB), _amount); + } + + function _performRemoteTransferAndGas( + uint256 _msgValue, + uint256 _amount, + uint256 _gasOverhead + ) internal { + uint256 ethBalance = ALICE.balance; + _performRemoteTransfer(_msgValue + _gasOverhead, _amount); + assertEq(ALICE.balance, ethBalance - REQUIRED_VALUE - _gasOverhead); + } + + function _performRemoteTransferWithEmit( + uint256 _msgValue, + uint256 _amount, + uint256 _gasOverhead + ) internal { + vm.expectEmit(true, true, false, true); + emit SentTransferRemote(DESTINATION, BOB.addressToBytes32(), _amount); + _performRemoteTransferAndGas(_msgValue, _amount, _gasOverhead); + } + + function testBenchmark_overheadGasUsage() public { + vm.prank(address(localMailbox)); + + uint256 gasBefore = gasleft(); + localToken.handle( + DESTINATION, + address(remoteToken).addressToBytes32(), + abi.encodePacked(BOB.addressToBytes32(), TRANSFER_AMT) + ); + uint256 gasAfter = gasleft(); + console.log("Overhead gas usage: %d", gasBefore - gasAfter); + } +} + +contract HypERC20Test is HypTokenTest { + using TypeCasts for address; + HypERC20 internal erc20Token; + + function setUp() public override { + super.setUp(); + + localToken = new HypERC20(DECIMALS); + erc20Token = HypERC20(address(localToken)); + + erc20Token.initialize( + address(localMailbox), + TOTAL_SUPPLY, + NAME, + SYMBOL + ); + + erc20Token.enrollRemoteRouter( + DESTINATION, + address(remoteToken).addressToBytes32() + ); + erc20Token.transfer(ALICE, 1000e18); + + _enrollRemoteTokenRouter(); + } + + function testInitialize_revert_ifAlreadyInitialized() public { + vm.expectRevert("Initializable: contract is already initialized"); + erc20Token.initialize(ALICE, TOTAL_SUPPLY, NAME, SYMBOL); + } + + function testTotalSupply() public { + assertEq(erc20Token.totalSupply(), TOTAL_SUPPLY); + } + + function testDecimals() public { + assertEq(erc20Token.decimals(), DECIMALS); + } + + function testLocalTransfers() public { + assertEq(erc20Token.balanceOf(ALICE), 1000e18); + assertEq(erc20Token.balanceOf(BOB), 0); + + vm.prank(ALICE); + erc20Token.transfer(BOB, 100e18); + assertEq(erc20Token.balanceOf(ALICE), 900e18); + assertEq(erc20Token.balanceOf(BOB), 100e18); + } + + function testRemoteTransfer() public { + remoteToken.enrollRemoteRouter( + ORIGIN, + address(localToken).addressToBytes32() + ); + uint256 balanceBefore = erc20Token.balanceOf(ALICE); + _performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0); + assertEq(erc20Token.balanceOf(ALICE), balanceBefore - TRANSFER_AMT); + } + + function testRemoteTransfer_invalidAmount() public { + vm.expectRevert("ERC20: burn amount exceeds balance"); + _performRemoteTransfer(REQUIRED_VALUE, TRANSFER_AMT * 11); + assertEq(erc20Token.balanceOf(ALICE), 1000e18); + } + + function testRemoteTransfer_withCustomGasConfig() public { + _setCustomGasConfig(); + + uint256 balanceBefore = erc20Token.balanceOf(ALICE); + _performRemoteTransferAndGas( + REQUIRED_VALUE, + TRANSFER_AMT, + GAS_LIMIT * IGP_GAS_PRICE + ); + assertEq(erc20Token.balanceOf(ALICE), balanceBefore - TRANSFER_AMT); + } +} + +contract HypERC20CollateralTest is HypTokenTest { + using TypeCasts for address; + HypERC20Collateral internal erc20Collateral; + + function setUp() public override { + super.setUp(); + + localToken = new HypERC20Collateral(address(primaryToken)); + erc20Collateral = HypERC20Collateral(address(localToken)); + + HypERC20Collateral(address(localToken)).initialize( + address(localMailbox) + ); + + erc20Collateral.enrollRemoteRouter( + DESTINATION, + address(remoteToken).addressToBytes32() + ); + + primaryToken.transfer(address(localToken), 1000e18); + primaryToken.transfer(ALICE, 1000e18); + + _enrollRemoteTokenRouter(); + } + + function testInitialize_revert_ifAlreadyInitialized() public { + vm.expectRevert("Initializable: contract is already initialized"); + erc20Collateral.initialize(ALICE); + } + + function testRemoteTransfer() public { + uint256 balanceBefore = localToken.balanceOf(ALICE); + + vm.prank(ALICE); + primaryToken.approve(address(localToken), TRANSFER_AMT); + _performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0); + assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT); + } + + function testRemoteTransfer_invalidAllowance() public { + vm.expectRevert("ERC20: insufficient allowance"); + _performRemoteTransfer(REQUIRED_VALUE, TRANSFER_AMT); + assertEq(localToken.balanceOf(ALICE), 1000e18); + } + + function testRemoteTransfer_withCustomGasConfig() public { + _setCustomGasConfig(); + + uint256 balanceBefore = localToken.balanceOf(ALICE); + + vm.prank(ALICE); + primaryToken.approve(address(localToken), TRANSFER_AMT); + _performRemoteTransferAndGas( + REQUIRED_VALUE, + TRANSFER_AMT, + GAS_LIMIT * IGP_GAS_PRICE + ); + assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT); + } +} + +contract HypNativeTest is HypTokenTest { + using TypeCasts for address; + HypNative internal nativeToken; + + function setUp() public override { + super.setUp(); + + localToken = new HypNative(); + nativeToken = HypNative(address(localToken)); + + nativeToken.initialize(address(localMailbox)); + + nativeToken.enrollRemoteRouter( + DESTINATION, + address(remoteToken).addressToBytes32() + ); + + vm.deal(address(localToken), 1000e18); + vm.deal(ALICE, 1000e18); + + _enrollRemoteTokenRouter(); + } + + function testInitialize_revert_ifAlreadyInitialized() public { + vm.expectRevert("Initializable: contract is already initialized"); + nativeToken.initialize(ALICE); + } + + function testRemoteTransfer() public { + _performRemoteTransferWithEmit( + REQUIRED_VALUE, + TRANSFER_AMT, + TRANSFER_AMT + ); + } + + function testRemoteTransfer_invalidAmount() public { + vm.expectRevert("Native: amount exceeds msg.value"); + _performRemoteTransfer( + REQUIRED_VALUE + TRANSFER_AMT, + TRANSFER_AMT * 10 + ); + assertEq(localToken.balanceOf(ALICE), 1000e18); + } + + function testRemoteTransfer_withCustomGasConfig() public { + _setCustomGasConfig(); + + _performRemoteTransferAndGas( + REQUIRED_VALUE, + TRANSFER_AMT, + TRANSFER_AMT + GAS_LIMIT * IGP_GAS_PRICE + ); + } +} diff --git a/typescript/token/test/HypERC721.t.sol b/typescript/token/test/HypERC721.t.sol new file mode 100644 index 0000000000..c42e7f041b --- /dev/null +++ b/typescript/token/test/HypERC721.t.sol @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.13; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +import "forge-std/Test.sol"; + +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +import {TestMailbox} from "@hyperlane-xyz/core/contracts/test/TestMailbox.sol"; +import {TestPostDispatchHook} from "@hyperlane-xyz/core/contracts/test/TestPostDispatchHook.sol"; +import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol"; + +import {ERC721Test} from "../contracts/test/ERC721Test.sol"; +import {TokenRouter} from "../contracts/libs/TokenRouter.sol"; +import {HypERC721} from "../contracts/HypERC721.sol"; +import {HypERC721Collateral} from "../contracts/HypERC721Collateral.sol"; +import {HypERC721URIStorage} from "../contracts/extensions/HypERC721URIStorage.sol"; +import {HypERC721URICollateral} from "../contracts/extensions/HypERC721URICollateral.sol"; + +import {ERC721URIStorageUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; + +abstract contract HypTokenTest is Test, IERC721Receiver { + using TypeCasts for address; + + uint256 internal constant INITIAL_SUPPLY = 10; + string internal constant NAME = "Hyperlane Hedgehogs"; + string internal constant SYMBOL = "HHH"; + + address internal constant ALICE = address(0x1); + address internal constant BOB = address(0x2); + uint32 internal constant ORIGIN = 11; + uint32 internal constant DESTINATION = 22; + uint256 internal constant TRANSFER_ID = 0; + string internal constant URI = "http://bit.ly/3reJLpx"; + + ERC721Test internal localPrimaryToken = + new ERC721Test(NAME, SYMBOL, INITIAL_SUPPLY * 2); + ERC721Test internal remotePrimaryToken = + new ERC721Test(NAME, SYMBOL, INITIAL_SUPPLY * 2); + TestMailbox internal localMailbox; + TestMailbox internal remoteMailbox; + TokenRouter internal localToken; + TokenRouter internal remoteToken; + TestPostDispatchHook internal noopHook; + + event Dispatch( + address indexed sender, + uint32 indexed destination, + bytes32 indexed recipient, + bytes message + ); + + function setUp() public virtual { + noopHook = new TestPostDispatchHook(); + + localMailbox = new TestMailbox(ORIGIN); + localMailbox.setDefaultHook(address(noopHook)); + localMailbox.setRequiredHook(address(noopHook)); + + remoteMailbox = new TestMailbox(DESTINATION); + + remoteToken = new HypERC721Collateral(address(remotePrimaryToken)); + } + + function _deployRemoteToken(bool isCollateral) internal { + if (isCollateral) { + remoteToken = new HypERC721Collateral(address(remotePrimaryToken)); + HypERC721Collateral(address(remoteToken)).initialize( + address(remoteMailbox) + ); + remotePrimaryToken.transferFrom( + address(this), + address(remoteToken), + 0 + ); // need for processing messages + } else { + remoteToken = new HypERC721(); + HypERC721(address(remoteToken)).initialize( + address(remoteMailbox), + 0, + NAME, + SYMBOL + ); + } + remoteToken.enrollRemoteRouter( + ORIGIN, + address(localToken).addressToBytes32() + ); + } + + function _processTransfers(address _recipient, uint256 _tokenId) internal { + vm.prank(address(remoteMailbox)); + remoteToken.handle( + ORIGIN, + address(localToken).addressToBytes32(), + abi.encodePacked(_recipient.addressToBytes32(), _tokenId) + ); + } + + function _performRemoteTransfer(uint256 _msgValue, uint256 _tokenId) + public + { + localToken.transferRemote{value: _msgValue}( + DESTINATION, + ALICE.addressToBytes32(), + _tokenId + ); + + _processTransfers(BOB, _tokenId); + assertEq(remoteToken.balanceOf(BOB), 1); + } + + function testBenchmark_overheadGasUsage() public { + vm.prank(address(localMailbox)); + + uint256 gasBefore = gasleft(); + localToken.handle( + DESTINATION, + address(remoteToken).addressToBytes32(), + abi.encodePacked(BOB.addressToBytes32(), INITIAL_SUPPLY + 1) + ); + uint256 gasAfter = gasleft(); + console.log("Overhead gas usage: %d", gasBefore - gasAfter); + } + + function onERC721Received( + address, /*operator*/ + address, /*from*/ + uint256, /*tokenId*/ + bytes calldata /*data*/ + ) external pure returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } +} + +contract HypERC721Test is HypTokenTest { + using TypeCasts for address; + + HypERC721 internal hyp721; + + function setUp() public virtual override { + super.setUp(); + + localToken = new HypERC721(); + hyp721 = HypERC721(address(localToken)); + + hyp721.initialize(address(localMailbox), INITIAL_SUPPLY, NAME, SYMBOL); + + hyp721.enrollRemoteRouter( + DESTINATION, + address(remoteToken).addressToBytes32() + ); + } + + function testInitialize_revert_ifAlreadyInitialized() public { + vm.expectRevert("Initializable: contract is already initialized"); + hyp721.initialize(address(localMailbox), INITIAL_SUPPLY, NAME, SYMBOL); + } + + function testTotalSupply() public { + assertEq(hyp721.balanceOf(address(this)), INITIAL_SUPPLY); + } + + function testOwnerOf() public { + assertEq(hyp721.ownerOf(0), address(this)); + } + + function testLocalTransfer() public { + hyp721.transferFrom(address(this), ALICE, 0); + assertEq(hyp721.balanceOf(address(this)), INITIAL_SUPPLY - 1); + assertEq(hyp721.balanceOf(ALICE), 1); + } + + function testLocalYTransfer_revert_invalidTokenId() public { + vm.expectRevert("ERC721: invalid token ID"); + hyp721.transferFrom(address(this), ALICE, INITIAL_SUPPLY); + } + + function testRemoteTransfer(bool isCollateral) public { + _deployRemoteToken(isCollateral); + _performRemoteTransfer(25000, 0); + assertEq(hyp721.balanceOf(address(this)), INITIAL_SUPPLY - 1); + } + + function testRemoteTransfer_revert_unowned() public { + hyp721.transferFrom(address(this), BOB, 1); + + _deployRemoteToken(false); + vm.expectRevert("!owner"); + _performRemoteTransfer(25000, 1); + assertEq(hyp721.balanceOf(address(this)), INITIAL_SUPPLY - 1); + } + + function testRemoteTransfer_revert_invalidTokenId() public { + _deployRemoteToken(false); + vm.expectRevert("ERC721: invalid token ID"); + _performRemoteTransfer(25000, INITIAL_SUPPLY); + assertEq(hyp721.balanceOf(address(this)), INITIAL_SUPPLY); + } +} + +contract MockHypERC721URIStorage is HypERC721URIStorage { + function setTokenURI(uint256 tokenId, string memory uri) public { + _setTokenURI(tokenId, uri); + } +} + +contract HypERC721URIStorageTest is HypTokenTest { + using TypeCasts for address; + + MockHypERC721URIStorage internal hyp721Storage; + + function setUp() public override { + super.setUp(); + + localToken = new MockHypERC721URIStorage(); + hyp721Storage = MockHypERC721URIStorage(address(localToken)); + + hyp721Storage.initialize( + address(localMailbox), + INITIAL_SUPPLY, + NAME, + SYMBOL + ); + hyp721Storage.setTokenURI(0, URI); + hyp721Storage.enrollRemoteRouter( + DESTINATION, + address(remoteToken).addressToBytes32() + ); + } + + function testRemoteTransfers_revert_burned() public { + _deployRemoteToken(false); + _performRemoteTransfer(25000, 0); + assertEq(hyp721Storage.balanceOf(address(this)), INITIAL_SUPPLY - 1); + + vm.expectRevert("ERC721: invalid token ID"); + assertEq(hyp721Storage.tokenURI(0), ""); + } +} + +contract HypERC721CollateralTest is HypTokenTest { + using TypeCasts for address; + + HypERC721Collateral internal hyp721Collateral; + + function setUp() public override { + super.setUp(); + + localToken = new HypERC721Collateral(address(localPrimaryToken)); + hyp721Collateral = HypERC721Collateral(address(localToken)); + + hyp721Collateral.initialize(address(localMailbox)); + + hyp721Collateral.enrollRemoteRouter( + DESTINATION, + address(remoteToken).addressToBytes32() + ); + + localPrimaryToken.transferFrom( + address(this), + address(hyp721Collateral), + INITIAL_SUPPLY + 1 + ); + } + + function testInitialize_revert_ifAlreadyInitialized() public { + vm.expectRevert("Initializable: contract is already initialized"); + hyp721Collateral.initialize(ALICE); + } + + function testRemoteTransfer(bool isCollateral) public { + localPrimaryToken.approve(address(hyp721Collateral), 0); + _deployRemoteToken(isCollateral); + _performRemoteTransfer(25000, 0); + assertEq( + hyp721Collateral.balanceOf(address(this)), + INITIAL_SUPPLY * 2 - 2 + ); + } + + function testRemoteTransfer_revert_unowned() public { + localPrimaryToken.transferFrom(address(this), BOB, 1); + + _deployRemoteToken(false); + vm.expectRevert("ERC721: caller is not token owner or approved"); + _performRemoteTransfer(25000, 1); + assertEq( + hyp721Collateral.balanceOf(address(this)), + INITIAL_SUPPLY * 2 - 2 + ); + } + + function testRemoteTransfer_revert_invalidTokenId() public { + _deployRemoteToken(false); + vm.expectRevert("ERC721: invalid token ID"); + _performRemoteTransfer(25000, INITIAL_SUPPLY * 2); + assertEq( + hyp721Collateral.balanceOf(address(this)), + INITIAL_SUPPLY * 2 - 1 + ); + } +} + +contract HypERC721CollateralURIStorageTest is HypTokenTest { + using TypeCasts for address; + + HypERC721URICollateral internal hyp721URICollateral; + + function setUp() public override { + super.setUp(); + + localToken = new HypERC721URICollateral(address(localPrimaryToken)); + hyp721URICollateral = HypERC721URICollateral(address(localToken)); + + hyp721URICollateral.initialize(address(localMailbox)); + hyp721URICollateral.enrollRemoteRouter( + DESTINATION, + address(remoteToken).addressToBytes32() + ); + + // localPrimaryToken.setTokenURI(0, URI); + localPrimaryToken.transferFrom( + address(this), + address(hyp721URICollateral), + INITIAL_SUPPLY + 1 + ); + localPrimaryToken.ownerOf(0); + } + + function testRemoteTransfers_revert_burned() public { + _deployRemoteToken(false); + localPrimaryToken.approve(address(hyp721URICollateral), 0); + + bytes + memory message = hex"03000000000000000b0000000000000000000000001d1499e622d69689cdf9004d05ec547d650ff21100000016000000000000000000000000a0cb889707d426a7a386870a03bc70d1b069759800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000544553542d424153452d55524930"; + vm.expectEmit(true, true, false, true); + + emit Dispatch( + address(localToken), + DESTINATION, + address(remoteToken).addressToBytes32(), + message + ); + localToken.transferRemote{value: 25000}( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_ID + ); + + _processTransfers(BOB, 0); + assertEq(remoteToken.balanceOf(BOB), 1); + assertEq( + hyp721URICollateral.balanceOf(address(this)), + INITIAL_SUPPLY * 2 - 2 + ); + } +} diff --git a/typescript/token/test/erc20.test.ts b/typescript/token/test/erc20.test.ts deleted file mode 100644 index c347f82a51..0000000000 --- a/typescript/token/test/erc20.test.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import '@nomiclabs/hardhat-waffle'; -import { expect } from 'chai'; -import { BigNumber, BigNumberish } from 'ethers'; -import { ethers } from 'hardhat'; - -import { InterchainGasPaymaster__factory } from '@hyperlane-xyz/core'; -import { - ChainMap, - Chains, - HyperlaneContractsMap, - MultiProvider, - RouterConfig, - TestCoreApp, - TestCoreDeployer, - deployTestIgpsAndGetRouterConfig, - objMap, -} from '@hyperlane-xyz/sdk'; -import { utils } from '@hyperlane-xyz/utils'; - -import { TokenConfig, TokenType } from '../src/config'; -import { HypERC20Factories } from '../src/contracts'; -import { HypERC20Deployer } from '../src/deploy'; -import { - ERC20, - ERC20Test__factory, - ERC20__factory, - HypERC20, - HypERC20Collateral, - HypNative, -} from '../src/types'; - -const localChain = Chains.test1; -const remoteChain = Chains.test2; -let localDomain: number; -let remoteDomain: number; -const totalSupply = 3000; -const amount = 10; - -const tokenMetadata = { - name: 'HypERC20', - symbol: 'HYP', - decimals: 18, - totalSupply, -}; - -for (const variant of [ - TokenType.synthetic, - TokenType.collateral, - TokenType.native, -]) { - describe(`HypERC20${variant}`, async () => { - let owner: SignerWithAddress; - let recipient: SignerWithAddress; - let core: TestCoreApp; - let deployer: HypERC20Deployer; - let contracts: HyperlaneContractsMap; - let localTokenConfig: TokenConfig; - let local: HypERC20 | HypERC20Collateral | HypNative; - let remote: HypERC20; - let interchainGasPayment: BigNumber; - - beforeEach(async () => { - [owner, recipient] = await ethers.getSigners(); - const multiProvider = MultiProvider.createTestMultiProvider({ - signer: owner, - }); - localDomain = multiProvider.getDomainId(localChain); - remoteDomain = multiProvider.getDomainId(remoteChain); - - const coreDeployer = new TestCoreDeployer(multiProvider); - const coreContractsMaps = await coreDeployer.deploy(); - core = new TestCoreApp(coreContractsMaps, multiProvider); - const routerConfig = await deployTestIgpsAndGetRouterConfig( - multiProvider, - owner.address, - core.contractsMap, - ); - - let erc20: ERC20 | undefined; - if (variant === TokenType.collateral) { - erc20 = await new ERC20Test__factory(owner).deploy( - tokenMetadata.name, - tokenMetadata.symbol, - tokenMetadata.totalSupply, - ); - localTokenConfig = { - type: variant, - token: erc20.address, - }; - } else if (variant === TokenType.native) { - localTokenConfig = { - type: variant, - }; - } else if (variant === TokenType.synthetic) { - localTokenConfig = { type: variant, ...tokenMetadata }; - } - - const config = objMap(routerConfig, (key) => ({ - ...routerConfig[key], - ...(key === localChain - ? localTokenConfig - : { type: TokenType.synthetic }), - owner: owner.address, - })) as ChainMap; - - deployer = new HypERC20Deployer(multiProvider); - contracts = await deployer.deploy(config); - local = contracts[localChain].router; - - interchainGasPayment = await local.quoteGasPayment(remoteDomain); - - if (variant === TokenType.native) { - interchainGasPayment = interchainGasPayment.add(amount); - } - - if (variant === TokenType.collateral) { - await erc20!.approve(local.address, amount); - } - - remote = contracts[remoteChain].router as HypERC20; - }); - - it('should not be initializable again', async () => { - const initializeTx = - variant === TokenType.collateral || variant === TokenType.native - ? (local as HypERC20Collateral).initialize( - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ) - : (local as HypERC20).initialize( - ethers.constants.AddressZero, - ethers.constants.AddressZero, - 0, - '', - '', - ); - await expect(initializeTx).to.be.revertedWith( - 'Initializable: contract is already initialized', - ); - }); - - if (variant === TokenType.synthetic) { - it('should mint total supply to deployer', async () => { - await expectBalance(local, recipient, 0); - await expectBalance(local, owner, totalSupply); - await expectBalance(remote, recipient, 0); - await expectBalance(remote, owner, totalSupply); - }); - - it('should allow for local transfers', async () => { - await (local as HypERC20).transfer(recipient.address, amount); - await expectBalance(local, recipient, amount); - await expectBalance(local, owner, totalSupply - amount); - await expectBalance(remote, recipient, 0); - await expectBalance(remote, owner, totalSupply); - }); - } - - it('benchmark handle gas overhead', async () => { - const localRaw = local.connect(ethers.provider); - const mailboxAddress = core.contractsMap[localChain].mailbox.address; - if (variant === TokenType.collateral) { - const tokenAddress = await (local as HypERC20Collateral).wrappedToken(); - const token = ERC20__factory.connect(tokenAddress, owner); - await token.transfer(local.address, totalSupply); - } else if (variant === TokenType.native) { - const remoteDomain = core.multiProvider.getDomainId(remoteChain); - // deposit amount - await local.transferRemote( - remoteDomain, - utils.addressToBytes32(remote.address), - amount, - { value: interchainGasPayment }, - ); - } - const message = `${utils.addressToBytes32( - recipient.address, - )}${BigNumber.from(amount).toHexString().slice(2).padStart(64, '0')}`; - const handleGas = await localRaw.estimateGas.handle( - remoteDomain, - utils.addressToBytes32(remote.address), - message, - { from: mailboxAddress }, - ); - console.log(handleGas); - }); - - it('should allow for remote transfers', async () => { - const localOwner = await local.balanceOf(owner.address); - const localRecipient = await local.balanceOf(recipient.address); - const remoteOwner = await remote.balanceOf(owner.address); - const remoteRecipient = await remote.balanceOf(recipient.address); - - await local.transferRemote( - remoteDomain, - utils.addressToBytes32(recipient.address), - amount, - { - value: interchainGasPayment, - }, - ); - - let expectedLocal = localOwner.sub(amount); - - await expectBalance(local, recipient, localRecipient); - if (variant === TokenType.native) { - // account for tx fees, rewards, etc. - expectedLocal = await local.balanceOf(owner.address); - } - await expectBalance(local, owner, expectedLocal); - await expectBalance(remote, recipient, remoteRecipient); - await expectBalance(remote, owner, remoteOwner); - - await core.processMessages(); - - await expectBalance(local, recipient, localRecipient); - if (variant === TokenType.native) { - // account for tx fees, rewards, etc. - expectedLocal = await local.balanceOf(owner.address); - } - await expectBalance(local, owner, expectedLocal); - await expectBalance(remote, recipient, remoteRecipient.add(amount)); - await expectBalance(remote, owner, remoteOwner); - }); - - it('allows interchain gas payment for remote transfers', async () => { - const interchainGasPaymaster = new InterchainGasPaymaster__factory() - .attach(await local.interchainGasPaymaster()) - .connect(owner); - await expect( - local.transferRemote( - remoteDomain, - utils.addressToBytes32(recipient.address), - amount, - { value: interchainGasPayment }, - ), - ).to.emit(interchainGasPaymaster, 'GasPayment'); - }); - - it('should prevent remote transfer of unowned balance', async () => { - const revertReason = (): string => { - switch (variant) { - case TokenType.synthetic: - return 'ERC20: burn amount exceeds balance'; - case TokenType.collateral: - return 'ERC20: insufficient allowance'; - case TokenType.native: - return 'Native: amount exceeds msg.value'; - } - return ''; - }; - const value = - variant === TokenType.native ? amount - 1 : interchainGasPayment; - await expect( - local - .connect(recipient) - .transferRemote( - remoteDomain, - utils.addressToBytes32(recipient.address), - amount, - { value }, - ), - ).to.be.revertedWith(revertReason()); - }); - - it('should emit TransferRemote events', async () => { - expect( - await local.transferRemote( - remoteDomain, - utils.addressToBytes32(recipient.address), - amount, - { value: interchainGasPayment }, - ), - ) - .to.emit(local, 'SentTransferRemote') - .withArgs(remoteDomain, recipient.address, amount); - expect(await core.processMessages()) - .to.emit(local, 'ReceivedTransferRemote') - .withArgs(localDomain, recipient.address, amount); - }); - }); -} - -const expectBalance = async ( - token: HypERC20 | HypERC20Collateral | ERC20 | HypNative, - signer: SignerWithAddress, - balance: BigNumberish, -) => { - return expect(await token.balanceOf(signer.address)).to.eq(balance); -}; diff --git a/typescript/token/test/erc721.test.ts b/typescript/token/test/erc721.test.ts deleted file mode 100644 index acd004e6ff..0000000000 --- a/typescript/token/test/erc721.test.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import '@nomiclabs/hardhat-waffle'; -import { expect } from 'chai'; -import { BigNumber, BigNumberish } from 'ethers'; -import { ethers } from 'hardhat'; - -import { InterchainGasPaymaster__factory } from '@hyperlane-xyz/core'; -import { - Chains, - HyperlaneContractsMap, - MultiProvider, - TestCoreApp, - TestCoreDeployer, - deployTestIgpsAndGetRouterConfig, - objMap, -} from '@hyperlane-xyz/sdk'; -import { utils } from '@hyperlane-xyz/utils'; - -import { TokenConfig, TokenType } from '../src/config'; -import { HypERC721Factories } from '../src/contracts'; -import { HypERC721Deployer } from '../src/deploy'; -import { - ERC721, - ERC721Test__factory, - ERC721__factory, - HypERC721, - HypERC721Collateral, - HypERC721URICollateral, - HypERC721URIStorage, -} from '../src/types'; - -const localChain = Chains.test1; -const remoteChain = Chains.test2; -let localDomain: number; -let remoteDomain: number; -const totalSupply = 50; -const tokenId = 10; -const tokenId2 = 20; -const tokenId3 = 30; -const tokenId4 = 40; - -const tokenMetadata = { - name: 'HypERC721', - symbol: 'HYP', - totalSupply, -}; - -for (const withCollateral of [true, false]) { - for (const withUri of [true, false]) { - const tokenConfig: TokenConfig = { - type: withUri ? TokenType.syntheticUri : TokenType.synthetic, - ...tokenMetadata, - }; - - const configMap = { - test1: tokenConfig, - test2: { - ...tokenConfig, - totalSupply: 0, - }, - test3: { - ...tokenConfig, - totalSupply: 0, - }, - }; - describe(`HypERC721${withUri ? 'URI' : ''}${ - withCollateral ? 'Collateral' : '' - }`, async () => { - let owner: SignerWithAddress; - let recipient: SignerWithAddress; - let core: TestCoreApp; - let deployer: HypERC721Deployer; - let contracts: HyperlaneContractsMap; - let local: HypERC721 | HypERC721Collateral | HypERC721URICollateral; - let remote: HypERC721 | HypERC721Collateral | HypERC721URIStorage; - let interchainGasPayment: BigNumberish; - - beforeEach(async () => { - [owner, recipient] = await ethers.getSigners(); - const multiProvider = MultiProvider.createTestMultiProvider({ - signer: owner, - }); - localDomain = multiProvider.getDomainId(localChain); - remoteDomain = multiProvider.getDomainId(remoteChain); - - const coreDeployer = new TestCoreDeployer(multiProvider); - const coreContractsMaps = await coreDeployer.deploy(); - core = new TestCoreApp(coreContractsMaps, multiProvider); - const coreConfig = await deployTestIgpsAndGetRouterConfig( - multiProvider, - owner.address, - core.contractsMap, - ); - const configWithTokenInfo = objMap(coreConfig, (key) => ({ - ...coreConfig[key], - ...configMap[key], - owner: owner.address, - })); - - let erc721: ERC721 | undefined; - if (withCollateral) { - erc721 = await new ERC721Test__factory(owner).deploy( - tokenConfig.name, - tokenConfig.symbol, - tokenConfig.totalSupply, - ); - configWithTokenInfo.test1 = { - type: withUri ? TokenType.collateralUri : TokenType.collateral, - token: erc721.address, - ...coreConfig.test1, - }; - } - - deployer = new HypERC721Deployer(multiProvider); - contracts = await deployer.deploy(configWithTokenInfo); - - local = contracts[localChain].router; - if (withCollateral) { - // approve wrapper to transfer tokens - await erc721!.approve(local.address, tokenId); - await erc721!.approve(local.address, tokenId2); - await erc721!.approve(local.address, tokenId3); - await erc721!.approve(local.address, tokenId4); - } - interchainGasPayment = await local.quoteGasPayment(remoteDomain); - - remote = contracts[remoteChain].router; - }); - - it('should not be initializable again', async () => { - const initializeTx = withCollateral - ? (local as HypERC721Collateral).initialize( - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ) - : (local as HypERC721).initialize( - ethers.constants.AddressZero, - ethers.constants.AddressZero, - 0, - '', - '', - ); - await expect(initializeTx).to.be.revertedWith( - 'Initializable: contract is already initialized', - ); - }); - - it('should mint total supply to deployer on local domain', async () => { - await expectBalance(local, recipient, 0); - await expectBalance(local, owner, totalSupply); - await expectBalance(remote, recipient, 0); - await expectBalance(remote, owner, 0); - }); - - // do not test underlying ERC721 collateral functionality - if (!withCollateral) { - it('should allow for local transfers', async () => { - await (local as HypERC721).transferFrom( - owner.address, - recipient.address, - tokenId, - ); - await expectBalance(local, recipient, 1); - await expectBalance(local, owner, totalSupply - 1); - await expectBalance(remote, recipient, 0); - await expectBalance(remote, owner, 0); - }); - } - - it('should not allow transfers of nonexistent identifiers', async () => { - const invalidTokenId = totalSupply + 10; - if (!withCollateral) { - await expect( - (local as HypERC721).transferFrom( - owner.address, - recipient.address, - invalidTokenId, - ), - ).to.be.revertedWith('ERC721: invalid token ID'); - } - await expect( - local.transferRemote( - remoteDomain, - utils.addressToBytes32(recipient.address), - invalidTokenId, - { value: interchainGasPayment }, - ), - ).to.be.revertedWith('ERC721: invalid token ID'); - }); - - it('should allow for remote transfers', async () => { - await local.transferRemote( - remoteDomain, - utils.addressToBytes32(recipient.address), - tokenId2, - { value: interchainGasPayment }, - ); - - await expectBalance(local, recipient, 0); - await expectBalance(local, owner, totalSupply - 1); - await expectBalance(remote, recipient, 0); - await expectBalance(remote, owner, 0); - - await core.processMessages(); - - await expectBalance(local, recipient, 0); - await expectBalance(local, owner, totalSupply - 1); - await expectBalance(remote, recipient, 1); - await expectBalance(remote, owner, 0); - }); - - if (withUri && withCollateral) { - it('should relay URI with remote transfer', async () => { - const remoteUri = remote as HypERC721URIStorage; - await expect(remoteUri.tokenURI(tokenId2)).to.be.revertedWith(''); - - await local.transferRemote( - remoteDomain, - utils.addressToBytes32(recipient.address), - tokenId2, - { value: interchainGasPayment }, - ); - - await expect(remoteUri.tokenURI(tokenId2)).to.be.revertedWith(''); - - await core.processMessages(); - - expect(await remoteUri.tokenURI(tokenId2)).to.equal( - `TEST-BASE-URI${tokenId2}`, - ); - }); - } - - it('should prevent remote transfer of unowned id', async () => { - const revertReason = withCollateral - ? 'ERC721: transfer from incorrect owner' - : '!owner'; - await expect( - local - .connect(recipient) - .transferRemote( - remoteDomain, - utils.addressToBytes32(recipient.address), - tokenId2, - { value: interchainGasPayment }, - ), - ).to.be.revertedWith(revertReason); - }); - - it('benchmark handle gas overhead', async () => { - const localRaw = local.connect(ethers.provider); - const mailboxAddress = core.contractsMap[localChain].mailbox.address; - let tokenIdToUse: number; - if (withCollateral) { - const tokenAddress = await ( - local as HypERC721Collateral - ).wrappedToken(); - const token = ERC721__factory.connect(tokenAddress, owner); - await token.transferFrom(owner.address, local.address, tokenId); - tokenIdToUse = tokenId; - } else { - tokenIdToUse = totalSupply + 1; - } - const message = `${utils.addressToBytes32( - recipient.address, - )}${BigNumber.from(tokenIdToUse) - .toHexString() - .slice(2) - .padStart(64, '0')}`; - try { - const gas = await localRaw.estimateGas.handle( - remoteDomain, - utils.addressToBytes32(remote.address), - message, - { from: mailboxAddress }, - ); - console.log(gas); - } catch (e) { - console.log('FAILED'); - } - }); - - it('allows interchain gas payment for remote transfers', async () => { - const interchainGasPaymaster = new InterchainGasPaymaster__factory() - .attach(await local.interchainGasPaymaster()) - .connect(owner); - await expect( - local.transferRemote( - remoteDomain, - utils.addressToBytes32(recipient.address), - tokenId3, - { - value: interchainGasPayment, - }, - ), - ).to.emit(interchainGasPaymaster, 'GasPayment'); - }); - - it('should emit TransferRemote events', async () => { - expect( - await local.transferRemote( - remoteDomain, - utils.addressToBytes32(recipient.address), - tokenId4, - { value: interchainGasPayment }, - ), - ) - .to.emit(local, 'SentTransferRemote') - .withArgs(remoteDomain, recipient.address, tokenId4); - expect(await core.processMessages()) - .to.emit(local, 'ReceivedTransferRemote') - .withArgs(localDomain, recipient.address, tokenId4); - }); - }); - } -} - -const expectBalance = async ( - token: HypERC721 | HypERC721Collateral | ERC721, - signer: SignerWithAddress, - balance: number, -) => { - expect(await token.balanceOf(signer.address)).to.eq(balance); -}; From 70260cfddca8ee1ee1851daf0c6e2e033064bb4e Mon Sep 17 00:00:00 2001 From: -f Date: Thu, 14 Sep 2023 16:23:39 -0400 Subject: [PATCH 24/27] key as {message_id,destination} --- .../agents/relayer/src/msg/gas_payment/mod.rs | 128 +++++++++++++----- .../src/db/rocks/hyperlane_db.rs | 26 +++- .../src/db/rocks/storage_types.rs | 8 +- rust/hyperlane-core/src/traits/encode.rs | 27 +++- rust/hyperlane-core/src/types/mod.rs | 9 ++ 5 files changed, 152 insertions(+), 46 deletions(-) diff --git a/rust/agents/relayer/src/msg/gas_payment/mod.rs b/rust/agents/relayer/src/msg/gas_payment/mod.rs index 4eb999145d..aa19c23fd8 100644 --- a/rust/agents/relayer/src/msg/gas_payment/mod.rs +++ b/rust/agents/relayer/src/msg/gas_payment/mod.rs @@ -6,8 +6,8 @@ use tracing::{debug, error, trace}; use hyperlane_base::db::HyperlaneRocksDB; use hyperlane_core::{ - HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment, TxCostEstimate, TxOutcome, - U256, + GasPaymentKey, HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment, + TxCostEstimate, TxOutcome, U256, }; use crate::msg::gas_payment::policies::GasPaymentPolicyOnChainFeeQuoting; @@ -78,7 +78,13 @@ impl GasPaymentEnforcer { tx_cost_estimate: &TxCostEstimate, ) -> Result> { let msg_id = message.id(); - let current_payment = self.db.retrieve_gas_payment_by_message_id(msg_id)?; + let gap_payment_key = GasPaymentKey { + message_id: msg_id, + destination: message.destination, + }; + let current_payment = self + .db + .retrieve_gas_payment_by_message_id(gap_payment_key)?; let current_expenditure = self.db.retrieve_gas_expenditure_by_message_id(msg_id)?; for (policy, whitelist) in &self.policies { if !whitelist.msg_matches(message, true) { @@ -97,17 +103,6 @@ impl GasPaymentEnforcer { ?whitelist, "Message matched whitelist for policy" ); - - if current_payment.destination != message.destination { - trace!( - msg=%message, - ?policy, - ?current_payment, - ?current_expenditure, - "Message destination did not match current payment destination" - ); - continue; - } debug!( msg=%message, ?policy, @@ -148,7 +143,10 @@ mod test { use std::str::FromStr; use hyperlane_base::db::{test_utils, HyperlaneRocksDB}; - use hyperlane_core::{HyperlaneDomain, HyperlaneMessage, TxCostEstimate, H160, H256, U256}; + use hyperlane_core::{ + HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, LogMeta, TxCostEstimate, H160, + H256, U256, + }; use crate::settings::{ matching_list::MatchingList, GasPaymentEnforcementConf, GasPaymentEnforcementPolicy, @@ -221,11 +219,10 @@ mod test { } #[tokio::test] - async fn test_check_destination() { + async fn test_different_destinations() { #[allow(unused_must_use)] test_utils::run_test_db(|db| async move { - let correct_destination_msg = HyperlaneMessage::default(); - let incorrect_destination_msg = HyperlaneMessage { + let msg = HyperlaneMessage { destination: 123, ..HyperlaneMessage::default() }; @@ -234,34 +231,101 @@ mod test { &HyperlaneDomain::new_test_domain("test_check_destination"), db, ); - let enforcer = GasPaymentEnforcer::new( - // Require a payment vec![GasPaymentEnforcementConf { - policy: GasPaymentEnforcementPolicy::None, + policy: GasPaymentEnforcementPolicy::Minimum { + payment: U256::one(), + }, matching_list: MatchingList::default(), }], - hyperlane_db, + hyperlane_db.clone(), ); - /// Ensure if the message has the correct destination, it meets the requirement + let wrong_destination_payment = InterchainGasPayment { + message_id: msg.id(), + destination: 456, + payment: U256::one(), + gas_amount: U256::one(), + }; + hyperlane_db.process_gas_payment(wrong_destination_payment, &LogMeta::default()); + // Ensure if the gas payment was made to the incorrect destination, it does not meet + // the requirement assert!(enforcer - .message_meets_gas_payment_requirement( - &correct_destination_msg, - &TxCostEstimate::default(), - ) + .message_meets_gas_payment_requirement(&msg, &TxCostEstimate::default(),) + .await + .unwrap() + .is_none()); + + let correct_destination_payment = InterchainGasPayment { + message_id: msg.id(), + destination: 123, + payment: U256::one(), + gas_amount: U256::one(), + }; + hyperlane_db.process_gas_payment(correct_destination_payment, &LogMeta::default()); + // Ensure if the gas payment was made to the correct destination, it meets the + // requirement + assert!(enforcer + .message_meets_gas_payment_requirement(&msg, &TxCostEstimate::default(),) .await .unwrap() .is_some()); - /// Ensure if the message has the incorrect destination, it does not meet the requirement + }) + .await; + } + + #[tokio::test] + async fn test_half_and_half_payment() { + #[allow(unused_must_use)] + test_utils::run_test_db(|db| async move { + let msg = HyperlaneMessage { + destination: 123, + ..HyperlaneMessage::default() + }; + + let hyperlane_db = HyperlaneRocksDB::new( + &HyperlaneDomain::new_test_domain("test_check_destination"), + db, + ); + + let enforcer = GasPaymentEnforcer::new( + vec![GasPaymentEnforcementConf { + policy: GasPaymentEnforcementPolicy::Minimum { + payment: U256::from(2), + }, + matching_list: MatchingList::default(), + }], + hyperlane_db.clone(), + ); + + let initial_payment = InterchainGasPayment { + message_id: msg.id(), + destination: msg.destination, + payment: U256::one(), + gas_amount: U256::one(), + }; + hyperlane_db.process_gas_payment(initial_payment, &LogMeta::default()); + + // Ensure if only half gas payment was made, it does not meet the requirement assert!(enforcer - .message_meets_gas_payment_requirement( - &incorrect_destination_msg, - &TxCostEstimate::default(), - ) + .message_meets_gas_payment_requirement(&msg, &TxCostEstimate::default(),) .await .unwrap() .is_none()); + + let deficit_payment = InterchainGasPayment { + message_id: msg.id(), + destination: 123, + payment: U256::one(), + gas_amount: U256::one(), + }; + hyperlane_db.process_gas_payment(deficit_payment, &LogMeta::default()); + // Ensure if the full gas payment was made, it meets the requirement + assert!(enforcer + .message_meets_gas_payment_requirement(&msg, &TxCostEstimate::default(),) + .await + .unwrap() + .is_some()); }) .await; } diff --git a/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs b/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs index 0f4f1a8aa5..38d7722315 100644 --- a/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs +++ b/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs @@ -8,7 +8,7 @@ use tokio::time::sleep; use tracing::{debug, instrument, trace}; use hyperlane_core::{ - HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, HyperlaneMessageStore, + GasPaymentKey, HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, HyperlaneMessageStore, HyperlaneWatermarkedLogStore, InterchainGasExpenditure, InterchainGasPayment, InterchainGasPaymentMeta, LogMeta, H256, }; @@ -161,11 +161,17 @@ impl HyperlaneRocksDB { /// Update the total gas payment for a message to include gas_payment fn update_gas_payment_by_message_id(&self, event: InterchainGasPayment) -> DbResult<()> { - let existing_payment = self.retrieve_gas_payment_by_message_id(event.message_id)?; + let key = GasPaymentKey { + message_id: event.message_id, + destination: event.destination, + }; + println!("update_gas_payment_by_message_id: {:?}", key); + let existing_payment = self.retrieve_gas_payment_by_message_id(key)?; + println!("existing_payment: {:?}", existing_payment); let total = existing_payment + event; debug!(?event, new_total_gas_payment=?total, "Storing gas payment"); - self.store_interchain_gas_payment_data_by_message_id(&total.message_id, &total.into())?; + self.store_interchain_gas_payment_data_by_message_id(&key, &total.into())?; Ok(()) } @@ -192,12 +198,18 @@ impl HyperlaneRocksDB { /// Retrieve the total gas payment for a message pub fn retrieve_gas_payment_by_message_id( &self, - message_id: H256, + key: GasPaymentKey, ) -> DbResult { + println!( + "retrieve_gas_payment_by_message_id: {:?}", + self.retrieve_interchain_gas_payment_data_by_message_id(&key)? + .unwrap_or_default() + .complete(key.message_id, key.destination) + ); Ok(self - .retrieve_interchain_gas_payment_data_by_message_id(&message_id)? + .retrieve_interchain_gas_payment_data_by_message_id(&key)? .unwrap_or_default() - .complete(message_id)) + .complete(key.message_id, key.destination)) } /// Retrieve the total gas payment for a message @@ -315,7 +327,7 @@ make_store_and_retrieve!(pub(self), dispatched_block_number_by_nonce, MESSAGE_DI make_store_and_retrieve!(pub, processed_by_nonce, NONCE_PROCESSED, u32, bool); make_store_and_retrieve!(pub(self), processed_by_gas_payment_meta, GAS_PAYMENT_META_PROCESSED, InterchainGasPaymentMeta, bool); make_store_and_retrieve!(pub(self), interchain_gas_expenditure_data_by_message_id, GAS_EXPENDITURE_FOR_MESSAGE_ID, H256, InterchainGasExpenditureData); -make_store_and_retrieve!(pub(self), interchain_gas_payment_data_by_message_id, GAS_PAYMENT_FOR_MESSAGE_ID, H256, InterchainGasPaymentData); +make_store_and_retrieve!(pub(self), interchain_gas_payment_data_by_message_id, GAS_PAYMENT_FOR_MESSAGE_ID, GasPaymentKey, InterchainGasPaymentData); make_store_and_retrieve!( pub, pending_message_retry_count_by_message_id, diff --git a/rust/hyperlane-base/src/db/rocks/storage_types.rs b/rust/hyperlane-base/src/db/rocks/storage_types.rs index 31965493bd..9c5282c344 100644 --- a/rust/hyperlane-base/src/db/rocks/storage_types.rs +++ b/rust/hyperlane-base/src/db/rocks/storage_types.rs @@ -9,7 +9,6 @@ use hyperlane_core::{ /// the key. #[derive(Debug, Copy, Clone)] pub(super) struct InterchainGasPaymentData { - pub destination: u32, pub payment: U256, pub gas_amount: U256, } @@ -25,7 +24,6 @@ pub(super) struct InterchainGasExpenditureData { impl Default for InterchainGasPaymentData { fn default() -> Self { Self { - destination: 0, payment: U256::zero(), gas_amount: U256::zero(), } @@ -33,10 +31,10 @@ impl Default for InterchainGasPaymentData { } impl InterchainGasPaymentData { - pub fn complete(self, message_id: H256) -> InterchainGasPayment { + pub fn complete(self, message_id: H256, destination: u32) -> InterchainGasPayment { InterchainGasPayment { message_id, - destination: self.destination, + destination, payment: self.payment, gas_amount: self.gas_amount, } @@ -46,7 +44,6 @@ impl InterchainGasPaymentData { impl From for InterchainGasPaymentData { fn from(p: InterchainGasPayment) -> Self { Self { - destination: p.destination, payment: p.payment, gas_amount: p.gas_amount, } @@ -69,7 +66,6 @@ impl Decode for InterchainGasPaymentData { Self: Sized, { Ok(Self { - destination: u32::read_from(reader)?, payment: U256::read_from(reader)?, gas_amount: U256::read_from(reader)?, }) diff --git a/rust/hyperlane-core/src/traits/encode.rs b/rust/hyperlane-core/src/traits/encode.rs index bf1287c7d1..98e40c6b8c 100644 --- a/rust/hyperlane-core/src/traits/encode.rs +++ b/rust/hyperlane-core/src/traits/encode.rs @@ -1,6 +1,6 @@ use std::io::{Error, ErrorKind}; -use crate::{HyperlaneProtocolError, H256, U256}; +use crate::{GasPaymentKey, HyperlaneProtocolError, H256, U256}; /// Simple trait for types with a canonical encoding pub trait Encode { @@ -171,3 +171,28 @@ impl Decode for bool { } } } + +impl Encode for GasPaymentKey { + fn write_to(&self, writer: &mut W) -> std::io::Result + where + W: std::io::Write, + { + let mut written = 0; + written += self.message_id.write_to(writer)?; + written += self.destination.write_to(writer)?; + Ok(written) + } +} + +impl Decode for GasPaymentKey { + fn read_from(reader: &mut R) -> Result + where + R: std::io::Read, + Self: Sized, + { + Ok(Self { + message_id: H256::read_from(reader)?, + destination: u32::read_from(reader)?, + }) + } +} diff --git a/rust/hyperlane-core/src/types/mod.rs b/rust/hyperlane-core/src/types/mod.rs index 5d58ff22dc..3143a3ed1a 100644 --- a/rust/hyperlane-core/src/types/mod.rs +++ b/rust/hyperlane-core/src/types/mod.rs @@ -103,6 +103,15 @@ impl From for ethers_core::types::Signature { } } +/// Key for the gas payment +#[derive(Debug, Copy, Clone)] +pub struct GasPaymentKey { + /// Id of the message + pub message_id: H256, + /// Destination domain paid for. + pub destination: u32, +} + /// A payment of a message's gas costs. #[derive(Debug, Copy, Clone)] pub struct InterchainGasPayment { From 4b4e4f15282cd0e68a763f97a7e5dc14f6d9f74e Mon Sep 17 00:00:00 2001 From: -f Date: Thu, 14 Sep 2023 16:36:06 -0400 Subject: [PATCH 25/27] import gaspaymentkey --- .github/workflows/node.yml | 46 +++++++++---------- .../agents/relayer/src/msg/gas_payment/mod.rs | 4 +- rust/hyperlane-core/src/traits/encode.rs | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index ba0843751d..2855529fe9 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -69,29 +69,29 @@ jobs: - name: core build run: yarn workspace @hyperlane-xyz/core build - # lint-prettier: - # runs-on: ubuntu-latest - # needs: [yarn-install] - # steps: - # - uses: actions/checkout@v3 - # - uses: actions/cache@v3 - # with: - # path: | - # **/node_modules - # .yarn/cache - # key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} - - # - name: lint - # run: yarn lint - - # - name: prettier - # run: | - # yarn prettier - # CHANGES=$(git status -s) - # if [[ ! -z $CHANGES ]]; then - # echo "Changes found: $CHANGES" - # exit 1 - # fi + lint-prettier: + runs-on: ubuntu-latest + needs: [yarn-install] + steps: + - uses: actions/checkout@v3 + - uses: actions/cache@v3 + with: + path: | + **/node_modules + .yarn/cache + key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} + + - name: lint + run: yarn lint + + - name: prettier + run: | + yarn prettier + CHANGES=$(git status -s) + if [[ ! -z $CHANGES ]]; then + echo "Changes found: $CHANGES" + exit 1 + fi # test-ts: # runs-on: ubuntu-latest diff --git a/rust/agents/relayer/src/msg/gas_payment/mod.rs b/rust/agents/relayer/src/msg/gas_payment/mod.rs index aa19c23fd8..abbccf996c 100644 --- a/rust/agents/relayer/src/msg/gas_payment/mod.rs +++ b/rust/agents/relayer/src/msg/gas_payment/mod.rs @@ -228,7 +228,7 @@ mod test { }; let hyperlane_db = HyperlaneRocksDB::new( - &HyperlaneDomain::new_test_domain("test_check_destination"), + &HyperlaneDomain::new_test_domain("test_different_destinations"), db, ); let enforcer = GasPaymentEnforcer::new( @@ -284,7 +284,7 @@ mod test { }; let hyperlane_db = HyperlaneRocksDB::new( - &HyperlaneDomain::new_test_domain("test_check_destination"), + &HyperlaneDomain::new_test_domain("test_half_and_half_payment"), db, ); diff --git a/rust/hyperlane-core/src/traits/encode.rs b/rust/hyperlane-core/src/traits/encode.rs index 9ab44a0fca..bd4b066eba 100644 --- a/rust/hyperlane-core/src/traits/encode.rs +++ b/rust/hyperlane-core/src/traits/encode.rs @@ -1,6 +1,6 @@ use std::io::{Error, ErrorKind}; -use crate::{HyperlaneProtocolError, H160, H256, H512, U256}; +use crate::{GasPaymentKey, HyperlaneProtocolError, H160, H256, H512, U256}; /// Simple trait for types with a canonical encoding pub trait Encode { From d3175fb094ce22e2eacfb70aca47dbc909ecd5f6 Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 15 Sep 2023 12:47:42 -0400 Subject: [PATCH 26/27] add random to LogMeta --- rust/agents/relayer/src/msg/gas_payment/mod.rs | 12 ++++++------ .../hyperlane-sealevel/src/interchain_gas.rs | 1 + rust/hyperlane-base/src/db/rocks/hyperlane_db.rs | 8 -------- rust/hyperlane-core/src/types/log_metadata.rs | 14 ++++++++++++++ 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/rust/agents/relayer/src/msg/gas_payment/mod.rs b/rust/agents/relayer/src/msg/gas_payment/mod.rs index abbccf996c..fab0a8d893 100644 --- a/rust/agents/relayer/src/msg/gas_payment/mod.rs +++ b/rust/agents/relayer/src/msg/gas_payment/mod.rs @@ -247,7 +247,7 @@ mod test { payment: U256::one(), gas_amount: U256::one(), }; - hyperlane_db.process_gas_payment(wrong_destination_payment, &LogMeta::default()); + hyperlane_db.process_gas_payment(wrong_destination_payment, &LogMeta::random()); // Ensure if the gas payment was made to the incorrect destination, it does not meet // the requirement assert!(enforcer @@ -258,11 +258,11 @@ mod test { let correct_destination_payment = InterchainGasPayment { message_id: msg.id(), - destination: 123, + destination: msg.destination, payment: U256::one(), gas_amount: U256::one(), }; - hyperlane_db.process_gas_payment(correct_destination_payment, &LogMeta::default()); + hyperlane_db.process_gas_payment(correct_destination_payment, &LogMeta::random()); // Ensure if the gas payment was made to the correct destination, it meets the // requirement assert!(enforcer @@ -304,7 +304,7 @@ mod test { payment: U256::one(), gas_amount: U256::one(), }; - hyperlane_db.process_gas_payment(initial_payment, &LogMeta::default()); + hyperlane_db.process_gas_payment(initial_payment, &LogMeta::random()); // Ensure if only half gas payment was made, it does not meet the requirement assert!(enforcer @@ -315,11 +315,11 @@ mod test { let deficit_payment = InterchainGasPayment { message_id: msg.id(), - destination: 123, + destination: msg.destination, payment: U256::one(), gas_amount: U256::one(), }; - hyperlane_db.process_gas_payment(deficit_payment, &LogMeta::default()); + hyperlane_db.process_gas_payment(deficit_payment, &LogMeta::random()); // Ensure if the full gas payment was made, it meets the requirement assert!(enforcer .message_meets_gas_payment_requirement(&msg, &TxCostEstimate::default(),) diff --git a/rust/chains/hyperlane-sealevel/src/interchain_gas.rs b/rust/chains/hyperlane-sealevel/src/interchain_gas.rs index be64643935..c7c2f3a753 100644 --- a/rust/chains/hyperlane-sealevel/src/interchain_gas.rs +++ b/rust/chains/hyperlane-sealevel/src/interchain_gas.rs @@ -210,6 +210,7 @@ impl SealevelInterchainGasPaymasterIndexer { let igp_payment = InterchainGasPayment { message_id: gas_payment_account.message_id, + destination: gas_payment_account.destination_domain, payment: gas_payment_account.payment.into(), gas_amount: gas_payment_account.gas_amount.into(), }; diff --git a/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs b/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs index 6e95f01edc..2a6420bc74 100644 --- a/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs +++ b/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs @@ -165,9 +165,7 @@ impl HyperlaneRocksDB { message_id: event.message_id, destination: event.destination, }; - println!("update_gas_payment_by_message_id: {:?}", key); let existing_payment = self.retrieve_gas_payment_by_message_id(key)?; - println!("existing_payment: {:?}", existing_payment); let total = existing_payment + event; debug!(?event, new_total_gas_payment=?total, "Storing gas payment"); @@ -200,12 +198,6 @@ impl HyperlaneRocksDB { &self, key: GasPaymentKey, ) -> DbResult { - println!( - "retrieve_gas_payment_by_message_id: {:?}", - self.retrieve_interchain_gas_payment_data_by_message_id(&key)? - .unwrap_or_default() - .complete(key.message_id, key.destination) - ); Ok(self .retrieve_interchain_gas_payment_data_by_message_id(&key)? .unwrap_or_default() diff --git a/rust/hyperlane-core/src/types/log_metadata.rs b/rust/hyperlane-core/src/types/log_metadata.rs index 263caad155..bdb4b043fe 100644 --- a/rust/hyperlane-core/src/types/log_metadata.rs +++ b/rust/hyperlane-core/src/types/log_metadata.rs @@ -67,3 +67,17 @@ impl Ord for LogMeta { self.partial_cmp(other).unwrap() } } + +impl LogMeta { + /// Create a new LogMeta with random transaction ID + pub fn random() -> Self { + Self { + address: H256::zero(), + block_number: 1, + block_hash: H256::zero(), + transaction_id: H512::random(), + transaction_index: 0, + log_index: U256::zero(), + } + } +} From 9e23ac92a7a4b097c1da2156539d4674bd9385f9 Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 15 Sep 2023 12:59:38 -0400 Subject: [PATCH 27/27] rename to _gas_payment_key --- .../agents/relayer/src/msg/gas_payment/mod.rs | 4 ++-- .../src/db/rocks/hyperlane_db.rs | 20 +++++++++---------- solidity/test/router.test.ts | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/rust/agents/relayer/src/msg/gas_payment/mod.rs b/rust/agents/relayer/src/msg/gas_payment/mod.rs index fab0a8d893..fa4497c6f8 100644 --- a/rust/agents/relayer/src/msg/gas_payment/mod.rs +++ b/rust/agents/relayer/src/msg/gas_payment/mod.rs @@ -78,13 +78,13 @@ impl GasPaymentEnforcer { tx_cost_estimate: &TxCostEstimate, ) -> Result> { let msg_id = message.id(); - let gap_payment_key = GasPaymentKey { + let gas_payment_key = GasPaymentKey { message_id: msg_id, destination: message.destination, }; let current_payment = self .db - .retrieve_gas_payment_by_message_id(gap_payment_key)?; + .retrieve_gas_payment_by_gas_payment_key(gas_payment_key)?; let current_expenditure = self.db.retrieve_gas_expenditure_by_message_id(msg_id)?; for (policy, whitelist) in &self.policies { if !whitelist.msg_matches(message, true) { diff --git a/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs b/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs index 2a6420bc74..aa186b45f6 100644 --- a/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs +++ b/rust/hyperlane-base/src/db/rocks/hyperlane_db.rs @@ -146,7 +146,7 @@ impl HyperlaneRocksDB { self.store_processed_by_gas_payment_meta(&payment_meta, &true)?; // Update the total gas payment for the message to include the payment - self.update_gas_payment_by_message_id(payment)?; + self.update_gas_payment_by_gas_payment_key(payment)?; // Return true to indicate the gas payment was processed for the first time Ok(true) @@ -160,16 +160,16 @@ impl HyperlaneRocksDB { } /// Update the total gas payment for a message to include gas_payment - fn update_gas_payment_by_message_id(&self, event: InterchainGasPayment) -> DbResult<()> { - let key = GasPaymentKey { + fn update_gas_payment_by_gas_payment_key(&self, event: InterchainGasPayment) -> DbResult<()> { + let gas_payment_key = GasPaymentKey { message_id: event.message_id, destination: event.destination, }; - let existing_payment = self.retrieve_gas_payment_by_message_id(key)?; + let existing_payment = self.retrieve_gas_payment_by_gas_payment_key(gas_payment_key)?; let total = existing_payment + event; debug!(?event, new_total_gas_payment=?total, "Storing gas payment"); - self.store_interchain_gas_payment_data_by_message_id(&key, &total.into())?; + self.store_interchain_gas_payment_data_by_gas_payment_key(&gas_payment_key, &total.into())?; Ok(()) } @@ -194,14 +194,14 @@ impl HyperlaneRocksDB { } /// Retrieve the total gas payment for a message - pub fn retrieve_gas_payment_by_message_id( + pub fn retrieve_gas_payment_by_gas_payment_key( &self, - key: GasPaymentKey, + gas_payment_key: GasPaymentKey, ) -> DbResult { Ok(self - .retrieve_interchain_gas_payment_data_by_message_id(&key)? + .retrieve_interchain_gas_payment_data_by_gas_payment_key(&gas_payment_key)? .unwrap_or_default() - .complete(key.message_id, key.destination)) + .complete(gas_payment_key.message_id, gas_payment_key.destination)) } /// Retrieve the total gas payment for a message @@ -319,7 +319,7 @@ make_store_and_retrieve!(pub(self), dispatched_block_number_by_nonce, MESSAGE_DI make_store_and_retrieve!(pub, processed_by_nonce, NONCE_PROCESSED, u32, bool); make_store_and_retrieve!(pub(self), processed_by_gas_payment_meta, GAS_PAYMENT_META_PROCESSED, InterchainGasPaymentMeta, bool); make_store_and_retrieve!(pub(self), interchain_gas_expenditure_data_by_message_id, GAS_EXPENDITURE_FOR_MESSAGE_ID, H256, InterchainGasExpenditureData); -make_store_and_retrieve!(pub(self), interchain_gas_payment_data_by_message_id, GAS_PAYMENT_FOR_MESSAGE_ID, GasPaymentKey, InterchainGasPaymentData); +make_store_and_retrieve!(pub(self), interchain_gas_payment_data_by_gas_payment_key, GAS_PAYMENT_FOR_MESSAGE_ID, GasPaymentKey, InterchainGasPaymentData); make_store_and_retrieve!( pub, pending_message_retry_count_by_message_id, diff --git a/solidity/test/router.test.ts b/solidity/test/router.test.ts index 86fbe2333e..e6ae6935a5 100644 --- a/solidity/test/router.test.ts +++ b/solidity/test/router.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { BigNumberish } from 'ethers'; +import { BigNumberish, ContractTransaction } from 'ethers'; import { ethers } from 'hardhat'; import { addressToBytes32 } from '@hyperlane-xyz/utils';