Skip to content

Commit

Permalink
LayerZero Hook and ISM (#3102)
Browse files Browse the repository at this point in the history
### Description
- Implements LayerZero V1 and V2 hook with V2 ISM.
- V2 ISM is backwards compatible i.e., V1 hook (which uses V1 endpoint
to send) can send to V2 ISM (which uses V2 endpoint to receive). See
[here](https://docs.layerzero.network/explore/migration#configuring-ultralightnode301)
for configuration.
- V2 uses `AbstractsMessageIdAuthorized*`. In. other words, hook passes
messageId through LayerZero, which stores into ISM to be used when
relayer calls `verify()` .
- Includes unit test for both V1 and V2 hook and ISM. 
   - V1 uses official LayerZero endpoint mock.

### Drive-by changes

<!--
Are there any minor or drive-by changes also included?
-->

### Related issues
- Implements #2853

### Backward compatibility
Yes

### Testing
Manual/Unit Tests
  • Loading branch information
ltyu authored and yorhodes committed Mar 22, 2024
1 parent 0cc0905 commit 290233d
Show file tree
Hide file tree
Showing 13 changed files with 1,539 additions and 37 deletions.
118 changes: 118 additions & 0 deletions solidity/contracts/hooks/layer-zero/LayerZeroV1Hook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
import {ILayerZeroEndpoint} from "@layerzerolabs/lz-evm-v1-0.7/contracts/interfaces/ILayerZeroEndpoint.sol";
import {Message} from "../../libs/Message.sol";
import {TypeCasts} from "../../libs/TypeCasts.sol";
import {MailboxClient} from "../../client/MailboxClient.sol";
import {Indexed} from "../../libs/Indexed.sol";
import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol";
import {AbstractPostDispatchHook} from "../libs/AbstractPostDispatchHook.sol";
import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol";

struct LayerZeroMetadata {
/// @dev the destination chain identifier
uint16 dstChainId;
/// @dev the user app address on this EVM chain. Contract address that calls Endpoint.send(). Used for LZ user app config lookup
address userApplication;
/// @dev if the source transaction is cheaper than the amount of value passed, refund the additional amount to this address
address refundAddress;
/// @dev the custom message to send over LayerZero
bytes payload;
/// @dev the address on destination chain (in bytes). A 40 length byte with remote and local addresses concatenated.
bytes destination;
/// @dev parameters for the adapter service, e.g. send some dust native token to dstChain
bytes adapterParam;
}

contract LayerZeroV1Hook is AbstractPostDispatchHook, MailboxClient {
using StandardHookMetadata for bytes;
using Message for bytes;
using TypeCasts for bytes32;

ILayerZeroEndpoint public immutable lZEndpoint;

constructor(address _mailbox, address _lZEndpoint) MailboxClient(_mailbox) {
lZEndpoint = ILayerZeroEndpoint(_lZEndpoint);
}

// ============ External Functions ============

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

/// @inheritdoc AbstractPostDispatchHook
function _postDispatch(
bytes calldata metadata,
bytes calldata message
) internal virtual override {
// ensure hook only dispatches messages that are dispatched by the mailbox
bytes32 id = message.id();
require(_isLatestDispatched(id), "message not dispatched by mailbox");

bytes calldata lZMetadata = metadata.getCustomMetadata();
LayerZeroMetadata memory layerZeroMetadata = parseLzMetadata(
lZMetadata
);
lZEndpoint.send{value: msg.value}(
layerZeroMetadata.dstChainId,
layerZeroMetadata.destination,
layerZeroMetadata.payload,
payable(layerZeroMetadata.refundAddress),
address(0), // _zroPaymentAddress is hardcoded to addr(0) because zro tokens should not be directly accepted
layerZeroMetadata.adapterParam
);
}

/// @inheritdoc AbstractPostDispatchHook
function _quoteDispatch(
bytes calldata metadata,
bytes calldata
) internal view virtual override returns (uint256 nativeFee) {
bytes calldata lZMetadata = metadata.getCustomMetadata();
LayerZeroMetadata memory layerZeroMetadata = parseLzMetadata(
lZMetadata
);
(nativeFee, ) = lZEndpoint.estimateFees(
layerZeroMetadata.dstChainId,
layerZeroMetadata.userApplication,
layerZeroMetadata.payload,
false, // _payInZRO is hardcoded to false because zro tokens should not be directly accepted
layerZeroMetadata.adapterParam
);
}

/**
* @notice Formats LayerZero metadata using default abi encoding
* @param layerZeroMetadata LayerZero specific metadata
* @return ABI encoded metadata
*/
function formatLzMetadata(
LayerZeroMetadata calldata layerZeroMetadata
) public pure returns (bytes memory) {
return abi.encode(layerZeroMetadata);
}

/**
* @notice Decodes LayerZero metadata. Should be used after formatLzMetadata()
* @param lZMetadata ABI encoded metadata
*/
function parseLzMetadata(
bytes calldata lZMetadata
) public pure returns (LayerZeroMetadata memory parsedLayerZeroMetadata) {
(parsedLayerZeroMetadata) = abi.decode(lZMetadata, (LayerZeroMetadata));
}
}
135 changes: 135 additions & 0 deletions solidity/contracts/hooks/layer-zero/LayerZeroV2Hook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/
import {MessagingParams, MessagingFee, ILayerZeroEndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import {Message} from "../../libs/Message.sol";
import {TypeCasts} from "../../libs/TypeCasts.sol";
import {Indexed} from "../../libs/Indexed.sol";
import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol";
import {AbstractMessageIdAuthHook} from "../libs/AbstractMessageIdAuthHook.sol";
import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol";

struct LayerZeroV2Metadata {
/// @dev the endpoint Id. prev dstChainId
uint32 eid;
/// @dev if the source transaction is cheaper than the amount of value passed, refund the additional amount to this address
address refundAddress;
/// @dev parameters for the adapter service, e.g. send some dust native token to dstChain. prev adapterParam
bytes options;
}

contract LayerZeroV2Hook is AbstractMessageIdAuthHook {
using StandardHookMetadata for bytes;
using Message for bytes;
using TypeCasts for bytes32;

ILayerZeroEndpointV2 public immutable lZEndpoint;

/// @dev offset for Layer Zero metadata parsing
uint8 constant EID_OFFSET = 0;
uint8 constant REFUND_ADDRESS_OFFSET = 4;
uint8 constant OPTIONS_OFFSET = 24;

constructor(
address _mailbox,
uint32 _destinationDomain,
bytes32 _ism,
address _lZEndpoint
) AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) {
lZEndpoint = ILayerZeroEndpointV2(_lZEndpoint);
}

// ============ External Functions ============

/// @inheritdoc AbstractMessageIdAuthHook
function _sendMessageId(
bytes calldata metadata,
bytes memory payload
) internal override {
bytes calldata lZMetadata = metadata.getCustomMetadata();
(
uint32 eid,
address refundAddress,
bytes memory options
) = parseLzMetadata(lZMetadata);

// Build and send message
MessagingParams memory msgParams = MessagingParams(
eid,
ism,
payload,
options,
false // payInLzToken
);
lZEndpoint.send{value: msg.value}(msgParams, refundAddress);
}

/// @dev payInZRO is hardcoded to false because zro tokens should not be directly accepted
function _quoteDispatch(
bytes calldata metadata,
bytes calldata message
) internal view virtual override returns (uint256) {
bytes calldata lZMetadata = metadata.getCustomMetadata();
(uint32 eid, , bytes memory options) = parseLzMetadata(lZMetadata);

// Build and quote message
MessagingParams memory msgParams = MessagingParams(
eid,
message.recipient(),
message.body(),
options,
false // payInLzToken
);
MessagingFee memory msgFee = lZEndpoint.quote(
msgParams,
message.senderAddress()
);

return msgFee.nativeFee;
}

/**
* @notice Formats LayerZero metadata using default abi encoding
* @param layerZeroMetadata LayerZero specific metadata
* @return ABI encoded metadata
*/
function formatLzMetadata(
LayerZeroV2Metadata calldata layerZeroMetadata
) public pure returns (bytes memory) {
return
abi.encodePacked(
layerZeroMetadata.eid,
layerZeroMetadata.refundAddress,
layerZeroMetadata.options
);
}

/**
* @notice Decodes LayerZero metadata. Should be used after formatLzMetadata()
* @param lZMetadata ABI encoded metadata
*/
function parseLzMetadata(
bytes calldata lZMetadata
)
public
pure
returns (uint32 eid, address refundAddress, bytes memory options)
{
eid = uint32(bytes4(lZMetadata[EID_OFFSET:REFUND_ADDRESS_OFFSET]));
refundAddress = address(
bytes20(lZMetadata[REFUND_ADDRESS_OFFSET:OPTIONS_OFFSET])
);
options = lZMetadata[OPTIONS_OFFSET:];
}
}
3 changes: 2 additions & 1 deletion solidity/contracts/interfaces/hooks/IPostDispatchHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ interface IPostDispatchHook {
FALLBACK_ROUTING,
ID_AUTH_ISM,
PAUSABLE,
PROTOCOL_FEE
PROTOCOL_FEE,
LAYER_ZERO_V1
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ abstract contract AbstractMessageIdAuthorizedIsm is
* @dev Only callable by the authorized hook.
* @param messageId Hyperlane Id of the message.
*/
function verifyMessageId(bytes32 messageId) external payable virtual {
function verifyMessageId(bytes32 messageId) public payable virtual {
require(
_isAuthorized(),
"AbstractMessageIdAuthorizedIsm: sender is not the hook"
Expand Down
114 changes: 114 additions & 0 deletions solidity/contracts/isms/hook/layer-zero/LayerZeroV2Ism.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// 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";
import {AbstractMessageIdAuthorizedIsm} from "../AbstractMessageIdAuthorizedIsm.sol";

// ============ External Imports ============
import {Origin} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";

/**
* @title LayerZeroV2Ism
* @notice Uses LayerZero V2 deliver and verify a messages Id
*/
contract LayerZeroV2Ism is AbstractMessageIdAuthorizedIsm {
using Message for bytes;
using TypeCasts for bytes32;

// Layerzero endpoint address
address public immutable endpoint;

// ============ Constants ============

uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.NULL);

// @dev the offset of msg.data where the function parameters (as bytes) begins. 4 bytes is always used when encoding the function selector
uint8 constant FUNC_SELECTOR_OFFSET = 4;

// @dev the offset of msg.data where Origin.sender begins. 32 is always used since calldata comes in 32 bytes.
uint8 constant ORIGIN_SENDER_OFFSET = FUNC_SELECTOR_OFFSET + 32;

// ============ Constructor ============
constructor(address _endpoint) {
require(
_endpoint != address(0),
"LayerZeroV2Ism: invalid authorized endpoint"
);
endpoint = _endpoint;
}

/**
* @notice Entry point for receiving msg/packet from the LayerZero endpoint.
* @param _lzMessage The payload of the received message.
* @dev Authorization verifcation is done within verifyMessageId() -> _isAuthorized()
*/
function lzReceive(
Origin calldata,
bytes32,
bytes calldata _lzMessage,
address,
bytes calldata
) external payable {
verifyMessageId(_messageId(_lzMessage));
}

// ============ Internal function ============

/**
* @notice Slices the messageId from the message delivered from LayerZeroV2Hook
* @dev message is created as abi.encodeCall(AbstractMessageIdAuthorizedIsm.verifyMessageId, id)
* @dev _message will be 36 bytes (4 bytes for function selector, and 32 bytes for messageId)
*/
function _messageId(
bytes calldata _message
) internal pure returns (bytes32) {
return bytes32(_message[FUNC_SELECTOR_OFFSET:]);
}

/**
* @notice Validates criterias to verify a message
* @dev this is called by AbstractMessageIdAuthorizedIsm.verifyMessageId
* @dev parses msg.value to get parameters from lzReceive()
*/
function _isAuthorized() internal view override returns (bool) {
require(_isAuthorizedHook(), "LayerZeroV2Ism: hook is not authorized");

require(
_isAuthorizedEndPoint(),
"LayerZeroV2Ism: endpoint is not authorized"
);

return true;
}

/**
* @notice check if origin.sender is the authorized hook
*/
function _isAuthorizedHook() internal view returns (bool) {
return bytes32(msg.data[ORIGIN_SENDER_OFFSET:]) == authorizedHook;
}

/**
* @notice check if LayerZero endpoint is authorized
*/
function _isAuthorizedEndPoint() internal view returns (bool) {
return msg.sender == endpoint;
}
}
Loading

0 comments on commit 290233d

Please sign in to comment.