-
Notifications
You must be signed in to change notification settings - Fork 412
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
### 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
Showing
13 changed files
with
1,539 additions
and
37 deletions.
There are no files selected for viewing
118 changes: 118 additions & 0 deletions
118
solidity/contracts/hooks/layer-zero/LayerZeroV1Hook.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
135
solidity/contracts/hooks/layer-zero/LayerZeroV2Hook.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
solidity/contracts/isms/hook/layer-zero/LayerZeroV2Ism.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.