diff --git a/src/base/Roles/CrossChain/Hyperlane/StandardHookMetadata.sol b/src/base/Roles/CrossChain/Hyperlane/StandardHookMetadata.sol new file mode 100644 index 0000000..26564ff --- /dev/null +++ b/src/base/Roles/CrossChain/Hyperlane/StandardHookMetadata.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +/** + * Format of metadata: + * + * [0:2] variant + * [2:34] msg.value + * [34:66] Gas limit for message (IGP) + * [66:86] Refund address for message (IGP) + * [86:] Custom metadata + */ +library StandardHookMetadata { + struct Metadata { + uint16 variant; + uint256 msgValue; + uint256 gasLimit; + address refundAddress; + } + + uint8 private constant VARIANT_OFFSET = 0; + uint8 private constant MSG_VALUE_OFFSET = 2; + uint8 private constant GAS_LIMIT_OFFSET = 34; + uint8 private constant REFUND_ADDRESS_OFFSET = 66; + uint256 private constant MIN_METADATA_LENGTH = 86; + + uint16 public constant VARIANT = 1; + + /** + * @notice Returns the variant of the metadata. + * @param _metadata ABI encoded standard hook metadata. + * @return variant of the metadata as uint8. + */ + function variant(bytes calldata _metadata) internal pure returns (uint16) { + if (_metadata.length < VARIANT_OFFSET + 2) return 0; + return uint16(bytes2(_metadata[VARIANT_OFFSET:VARIANT_OFFSET + 2])); + } + + /** + * @notice Returns the specified value for the message. + * @param _metadata ABI encoded standard hook metadata. + * @param _default Default fallback value. + * @return Value for the message as uint256. + */ + function msgValue(bytes calldata _metadata, uint256 _default) internal pure returns (uint256) { + if (_metadata.length < MSG_VALUE_OFFSET + 32) return _default; + return uint256(bytes32(_metadata[MSG_VALUE_OFFSET:MSG_VALUE_OFFSET + 32])); + } + + /** + * @notice Returns the specified gas limit for the message. + * @param _metadata ABI encoded standard hook metadata. + * @param _default Default fallback gas limit. + * @return Gas limit for the message as uint256. + */ + 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])); + } + + /** + * @notice Returns the specified refund address for the message. + * @param _metadata ABI encoded standard hook metadata. + * @param _default Default fallback refund address. + * @return Refund address for the message as address. + */ + function refundAddress(bytes calldata _metadata, address _default) internal pure returns (address) { + if (_metadata.length < REFUND_ADDRESS_OFFSET + 20) return _default; + return address(bytes20(_metadata[REFUND_ADDRESS_OFFSET:REFUND_ADDRESS_OFFSET + 20])); + } + + /** + * @notice Returns any custom metadata. + * @param _metadata ABI encoded standard hook metadata. + * @return Custom metadata. + */ + function getCustomMetadata(bytes calldata _metadata) internal pure returns (bytes calldata) { + if (_metadata.length < MIN_METADATA_LENGTH) return _metadata[0:0]; + return _metadata[MIN_METADATA_LENGTH:]; + } + + /** + * @notice Formats the specified gas limit and refund address into standard hook metadata. + * @param _msgValue msg.value for the message. + * @param _gasLimit Gas limit for the message. + * @param _refundAddress Refund address for the message. + * @param _customMetadata Additional metadata to include in the standard hook metadata. + * @return ABI encoded standard hook metadata. + */ + function formatMetadata( + uint256 _msgValue, + uint256 _gasLimit, + address _refundAddress, + bytes memory _customMetadata + ) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(VARIANT, _msgValue, _gasLimit, _refundAddress, _customMetadata); + } + + /** + * @notice Formats the specified gas limit and refund address into standard hook metadata. + * @param _msgValue msg.value for the message. + * @return ABI encoded standard hook metadata. + */ + function overrideMsgValue(uint256 _msgValue) internal view returns (bytes memory) { + return formatMetadata(_msgValue, uint256(0), msg.sender, ""); + } + + /** + * @notice Formats the specified gas limit and refund address into standard hook metadata. + * @param _gasLimit Gas limit for the message. + * @return ABI encoded standard hook metadata. + */ + function overrideGasLimit(uint256 _gasLimit) internal view returns (bytes memory) { + return formatMetadata(uint256(0), _gasLimit, msg.sender, ""); + } + + /** + * @notice Formats the specified refund address into standard hook metadata. + * @param _refundAddress Refund address for the message. + * @return ABI encoded standard hook metadata. + */ + function overrideRefundAddress(address _refundAddress) internal pure returns (bytes memory) { + return formatMetadata(uint256(0), uint256(0), _refundAddress, ""); + } +} diff --git a/src/base/Roles/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.sol b/src/base/Roles/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.sol new file mode 100644 index 0000000..d681e76 --- /dev/null +++ b/src/base/Roles/CrossChain/MultiChainHyperlaneTellerWithMultiAssetSupport.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { + MultiChainTellerBase, + MultiChainTellerBase_MessagesNotAllowedFrom, + MultiChainTellerBase_MessagesNotAllowedFromSender, + Chain +} from "./MultiChainTellerBase.sol"; +import { BridgeData, ERC20 } from "./CrossChainTellerBase.sol"; +import { StandardHookMetadata } from "./Hyperlane/StandardHookMetadata.sol"; +import { IMailbox } from "../../../interfaces/hyperlane/IMailbox.sol"; + +/** + * @title MultiChainHyperlaneTellerWithMultiAssetSupport + * @notice Hyperlane implementation of MultiChainTeller + * @custom:security-contact security@molecularlabs.io + */ +contract MultiChainHyperlaneTellerWithMultiAssetSupport is MultiChainTellerBase { + // ========================================= STATE ========================================= + + /** + * @notice The hyperlane mailbox contract. + */ + IMailbox public immutable mailbox; + + /** + * @notice A nonce used to generate unique message IDs. + */ + uint128 public nonce; + + //============================== ERRORS =============================== + + error MultiChainHyperlaneTeller_InvalidToken(); + error MultiChainHyperlaneTeller_CallerMustBeMailbox(address caller); + + constructor( + address _owner, + address _vault, + address _accountant, + IMailbox _mailbox + ) + MultiChainTellerBase(_owner, _vault, _accountant) + { + mailbox = _mailbox; + } + + /** + * @notice function override to return the fee quote + * @param shareAmount to be sent as a message + * @param data Bridge data + * @returns fee to be paid for bridging + */ + function _quote(uint256 shareAmount, BridgeData calldata data) internal view override returns (uint256) { + bytes memory _payload = abi.encode(shareAmount, data.destinationChainReceiver); + bytes32 msgRecipient = _addressToBytes32(selectorToChains[data.chainSelector].targetTeller); + + return mailbox.quoteDispatch(data.chainSelector, msgRecipient, _payload); // TODO Should there be hook metadata + // for quoteDispatch? + } + + /** + * @notice Called when data is received from the protocol. It overrides the equivalent function in the parent + * contract. + * Protocol messages are defined as packets, comprised of the following parameters. + * @param origin A struct containing information about where the packet came from. + * @param sender The contract that sent this message. + * @param payload Encoded message. + */ + function handle(uint32 origin, bytes32 sender, bytes calldata payload) external payable { + _beforeReceive(); + + Chain memory chain = selectorToChains[origin]; + + // Three things must be checked. + // 1. This function must only be called by the mailbox + // 2. The sender must be the teller from the source chain + // 3. The origin aka chainSelector must be allowed to send message to this + // contract through the `Chain` config. + + // TODO How does setting the ISM work? Is it necessary? + if (msg.sender != address(mailbox)) { + revert MultiChainHyperlaneTeller_CallerMustBeMailbox(msg.sender); + } + + // TODO check that bytes32 to address works properly + if (sender != _addressToBytes32(chain.targetTeller)) { + revert MultiChainTellerBase_MessagesNotAllowedFromSender(uint256(origin), _bytes32ToAddress(sender)); + } + + if (!chain.allowMessagesFrom) { + revert MultiChainTellerBase_MessagesNotAllowedFrom(origin); + } + + (uint256 shareAmount, address receiver, bytes32 messageId) = abi.decode(payload, (uint256, address, bytes32)); + vault.enter(address(0), ERC20(address(0)), 0, receiver, shareAmount); + + _afterReceive(shareAmount, receiver, messageId); + } + + /** + * @notice bridge override to allow bridge logic to be done for bridge() and depositAndBridge() + * @param shareAmount to be moved across chain + * @param data BridgeData + * @return messageId a unique hash for the message + */ + function _bridge(uint256 shareAmount, BridgeData calldata data) internal override returns (bytes32 messageId) { + unchecked { + messageId = keccak256(abi.encodePacked(++nonce, address(this), block.chainid)); + } + + bytes memory _payload = abi.encode(shareAmount, data.destinationChainReceiver, messageId); + + // Unlike L0 that has a built in peer check, this contract must + // constrain the message recipient itself. We do this by our own + // configuration. + bytes32 msgRecipient = _addressToBytes32(selectorToChains[data.chainSelector].targetTeller); + + bytes32 messageId = mailbox.dispatch{ value: msg.value }( + data.chainSelector, // must be `destinationDomain` on hyperlane + msgRecipient, // must be the teller address left-padded to bytes32 + _payload, + StandardHookMetadata.overrideGasLimit(data.messageGas) // Sets the refund address to msg.sender, sets + // `_msgValue` + // to zero + ); + } + + function _addressToBytes32(address _address) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_address))); + } + + function _bytes32ToAddress(bytes32 _address) internal pure returns (address) { + return address(uint160(uint256(_address))); + } +} diff --git a/src/interfaces/hyperlane/IInterchainSecurityModule.sol b/src/interfaces/hyperlane/IInterchainSecurityModule.sol new file mode 100644 index 0000000..bb4fe90 --- /dev/null +++ b/src/interfaces/hyperlane/IInterchainSecurityModule.sol @@ -0,0 +1,40 @@ +// 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, + ARB_L2_TO_L1, + WEIGHTED_MERKLE_ROOT_MULTISIG, + WEIGHTED_MESSAGE_ID_MULTISIG, + OP_L2_TO_L1 + } + + /** + * @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/interfaces/hyperlane/IMailbox.sol b/src/interfaces/hyperlane/IMailbox.sol new file mode 100644 index 0000000..b0fe1d7 --- /dev/null +++ b/src/interfaces/hyperlane/IMailbox.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import { IInterchainSecurityModule } from "./IInterchainSecurityModule.sol"; +import { IPostDispatchHook } from "./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/interfaces/hyperlane/IPostDispatchHook.sol b/src/interfaces/hyperlane/IPostDispatchHook.sol new file mode 100644 index 0000000..26fcedc --- /dev/null +++ b/src/interfaces/hyperlane/IPostDispatchHook.sol @@ -0,0 +1,59 @@ +// 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, + LAYER_ZERO_V1, + RATE_LIMITED, + ARB_L2_TO_L1, + OP_L2_TO_L1 + } + + /** + * @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); +}