From 1de383da8f1e885edb574a6972ed4656083380a9 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 7 Dec 2023 16:28:54 +0100 Subject: [PATCH 1/7] feat: hyperlane initial commit --- .../hyperlane/IncentivizedHyperlaneEscrow.sol | 120 ++++++++++++++ src/apps/hyperlane/interfaces/IGasOracle.sol | 15 ++ .../interfaces/IInterchainGasPaymaster.sol | 35 ++++ .../interfaces/IInterchainSecurityModule.sol | 42 +++++ .../ILiquidityLayerMessageRecipient.sol | 12 ++ .../interfaces/ILiquidityLayerRouter.sol | 13 ++ src/apps/hyperlane/interfaces/IMailbox.sol | 109 +++++++++++++ .../interfaces/IMessageRecipient.sol | 10 ++ src/apps/hyperlane/interfaces/IRouter.sol | 15 ++ .../interfaces/IValidatorAnnounce.sol | 29 ++++ .../interfaces/hooks/IMessageDispatcher.sol | 30 ++++ .../interfaces/hooks/IPostDispatchHook.sol | 63 ++++++++ .../interfaces/isms/IAggregationIsm.sol | 18 +++ .../interfaces/isms/ICcipReadIsm.sol | 28 ++++ .../interfaces/isms/IMultisigIsm.sol | 18 +++ .../hyperlane/interfaces/isms/IRoutingIsm.sol | 16 ++ .../optimism/ICrossDomainMessenger.sol | 39 +++++ src/apps/hyperlane/libs/Message.sol | 153 ++++++++++++++++++ src/apps/hyperlane/libs/TypeCasts.sol | 14 ++ 19 files changed, 779 insertions(+) create mode 100644 src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol create mode 100644 src/apps/hyperlane/interfaces/IGasOracle.sol create mode 100644 src/apps/hyperlane/interfaces/IInterchainGasPaymaster.sol create mode 100644 src/apps/hyperlane/interfaces/IInterchainSecurityModule.sol create mode 100644 src/apps/hyperlane/interfaces/ILiquidityLayerMessageRecipient.sol create mode 100644 src/apps/hyperlane/interfaces/ILiquidityLayerRouter.sol create mode 100644 src/apps/hyperlane/interfaces/IMailbox.sol create mode 100644 src/apps/hyperlane/interfaces/IMessageRecipient.sol create mode 100644 src/apps/hyperlane/interfaces/IRouter.sol create mode 100644 src/apps/hyperlane/interfaces/IValidatorAnnounce.sol create mode 100644 src/apps/hyperlane/interfaces/hooks/IMessageDispatcher.sol create mode 100644 src/apps/hyperlane/interfaces/hooks/IPostDispatchHook.sol create mode 100644 src/apps/hyperlane/interfaces/isms/IAggregationIsm.sol create mode 100644 src/apps/hyperlane/interfaces/isms/ICcipReadIsm.sol create mode 100644 src/apps/hyperlane/interfaces/isms/IMultisigIsm.sol create mode 100644 src/apps/hyperlane/interfaces/isms/IRoutingIsm.sol create mode 100644 src/apps/hyperlane/interfaces/optimism/ICrossDomainMessenger.sol create mode 100644 src/apps/hyperlane/libs/Message.sol create mode 100644 src/apps/hyperlane/libs/TypeCasts.sol diff --git a/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol b/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol new file mode 100644 index 0000000..084eec1 --- /dev/null +++ b/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; + + +import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "./interfaces/IInterchainSecurityModule.sol"; +import { Message } from "./libs/Message.sol"; +import { IMailbox } from "./interfaces/IMailbox.sol"; + +interface IVersioned { + function VERSION() view external returns(uint8); +} + +/// @notice Hyperlane implementation of Generalised incentives. +contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow { + // ============ Libraries ============ + + using Message for bytes; + + + error BadChainIdentifier(); + + address CUSTOM_HOOK = address(this); + bytes NOTHING = hex""; + + uint32 public immutable localDomain; + IMailbox public immutable MAILBOX; + uint8 public immutable VERSION; + + constructor(address sendLostGasTo, address mailbox_) IncentivizedMessageEscrow(sendLostGasTo){ + MAILBOX = IMailbox(mailbox_); + + // Collect the chain identifier from the mailbox and store it here. + // localDomain is immutable on mailbox. + localDomain = MAILBOX.localDomain(); + VERSION = IVersioned(mailbox_).VERSION(); + } + + function _quoteDispatch() internal view returns(uint256 amount) { + amount = MAILBOX.quoteDispatch(uint32(0), address(0), NOTHING, NOTHING, CUSTOM_HOOK); + } + + function estimateAdditionalCost() external view returns(address asset, uint256 amount) { + asset = address(0); + amount = _quoteDispatch(); + } + + function _getMessageIdentifier( + bytes32 destinationIdentifier, + bytes calldata message + ) internal override view returns(bytes32) { + return keccak256( + abi.encodePacked( + bytes32(block.number), + localDomain, + destinationIdentifier, + message + ) + ); + } + + function _verifyPacket(bytes calldata _metadata, bytes calldata _message) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { + /// CHECKS /// + + // Check that the message was intended for this mailbox. + require(_message.version() == VERSION, "Mailbox: bad version"); + require( + _message.destination() == localDomain, + "Mailbox: unexpected destination" + ); + + // Get the recipient's ISM. + address recipient = _message.recipientAddress(); + IInterchainSecurityModule ism = MAILBOX.recipientIsm(recipient); + + /// EFFECTS /// + + sourceIdentifier = bytes32(_message.origin()); + implementationIdentifier = abi.encodePacked(_message.sender()); + + /// INTERACTIONS /// + + // Verify the message via the interchain security module. + require( + ism.verify(_metadata, _message), + "Mailbox: ISM verification failed" + ); + + // Load the identifier for the calling contract. + implementationIdentifier = abi.encodePacked(vm.emitterAddress); + + // Local "supposedly" this chain identifier. + bytes32 thisChainIdentifier = bytes32(payload[0:32]); + + // Check that the message is intended for this chain. + if (thisChainIdentifier != bytes32(uint256(chainId()))) revert BadChainIdentifier(); + + // Local the identifier for the source chain. + sourceIdentifier = bytes32(bytes2(vm.emitterChainId)); + + // Get the application message. + message_ = payload[32:]; + } + + function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfsendPacketInNativeToken) { + // Get the cost of sending wormhole messages. + costOfsendPacketInNativeToken = uint128(_quoteDispatch()); + uint32 destinationDomain = uint32(destinationChainIdentifier); + + // Handoff the message to hyperlane + MAILBOX.dispatch{value: costOfsendPacketInNativeToken}( + uint32(destinationDomain), + bytes32(destinationImplementation), + message, + hex"", + CUSTOM_HOOK + ); + } +} \ No newline at end of file diff --git a/src/apps/hyperlane/interfaces/IGasOracle.sol b/src/apps/hyperlane/interfaces/IGasOracle.sol new file mode 100644 index 0000000..1d4251c --- /dev/null +++ b/src/apps/hyperlane/interfaces/IGasOracle.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +interface IGasOracle { + struct RemoteGasData { + // The exchange rate of the remote native token quoted in the local native token. + // Scaled with 10 decimals, i.e. 1e10 is "one". + uint128 tokenExchangeRate; + uint128 gasPrice; + } + + function getExchangeRateAndGasPrice( + uint32 _destinationDomain + ) external view returns (uint128 tokenExchangeRate, uint128 gasPrice); +} diff --git a/src/apps/hyperlane/interfaces/IInterchainGasPaymaster.sol b/src/apps/hyperlane/interfaces/IInterchainGasPaymaster.sol new file mode 100644 index 0000000..405bd75 --- /dev/null +++ b/src/apps/hyperlane/interfaces/IInterchainGasPaymaster.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +/** + * @title IInterchainGasPaymaster + * @notice Manages payments on a source chain to cover gas costs of relaying + * messages to destination chains. + */ +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 + ); + + function payForGas( + bytes32 _messageId, + uint32 _destinationDomain, + uint256 _gasAmount, + address _refundAddress + ) external payable; + + function quoteGasPayment( + uint32 _destinationDomain, + uint256 _gasAmount + ) external view returns (uint256); +} diff --git a/src/apps/hyperlane/interfaces/IInterchainSecurityModule.sol b/src/apps/hyperlane/interfaces/IInterchainSecurityModule.sol new file mode 100644 index 0000000..d4e6c30 --- /dev/null +++ b/src/apps/hyperlane/interfaces/IInterchainSecurityModule.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +interface IInterchainSecurityModule { + enum Types { + UNUSED, + ROUTING, + AGGREGATION, + LEGACY_MULTISIG, + MERKLE_ROOT_MULTISIG, + MESSAGE_ID_MULTISIG, + NULL, // used with relayer carrying no metadata + CCIP_READ + } + + /** + * @notice Returns an enum that represents the type of security model + * encoded by this ISM. + * @dev Relayers infer how to fetch and format metadata. + */ + function moduleType() external view returns (uint8); + + /** + * @notice Defines a security model responsible for verifying interchain + * messages based on the provided metadata. + * @param _metadata Off-chain metadata provided by a relayer, specific to + * the security model encoded by the module (e.g. validator signatures) + * @param _message Hyperlane encoded interchain message + * @return True if the message was verified + */ + function verify( + bytes calldata _metadata, + bytes calldata _message + ) external returns (bool); +} + +interface ISpecifiesInterchainSecurityModule { + function interchainSecurityModule() + external + view + returns (IInterchainSecurityModule); +} diff --git a/src/apps/hyperlane/interfaces/ILiquidityLayerMessageRecipient.sol b/src/apps/hyperlane/interfaces/ILiquidityLayerMessageRecipient.sol new file mode 100644 index 0000000..1fc03e3 --- /dev/null +++ b/src/apps/hyperlane/interfaces/ILiquidityLayerMessageRecipient.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +interface ILiquidityLayerMessageRecipient { + function handleWithTokens( + uint32 _origin, + bytes32 _sender, + bytes calldata _message, + address _token, + uint256 _amount + ) external; +} diff --git a/src/apps/hyperlane/interfaces/ILiquidityLayerRouter.sol b/src/apps/hyperlane/interfaces/ILiquidityLayerRouter.sol new file mode 100644 index 0000000..db9c254 --- /dev/null +++ b/src/apps/hyperlane/interfaces/ILiquidityLayerRouter.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +interface ILiquidityLayerRouter { + function dispatchWithTokens( + uint32 _destinationDomain, + bytes32 _recipientAddress, + address _token, + uint256 _amount, + string calldata _bridge, + bytes calldata _messageBody + ) external returns (bytes32); +} diff --git a/src/apps/hyperlane/interfaces/IMailbox.sol b/src/apps/hyperlane/interfaces/IMailbox.sol new file mode 100644 index 0000000..60d2e9a --- /dev/null +++ b/src/apps/hyperlane/interfaces/IMailbox.sol @@ -0,0 +1,109 @@ +// 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 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 + ); + + /** + * @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 requiredHook() external view returns (IPostDispatchHook); + + function latestDispatchedId() external view returns (bytes32); + + function dispatch( + uint32 destinationDomain, + bytes32 recipientAddress, + 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, + bytes calldata body, + 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, + 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; + + function recipientIsm( + address recipient + ) external view returns (IInterchainSecurityModule module); +} diff --git a/src/apps/hyperlane/interfaces/IMessageRecipient.sol b/src/apps/hyperlane/interfaces/IMessageRecipient.sol new file mode 100644 index 0000000..187194b --- /dev/null +++ b/src/apps/hyperlane/interfaces/IMessageRecipient.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; +} diff --git a/src/apps/hyperlane/interfaces/IRouter.sol b/src/apps/hyperlane/interfaces/IRouter.sol new file mode 100644 index 0000000..a26020d --- /dev/null +++ b/src/apps/hyperlane/interfaces/IRouter.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +interface IRouter { + function domains() external view returns (uint32[] memory); + + function routers(uint32 _domain) external view returns (bytes32); + + function enrollRemoteRouter(uint32 _domain, bytes32 _router) external; + + function enrollRemoteRouters( + uint32[] calldata _domains, + bytes32[] calldata _routers + ) external; +} diff --git a/src/apps/hyperlane/interfaces/IValidatorAnnounce.sol b/src/apps/hyperlane/interfaces/IValidatorAnnounce.sol new file mode 100644 index 0000000..938eb92 --- /dev/null +++ b/src/apps/hyperlane/interfaces/IValidatorAnnounce.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +interface IValidatorAnnounce { + /// @notice Returns a list of validators that have made announcements + function getAnnouncedValidators() external view returns (address[] memory); + + /** + * @notice Returns a list of all announced storage locations for `validators` + * @param _validators The list of validators to get storage locations for + * @return A list of announced storage locations + */ + function getAnnouncedStorageLocations( + address[] calldata _validators + ) external view returns (string[][] memory); + + /** + * @notice Announces a validator signature storage location + * @param _storageLocation Information encoding the location of signed + * checkpoints + * @param _signature The signed validator announcement + * @return True upon success + */ + function announce( + address _validator, + string calldata _storageLocation, + bytes calldata _signature + ) external returns (bool); +} diff --git a/src/apps/hyperlane/interfaces/hooks/IMessageDispatcher.sol b/src/apps/hyperlane/interfaces/hooks/IMessageDispatcher.sol new file mode 100644 index 0000000..b905868 --- /dev/null +++ b/src/apps/hyperlane/interfaces/hooks/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/src/apps/hyperlane/interfaces/hooks/IPostDispatchHook.sol b/src/apps/hyperlane/interfaces/hooks/IPostDispatchHook.sol new file mode 100644 index 0000000..69a44c7 --- /dev/null +++ b/src/apps/hyperlane/interfaces/hooks/IPostDispatchHook.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +interface IPostDispatchHook { + enum Types { + UNUSED, + ROUTING, + AGGREGATION, + MERKLE_TREE, + INTERCHAIN_GAS_PAYMASTER, + FALLBACK_ROUTING, + ID_AUTH_ISM, + PAUSABLE, + PROTOCOL_FEE + } + + /** + * @notice Returns an enum that represents the type of hook + */ + function hookType() external view returns (uint8); + + /** + * @notice Returns whether the hook supports metadata + * @param metadata metadata + * @return Whether the hook supports metadata + */ + function supportsMetadata( + bytes calldata metadata + ) external view returns (bool); + + /** + * @notice Post action after 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 Compute the payment required by the postDispatch call + * @param metadata The metadata required for the hook + * @param message The message passed from the Mailbox.dispatch() call + * @return Quoted payment for the postDispatch call + */ + function quoteDispatch( + bytes calldata metadata, + bytes calldata message + ) external view returns (uint256); +} diff --git a/src/apps/hyperlane/interfaces/isms/IAggregationIsm.sol b/src/apps/hyperlane/interfaces/isms/IAggregationIsm.sol new file mode 100644 index 0000000..3f87260 --- /dev/null +++ b/src/apps/hyperlane/interfaces/isms/IAggregationIsm.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +import {IInterchainSecurityModule} from "../IInterchainSecurityModule.sol"; + +interface IAggregationIsm is IInterchainSecurityModule { + /** + * @notice Returns the set of modules responsible for verifying _message + * and the number of modules that must verify + * @dev Can change based on the content of _message + * @param _message Hyperlane formatted interchain message + * @return modules The array of ISM addresses + * @return threshold The number of modules needed to verify + */ + function modulesAndThreshold( + bytes calldata _message + ) external view returns (address[] memory modules, uint8 threshold); +} diff --git a/src/apps/hyperlane/interfaces/isms/ICcipReadIsm.sol b/src/apps/hyperlane/interfaces/isms/ICcipReadIsm.sol new file mode 100644 index 0000000..d0534b0 --- /dev/null +++ b/src/apps/hyperlane/interfaces/isms/ICcipReadIsm.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {IInterchainSecurityModule} from "../IInterchainSecurityModule.sol"; + +interface ICcipReadIsm is IInterchainSecurityModule { + /// @dev https://eips.ethereum.org/EIPS/eip-3668 + /// @param sender the address of the contract making the call, usually address(this) + /// @param urls the URLs to query for offchain data + /// @param callData context needed for offchain service to service request + /// @param callbackFunction function selector to call with offchain information + /// @param extraData additional passthrough information to call callbackFunction with + error OffchainLookup( + address sender, + string[] urls, + bytes callData, + bytes4 callbackFunction, + bytes extraData + ); + + /** + * @notice Reverts with the data needed to query information offchain + * and be submitted via the origin mailbox + * @dev See https://eips.ethereum.org/EIPS/eip-3668 for more information + * @param _message data that will help construct the offchain query + */ + function getOffchainVerifyInfo(bytes calldata _message) external view; +} diff --git a/src/apps/hyperlane/interfaces/isms/IMultisigIsm.sol b/src/apps/hyperlane/interfaces/isms/IMultisigIsm.sol new file mode 100644 index 0000000..fa4767f --- /dev/null +++ b/src/apps/hyperlane/interfaces/isms/IMultisigIsm.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +import {IInterchainSecurityModule} from "../IInterchainSecurityModule.sol"; + +interface IMultisigIsm is IInterchainSecurityModule { + /** + * @notice Returns the set of validators responsible for verifying _message + * and the number of signatures required + * @dev Can change based on the content of _message + * @param _message Hyperlane formatted interchain message + * @return validators The array of validator addresses + * @return threshold The number of validator signatures needed + */ + function validatorsAndThreshold( + bytes calldata _message + ) external view returns (address[] memory validators, uint8 threshold); +} diff --git a/src/apps/hyperlane/interfaces/isms/IRoutingIsm.sol b/src/apps/hyperlane/interfaces/isms/IRoutingIsm.sol new file mode 100644 index 0000000..65826da --- /dev/null +++ b/src/apps/hyperlane/interfaces/isms/IRoutingIsm.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {IInterchainSecurityModule} from "../IInterchainSecurityModule.sol"; + +interface IRoutingIsm is IInterchainSecurityModule { + /** + * @notice Returns the ISM responsible for verifying _message + * @dev Can change based on the content of _message + * @param _message Formatted Hyperlane message (see Message.sol). + * @return module The ISM to use to verify _message + */ + function route( + bytes calldata _message + ) external view returns (IInterchainSecurityModule); +} diff --git a/src/apps/hyperlane/interfaces/optimism/ICrossDomainMessenger.sol b/src/apps/hyperlane/interfaces/optimism/ICrossDomainMessenger.sol new file mode 100644 index 0000000..9bffafc --- /dev/null +++ b/src/apps/hyperlane/interfaces/optimism/ICrossDomainMessenger.sol @@ -0,0 +1,39 @@ +// 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; + + function relayMessage( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes calldata _message + ) external payable; + + function xDomainMessageSender() external view returns (address); + + function OTHER_MESSENGER() external view returns (address); +} + +interface IL1CrossDomainMessenger is ICrossDomainMessenger {} + +interface IL2CrossDomainMessenger is ICrossDomainMessenger { + function messageNonce() external view returns (uint256); +} diff --git a/src/apps/hyperlane/libs/Message.sol b/src/apps/hyperlane/libs/Message.sol new file mode 100644 index 0000000..3eb6912 --- /dev/null +++ b/src/apps/hyperlane/libs/Message.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import {TypeCasts} from "./TypeCasts.sol"; + +/** + * @title Hyperlane Message Library + * @notice Library for formatted messages used by Mailbox + **/ +library Message { + using TypeCasts for bytes32; + + uint256 private constant VERSION_OFFSET = 0; + uint256 private constant NONCE_OFFSET = 1; + uint256 private constant ORIGIN_OFFSET = 5; + uint256 private constant SENDER_OFFSET = 9; + uint256 private constant DESTINATION_OFFSET = 41; + uint256 private constant RECIPIENT_OFFSET = 45; + uint256 private constant BODY_OFFSET = 77; + + /** + * @notice Returns formatted (packed) Hyperlane message with provided fields + * @dev This function should only be used in memory message construction. + * @param _version The version of the origin and destination Mailboxes + * @param _nonce A nonce to uniquely identify the message on its origin chain + * @param _originDomain Domain of origin chain + * @param _sender Address of sender as bytes32 + * @param _destinationDomain Domain of destination chain + * @param _recipient Address of recipient on destination chain as bytes32 + * @param _messageBody Raw bytes of message body + * @return Formatted message + */ + function formatMessage( + uint8 _version, + uint32 _nonce, + uint32 _originDomain, + bytes32 _sender, + uint32 _destinationDomain, + bytes32 _recipient, + bytes calldata _messageBody + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _version, + _nonce, + _originDomain, + _sender, + _destinationDomain, + _recipient, + _messageBody + ); + } + + /** + * @notice Returns the message ID. + * @param _message ABI encoded Hyperlane message. + * @return ID of `_message` + */ + function id(bytes memory _message) internal pure returns (bytes32) { + return keccak256(_message); + } + + /** + * @notice Returns the message version. + * @param _message ABI encoded Hyperlane message. + * @return Version of `_message` + */ + function version(bytes calldata _message) internal pure returns (uint8) { + return uint8(bytes1(_message[VERSION_OFFSET:NONCE_OFFSET])); + } + + /** + * @notice Returns the message nonce. + * @param _message ABI encoded Hyperlane message. + * @return Nonce of `_message` + */ + function nonce(bytes calldata _message) internal pure returns (uint32) { + return uint32(bytes4(_message[NONCE_OFFSET:ORIGIN_OFFSET])); + } + + /** + * @notice Returns the message origin domain. + * @param _message ABI encoded Hyperlane message. + * @return Origin domain of `_message` + */ + function origin(bytes calldata _message) internal pure returns (uint32) { + return uint32(bytes4(_message[ORIGIN_OFFSET:SENDER_OFFSET])); + } + + /** + * @notice Returns the message sender as bytes32. + * @param _message ABI encoded Hyperlane message. + * @return Sender of `_message` as bytes32 + */ + function sender(bytes calldata _message) internal pure returns (bytes32) { + return bytes32(_message[SENDER_OFFSET:DESTINATION_OFFSET]); + } + + /** + * @notice Returns the message sender as address. + * @param _message ABI encoded Hyperlane message. + * @return Sender of `_message` as address + */ + function senderAddress( + bytes calldata _message + ) internal pure returns (address) { + return sender(_message).bytes32ToAddress(); + } + + /** + * @notice Returns the message destination domain. + * @param _message ABI encoded Hyperlane message. + * @return Destination domain of `_message` + */ + function destination( + bytes calldata _message + ) internal pure returns (uint32) { + return uint32(bytes4(_message[DESTINATION_OFFSET:RECIPIENT_OFFSET])); + } + + /** + * @notice Returns the message recipient as bytes32. + * @param _message ABI encoded Hyperlane message. + * @return Recipient of `_message` as bytes32 + */ + function recipient( + bytes calldata _message + ) internal pure returns (bytes32) { + return bytes32(_message[RECIPIENT_OFFSET:BODY_OFFSET]); + } + + /** + * @notice Returns the message recipient as address. + * @param _message ABI encoded Hyperlane message. + * @return Recipient of `_message` as address + */ + function recipientAddress( + bytes calldata _message + ) internal pure returns (address) { + return recipient(_message).bytes32ToAddress(); + } + + /** + * @notice Returns the message body. + * @param _message ABI encoded Hyperlane message. + * @return Body of `_message` + */ + function body( + bytes calldata _message + ) internal pure returns (bytes calldata) { + return bytes(_message[BODY_OFFSET:]); + } +} diff --git a/src/apps/hyperlane/libs/TypeCasts.sol b/src/apps/hyperlane/libs/TypeCasts.sol new file mode 100644 index 0000000..4440f63 --- /dev/null +++ b/src/apps/hyperlane/libs/TypeCasts.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.11; + +library TypeCasts { + // alignment preserving cast + function addressToBytes32(address _addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } + + // alignment preserving cast + function bytes32ToAddress(bytes32 _buf) internal pure returns (address) { + return address(uint160(uint256(_buf))); + } +} From 51e46535668cf1fa810c9c845229cf8eb4408f35 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 8 Dec 2023 12:56:23 +0100 Subject: [PATCH 2/7] feat: replacement hook for default hook which allows underwriting the IGP --- src/apps/hyperlane/ReplacementHook.sol | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/apps/hyperlane/ReplacementHook.sol diff --git a/src/apps/hyperlane/ReplacementHook.sol b/src/apps/hyperlane/ReplacementHook.sol new file mode 100644 index 0000000..7e99bf0 --- /dev/null +++ b/src/apps/hyperlane/ReplacementHook.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; + +import { IPostDispatchHook } from "./interfaces/hooks/IPostDispatchHook.sol"; + +/// @notice Hyperlane implementation of Generalised incentives. +contract ReplacementHook is IPostDispatchHook { + /** + * @notice Returns an enum that represents the type of hook + */ + function hookType() external pure returns (uint8) { + return uint8(Types.INTERCHAIN_GAS_PAYMASTER); + } + + /** + * @notice Returns whether the hook supports metadata + * @dev The following params aren't used: + * param metadata metadata + * @return Whether the hook supports metadata + */ + function supportsMetadata( + bytes calldata /* metadata */ + ) external pure returns (bool) { + return false; + } + + /** + * @notice Post action after a message is dispatched via the Mailbox + * @dev The following params aren't used: + * 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 { + return; + } + + /** + * @notice Compute the payment required by the postDispatch call + * @dev The following params aren't used: + * param metadata The metadata required for the hook + * param message The message passed from the Mailbox.dispatch() call + * @return Quoted payment for the postDispatch call + */ + function quoteDispatch( + bytes calldata /* metadata */, + bytes calldata /* message */ + ) external pure returns (uint256) { + return 0; + } +} \ No newline at end of file From 4a3472a282e88c988b457668b4fb2d44dc6143ed Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 8 Dec 2023 13:15:48 +0100 Subject: [PATCH 3/7] feat: organise hyperlane escrow --- .../hyperlane/IncentivizedHyperlaneEscrow.sol | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol b/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol index 084eec1..ecd7ed0 100644 --- a/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol +++ b/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.13; import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; - +import { ReplacementHook } from "./ReplacementHook.sol"; import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "./interfaces/IInterchainSecurityModule.sol"; +import { IPostDispatchHook } from "./interfaces/hooks/IPostDispatchHook.sol"; import { Message } from "./libs/Message.sol"; import { IMailbox } from "./interfaces/IMailbox.sol"; @@ -13,15 +14,19 @@ interface IVersioned { } /// @notice Hyperlane implementation of Generalised incentives. -contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow { +contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow, ReplacementHook { // ============ Libraries ============ using Message for bytes; error BadChainIdentifier(); + error BadMailboxVersion(uint8 VERSION, uint8 messageVersion); + error BadDomain(uint32 localDomain, uint32 messageDestination); + error ISMVerificationFailed(); + - address CUSTOM_HOOK = address(this); + IPostDispatchHook CUSTOM_HOOK = IPostDispatchHook(address(this)); bytes NOTHING = hex""; uint32 public immutable localDomain; @@ -37,13 +42,17 @@ contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow { VERSION = IVersioned(mailbox_).VERSION(); } - function _quoteDispatch() internal view returns(uint256 amount) { - amount = MAILBOX.quoteDispatch(uint32(0), address(0), NOTHING, NOTHING, CUSTOM_HOOK); + function _requiredHookQuote() internal view returns(uint256 amount) { + IPostDispatchHook requiredHook = MAILBOX.requiredHook(); + + bytes memory message = NOTHING; + + return amount = requiredHook.quoteDispatch(NOTHING, message); } function estimateAdditionalCost() external view returns(address asset, uint256 amount) { asset = address(0); - amount = _quoteDispatch(); + amount = _requiredHookQuote(); } function _getMessageIdentifier( @@ -60,15 +69,12 @@ contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow { ); } - function _verifyPacket(bytes calldata _metadata, bytes calldata _message) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { + function _verifyPacket(bytes calldata _metadata, bytes calldata _message) internal override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { /// CHECKS /// // Check that the message was intended for this mailbox. - require(_message.version() == VERSION, "Mailbox: bad version"); - require( - _message.destination() == localDomain, - "Mailbox: unexpected destination" - ); + if(VERSION != _message.version()) revert BadMailboxVersion(VERSION, _message.version()); + if (localDomain != _message.destination()) revert BadDomain(localDomain, _message.destination()); // Get the recipient's ISM. address recipient = _message.recipientAddress(); @@ -76,37 +82,22 @@ contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow { /// EFFECTS /// - sourceIdentifier = bytes32(_message.origin()); + sourceIdentifier = bytes32(uint256(_message.origin())); implementationIdentifier = abi.encodePacked(_message.sender()); /// INTERACTIONS /// // Verify the message via the interchain security module. - require( - ism.verify(_metadata, _message), - "Mailbox: ISM verification failed" - ); - - // Load the identifier for the calling contract. - implementationIdentifier = abi.encodePacked(vm.emitterAddress); - - // Local "supposedly" this chain identifier. - bytes32 thisChainIdentifier = bytes32(payload[0:32]); - - // Check that the message is intended for this chain. - if (thisChainIdentifier != bytes32(uint256(chainId()))) revert BadChainIdentifier(); - - // Local the identifier for the source chain. - sourceIdentifier = bytes32(bytes2(vm.emitterChainId)); + if (!ism.verify(_metadata, _message)) revert ISMVerificationFailed(); // Get the application message. - message_ = payload[32:]; + message_ = _message.body(); } function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfsendPacketInNativeToken) { // Get the cost of sending wormhole messages. - costOfsendPacketInNativeToken = uint128(_quoteDispatch()); - uint32 destinationDomain = uint32(destinationChainIdentifier); + costOfsendPacketInNativeToken = uint128(_requiredHookQuote()); + uint32 destinationDomain = uint32(uint256(destinationChainIdentifier)); // Handoff the message to hyperlane MAILBOX.dispatch{value: costOfsendPacketInNativeToken}( From 4071fbaea358730690e76e7650c15618d787c640 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 12 Dec 2023 16:34:14 +0100 Subject: [PATCH 4/7] fix: correct the verify function and add more functionality to the implementation --- .../hyperlane/IncentivizedHyperlaneEscrow.sol | 57 ++++++++++++++----- src/apps/hyperlane/README.md | 5 ++ 2 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 src/apps/hyperlane/README.md diff --git a/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol b/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol index ecd7ed0..7967664 100644 --- a/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol +++ b/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol @@ -5,6 +5,7 @@ import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; import { ReplacementHook } from "./ReplacementHook.sol"; import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "./interfaces/IInterchainSecurityModule.sol"; +import { IMessageRecipient } from "./interfaces/IMessageRecipient.sol"; import { IPostDispatchHook } from "./interfaces/hooks/IPostDispatchHook.sol"; import { Message } from "./libs/Message.sol"; import { IMailbox } from "./interfaces/IMailbox.sol"; @@ -14,16 +15,18 @@ interface IVersioned { } /// @notice Hyperlane implementation of Generalised incentives. -contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow, ReplacementHook { +contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow, ReplacementHook, ISpecifiesInterchainSecurityModule, IMessageRecipient { // ============ Libraries ============ using Message for bytes; - error BadChainIdentifier(); - error BadMailboxVersion(uint8 VERSION, uint8 messageVersion); - error BadDomain(uint32 localDomain, uint32 messageDestination); - error ISMVerificationFailed(); + error BadChainIdentifier(); // 0x3c1e02c; + error BadMailboxVersion(uint8 VERSION, uint8 messageVersion); // 0x0d8b788f + error BadDomain(uint32 localDomain, uint32 messageDestination); // 0x2da4acb6 + error WrongRecipient(address trueRecipient); // 0xb2d27e64 + error ISMVerificationFailed(); // 0x902013b + error DeliverMessageDirectlyToGeneralisedIncentvies(); // 0xecb92632 IPostDispatchHook CUSTOM_HOOK = IPostDispatchHook(address(this)); @@ -32,16 +35,34 @@ contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow, ReplacementHo uint32 public immutable localDomain; IMailbox public immutable MAILBOX; uint8 public immutable VERSION; + IInterchainSecurityModule immutable INTERCHAIN_SECURITY_MODULE; - constructor(address sendLostGasTo, address mailbox_) IncentivizedMessageEscrow(sendLostGasTo){ + function interchainSecurityModule() external view returns (IInterchainSecurityModule) { + return INTERCHAIN_SECURITY_MODULE; + } + + /// @dev The hyperlane mailbox requires the call to not fail. + /// By always failing, the message cannot be delivered through the hyperlane mailbox. + function handle( + uint32 /* _origin */, + bytes32 /* _sender */, + bytes calldata /* _message */ + ) external payable { + revert DeliverMessageDirectlyToGeneralisedIncentvies(); + } + + constructor(address sendLostGasTo, address interchainSecurityModule_, address mailbox_) IncentivizedMessageEscrow(sendLostGasTo){ MAILBOX = IMailbox(mailbox_); // Collect the chain identifier from the mailbox and store it here. // localDomain is immutable on mailbox. localDomain = MAILBOX.localDomain(); VERSION = IVersioned(mailbox_).VERSION(); + INTERCHAIN_SECURITY_MODULE = IInterchainSecurityModule(interchainSecurityModule_); } + /// @notice Get the required cost of the requiredHook. Calling the mailbox directly requires significantly + /// more gas as it would eventually also call this contract. function _requiredHookQuote() internal view returns(uint256 amount) { IPostDispatchHook requiredHook = MAILBOX.requiredHook(); @@ -69,28 +90,38 @@ contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow, ReplacementHo ); } + /// @notice Verify a Hyperlane package. The heavy lifting is done by + /// the ism. This function is based on the function `process` + /// in the Hyperlane Mailbox with optimisations around the errors. + /// @dev Normally, it is enforced that this function is without side-effects. + /// However, the ISM interface allows .verify to modify state. + /// As a result, we cannot manage to promise that. function _verifyPacket(bytes calldata _metadata, bytes calldata _message) internal override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { /// CHECKS /// - // Check that the message was intended for this mailbox. - if(VERSION != _message.version()) revert BadMailboxVersion(VERSION, _message.version()); + // Check that the message was intended for this "mailbox". + if (VERSION != _message.version()) revert BadMailboxVersion(VERSION, _message.version()); if (localDomain != _message.destination()) revert BadDomain(localDomain, _message.destination()); - // Get the recipient's ISM. + // Notice that there is no verification that a message hasn't been already delivered. That is because that is done elsewhere. + + // Check if the recipient is this contract. address recipient = _message.recipientAddress(); - IInterchainSecurityModule ism = MAILBOX.recipientIsm(recipient); + if (recipient != address(this)) revert WrongRecipient(recipient); + // We don't have to get the ism, since it is read anyway from this contract. The line would be MAILBOX.recipientIsm(recipient) but it would eventually call this contract anyway. /// EFFECTS /// - sourceIdentifier = bytes32(uint256(_message.origin())); - implementationIdentifier = abi.encodePacked(_message.sender()); + // We are not emitting the events since that is not useful. /// INTERACTIONS /// // Verify the message via the interchain security module. - if (!ism.verify(_metadata, _message)) revert ISMVerificationFailed(); + if (!INTERCHAIN_SECURITY_MODULE.verify(_metadata, _message)) revert ISMVerificationFailed(); // Get the application message. + sourceIdentifier = bytes32(uint256(_message.origin())); + implementationIdentifier = abi.encodePacked(_message.sender()); message_ = _message.body(); } diff --git a/src/apps/hyperlane/README.md b/src/apps/hyperlane/README.md new file mode 100644 index 0000000..8f74652 --- /dev/null +++ b/src/apps/hyperlane/README.md @@ -0,0 +1,5 @@ +# Hyperlane Generalised Incentives + +This is a Hyperlane implementation of Generalised Incentives. It works by using the Hyperlane Mailbox to emit messages but instead of using the Hyperlane mailbox to verify messages, messages are to be delivered directly to this contract for verification and execution. + +The messages are still delivered to the Hyperlane Mailbox for emitting. \ No newline at end of file From 6ae2007b32062df92918fabef785db4d9dec6ff1 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 13 Dec 2023 16:03:09 +0100 Subject: [PATCH 5/7] test: hyperlane --- src/apps/hyperlane/README.md | 16 +- test/hyperlane/HyperlaneIntegration.t.sol | 129 +++++++ test/hyperlane/mocks/MockIsm.sol | 12 + test/hyperlane/mocks/MockMailbox.sol | 411 ++++++++++++++++++++++ 4 files changed, 567 insertions(+), 1 deletion(-) create mode 100644 test/hyperlane/HyperlaneIntegration.t.sol create mode 100644 test/hyperlane/mocks/MockIsm.sol create mode 100644 test/hyperlane/mocks/MockMailbox.sol diff --git a/src/apps/hyperlane/README.md b/src/apps/hyperlane/README.md index 8f74652..1cd22b5 100644 --- a/src/apps/hyperlane/README.md +++ b/src/apps/hyperlane/README.md @@ -2,4 +2,18 @@ This is a Hyperlane implementation of Generalised Incentives. It works by using the Hyperlane Mailbox to emit messages but instead of using the Hyperlane mailbox to verify messages, messages are to be delivered directly to this contract for verification and execution. -The messages are still delivered to the Hyperlane Mailbox for emitting. \ No newline at end of file +The messages are still delivered to the Hyperlane Mailbox for emitting. + + +# Address encoding: + +Hyperlane uses 32 bytes to represent addresses. + +EVM -> EVM +```solidity + +function _to_bytes32(address target) internal returns(bytes memory) { + return abi.encode(target); +} + +``` \ No newline at end of file diff --git a/test/hyperlane/HyperlaneIntegration.t.sol b/test/hyperlane/HyperlaneIntegration.t.sol new file mode 100644 index 0000000..22726be --- /dev/null +++ b/test/hyperlane/HyperlaneIntegration.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +// mocks +import { MockIsm } from "./mocks/mockIsm.sol"; +import { MockMailbox } from "./mocks/MockMailbox.sol"; +import { MockApplication } from "../mocks/MockApplication.sol"; +import { ReplacementHook } from "../../src/apps/hyperlane/ReplacementHook.sol"; + +import { IncentivizedHyperlaneEscrow } from "../../src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol"; +import { IMessageEscrowStructs } from "../../src/interfaces/IMessageEscrowStructs.sol"; + +contract HyperlaneTest is Test, IMessageEscrowStructs { + + IncentiveDescription _INCENTIVE; + + address application; + IncentivizedHyperlaneEscrow escrow; + bytes32 destinationIdentifier; + address sendLostGasTo; + + // Deploy relevant contracts + function setUp() external { + + + destinationIdentifier = bytes32(block.chainid); + uint32 destinationIdentifier_uint32 = uint32(uint256(destinationIdentifier)); + address MockHook = address(new ReplacementHook()); + address mockIsm = address(new MockIsm()); + address mockMailbox = address(new MockMailbox(destinationIdentifier_uint32, mockIsm, MockHook, MockHook)); + sendLostGasTo = address(uint160(57005)); + escrow = new IncentivizedHyperlaneEscrow(sendLostGasTo, mockIsm, mockMailbox); + + application = address(new MockApplication(address(escrow))); + + + _INCENTIVE = IncentiveDescription({ + maxGasDelivery: 1199199, + maxGasAck: 1188188, + refundGasTo: sendLostGasTo, + priceOfDeliveryGas: 123321, + priceOfAckGas: 321123, + targetDelta: 30 minutes + }); + } + + function _getTotalIncentive(IncentiveDescription memory incentive) internal pure returns(uint256) { + return incentive.maxGasDelivery * incentive.priceOfDeliveryGas + incentive.maxGasAck * incentive.priceOfAckGas; + } + + function test_hyperlane_integration() external { + bytes memory message = abi.encode(uint256(251251251)); + payable(application).transfer(_getTotalIncentive(_INCENTIVE)); + + // Set remote implementation contract + vm.prank(application); + escrow.setRemoteImplementation(destinationIdentifier, abi.encode(address(escrow))); + + + // Escrow a message as the application + vm.prank(application); + vm.recordLogs(); + escrow.submitMessage{value: _getTotalIncentive(_INCENTIVE)}( + destinationIdentifier, + abi.encodePacked( + uint8(20), + bytes32(0), + abi.encode(application) + ), + message, + _INCENTIVE + ); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + // The majority of the data emitted by the mailbox is indexed. That is ignored by this function. + // If we wanted to get that, we should look in the topics. + (bytes memory package) = abi.decode(entries[1].data, (bytes)); + + // Source to destination + vm.recordLogs(); + escrow.processPacket(hex"01", package, bytes32(uint256(uint160(sendLostGasTo)))); + + entries = vm.getRecordedLogs(); + + // Get the new package. + (package) = abi.decode(entries[1].data, (bytes)); + + escrow.processPacket(hex"01", package, bytes32(uint256(uint160(sendLostGasTo)))); + } + + function test_hyperlane_integration(bytes memory message) external { + payable(application).transfer(_getTotalIncentive(_INCENTIVE)); + + // Set remote implementation contract + vm.prank(application); + escrow.setRemoteImplementation(destinationIdentifier, abi.encode(address(escrow))); + + + // Escrow a message as the application + vm.prank(application); + vm.recordLogs(); + escrow.submitMessage{value: _getTotalIncentive(_INCENTIVE)}( + destinationIdentifier, + abi.encodePacked( + uint8(20), + bytes32(0), + abi.encode(application) + ), + message, + _INCENTIVE + ); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + (bytes memory package) = abi.decode(entries[1].data, (bytes)); + + // IGNORE ABOVE, FOCUS BELOW: + + // Tell the ISM to fail: + vm.expectRevert( + abi.encodeWithSignature("ISMVerificationFailed()") + ); + escrow.processPacket(hex"00", package, bytes32(uint256(uint160(sendLostGasTo)))); + + // Tell the ISM to not fail. + escrow.processPacket(hex"01", package, bytes32(uint256(uint160(sendLostGasTo)))); + } +} \ No newline at end of file diff --git a/test/hyperlane/mocks/MockIsm.sol b/test/hyperlane/mocks/MockIsm.sol new file mode 100644 index 0000000..8bebcc6 --- /dev/null +++ b/test/hyperlane/mocks/MockIsm.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {IInterchainSecurityModule} from "../../../src/apps/hyperlane/interfaces/IInterchainSecurityModule.sol"; + +contract MockIsm is IInterchainSecurityModule { + uint8 public moduleType = uint8(Types.UNUSED); + + function verify(bytes calldata metadata, bytes calldata /* _message */) external pure returns (bool) { + return bytes1(metadata) == bytes1(0x01); + } +} \ No newline at end of file diff --git a/test/hyperlane/mocks/MockMailbox.sol b/test/hyperlane/mocks/MockMailbox.sol new file mode 100644 index 0000000..d799ef9 --- /dev/null +++ b/test/hyperlane/mocks/MockMailbox.sol @@ -0,0 +1,411 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +// ============ Internal Imports ============ +import {Message} from "../../../src/apps/hyperlane/libs/Message.sol"; +import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "../../../src/apps/hyperlane/interfaces/IInterchainSecurityModule.sol"; +import {IPostDispatchHook} from "../../../src/apps/hyperlane/interfaces/hooks/IPostDispatchHook.sol"; +import {IMessageRecipient} from "../../../src/apps/hyperlane/interfaces/IMessageRecipient.sol"; +import {IMailbox} from "../../../src/apps/hyperlane/interfaces/IMailbox.sol"; + + +contract MockMailbox is IMailbox { + // ============ Libraries ============ + + using Message for bytes; + + // ============ Constants ============ + + uint8 public constant VERSION = 3; + + // Domain of chain on which the contract is deployed + uint32 public immutable localDomain; + + // ============ Public Storage ============ + + // A monotonically increasing nonce for outbound unique message IDs. + uint32 public nonce; + + // The latest dispatched message ID used for auth in post-dispatch hooks. + bytes32 public latestDispatchedId; + + // The default ISM, used if the recipient fails to specify one. + IInterchainSecurityModule public defaultIsm; + + // 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; + + uint256 public immutable deployedBlock; + + // Mapping of message ID to delivery context that processed the message. + struct Delivery { + address processor; + uint48 blockNumber; + } + mapping(bytes32 => Delivery) internal deliveries; + + // ============ 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); + + /** + * @notice Emitted when the required hook is updated + * @param hook The new required hook + */ + event RequiredHookSet(address indexed hook); + + // ============ Constructor ============ + constructor( + uint32 _localDomain, + address _defaultIsm, + address _defaultHook, + address _requiredHook + ) { + localDomain = _localDomain; + deployedBlock = block.number; + setDefaultIsm(_defaultIsm); + setDefaultHook(_defaultHook); + setRequiredHook(_requiredHook); + } + // ============ External Functions ============ + /** + * @notice Dispatches a message to the destination domain & recipient + * using the default hook and empty metadata. + * @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) { + return + dispatch( + _destinationDomain, + _recipientAddress, + _messageBody, + _messageBody[0:0], + defaultHook + ); + } + + /** + * @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, + defaultHook + ); + } + + /** + * @notice Computes quote for dipatching a message to the destination domain & recipient + * using the default hook and empty metadata. + * @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 + ) external view returns (uint256 fee) { + return + quoteDispatch( + destinationDomain, + recipientAddress, + messageBody, + messageBody[0:0], + defaultHook + ); + } + + /** + * @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 + ); + } + + /** + * @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 { + /// CHECKS /// + + // Check that the message was intended for this mailbox. + require(_message.version() == VERSION, "Mailbox: bad version"); + require( + _message.destination() == localDomain, + "Mailbox: unexpected destination" + ); + + // Check that the message hasn't already been delivered. + bytes32 _id = _message.id(); + require(delivered(_id) == false, "Mailbox: already delivered"); + + // Get the recipient's ISM. + address recipient = _message.recipientAddress(); + IInterchainSecurityModule ism = recipientIsm(recipient); + + /// EFFECTS /// + + deliveries[_id] = Delivery({ + processor: msg.sender, + blockNumber: uint48(block.number) + }); + emit Process(_message.origin(), _message.sender(), recipient); + emit ProcessId(_id); + + /// INTERACTIONS /// + + // Verify the message via the interchain security module. + require( + ism.verify(_metadata, _message), + "Mailbox: ISM verification failed" + ); + + // Deliver the message to the recipient. + IMessageRecipient(recipient).handle{value: msg.value}( + _message.origin(), + _message.sender(), + _message.body() + ); + } + + /** + * @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 number of the block that the message was processed at. + */ + function processedAt(bytes32 _id) external view returns (uint48) { + return deliveries[_id].blockNumber; + } + + // ============ 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 virtual returns (bytes32) { + if (address(hook) == address(0)) { + hook = defaultHook; + } + + /// 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); + // if underpaying, defer to required hook's reverting behavior + if (msg.value < requiredValue) { + requiredValue = msg.value; + } + 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) { + if (address(hook) == address(0)) { + hook = defaultHook; + } + + 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].blockNumber > 0; + } + + /** + * @notice Sets the default ISM for the Mailbox. + * @param _module The new default ISM. Must be a contract. + */ + function setDefaultIsm(address _module) public { + 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 { + 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 { + requiredHook = IPostDispatchHook(_hook); + emit RequiredHookSet(_hook); + } + + /** + * @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 low-level staticcall in case of revert or empty return data + (bool success, bytes memory returnData) = _recipient.staticcall( + abi.encodeCall( + ISpecifiesInterchainSecurityModule.interchainSecurityModule, + () + ) + ); + // check if call was successful and returned data + if (success && returnData.length != 0) { + // check if returnData is a valid address + address ism = abi.decode(returnData, (address)); + // check if the ISM is a contract + if (ism != address(0)) { + return IInterchainSecurityModule(ism); + } + } + // Use the default if a valid one is not specified by the recipient. + return defaultIsm; + } + + // ============ Internal Functions ============ + function _buildMessage( + uint32 destinationDomain, + bytes32 recipientAddress, + bytes calldata messageBody + ) internal view returns (bytes memory) { + return + Message.formatMessage( + VERSION, + nonce, + localDomain, + bytes32(uint256(uint160(msg.sender))), + destinationDomain, + recipientAddress, + messageBody + ); + } +} \ No newline at end of file From 365eadb496ecd3606ed0dcac78895ab8d2e94637 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 13 Dec 2023 16:04:51 +0100 Subject: [PATCH 6/7] fix: MockIsm with capital M --- test/hyperlane/HyperlaneIntegration.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hyperlane/HyperlaneIntegration.t.sol b/test/hyperlane/HyperlaneIntegration.t.sol index 22726be..c163edf 100644 --- a/test/hyperlane/HyperlaneIntegration.t.sol +++ b/test/hyperlane/HyperlaneIntegration.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; // mocks -import { MockIsm } from "./mocks/mockIsm.sol"; +import { MockIsm } from "./mocks/MockIsm.sol"; import { MockMailbox } from "./mocks/MockMailbox.sol"; import { MockApplication } from "../mocks/MockApplication.sol"; import { ReplacementHook } from "../../src/apps/hyperlane/ReplacementHook.sol"; From c3755894a15625d2b65c9ad808ac22d72fa7e40f Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 1 Feb 2024 16:40:54 +0100 Subject: [PATCH 7/7] feat: add msg.sender to message identifier --- src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol b/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol index 7967664..896b2ee 100644 --- a/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol +++ b/src/apps/hyperlane/IncentivizedHyperlaneEscrow.sol @@ -82,6 +82,7 @@ contract IncentivizedHyperlaneEscrow is IncentivizedMessageEscrow, ReplacementHo ) internal override view returns(bytes32) { return keccak256( abi.encodePacked( + msg.sender, bytes32(block.number), localDomain, destinationIdentifier,