From 5c49a2aebe5b82b04929366e0eff0ea33edc08b5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 01/61] forge install: LayerZero --- .gitmodules | 3 +++ lib/LayerZero | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/LayerZero diff --git a/.gitmodules b/.gitmodules index cf378b7..3bf7079 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,9 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/LayerZero"] + path = lib/LayerZero + url = https://github.com/LayerZero-Labs/LayerZero [submodule "lib/vibc-core-smart-contracts"] path = lib/vibc-core-smart-contracts url = https://github.com/open-ibc/vibc-core-smart-contracts diff --git a/lib/LayerZero b/lib/LayerZero new file mode 160000 index 0000000..48c21c3 --- /dev/null +++ b/lib/LayerZero @@ -0,0 +1 @@ +Subproject commit 48c21c3921931798184367fc02d3a8132b041942 From 3b4f5ec5881579874f37ff3d943095ce4b3e0c04 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 02/61] feat: initial lz sketch --- src/apps/layerzero/IncentiveFP.sol | 43 +++++++++++ src/apps/layerzero/IncentiveMPT.sol | 71 ++++++++++++++++++ .../layerzero/IncentivizedLayerZeroEscrow.sol | 72 +++++++++++++++++++ src/apps/layerzero/README.md | 3 + 4 files changed, 189 insertions(+) create mode 100644 src/apps/layerzero/IncentiveFP.sol create mode 100644 src/apps/layerzero/IncentiveMPT.sol create mode 100644 src/apps/layerzero/IncentivizedLayerZeroEscrow.sol create mode 100644 src/apps/layerzero/README.md diff --git a/src/apps/layerzero/IncentiveFP.sol b/src/apps/layerzero/IncentiveFP.sol new file mode 100644 index 0000000..fede99b --- /dev/null +++ b/src/apps/layerzero/IncentiveFP.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BUSL-1.1 + +// TODO: Check if we can upgrade solidity version +pragma solidity 0.7.6; +pragma abicoder v2; + +import "LayerZero/proof/utility/LayerZeroPacket.sol"; +import "LayerZero/interfaces/ILayerZeroValidationLibrary.sol"; +import "LayerZero/interfaces/IValidationLibraryHelperV2.sol"; + +contract FPValidator is ILayerZeroValidationLibrary, IValidationLibraryHelperV2 { + uint8 public proofType = 2; + uint8 public utilsVersion = 1; + + function validateProof(bytes32 _packetHash, bytes calldata _transactionProof, uint _remoteAddressSize) external view override returns (LayerZeroPacket.Packet memory packet) { + require(_remoteAddressSize > 0, "ProofLib: invalid address size"); + // _transactionProof = srcUlnAddress (32 bytes) + lzPacket + require(_transactionProof.length > 32 && keccak256(_transactionProof) == _packetHash, "ProofLib: invalid transaction proof"); + + bytes memory ulnAddressBytes = bytes(_transactionProof[0:32]); + bytes32 ulnAddress; + assembly { + ulnAddress := mload(add(ulnAddressBytes, 32)) + } + packet = LayerZeroPacket.getPacketV3(_transactionProof[32:], _remoteAddressSize, ulnAddress); + + return packet; + } + + function getUtilsVersion() external view override returns (uint8) { + return utilsVersion; + } + + function getProofType() external view override returns (uint8) { + return proofType; + } + + function getVerifyLog(bytes32, uint[] calldata, uint, bytes[] calldata proof) external pure override returns (ULNLog memory log) {} + + function getPacket(bytes memory data, uint sizeOfSrcAddress, bytes32 ulnAddress) external pure override returns (LayerZeroPacket.Packet memory) { + return LayerZeroPacket.getPacketV3(data, sizeOfSrcAddress, ulnAddress); + } +} diff --git a/src/apps/layerzero/IncentiveMPT.sol b/src/apps/layerzero/IncentiveMPT.sol new file mode 100644 index 0000000..baf09ed --- /dev/null +++ b/src/apps/layerzero/IncentiveMPT.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BUSL-1.1 + +// TODO: Check if we can upgrade solidity version +pragma solidity 0.7.6; +pragma abicoder v2; + +import "LayerZero/proof/utility/LayerZeroPacket.sol"; +import "LayerZero/proof/utility/UltraLightNodeEVMDecoder.sol"; +import "LayerZero/interfaces/IValidationLibraryHelperV2.sol"; +import "LayerZero/interfaces/ILayerZeroValidationLibrary.sol"; + +contract MPTValidator01 is ILayerZeroValidationLibrary, IValidationLibraryHelperV2 { + using RLPDecode for RLPDecode.RLPItem; + using RLPDecode for RLPDecode.Iterator; + + uint8 public proofType = 1; + uint8 public utilsVersion = 4; + bytes32 public constant PACKET_SIGNATURE = 0xe9bded5f24a4168e4f3bf44e00298c993b22376aad8c58c7dda9718a54cbea82; + + function validateProof(bytes32 _receiptsRoot, bytes calldata _transactionProof, uint _remoteAddressSize) external view override returns (LayerZeroPacket.Packet memory packet) { + require(_remoteAddressSize > 0, "ProofLib: invalid address size"); + (bytes[] memory proof, uint[] memory receiptSlotIndex, uint logIndex) = abi.decode(_transactionProof, (bytes[], uint[], uint)); + + ULNLog memory log = _getVerifiedLog(_receiptsRoot, receiptSlotIndex, logIndex, proof); + require(log.topicZeroSig == PACKET_SIGNATURE, "ProofLib: packet not recognized"); //data + + packet = LayerZeroPacket.getPacketV2(log.data, _remoteAddressSize, log.contractAddress); + + return packet; + } + + function _getVerifiedLog(bytes32 hashRoot, uint[] memory paths, uint logIndex, bytes[] memory proof) internal pure returns (ULNLog memory) { + require(paths.length == proof.length, "ProofLib: invalid proof size"); + require(proof.length > 0, "ProofLib: proof size must > 0"); + RLPDecode.RLPItem memory item; + bytes memory proofBytes; + + for (uint i = 0; i < proof.length; i++) { + proofBytes = proof[i]; + require(hashRoot == keccak256(proofBytes), "ProofLib: invalid hashlink"); + item = RLPDecode.toRlpItem(proofBytes).safeGetItemByIndex(paths[i]); + if (i < proof.length - 1) hashRoot = bytes32(item.toUint()); + } + + // burning status + gasUsed + logBloom + RLPDecode.RLPItem memory logItem = item.typeOffset().safeGetItemByIndex(3); + RLPDecode.Iterator memory it = logItem.safeGetItemByIndex(logIndex).iterator(); + ULNLog memory log; + log.contractAddress = bytes32(it.next().toUint()); + log.topicZeroSig = bytes32(it.next().safeGetItemByIndex(0).toUint()); + log.data = it.next().toBytes(); + + return log; + } + + function getUtilsVersion() external view override returns (uint8) { + return utilsVersion; + } + + function getProofType() external view override returns (uint8) { + return proofType; + } + + function getVerifyLog(bytes32 hashRoot, uint[] memory receiptSlotIndex, uint logIndex, bytes[] memory proof) external pure override returns (ULNLog memory) { + return _getVerifiedLog(hashRoot, receiptSlotIndex, logIndex, proof); + } + + function getPacket(bytes memory data, uint sizeOfSrcAddress, bytes32 ulnAddress) external pure override returns (LayerZeroPacket.Packet memory) { + return LayerZeroPacket.getPacketV2(data, sizeOfSrcAddress, ulnAddress); + } +} diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol new file mode 100644 index 0000000..319a552 --- /dev/null +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: DO-NOT-USE +pragma solidity ^0.8.13; + +import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; +import { ILayerZeroValidationLibrary } from "LayerZero/interfaces/ILayerZeroValidationLibrary.sol"; + +/** + * @notice LayerZero escrow. + * Do not use because of license issues. + */ +abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ILayerZeroValidationLibrary { + + constructor(address sendLostGasTo) IncentivizedMessageEscrow(sendLostGasTo) { + } + + function estimateAdditionalCost() external view returns(address asset, uint256 amount) { + asset = address(0); + amount = costOfMessages; + } + + function collectPayments() external { + payable(owner()).transfer(accumulator - 1); + accumulator = 1; + } + + function _getMessageIdentifier( + bytes32 destinationIdentifier, + bytes calldata message + ) internal override view returns(bytes32) { + return keccak256( + abi.encodePacked( + bytes32(block.number), + UNIQUE_SOURCE_IDENTIFIER, + destinationIdentifier, + message + ) + ); + } + + function _verifyPacket(bytes calldata _metadata, bytes calldata _message) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { + + // Get signature from message payload + (uint8 v, bytes32 r, bytes32 s) = abi.decode(_metadata, (uint8, bytes32, bytes32)); + + // Get signer of message + address messageSigner = ecrecover(keccak256(_message), v, r, s); + + // Check signer is the same as the stored signer. + require(messageSigner == owner(), "!signer"); + + // Load the identifier for the calling contract. + implementationIdentifier = _message[0:32]; + + // Local "supposedly" this chain identifier. + bytes32 thisChainIdentifier = bytes32(_message[64:96]); + + // Check that the message is intended for this chain. + require(thisChainIdentifier == UNIQUE_SOURCE_IDENTIFIER, "!Identifier"); + + // Local the identifier for the source chain. + sourceIdentifier = bytes32(_message[32:64]); + + // Get the application message. + message_ = _message[96:]; + } + + function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfsendPacketInNativeToken) { + // Handoff package to LZ. + + return costOfsendPacketInNativeToken = uint128(0); + } +} \ No newline at end of file diff --git a/src/apps/layerzero/README.md b/src/apps/layerzero/README.md new file mode 100644 index 0000000..4d186f8 --- /dev/null +++ b/src/apps/layerzero/README.md @@ -0,0 +1,3 @@ +# Wormhole Generalised Incentives + +This is a Layer Zero implementation of Generalised Incentives. Do not use these contracts. \ No newline at end of file From 94fce2932fe54db8ecd9f67c597bf70ba98e09e6 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 03/61] feat: inits --- src/apps/layerzero/IncentiveFP.sol | 6 +- src/apps/layerzero/IncentiveMPT.sol | 8 +- src/apps/layerzero/utility/Buffer.sol | 180 ++++++++ src/apps/layerzero/utility/BufferOrg.sol | 260 ++++++++++++ .../layerzero/utility/LayerZeroPacket.sol | 152 +++++++ src/apps/layerzero/utility/RLPDecode.sol | 389 ++++++++++++++++++ .../utility/UltraLightNodeEVMDecoder.sol | 48 +++ 7 files changed, 1036 insertions(+), 7 deletions(-) create mode 100644 src/apps/layerzero/utility/Buffer.sol create mode 100644 src/apps/layerzero/utility/BufferOrg.sol create mode 100644 src/apps/layerzero/utility/LayerZeroPacket.sol create mode 100644 src/apps/layerzero/utility/RLPDecode.sol create mode 100644 src/apps/layerzero/utility/UltraLightNodeEVMDecoder.sol diff --git a/src/apps/layerzero/IncentiveFP.sol b/src/apps/layerzero/IncentiveFP.sol index fede99b..fe588ea 100644 --- a/src/apps/layerzero/IncentiveFP.sol +++ b/src/apps/layerzero/IncentiveFP.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 // TODO: Check if we can upgrade solidity version -pragma solidity 0.7.6; -pragma abicoder v2; +// pragma solidity 0.7.6; +pragma solidity ^0.8.13; -import "LayerZero/proof/utility/LayerZeroPacket.sol"; +import "./utility/LayerZeroPacket.sol"; import "LayerZero/interfaces/ILayerZeroValidationLibrary.sol"; import "LayerZero/interfaces/IValidationLibraryHelperV2.sol"; diff --git a/src/apps/layerzero/IncentiveMPT.sol b/src/apps/layerzero/IncentiveMPT.sol index baf09ed..af686c4 100644 --- a/src/apps/layerzero/IncentiveMPT.sol +++ b/src/apps/layerzero/IncentiveMPT.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: BUSL-1.1 // TODO: Check if we can upgrade solidity version -pragma solidity 0.7.6; -pragma abicoder v2; +// pragma solidity 0.7.6; +pragma solidity ^0.8.13; -import "LayerZero/proof/utility/LayerZeroPacket.sol"; -import "LayerZero/proof/utility/UltraLightNodeEVMDecoder.sol"; +import "./utility/LayerZeroPacket.sol"; +import "./utility/UltraLightNodeEVMDecoder.sol"; import "LayerZero/interfaces/IValidationLibraryHelperV2.sol"; import "LayerZero/interfaces/ILayerZeroValidationLibrary.sol"; diff --git a/src/apps/layerzero/utility/Buffer.sol b/src/apps/layerzero/utility/Buffer.sol new file mode 100644 index 0000000..52e5bfe --- /dev/null +++ b/src/apps/layerzero/utility/Buffer.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: BUSL-1.1 + +// https://github.com/ensdomains/buffer + +// TODO: CHECK IF WE CAN UPGRADE +// pragma solidity ^0.7.0; +pragma solidity ^0.8.13; + +/** + * @dev A library for working with mutable byte buffers in Solidity. + * + * Byte buffers are mutable and expandable, and provide a variety of primitives + * for writing to them. At any time you can fetch a bytes object containing the + * current contents of the buffer. The bytes object should not be stored between + * operations, as it may change due to resizing of the buffer. + */ +library Buffer { + /** + * @dev Represents a mutable buffer. Buffers have a current value (buf) and + * a capacity. The capacity may be longer than the current value, in + * which case it can be extended without the need to allocate more memory. + */ + struct buffer { + bytes buf; + uint capacity; + } + + /** + * @dev Initializes a buffer with an initial capacity. + * @param buf The buffer to initialize. + * @param capacity The number of bytes of space to allocate the buffer. + * @return The buffer, for chaining. + */ + function init(buffer memory buf, uint capacity) internal pure returns (buffer memory) { + if (capacity % 32 != 0) { + capacity += 32 - (capacity % 32); + } + // Allocate space for the buffer data + buf.capacity = capacity; + assembly { + let ptr := mload(0x40) + mstore(buf, ptr) + mstore(ptr, 0) + let fpm := add(32, add(ptr, capacity)) + if lt(fpm, ptr) { + revert(0, 0) + } + mstore(0x40, fpm) + } + return buf; + } + + + /** + * @dev Writes a byte string to a buffer. Resizes if doing so would exceed + * the capacity of the buffer. + * @param buf The buffer to append to. + * @param off The start offset to write to. + * @param rawData The data to append. + * @param len The number of bytes to copy. + * @return The original buffer, for chaining. + */ + function writeRawBytes( + buffer memory buf, + uint off, + bytes memory rawData, + uint offData, + uint len + ) internal pure returns (buffer memory) { + if (off + len > buf.capacity) { + resize(buf, max(buf.capacity, len + off) * 2); + } + + uint dest; + uint src; + assembly { + // Memory address of the buffer data + let bufptr := mload(buf) + // Length of existing buffer data + let buflen := mload(bufptr) + // Start address = buffer address + offset + sizeof(buffer length) + dest := add(add(bufptr, 32), off) + // Update buffer length if we're extending it + if gt(add(len, off), buflen) { + mstore(bufptr, add(len, off)) + } + src := add(rawData, offData) + } + + // Copy word-length chunks while possible + for (; len >= 32; len -= 32) { + assembly { + mstore(dest, mload(src)) + } + dest += 32; + src += 32; + } + + // Copy remaining bytes + unchecked { + uint mask = (256 ** (32 - len)) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) + let destpart := and(mload(dest), mask) + mstore(dest, or(destpart, srcpart)) + } + } + + return buf; + } + + /** + * @dev Writes a byte string to a buffer. Resizes if doing so would exceed + * the capacity of the buffer. + * @param buf The buffer to append to. + * @param off The start offset to write to. + * @param data The data to append. + * @param len The number of bytes to copy. + * @return The original buffer, for chaining. + */ + function write(buffer memory buf, uint off, bytes memory data, uint len) internal pure returns (buffer memory) { + require(len <= data.length); + + if (off + len > buf.capacity) { + resize(buf, max(buf.capacity, len + off) * 2); + } + + uint dest; + uint src; + assembly { + // Memory address of the buffer data + let bufptr := mload(buf) + // Length of existing buffer data + let buflen := mload(bufptr) + // Start address = buffer address + offset + sizeof(buffer length) + dest := add(add(bufptr, 32), off) + // Update buffer length if we're extending it + if gt(add(len, off), buflen) { + mstore(bufptr, add(len, off)) + } + src := add(data, 32) + } + + // Copy word-length chunks while possible + for (; len >= 32; len -= 32) { + assembly { + mstore(dest, mload(src)) + } + dest += 32; + src += 32; + } + + // Copy remaining bytes + uint mask = 256**(32 - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) + let destpart := and(mload(dest), mask) + mstore(dest, or(destpart, srcpart)) + } + + return buf; + } + + function append(buffer memory buf, bytes memory data) internal pure returns (buffer memory) { + return write(buf, buf.buf.length, data, data.length); + } + + function resize(buffer memory buf, uint capacity) private pure { + bytes memory oldbuf = buf.buf; + init(buf, capacity); + append(buf, oldbuf); + } + + function max(uint a, uint b) private pure returns (uint) { + if (a > b) { + return a; + } + return b; + } +} diff --git a/src/apps/layerzero/utility/BufferOrg.sol b/src/apps/layerzero/utility/BufferOrg.sol new file mode 100644 index 0000000..3314a65 --- /dev/null +++ b/src/apps/layerzero/utility/BufferOrg.sol @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: BSD-2-Clause +pragma solidity ^0.8.4; + +/** +* @dev A library for working with mutable byte buffers in Solidity. +* +* Byte buffers are mutable and expandable, and provide a variety of primitives +* for appending to them. At any time you can fetch a bytes object containing the +* current contents of the buffer. The bytes object should not be stored between +* operations, as it may change due to resizing of the buffer. +*/ +library Buffer { + /** + * @dev Represents a mutable buffer. Buffers have a current value (buf) and + * a capacity. The capacity may be longer than the current value, in + * which case it can be extended without the need to allocate more memory. + */ + struct buffer { + bytes buf; + uint capacity; + } + + /** + * @dev Initializes a buffer with an initial capacity. + * @param buf The buffer to initialize. + * @param capacity The number of bytes of space to allocate the buffer. + * @return The buffer, for chaining. + */ + function init(buffer memory buf, uint capacity) internal pure returns (buffer memory) { + if (capacity % 32 != 0) { + capacity += 32 - (capacity % 32); + } + // Allocate space for the buffer data + buf.capacity = capacity; + assembly { + let ptr := mload(0x40) + mstore(buf, ptr) + mstore(ptr, 0) + let fpm := add(32, add(ptr, capacity)) + if lt(fpm, ptr) { + revert(0, 0) + } + mstore(0x40, fpm) + } + return buf; + } + + /** + * @dev Initializes a new buffer from an existing bytes object. + * Changes to the buffer may mutate the original value. + * @param b The bytes object to initialize the buffer with. + * @return A new buffer. + */ + function fromBytes(bytes memory b) internal pure returns(buffer memory) { + buffer memory buf; + buf.buf = b; + buf.capacity = b.length; + return buf; + } + + function resize(buffer memory buf, uint capacity) private pure { + bytes memory oldbuf = buf.buf; + init(buf, capacity); + append(buf, oldbuf); + } + + /** + * @dev Sets buffer length to 0. + * @param buf The buffer to truncate. + * @return The original buffer, for chaining.. + */ + function truncate(buffer memory buf) internal pure returns (buffer memory) { + assembly { + let bufptr := mload(buf) + mstore(bufptr, 0) + } + return buf; + } + + /** + * @dev Appends len bytes of a byte string to a buffer. Resizes if doing so would exceed + * the capacity of the buffer. + * @param buf The buffer to append to. + * @param data The data to append. + * @param len The number of bytes to copy. + * @return The original buffer, for chaining. + */ + function append(buffer memory buf, bytes memory data, uint len) internal pure returns(buffer memory) { + require(len <= data.length); + + uint off = buf.buf.length; + uint newCapacity = off + len; + if (newCapacity > buf.capacity) { + resize(buf, newCapacity * 2); + } + + uint dest; + uint src; + assembly { + // Memory address of the buffer data + let bufptr := mload(buf) + // Length of existing buffer data + let buflen := mload(bufptr) + // Start address = buffer address + offset + sizeof(buffer length) + dest := add(add(bufptr, 32), off) + // Update buffer length if we're extending it + if gt(newCapacity, buflen) { + mstore(bufptr, newCapacity) + } + src := add(data, 32) + } + + // Copy word-length chunks while possible + for (; len >= 32; len -= 32) { + assembly { + mstore(dest, mload(src)) + } + dest += 32; + src += 32; + } + + // Copy remaining bytes + unchecked { + uint mask = (256 ** (32 - len)) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) + let destpart := and(mload(dest), mask) + mstore(dest, or(destpart, srcpart)) + } + } + + return buf; + } + + /** + * @dev Appends a byte string to a buffer. Resizes if doing so would exceed + * the capacity of the buffer. + * @param buf The buffer to append to. + * @param data The data to append. + * @return The original buffer, for chaining. + */ + function append(buffer memory buf, bytes memory data) internal pure returns (buffer memory) { + return append(buf, data, data.length); + } + + /** + * @dev Appends a byte to the buffer. Resizes if doing so would exceed the + * capacity of the buffer. + * @param buf The buffer to append to. + * @param data The data to append. + * @return The original buffer, for chaining. + */ + function appendUint8(buffer memory buf, uint8 data) internal pure returns(buffer memory) { + uint off = buf.buf.length; + uint offPlusOne = off + 1; + if (off >= buf.capacity) { + resize(buf, offPlusOne * 2); + } + + assembly { + // Memory address of the buffer data + let bufptr := mload(buf) + // Address = buffer address + sizeof(buffer length) + off + let dest := add(add(bufptr, off), 32) + mstore8(dest, data) + // Update buffer length if we extended it + if gt(offPlusOne, mload(bufptr)) { + mstore(bufptr, offPlusOne) + } + } + + return buf; + } + + /** + * @dev Appends len bytes of bytes32 to a buffer. Resizes if doing so would + * exceed the capacity of the buffer. + * @param buf The buffer to append to. + * @param data The data to append. + * @param len The number of bytes to write (left-aligned). + * @return The original buffer, for chaining. + */ + function append(buffer memory buf, bytes32 data, uint len) private pure returns(buffer memory) { + uint off = buf.buf.length; + uint newCapacity = len + off; + if (newCapacity > buf.capacity) { + resize(buf, newCapacity * 2); + } + + unchecked { + uint mask = (256 ** len) - 1; + // Right-align data + data = data >> (8 * (32 - len)); + assembly { + // Memory address of the buffer data + let bufptr := mload(buf) + // Address = buffer address + sizeof(buffer length) + newCapacity + let dest := add(bufptr, newCapacity) + mstore(dest, or(and(mload(dest), not(mask)), data)) + // Update buffer length if we extended it + if gt(newCapacity, mload(bufptr)) { + mstore(bufptr, newCapacity) + } + } + } + return buf; + } + + /** + * @dev Appends a bytes20 to the buffer. Resizes if doing so would exceed + * the capacity of the buffer. + * @param buf The buffer to append to. + * @param data The data to append. + * @return The original buffer, for chhaining. + */ + function appendBytes20(buffer memory buf, bytes20 data) internal pure returns (buffer memory) { + return append(buf, bytes32(data), 20); + } + + /** + * @dev Appends a bytes32 to the buffer. Resizes if doing so would exceed + * the capacity of the buffer. + * @param buf The buffer to append to. + * @param data The data to append. + * @return The original buffer, for chaining. + */ + function appendBytes32(buffer memory buf, bytes32 data) internal pure returns (buffer memory) { + return append(buf, data, 32); + } + + /** + * @dev Appends a byte to the end of the buffer. Resizes if doing so would + * exceed the capacity of the buffer. + * @param buf The buffer to append to. + * @param data The data to append. + * @param len The number of bytes to write (right-aligned). + * @return The original buffer. + */ + function appendInt(buffer memory buf, uint data, uint len) internal pure returns(buffer memory) { + uint off = buf.buf.length; + uint newCapacity = len + off; + if (newCapacity > buf.capacity) { + resize(buf, newCapacity * 2); + } + + uint mask = (256 ** len) - 1; + assembly { + // Memory address of the buffer data + let bufptr := mload(buf) + // Address = buffer address + sizeof(buffer length) + newCapacity + let dest := add(bufptr, newCapacity) + mstore(dest, or(and(mload(dest), not(mask)), data)) + // Update buffer length if we extended it + if gt(newCapacity, mload(bufptr)) { + mstore(bufptr, newCapacity) + } + } + return buf; + } +} \ No newline at end of file diff --git a/src/apps/layerzero/utility/LayerZeroPacket.sol b/src/apps/layerzero/utility/LayerZeroPacket.sol new file mode 100644 index 0000000..a524239 --- /dev/null +++ b/src/apps/layerzero/utility/LayerZeroPacket.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: BUSL-1.1 + +// TODO: CHECK IF WE CAN UPGRADE +// pragma solidity ^0.7.0; +pragma solidity ^0.8.13; + +import "./Buffer.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +library LayerZeroPacket { + using Buffer for Buffer.buffer; + + struct Packet { + uint16 srcChainId; + uint16 dstChainId; + uint64 nonce; + address dstAddress; + bytes srcAddress; + bytes32 ulnAddress; + bytes payload; + } + + function getPacket( + bytes memory data, + uint16 srcChain, + uint sizeOfSrcAddress, + bytes32 ulnAddress + ) internal pure returns (LayerZeroPacket.Packet memory) { + uint16 dstChainId; + address dstAddress; + uint size; + uint64 nonce; + + // The log consists of the destination chain id and then a bytes payload + // 0--------------------------------------------31 + // 0 | total bytes size + // 32 | destination chain id + // 64 | bytes offset + // 96 | bytes array size + // 128 | payload + assembly { + dstChainId := mload(add(data, 32)) + size := mload(add(data, 96)) /// size of the byte array + nonce := mload(add(data, 104)) // offset to convert to uint64 128 is index -24 + dstAddress := mload(add(data, sub(add(128, sizeOfSrcAddress), 4))) // offset to convert to address 12 -8 + } + + Buffer.buffer memory srcAddressBuffer; + srcAddressBuffer.init(sizeOfSrcAddress); + srcAddressBuffer.writeRawBytes(0, data, 136, sizeOfSrcAddress); // 128 + 8 + + uint payloadSize = size.sub(28).sub(sizeOfSrcAddress); + Buffer.buffer memory payloadBuffer; + payloadBuffer.init(payloadSize); + payloadBuffer.writeRawBytes(0, data, sizeOfSrcAddress.add(156), payloadSize); // 148 + 8 + return LayerZeroPacket.Packet(srcChain, dstChainId, nonce, dstAddress, srcAddressBuffer.buf, ulnAddress, payloadBuffer.buf); + } + + function getPacketV2( + bytes memory data, + uint sizeOfSrcAddress, + bytes32 ulnAddress + ) internal pure returns (LayerZeroPacket.Packet memory) { + // packet def: abi.encodePacked(nonce, srcChain, srcAddress, dstChain, dstAddress, payload); + // data def: abi.encode(packet) = offset(32) + length(32) + packet + // if from EVM + // 0 - 31 0 - 31 | total bytes size + // 32 - 63 32 - 63 | location + // 64 - 95 64 - 95 | size of the packet + // 96 - 103 96 - 103 | nonce + // 104 - 105 104 - 105 | srcChainId + // 106 - P 106 - 125 | srcAddress, where P = 106 + sizeOfSrcAddress - 1, + // P+1 - P+2 126 - 127 | dstChainId + // P+3 - P+22 128 - 147 | dstAddress + // P+23 - END 148 - END | payload + + // decode the packet + uint256 realSize; + uint64 nonce; + uint16 srcChain; + uint16 dstChain; + address dstAddress; + assembly { + realSize := mload(add(data, 64)) + nonce := mload(add(data, 72)) // 104 - 32 + srcChain := mload(add(data, 74)) // 106 - 32 + dstChain := mload(add(data, add(76, sizeOfSrcAddress))) // P + 3 - 32 = 105 + size + 3 - 32 = 76 + size + dstAddress := mload(add(data, add(96, sizeOfSrcAddress))) // P + 23 - 32 = 105 + size + 23 - 32 = 96 + size + } + + require(srcChain != 0, "LayerZeroPacket: invalid packet"); + + Buffer.buffer memory srcAddressBuffer; + srcAddressBuffer.init(sizeOfSrcAddress); + srcAddressBuffer.writeRawBytes(0, data, 106, sizeOfSrcAddress); + + uint nonPayloadSize = sizeOfSrcAddress.add(32);// 2 + 2 + 8 + 20, 32 + 20 = 52 if sizeOfSrcAddress == 20 + uint payloadSize = realSize.sub(nonPayloadSize); + Buffer.buffer memory payloadBuffer; + payloadBuffer.init(payloadSize); + payloadBuffer.writeRawBytes(0, data, nonPayloadSize.add(96), payloadSize); + + return LayerZeroPacket.Packet(srcChain, dstChain, nonce, dstAddress, srcAddressBuffer.buf, ulnAddress, payloadBuffer.buf); + } + + function getPacketV3( + bytes memory data, + uint sizeOfSrcAddress, + bytes32 ulnAddress + ) internal pure returns (LayerZeroPacket.Packet memory) { + // data def: abi.encodePacked(nonce, srcChain, srcAddress, dstChain, dstAddress, payload); + // if from EVM + // 0 - 31 0 - 31 | total bytes size + // 32 - 39 32 - 39 | nonce + // 40 - 41 40 - 41 | srcChainId + // 42 - P 42 - 61 | srcAddress, where P = 41 + sizeOfSrcAddress, + // P+1 - P+2 62 - 63 | dstChainId + // P+3 - P+22 64 - 83 | dstAddress + // P+23 - END 84 - END | payload + + // decode the packet + uint256 realSize = data.length; + uint nonPayloadSize = sizeOfSrcAddress.add(32);// 2 + 2 + 8 + 20, 32 + 20 = 52 if sizeOfSrcAddress == 20 + require(realSize >= nonPayloadSize, "LayerZeroPacket: invalid packet"); + uint payloadSize = realSize - nonPayloadSize; + + uint64 nonce; + uint16 srcChain; + uint16 dstChain; + address dstAddress; + assembly { + nonce := mload(add(data, 8)) // 40 - 32 + srcChain := mload(add(data, 10)) // 42 - 32 + dstChain := mload(add(data, add(12, sizeOfSrcAddress))) // P + 3 - 32 = 41 + size + 3 - 32 = 12 + size + dstAddress := mload(add(data, add(32, sizeOfSrcAddress))) // P + 23 - 32 = 41 + size + 23 - 32 = 32 + size + } + + require(srcChain != 0, "LayerZeroPacket: invalid packet"); + + Buffer.buffer memory srcAddressBuffer; + srcAddressBuffer.init(sizeOfSrcAddress); + srcAddressBuffer.writeRawBytes(0, data, 42, sizeOfSrcAddress); + + Buffer.buffer memory payloadBuffer; + if (payloadSize > 0) { + payloadBuffer.init(payloadSize); + payloadBuffer.writeRawBytes(0, data, nonPayloadSize.add(32), payloadSize); + } + + return LayerZeroPacket.Packet(srcChain, dstChain, nonce, dstAddress, srcAddressBuffer.buf, ulnAddress, payloadBuffer.buf); + } +} diff --git a/src/apps/layerzero/utility/RLPDecode.sol b/src/apps/layerzero/utility/RLPDecode.sol new file mode 100644 index 0000000..723529f --- /dev/null +++ b/src/apps/layerzero/utility/RLPDecode.sol @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: BUSL-1.1 + +// https://github.com/hamdiallam/solidity-rlp + +// TODO: CHECK IF WE CAN UPGRADE +// pragma solidity ^0.7.0; +pragma solidity ^0.8.13; + +library RLPDecode { + uint8 constant STRING_SHORT_START = 0x80; + uint8 constant STRING_LONG_START = 0xb8; + uint8 constant LIST_SHORT_START = 0xc0; + uint8 constant LIST_LONG_START = 0xf8; + uint8 constant WORD_SIZE = 32; + + struct RLPItem { + uint len; + uint memPtr; + } + + struct Iterator { + RLPItem item; // Item that's being iterated over. + uint nextPtr; // Position of the next item in the list. + } + + /* + * @dev Returns the next element in the iteration. Reverts if it has not next element. + * @param self The iterator. + * @return The next element in the iteration. + */ + function next(Iterator memory self) internal pure returns (RLPItem memory) { + require(hasNext(self), "RLPDecoder iterator has no next"); + + uint ptr = self.nextPtr; + uint itemLength = _itemLength(ptr); + self.nextPtr = ptr + itemLength; + + return RLPItem(itemLength, ptr); + } + + /* + * @dev Returns true if the iteration has more elements. + * @param self The iterator. + * @return true if the iteration has more elements. + */ + function hasNext(Iterator memory self) internal pure returns (bool) { + RLPItem memory item = self.item; + return self.nextPtr < item.memPtr + item.len; + } + + /* + * @param item RLP encoded bytes + */ + + function toRlpItem(bytes memory item) internal pure returns (RLPItem memory) { + uint memPtr; + assembly { + memPtr := add(item, 0x20) + } + // offset the pointer if the first byte + + uint8 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + uint len = item.length; + if (len > 0 && byte0 < LIST_SHORT_START) { + assembly { + memPtr := add(memPtr, 0x01) + } + len -= 1; + } + return RLPItem(len, memPtr); + } + + /* + * @dev Create an iterator. Reverts if item is not a list. + * @param self The RLP item. + * @return An 'Iterator' over the item. + */ + function iterator(RLPItem memory self) internal pure returns (Iterator memory) { + require(isList(self), "RLPDecoder iterator is not list"); + + uint ptr = self.memPtr + _payloadOffset(self.memPtr); + return Iterator(self, ptr); + } + + /* + * @param item RLP encoded bytes + */ + function rlpLen(RLPItem memory item) internal pure returns (uint) { + return item.len; + } + + /* + * @param item RLP encoded bytes + */ + function payloadLen(RLPItem memory item) internal pure returns (uint) { + uint offset = _payloadOffset(item.memPtr); + require(item.len >= offset, "RLPDecoder: invalid uint RLP item offset size"); + return item.len - offset; + } + + /* + * @param item RLP encoded list in bytes + */ + function toList(RLPItem memory item) internal pure returns (RLPItem[] memory) { + require(isList(item), "RLPDecoder iterator is not a list"); + + uint items = numItems(item); + RLPItem[] memory result = new RLPItem[](items); + + uint memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint dataLen; + for (uint i = 0; i < items; i++) { + dataLen = _itemLength(memPtr); + result[i] = RLPItem(dataLen, memPtr); + memPtr = memPtr + dataLen; + } + + return result; + } + + /* + * @param get the RLP item by index. save gas. + */ + function getItemByIndex(RLPItem memory item, uint idx) internal pure returns (RLPItem memory) { + require(isList(item), "RLPDecoder iterator is not a list"); + + uint memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint dataLen; + for (uint i = 0; i < idx; i++) { + dataLen = _itemLength(memPtr); + memPtr = memPtr + dataLen; + } + dataLen = _itemLength(memPtr); + return RLPItem(dataLen, memPtr); + } + + + /* + * @param get the RLP item by index. save gas. + */ + function safeGetItemByIndex(RLPItem memory item, uint idx) internal pure returns (RLPItem memory) { + require(isList(item), "RLPDecoder iterator is not a list"); + require(idx < numItems(item), "RLP item out of bounds"); + uint endPtr = item.memPtr + item.len; + + uint memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint dataLen; + for (uint i = 0; i < idx; i++) { + dataLen = _itemLength(memPtr); + memPtr = memPtr + dataLen; + } + dataLen = _itemLength(memPtr); + + require(memPtr + dataLen <= endPtr, "RLP item overflow"); + return RLPItem(dataLen, memPtr); + } + + /* + * @param offset the receipt bytes item + */ + function typeOffset(RLPItem memory item) internal pure returns (RLPItem memory) { + uint offset = _payloadOffset(item.memPtr); + uint8 byte0; + uint memPtr = item.memPtr; + uint len = item.len; + assembly { + memPtr := add(memPtr, offset) + byte0 := byte(0, mload(memPtr)) + } + if (len >0 && byte0 < LIST_SHORT_START) { + assembly { + memPtr := add(memPtr, 0x01) + } + len -= 1; + } + return RLPItem(len, memPtr); + } + + // @return indicator whether encoded payload is a list. negate this function call for isData. + function isList(RLPItem memory item) internal pure returns (bool) { + if (item.len == 0) return false; + + uint8 byte0; + uint memPtr = item.memPtr; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < LIST_SHORT_START) return false; + return true; + } + + /** RLPItem conversions into data types **/ + + // @returns raw rlp encoding in bytes + function toRlpBytes(RLPItem memory item) internal pure returns (bytes memory) { + bytes memory result = new bytes(item.len); + if (result.length == 0) return result; + + uint ptr; + assembly { + ptr := add(0x20, result) + } + + copy(item.memPtr, ptr, item.len); + return result; + } + + // any non-zero byte except "0x80" is considered true + function toBoolean(RLPItem memory item) internal pure returns (bool) { + require(item.len == 1, "RLPDecoder toBoolean invalid length"); + uint result; + uint memPtr = item.memPtr; + assembly { + result := byte(0, mload(memPtr)) + } + + // SEE Github Issue #5. + // Summary: Most commonly used RLP libraries (i.e Geth) will encode + // "0" as "0x80" instead of as "0". We handle this edge case explicitly + // here. + if (result == 0 || result == STRING_SHORT_START) { + return false; + } else { + return true; + } + } + + function toAddress(RLPItem memory item) internal pure returns (address) { + // 1 byte for the length prefix + require(item.len == 21, "RLPDecoder toAddress invalid length"); + + return address(toUint(item)); + } + + function toUint(RLPItem memory item) internal pure returns (uint) { + require(item.len > 0 && item.len <= 33, "RLPDecoder toUint invalid length"); + + uint offset = _payloadOffset(item.memPtr); + require(item.len >= offset, "RLPDecoder: invalid RLP item offset size"); + uint len = item.len - offset; + + uint result; + uint memPtr = item.memPtr + offset; + assembly { + result := mload(memPtr) + + // shift to the correct location if necessary + if lt(len, 32) { + result := div(result, exp(256, sub(32, len))) + } + } + + return result; + } + + // enforces 32 byte length + function toUintStrict(RLPItem memory item) internal pure returns (uint) { + // one byte prefix + require(item.len == 33, "RLPDecoder toUintStrict invalid length"); + + uint result; + uint memPtr = item.memPtr + 1; + assembly { + result := mload(memPtr) + } + + return result; + } + + function toBytes(RLPItem memory item) internal pure returns (bytes memory) { + require(item.len > 0, "RLPDecoder toBytes invalid length"); + + uint offset = _payloadOffset(item.memPtr); + require(item.len >= offset, "RLPDecoder: invalid RLP item offset size"); + uint len = item.len - offset; // data length + bytes memory result = new bytes(len); + + uint destPtr; + assembly { + destPtr := add(0x20, result) + } + + copy(item.memPtr + offset, destPtr, len); + return result; + } + + /* + * Private Helpers + */ + + // @return number of payload items inside an encoded list. + function numItems(RLPItem memory item) internal pure returns (uint) { + if (item.len == 0) return 0; + + uint count = 0; + uint currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint endPtr = item.memPtr + item.len; + while (currPtr < endPtr) { + currPtr = currPtr + _itemLength(currPtr); // skip over an item + count++; + } + + return count; + } + + // @return entire rlp item byte length + function _itemLength(uint memPtr) private pure returns (uint) { + uint itemLen; + uint byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) itemLen = 1; + else if (byte0 < STRING_LONG_START) itemLen = byte0 - STRING_SHORT_START + 1; + else if (byte0 < LIST_SHORT_START) { + assembly { + let byteLen := sub(byte0, 0xb7) // # of bytes the actual length is + memPtr := add(memPtr, 1) // skip over the first byte + + /* 32 byte word size */ + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len + itemLen := add(dataLen, add(byteLen, 1)) + } + } else if (byte0 < LIST_LONG_START) { + itemLen = byte0 - LIST_SHORT_START + 1; + } else { + assembly { + let byteLen := sub(byte0, 0xf7) + memPtr := add(memPtr, 1) + + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length + itemLen := add(dataLen, add(byteLen, 1)) + } + } + + return itemLen; + } + + // @return number of bytes until the data + function _payloadOffset(uint memPtr) private pure returns (uint) { + uint byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) return 0; + else if (byte0 < STRING_LONG_START || (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START)) return 1; + else if (byte0 < LIST_SHORT_START) + // being explicit + return byte0 - (STRING_LONG_START - 1) + 1; + else return byte0 - (LIST_LONG_START - 1) + 1; + } + + /* + * @param src Pointer to source + * @param dest Pointer to destination + * @param len Amount of memory to copy from the source + */ + function copy( + uint src, + uint dest, + uint len + ) private pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + // left over bytes. Mask is used to remove unwanted bytes from the word + uint mask = 256**(WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } +} diff --git a/src/apps/layerzero/utility/UltraLightNodeEVMDecoder.sol b/src/apps/layerzero/utility/UltraLightNodeEVMDecoder.sol new file mode 100644 index 0000000..6809d14 --- /dev/null +++ b/src/apps/layerzero/utility/UltraLightNodeEVMDecoder.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BUSL-1.1 + +// TODO: CHECK IF WE CAN UPGRADE +// pragma solidity ^0.7.0; +pragma solidity ^0.8.13; + +import "./RLPDecode.sol"; + +library UltraLightNodeEVMDecoder { + using RLPDecode for RLPDecode.RLPItem; + using RLPDecode for RLPDecode.Iterator; + + struct Log { + address contractAddress; + bytes32 topicZero; + bytes data; + } + + function getReceiptLog(bytes memory data, uint logIndex) internal pure returns (Log memory) { + RLPDecode.Iterator memory it = RLPDecode.toRlpItem(data).iterator(); + uint idx; + while (it.hasNext()) { + if (idx == 3) { + return toReceiptLog(it.next().getItemByIndex(logIndex).toRlpBytes()); + } else it.next(); + idx++; + } + revert("no log index in receipt"); + } + + function toReceiptLog(bytes memory data) internal pure returns (Log memory) { + RLPDecode.Iterator memory it = RLPDecode.toRlpItem(data).iterator(); + Log memory log; + + uint idx; + while (it.hasNext()) { + if (idx == 0) { + log.contractAddress = it.next().toAddress(); + } else if (idx == 1) { + RLPDecode.RLPItem memory item = it.next().getItemByIndex(0); + log.topicZero = bytes32(item.toUint()); + } else if (idx == 2) log.data = it.next().toBytes(); + else it.next(); + idx++; + } + return log; + } +} From 223d1ab52c30ab3bb7db44a91c9d5bbf57f30b6e Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 04/61] feat: move requirements into our repo --- lib/LayerZero | 1 - src/apps/layerzero/IncentiveFP.sol | 4 ++-- src/apps/layerzero/IncentiveMPT.sol | 4 ++-- .../ILayerZeroValidationLibrary.sol | 9 ++++++++ .../interfaces/IValidationLibraryHelperV2.sol | 21 +++++++++++++++++++ src/apps/layerzero/utility/Buffer.sol | 2 +- .../layerzero/utility/LayerZeroPacket.sol | 13 ++++++------ 7 files changed, 41 insertions(+), 13 deletions(-) delete mode 160000 lib/LayerZero create mode 100644 src/apps/layerzero/interfaces/ILayerZeroValidationLibrary.sol create mode 100644 src/apps/layerzero/interfaces/IValidationLibraryHelperV2.sol diff --git a/lib/LayerZero b/lib/LayerZero deleted file mode 160000 index 48c21c3..0000000 --- a/lib/LayerZero +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 48c21c3921931798184367fc02d3a8132b041942 diff --git a/src/apps/layerzero/IncentiveFP.sol b/src/apps/layerzero/IncentiveFP.sol index fe588ea..2e77ce9 100644 --- a/src/apps/layerzero/IncentiveFP.sol +++ b/src/apps/layerzero/IncentiveFP.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.13; import "./utility/LayerZeroPacket.sol"; -import "LayerZero/interfaces/ILayerZeroValidationLibrary.sol"; -import "LayerZero/interfaces/IValidationLibraryHelperV2.sol"; +import "./interfaces/ILayerZeroValidationLibrary.sol"; +import "./interfaces/IValidationLibraryHelperV2.sol"; contract FPValidator is ILayerZeroValidationLibrary, IValidationLibraryHelperV2 { uint8 public proofType = 2; diff --git a/src/apps/layerzero/IncentiveMPT.sol b/src/apps/layerzero/IncentiveMPT.sol index af686c4..df1ba48 100644 --- a/src/apps/layerzero/IncentiveMPT.sol +++ b/src/apps/layerzero/IncentiveMPT.sol @@ -6,8 +6,8 @@ pragma solidity ^0.8.13; import "./utility/LayerZeroPacket.sol"; import "./utility/UltraLightNodeEVMDecoder.sol"; -import "LayerZero/interfaces/IValidationLibraryHelperV2.sol"; -import "LayerZero/interfaces/ILayerZeroValidationLibrary.sol"; +import "./interfaces/IValidationLibraryHelperV2.sol"; +import "./interfaces/ILayerZeroValidationLibrary.sol"; contract MPTValidator01 is ILayerZeroValidationLibrary, IValidationLibraryHelperV2 { using RLPDecode for RLPDecode.RLPItem; diff --git a/src/apps/layerzero/interfaces/ILayerZeroValidationLibrary.sol b/src/apps/layerzero/interfaces/ILayerZeroValidationLibrary.sol new file mode 100644 index 0000000..f9435e0 --- /dev/null +++ b/src/apps/layerzero/interfaces/ILayerZeroValidationLibrary.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity >=0.8.0; + +import "../utility/LayerZeroPacket.sol"; + +interface ILayerZeroValidationLibrary { + function validateProof(bytes32 blockData, bytes calldata _data, uint _remoteAddressSize) external returns (LayerZeroPacket.Packet memory packet); +} diff --git a/src/apps/layerzero/interfaces/IValidationLibraryHelperV2.sol b/src/apps/layerzero/interfaces/IValidationLibraryHelperV2.sol new file mode 100644 index 0000000..ce2ed18 --- /dev/null +++ b/src/apps/layerzero/interfaces/IValidationLibraryHelperV2.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity >=0.8.0; + +import "../utility/LayerZeroPacket.sol"; + +interface IValidationLibraryHelperV2 { + struct ULNLog { + bytes32 contractAddress; + bytes32 topicZeroSig; + bytes data; + } + + function getVerifyLog(bytes32 hashRoot, uint[] calldata receiptSlotIndex, uint logIndex, bytes[] calldata proof) external pure returns (ULNLog memory); + + function getPacket(bytes calldata data, uint sizeOfSrcAddress, bytes32 ulnAddress) external pure returns (LayerZeroPacket.Packet memory); + + function getUtilsVersion() external view returns (uint8); + + function getProofType() external view returns (uint8); +} diff --git a/src/apps/layerzero/utility/Buffer.sol b/src/apps/layerzero/utility/Buffer.sol index 52e5bfe..a2b9fa9 100644 --- a/src/apps/layerzero/utility/Buffer.sol +++ b/src/apps/layerzero/utility/Buffer.sol @@ -3,8 +3,8 @@ // https://github.com/ensdomains/buffer // TODO: CHECK IF WE CAN UPGRADE -// pragma solidity ^0.7.0; pragma solidity ^0.8.13; +// pragma solidity ^0.7.0; /** * @dev A library for working with mutable byte buffers in Solidity. diff --git a/src/apps/layerzero/utility/LayerZeroPacket.sol b/src/apps/layerzero/utility/LayerZeroPacket.sol index a524239..fcd6d9d 100644 --- a/src/apps/layerzero/utility/LayerZeroPacket.sol +++ b/src/apps/layerzero/utility/LayerZeroPacket.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 // TODO: CHECK IF WE CAN UPGRADE -// pragma solidity ^0.7.0; pragma solidity ^0.8.13; +// pragma solidity ^0.7.0; import "./Buffer.sol"; -import "@openzeppelin/contracts/math/SafeMath.sol"; library LayerZeroPacket { using Buffer for Buffer.buffer; @@ -49,10 +48,10 @@ library LayerZeroPacket { srcAddressBuffer.init(sizeOfSrcAddress); srcAddressBuffer.writeRawBytes(0, data, 136, sizeOfSrcAddress); // 128 + 8 - uint payloadSize = size.sub(28).sub(sizeOfSrcAddress); + uint payloadSize = (size - 28) - sizeOfSrcAddress; Buffer.buffer memory payloadBuffer; payloadBuffer.init(payloadSize); - payloadBuffer.writeRawBytes(0, data, sizeOfSrcAddress.add(156), payloadSize); // 148 + 8 + payloadBuffer.writeRawBytes(0, data, sizeOfSrcAddress + 156, payloadSize); // 148 + 8 return LayerZeroPacket.Packet(srcChain, dstChainId, nonce, dstAddress, srcAddressBuffer.buf, ulnAddress, payloadBuffer.buf); } @@ -94,7 +93,7 @@ library LayerZeroPacket { srcAddressBuffer.init(sizeOfSrcAddress); srcAddressBuffer.writeRawBytes(0, data, 106, sizeOfSrcAddress); - uint nonPayloadSize = sizeOfSrcAddress.add(32);// 2 + 2 + 8 + 20, 32 + 20 = 52 if sizeOfSrcAddress == 20 + uint nonPayloadSize = sizeOfSrcAddress + 32;// 2 + 2 + 8 + 20, 32 + 20 = 52 if sizeOfSrcAddress == 20 uint payloadSize = realSize.sub(nonPayloadSize); Buffer.buffer memory payloadBuffer; payloadBuffer.init(payloadSize); @@ -120,7 +119,7 @@ library LayerZeroPacket { // decode the packet uint256 realSize = data.length; - uint nonPayloadSize = sizeOfSrcAddress.add(32);// 2 + 2 + 8 + 20, 32 + 20 = 52 if sizeOfSrcAddress == 20 + uint nonPayloadSize = sizeOfSrcAddress + 32;// 2 + 2 + 8 + 20, 32 + 20 = 52 if sizeOfSrcAddress == 20 require(realSize >= nonPayloadSize, "LayerZeroPacket: invalid packet"); uint payloadSize = realSize - nonPayloadSize; @@ -144,7 +143,7 @@ library LayerZeroPacket { Buffer.buffer memory payloadBuffer; if (payloadSize > 0) { payloadBuffer.init(payloadSize); - payloadBuffer.writeRawBytes(0, data, nonPayloadSize.add(32), payloadSize); + payloadBuffer.writeRawBytes(0, data, nonPayloadSize + 32, payloadSize); } return LayerZeroPacket.Packet(srcChain, dstChain, nonce, dstAddress, srcAddressBuffer.buf, ulnAddress, payloadBuffer.buf); From 0a53131f1db01c351512c960fc8e9be605451f70 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 05/61] feat: compiling in on 0.8 --- src/apps/layerzero/IncentiveFP.sol | 4 +- src/apps/layerzero/IncentiveMPT.sol | 4 +- .../layerzero/IncentivizedLayerZeroEscrow.sol | 56 +++++++----- .../interfaces/ILayerZeroEndpoint.sol | 87 +++++++++++++++++++ .../ILayerZeroUserApplicationConfig.sol | 25 ++++++ .../layerzero/utility/LayerZeroPacket.sol | 4 +- src/apps/layerzero/utility/RLPDecode.sol | 2 +- 7 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 src/apps/layerzero/interfaces/ILayerZeroEndpoint.sol create mode 100644 src/apps/layerzero/interfaces/ILayerZeroUserApplicationConfig.sol diff --git a/src/apps/layerzero/IncentiveFP.sol b/src/apps/layerzero/IncentiveFP.sol index 2e77ce9..103f455 100644 --- a/src/apps/layerzero/IncentiveFP.sol +++ b/src/apps/layerzero/IncentiveFP.sol @@ -9,8 +9,8 @@ import "./interfaces/ILayerZeroValidationLibrary.sol"; import "./interfaces/IValidationLibraryHelperV2.sol"; contract FPValidator is ILayerZeroValidationLibrary, IValidationLibraryHelperV2 { - uint8 public proofType = 2; - uint8 public utilsVersion = 1; + uint8 public constant proofType = 2; + uint8 public constant utilsVersion = 1; function validateProof(bytes32 _packetHash, bytes calldata _transactionProof, uint _remoteAddressSize) external view override returns (LayerZeroPacket.Packet memory packet) { require(_remoteAddressSize > 0, "ProofLib: invalid address size"); diff --git a/src/apps/layerzero/IncentiveMPT.sol b/src/apps/layerzero/IncentiveMPT.sol index df1ba48..dbac36b 100644 --- a/src/apps/layerzero/IncentiveMPT.sol +++ b/src/apps/layerzero/IncentiveMPT.sol @@ -13,8 +13,8 @@ contract MPTValidator01 is ILayerZeroValidationLibrary, IValidationLibraryHelper using RLPDecode for RLPDecode.RLPItem; using RLPDecode for RLPDecode.Iterator; - uint8 public proofType = 1; - uint8 public utilsVersion = 4; + uint8 public constant proofType = 1; + uint8 public constant utilsVersion = 4; bytes32 public constant PACKET_SIGNATURE = 0xe9bded5f24a4168e4f3bf44e00298c993b22376aad8c58c7dda9718a54cbea82; function validateProof(bytes32 _receiptsRoot, bytes calldata _transactionProof, uint _remoteAddressSize) external view override returns (LayerZeroPacket.Packet memory packet) { diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 319a552..2a93324 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -2,25 +2,31 @@ pragma solidity ^0.8.13; import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; -import { ILayerZeroValidationLibrary } from "LayerZero/interfaces/ILayerZeroValidationLibrary.sol"; +import { ILayerZeroValidationLibrary } from "./interfaces/ILayerZeroValidationLibrary.sol"; +import { ILayerZeroValidationLibrary } from "./interfaces/ILayerZeroValidationLibrary.sol"; +import { ILayerZeroEndpoint } from "./interfaces/ILayerZeroEndpoint.sol"; /** * @notice LayerZero escrow. * Do not use because of license issues. */ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ILayerZeroValidationLibrary { + error LayerZeroCannotBeAddress0(); - constructor(address sendLostGasTo) IncentivizedMessageEscrow(sendLostGasTo) { + ILayerZeroEndpoint immutable LAYER_ZERO; + + // chainid is immutable on LayerZero endpoint, so we read it and store it likewise. + uint16 public immutable chainId; + + constructor(address sendLostGasTo, address layer_zero) IncentivizedMessageEscrow(sendLostGasTo) { + if (layer_zero == address(0)) revert LayerZeroCannotBeAddress0(); + LAYER_ZERO = ILayerZeroEndpoint(layer_zero); + chainId = LAYER_ZERO.getChainId(); } function estimateAdditionalCost() external view returns(address asset, uint256 amount) { asset = address(0); - amount = costOfMessages; - } - - function collectPayments() external { - payable(owner()).transfer(accumulator - 1); - accumulator = 1; + amount = 0; // TODO: Verify. } function _getMessageIdentifier( @@ -29,8 +35,9 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ) internal override view returns(bytes32) { return keccak256( abi.encodePacked( + msg.sender, bytes32(block.number), - UNIQUE_SOURCE_IDENTIFIER, + chainId, destinationIdentifier, message ) @@ -38,24 +45,17 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, } function _verifyPacket(bytes calldata _metadata, bytes calldata _message) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { - - // Get signature from message payload - (uint8 v, bytes32 r, bytes32 s) = abi.decode(_metadata, (uint8, bytes32, bytes32)); - - // Get signer of message - address messageSigner = ecrecover(keccak256(_message), v, r, s); - - // Check signer is the same as the stored signer. - require(messageSigner == owner(), "!signer"); + // TODO: Set verification logic. + // require(messageSigner == owner(), "!signer"); // Load the identifier for the calling contract. implementationIdentifier = _message[0:32]; // Local "supposedly" this chain identifier. - bytes32 thisChainIdentifier = bytes32(_message[64:96]); + uint16 thisChainIdentifier = uint16(uint256(bytes32(_message[64:96]))); // Check that the message is intended for this chain. - require(thisChainIdentifier == UNIQUE_SOURCE_IDENTIFIER, "!Identifier"); + require(thisChainIdentifier == chainId, "!Identifier"); // Local the identifier for the source chain. sourceIdentifier = bytes32(_message[32:64]); @@ -66,6 +66,22 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfsendPacketInNativeToken) { // Handoff package to LZ. + uint16 _dstChainId = uint16(uint256(destinationChainIdentifier)); + // From LZ docs: "the address on destination chain (in bytes). address length/format may vary by chains" + // bytes memory _destination = destinationImplementation; + // bytes memory _payload = message; + address payable _refundAddress = payable(0); // TODO: What here? + address _zroPaymentAddress = address(0); // TODO: What here? + bytes memory _adapterParams = hex""; + + LAYER_ZERO.send( + _dstChainId, + destinationImplementation, + message, + _refundAddress, + _zroPaymentAddress, + _adapterParams + ); return costOfsendPacketInNativeToken = uint128(0); } diff --git a/src/apps/layerzero/interfaces/ILayerZeroEndpoint.sol b/src/apps/layerzero/interfaces/ILayerZeroEndpoint.sol new file mode 100644 index 0000000..d9280ed --- /dev/null +++ b/src/apps/layerzero/interfaces/ILayerZeroEndpoint.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity >=0.5.0; + +import "./ILayerZeroUserApplicationConfig.sol"; + +interface ILayerZeroEndpoint is ILayerZeroUserApplicationConfig { + // @notice send a LayerZero message to the specified address at a LayerZero endpoint. + // @param _dstChainId - the destination chain identifier + // @param _destination - the address on destination chain (in bytes). address length/format may vary by chains + // @param _payload - a custom bytes payload to send to the destination contract + // @param _refundAddress - if the source transaction is cheaper than the amount of value passed, refund the additional amount to this address + // @param _zroPaymentAddress - the address of the ZRO token holder who would pay for the transaction + // @param _adapterParams - parameters for custom functionality. e.g. receive airdropped native gas from the relayer on destination + function send(uint16 _dstChainId, bytes calldata _destination, bytes calldata _payload, address payable _refundAddress, address _zroPaymentAddress, bytes calldata _adapterParams) external payable; + + // @notice used by the messaging library to publish verified payload + // @param _srcChainId - the source chain identifier + // @param _srcAddress - the source contract (as bytes) at the source chain + // @param _dstAddress - the address on destination chain + // @param _nonce - the unbound message ordering nonce + // @param _gasLimit - the gas limit for external contract execution + // @param _payload - verified payload to send to the destination contract + function receivePayload(uint16 _srcChainId, bytes calldata _srcAddress, address _dstAddress, uint64 _nonce, uint _gasLimit, bytes calldata _payload) external; + + // @notice get the inboundNonce of a receiver from a source chain which could be EVM or non-EVM chain + // @param _srcChainId - the source chain identifier + // @param _srcAddress - the source chain contract address + function getInboundNonce(uint16 _srcChainId, bytes calldata _srcAddress) external view returns (uint64); + + // @notice get the outboundNonce from this source chain which, consequently, is always an EVM + // @param _srcAddress - the source chain contract address + function getOutboundNonce(uint16 _dstChainId, address _srcAddress) external view returns (uint64); + + // @notice gets a quote in source native gas, for the amount that send() requires to pay for message delivery + // @param _dstChainId - the destination chain identifier + // @param _userApplication - the user app address on this EVM chain + // @param _payload - the custom message to send over LayerZero + // @param _payInZRO - if false, user app pays the protocol fee in native token + // @param _adapterParam - parameters for the adapter service, e.g. send some dust native token to dstChain + function estimateFees(uint16 _dstChainId, address _userApplication, bytes calldata _payload, bool _payInZRO, bytes calldata _adapterParam) external view returns (uint nativeFee, uint zroFee); + + // @notice get this Endpoint's immutable source identifier + function getChainId() external view returns (uint16); + + // @notice the interface to retry failed message on this Endpoint destination + // @param _srcChainId - the source chain identifier + // @param _srcAddress - the source chain contract address + // @param _payload - the payload to be retried + function retryPayload(uint16 _srcChainId, bytes calldata _srcAddress, bytes calldata _payload) external; + + // @notice query if any STORED payload (message blocking) at the endpoint. + // @param _srcChainId - the source chain identifier + // @param _srcAddress - the source chain contract address + function hasStoredPayload(uint16 _srcChainId, bytes calldata _srcAddress) external view returns (bool); + + // @notice query if the _libraryAddress is valid for sending msgs. + // @param _userApplication - the user app address on this EVM chain + function getSendLibraryAddress(address _userApplication) external view returns (address); + + // @notice query if the _libraryAddress is valid for receiving msgs. + // @param _userApplication - the user app address on this EVM chain + function getReceiveLibraryAddress(address _userApplication) external view returns (address); + + // @notice query if the non-reentrancy guard for send() is on + // @return true if the guard is on. false otherwise + function isSendingPayload() external view returns (bool); + + // @notice query if the non-reentrancy guard for receive() is on + // @return true if the guard is on. false otherwise + function isReceivingPayload() external view returns (bool); + + // @notice get the configuration of the LayerZero messaging library of the specified version + // @param _version - messaging library version + // @param _chainId - the chainId for the pending config change + // @param _userApplication - the contract address of the user application + // @param _configType - type of configuration. every messaging library has its own convention. + function getConfig(uint16 _version, uint16 _chainId, address _userApplication, uint _configType) external view returns (bytes memory); + + // @notice get the send() LayerZero messaging library version + // @param _userApplication - the contract address of the user application + function getSendVersion(address _userApplication) external view returns (uint16); + + // @notice get the lzReceive() LayerZero messaging library version + // @param _userApplication - the contract address of the user application + function getReceiveVersion(address _userApplication) external view returns (uint16); +} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/ILayerZeroUserApplicationConfig.sol b/src/apps/layerzero/interfaces/ILayerZeroUserApplicationConfig.sol new file mode 100644 index 0000000..8197ebf --- /dev/null +++ b/src/apps/layerzero/interfaces/ILayerZeroUserApplicationConfig.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity >=0.5.0; + +interface ILayerZeroUserApplicationConfig { + // @notice set the configuration of the LayerZero messaging library of the specified version + // @param _version - messaging library version + // @param _chainId - the chainId for the pending config change + // @param _configType - type of configuration. every messaging library has its own convention. + // @param _config - configuration in the bytes. can encode arbitrary content. + function setConfig(uint16 _version, uint16 _chainId, uint _configType, bytes calldata _config) external; + + // @notice set the send() LayerZero messaging library version to _version + // @param _version - new messaging library version + function setSendVersion(uint16 _version) external; + + // @notice set the lzReceive() LayerZero messaging library version to _version + // @param _version - new messaging library version + function setReceiveVersion(uint16 _version) external; + + // @notice Only when the UA needs to resume the message flow in blocking mode and clear the stored payload + // @param _srcChainId - the chainId of the source chain + // @param _srcAddress - the contract address of the source contract at the source chain + function forceResumeReceive(uint16 _srcChainId, bytes calldata _srcAddress) external; +} \ No newline at end of file diff --git a/src/apps/layerzero/utility/LayerZeroPacket.sol b/src/apps/layerzero/utility/LayerZeroPacket.sol index fcd6d9d..e42f965 100644 --- a/src/apps/layerzero/utility/LayerZeroPacket.sol +++ b/src/apps/layerzero/utility/LayerZeroPacket.sol @@ -94,10 +94,10 @@ library LayerZeroPacket { srcAddressBuffer.writeRawBytes(0, data, 106, sizeOfSrcAddress); uint nonPayloadSize = sizeOfSrcAddress + 32;// 2 + 2 + 8 + 20, 32 + 20 = 52 if sizeOfSrcAddress == 20 - uint payloadSize = realSize.sub(nonPayloadSize); + uint payloadSize = realSize - nonPayloadSize; Buffer.buffer memory payloadBuffer; payloadBuffer.init(payloadSize); - payloadBuffer.writeRawBytes(0, data, nonPayloadSize.add(96), payloadSize); + payloadBuffer.writeRawBytes(0, data, nonPayloadSize + 96, payloadSize); return LayerZeroPacket.Packet(srcChain, dstChain, nonce, dstAddress, srcAddressBuffer.buf, ulnAddress, payloadBuffer.buf); } diff --git a/src/apps/layerzero/utility/RLPDecode.sol b/src/apps/layerzero/utility/RLPDecode.sol index 723529f..bd6324b 100644 --- a/src/apps/layerzero/utility/RLPDecode.sol +++ b/src/apps/layerzero/utility/RLPDecode.sol @@ -233,7 +233,7 @@ library RLPDecode { // 1 byte for the length prefix require(item.len == 21, "RLPDecoder toAddress invalid length"); - return address(toUint(item)); + return address(uint160((toUint(item)))); } function toUint(RLPItem memory item) internal pure returns (uint) { From 185c27719b0de52754e0c4bc1405a413efe0d013 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 06/61] feat: send in endpoint v2 --- src/apps/layerzero/IncentiveFP.sol | 43 -- src/apps/layerzero/IncentiveMPT.sol | 71 ---- .../layerzero/IncentivizedLayerZeroEscrow.sol | 43 +- .../interfaces/ILayerZeroEndpoint.sol | 87 ---- .../interfaces/ILayerZeroEndpointV2.sol | 89 ++++ .../ILayerZeroUserApplicationConfig.sol | 25 -- .../ILayerZeroValidationLibrary.sol | 9 - .../interfaces/IMessageLibManager.sol | 70 ++++ .../interfaces/IMessagingChannel.sol | 34 ++ .../interfaces/IMessagingComposer.sol | 38 ++ .../interfaces/IMessagingContext.sol | 9 + .../interfaces/IValidationLibraryHelperV2.sol | 21 - src/apps/layerzero/utility/Buffer.sol | 180 -------- src/apps/layerzero/utility/BufferOrg.sol | 260 ------------ .../layerzero/utility/LayerZeroPacket.sol | 151 ------- src/apps/layerzero/utility/RLPDecode.sol | 389 ------------------ .../utility/UltraLightNodeEVMDecoder.sol | 48 --- 17 files changed, 259 insertions(+), 1308 deletions(-) delete mode 100644 src/apps/layerzero/IncentiveFP.sol delete mode 100644 src/apps/layerzero/IncentiveMPT.sol delete mode 100644 src/apps/layerzero/interfaces/ILayerZeroEndpoint.sol create mode 100644 src/apps/layerzero/interfaces/ILayerZeroEndpointV2.sol delete mode 100644 src/apps/layerzero/interfaces/ILayerZeroUserApplicationConfig.sol delete mode 100644 src/apps/layerzero/interfaces/ILayerZeroValidationLibrary.sol create mode 100644 src/apps/layerzero/interfaces/IMessageLibManager.sol create mode 100644 src/apps/layerzero/interfaces/IMessagingChannel.sol create mode 100644 src/apps/layerzero/interfaces/IMessagingComposer.sol create mode 100644 src/apps/layerzero/interfaces/IMessagingContext.sol delete mode 100644 src/apps/layerzero/interfaces/IValidationLibraryHelperV2.sol delete mode 100644 src/apps/layerzero/utility/Buffer.sol delete mode 100644 src/apps/layerzero/utility/BufferOrg.sol delete mode 100644 src/apps/layerzero/utility/LayerZeroPacket.sol delete mode 100644 src/apps/layerzero/utility/RLPDecode.sol delete mode 100644 src/apps/layerzero/utility/UltraLightNodeEVMDecoder.sol diff --git a/src/apps/layerzero/IncentiveFP.sol b/src/apps/layerzero/IncentiveFP.sol deleted file mode 100644 index 103f455..0000000 --- a/src/apps/layerzero/IncentiveFP.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -// TODO: Check if we can upgrade solidity version -// pragma solidity 0.7.6; -pragma solidity ^0.8.13; - -import "./utility/LayerZeroPacket.sol"; -import "./interfaces/ILayerZeroValidationLibrary.sol"; -import "./interfaces/IValidationLibraryHelperV2.sol"; - -contract FPValidator is ILayerZeroValidationLibrary, IValidationLibraryHelperV2 { - uint8 public constant proofType = 2; - uint8 public constant utilsVersion = 1; - - function validateProof(bytes32 _packetHash, bytes calldata _transactionProof, uint _remoteAddressSize) external view override returns (LayerZeroPacket.Packet memory packet) { - require(_remoteAddressSize > 0, "ProofLib: invalid address size"); - // _transactionProof = srcUlnAddress (32 bytes) + lzPacket - require(_transactionProof.length > 32 && keccak256(_transactionProof) == _packetHash, "ProofLib: invalid transaction proof"); - - bytes memory ulnAddressBytes = bytes(_transactionProof[0:32]); - bytes32 ulnAddress; - assembly { - ulnAddress := mload(add(ulnAddressBytes, 32)) - } - packet = LayerZeroPacket.getPacketV3(_transactionProof[32:], _remoteAddressSize, ulnAddress); - - return packet; - } - - function getUtilsVersion() external view override returns (uint8) { - return utilsVersion; - } - - function getProofType() external view override returns (uint8) { - return proofType; - } - - function getVerifyLog(bytes32, uint[] calldata, uint, bytes[] calldata proof) external pure override returns (ULNLog memory log) {} - - function getPacket(bytes memory data, uint sizeOfSrcAddress, bytes32 ulnAddress) external pure override returns (LayerZeroPacket.Packet memory) { - return LayerZeroPacket.getPacketV3(data, sizeOfSrcAddress, ulnAddress); - } -} diff --git a/src/apps/layerzero/IncentiveMPT.sol b/src/apps/layerzero/IncentiveMPT.sol deleted file mode 100644 index dbac36b..0000000 --- a/src/apps/layerzero/IncentiveMPT.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -// TODO: Check if we can upgrade solidity version -// pragma solidity 0.7.6; -pragma solidity ^0.8.13; - -import "./utility/LayerZeroPacket.sol"; -import "./utility/UltraLightNodeEVMDecoder.sol"; -import "./interfaces/IValidationLibraryHelperV2.sol"; -import "./interfaces/ILayerZeroValidationLibrary.sol"; - -contract MPTValidator01 is ILayerZeroValidationLibrary, IValidationLibraryHelperV2 { - using RLPDecode for RLPDecode.RLPItem; - using RLPDecode for RLPDecode.Iterator; - - uint8 public constant proofType = 1; - uint8 public constant utilsVersion = 4; - bytes32 public constant PACKET_SIGNATURE = 0xe9bded5f24a4168e4f3bf44e00298c993b22376aad8c58c7dda9718a54cbea82; - - function validateProof(bytes32 _receiptsRoot, bytes calldata _transactionProof, uint _remoteAddressSize) external view override returns (LayerZeroPacket.Packet memory packet) { - require(_remoteAddressSize > 0, "ProofLib: invalid address size"); - (bytes[] memory proof, uint[] memory receiptSlotIndex, uint logIndex) = abi.decode(_transactionProof, (bytes[], uint[], uint)); - - ULNLog memory log = _getVerifiedLog(_receiptsRoot, receiptSlotIndex, logIndex, proof); - require(log.topicZeroSig == PACKET_SIGNATURE, "ProofLib: packet not recognized"); //data - - packet = LayerZeroPacket.getPacketV2(log.data, _remoteAddressSize, log.contractAddress); - - return packet; - } - - function _getVerifiedLog(bytes32 hashRoot, uint[] memory paths, uint logIndex, bytes[] memory proof) internal pure returns (ULNLog memory) { - require(paths.length == proof.length, "ProofLib: invalid proof size"); - require(proof.length > 0, "ProofLib: proof size must > 0"); - RLPDecode.RLPItem memory item; - bytes memory proofBytes; - - for (uint i = 0; i < proof.length; i++) { - proofBytes = proof[i]; - require(hashRoot == keccak256(proofBytes), "ProofLib: invalid hashlink"); - item = RLPDecode.toRlpItem(proofBytes).safeGetItemByIndex(paths[i]); - if (i < proof.length - 1) hashRoot = bytes32(item.toUint()); - } - - // burning status + gasUsed + logBloom - RLPDecode.RLPItem memory logItem = item.typeOffset().safeGetItemByIndex(3); - RLPDecode.Iterator memory it = logItem.safeGetItemByIndex(logIndex).iterator(); - ULNLog memory log; - log.contractAddress = bytes32(it.next().toUint()); - log.topicZeroSig = bytes32(it.next().safeGetItemByIndex(0).toUint()); - log.data = it.next().toBytes(); - - return log; - } - - function getUtilsVersion() external view override returns (uint8) { - return utilsVersion; - } - - function getProofType() external view override returns (uint8) { - return proofType; - } - - function getVerifyLog(bytes32 hashRoot, uint[] memory receiptSlotIndex, uint logIndex, bytes[] memory proof) external pure override returns (ULNLog memory) { - return _getVerifiedLog(hashRoot, receiptSlotIndex, logIndex, proof); - } - - function getPacket(bytes memory data, uint sizeOfSrcAddress, bytes32 ulnAddress) external pure override returns (LayerZeroPacket.Packet memory) { - return LayerZeroPacket.getPacketV2(data, sizeOfSrcAddress, ulnAddress); - } -} diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 2a93324..6ed657e 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -2,26 +2,24 @@ pragma solidity ^0.8.13; import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; -import { ILayerZeroValidationLibrary } from "./interfaces/ILayerZeroValidationLibrary.sol"; -import { ILayerZeroValidationLibrary } from "./interfaces/ILayerZeroValidationLibrary.sol"; -import { ILayerZeroEndpoint } from "./interfaces/ILayerZeroEndpoint.sol"; +import { ILayerZeroEndpointV2, MessagingParams } from "./interfaces/ILayerZeroEndpointV2.sol"; /** * @notice LayerZero escrow. * Do not use because of license issues. */ -abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ILayerZeroValidationLibrary { +abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { error LayerZeroCannotBeAddress0(); - ILayerZeroEndpoint immutable LAYER_ZERO; + ILayerZeroEndpointV2 immutable LAYER_ZERO; // chainid is immutable on LayerZero endpoint, so we read it and store it likewise. - uint16 public immutable chainId; + uint32 public immutable chainId; constructor(address sendLostGasTo, address layer_zero) IncentivizedMessageEscrow(sendLostGasTo) { if (layer_zero == address(0)) revert LayerZeroCannotBeAddress0(); - LAYER_ZERO = ILayerZeroEndpoint(layer_zero); - chainId = LAYER_ZERO.getChainId(); + LAYER_ZERO = ILayerZeroEndpointV2(layer_zero); + chainId = LAYER_ZERO.eid(); } function estimateAdditionalCost() external view returns(address asset, uint256 amount) { @@ -65,24 +63,21 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, } function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfsendPacketInNativeToken) { + + costOfsendPacketInNativeToken = 0; // TODO + // Handoff package to LZ. - uint16 _dstChainId = uint16(uint256(destinationChainIdentifier)); - // From LZ docs: "the address on destination chain (in bytes). address length/format may vary by chains" - // bytes memory _destination = destinationImplementation; - // bytes memory _payload = message; - address payable _refundAddress = payable(0); // TODO: What here? - address _zroPaymentAddress = address(0); // TODO: What here? - bytes memory _adapterParams = hex""; - - LAYER_ZERO.send( - _dstChainId, - destinationImplementation, - message, - _refundAddress, - _zroPaymentAddress, - _adapterParams + LAYER_ZERO.send{value: costOfsendPacketInNativeToken}( + MessagingParams({ + dstEid: uint32(uint256(destinationChainIdentifier)), + receiver: bytes32(destinationImplementation), + message: message, + options: hex"", + payInLzToken: false + }), + msg.sender // TODO: ); - return costOfsendPacketInNativeToken = uint128(0); + return costOfsendPacketInNativeToken; } } \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/ILayerZeroEndpoint.sol b/src/apps/layerzero/interfaces/ILayerZeroEndpoint.sol deleted file mode 100644 index d9280ed..0000000 --- a/src/apps/layerzero/interfaces/ILayerZeroEndpoint.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity >=0.5.0; - -import "./ILayerZeroUserApplicationConfig.sol"; - -interface ILayerZeroEndpoint is ILayerZeroUserApplicationConfig { - // @notice send a LayerZero message to the specified address at a LayerZero endpoint. - // @param _dstChainId - the destination chain identifier - // @param _destination - the address on destination chain (in bytes). address length/format may vary by chains - // @param _payload - a custom bytes payload to send to the destination contract - // @param _refundAddress - if the source transaction is cheaper than the amount of value passed, refund the additional amount to this address - // @param _zroPaymentAddress - the address of the ZRO token holder who would pay for the transaction - // @param _adapterParams - parameters for custom functionality. e.g. receive airdropped native gas from the relayer on destination - function send(uint16 _dstChainId, bytes calldata _destination, bytes calldata _payload, address payable _refundAddress, address _zroPaymentAddress, bytes calldata _adapterParams) external payable; - - // @notice used by the messaging library to publish verified payload - // @param _srcChainId - the source chain identifier - // @param _srcAddress - the source contract (as bytes) at the source chain - // @param _dstAddress - the address on destination chain - // @param _nonce - the unbound message ordering nonce - // @param _gasLimit - the gas limit for external contract execution - // @param _payload - verified payload to send to the destination contract - function receivePayload(uint16 _srcChainId, bytes calldata _srcAddress, address _dstAddress, uint64 _nonce, uint _gasLimit, bytes calldata _payload) external; - - // @notice get the inboundNonce of a receiver from a source chain which could be EVM or non-EVM chain - // @param _srcChainId - the source chain identifier - // @param _srcAddress - the source chain contract address - function getInboundNonce(uint16 _srcChainId, bytes calldata _srcAddress) external view returns (uint64); - - // @notice get the outboundNonce from this source chain which, consequently, is always an EVM - // @param _srcAddress - the source chain contract address - function getOutboundNonce(uint16 _dstChainId, address _srcAddress) external view returns (uint64); - - // @notice gets a quote in source native gas, for the amount that send() requires to pay for message delivery - // @param _dstChainId - the destination chain identifier - // @param _userApplication - the user app address on this EVM chain - // @param _payload - the custom message to send over LayerZero - // @param _payInZRO - if false, user app pays the protocol fee in native token - // @param _adapterParam - parameters for the adapter service, e.g. send some dust native token to dstChain - function estimateFees(uint16 _dstChainId, address _userApplication, bytes calldata _payload, bool _payInZRO, bytes calldata _adapterParam) external view returns (uint nativeFee, uint zroFee); - - // @notice get this Endpoint's immutable source identifier - function getChainId() external view returns (uint16); - - // @notice the interface to retry failed message on this Endpoint destination - // @param _srcChainId - the source chain identifier - // @param _srcAddress - the source chain contract address - // @param _payload - the payload to be retried - function retryPayload(uint16 _srcChainId, bytes calldata _srcAddress, bytes calldata _payload) external; - - // @notice query if any STORED payload (message blocking) at the endpoint. - // @param _srcChainId - the source chain identifier - // @param _srcAddress - the source chain contract address - function hasStoredPayload(uint16 _srcChainId, bytes calldata _srcAddress) external view returns (bool); - - // @notice query if the _libraryAddress is valid for sending msgs. - // @param _userApplication - the user app address on this EVM chain - function getSendLibraryAddress(address _userApplication) external view returns (address); - - // @notice query if the _libraryAddress is valid for receiving msgs. - // @param _userApplication - the user app address on this EVM chain - function getReceiveLibraryAddress(address _userApplication) external view returns (address); - - // @notice query if the non-reentrancy guard for send() is on - // @return true if the guard is on. false otherwise - function isSendingPayload() external view returns (bool); - - // @notice query if the non-reentrancy guard for receive() is on - // @return true if the guard is on. false otherwise - function isReceivingPayload() external view returns (bool); - - // @notice get the configuration of the LayerZero messaging library of the specified version - // @param _version - messaging library version - // @param _chainId - the chainId for the pending config change - // @param _userApplication - the contract address of the user application - // @param _configType - type of configuration. every messaging library has its own convention. - function getConfig(uint16 _version, uint16 _chainId, address _userApplication, uint _configType) external view returns (bytes memory); - - // @notice get the send() LayerZero messaging library version - // @param _userApplication - the contract address of the user application - function getSendVersion(address _userApplication) external view returns (uint16); - - // @notice get the lzReceive() LayerZero messaging library version - // @param _userApplication - the contract address of the user application - function getReceiveVersion(address _userApplication) external view returns (uint16); -} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/ILayerZeroEndpointV2.sol b/src/apps/layerzero/interfaces/ILayerZeroEndpointV2.sol new file mode 100644 index 0000000..3cb76ad --- /dev/null +++ b/src/apps/layerzero/interfaces/ILayerZeroEndpointV2.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +import { IMessageLibManager } from "./IMessageLibManager.sol"; +import { IMessagingComposer } from "./IMessagingComposer.sol"; +import { IMessagingChannel } from "./IMessagingChannel.sol"; +import { IMessagingContext } from "./IMessagingContext.sol"; + +struct MessagingParams { + uint32 dstEid; + bytes32 receiver; + bytes message; + bytes options; + bool payInLzToken; +} + +struct MessagingReceipt { + bytes32 guid; + uint64 nonce; + MessagingFee fee; +} + +struct MessagingFee { + uint256 nativeFee; + uint256 lzTokenFee; +} + +struct Origin { + uint32 srcEid; + bytes32 sender; + uint64 nonce; +} + +interface ILayerZeroEndpointV2 is IMessageLibManager, IMessagingComposer, IMessagingChannel, IMessagingContext { + event PacketSent(bytes encodedPayload, bytes options, address sendLibrary); + + event PacketVerified(Origin origin, address receiver, bytes32 payloadHash); + + event PacketDelivered(Origin origin, address receiver); + + event LzReceiveAlert( + address indexed receiver, + address indexed executor, + Origin origin, + bytes32 guid, + uint256 gas, + uint256 value, + bytes message, + bytes extraData, + bytes reason + ); + + event LzTokenSet(address token); + + event DelegateSet(address sender, address delegate); + + function quote(MessagingParams calldata _params, address _sender) external view returns (MessagingFee memory); + + function send( + MessagingParams calldata _params, + address _refundAddress + ) external payable returns (MessagingReceipt memory); + + function verify(Origin calldata _origin, address _receiver, bytes32 _payloadHash) external; + + function verifiable(Origin calldata _origin, address _receiver) external view returns (bool); + + function initializable(Origin calldata _origin, address _receiver) external view returns (bool); + + function lzReceive( + Origin calldata _origin, + address _receiver, + bytes32 _guid, + bytes calldata _message, + bytes calldata _extraData + ) external payable; + + // oapp can burn messages partially by calling this function with its own business logic if messages are verified in order + function clear(address _oapp, Origin calldata _origin, bytes32 _guid, bytes calldata _message) external; + + function setLzToken(address _lzToken) external; + + function lzToken() external view returns (address); + + function nativeToken() external view returns (address); + + function setDelegate(address _delegate) external; +} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/ILayerZeroUserApplicationConfig.sol b/src/apps/layerzero/interfaces/ILayerZeroUserApplicationConfig.sol deleted file mode 100644 index 8197ebf..0000000 --- a/src/apps/layerzero/interfaces/ILayerZeroUserApplicationConfig.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity >=0.5.0; - -interface ILayerZeroUserApplicationConfig { - // @notice set the configuration of the LayerZero messaging library of the specified version - // @param _version - messaging library version - // @param _chainId - the chainId for the pending config change - // @param _configType - type of configuration. every messaging library has its own convention. - // @param _config - configuration in the bytes. can encode arbitrary content. - function setConfig(uint16 _version, uint16 _chainId, uint _configType, bytes calldata _config) external; - - // @notice set the send() LayerZero messaging library version to _version - // @param _version - new messaging library version - function setSendVersion(uint16 _version) external; - - // @notice set the lzReceive() LayerZero messaging library version to _version - // @param _version - new messaging library version - function setReceiveVersion(uint16 _version) external; - - // @notice Only when the UA needs to resume the message flow in blocking mode and clear the stored payload - // @param _srcChainId - the chainId of the source chain - // @param _srcAddress - the contract address of the source contract at the source chain - function forceResumeReceive(uint16 _srcChainId, bytes calldata _srcAddress) external; -} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/ILayerZeroValidationLibrary.sol b/src/apps/layerzero/interfaces/ILayerZeroValidationLibrary.sol deleted file mode 100644 index f9435e0..0000000 --- a/src/apps/layerzero/interfaces/ILayerZeroValidationLibrary.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity >=0.8.0; - -import "../utility/LayerZeroPacket.sol"; - -interface ILayerZeroValidationLibrary { - function validateProof(bytes32 blockData, bytes calldata _data, uint _remoteAddressSize) external returns (LayerZeroPacket.Packet memory packet); -} diff --git a/src/apps/layerzero/interfaces/IMessageLibManager.sol b/src/apps/layerzero/interfaces/IMessageLibManager.sol new file mode 100644 index 0000000..9302b69 --- /dev/null +++ b/src/apps/layerzero/interfaces/IMessageLibManager.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +struct SetConfigParam { + uint32 eid; + uint32 configType; + bytes config; +} + +interface IMessageLibManager { + struct Timeout { + address lib; + uint256 expiry; + } + + event LibraryRegistered(address newLib); + event DefaultSendLibrarySet(uint32 eid, address newLib); + event DefaultReceiveLibrarySet(uint32 eid, address newLib); + event DefaultReceiveLibraryTimeoutSet(uint32 eid, address oldLib, uint256 expiry); + event SendLibrarySet(address sender, uint32 eid, address newLib); + event ReceiveLibrarySet(address receiver, uint32 eid, address newLib); + event ReceiveLibraryTimeoutSet(address receiver, uint32 eid, address oldLib, uint256 timeout); + + function registerLibrary(address _lib) external; + + function isRegisteredLibrary(address _lib) external view returns (bool); + + function getRegisteredLibraries() external view returns (address[] memory); + + function setDefaultSendLibrary(uint32 _eid, address _newLib) external; + + function defaultSendLibrary(uint32 _eid) external view returns (address); + + function setDefaultReceiveLibrary(uint32 _eid, address _newLib, uint256 _timeout) external; + + function defaultReceiveLibrary(uint32 _eid) external view returns (address); + + function setDefaultReceiveLibraryTimeout(uint32 _eid, address _lib, uint256 _expiry) external; + + function defaultReceiveLibraryTimeout(uint32 _eid) external view returns (address lib, uint256 expiry); + + function isSupportedEid(uint32 _eid) external view returns (bool); + + function isValidReceiveLibrary(address _receiver, uint32 _eid, address _lib) external view returns (bool); + + /// ------------------- OApp interfaces ------------------- + function setSendLibrary(address _oapp, uint32 _eid, address _newLib) external; + + function getSendLibrary(address _sender, uint32 _eid) external view returns (address lib); + + function isDefaultSendLibrary(address _sender, uint32 _eid) external view returns (bool); + + function setReceiveLibrary(address _oapp, uint32 _eid, address _newLib, uint256 _gracePeriod) external; + + function getReceiveLibrary(address _receiver, uint32 _eid) external view returns (address lib, bool isDefault); + + function setReceiveLibraryTimeout(address _oapp, uint32 _eid, address _lib, uint256 _gracePeriod) external; + + function receiveLibraryTimeout(address _receiver, uint32 _eid) external view returns (address lib, uint256 expiry); + + function setConfig(address _oapp, address _lib, SetConfigParam[] calldata _params) external; + + function getConfig( + address _oapp, + address _lib, + uint32 _eid, + uint32 _configType + ) external view returns (bytes memory config); +} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IMessagingChannel.sol b/src/apps/layerzero/interfaces/IMessagingChannel.sol new file mode 100644 index 0000000..93cff27 --- /dev/null +++ b/src/apps/layerzero/interfaces/IMessagingChannel.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +interface IMessagingChannel { + event InboundNonceSkipped(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce); + event PacketNilified(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce, bytes32 payloadHash); + event PacketBurnt(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce, bytes32 payloadHash); + + function eid() external view returns (uint32); + + // this is an emergency function if a message cannot be verified for some reasons + // required to provide _nextNonce to avoid race condition + function skip(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce) external; + + function nilify(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external; + + function burn(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external; + + function nextGuid(address _sender, uint32 _dstEid, bytes32 _receiver) external view returns (bytes32); + + function inboundNonce(address _receiver, uint32 _srcEid, bytes32 _sender) external view returns (uint64); + + function outboundNonce(address _sender, uint32 _dstEid, bytes32 _receiver) external view returns (uint64); + + function inboundPayloadHash( + address _receiver, + uint32 _srcEid, + bytes32 _sender, + uint64 _nonce + ) external view returns (bytes32); + + function lazyInboundNonce(address _receiver, uint32 _srcEid, bytes32 _sender) external view returns (uint64); +} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IMessagingComposer.sol b/src/apps/layerzero/interfaces/IMessagingComposer.sol new file mode 100644 index 0000000..5918bd4 --- /dev/null +++ b/src/apps/layerzero/interfaces/IMessagingComposer.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +interface IMessagingComposer { + event ComposeSent(address from, address to, bytes32 guid, uint16 index, bytes message); + event ComposeDelivered(address from, address to, bytes32 guid, uint16 index); + event LzComposeAlert( + address indexed from, + address indexed to, + address indexed executor, + bytes32 guid, + uint16 index, + uint256 gas, + uint256 value, + bytes message, + bytes extraData, + bytes reason + ); + + function composeQueue( + address _from, + address _to, + bytes32 _guid, + uint16 _index + ) external view returns (bytes32 messageHash); + + function sendCompose(address _to, bytes32 _guid, uint16 _index, bytes calldata _message) external; + + function lzCompose( + address _from, + address _to, + bytes32 _guid, + uint16 _index, + bytes calldata _message, + bytes calldata _extraData + ) external payable; +} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IMessagingContext.sol b/src/apps/layerzero/interfaces/IMessagingContext.sol new file mode 100644 index 0000000..a7aaac9 --- /dev/null +++ b/src/apps/layerzero/interfaces/IMessagingContext.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +interface IMessagingContext { + function isSendingMessage() external view returns (bool); + + function getSendContext() external view returns (uint32 dstEid, address sender); +} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IValidationLibraryHelperV2.sol b/src/apps/layerzero/interfaces/IValidationLibraryHelperV2.sol deleted file mode 100644 index ce2ed18..0000000 --- a/src/apps/layerzero/interfaces/IValidationLibraryHelperV2.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity >=0.8.0; - -import "../utility/LayerZeroPacket.sol"; - -interface IValidationLibraryHelperV2 { - struct ULNLog { - bytes32 contractAddress; - bytes32 topicZeroSig; - bytes data; - } - - function getVerifyLog(bytes32 hashRoot, uint[] calldata receiptSlotIndex, uint logIndex, bytes[] calldata proof) external pure returns (ULNLog memory); - - function getPacket(bytes calldata data, uint sizeOfSrcAddress, bytes32 ulnAddress) external pure returns (LayerZeroPacket.Packet memory); - - function getUtilsVersion() external view returns (uint8); - - function getProofType() external view returns (uint8); -} diff --git a/src/apps/layerzero/utility/Buffer.sol b/src/apps/layerzero/utility/Buffer.sol deleted file mode 100644 index a2b9fa9..0000000 --- a/src/apps/layerzero/utility/Buffer.sol +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -// https://github.com/ensdomains/buffer - -// TODO: CHECK IF WE CAN UPGRADE -pragma solidity ^0.8.13; -// pragma solidity ^0.7.0; - -/** - * @dev A library for working with mutable byte buffers in Solidity. - * - * Byte buffers are mutable and expandable, and provide a variety of primitives - * for writing to them. At any time you can fetch a bytes object containing the - * current contents of the buffer. The bytes object should not be stored between - * operations, as it may change due to resizing of the buffer. - */ -library Buffer { - /** - * @dev Represents a mutable buffer. Buffers have a current value (buf) and - * a capacity. The capacity may be longer than the current value, in - * which case it can be extended without the need to allocate more memory. - */ - struct buffer { - bytes buf; - uint capacity; - } - - /** - * @dev Initializes a buffer with an initial capacity. - * @param buf The buffer to initialize. - * @param capacity The number of bytes of space to allocate the buffer. - * @return The buffer, for chaining. - */ - function init(buffer memory buf, uint capacity) internal pure returns (buffer memory) { - if (capacity % 32 != 0) { - capacity += 32 - (capacity % 32); - } - // Allocate space for the buffer data - buf.capacity = capacity; - assembly { - let ptr := mload(0x40) - mstore(buf, ptr) - mstore(ptr, 0) - let fpm := add(32, add(ptr, capacity)) - if lt(fpm, ptr) { - revert(0, 0) - } - mstore(0x40, fpm) - } - return buf; - } - - - /** - * @dev Writes a byte string to a buffer. Resizes if doing so would exceed - * the capacity of the buffer. - * @param buf The buffer to append to. - * @param off The start offset to write to. - * @param rawData The data to append. - * @param len The number of bytes to copy. - * @return The original buffer, for chaining. - */ - function writeRawBytes( - buffer memory buf, - uint off, - bytes memory rawData, - uint offData, - uint len - ) internal pure returns (buffer memory) { - if (off + len > buf.capacity) { - resize(buf, max(buf.capacity, len + off) * 2); - } - - uint dest; - uint src; - assembly { - // Memory address of the buffer data - let bufptr := mload(buf) - // Length of existing buffer data - let buflen := mload(bufptr) - // Start address = buffer address + offset + sizeof(buffer length) - dest := add(add(bufptr, 32), off) - // Update buffer length if we're extending it - if gt(add(len, off), buflen) { - mstore(bufptr, add(len, off)) - } - src := add(rawData, offData) - } - - // Copy word-length chunks while possible - for (; len >= 32; len -= 32) { - assembly { - mstore(dest, mload(src)) - } - dest += 32; - src += 32; - } - - // Copy remaining bytes - unchecked { - uint mask = (256 ** (32 - len)) - 1; - assembly { - let srcpart := and(mload(src), not(mask)) - let destpart := and(mload(dest), mask) - mstore(dest, or(destpart, srcpart)) - } - } - - return buf; - } - - /** - * @dev Writes a byte string to a buffer. Resizes if doing so would exceed - * the capacity of the buffer. - * @param buf The buffer to append to. - * @param off The start offset to write to. - * @param data The data to append. - * @param len The number of bytes to copy. - * @return The original buffer, for chaining. - */ - function write(buffer memory buf, uint off, bytes memory data, uint len) internal pure returns (buffer memory) { - require(len <= data.length); - - if (off + len > buf.capacity) { - resize(buf, max(buf.capacity, len + off) * 2); - } - - uint dest; - uint src; - assembly { - // Memory address of the buffer data - let bufptr := mload(buf) - // Length of existing buffer data - let buflen := mload(bufptr) - // Start address = buffer address + offset + sizeof(buffer length) - dest := add(add(bufptr, 32), off) - // Update buffer length if we're extending it - if gt(add(len, off), buflen) { - mstore(bufptr, add(len, off)) - } - src := add(data, 32) - } - - // Copy word-length chunks while possible - for (; len >= 32; len -= 32) { - assembly { - mstore(dest, mload(src)) - } - dest += 32; - src += 32; - } - - // Copy remaining bytes - uint mask = 256**(32 - len) - 1; - assembly { - let srcpart := and(mload(src), not(mask)) - let destpart := and(mload(dest), mask) - mstore(dest, or(destpart, srcpart)) - } - - return buf; - } - - function append(buffer memory buf, bytes memory data) internal pure returns (buffer memory) { - return write(buf, buf.buf.length, data, data.length); - } - - function resize(buffer memory buf, uint capacity) private pure { - bytes memory oldbuf = buf.buf; - init(buf, capacity); - append(buf, oldbuf); - } - - function max(uint a, uint b) private pure returns (uint) { - if (a > b) { - return a; - } - return b; - } -} diff --git a/src/apps/layerzero/utility/BufferOrg.sol b/src/apps/layerzero/utility/BufferOrg.sol deleted file mode 100644 index 3314a65..0000000 --- a/src/apps/layerzero/utility/BufferOrg.sol +++ /dev/null @@ -1,260 +0,0 @@ -// SPDX-License-Identifier: BSD-2-Clause -pragma solidity ^0.8.4; - -/** -* @dev A library for working with mutable byte buffers in Solidity. -* -* Byte buffers are mutable and expandable, and provide a variety of primitives -* for appending to them. At any time you can fetch a bytes object containing the -* current contents of the buffer. The bytes object should not be stored between -* operations, as it may change due to resizing of the buffer. -*/ -library Buffer { - /** - * @dev Represents a mutable buffer. Buffers have a current value (buf) and - * a capacity. The capacity may be longer than the current value, in - * which case it can be extended without the need to allocate more memory. - */ - struct buffer { - bytes buf; - uint capacity; - } - - /** - * @dev Initializes a buffer with an initial capacity. - * @param buf The buffer to initialize. - * @param capacity The number of bytes of space to allocate the buffer. - * @return The buffer, for chaining. - */ - function init(buffer memory buf, uint capacity) internal pure returns (buffer memory) { - if (capacity % 32 != 0) { - capacity += 32 - (capacity % 32); - } - // Allocate space for the buffer data - buf.capacity = capacity; - assembly { - let ptr := mload(0x40) - mstore(buf, ptr) - mstore(ptr, 0) - let fpm := add(32, add(ptr, capacity)) - if lt(fpm, ptr) { - revert(0, 0) - } - mstore(0x40, fpm) - } - return buf; - } - - /** - * @dev Initializes a new buffer from an existing bytes object. - * Changes to the buffer may mutate the original value. - * @param b The bytes object to initialize the buffer with. - * @return A new buffer. - */ - function fromBytes(bytes memory b) internal pure returns(buffer memory) { - buffer memory buf; - buf.buf = b; - buf.capacity = b.length; - return buf; - } - - function resize(buffer memory buf, uint capacity) private pure { - bytes memory oldbuf = buf.buf; - init(buf, capacity); - append(buf, oldbuf); - } - - /** - * @dev Sets buffer length to 0. - * @param buf The buffer to truncate. - * @return The original buffer, for chaining.. - */ - function truncate(buffer memory buf) internal pure returns (buffer memory) { - assembly { - let bufptr := mload(buf) - mstore(bufptr, 0) - } - return buf; - } - - /** - * @dev Appends len bytes of a byte string to a buffer. Resizes if doing so would exceed - * the capacity of the buffer. - * @param buf The buffer to append to. - * @param data The data to append. - * @param len The number of bytes to copy. - * @return The original buffer, for chaining. - */ - function append(buffer memory buf, bytes memory data, uint len) internal pure returns(buffer memory) { - require(len <= data.length); - - uint off = buf.buf.length; - uint newCapacity = off + len; - if (newCapacity > buf.capacity) { - resize(buf, newCapacity * 2); - } - - uint dest; - uint src; - assembly { - // Memory address of the buffer data - let bufptr := mload(buf) - // Length of existing buffer data - let buflen := mload(bufptr) - // Start address = buffer address + offset + sizeof(buffer length) - dest := add(add(bufptr, 32), off) - // Update buffer length if we're extending it - if gt(newCapacity, buflen) { - mstore(bufptr, newCapacity) - } - src := add(data, 32) - } - - // Copy word-length chunks while possible - for (; len >= 32; len -= 32) { - assembly { - mstore(dest, mload(src)) - } - dest += 32; - src += 32; - } - - // Copy remaining bytes - unchecked { - uint mask = (256 ** (32 - len)) - 1; - assembly { - let srcpart := and(mload(src), not(mask)) - let destpart := and(mload(dest), mask) - mstore(dest, or(destpart, srcpart)) - } - } - - return buf; - } - - /** - * @dev Appends a byte string to a buffer. Resizes if doing so would exceed - * the capacity of the buffer. - * @param buf The buffer to append to. - * @param data The data to append. - * @return The original buffer, for chaining. - */ - function append(buffer memory buf, bytes memory data) internal pure returns (buffer memory) { - return append(buf, data, data.length); - } - - /** - * @dev Appends a byte to the buffer. Resizes if doing so would exceed the - * capacity of the buffer. - * @param buf The buffer to append to. - * @param data The data to append. - * @return The original buffer, for chaining. - */ - function appendUint8(buffer memory buf, uint8 data) internal pure returns(buffer memory) { - uint off = buf.buf.length; - uint offPlusOne = off + 1; - if (off >= buf.capacity) { - resize(buf, offPlusOne * 2); - } - - assembly { - // Memory address of the buffer data - let bufptr := mload(buf) - // Address = buffer address + sizeof(buffer length) + off - let dest := add(add(bufptr, off), 32) - mstore8(dest, data) - // Update buffer length if we extended it - if gt(offPlusOne, mload(bufptr)) { - mstore(bufptr, offPlusOne) - } - } - - return buf; - } - - /** - * @dev Appends len bytes of bytes32 to a buffer. Resizes if doing so would - * exceed the capacity of the buffer. - * @param buf The buffer to append to. - * @param data The data to append. - * @param len The number of bytes to write (left-aligned). - * @return The original buffer, for chaining. - */ - function append(buffer memory buf, bytes32 data, uint len) private pure returns(buffer memory) { - uint off = buf.buf.length; - uint newCapacity = len + off; - if (newCapacity > buf.capacity) { - resize(buf, newCapacity * 2); - } - - unchecked { - uint mask = (256 ** len) - 1; - // Right-align data - data = data >> (8 * (32 - len)); - assembly { - // Memory address of the buffer data - let bufptr := mload(buf) - // Address = buffer address + sizeof(buffer length) + newCapacity - let dest := add(bufptr, newCapacity) - mstore(dest, or(and(mload(dest), not(mask)), data)) - // Update buffer length if we extended it - if gt(newCapacity, mload(bufptr)) { - mstore(bufptr, newCapacity) - } - } - } - return buf; - } - - /** - * @dev Appends a bytes20 to the buffer. Resizes if doing so would exceed - * the capacity of the buffer. - * @param buf The buffer to append to. - * @param data The data to append. - * @return The original buffer, for chhaining. - */ - function appendBytes20(buffer memory buf, bytes20 data) internal pure returns (buffer memory) { - return append(buf, bytes32(data), 20); - } - - /** - * @dev Appends a bytes32 to the buffer. Resizes if doing so would exceed - * the capacity of the buffer. - * @param buf The buffer to append to. - * @param data The data to append. - * @return The original buffer, for chaining. - */ - function appendBytes32(buffer memory buf, bytes32 data) internal pure returns (buffer memory) { - return append(buf, data, 32); - } - - /** - * @dev Appends a byte to the end of the buffer. Resizes if doing so would - * exceed the capacity of the buffer. - * @param buf The buffer to append to. - * @param data The data to append. - * @param len The number of bytes to write (right-aligned). - * @return The original buffer. - */ - function appendInt(buffer memory buf, uint data, uint len) internal pure returns(buffer memory) { - uint off = buf.buf.length; - uint newCapacity = len + off; - if (newCapacity > buf.capacity) { - resize(buf, newCapacity * 2); - } - - uint mask = (256 ** len) - 1; - assembly { - // Memory address of the buffer data - let bufptr := mload(buf) - // Address = buffer address + sizeof(buffer length) + newCapacity - let dest := add(bufptr, newCapacity) - mstore(dest, or(and(mload(dest), not(mask)), data)) - // Update buffer length if we extended it - if gt(newCapacity, mload(bufptr)) { - mstore(bufptr, newCapacity) - } - } - return buf; - } -} \ No newline at end of file diff --git a/src/apps/layerzero/utility/LayerZeroPacket.sol b/src/apps/layerzero/utility/LayerZeroPacket.sol deleted file mode 100644 index e42f965..0000000 --- a/src/apps/layerzero/utility/LayerZeroPacket.sol +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -// TODO: CHECK IF WE CAN UPGRADE -pragma solidity ^0.8.13; -// pragma solidity ^0.7.0; - -import "./Buffer.sol"; - -library LayerZeroPacket { - using Buffer for Buffer.buffer; - - struct Packet { - uint16 srcChainId; - uint16 dstChainId; - uint64 nonce; - address dstAddress; - bytes srcAddress; - bytes32 ulnAddress; - bytes payload; - } - - function getPacket( - bytes memory data, - uint16 srcChain, - uint sizeOfSrcAddress, - bytes32 ulnAddress - ) internal pure returns (LayerZeroPacket.Packet memory) { - uint16 dstChainId; - address dstAddress; - uint size; - uint64 nonce; - - // The log consists of the destination chain id and then a bytes payload - // 0--------------------------------------------31 - // 0 | total bytes size - // 32 | destination chain id - // 64 | bytes offset - // 96 | bytes array size - // 128 | payload - assembly { - dstChainId := mload(add(data, 32)) - size := mload(add(data, 96)) /// size of the byte array - nonce := mload(add(data, 104)) // offset to convert to uint64 128 is index -24 - dstAddress := mload(add(data, sub(add(128, sizeOfSrcAddress), 4))) // offset to convert to address 12 -8 - } - - Buffer.buffer memory srcAddressBuffer; - srcAddressBuffer.init(sizeOfSrcAddress); - srcAddressBuffer.writeRawBytes(0, data, 136, sizeOfSrcAddress); // 128 + 8 - - uint payloadSize = (size - 28) - sizeOfSrcAddress; - Buffer.buffer memory payloadBuffer; - payloadBuffer.init(payloadSize); - payloadBuffer.writeRawBytes(0, data, sizeOfSrcAddress + 156, payloadSize); // 148 + 8 - return LayerZeroPacket.Packet(srcChain, dstChainId, nonce, dstAddress, srcAddressBuffer.buf, ulnAddress, payloadBuffer.buf); - } - - function getPacketV2( - bytes memory data, - uint sizeOfSrcAddress, - bytes32 ulnAddress - ) internal pure returns (LayerZeroPacket.Packet memory) { - // packet def: abi.encodePacked(nonce, srcChain, srcAddress, dstChain, dstAddress, payload); - // data def: abi.encode(packet) = offset(32) + length(32) + packet - // if from EVM - // 0 - 31 0 - 31 | total bytes size - // 32 - 63 32 - 63 | location - // 64 - 95 64 - 95 | size of the packet - // 96 - 103 96 - 103 | nonce - // 104 - 105 104 - 105 | srcChainId - // 106 - P 106 - 125 | srcAddress, where P = 106 + sizeOfSrcAddress - 1, - // P+1 - P+2 126 - 127 | dstChainId - // P+3 - P+22 128 - 147 | dstAddress - // P+23 - END 148 - END | payload - - // decode the packet - uint256 realSize; - uint64 nonce; - uint16 srcChain; - uint16 dstChain; - address dstAddress; - assembly { - realSize := mload(add(data, 64)) - nonce := mload(add(data, 72)) // 104 - 32 - srcChain := mload(add(data, 74)) // 106 - 32 - dstChain := mload(add(data, add(76, sizeOfSrcAddress))) // P + 3 - 32 = 105 + size + 3 - 32 = 76 + size - dstAddress := mload(add(data, add(96, sizeOfSrcAddress))) // P + 23 - 32 = 105 + size + 23 - 32 = 96 + size - } - - require(srcChain != 0, "LayerZeroPacket: invalid packet"); - - Buffer.buffer memory srcAddressBuffer; - srcAddressBuffer.init(sizeOfSrcAddress); - srcAddressBuffer.writeRawBytes(0, data, 106, sizeOfSrcAddress); - - uint nonPayloadSize = sizeOfSrcAddress + 32;// 2 + 2 + 8 + 20, 32 + 20 = 52 if sizeOfSrcAddress == 20 - uint payloadSize = realSize - nonPayloadSize; - Buffer.buffer memory payloadBuffer; - payloadBuffer.init(payloadSize); - payloadBuffer.writeRawBytes(0, data, nonPayloadSize + 96, payloadSize); - - return LayerZeroPacket.Packet(srcChain, dstChain, nonce, dstAddress, srcAddressBuffer.buf, ulnAddress, payloadBuffer.buf); - } - - function getPacketV3( - bytes memory data, - uint sizeOfSrcAddress, - bytes32 ulnAddress - ) internal pure returns (LayerZeroPacket.Packet memory) { - // data def: abi.encodePacked(nonce, srcChain, srcAddress, dstChain, dstAddress, payload); - // if from EVM - // 0 - 31 0 - 31 | total bytes size - // 32 - 39 32 - 39 | nonce - // 40 - 41 40 - 41 | srcChainId - // 42 - P 42 - 61 | srcAddress, where P = 41 + sizeOfSrcAddress, - // P+1 - P+2 62 - 63 | dstChainId - // P+3 - P+22 64 - 83 | dstAddress - // P+23 - END 84 - END | payload - - // decode the packet - uint256 realSize = data.length; - uint nonPayloadSize = sizeOfSrcAddress + 32;// 2 + 2 + 8 + 20, 32 + 20 = 52 if sizeOfSrcAddress == 20 - require(realSize >= nonPayloadSize, "LayerZeroPacket: invalid packet"); - uint payloadSize = realSize - nonPayloadSize; - - uint64 nonce; - uint16 srcChain; - uint16 dstChain; - address dstAddress; - assembly { - nonce := mload(add(data, 8)) // 40 - 32 - srcChain := mload(add(data, 10)) // 42 - 32 - dstChain := mload(add(data, add(12, sizeOfSrcAddress))) // P + 3 - 32 = 41 + size + 3 - 32 = 12 + size - dstAddress := mload(add(data, add(32, sizeOfSrcAddress))) // P + 23 - 32 = 41 + size + 23 - 32 = 32 + size - } - - require(srcChain != 0, "LayerZeroPacket: invalid packet"); - - Buffer.buffer memory srcAddressBuffer; - srcAddressBuffer.init(sizeOfSrcAddress); - srcAddressBuffer.writeRawBytes(0, data, 42, sizeOfSrcAddress); - - Buffer.buffer memory payloadBuffer; - if (payloadSize > 0) { - payloadBuffer.init(payloadSize); - payloadBuffer.writeRawBytes(0, data, nonPayloadSize + 32, payloadSize); - } - - return LayerZeroPacket.Packet(srcChain, dstChain, nonce, dstAddress, srcAddressBuffer.buf, ulnAddress, payloadBuffer.buf); - } -} diff --git a/src/apps/layerzero/utility/RLPDecode.sol b/src/apps/layerzero/utility/RLPDecode.sol deleted file mode 100644 index bd6324b..0000000 --- a/src/apps/layerzero/utility/RLPDecode.sol +++ /dev/null @@ -1,389 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -// https://github.com/hamdiallam/solidity-rlp - -// TODO: CHECK IF WE CAN UPGRADE -// pragma solidity ^0.7.0; -pragma solidity ^0.8.13; - -library RLPDecode { - uint8 constant STRING_SHORT_START = 0x80; - uint8 constant STRING_LONG_START = 0xb8; - uint8 constant LIST_SHORT_START = 0xc0; - uint8 constant LIST_LONG_START = 0xf8; - uint8 constant WORD_SIZE = 32; - - struct RLPItem { - uint len; - uint memPtr; - } - - struct Iterator { - RLPItem item; // Item that's being iterated over. - uint nextPtr; // Position of the next item in the list. - } - - /* - * @dev Returns the next element in the iteration. Reverts if it has not next element. - * @param self The iterator. - * @return The next element in the iteration. - */ - function next(Iterator memory self) internal pure returns (RLPItem memory) { - require(hasNext(self), "RLPDecoder iterator has no next"); - - uint ptr = self.nextPtr; - uint itemLength = _itemLength(ptr); - self.nextPtr = ptr + itemLength; - - return RLPItem(itemLength, ptr); - } - - /* - * @dev Returns true if the iteration has more elements. - * @param self The iterator. - * @return true if the iteration has more elements. - */ - function hasNext(Iterator memory self) internal pure returns (bool) { - RLPItem memory item = self.item; - return self.nextPtr < item.memPtr + item.len; - } - - /* - * @param item RLP encoded bytes - */ - - function toRlpItem(bytes memory item) internal pure returns (RLPItem memory) { - uint memPtr; - assembly { - memPtr := add(item, 0x20) - } - // offset the pointer if the first byte - - uint8 byte0; - assembly { - byte0 := byte(0, mload(memPtr)) - } - uint len = item.length; - if (len > 0 && byte0 < LIST_SHORT_START) { - assembly { - memPtr := add(memPtr, 0x01) - } - len -= 1; - } - return RLPItem(len, memPtr); - } - - /* - * @dev Create an iterator. Reverts if item is not a list. - * @param self The RLP item. - * @return An 'Iterator' over the item. - */ - function iterator(RLPItem memory self) internal pure returns (Iterator memory) { - require(isList(self), "RLPDecoder iterator is not list"); - - uint ptr = self.memPtr + _payloadOffset(self.memPtr); - return Iterator(self, ptr); - } - - /* - * @param item RLP encoded bytes - */ - function rlpLen(RLPItem memory item) internal pure returns (uint) { - return item.len; - } - - /* - * @param item RLP encoded bytes - */ - function payloadLen(RLPItem memory item) internal pure returns (uint) { - uint offset = _payloadOffset(item.memPtr); - require(item.len >= offset, "RLPDecoder: invalid uint RLP item offset size"); - return item.len - offset; - } - - /* - * @param item RLP encoded list in bytes - */ - function toList(RLPItem memory item) internal pure returns (RLPItem[] memory) { - require(isList(item), "RLPDecoder iterator is not a list"); - - uint items = numItems(item); - RLPItem[] memory result = new RLPItem[](items); - - uint memPtr = item.memPtr + _payloadOffset(item.memPtr); - uint dataLen; - for (uint i = 0; i < items; i++) { - dataLen = _itemLength(memPtr); - result[i] = RLPItem(dataLen, memPtr); - memPtr = memPtr + dataLen; - } - - return result; - } - - /* - * @param get the RLP item by index. save gas. - */ - function getItemByIndex(RLPItem memory item, uint idx) internal pure returns (RLPItem memory) { - require(isList(item), "RLPDecoder iterator is not a list"); - - uint memPtr = item.memPtr + _payloadOffset(item.memPtr); - uint dataLen; - for (uint i = 0; i < idx; i++) { - dataLen = _itemLength(memPtr); - memPtr = memPtr + dataLen; - } - dataLen = _itemLength(memPtr); - return RLPItem(dataLen, memPtr); - } - - - /* - * @param get the RLP item by index. save gas. - */ - function safeGetItemByIndex(RLPItem memory item, uint idx) internal pure returns (RLPItem memory) { - require(isList(item), "RLPDecoder iterator is not a list"); - require(idx < numItems(item), "RLP item out of bounds"); - uint endPtr = item.memPtr + item.len; - - uint memPtr = item.memPtr + _payloadOffset(item.memPtr); - uint dataLen; - for (uint i = 0; i < idx; i++) { - dataLen = _itemLength(memPtr); - memPtr = memPtr + dataLen; - } - dataLen = _itemLength(memPtr); - - require(memPtr + dataLen <= endPtr, "RLP item overflow"); - return RLPItem(dataLen, memPtr); - } - - /* - * @param offset the receipt bytes item - */ - function typeOffset(RLPItem memory item) internal pure returns (RLPItem memory) { - uint offset = _payloadOffset(item.memPtr); - uint8 byte0; - uint memPtr = item.memPtr; - uint len = item.len; - assembly { - memPtr := add(memPtr, offset) - byte0 := byte(0, mload(memPtr)) - } - if (len >0 && byte0 < LIST_SHORT_START) { - assembly { - memPtr := add(memPtr, 0x01) - } - len -= 1; - } - return RLPItem(len, memPtr); - } - - // @return indicator whether encoded payload is a list. negate this function call for isData. - function isList(RLPItem memory item) internal pure returns (bool) { - if (item.len == 0) return false; - - uint8 byte0; - uint memPtr = item.memPtr; - assembly { - byte0 := byte(0, mload(memPtr)) - } - - if (byte0 < LIST_SHORT_START) return false; - return true; - } - - /** RLPItem conversions into data types **/ - - // @returns raw rlp encoding in bytes - function toRlpBytes(RLPItem memory item) internal pure returns (bytes memory) { - bytes memory result = new bytes(item.len); - if (result.length == 0) return result; - - uint ptr; - assembly { - ptr := add(0x20, result) - } - - copy(item.memPtr, ptr, item.len); - return result; - } - - // any non-zero byte except "0x80" is considered true - function toBoolean(RLPItem memory item) internal pure returns (bool) { - require(item.len == 1, "RLPDecoder toBoolean invalid length"); - uint result; - uint memPtr = item.memPtr; - assembly { - result := byte(0, mload(memPtr)) - } - - // SEE Github Issue #5. - // Summary: Most commonly used RLP libraries (i.e Geth) will encode - // "0" as "0x80" instead of as "0". We handle this edge case explicitly - // here. - if (result == 0 || result == STRING_SHORT_START) { - return false; - } else { - return true; - } - } - - function toAddress(RLPItem memory item) internal pure returns (address) { - // 1 byte for the length prefix - require(item.len == 21, "RLPDecoder toAddress invalid length"); - - return address(uint160((toUint(item)))); - } - - function toUint(RLPItem memory item) internal pure returns (uint) { - require(item.len > 0 && item.len <= 33, "RLPDecoder toUint invalid length"); - - uint offset = _payloadOffset(item.memPtr); - require(item.len >= offset, "RLPDecoder: invalid RLP item offset size"); - uint len = item.len - offset; - - uint result; - uint memPtr = item.memPtr + offset; - assembly { - result := mload(memPtr) - - // shift to the correct location if necessary - if lt(len, 32) { - result := div(result, exp(256, sub(32, len))) - } - } - - return result; - } - - // enforces 32 byte length - function toUintStrict(RLPItem memory item) internal pure returns (uint) { - // one byte prefix - require(item.len == 33, "RLPDecoder toUintStrict invalid length"); - - uint result; - uint memPtr = item.memPtr + 1; - assembly { - result := mload(memPtr) - } - - return result; - } - - function toBytes(RLPItem memory item) internal pure returns (bytes memory) { - require(item.len > 0, "RLPDecoder toBytes invalid length"); - - uint offset = _payloadOffset(item.memPtr); - require(item.len >= offset, "RLPDecoder: invalid RLP item offset size"); - uint len = item.len - offset; // data length - bytes memory result = new bytes(len); - - uint destPtr; - assembly { - destPtr := add(0x20, result) - } - - copy(item.memPtr + offset, destPtr, len); - return result; - } - - /* - * Private Helpers - */ - - // @return number of payload items inside an encoded list. - function numItems(RLPItem memory item) internal pure returns (uint) { - if (item.len == 0) return 0; - - uint count = 0; - uint currPtr = item.memPtr + _payloadOffset(item.memPtr); - uint endPtr = item.memPtr + item.len; - while (currPtr < endPtr) { - currPtr = currPtr + _itemLength(currPtr); // skip over an item - count++; - } - - return count; - } - - // @return entire rlp item byte length - function _itemLength(uint memPtr) private pure returns (uint) { - uint itemLen; - uint byte0; - assembly { - byte0 := byte(0, mload(memPtr)) - } - - if (byte0 < STRING_SHORT_START) itemLen = 1; - else if (byte0 < STRING_LONG_START) itemLen = byte0 - STRING_SHORT_START + 1; - else if (byte0 < LIST_SHORT_START) { - assembly { - let byteLen := sub(byte0, 0xb7) // # of bytes the actual length is - memPtr := add(memPtr, 1) // skip over the first byte - - /* 32 byte word size */ - let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len - itemLen := add(dataLen, add(byteLen, 1)) - } - } else if (byte0 < LIST_LONG_START) { - itemLen = byte0 - LIST_SHORT_START + 1; - } else { - assembly { - let byteLen := sub(byte0, 0xf7) - memPtr := add(memPtr, 1) - - let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length - itemLen := add(dataLen, add(byteLen, 1)) - } - } - - return itemLen; - } - - // @return number of bytes until the data - function _payloadOffset(uint memPtr) private pure returns (uint) { - uint byte0; - assembly { - byte0 := byte(0, mload(memPtr)) - } - - if (byte0 < STRING_SHORT_START) return 0; - else if (byte0 < STRING_LONG_START || (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START)) return 1; - else if (byte0 < LIST_SHORT_START) - // being explicit - return byte0 - (STRING_LONG_START - 1) + 1; - else return byte0 - (LIST_LONG_START - 1) + 1; - } - - /* - * @param src Pointer to source - * @param dest Pointer to destination - * @param len Amount of memory to copy from the source - */ - function copy( - uint src, - uint dest, - uint len - ) private pure { - if (len == 0) return; - - // copy as many word sizes as possible - for (; len >= WORD_SIZE; len -= WORD_SIZE) { - assembly { - mstore(dest, mload(src)) - } - - src += WORD_SIZE; - dest += WORD_SIZE; - } - - // left over bytes. Mask is used to remove unwanted bytes from the word - uint mask = 256**(WORD_SIZE - len) - 1; - assembly { - let srcpart := and(mload(src), not(mask)) // zero out src - let destpart := and(mload(dest), mask) // retrieve the bytes - mstore(dest, or(destpart, srcpart)) - } - } -} diff --git a/src/apps/layerzero/utility/UltraLightNodeEVMDecoder.sol b/src/apps/layerzero/utility/UltraLightNodeEVMDecoder.sol deleted file mode 100644 index 6809d14..0000000 --- a/src/apps/layerzero/utility/UltraLightNodeEVMDecoder.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -// TODO: CHECK IF WE CAN UPGRADE -// pragma solidity ^0.7.0; -pragma solidity ^0.8.13; - -import "./RLPDecode.sol"; - -library UltraLightNodeEVMDecoder { - using RLPDecode for RLPDecode.RLPItem; - using RLPDecode for RLPDecode.Iterator; - - struct Log { - address contractAddress; - bytes32 topicZero; - bytes data; - } - - function getReceiptLog(bytes memory data, uint logIndex) internal pure returns (Log memory) { - RLPDecode.Iterator memory it = RLPDecode.toRlpItem(data).iterator(); - uint idx; - while (it.hasNext()) { - if (idx == 3) { - return toReceiptLog(it.next().getItemByIndex(logIndex).toRlpBytes()); - } else it.next(); - idx++; - } - revert("no log index in receipt"); - } - - function toReceiptLog(bytes memory data) internal pure returns (Log memory) { - RLPDecode.Iterator memory it = RLPDecode.toRlpItem(data).iterator(); - Log memory log; - - uint idx; - while (it.hasNext()) { - if (idx == 0) { - log.contractAddress = it.next().toAddress(); - } else if (idx == 1) { - RLPDecode.RLPItem memory item = it.next().getItemByIndex(0); - log.topicZero = bytes32(item.toUint()); - } else if (idx == 2) log.data = it.next().toBytes(); - else it.next(); - idx++; - } - return log; - } -} From fe932702f01662f4e7b83c36f6951f327650b11d Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 07/61] feat: optimise gas queries for LZ --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 6ed657e..f470f09 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -2,17 +2,23 @@ pragma solidity ^0.8.13; import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; -import { ILayerZeroEndpointV2, MessagingParams } from "./interfaces/ILayerZeroEndpointV2.sol"; +import { ILayerZeroEndpointV2, MessagingParams, MessagingFee } from "./interfaces/ILayerZeroEndpointV2.sol"; /** * @notice LayerZero escrow. - * Do not use because of license issues. + * TODO: Set config such that we are the executor. + * TODO: Figure out if we can verify + * If not, then figure out how to decode the payload and then do both the composer and the executor step in 1. */ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { error LayerZeroCannotBeAddress0(); ILayerZeroEndpointV2 immutable LAYER_ZERO; + // TODO: Are these values packed? + uint128 excessPaid = 1; // Set to 1 so we never have to pay zero to non-zero cost. + bool allowExternalCall = false; + // chainid is immutable on LayerZero endpoint, so we read it and store it likewise. uint32 public immutable chainId; @@ -22,9 +28,18 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { chainId = LAYER_ZERO.eid(); } + // TODO: Fix function estimateAdditionalCost() external view returns(address asset, uint256 amount) { + MessagingParams memory params = MessagingParams({ + dstEid: uint32(uint256(0)), // TODO: Fix + receiver: bytes32(0), // TODO: FIX + message: hex"", + options: hex"", + payInLzToken: false + }); + MessagingFee memory fee = LAYER_ZERO.quote(params, address(this)); + amount = fee.nativeFee; asset = address(0); - amount = 0; // TODO: Verify. } function _getMessageIdentifier( @@ -64,20 +79,38 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfsendPacketInNativeToken) { - costOfsendPacketInNativeToken = 0; // TODO + // TODO: Optimise this. + MessagingParams memory params = MessagingParams({ + dstEid: uint32(uint256(destinationChainIdentifier)), + receiver: bytes32(destinationImplementation), + message: message, + options: hex"", + payInLzToken: false + }); + + // MessagingFee memory fee = LAYER_ZERO.quote(params, address(this)); + // costOfsendPacketInNativeToken = uint128(fee.nativeFee); // Layer zero doesn't need that much. // Handoff package to LZ. - LAYER_ZERO.send{value: costOfsendPacketInNativeToken}( - MessagingParams({ - dstEid: uint32(uint256(destinationChainIdentifier)), - receiver: bytes32(destinationImplementation), - message: message, - options: hex"", - payInLzToken: false - }), - msg.sender // TODO: + // We are getting a refund on any excess value we sent. Since that refund is + // coming before the end of this call, we can record it. + allowExternalCall = true; + LAYER_ZERO.send{value: msg.value}( + params, + address(this) ); + // Set the cost of the sendPacket to msg.value + costOfsendPacketInNativeToken = uint128(msg.value - (excessPaid - 1)); + excessPaid = 1; return costOfsendPacketInNativeToken; } + + // Record refunds coming in. + // Ideally, disallow randoms from sending to this contract but that wou + receive() external payable { + require(allowExternalCall, "Do not send ether to this address"); + excessPaid = uint128(1 + msg.value); + allowExternalCall = false; + } } \ No newline at end of file From a1f70103be4a44409d45700b238b46d9b9557b50 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 08/61] feat: verify LZ packages --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 51 ++++++--- src/apps/layerzero/SimpleLZULN.sol | 65 +++++++++++ src/apps/layerzero/interfaces/IPacket.sol | 13 +++ src/apps/layerzero/interfaces/IUlnBase.sol | 27 +++++ src/apps/layerzero/libs/AddressCast.sol | 41 +++++++ src/apps/layerzero/libs/PacketV1Codec.sol | 108 ++++++++++++++++++ 6 files changed, 289 insertions(+), 16 deletions(-) create mode 100644 src/apps/layerzero/SimpleLZULN.sol create mode 100644 src/apps/layerzero/interfaces/IPacket.sol create mode 100644 src/apps/layerzero/interfaces/IUlnBase.sol create mode 100644 src/apps/layerzero/libs/AddressCast.sol create mode 100644 src/apps/layerzero/libs/PacketV1Codec.sol diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index f470f09..98df2c0 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -3,6 +3,9 @@ pragma solidity ^0.8.13; import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; import { ILayerZeroEndpointV2, MessagingParams, MessagingFee } from "./interfaces/ILayerZeroEndpointV2.sol"; +import { PacketV1Codec } from "./libs/PacketV1Codec.sol"; +import { UlnConfig } from "./interfaces/IUlnBase.sol"; +import { SimpleLZULN } from "./SimpleLZULN.sol"; /** * @notice LayerZero escrow. @@ -10,10 +13,13 @@ import { ILayerZeroEndpointV2, MessagingParams, MessagingFee } from "./interface * TODO: Figure out if we can verify * If not, then figure out how to decode the payload and then do both the composer and the executor step in 1. */ -abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { +abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, SimpleLZULN { + using PacketV1Codec for bytes; + error LayerZeroCannotBeAddress0(); + error LZ_ULN_Verifying(); - ILayerZeroEndpointV2 immutable LAYER_ZERO; + ILayerZeroEndpointV2 immutable ENDPOINT; // TODO: Are these values packed? uint128 excessPaid = 1; // Set to 1 so we never have to pay zero to non-zero cost. @@ -21,11 +27,13 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { // chainid is immutable on LayerZero endpoint, so we read it and store it likewise. uint32 public immutable chainId; + address private constant DEFAULT_CONFIG = address(0); - constructor(address sendLostGasTo, address layer_zero) IncentivizedMessageEscrow(sendLostGasTo) { - if (layer_zero == address(0)) revert LayerZeroCannotBeAddress0(); - LAYER_ZERO = ILayerZeroEndpointV2(layer_zero); - chainId = LAYER_ZERO.eid(); + constructor(address sendLostGasTo, address lzEndpointV2, address ULN) IncentivizedMessageEscrow(sendLostGasTo) SimpleLZULN(ULN) { + if (lzEndpointV2 == address(0)) revert LayerZeroCannotBeAddress0(); + ENDPOINT = ILayerZeroEndpointV2(lzEndpointV2); + chainId = ENDPOINT.eid(); + // TODO: Set executor as this contract. } // TODO: Fix @@ -37,7 +45,7 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { options: hex"", payInLzToken: false }); - MessagingFee memory fee = LAYER_ZERO.quote(params, address(this)); + MessagingFee memory fee = ENDPOINT.quote(params, address(this)); amount = fee.nativeFee; asset = address(0); } @@ -53,28 +61,38 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { chainId, destinationIdentifier, message - ) + ) ); } - function _verifyPacket(bytes calldata _metadata, bytes calldata _message) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { + function _verifyPacket(bytes calldata _packetHeader, bytes calldata _payload) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { // TODO: Set verification logic. - // require(messageSigner == owner(), "!signer"); + // TODO: We need to check the header. + // _assertHeader(_packetHeader, localEid); + + // cache these values to save gas + address receiver = _packetHeader.receiverB20(); + uint32 srcEid = _packetHeader.srcEid(); + + bytes32 _headerHash = keccak256(_packetHeader); + bytes32 _payloadHash = keccak256(_payload); + if (!_checkVerifiable(srcEid, _headerHash, _payloadHash)) revert LZ_ULN_Verifying(); + // Load the identifier for the calling contract. - implementationIdentifier = _message[0:32]; + implementationIdentifier = _payload[0:32]; // Local "supposedly" this chain identifier. - uint16 thisChainIdentifier = uint16(uint256(bytes32(_message[64:96]))); + uint16 thisChainIdentifier = uint16(uint256(bytes32(_payload[64:96]))); // Check that the message is intended for this chain. require(thisChainIdentifier == chainId, "!Identifier"); // Local the identifier for the source chain. - sourceIdentifier = bytes32(_message[32:64]); + sourceIdentifier = bytes32(_payload[32:64]); // Get the application message. - message_ = _message[96:]; + message_ = _payload[96:]; } function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfsendPacketInNativeToken) { @@ -88,14 +106,14 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { payInLzToken: false }); - // MessagingFee memory fee = LAYER_ZERO.quote(params, address(this)); + // MessagingFee memory fee = ENDPOINT.quote(params, address(this)); // costOfsendPacketInNativeToken = uint128(fee.nativeFee); // Layer zero doesn't need that much. // Handoff package to LZ. // We are getting a refund on any excess value we sent. Since that refund is // coming before the end of this call, we can record it. allowExternalCall = true; - LAYER_ZERO.send{value: msg.value}( + ENDPOINT.send{value: msg.value}( params, address(this) ); @@ -113,4 +131,5 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { excessPaid = uint128(1 + msg.value); allowExternalCall = false; } + } \ No newline at end of file diff --git a/src/apps/layerzero/SimpleLZULN.sol b/src/apps/layerzero/SimpleLZULN.sol new file mode 100644 index 0000000..e60cc65 --- /dev/null +++ b/src/apps/layerzero/SimpleLZULN.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: LZBL-1.2 +// TODO: License + +pragma solidity ^0.8.13; + +import { IReceiveUlnBase, UlnConfig, Verification } from "./interfaces/IUlnBase.sol"; + +contract SimpleLZULN { + + IReceiveUlnBase immutable ULTRA_LIGHT_NODE; + + constructor(address ULN) { + ULTRA_LIGHT_NODE = IReceiveUlnBase(ULN); + } + + /// @dev for verifiable view function + /// @dev checks if this verification is ready to be committed to the endpoint + function _checkVerifiable( + uint32 _srcEid, + bytes32 _headerHash, + bytes32 _payloadHash + ) internal view returns (bool) { + UlnConfig memory _config = ULTRA_LIGHT_NODE.getUlnConfig(address(this), _srcEid); + // iterate the required DVNs + if (_config.requiredDVNCount > 0) { + for (uint8 i = 0; i < _config.requiredDVNCount; ++i) { + if (!_verified(_config.requiredDVNs[i], _headerHash, _payloadHash, _config.confirmations)) { + // return if any of the required DVNs haven't signed + return false; + } + } + if (_config.optionalDVNCount == 0) { + // returns early if all required DVNs have signed and there are no optional DVNs + return true; + } + } + + // then it must require optional validations + uint8 threshold = _config.optionalDVNThreshold; + for (uint8 i = 0; i < _config.optionalDVNCount; ++i) { + if (_verified(_config.optionalDVNs[i], _headerHash, _payloadHash, _config.confirmations)) { + // increment the optional count if the optional DVN has signed + threshold--; + if (threshold == 0) { + // early return if the optional threshold has hit + return true; + } + } + } + + // return false as a catch-all + return false; + } + + function _verified( + address _dvn, + bytes32 _headerHash, + bytes32 _payloadHash, + uint64 _requiredConfirmation + ) internal view returns (bool verified) { + Verification memory verification = ULTRA_LIGHT_NODE.hashLookup(_headerHash, _payloadHash, _dvn); + // return true if the dvn has signed enough confirmations + verified = verification.submitted && verification.confirmations >= _requiredConfirmation; + } +} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IPacket.sol b/src/apps/layerzero/interfaces/IPacket.sol new file mode 100644 index 0000000..cca1c69 --- /dev/null +++ b/src/apps/layerzero/interfaces/IPacket.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +struct Packet { + uint64 nonce; + uint32 srcEid; + address sender; + uint32 dstEid; + bytes32 receiver; + bytes32 guid; + bytes message; +} diff --git a/src/apps/layerzero/interfaces/IUlnBase.sol b/src/apps/layerzero/interfaces/IUlnBase.sol new file mode 100644 index 0000000..cd052c3 --- /dev/null +++ b/src/apps/layerzero/interfaces/IUlnBase.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: LZBL-1.2 +// TODO: License +pragma solidity ^0.8.13; + + +// the formal properties are documented in the setter functions +struct UlnConfig { + uint64 confirmations; + // we store the length of required DVNs and optional DVNs instead of using DVN.length directly to save gas + uint8 requiredDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) + uint8 optionalDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) + uint8 optionalDVNThreshold; // (0, optionalDVNCount] + address[] requiredDVNs; // no duplicates. sorted an an ascending order. allowed overlap with optionalDVNs + address[] optionalDVNs; // no duplicates. sorted an an ascending order. allowed overlap with requiredDVNs +} + +struct Verification { + bool submitted; + uint64 confirmations; +} + +interface IReceiveUlnBase { + // mapping(bytes32 headerHash => mapping(bytes32 payloadHash => mapping(address dvn => Verification))) public hashLookup; + function hashLookup(bytes32 headerHash, bytes32 payloadHash, address dvn) external view returns (Verification memory); + + function getUlnConfig(address _oapp, uint32 _remoteEid) external view returns (UlnConfig memory rtnConfig); +} \ No newline at end of file diff --git a/src/apps/layerzero/libs/AddressCast.sol b/src/apps/layerzero/libs/AddressCast.sol new file mode 100644 index 0000000..e82a76b --- /dev/null +++ b/src/apps/layerzero/libs/AddressCast.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.13; + +library AddressCast { + error AddressCast_InvalidSizeForAddress(); + error AddressCast_InvalidAddress(); + + function toBytes32(bytes calldata _addressBytes) internal pure returns (bytes32 result) { + if (_addressBytes.length > 32) revert AddressCast_InvalidAddress(); + result = bytes32(_addressBytes); + unchecked { + uint256 offset = 32 - _addressBytes.length; + result = result >> (offset * 8); + } + } + + function toBytes32(address _address) internal pure returns (bytes32 result) { + result = bytes32(uint256(uint160(_address))); + } + + function toBytes(bytes32 _addressBytes32, uint256 _size) internal pure returns (bytes memory result) { + if (_size == 0 || _size > 32) revert AddressCast_InvalidSizeForAddress(); + result = new bytes(_size); + unchecked { + uint256 offset = 256 - _size * 8; + assembly { + mstore(add(result, 32), shl(offset, _addressBytes32)) + } + } + } + + function toAddress(bytes32 _addressBytes32) internal pure returns (address result) { + result = address(uint160(uint256(_addressBytes32))); + } + + function toAddress(bytes calldata _addressBytes) internal pure returns (address result) { + if (_addressBytes.length != 20) revert AddressCast_InvalidAddress(); + result = address(bytes20(_addressBytes)); + } +} \ No newline at end of file diff --git a/src/apps/layerzero/libs/PacketV1Codec.sol b/src/apps/layerzero/libs/PacketV1Codec.sol new file mode 100644 index 0000000..750973f --- /dev/null +++ b/src/apps/layerzero/libs/PacketV1Codec.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.13; + +import { Packet } from "../interfaces/IPacket.sol"; +import { AddressCast } from "./AddressCast.sol"; + +library PacketV1Codec { + using AddressCast for address; + using AddressCast for bytes32; + + uint8 internal constant PACKET_VERSION = 1; + + // header (version + nonce + path) + // version + uint256 private constant PACKET_VERSION_OFFSET = 0; + // nonce + uint256 private constant NONCE_OFFSET = 1; + // path + uint256 private constant SRC_EID_OFFSET = 9; + uint256 private constant SENDER_OFFSET = 13; + uint256 private constant DST_EID_OFFSET = 45; + uint256 private constant RECEIVER_OFFSET = 49; + // payload (guid + message) + uint256 private constant GUID_OFFSET = 81; // keccak256(nonce + path) + uint256 private constant MESSAGE_OFFSET = 113; + + function encode(Packet memory _packet) internal pure returns (bytes memory encodedPacket) { + encodedPacket = abi.encodePacked( + PACKET_VERSION, + _packet.nonce, + _packet.srcEid, + _packet.sender.toBytes32(), + _packet.dstEid, + _packet.receiver, + _packet.guid, + _packet.message + ); + } + + function encodePacketHeader(Packet memory _packet) internal pure returns (bytes memory) { + return + abi.encodePacked( + PACKET_VERSION, + _packet.nonce, + _packet.srcEid, + _packet.sender.toBytes32(), + _packet.dstEid, + _packet.receiver + ); + } + + function encodePayload(Packet memory _packet) internal pure returns (bytes memory) { + return abi.encodePacked(_packet.guid, _packet.message); + } + + function header(bytes calldata _packet) internal pure returns (bytes calldata) { + return _packet[0:GUID_OFFSET]; + } + + function version(bytes calldata _packet) internal pure returns (uint8) { + return uint8(bytes1(_packet[PACKET_VERSION_OFFSET:NONCE_OFFSET])); + } + + function nonce(bytes calldata _packet) internal pure returns (uint64) { + return uint64(bytes8(_packet[NONCE_OFFSET:SRC_EID_OFFSET])); + } + + function srcEid(bytes calldata _packet) internal pure returns (uint32) { + return uint32(bytes4(_packet[SRC_EID_OFFSET:SENDER_OFFSET])); + } + + function sender(bytes calldata _packet) internal pure returns (bytes32) { + return bytes32(_packet[SENDER_OFFSET:DST_EID_OFFSET]); + } + + function senderAddressB20(bytes calldata _packet) internal pure returns (address) { + return sender(_packet).toAddress(); + } + + function dstEid(bytes calldata _packet) internal pure returns (uint32) { + return uint32(bytes4(_packet[DST_EID_OFFSET:RECEIVER_OFFSET])); + } + + function receiver(bytes calldata _packet) internal pure returns (bytes32) { + return bytes32(_packet[RECEIVER_OFFSET:GUID_OFFSET]); + } + + function receiverB20(bytes calldata _packet) internal pure returns (address) { + return receiver(_packet).toAddress(); + } + + function guid(bytes calldata _packet) internal pure returns (bytes32) { + return bytes32(_packet[GUID_OFFSET:MESSAGE_OFFSET]); + } + + function message(bytes calldata _packet) internal pure returns (bytes calldata) { + return bytes(_packet[MESSAGE_OFFSET:]); + } + + function payload(bytes calldata _packet) internal pure returns (bytes calldata) { + return bytes(_packet[GUID_OFFSET:]); + } + + function payloadHash(bytes calldata _packet) internal pure returns (bytes32) { + return keccak256(payload(_packet)); + } +} \ No newline at end of file From cf9973574db2f3745e57dcf644ef3bc20a2e43e5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 09/61] feat: doc --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 1 + src/apps/layerzero/README.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 98df2c0..cd83031 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -79,6 +79,7 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, if (!_checkVerifiable(srcEid, _headerHash, _payloadHash)) revert LZ_ULN_Verifying(); + // TODO: everything below. // Load the identifier for the calling contract. implementationIdentifier = _payload[0:32]; diff --git a/src/apps/layerzero/README.md b/src/apps/layerzero/README.md index 4d186f8..f0a8306 100644 --- a/src/apps/layerzero/README.md +++ b/src/apps/layerzero/README.md @@ -1,3 +1,5 @@ # Wormhole Generalised Incentives -This is a Layer Zero implementation of Generalised Incentives. Do not use these contracts. \ No newline at end of file +This is a Layer Zero implementation of Generalised Incentives. + +The contracts are not allowed to be used in production. \ No newline at end of file From 5bf66f09246c229accb2517623454900323dd987 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 10/61] feat: save gas by calling verifyable directly --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 14 +++--- src/apps/layerzero/SimpleLZULN.sol | 49 ------------------- src/apps/layerzero/interfaces/IUlnBase.sol | 7 ++- 3 files changed, 12 insertions(+), 58 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index cd83031..7d2c286 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -5,21 +5,20 @@ import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; import { ILayerZeroEndpointV2, MessagingParams, MessagingFee } from "./interfaces/ILayerZeroEndpointV2.sol"; import { PacketV1Codec } from "./libs/PacketV1Codec.sol"; import { UlnConfig } from "./interfaces/IUlnBase.sol"; -import { SimpleLZULN } from "./SimpleLZULN.sol"; +import { IReceiveUlnBase, UlnConfig, Verification } from "./interfaces/IUlnBase.sol"; /** * @notice LayerZero escrow. * TODO: Set config such that we are the executor. - * TODO: Figure out if we can verify - * If not, then figure out how to decode the payload and then do both the composer and the executor step in 1. */ -abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, SimpleLZULN { +abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { using PacketV1Codec for bytes; error LayerZeroCannotBeAddress0(); error LZ_ULN_Verifying(); ILayerZeroEndpointV2 immutable ENDPOINT; + IReceiveUlnBase immutable ULTRA_LIGHT_NODE; // TODO: Are these values packed? uint128 excessPaid = 1; // Set to 1 so we never have to pay zero to non-zero cost. @@ -29,10 +28,11 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, uint32 public immutable chainId; address private constant DEFAULT_CONFIG = address(0); - constructor(address sendLostGasTo, address lzEndpointV2, address ULN) IncentivizedMessageEscrow(sendLostGasTo) SimpleLZULN(ULN) { + constructor(address sendLostGasTo, address lzEndpointV2, address ULN) IncentivizedMessageEscrow(sendLostGasTo) { if (lzEndpointV2 == address(0)) revert LayerZeroCannotBeAddress0(); ENDPOINT = ILayerZeroEndpointV2(lzEndpointV2); chainId = ENDPOINT.eid(); + ULTRA_LIGHT_NODE = IReceiveUlnBase(ULN); // TODO: Set executor as this contract. } @@ -76,8 +76,8 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, bytes32 _headerHash = keccak256(_packetHeader); bytes32 _payloadHash = keccak256(_payload); - if (!_checkVerifiable(srcEid, _headerHash, _payloadHash)) revert LZ_ULN_Verifying(); - + UlnConfig memory _config = ULTRA_LIGHT_NODE.getUlnConfig(address(this), srcEid); + if (ULTRA_LIGHT_NODE.verifiable(_config, _headerHash, _payloadHash)) revert LZ_ULN_Verifying(); // TODO: everything below. // Load the identifier for the calling contract. diff --git a/src/apps/layerzero/SimpleLZULN.sol b/src/apps/layerzero/SimpleLZULN.sol index e60cc65..c665be2 100644 --- a/src/apps/layerzero/SimpleLZULN.sol +++ b/src/apps/layerzero/SimpleLZULN.sol @@ -13,53 +13,4 @@ contract SimpleLZULN { ULTRA_LIGHT_NODE = IReceiveUlnBase(ULN); } - /// @dev for verifiable view function - /// @dev checks if this verification is ready to be committed to the endpoint - function _checkVerifiable( - uint32 _srcEid, - bytes32 _headerHash, - bytes32 _payloadHash - ) internal view returns (bool) { - UlnConfig memory _config = ULTRA_LIGHT_NODE.getUlnConfig(address(this), _srcEid); - // iterate the required DVNs - if (_config.requiredDVNCount > 0) { - for (uint8 i = 0; i < _config.requiredDVNCount; ++i) { - if (!_verified(_config.requiredDVNs[i], _headerHash, _payloadHash, _config.confirmations)) { - // return if any of the required DVNs haven't signed - return false; - } - } - if (_config.optionalDVNCount == 0) { - // returns early if all required DVNs have signed and there are no optional DVNs - return true; - } - } - - // then it must require optional validations - uint8 threshold = _config.optionalDVNThreshold; - for (uint8 i = 0; i < _config.optionalDVNCount; ++i) { - if (_verified(_config.optionalDVNs[i], _headerHash, _payloadHash, _config.confirmations)) { - // increment the optional count if the optional DVN has signed - threshold--; - if (threshold == 0) { - // early return if the optional threshold has hit - return true; - } - } - } - - // return false as a catch-all - return false; - } - - function _verified( - address _dvn, - bytes32 _headerHash, - bytes32 _payloadHash, - uint64 _requiredConfirmation - ) internal view returns (bool verified) { - Verification memory verification = ULTRA_LIGHT_NODE.hashLookup(_headerHash, _payloadHash, _dvn); - // return true if the dvn has signed enough confirmations - verified = verification.submitted && verification.confirmations >= _requiredConfirmation; - } } \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IUlnBase.sol b/src/apps/layerzero/interfaces/IUlnBase.sol index cd052c3..41411e0 100644 --- a/src/apps/layerzero/interfaces/IUlnBase.sol +++ b/src/apps/layerzero/interfaces/IUlnBase.sol @@ -20,8 +20,11 @@ struct Verification { } interface IReceiveUlnBase { - // mapping(bytes32 headerHash => mapping(bytes32 payloadHash => mapping(address dvn => Verification))) public hashLookup; - function hashLookup(bytes32 headerHash, bytes32 payloadHash, address dvn) external view returns (Verification memory); + function verifiable( + UlnConfig memory _config, + bytes32 _headerHash, + bytes32 _payloadHash + ) external view returns (bool); function getUlnConfig(address _oapp, uint32 _remoteEid) external view returns (UlnConfig memory rtnConfig); } \ No newline at end of file From 34ec03c6ac751c48c2f964c9bbcaa86bd85c1fbf Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 11/61] feat: cleanup packet verification --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 7d2c286..9f0880e 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -16,6 +16,9 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { error LayerZeroCannotBeAddress0(); error LZ_ULN_Verifying(); + error LZ_ULN_InvalidPacketHeader(); + error LZ_ULN_InvalidPacketVersion(); + error LZ_ULN_InvalidEid(); ILayerZeroEndpointV2 immutable ENDPOINT; IReceiveUlnBase immutable ULTRA_LIGHT_NODE; @@ -65,35 +68,28 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { ); } - function _verifyPacket(bytes calldata _packetHeader, bytes calldata _payload) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { - // TODO: Set verification logic. - // TODO: We need to check the header. - // _assertHeader(_packetHeader, localEid); + function _verifyPacket(bytes calldata _packetHeader, bytes calldata _packet) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { + _assertHeader(_packetHeader); - // cache these values to save gas + // Check that we are the receiver address receiver = _packetHeader.receiverB20(); + require(receiver == address(this)); // TODO: update + + // Get the source chain. uint32 srcEid = _packetHeader.srcEid(); + bytes32 _headerHash = keccak256(_packetHeader); - bytes32 _payloadHash = keccak256(_payload); + bytes32 _payloadHash = _packet.payloadHash(); UlnConfig memory _config = ULTRA_LIGHT_NODE.getUlnConfig(address(this), srcEid); if (ULTRA_LIGHT_NODE.verifiable(_config, _headerHash, _payloadHash)) revert LZ_ULN_Verifying(); - // TODO: everything below. - // Load the identifier for the calling contract. - implementationIdentifier = _payload[0:32]; - - // Local "supposedly" this chain identifier. - uint16 thisChainIdentifier = uint16(uint256(bytes32(_payload[64:96]))); - - // Check that the message is intended for this chain. - require(thisChainIdentifier == chainId, "!Identifier"); - - // Local the identifier for the source chain. - sourceIdentifier = bytes32(_payload[32:64]); - - // Get the application message. - message_ = _payload[96:]; + // Get the sourec chain + sourceIdentifier = bytes32(uint256(srcEid)); + // Get the sender + implementationIdentifier = abi.encode(_packetHeader.sender()); + // Get the message + message_ = _packet.message(); } function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfsendPacketInNativeToken) { @@ -133,4 +129,13 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { allowExternalCall = false; } + + function _assertHeader(bytes calldata _packetHeader) internal view { + // assert packet header is of right size 81 + if (_packetHeader.length != 81) revert LZ_ULN_InvalidPacketHeader(); + // assert packet header version is the same as ULN + if (_packetHeader.version() != PacketV1Codec.PACKET_VERSION) revert LZ_ULN_InvalidPacketVersion(); + // assert the packet is for this endpoint + if (_packetHeader.dstEid() != chainId) revert LZ_ULN_InvalidEid(); + } } \ No newline at end of file From 6fbe4e2e67665e57e48a861fa74d8724e7a5179c Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 12/61] feat: cleanup implementation --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 9f0880e..ab39561 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -19,6 +19,7 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { error LZ_ULN_InvalidPacketHeader(); error LZ_ULN_InvalidPacketVersion(); error LZ_ULN_InvalidEid(); + error IncorrectDestination(address actual); ILayerZeroEndpointV2 immutable ENDPOINT; IReceiveUlnBase immutable ULTRA_LIGHT_NODE; @@ -73,7 +74,7 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { // Check that we are the receiver address receiver = _packetHeader.receiverB20(); - require(receiver == address(this)); // TODO: update + if(receiver != address(this)) revert IncorrectDestination(receiver); // Get the source chain. uint32 srcEid = _packetHeader.srcEid(); @@ -94,7 +95,6 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfsendPacketInNativeToken) { - // TODO: Optimise this. MessagingParams memory params = MessagingParams({ dstEid: uint32(uint256(destinationChainIdentifier)), receiver: bytes32(destinationImplementation), @@ -103,9 +103,6 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { payInLzToken: false }); - // MessagingFee memory fee = ENDPOINT.quote(params, address(this)); - // costOfsendPacketInNativeToken = uint128(fee.nativeFee); // Layer zero doesn't need that much. - // Handoff package to LZ. // We are getting a refund on any excess value we sent. Since that refund is // coming before the end of this call, we can record it. From 351f0f90a4e2a7126371238e4f3fd981dd6cb509 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 13/61] feat: clean up --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index ab39561..b8f1fb3 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -11,7 +11,7 @@ import { IReceiveUlnBase, UlnConfig, Verification } from "./interfaces/IUlnBase. * @notice LayerZero escrow. * TODO: Set config such that we are the executor. */ -abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { +contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { using PacketV1Codec for bytes; error LayerZeroCannotBeAddress0(); @@ -21,16 +21,18 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { error LZ_ULN_InvalidEid(); error IncorrectDestination(address actual); + // Layer Zero associated addresses ILayerZeroEndpointV2 immutable ENDPOINT; IReceiveUlnBase immutable ULTRA_LIGHT_NODE; - // TODO: Are these values packed? + // chainid is immutable on LayerZero endpoint, so we read it and store it likewise. + uint32 public immutable chainId; + + /// @notice How much extra did we sent to LZ? uint128 excessPaid = 1; // Set to 1 so we never have to pay zero to non-zero cost. + /// @notice Only allow LZ to send bool allowExternalCall = false; - // chainid is immutable on LayerZero endpoint, so we read it and store it likewise. - uint32 public immutable chainId; - address private constant DEFAULT_CONFIG = address(0); constructor(address sendLostGasTo, address lzEndpointV2, address ULN) IncentivizedMessageEscrow(sendLostGasTo) { if (lzEndpointV2 == address(0)) revert LayerZeroCannotBeAddress0(); @@ -121,6 +123,7 @@ abstract contract BareIncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { // Record refunds coming in. // Ideally, disallow randoms from sending to this contract but that wou receive() external payable { + // TODO: Do we have enough gas for this? I hope we have since allowExternalCall is warm. require(allowExternalCall, "Do not send ether to this address"); excessPaid = uint128(1 + msg.value); allowExternalCall = false; From 51676ddc9b80c14dd06890f656f40ed7edf46e75 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 14/61] test: prepare tests --- test/layerzero/LZCommon.sol | 21 +++++++++++++++++++++ test/layerzero/mock/MockLayerZeroEscrow.sol | 20 ++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 test/layerzero/LZCommon.sol create mode 100644 test/layerzero/mock/MockLayerZeroEscrow.sol diff --git a/test/layerzero/LZCommon.sol b/test/layerzero/LZCommon.sol new file mode 100644 index 0000000..d445598 --- /dev/null +++ b/test/layerzero/LZCommon.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import { MockLayerZeroEscrow } from "./mock/MockLayerZeroEscrow.sol"; + +contract TestLzCommon is Test { + + MockLayerZeroEscrow mockLayerZeroEscrow; + + address SEND_LOST_GAS_TO = address(uint160(0xdead)); + address lzEndpointV2; + address ULN; + + function setUp() virtual public { + + lzEndpointV2 = address(0); + ULN = address(0); + mockLayerZeroEscrow = new MockLayerZeroEscrow(SEND_LOST_GAS_TO, lzEndpointV2, ULN); + } +} diff --git a/test/layerzero/mock/MockLayerZeroEscrow.sol b/test/layerzero/mock/MockLayerZeroEscrow.sol new file mode 100644 index 0000000..5ccaeb2 --- /dev/null +++ b/test/layerzero/mock/MockLayerZeroEscrow.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: IncentivizedLayerZeroEscrow +pragma solidity ^0.8.13; + +import { IncentivizedLayerZeroEscrow } from "../../../src/apps/layerzero/IncentivizedLayerZeroEscrow.sol"; + +/** + * @notice Mock Layer Zero Escrow + */ +contract MockLayerZeroEscrow is IncentivizedLayerZeroEscrow { + + constructor(address sendLostGasTo, address lzEndpointV2, address ULN) IncentivizedLayerZeroEscrow(sendLostGasTo, lzEndpointV2, ULN) {} + + function verifyPacket(bytes calldata _packetHeader, bytes calldata _packet) external view returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { + return _verifyPacket(_packetHeader, _packet); + } + + function sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) external returns(uint128 costOfsendPacketInNativeToken) { + return _sendPacket(destinationChainIdentifier, destinationImplementation, message); + } +} \ No newline at end of file From bdab3a9a91c12b96ce3d1bb537e02f009fedb757 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 15/61] forge install: LayerZero-v2 --- .gitmodules | 6 +++--- lib/LayerZero-v2 | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) create mode 160000 lib/LayerZero-v2 diff --git a/.gitmodules b/.gitmodules index 3bf7079..84c91fa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,9 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/LayerZero"] - path = lib/LayerZero - url = https://github.com/LayerZero-Labs/LayerZero [submodule "lib/vibc-core-smart-contracts"] path = lib/vibc-core-smart-contracts url = https://github.com/open-ibc/vibc-core-smart-contracts +[submodule "lib/LayerZero-v2"] + path = lib/LayerZero-v2 + url = https://github.com/LayerZero-Labs/LayerZero-v2 diff --git a/lib/LayerZero-v2 b/lib/LayerZero-v2 new file mode 160000 index 0000000..3fd23fd --- /dev/null +++ b/lib/LayerZero-v2 @@ -0,0 +1 @@ +Subproject commit 3fd23fd21a3c7c42473d43bee44cedecbc93569e From afb16e0515bd1da536218c9a90fbe7d159c6eb55 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 16/61] feat: Directly import OZ to remove interfaces and simplify testing --- remappings.txt | 3 +- .../layerzero/IncentivizedLayerZeroEscrow.sol | 5 +- src/apps/layerzero/SimpleLZULN.sol | 16 --- .../interfaces/ILayerZeroEndpointV2.sol | 89 --------------- .../interfaces/IMessageLibManager.sol | 70 ------------ .../interfaces/IMessagingChannel.sol | 34 ------ .../interfaces/IMessagingComposer.sol | 38 ------ .../interfaces/IMessagingContext.sol | 9 -- src/apps/layerzero/interfaces/IPacket.sol | 13 --- src/apps/layerzero/libs/AddressCast.sol | 41 ------- src/apps/layerzero/libs/PacketV1Codec.sol | 108 ------------------ test/layerzero/LZCommon.sol | 1 + 12 files changed, 6 insertions(+), 421 deletions(-) delete mode 100644 src/apps/layerzero/SimpleLZULN.sol delete mode 100644 src/apps/layerzero/interfaces/ILayerZeroEndpointV2.sol delete mode 100644 src/apps/layerzero/interfaces/IMessageLibManager.sol delete mode 100644 src/apps/layerzero/interfaces/IMessagingChannel.sol delete mode 100644 src/apps/layerzero/interfaces/IMessagingComposer.sol delete mode 100644 src/apps/layerzero/interfaces/IMessagingContext.sol delete mode 100644 src/apps/layerzero/interfaces/IPacket.sol delete mode 100644 src/apps/layerzero/libs/AddressCast.sol delete mode 100644 src/apps/layerzero/libs/PacketV1Codec.sol diff --git a/remappings.txt b/remappings.txt index 104eeca..76c4037 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,4 @@ forge-std/=lib/forge-std/src/ openzeppelin/=lib/openzeppelin-contracts/contracts/ -vibc-core-smart-contracts/=lib/vibc-core-smart-contracts/contracts/ \ No newline at end of file +vibc-core-smart-contracts/=lib/vibc-core-smart-contracts/contracts/ +@openzeppelin/=lib/openzeppelin-contracts/ \ No newline at end of file diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index b8f1fb3..3e20977 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: DO-NOT-USE pragma solidity ^0.8.13; +import { ILayerZeroEndpointV2, MessagingParams, MessagingFee } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { PacketV1Codec } from "LayerZero-v2/protocol/contracts/messagelib/libs/PacketV1Codec.sol"; + import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; -import { ILayerZeroEndpointV2, MessagingParams, MessagingFee } from "./interfaces/ILayerZeroEndpointV2.sol"; -import { PacketV1Codec } from "./libs/PacketV1Codec.sol"; import { UlnConfig } from "./interfaces/IUlnBase.sol"; import { IReceiveUlnBase, UlnConfig, Verification } from "./interfaces/IUlnBase.sol"; diff --git a/src/apps/layerzero/SimpleLZULN.sol b/src/apps/layerzero/SimpleLZULN.sol deleted file mode 100644 index c665be2..0000000 --- a/src/apps/layerzero/SimpleLZULN.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: LZBL-1.2 -// TODO: License - -pragma solidity ^0.8.13; - -import { IReceiveUlnBase, UlnConfig, Verification } from "./interfaces/IUlnBase.sol"; - -contract SimpleLZULN { - - IReceiveUlnBase immutable ULTRA_LIGHT_NODE; - - constructor(address ULN) { - ULTRA_LIGHT_NODE = IReceiveUlnBase(ULN); - } - -} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/ILayerZeroEndpointV2.sol b/src/apps/layerzero/interfaces/ILayerZeroEndpointV2.sol deleted file mode 100644 index 3cb76ad..0000000 --- a/src/apps/layerzero/interfaces/ILayerZeroEndpointV2.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.0; - -import { IMessageLibManager } from "./IMessageLibManager.sol"; -import { IMessagingComposer } from "./IMessagingComposer.sol"; -import { IMessagingChannel } from "./IMessagingChannel.sol"; -import { IMessagingContext } from "./IMessagingContext.sol"; - -struct MessagingParams { - uint32 dstEid; - bytes32 receiver; - bytes message; - bytes options; - bool payInLzToken; -} - -struct MessagingReceipt { - bytes32 guid; - uint64 nonce; - MessagingFee fee; -} - -struct MessagingFee { - uint256 nativeFee; - uint256 lzTokenFee; -} - -struct Origin { - uint32 srcEid; - bytes32 sender; - uint64 nonce; -} - -interface ILayerZeroEndpointV2 is IMessageLibManager, IMessagingComposer, IMessagingChannel, IMessagingContext { - event PacketSent(bytes encodedPayload, bytes options, address sendLibrary); - - event PacketVerified(Origin origin, address receiver, bytes32 payloadHash); - - event PacketDelivered(Origin origin, address receiver); - - event LzReceiveAlert( - address indexed receiver, - address indexed executor, - Origin origin, - bytes32 guid, - uint256 gas, - uint256 value, - bytes message, - bytes extraData, - bytes reason - ); - - event LzTokenSet(address token); - - event DelegateSet(address sender, address delegate); - - function quote(MessagingParams calldata _params, address _sender) external view returns (MessagingFee memory); - - function send( - MessagingParams calldata _params, - address _refundAddress - ) external payable returns (MessagingReceipt memory); - - function verify(Origin calldata _origin, address _receiver, bytes32 _payloadHash) external; - - function verifiable(Origin calldata _origin, address _receiver) external view returns (bool); - - function initializable(Origin calldata _origin, address _receiver) external view returns (bool); - - function lzReceive( - Origin calldata _origin, - address _receiver, - bytes32 _guid, - bytes calldata _message, - bytes calldata _extraData - ) external payable; - - // oapp can burn messages partially by calling this function with its own business logic if messages are verified in order - function clear(address _oapp, Origin calldata _origin, bytes32 _guid, bytes calldata _message) external; - - function setLzToken(address _lzToken) external; - - function lzToken() external view returns (address); - - function nativeToken() external view returns (address); - - function setDelegate(address _delegate) external; -} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IMessageLibManager.sol b/src/apps/layerzero/interfaces/IMessageLibManager.sol deleted file mode 100644 index 9302b69..0000000 --- a/src/apps/layerzero/interfaces/IMessageLibManager.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.0; - -struct SetConfigParam { - uint32 eid; - uint32 configType; - bytes config; -} - -interface IMessageLibManager { - struct Timeout { - address lib; - uint256 expiry; - } - - event LibraryRegistered(address newLib); - event DefaultSendLibrarySet(uint32 eid, address newLib); - event DefaultReceiveLibrarySet(uint32 eid, address newLib); - event DefaultReceiveLibraryTimeoutSet(uint32 eid, address oldLib, uint256 expiry); - event SendLibrarySet(address sender, uint32 eid, address newLib); - event ReceiveLibrarySet(address receiver, uint32 eid, address newLib); - event ReceiveLibraryTimeoutSet(address receiver, uint32 eid, address oldLib, uint256 timeout); - - function registerLibrary(address _lib) external; - - function isRegisteredLibrary(address _lib) external view returns (bool); - - function getRegisteredLibraries() external view returns (address[] memory); - - function setDefaultSendLibrary(uint32 _eid, address _newLib) external; - - function defaultSendLibrary(uint32 _eid) external view returns (address); - - function setDefaultReceiveLibrary(uint32 _eid, address _newLib, uint256 _timeout) external; - - function defaultReceiveLibrary(uint32 _eid) external view returns (address); - - function setDefaultReceiveLibraryTimeout(uint32 _eid, address _lib, uint256 _expiry) external; - - function defaultReceiveLibraryTimeout(uint32 _eid) external view returns (address lib, uint256 expiry); - - function isSupportedEid(uint32 _eid) external view returns (bool); - - function isValidReceiveLibrary(address _receiver, uint32 _eid, address _lib) external view returns (bool); - - /// ------------------- OApp interfaces ------------------- - function setSendLibrary(address _oapp, uint32 _eid, address _newLib) external; - - function getSendLibrary(address _sender, uint32 _eid) external view returns (address lib); - - function isDefaultSendLibrary(address _sender, uint32 _eid) external view returns (bool); - - function setReceiveLibrary(address _oapp, uint32 _eid, address _newLib, uint256 _gracePeriod) external; - - function getReceiveLibrary(address _receiver, uint32 _eid) external view returns (address lib, bool isDefault); - - function setReceiveLibraryTimeout(address _oapp, uint32 _eid, address _lib, uint256 _gracePeriod) external; - - function receiveLibraryTimeout(address _receiver, uint32 _eid) external view returns (address lib, uint256 expiry); - - function setConfig(address _oapp, address _lib, SetConfigParam[] calldata _params) external; - - function getConfig( - address _oapp, - address _lib, - uint32 _eid, - uint32 _configType - ) external view returns (bytes memory config); -} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IMessagingChannel.sol b/src/apps/layerzero/interfaces/IMessagingChannel.sol deleted file mode 100644 index 93cff27..0000000 --- a/src/apps/layerzero/interfaces/IMessagingChannel.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.0; - -interface IMessagingChannel { - event InboundNonceSkipped(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce); - event PacketNilified(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce, bytes32 payloadHash); - event PacketBurnt(uint32 srcEid, bytes32 sender, address receiver, uint64 nonce, bytes32 payloadHash); - - function eid() external view returns (uint32); - - // this is an emergency function if a message cannot be verified for some reasons - // required to provide _nextNonce to avoid race condition - function skip(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce) external; - - function nilify(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external; - - function burn(address _oapp, uint32 _srcEid, bytes32 _sender, uint64 _nonce, bytes32 _payloadHash) external; - - function nextGuid(address _sender, uint32 _dstEid, bytes32 _receiver) external view returns (bytes32); - - function inboundNonce(address _receiver, uint32 _srcEid, bytes32 _sender) external view returns (uint64); - - function outboundNonce(address _sender, uint32 _dstEid, bytes32 _receiver) external view returns (uint64); - - function inboundPayloadHash( - address _receiver, - uint32 _srcEid, - bytes32 _sender, - uint64 _nonce - ) external view returns (bytes32); - - function lazyInboundNonce(address _receiver, uint32 _srcEid, bytes32 _sender) external view returns (uint64); -} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IMessagingComposer.sol b/src/apps/layerzero/interfaces/IMessagingComposer.sol deleted file mode 100644 index 5918bd4..0000000 --- a/src/apps/layerzero/interfaces/IMessagingComposer.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.0; - -interface IMessagingComposer { - event ComposeSent(address from, address to, bytes32 guid, uint16 index, bytes message); - event ComposeDelivered(address from, address to, bytes32 guid, uint16 index); - event LzComposeAlert( - address indexed from, - address indexed to, - address indexed executor, - bytes32 guid, - uint16 index, - uint256 gas, - uint256 value, - bytes message, - bytes extraData, - bytes reason - ); - - function composeQueue( - address _from, - address _to, - bytes32 _guid, - uint16 _index - ) external view returns (bytes32 messageHash); - - function sendCompose(address _to, bytes32 _guid, uint16 _index, bytes calldata _message) external; - - function lzCompose( - address _from, - address _to, - bytes32 _guid, - uint16 _index, - bytes calldata _message, - bytes calldata _extraData - ) external payable; -} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IMessagingContext.sol b/src/apps/layerzero/interfaces/IMessagingContext.sol deleted file mode 100644 index a7aaac9..0000000 --- a/src/apps/layerzero/interfaces/IMessagingContext.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.0; - -interface IMessagingContext { - function isSendingMessage() external view returns (bool); - - function getSendContext() external view returns (uint32 dstEid, address sender); -} \ No newline at end of file diff --git a/src/apps/layerzero/interfaces/IPacket.sol b/src/apps/layerzero/interfaces/IPacket.sol deleted file mode 100644 index cca1c69..0000000 --- a/src/apps/layerzero/interfaces/IPacket.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.0; - -struct Packet { - uint64 nonce; - uint32 srcEid; - address sender; - uint32 dstEid; - bytes32 receiver; - bytes32 guid; - bytes message; -} diff --git a/src/apps/layerzero/libs/AddressCast.sol b/src/apps/layerzero/libs/AddressCast.sol deleted file mode 100644 index e82a76b..0000000 --- a/src/apps/layerzero/libs/AddressCast.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: LZBL-1.2 - -pragma solidity ^0.8.13; - -library AddressCast { - error AddressCast_InvalidSizeForAddress(); - error AddressCast_InvalidAddress(); - - function toBytes32(bytes calldata _addressBytes) internal pure returns (bytes32 result) { - if (_addressBytes.length > 32) revert AddressCast_InvalidAddress(); - result = bytes32(_addressBytes); - unchecked { - uint256 offset = 32 - _addressBytes.length; - result = result >> (offset * 8); - } - } - - function toBytes32(address _address) internal pure returns (bytes32 result) { - result = bytes32(uint256(uint160(_address))); - } - - function toBytes(bytes32 _addressBytes32, uint256 _size) internal pure returns (bytes memory result) { - if (_size == 0 || _size > 32) revert AddressCast_InvalidSizeForAddress(); - result = new bytes(_size); - unchecked { - uint256 offset = 256 - _size * 8; - assembly { - mstore(add(result, 32), shl(offset, _addressBytes32)) - } - } - } - - function toAddress(bytes32 _addressBytes32) internal pure returns (address result) { - result = address(uint160(uint256(_addressBytes32))); - } - - function toAddress(bytes calldata _addressBytes) internal pure returns (address result) { - if (_addressBytes.length != 20) revert AddressCast_InvalidAddress(); - result = address(bytes20(_addressBytes)); - } -} \ No newline at end of file diff --git a/src/apps/layerzero/libs/PacketV1Codec.sol b/src/apps/layerzero/libs/PacketV1Codec.sol deleted file mode 100644 index 750973f..0000000 --- a/src/apps/layerzero/libs/PacketV1Codec.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: LZBL-1.2 - -pragma solidity ^0.8.13; - -import { Packet } from "../interfaces/IPacket.sol"; -import { AddressCast } from "./AddressCast.sol"; - -library PacketV1Codec { - using AddressCast for address; - using AddressCast for bytes32; - - uint8 internal constant PACKET_VERSION = 1; - - // header (version + nonce + path) - // version - uint256 private constant PACKET_VERSION_OFFSET = 0; - // nonce - uint256 private constant NONCE_OFFSET = 1; - // path - uint256 private constant SRC_EID_OFFSET = 9; - uint256 private constant SENDER_OFFSET = 13; - uint256 private constant DST_EID_OFFSET = 45; - uint256 private constant RECEIVER_OFFSET = 49; - // payload (guid + message) - uint256 private constant GUID_OFFSET = 81; // keccak256(nonce + path) - uint256 private constant MESSAGE_OFFSET = 113; - - function encode(Packet memory _packet) internal pure returns (bytes memory encodedPacket) { - encodedPacket = abi.encodePacked( - PACKET_VERSION, - _packet.nonce, - _packet.srcEid, - _packet.sender.toBytes32(), - _packet.dstEid, - _packet.receiver, - _packet.guid, - _packet.message - ); - } - - function encodePacketHeader(Packet memory _packet) internal pure returns (bytes memory) { - return - abi.encodePacked( - PACKET_VERSION, - _packet.nonce, - _packet.srcEid, - _packet.sender.toBytes32(), - _packet.dstEid, - _packet.receiver - ); - } - - function encodePayload(Packet memory _packet) internal pure returns (bytes memory) { - return abi.encodePacked(_packet.guid, _packet.message); - } - - function header(bytes calldata _packet) internal pure returns (bytes calldata) { - return _packet[0:GUID_OFFSET]; - } - - function version(bytes calldata _packet) internal pure returns (uint8) { - return uint8(bytes1(_packet[PACKET_VERSION_OFFSET:NONCE_OFFSET])); - } - - function nonce(bytes calldata _packet) internal pure returns (uint64) { - return uint64(bytes8(_packet[NONCE_OFFSET:SRC_EID_OFFSET])); - } - - function srcEid(bytes calldata _packet) internal pure returns (uint32) { - return uint32(bytes4(_packet[SRC_EID_OFFSET:SENDER_OFFSET])); - } - - function sender(bytes calldata _packet) internal pure returns (bytes32) { - return bytes32(_packet[SENDER_OFFSET:DST_EID_OFFSET]); - } - - function senderAddressB20(bytes calldata _packet) internal pure returns (address) { - return sender(_packet).toAddress(); - } - - function dstEid(bytes calldata _packet) internal pure returns (uint32) { - return uint32(bytes4(_packet[DST_EID_OFFSET:RECEIVER_OFFSET])); - } - - function receiver(bytes calldata _packet) internal pure returns (bytes32) { - return bytes32(_packet[RECEIVER_OFFSET:GUID_OFFSET]); - } - - function receiverB20(bytes calldata _packet) internal pure returns (address) { - return receiver(_packet).toAddress(); - } - - function guid(bytes calldata _packet) internal pure returns (bytes32) { - return bytes32(_packet[GUID_OFFSET:MESSAGE_OFFSET]); - } - - function message(bytes calldata _packet) internal pure returns (bytes calldata) { - return bytes(_packet[MESSAGE_OFFSET:]); - } - - function payload(bytes calldata _packet) internal pure returns (bytes calldata) { - return bytes(_packet[GUID_OFFSET:]); - } - - function payloadHash(bytes calldata _packet) internal pure returns (bytes32) { - return keccak256(payload(_packet)); - } -} \ No newline at end of file diff --git a/test/layerzero/LZCommon.sol b/test/layerzero/LZCommon.sol index d445598..cf729ad 100644 --- a/test/layerzero/LZCommon.sol +++ b/test/layerzero/LZCommon.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import { MockLayerZeroEscrow } from "./mock/MockLayerZeroEscrow.sol"; +import { EndpointV2 } from "LayerZero-v2/protocol/contracts/EndpointV2.sol"; contract TestLzCommon is Test { From 4f79b65a80c7a6c61e25bf0c30f5da2e49cdb324 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 17/61] test: prepare testing suite for lz --- remappings.txt | 3 ++- test/layerzero/LZCommon.sol | 33 +++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/remappings.txt b/remappings.txt index 76c4037..81226a2 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,5 @@ forge-std/=lib/forge-std/src/ openzeppelin/=lib/openzeppelin-contracts/contracts/ vibc-core-smart-contracts/=lib/vibc-core-smart-contracts/contracts/ -@openzeppelin/=lib/openzeppelin-contracts/ \ No newline at end of file +@openzeppelin/=lib/openzeppelin-contracts/ +@layerzerolabs/lz-evm-protocol-v2/=lib/LayerZero-v2/protocol/ \ No newline at end of file diff --git a/test/layerzero/LZCommon.sol b/test/layerzero/LZCommon.sol index cf729ad..fac233a 100644 --- a/test/layerzero/LZCommon.sol +++ b/test/layerzero/LZCommon.sol @@ -1,22 +1,43 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +import { EndpointV2 } from "LayerZero-v2/protocol/contracts/EndpointV2.sol"; +import { SimpleMessageLib } from "LayerZero-v2/protocol/contracts/messagelib/SimpleMessageLib.sol"; +import { ReceiveUln302 } from "LayerZero-v2/messagelib/contracts/uln/uln302/ReceiveUln302.sol"; + import "forge-std/Test.sol"; import { MockLayerZeroEscrow } from "./mock/MockLayerZeroEscrow.sol"; -import { EndpointV2 } from "LayerZero-v2/protocol/contracts/EndpointV2.sol"; contract TestLzCommon is Test { + uint32 internal localEid; + uint32 internal remoteEid; + EndpointV2 internal endpoint; + SimpleMessageLib internal simpleMsgLib; + ReceiveUln302 ULN; MockLayerZeroEscrow mockLayerZeroEscrow; address SEND_LOST_GAS_TO = address(uint160(0xdead)); - address lzEndpointV2; - address ULN; function setUp() virtual public { + localEid = 1; + remoteEid = 2; + + endpoint = new EndpointV2(localEid, address(this)); + ULN = new ReceiveUln302(address(endpoint)); + + SimpleMessageLib msgLib = new SimpleMessageLib(address(endpoint), address(0)); + + // register msg lib + endpoint.registerLibrary(address(msgLib)); + + // Set default libs + endpoint.setDefaultSendLibrary(remoteEid, address(msgLib)); + endpoint.setDefaultReceiveLibrary(remoteEid, address(msgLib), 0); + + + // Setup our mock escrow + mockLayerZeroEscrow = new MockLayerZeroEscrow(SEND_LOST_GAS_TO, address(endpoint), address(ULN)); - lzEndpointV2 = address(0); - ULN = address(0); - mockLayerZeroEscrow = new MockLayerZeroEscrow(SEND_LOST_GAS_TO, lzEndpointV2, ULN); } } From 4e8cfba422af11e8688b54565af966e3cc594443 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 18/61] test: expose allowExternalCall --- test/layerzero/mock/MockLayerZeroEscrow.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/layerzero/mock/MockLayerZeroEscrow.sol b/test/layerzero/mock/MockLayerZeroEscrow.sol index 5ccaeb2..47912ad 100644 --- a/test/layerzero/mock/MockLayerZeroEscrow.sol +++ b/test/layerzero/mock/MockLayerZeroEscrow.sol @@ -17,4 +17,8 @@ contract MockLayerZeroEscrow is IncentivizedLayerZeroEscrow { function sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) external returns(uint128 costOfsendPacketInNativeToken) { return _sendPacket(destinationChainIdentifier, destinationImplementation, message); } + + function setAllowExternalCall(bool state) { + allowExternalCall = state; + } } \ No newline at end of file From e1f5ce89759aac8ecc5c314bf826af78e9ff8c56 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 19/61] fix: allow contracts to be compiled --- test/layerzero/mock/MockLayerZeroEscrow.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/layerzero/mock/MockLayerZeroEscrow.sol b/test/layerzero/mock/MockLayerZeroEscrow.sol index 47912ad..5e487d2 100644 --- a/test/layerzero/mock/MockLayerZeroEscrow.sol +++ b/test/layerzero/mock/MockLayerZeroEscrow.sol @@ -18,7 +18,7 @@ contract MockLayerZeroEscrow is IncentivizedLayerZeroEscrow { return _sendPacket(destinationChainIdentifier, destinationImplementation, message); } - function setAllowExternalCall(bool state) { + function setAllowExternalCall(bool state) external { allowExternalCall = state; } } \ No newline at end of file From 98998c8ccba6a04c569af61fcfa6d4a10bbb3e8f Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 20/61] feat: improve LZ refund --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 3e20977..6b8b1af 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -110,24 +110,22 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { // We are getting a refund on any excess value we sent. Since that refund is // coming before the end of this call, we can record it. allowExternalCall = true; - ENDPOINT.send{value: msg.value}( + MessagingReceipt memory receipt = ENDPOINT.send{value: msg.value}( params, address(this) ); + allowExternalCall = false; // Set the cost of the sendPacket to msg.value - costOfsendPacketInNativeToken = uint128(msg.value - (excessPaid - 1)); - excessPaid = 1; + costOfsendPacketInNativeToken = uint128(msg.value - receipt.fee.nativeFee); return costOfsendPacketInNativeToken; } - // Record refunds coming in. - // Ideally, disallow randoms from sending to this contract but that wou + // Allow LZ refunds to come in while disallowing randoms from sending to this contract. + // It won't stop abuses but it is the best we can do. receive() external payable { - // TODO: Do we have enough gas for this? I hope we have since allowExternalCall is warm. + // allowExternalCall is hot so it shouldn't be that expensive. require(allowExternalCall, "Do not send ether to this address"); - excessPaid = uint128(1 + msg.value); - allowExternalCall = false; } From 44cee2db56fdaa9757663bc88e75e10485c8a045 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 21/61] doc: questions for LZ working --- src/IncentivizedMessageEscrow.sol | 3 +- .../layerzero/IncentivizedLayerZeroEscrow.sol | 66 +++++++++++++------ test/layerzero/mock/MockLayerZeroEscrow.sol | 2 +- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/IncentivizedMessageEscrow.sol b/src/IncentivizedMessageEscrow.sol index 5bc82b2..8024a6d 100644 --- a/src/IncentivizedMessageEscrow.sol +++ b/src/IncentivizedMessageEscrow.sol @@ -255,7 +255,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes return _messageDelivered[sourceIdentifier][sourceImplementationIdentifier][messageIdentifier]; } - /** + /** * @notice Sets the escrow implementation for a specific chain * @dev This can only be set once. When set, it cannot be changed. * This is to protect relayers as this could be used to fail acks. @@ -419,6 +419,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes if (msg.value > sum) { // We know: msg.value > sum, thus msg.value - sum > 0. gasRefund = msg.value - sum; + // Send the refund to the refund address. Address.sendValue(payable(incentive.refundGasTo), uint256(gasRefund)); return (gasRefund, messageIdentifier); } diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 6b8b1af..7fc2d7c 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: DO-NOT-USE pragma solidity ^0.8.13; -import { ILayerZeroEndpointV2, MessagingParams, MessagingFee } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { ILayerZeroEndpointV2, MessagingParams, MessagingFee, MessagingReceipt } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { PacketV1Codec } from "LayerZero-v2/protocol/contracts/messagelib/libs/PacketV1Codec.sol"; import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; @@ -10,17 +10,18 @@ import { IReceiveUlnBase, UlnConfig, Verification } from "./interfaces/IUlnBase. /** * @notice LayerZero escrow. - * TODO: Set config such that we are the executor. */ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { using PacketV1Codec for bytes; error LayerZeroCannotBeAddress0(); + error IncorrectDestination(address actual); + + // Errors inherited from LZ. error LZ_ULN_Verifying(); error LZ_ULN_InvalidPacketHeader(); error LZ_ULN_InvalidPacketVersion(); error LZ_ULN_InvalidEid(); - error IncorrectDestination(address actual); // Layer Zero associated addresses ILayerZeroEndpointV2 immutable ENDPOINT; @@ -29,10 +30,8 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { // chainid is immutable on LayerZero endpoint, so we read it and store it likewise. uint32 public immutable chainId; - /// @notice How much extra did we sent to LZ? - uint128 excessPaid = 1; // Set to 1 so we never have to pay zero to non-zero cost. /// @notice Only allow LZ to send - bool allowExternalCall = false; + uint8 allowExternalCall = 1; constructor(address sendLostGasTo, address lzEndpointV2, address ULN) IncentivizedMessageEscrow(sendLostGasTo) { @@ -40,14 +39,28 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { ENDPOINT = ILayerZeroEndpointV2(lzEndpointV2); chainId = ENDPOINT.eid(); ULTRA_LIGHT_NODE = IReceiveUlnBase(ULN); - // TODO: Set executor as this contract. + + // uint256 srcEID = 0; + // ENDPOINT.setReceiveLibrary(address(this), srcEID, address(this), 0); } - // TODO: Fix + // function allowInitializePath(Origin calldata /* _origin */) external pure returns(bool) { + // return false; + // } + + // INTERNAL: We might have to update this ABI to take into consideration where the message is going + /** + * TODO: Can we set ourself as the executor? + * We want to do this because the executor is also paid for when we send the message + * However, this incentive scheme is designed to act as its own incentive model and as such + * we don't need to paid for another set for relayers. So: Can we set ourself as the exector + * and will the DVNs continue to be paid and deliver their "proofs/commit" to the destination chain + * for us to use when calling verifiable? + */ function estimateAdditionalCost() external view returns(address asset, uint256 amount) { MessagingParams memory params = MessagingParams({ - dstEid: uint32(uint256(0)), // TODO: Fix - receiver: bytes32(0), // TODO: FIX + dstEid: uint32(uint256(0)), // INTERNAL: figure out a replacement. + receiver: bytes32(0), // INTERNAL: figure out a replacement. message: hex"", options: hex"", payInLzToken: false @@ -82,13 +95,28 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { // Get the source chain. uint32 srcEid = _packetHeader.srcEid(); - bytes32 _headerHash = keccak256(_packetHeader); bytes32 _payloadHash = _packet.payloadHash(); UlnConfig memory _config = ULTRA_LIGHT_NODE.getUlnConfig(address(this), srcEid); - if (ULTRA_LIGHT_NODE.verifiable(_config, _headerHash, _payloadHash)) revert LZ_ULN_Verifying(); - // Get the sourec chain + + // TODO: Is calling `verifiable` okay or do we need to call `commitVerification`? + // 1. `commitVerification` has a lot of side effects compared to just calling `verifyable`. + // most significantly, it also changes a zero storage slot to non-zero thus is `kind of` expensive. + // Is the deletion of commits worth it gas wise? + // + // 2. Can we block `commitVerification` from being called? verifiable stop returning true whenever + // `commitVerification` is called. From a quick look at the contracts, there are 2 ways to block the call. + // 2.1: `isValidReceiveLibrary`. That function can be made to call another contract, which could be this one + // We can then force the function to fail making `commitVerification` fail and make sure it can never be called. + // 2.2: `_initializable` is called which eventually makes a call to ILayerZeroReceiver(_receiver).allowInitializePath(_origin); + // We can easily expose allowInitializePath and return false. Thus blocking that call. Is it doable? + // + // 3. In case everything breaks, should we also check against the "verified" proof on the endpoint? + // That will be set after someone calls `commitVerification` and it doesn't revert. + if (!ULTRA_LIGHT_NODE.verifiable(_config, _headerHash, _payloadHash)) revert LZ_ULN_Verifying(); + + // Get the source chain sourceIdentifier = bytes32(uint256(srcEid)); // Get the sender implementationIdentifier = abi.encode(_packetHeader.sender()); @@ -107,14 +135,14 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { }); // Handoff package to LZ. - // We are getting a refund on any excess value we sent. Since that refund is - // coming before the end of this call, we can record it. - allowExternalCall = true; + // We are getting a refund on any excess value we sent. We can get the natice fee by subtracting it from + // the value we sent. + allowExternalCall = 2; MessagingReceipt memory receipt = ENDPOINT.send{value: msg.value}( params, address(this) ); - allowExternalCall = false; + allowExternalCall = 1; // Set the cost of the sendPacket to msg.value costOfsendPacketInNativeToken = uint128(msg.value - receipt.fee.nativeFee); @@ -124,8 +152,8 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { // Allow LZ refunds to come in while disallowing randoms from sending to this contract. // It won't stop abuses but it is the best we can do. receive() external payable { - // allowExternalCall is hot so it shouldn't be that expensive. - require(allowExternalCall, "Do not send ether to this address"); + // allowExternalCall is hot so it shouldn't be that expensive to read. + require(allowExternalCall != 1, "Do not send ether to this address"); } diff --git a/test/layerzero/mock/MockLayerZeroEscrow.sol b/test/layerzero/mock/MockLayerZeroEscrow.sol index 5e487d2..f7313e6 100644 --- a/test/layerzero/mock/MockLayerZeroEscrow.sol +++ b/test/layerzero/mock/MockLayerZeroEscrow.sol @@ -19,6 +19,6 @@ contract MockLayerZeroEscrow is IncentivizedLayerZeroEscrow { } function setAllowExternalCall(bool state) external { - allowExternalCall = state; + allowExternalCall = state ? 2 : 1; } } \ No newline at end of file From 34e2d7828eefee971bef4f35aa9c2dac52dd36b9 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 22/61] feat: setallowInitializePath --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 7fc2d7c..cfcb2ee 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -44,9 +44,15 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { // ENDPOINT.setReceiveLibrary(address(this), srcEID, address(this), 0); } - // function allowInitializePath(Origin calldata /* _origin */) external pure returns(bool) { - // return false; - // } + /** + * @notice Block any calls from the LZ endpoint so that no messages can ever get "verified" on the endpoint. + * This is very important, as otherwise, the package status can progress on the LZ endpoint which causes + * `verifiyable` which we rely on to be able to switch from true to false by commiting the proof to the endpoint. + * While this function is not intended for this use case, it should work. + */ + function allowInitializePath(Origin calldata /* _origin */) external pure returns(bool) { + return false; + } // INTERNAL: We might have to update this ABI to take into consideration where the message is going /** From 397791072c42f0b694c6088ff97af351d5b30812 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 23/61] feat: set configs of LZ --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 108 +++++++++++------- test/layerzero/mock/MockLayerZeroEscrow.sol | 4 +- 2 files changed, 71 insertions(+), 41 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index cfcb2ee..5158877 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: DO-NOT-USE pragma solidity ^0.8.13; -import { ILayerZeroEndpointV2, MessagingParams, MessagingFee, MessagingReceipt } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { ILayerZeroEndpointV2, MessagingParams, MessagingFee, MessagingReceipt, Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { IMessageLibManager, SetConfigParam } from "LayerZero-v2/protocol/contracts/interfaces/IMessageLibManager.sol"; import { PacketV1Codec } from "LayerZero-v2/protocol/contracts/messagelib/libs/PacketV1Codec.sol"; import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; @@ -13,6 +14,13 @@ import { IReceiveUlnBase, UlnConfig, Verification } from "./interfaces/IUlnBase. */ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { using PacketV1Codec for bytes; + uint32 CONFIG_TYPE_EXECUTOR = 1; + uint32 MAX_MESSAGE_SIZE = 4096; + + struct ConfigTypeExecutor { + uint32 maxMessageSize; + address executorAddress; + } error LayerZeroCannotBeAddress0(); error IncorrectDestination(address actual); @@ -34,14 +42,58 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { uint8 allowExternalCall = 1; + /** + * @param sendLostGasTo Address to get gas that could not get sent to the recipitent. + * @param lzEndpointV2 LayerZero endpount. Is used for sending messages. + * @param ULN LayerZero Ultra Light Node. Used for verifying messages. + */ constructor(address sendLostGasTo, address lzEndpointV2, address ULN) IncentivizedMessageEscrow(sendLostGasTo) { - if (lzEndpointV2 == address(0)) revert LayerZeroCannotBeAddress0(); + if (lzEndpointV2 == address(0) || ULN == address(0)) revert LayerZeroCannotBeAddress0(); + + // Load the LZ endpoint. This is the contract we will be sending events to. ENDPOINT = ILayerZeroEndpointV2(lzEndpointV2); + // Set chainId. chainId = ENDPOINT.eid(); + // Set the ultra light node. This is the contract we will be verifying packages against. ULTRA_LIGHT_NODE = IReceiveUlnBase(ULN); + } + + function _uniqueSourceIdentifier() override internal view returns(bytes32) { + return bytes32(uint256(chainId)); + } - // uint256 srcEID = 0; - // ENDPOINT.setReceiveLibrary(address(this), srcEID, address(this), 0); + function _proofValidPeriod(bytes32 destinationIdentifier) override internal pure returns(uint64 timestamp) { + return 0; + } + + /** + * @notice Set ourself as executor on all (provided) remote chains. This is required before we anyone + * can send message out of that chain. + * @dev sendLibrary is not checked. It is assumed that any endpoint will accept anything as long as it is somewhat sane. + * @param sendLibrary Contract to set config on. + * @param remoteEids List of remote Eids to set config on. + */ + function initConfig(address sendLibrary, uint32[] calldata remoteEids) external { + unchecked { + + bytes memory configExecutorBytes = abi.encode(ConfigTypeExecutor({ + maxMessageSize: MAX_MESSAGE_SIZE, + executorAddress: address(this) + })); + + uint256 numEids = remoteEids.length; + SetConfigParam[] memory params = new SetConfigParam[](numEids); + for (uint256 i = 0; i < numEids; ++i) { + SetConfigParam memory configParam = SetConfigParam({ + eid: remoteEids[i], + configType: CONFIG_TYPE_EXECUTOR, + config: configExecutorBytes + }); + params[i] = configParam; + } + ENDPOINT.setConfig(address(this), sendLibrary, params); + + } } /** @@ -54,7 +106,12 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { return false; } - // INTERNAL: We might have to update this ABI to take into consideration where the message is going + // TODO: load interface and correct implement then return 0 regardless of parameters. + function getFee() external pure returns(uint256 fee) { + return fee = 0; + } + + // TODO:: We might have to update this ABI to take into consideration where the message is going /** * TODO: Can we set ourself as the executor? * We want to do this because the executor is also paid for when we send the message @@ -65,8 +122,8 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { */ function estimateAdditionalCost() external view returns(address asset, uint256 amount) { MessagingParams memory params = MessagingParams({ - dstEid: uint32(uint256(0)), // INTERNAL: figure out a replacement. - receiver: bytes32(0), // INTERNAL: figure out a replacement. + dstEid: uint32(uint256(0)), // TODO:: figure out a replacement. + receiver: bytes32(0), // TODO:: figure out a replacement. message: hex"", options: hex"", payInLzToken: false @@ -76,27 +133,12 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { asset = address(0); } - function _getMessageIdentifier( - bytes32 destinationIdentifier, - bytes calldata message - ) internal override view returns(bytes32) { - return keccak256( - abi.encodePacked( - msg.sender, - bytes32(block.number), - chainId, - destinationIdentifier, - message - ) - ); - } - function _verifyPacket(bytes calldata _packetHeader, bytes calldata _packet) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { _assertHeader(_packetHeader); // Check that we are the receiver address receiver = _packetHeader.receiverB20(); - if(receiver != address(this)) revert IncorrectDestination(receiver); + if (receiver != address(this)) revert IncorrectDestination(receiver); // Get the source chain. uint32 srcEid = _packetHeader.srcEid(); @@ -105,21 +147,9 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { bytes32 _payloadHash = _packet.payloadHash(); UlnConfig memory _config = ULTRA_LIGHT_NODE.getUlnConfig(address(this), srcEid); - - // TODO: Is calling `verifiable` okay or do we need to call `commitVerification`? - // 1. `commitVerification` has a lot of side effects compared to just calling `verifyable`. - // most significantly, it also changes a zero storage slot to non-zero thus is `kind of` expensive. - // Is the deletion of commits worth it gas wise? - // - // 2. Can we block `commitVerification` from being called? verifiable stop returning true whenever - // `commitVerification` is called. From a quick look at the contracts, there are 2 ways to block the call. - // 2.1: `isValidReceiveLibrary`. That function can be made to call another contract, which could be this one - // We can then force the function to fail making `commitVerification` fail and make sure it can never be called. - // 2.2: `_initializable` is called which eventually makes a call to ILayerZeroReceiver(_receiver).allowInitializePath(_origin); - // We can easily expose allowInitializePath and return false. Thus blocking that call. Is it doable? - // - // 3. In case everything breaks, should we also check against the "verified" proof on the endpoint? - // That will be set after someone calls `commitVerification` and it doesn't revert. + // Verify the message on the LZ ultra light node. + // Note that this can could technically be DoS except that allowInitializePath returning false denies this DoS + // vector. As a result, this should always return true and can never turn false. if (!ULTRA_LIGHT_NODE.verifiable(_config, _headerHash, _payloadHash)) revert LZ_ULN_Verifying(); // Get the source chain @@ -130,7 +160,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { message_ = _packet.message(); } - function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) internal override returns(uint128 costOfsendPacketInNativeToken) { + function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message, uint64 deadline) internal override returns(uint128 costOfsendPacketInNativeToken) { MessagingParams memory params = MessagingParams({ dstEid: uint32(uint256(destinationChainIdentifier)), diff --git a/test/layerzero/mock/MockLayerZeroEscrow.sol b/test/layerzero/mock/MockLayerZeroEscrow.sol index f7313e6..fd15dd2 100644 --- a/test/layerzero/mock/MockLayerZeroEscrow.sol +++ b/test/layerzero/mock/MockLayerZeroEscrow.sol @@ -14,8 +14,8 @@ contract MockLayerZeroEscrow is IncentivizedLayerZeroEscrow { return _verifyPacket(_packetHeader, _packet); } - function sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message) external returns(uint128 costOfsendPacketInNativeToken) { - return _sendPacket(destinationChainIdentifier, destinationImplementation, message); + function sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message, uint64 deadline) external returns(uint128 costOfsendPacketInNativeToken) { + return _sendPacket(destinationChainIdentifier, destinationImplementation, message, deadline); } function setAllowExternalCall(bool state) external { From 8352e1252b7ee2960c27429222e45473bea96d6f Mon Sep 17 00:00:00 2001 From: ajimeno04 Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 24/61] Modify bridge_contracts --- script/bridge_contracts.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/script/bridge_contracts.json b/script/bridge_contracts.json index 1404547..13c33e7 100644 --- a/script/bridge_contracts.json +++ b/script/bridge_contracts.json @@ -46,5 +46,22 @@ "bridge": "0xE2029629f51ab994210d671Dc08b7Ec94899b278", "escrow": "0x87AE7bC6B565E545bDD51788C43BF9E5cbB72EBD" } + }, + "LayerZero": { + "basesepolia": { + "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", + "escrow": "0x794d16F3a32FF13E1734D245f1c4C378622a53a5", + "ULN": "0x9eCf72299027e8AeFee5DC5351D6d92294F46d2b" + }, + "optimismsepolia": { + "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", + "escrow": "0xe2C449C0f8b0bF55655e4E92299d9d1c27bDE585", + "ULN": "0x420667429538adBF982aDa16C268ba561f097F74" + }, + "blasttestnet": { + "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", + "escrow": "0xe2FD2FEB02DD96eF1D3c0a8823a38C3005fDCB15", + "ULN": "0x8dF53a660a00C3D977d7E778fB7385ECf4482D16" + } } } \ No newline at end of file From b2327bc7333b0d64206dd101f26d53a2f4d9a1a6 Mon Sep 17 00:00:00 2001 From: ajimeno04 Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 25/61] modify .env.example --- .env.example | 3 ++- script/Deploy.sol | 44 +++++++++++++++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 02a7125..1dee661 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,5 @@ mumbai= sepolia= basesepolia= arbitrumsepolia= -optimismsepolia= \ No newline at end of file +optimismsepolia= +basttestnet= \ No newline at end of file diff --git a/script/Deploy.sol b/script/Deploy.sol index 5ae3a76..1ea2cb1 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -1,15 +1,7 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.22; - -import "forge-std/Script.sol"; -import {stdJson} from "forge-std/StdJson.sol"; - -import { BaseMultiChainDeployer} from "./BaseMultiChainDeployer.s.sol"; - -// Import all the Apps for deployment here. import { IncentivizedMockEscrow } from "../src/apps/mock/IncentivizedMockEscrow.sol"; import { IncentivizedWormholeEscrow } from "../src/apps/wormhole/IncentivizedWormholeEscrow.sol"; import { IncentivizedPolymerEscrow } from "../src/apps/polymer/vIBCEscrow.sol"; +import { IncentivizedLayerZeroEscrow } from "../src/apps/layerzero/IncentivizedLayerZeroEscrow.sol"; contract DeployGeneralisedIncentives is BaseMultiChainDeployer { using stdJson for string; @@ -28,6 +20,9 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { // define a list of AMB mappings so we can get their addresses. mapping(string => mapping(string => address)) bridgeContract; + // mapping to store ULN addresses + mapping(string => mapping(string => address)) ulnContract; + constructor() { // Here we can define input salts. These are always assumed to be dependent on the secondary argument @@ -93,6 +88,26 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { IncentivizedPolymerEscrow polymerEscrow = new IncentivizedPolymerEscrow{salt: salt}(vm.envAddress("SEND_LOST_GAS_TO"), polymerBridgeContract); incentive = address(polymerEscrow); + + } else if (versionHash == keccak256(abi.encodePacked("LayerZero"))) { + address layerZeroBridgeContract = bridgeContract[version][currentChainKey]; + address ulnAddress = ulnContract[version][currentChainKey]; + bytes32 salt = deploySalts[layerZeroBridgeContract]; + require(layerZeroBridgeContract != address(0), "bridge cannot be address(0)"); + require(ulnAddress != address(0), "ULN cannot be address(0)"); + + address expectedAddress = _getAddress( + abi.encodePacked( + type(IncentivizedLayerZeroEscrow).creationCode, + abi.encode(vm.envAddress("SEND_LOST_GAS_TO"), layerZeroBridgeContract, ulnAddress) + ), + salt + ); + + if (expectedAddress.codehash != bytes32(0)) return expectedAddress; + + IncentivizedLayerZeroEscrow layerZeroEscrow = new IncentivizedLayerZeroEscrow{salt: salt}(vm.envAddress("SEND_LOST_GAS_TO"), layerZeroBridgeContract, ulnAddress); + incentive = address(layerZeroEscrow); } else { revert IncentivesVersionNotFound(); } @@ -119,13 +134,20 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { // For each bridge, decode their contracts for each chain. for (uint256 i = 0; i < availableBridges.length; ++i) { string memory bridge = availableBridges[i]; - // Get the chains this bridge support. + // Get the chains this bridge supports. string[] memory availableBridgesChains = vm.parseJsonKeys(bridge_config, string.concat(".", bridge)); for (uint256 j = 0; j < availableBridgesChains.length; ++j) { string memory chain = availableBridgesChains[j]; - // decode the address + // Decode the address address _bridgeContract = vm.parseJsonAddress(bridge_config, string.concat(".", bridge, ".", chain, ".bridge")); bridgeContract[bridge][chain] = _bridgeContract; + + // Check if the bridge is LayerZero + if (keccak256(abi.encodePacked(bridge)) == keccak256(abi.encodePacked("LayerZero"))) { + // Read the ULN address + address ulnAddress = vm.parseJsonAddress(bridge_config, string.concat(".", bridge, ".", chain, ".ULN")); + ulnContract[bridge][chain] = ulnAddress; + } } } From 5e37aa87ea96f3d1909eb33865f5148749ab8eb4 Mon Sep 17 00:00:00 2001 From: ajimeno04 Date: Mon, 27 May 2024 15:10:26 +0200 Subject: [PATCH 26/61] update deploy script --- script/Deploy.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/script/Deploy.sol b/script/Deploy.sol index 1ea2cb1..9c1c1d0 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -1,3 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import { BaseMultiChainDeployer} from "./BaseMultiChainDeployer.s.sol"; + +// Import all the Apps for deployment here. import { IncentivizedMockEscrow } from "../src/apps/mock/IncentivizedMockEscrow.sol"; import { IncentivizedWormholeEscrow } from "../src/apps/wormhole/IncentivizedWormholeEscrow.sol"; import { IncentivizedPolymerEscrow } from "../src/apps/polymer/vIBCEscrow.sol"; From 7e2df82a8435ca7c177abbb813e6696f6b915b61 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 17:26:26 +0200 Subject: [PATCH 27/61] feat: update much of the LZ implementation to better fit how LZ is intended to be used --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 104 ++++++++++++------ 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 5158877..55b9a4f 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.13; import { ILayerZeroEndpointV2, MessagingParams, MessagingFee, MessagingReceipt, Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { ILayerZeroExecutor } from "LayerZero-v2/messagelib/contracts/interfaces/ILayerZeroExecutor.sol"; import { IMessageLibManager, SetConfigParam } from "LayerZero-v2/protocol/contracts/interfaces/IMessageLibManager.sol"; import { PacketV1Codec } from "LayerZero-v2/protocol/contracts/messagelib/libs/PacketV1Codec.sol"; @@ -9,10 +10,34 @@ import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; import { UlnConfig } from "./interfaces/IUlnBase.sol"; import { IReceiveUlnBase, UlnConfig, Verification } from "./interfaces/IUlnBase.sol"; +/** + * @notice Always returns 0 to any job. + * @dev We have set ourself as the executor. As a result, we need to implement the executor interfaces. + */ +contract ExecutorZero is ILayerZeroExecutor { + function assignJob( + uint32 /* _dstEid */, + address /* _sender */, + uint256 /* _calldataSize */, + bytes calldata /* _options */ + ) external pure returns (uint256 price) { + return price = 0; + } + + function getFee( + uint32 /* _dstEid */, + address /* _sender */, + uint256 /* _calldataSize */, + bytes calldata /* _options */ + ) external pure returns (uint256 price) { + return price = 0; + } +} + /** * @notice LayerZero escrow. */ -contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { +contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero { using PacketV1Codec for bytes; uint32 CONFIG_TYPE_EXECUTOR = 1; uint32 MAX_MESSAGE_SIZE = 4096; @@ -22,6 +47,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { address executorAddress; } + // Errors specific to this contract. error LayerZeroCannotBeAddress0(); error IncorrectDestination(address actual); @@ -33,7 +59,6 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { // Layer Zero associated addresses ILayerZeroEndpointV2 immutable ENDPOINT; - IReceiveUlnBase immutable ULTRA_LIGHT_NODE; // chainid is immutable on LayerZero endpoint, so we read it and store it likewise. uint32 public immutable chainId; @@ -45,25 +70,22 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { /** * @param sendLostGasTo Address to get gas that could not get sent to the recipitent. * @param lzEndpointV2 LayerZero endpount. Is used for sending messages. - * @param ULN LayerZero Ultra Light Node. Used for verifying messages. */ - constructor(address sendLostGasTo, address lzEndpointV2, address ULN) IncentivizedMessageEscrow(sendLostGasTo) { - if (lzEndpointV2 == address(0) || ULN == address(0)) revert LayerZeroCannotBeAddress0(); + constructor(address sendLostGasTo, address lzEndpointV2) IncentivizedMessageEscrow(sendLostGasTo) { + if (lzEndpointV2 == address(0)) revert LayerZeroCannotBeAddress0(); // Load the LZ endpoint. This is the contract we will be sending events to. ENDPOINT = ILayerZeroEndpointV2(lzEndpointV2); // Set chainId. chainId = ENDPOINT.eid(); - // Set the ultra light node. This is the contract we will be verifying packages against. - ULTRA_LIGHT_NODE = IReceiveUlnBase(ULN); } function _uniqueSourceIdentifier() override internal view returns(bytes32) { return bytes32(uint256(chainId)); } - function _proofValidPeriod(bytes32 destinationIdentifier) override internal pure returns(uint64 timestamp) { - return 0; + function _proofValidPeriod(bytes32 /* destinationIdentifier */) override internal pure returns(uint64 timestamp) { + return 0; // TODO: Set to something like 1 month. } /** @@ -106,30 +128,34 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { return false; } - // TODO: load interface and correct implement then return 0 regardless of parameters. - function getFee() external pure returns(uint256 fee) { - return fee = 0; - } - - // TODO:: We might have to update this ABI to take into consideration where the message is going - /** - * TODO: Can we set ourself as the executor? - * We want to do this because the executor is also paid for when we send the message - * However, this incentive scheme is designed to act as its own incentive model and as such - * we don't need to paid for another set for relayers. So: Can we set ourself as the exector - * and will the DVNs continue to be paid and deliver their "proofs/commit" to the destination chain - * for us to use when calling verifiable? - */ - function estimateAdditionalCost() external view returns(address asset, uint256 amount) { + function _estimateAdditionalCost(uint32 destEid) view internal returns(uint256 amount) { MessagingParams memory params = MessagingParams({ - dstEid: uint32(uint256(0)), // TODO:: figure out a replacement. - receiver: bytes32(0), // TODO:: figure out a replacement. + dstEid: uint32(destEid), + receiver: bytes32(0), // Is unused by LZ. message: hex"", - options: hex"", + options: hex"", // TODO: Are these options important? payInLzToken: false }); + MessagingFee memory fee = ENDPOINT.quote(params, address(this)); amount = fee.nativeFee; + } + + /** + * @notice Get a very rough estimate of the additional cost to send a message. + * Layer Zero requires knowing the destination chain and that is not possible with this function signature. + * For a better quote, use the function overload. + */ + function estimateAdditionalCost() external view returns(address asset, uint256 amount) { + amount = _estimateAdditionalCost(chainId); + asset = address(0); + } + + /** + * @notice Get an exact quote. + */ + function estimateAdditionalCost(uint256 destinationChainId) external view returns(address asset, uint256 amount) { + amount = _estimateAdditionalCost(uint32(destinationChainId)); asset = address(0); } @@ -145,12 +171,24 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { bytes32 _headerHash = keccak256(_packetHeader); bytes32 _payloadHash = _packet.payloadHash(); - UlnConfig memory _config = ULTRA_LIGHT_NODE.getUlnConfig(address(this), srcEid); + + // The ULN may not be constant since it depends on the srcEid. :( + // We need to read the ULN from the endpoint. + IReceiveUlnBase ULN = IReceiveUlnBase(ENDPOINT.defaultReceiveLibrary(srcEid)); + + UlnConfig memory _config = ULN.getUlnConfig(address(this), srcEid); // Verify the message on the LZ ultra light node. - // Note that this can could technically be DoS except that allowInitializePath returning false denies this DoS - // vector. As a result, this should always return true and can never turn false. - if (!ULTRA_LIGHT_NODE.verifiable(_config, _headerHash, _payloadHash)) revert LZ_ULN_Verifying(); + // Without any protection, this is a DoS vector. It is protected by setting allowInitializePath to return false + // As a result, once this returns true it should return true perpetually. + bool verifyable = ULN.verifiable(_config, _headerHash, _payloadHash); + if (!verifyable) { + // LayerZero may have migrated to a new receive library. Check the timeout receive library. + (address timeoutULN, ) = ENDPOINT.defaultReceiveLibraryTimeout(srcEid); + ULN = IReceiveUlnBase(timeoutULN); + verifyable = ULN.verifiable(_config, _headerHash, _payloadHash); + if (!verifyable) revert LZ_ULN_Verifying(); + } // Get the source chain sourceIdentifier = bytes32(uint256(srcEid)); @@ -160,7 +198,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { message_ = _packet.message(); } - function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message, uint64 deadline) internal override returns(uint128 costOfsendPacketInNativeToken) { + function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message, uint64 /* deadline */) internal override returns(uint128 costOfsendPacketInNativeToken) { MessagingParams memory params = MessagingParams({ dstEid: uint32(uint256(destinationChainIdentifier)), @@ -171,7 +209,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow { }); // Handoff package to LZ. - // We are getting a refund on any excess value we sent. We can get the natice fee by subtracting it from + // We are getting a refund on any excess value we sent. We can get the native fee by subtracting it from // the value we sent. allowExternalCall = 2; MessagingReceipt memory receipt = ENDPOINT.send{value: msg.value}( From 258a1c631a4c1c611b74044c8c548dbb84eff03e Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 17:28:49 +0200 Subject: [PATCH 28/61] feat: update tests to match ULN removal --- script/Deploy.sol | 15 ++------------- test/layerzero/LZCommon.sol | 2 +- test/layerzero/mock/MockLayerZeroEscrow.sol | 2 +- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/script/Deploy.sol b/script/Deploy.sol index 9c1c1d0..b9820f1 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -29,8 +29,6 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { // define a list of AMB mappings so we can get their addresses. mapping(string => mapping(string => address)) bridgeContract; - // mapping to store ULN addresses - mapping(string => mapping(string => address)) ulnContract; constructor() { @@ -100,22 +98,20 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { } else if (versionHash == keccak256(abi.encodePacked("LayerZero"))) { address layerZeroBridgeContract = bridgeContract[version][currentChainKey]; - address ulnAddress = ulnContract[version][currentChainKey]; bytes32 salt = deploySalts[layerZeroBridgeContract]; require(layerZeroBridgeContract != address(0), "bridge cannot be address(0)"); - require(ulnAddress != address(0), "ULN cannot be address(0)"); address expectedAddress = _getAddress( abi.encodePacked( type(IncentivizedLayerZeroEscrow).creationCode, - abi.encode(vm.envAddress("SEND_LOST_GAS_TO"), layerZeroBridgeContract, ulnAddress) + abi.encode(vm.envAddress("SEND_LOST_GAS_TO"), layerZeroBridgeContract) ), salt ); if (expectedAddress.codehash != bytes32(0)) return expectedAddress; - IncentivizedLayerZeroEscrow layerZeroEscrow = new IncentivizedLayerZeroEscrow{salt: salt}(vm.envAddress("SEND_LOST_GAS_TO"), layerZeroBridgeContract, ulnAddress); + IncentivizedLayerZeroEscrow layerZeroEscrow = new IncentivizedLayerZeroEscrow{salt: salt}(vm.envAddress("SEND_LOST_GAS_TO"), layerZeroBridgeContract); incentive = address(layerZeroEscrow); } else { revert IncentivesVersionNotFound(); @@ -150,13 +146,6 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { // Decode the address address _bridgeContract = vm.parseJsonAddress(bridge_config, string.concat(".", bridge, ".", chain, ".bridge")); bridgeContract[bridge][chain] = _bridgeContract; - - // Check if the bridge is LayerZero - if (keccak256(abi.encodePacked(bridge)) == keccak256(abi.encodePacked("LayerZero"))) { - // Read the ULN address - address ulnAddress = vm.parseJsonAddress(bridge_config, string.concat(".", bridge, ".", chain, ".ULN")); - ulnContract[bridge][chain] = ulnAddress; - } } } diff --git a/test/layerzero/LZCommon.sol b/test/layerzero/LZCommon.sol index fac233a..686a26b 100644 --- a/test/layerzero/LZCommon.sol +++ b/test/layerzero/LZCommon.sol @@ -37,7 +37,7 @@ contract TestLzCommon is Test { // Setup our mock escrow - mockLayerZeroEscrow = new MockLayerZeroEscrow(SEND_LOST_GAS_TO, address(endpoint), address(ULN)); + mockLayerZeroEscrow = new MockLayerZeroEscrow(SEND_LOST_GAS_TO, address(endpoint)); } } diff --git a/test/layerzero/mock/MockLayerZeroEscrow.sol b/test/layerzero/mock/MockLayerZeroEscrow.sol index fd15dd2..e9df6ec 100644 --- a/test/layerzero/mock/MockLayerZeroEscrow.sol +++ b/test/layerzero/mock/MockLayerZeroEscrow.sol @@ -8,7 +8,7 @@ import { IncentivizedLayerZeroEscrow } from "../../../src/apps/layerzero/Incenti */ contract MockLayerZeroEscrow is IncentivizedLayerZeroEscrow { - constructor(address sendLostGasTo, address lzEndpointV2, address ULN) IncentivizedLayerZeroEscrow(sendLostGasTo, lzEndpointV2, ULN) {} + constructor(address sendLostGasTo, address lzEndpointV2) IncentivizedLayerZeroEscrow(sendLostGasTo, lzEndpointV2) {} function verifyPacket(bytes calldata _packetHeader, bytes calldata _packet) external view returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { return _verifyPacket(_packetHeader, _packet); From c112a30252be7b3d7347af0924fb60b32197a640 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 18:46:31 +0200 Subject: [PATCH 29/61] feat: set options to TYPE_3 only --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 55b9a4f..ee19136 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: DO-NOT-USE pragma solidity ^0.8.13; +import { OptionsBuilder } from "LayerZero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import { ILayerZeroEndpointV2, MessagingParams, MessagingFee, MessagingReceipt, Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { ILayerZeroExecutor } from "LayerZero-v2/messagelib/contracts/interfaces/ILayerZeroExecutor.sol"; import { IMessageLibManager, SetConfigParam } from "LayerZero-v2/protocol/contracts/interfaces/IMessageLibManager.sol"; @@ -57,19 +58,21 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero error LZ_ULN_InvalidPacketVersion(); error LZ_ULN_InvalidEid(); - // Layer Zero associated addresses + uint16 internal constant TYPE_3 = 3; + bytes constant LAYERZERO_OPTIONS = abi.encodePacked(TYPE_3); + + /** @notice The Layer Zero Endpoint. It is the destination for packages & configuration */ ILayerZeroEndpointV2 immutable ENDPOINT; - // chainid is immutable on LayerZero endpoint, so we read it and store it likewise. + /** @notice chainid is immutable on LayerZero endpoint, so we read it and store it likewise. */ uint32 public immutable chainId; - /// @notice Only allow LZ to send + /** @notice Only allow LZ to send value to this contract */ uint8 allowExternalCall = 1; - /** * @param sendLostGasTo Address to get gas that could not get sent to the recipitent. - * @param lzEndpointV2 LayerZero endpount. Is used for sending messages. + * @param lzEndpointV2 LayerZero endpount. It is used for sending messages. */ constructor(address sendLostGasTo, address lzEndpointV2) IncentivizedMessageEscrow(sendLostGasTo) { if (lzEndpointV2 == address(0)) revert LayerZeroCannotBeAddress0(); @@ -133,7 +136,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero dstEid: uint32(destEid), receiver: bytes32(0), // Is unused by LZ. message: hex"", - options: hex"", // TODO: Are these options important? + options: LAYERZERO_OPTIONS, // TODO: Are these options important? payInLzToken: false }); @@ -204,7 +207,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero dstEid: uint32(uint256(destinationChainIdentifier)), receiver: bytes32(destinationImplementation), message: message, - options: hex"", + options: LAYERZERO_OPTIONS, payInLzToken: false }); From 0196787850bd6218f2933433c06f74d670664c20 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 19:21:31 +0200 Subject: [PATCH 30/61] feat: documentation updates --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index ee19136..815826d 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: DO-NOT-USE pragma solidity ^0.8.13; -import { OptionsBuilder } from "LayerZero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import { ILayerZeroEndpointV2, MessagingParams, MessagingFee, MessagingReceipt, Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { ILayerZeroExecutor } from "LayerZero-v2/messagelib/contracts/interfaces/ILayerZeroExecutor.sol"; import { IMessageLibManager, SetConfigParam } from "LayerZero-v2/protocol/contracts/interfaces/IMessageLibManager.sol"; @@ -36,12 +35,38 @@ contract ExecutorZero is ILayerZeroExecutor { } /** - * @notice LayerZero escrow. + * @title Incentivized LayerZero Messag Escrow + * @notice Provides an alternative pathway to incentivize LayerZero message relaying. + * While Layer Zero has a native way to incentivize message relaying, it lacks: + * - Gas refunds of unspent gas. + * No gas refunds increase the cost of cross-chain messages by ~10% to ~20%. + * That is before accounting for the fact that the cross-chain gas prices are fixed + * and charged a margin on. + * + * - Payment conditional on execution. + * By not allowing anyone to claim messaging payment, the relaying incentive becomes + * a denial-of-service vector. If the relayer specified in the LZ config does not + * relay the message, it likely won't get relayed. It is even built directly into LZ + * that some relayers (the default included) may adjust their quotes depending on the + * application. While permissionwise, it is 1/N, the economic security is 1/1. + * + * @dev This contract only allows messages smaller than or equal to 65536 bytes to be sent. + * This implementation works by breaking the LZ endpoint flow. It relies on the + * `.verfiyable` check on the ULN. When a cross-chain message is verified (step 2) + * `commitVerification` is called and it deletes the storage for the verification: https://github.com/LayerZero-Labs/LayerZero-v2/blob/1fde89479fdc68b1a54cda7f19efa84483fcacc4/messagelib/contracts/uln/uln302/ReceiveUln302.sol#L56 + * this exactly `verfiyable: true -> false`. + * We break this making the subcall `EndpointV2::verify` revert on _initializable: + * https://github.com/LayerZero-Labs/LayerZero-v2/blob/1fde89479fdc68b1a54cda7f19efa84483fcacc4/protocol/contracts/EndpointV2.sol#L340 + * That is the purpose of `allowInitializePath`. + * + * Then we can use `verfiyable` to check if a message has been verified by DVNs. + * + * */ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero { using PacketV1Codec for bytes; uint32 CONFIG_TYPE_EXECUTOR = 1; - uint32 MAX_MESSAGE_SIZE = 4096; + uint32 MAX_MESSAGE_SIZE = 65536; struct ConfigTypeExecutor { uint32 maxMessageSize; From df38b56ce225c7c04839751d88b58f3733f3edf2 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 19:21:54 +0200 Subject: [PATCH 31/61] chore: remove unused comments --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 815826d..d0d483a 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -60,8 +60,6 @@ contract ExecutorZero is ILayerZeroExecutor { * That is the purpose of `allowInitializePath`. * * Then we can use `verfiyable` to check if a message has been verified by DVNs. - * - * */ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero { using PacketV1Codec for bytes; From 4ec0a69fa3629df4b226cba2478e03a0fd0c2e9e Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 21:55:32 +0200 Subject: [PATCH 32/61] fix: set LAYERZERO_OPTIONS to hex'' --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index d0d483a..9a2aff1 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -81,8 +81,11 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero error LZ_ULN_InvalidPacketVersion(); error LZ_ULN_InvalidEid(); - uint16 internal constant TYPE_3 = 3; - bytes constant LAYERZERO_OPTIONS = abi.encodePacked(TYPE_3); + /** + * @notice Set the LayerZero options to nothing. We are not using any special options + * and have set ourself to manage gas so we don't see special config. + */ + bytes constant LAYERZERO_OPTIONS = hex''; /** @notice The Layer Zero Endpoint. It is the destination for packages & configuration */ ILayerZeroEndpointV2 immutable ENDPOINT; @@ -265,4 +268,4 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero // assert the packet is for this endpoint if (_packetHeader.dstEid() != chainId) revert LZ_ULN_InvalidEid(); } -} \ No newline at end of file +} From 7d9c76e9870c50f6ecfb0830aa91c2f8044d56d7 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 22:08:20 +0200 Subject: [PATCH 33/61] Revert "fix: set LAYERZERO_OPTIONS to hex''" This reverts commit 4ec0a69fa3629df4b226cba2478e03a0fd0c2e9e. --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 9a2aff1..d0d483a 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -81,11 +81,8 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero error LZ_ULN_InvalidPacketVersion(); error LZ_ULN_InvalidEid(); - /** - * @notice Set the LayerZero options to nothing. We are not using any special options - * and have set ourself to manage gas so we don't see special config. - */ - bytes constant LAYERZERO_OPTIONS = hex''; + uint16 internal constant TYPE_3 = 3; + bytes constant LAYERZERO_OPTIONS = abi.encodePacked(TYPE_3); /** @notice The Layer Zero Endpoint. It is the destination for packages & configuration */ ILayerZeroEndpointV2 immutable ENDPOINT; @@ -268,4 +265,4 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero // assert the packet is for this endpoint if (_packetHeader.dstEid() != chainId) revert LZ_ULN_InvalidEid(); } -} +} \ No newline at end of file From 3fb4a4dfcfbdc13d933e024f67a847e72941b039 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 27 May 2024 22:09:19 +0200 Subject: [PATCH 34/61] feat: add comment regarding the minimum option length --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index d0d483a..9a95aba 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -82,6 +82,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero error LZ_ULN_InvalidEid(); uint16 internal constant TYPE_3 = 3; + /** @notice Set the LayerZero options. Needs to be 2 bytes with a version for the optionsSplit Library to process. */ bytes constant LAYERZERO_OPTIONS = abi.encodePacked(TYPE_3); /** @notice The Layer Zero Endpoint. It is the destination for packages & configuration */ From 805127631af6313b9a13381736d5c46e049e3f41 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 30 May 2024 14:21:02 +0200 Subject: [PATCH 35/61] feat: make test for LZ --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 3 ++- src/apps/layerzero/interfaces/IUlnBase.sol | 5 ++--- .../layerzero/{LZCommon.sol => LZCommon.t.sol} | 18 ++++++++++-------- test/layerzero/TestLZInitConfig.t.sol | 14 ++++++++++++++ test/layerzero/TestLZMisc.t.sol | 14 ++++++++++++++ test/layerzero/TestLZSendPacket.t.sol | 0 test/layerzero/TestLZVerifyPacket.t.sol | 0 7 files changed, 42 insertions(+), 12 deletions(-) rename test/layerzero/{LZCommon.sol => LZCommon.t.sol} (64%) create mode 100644 test/layerzero/TestLZInitConfig.t.sol create mode 100644 test/layerzero/TestLZMisc.t.sol create mode 100644 test/layerzero/TestLZSendPacket.t.sol create mode 100644 test/layerzero/TestLZVerifyPacket.t.sol diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 9a95aba..97e796a 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: DO-NOT-USE -pragma solidity ^0.8.13; +pragma solidity ^0.8.22; import { ILayerZeroEndpointV2, MessagingParams, MessagingFee, MessagingReceipt, Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { ILayerZeroExecutor } from "LayerZero-v2/messagelib/contracts/interfaces/ILayerZeroExecutor.sol"; @@ -121,6 +121,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero * @dev sendLibrary is not checked. It is assumed that any endpoint will accept anything as long as it is somewhat sane. * @param sendLibrary Contract to set config on. * @param remoteEids List of remote Eids to set config on. + // TODO: read sendLibrary from Endpoint maybe. */ function initConfig(address sendLibrary, uint32[] calldata remoteEids) external { unchecked { diff --git a/src/apps/layerzero/interfaces/IUlnBase.sol b/src/apps/layerzero/interfaces/IUlnBase.sol index 41411e0..0ecb06b 100644 --- a/src/apps/layerzero/interfaces/IUlnBase.sol +++ b/src/apps/layerzero/interfaces/IUlnBase.sol @@ -1,6 +1,5 @@ -// SPDX-License-Identifier: LZBL-1.2 -// TODO: License -pragma solidity ^0.8.13; +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; // the formal properties are documented in the setter functions diff --git a/test/layerzero/LZCommon.sol b/test/layerzero/LZCommon.t.sol similarity index 64% rename from test/layerzero/LZCommon.sol rename to test/layerzero/LZCommon.t.sol index 686a26b..798f2bd 100644 --- a/test/layerzero/LZCommon.sol +++ b/test/layerzero/LZCommon.t.sol @@ -4,18 +4,20 @@ pragma solidity ^0.8.13; import { EndpointV2 } from "LayerZero-v2/protocol/contracts/EndpointV2.sol"; import { SimpleMessageLib } from "LayerZero-v2/protocol/contracts/messagelib/SimpleMessageLib.sol"; import { ReceiveUln302 } from "LayerZero-v2/messagelib/contracts/uln/uln302/ReceiveUln302.sol"; +import { SendUln302 } from "LayerZero-v2/messagelib/contracts/uln/uln302/SendUln302.sol"; import "forge-std/Test.sol"; import { MockLayerZeroEscrow } from "./mock/MockLayerZeroEscrow.sol"; -contract TestLzCommon is Test { +contract LZCommon is Test { uint32 internal localEid; uint32 internal remoteEid; EndpointV2 internal endpoint; SimpleMessageLib internal simpleMsgLib; - ReceiveUln302 ULN; + ReceiveUln302 ReceiveULN; + SendUln302 SendULN; - MockLayerZeroEscrow mockLayerZeroEscrow; + MockLayerZeroEscrow layerZeroEscrow; address SEND_LOST_GAS_TO = address(uint160(0xdead)); @@ -24,7 +26,8 @@ contract TestLzCommon is Test { remoteEid = 2; endpoint = new EndpointV2(localEid, address(this)); - ULN = new ReceiveUln302(address(endpoint)); + ReceiveUln302 = new ReceiveUln302(address(endpoint)); + SendUln302 = new SendUln302(address(endpoint)); SimpleMessageLib msgLib = new SimpleMessageLib(address(endpoint), address(0)); @@ -32,12 +35,11 @@ contract TestLzCommon is Test { endpoint.registerLibrary(address(msgLib)); // Set default libs - endpoint.setDefaultSendLibrary(remoteEid, address(msgLib)); - endpoint.setDefaultReceiveLibrary(remoteEid, address(msgLib), 0); + endpoint.setDefaultSendLibrary(remoteEid, address(SendUln302)); + endpoint.setDefaultReceiveLibrary(remoteEid, address(ReceiveUln302), 0); // Setup our mock escrow - mockLayerZeroEscrow = new MockLayerZeroEscrow(SEND_LOST_GAS_TO, address(endpoint)); - + layerZeroEscrow = new MockLayerZeroEscrow(SEND_LOST_GAS_TO, address(endpoint)); } } diff --git a/test/layerzero/TestLZInitConfig.t.sol b/test/layerzero/TestLZInitConfig.t.sol new file mode 100644 index 0000000..4a10b50 --- /dev/null +++ b/test/layerzero/TestLZInitConfig.t.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import { LZCommon } from "./LZCommon.t.sol"; + +import { Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +contract TestLZInitConfig is LZCommon { + function test_init_config(uint32[] calldata remoteEids) external { + vm.assume(remoteEids.length > 0); + address sendLibrary = endpoint.getSendLibrary(address(layerZeroEscrow), remoteEid); + layerZeroEscrow.initConfig(sendLibrary, remoteEids); + } +} \ No newline at end of file diff --git a/test/layerzero/TestLZMisc.t.sol b/test/layerzero/TestLZMisc.t.sol new file mode 100644 index 0000000..e7c8e41 --- /dev/null +++ b/test/layerzero/TestLZMisc.t.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import { LZCommon } from "./LZCommon.t.sol"; + +import { Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +contract TestLZMisc is LZCommon { + function test_allowInitializePath(Origin calldata origin) external { + bool result = layerZeroEscrow.allowInitializePath(origin); + + assertEq(result, false, "allowInitializePath returns true"); + } +} \ No newline at end of file diff --git a/test/layerzero/TestLZSendPacket.t.sol b/test/layerzero/TestLZSendPacket.t.sol new file mode 100644 index 0000000..e69de29 diff --git a/test/layerzero/TestLZVerifyPacket.t.sol b/test/layerzero/TestLZVerifyPacket.t.sol new file mode 100644 index 0000000..e69de29 From 066d731af2f9393ed75b9dc42b1a315d38577143 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 30 May 2024 14:21:05 +0200 Subject: [PATCH 36/61] forge install: solidity-bytes-utils v0.8.2 --- .gitmodules | 3 +++ lib/solidity-bytes-utils | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/solidity-bytes-utils diff --git a/.gitmodules b/.gitmodules index 84c91fa..9523b3f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/LayerZero-v2"] path = lib/LayerZero-v2 url = https://github.com/LayerZero-Labs/LayerZero-v2 +[submodule "lib/solidity-bytes-utils"] + path = lib/solidity-bytes-utils + url = https://github.com/GNSPS/solidity-bytes-utils diff --git a/lib/solidity-bytes-utils b/lib/solidity-bytes-utils new file mode 160000 index 0000000..e0115c4 --- /dev/null +++ b/lib/solidity-bytes-utils @@ -0,0 +1 @@ +Subproject commit e0115c4d231910df47ce3b60625ce562fe4af985 From 563215ce1a5726e4483d6a2c9598918a59337bb4 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 31 May 2024 17:47:15 +0200 Subject: [PATCH 37/61] test: init config & send packet --- remappings.txt | 4 +- test/layerzero/LZCommon.t.sol | 60 +++++++++++++++--- test/layerzero/TestLZInitConfig.t.sol | 46 +++++++++++++- test/layerzero/TestLZMisc.t.sol | 2 +- test/layerzero/TestLZSendPacket.t.sol | 67 +++++++++++++++++++++ test/layerzero/mock/MockDVN.sol | 22 +++++++ test/layerzero/mock/MockLayerZeroEscrow.sol | 2 +- 7 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 test/layerzero/mock/MockDVN.sol diff --git a/remappings.txt b/remappings.txt index 81226a2..90d3c22 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,4 +2,6 @@ forge-std/=lib/forge-std/src/ openzeppelin/=lib/openzeppelin-contracts/contracts/ vibc-core-smart-contracts/=lib/vibc-core-smart-contracts/contracts/ @openzeppelin/=lib/openzeppelin-contracts/ -@layerzerolabs/lz-evm-protocol-v2/=lib/LayerZero-v2/protocol/ \ No newline at end of file +@layerzerolabs/lz-evm-protocol-v2/=lib/LayerZero-v2/protocol/ + +solidity-bytes-utils/=lib/solidity-bytes-utils/ diff --git a/test/layerzero/LZCommon.t.sol b/test/layerzero/LZCommon.t.sol index 798f2bd..0fd9824 100644 --- a/test/layerzero/LZCommon.t.sol +++ b/test/layerzero/LZCommon.t.sol @@ -1,42 +1,88 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.22; import { EndpointV2 } from "LayerZero-v2/protocol/contracts/EndpointV2.sol"; import { SimpleMessageLib } from "LayerZero-v2/protocol/contracts/messagelib/SimpleMessageLib.sol"; +import { UlnConfig, SetDefaultUlnConfigParam, UlnConfig } from "LayerZero-v2/messagelib/contracts/uln/UlnBase.sol"; import { ReceiveUln302 } from "LayerZero-v2/messagelib/contracts/uln/uln302/ReceiveUln302.sol"; import { SendUln302 } from "LayerZero-v2/messagelib/contracts/uln/uln302/SendUln302.sol"; import "forge-std/Test.sol"; import { MockLayerZeroEscrow } from "./mock/MockLayerZeroEscrow.sol"; +import { MockDVN } from "./mock/MockDVN.sol"; contract LZCommon is Test { + uint32 MAX_MESSAGE_SIZE = 65536; + uint32 internal localEid; uint32 internal remoteEid; EndpointV2 internal endpoint; SimpleMessageLib internal simpleMsgLib; - ReceiveUln302 ReceiveULN; - SendUln302 SendULN; + ReceiveUln302 receiveULN; + SendUln302 sendULN; + MockDVN mockDVN; + + address signer; + uint256 privatekey; MockLayerZeroEscrow layerZeroEscrow; address SEND_LOST_GAS_TO = address(uint160(0xdead)); + uint32 dvnVID = 1; + function setUp() virtual public { + (signer, privatekey) = makeAddrAndKey("signer"); + localEid = 1; remoteEid = 2; endpoint = new EndpointV2(localEid, address(this)); - ReceiveUln302 = new ReceiveUln302(address(endpoint)); - SendUln302 = new SendUln302(address(endpoint)); + sendULN = new SendUln302(address(endpoint), 0, 0); + receiveULN = new ReceiveUln302(address(endpoint)); + + mockDVN = new MockDVN(); + + address[] memory optionalDVNs = new address[](0); + address[] memory requiredDVNs = new address[](1); + requiredDVNs[0] = address(mockDVN); + + // Set ULN Config + UlnConfig memory baseConfig = UlnConfig({ + confirmations: 0, + requiredDVNCount: 1, + optionalDVNCount: 0, + optionalDVNThreshold: 0, + requiredDVNs: requiredDVNs, + optionalDVNs: optionalDVNs + }); + + SetDefaultUlnConfigParam[] memory _params = new SetDefaultUlnConfigParam[](2); + _params[0] = SetDefaultUlnConfigParam({ + eid: localEid, + config: baseConfig + }); + _params[1] = SetDefaultUlnConfigParam({ + eid: remoteEid, + config: baseConfig + }); + + sendULN.setDefaultUlnConfigs(_params); + receiveULN.setDefaultUlnConfigs(_params); SimpleMessageLib msgLib = new SimpleMessageLib(address(endpoint), address(0)); // register msg lib endpoint.registerLibrary(address(msgLib)); + endpoint.registerLibrary(address(sendULN)); + endpoint.registerLibrary(address(receiveULN)); + // Set default libs - endpoint.setDefaultSendLibrary(remoteEid, address(SendUln302)); - endpoint.setDefaultReceiveLibrary(remoteEid, address(ReceiveUln302), 0); + endpoint.setDefaultSendLibrary(localEid, address(sendULN)); + endpoint.setDefaultReceiveLibrary(localEid, address(receiveULN), 0); + endpoint.setDefaultSendLibrary(remoteEid, address(sendULN)); + endpoint.setDefaultReceiveLibrary(remoteEid, address(receiveULN), 0); // Setup our mock escrow diff --git a/test/layerzero/TestLZInitConfig.t.sol b/test/layerzero/TestLZInitConfig.t.sol index 4a10b50..d3c1328 100644 --- a/test/layerzero/TestLZInitConfig.t.sol +++ b/test/layerzero/TestLZInitConfig.t.sol @@ -1,14 +1,54 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.22; import { LZCommon } from "./LZCommon.t.sol"; import { Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { ExecutorConfig } from "LayerZero-v2/messagelib/contracts/SendLibBase.sol"; contract TestLZInitConfig is LZCommon { - function test_init_config(uint32[] calldata remoteEids) external { - vm.assume(remoteEids.length > 0); + function test_init_config_remote() external { + uint32[] memory remoteEids = new uint32[](1); + remoteEids[0] = remoteEid; + + address sendLibrary = endpoint.getSendLibrary(address(layerZeroEscrow), remoteEid); + layerZeroEscrow.initConfig(sendLibrary, remoteEids); + + bytes memory config = endpoint.getConfig(address(layerZeroEscrow), sendLibrary, remoteEid, 1); + ExecutorConfig memory executorConfig = abi.decode(config, (ExecutorConfig)); + assertEq(executorConfig.executor, address(layerZeroEscrow)); + assertEq(executorConfig.maxMessageSize, MAX_MESSAGE_SIZE); + } + + function test_init_config_local() external { + uint32[] memory remoteEids = new uint32[](1); + remoteEids[0] = localEid; + address sendLibrary = endpoint.getSendLibrary(address(layerZeroEscrow), remoteEid); layerZeroEscrow.initConfig(sendLibrary, remoteEids); + + bytes memory config = endpoint.getConfig(address(layerZeroEscrow), sendLibrary, localEid, 1); + ExecutorConfig memory executorConfig = abi.decode(config, (ExecutorConfig)); + assertEq(executorConfig.executor, address(layerZeroEscrow)); + assertEq(executorConfig.maxMessageSize, MAX_MESSAGE_SIZE); + } + + function test_init_config_remote_local() external { + uint32[] memory remoteEids = new uint32[](2); + remoteEids[0] = remoteEid; + remoteEids[1] = localEid; + + address sendLibrary = endpoint.getSendLibrary(address(layerZeroEscrow), remoteEid); + layerZeroEscrow.initConfig(sendLibrary, remoteEids); + + bytes memory config = endpoint.getConfig(address(layerZeroEscrow), sendLibrary, localEid, 1); + ExecutorConfig memory executorConfig = abi.decode(config, (ExecutorConfig)); + assertEq(executorConfig.executor, address(layerZeroEscrow)); + assertEq(executorConfig.maxMessageSize, MAX_MESSAGE_SIZE); + + config = endpoint.getConfig(address(layerZeroEscrow), sendLibrary, remoteEid, 1); + executorConfig = abi.decode(config, (ExecutorConfig)); + assertEq(executorConfig.executor, address(layerZeroEscrow)); + assertEq(executorConfig.maxMessageSize, MAX_MESSAGE_SIZE); } } \ No newline at end of file diff --git a/test/layerzero/TestLZMisc.t.sol b/test/layerzero/TestLZMisc.t.sol index e7c8e41..7febd47 100644 --- a/test/layerzero/TestLZMisc.t.sol +++ b/test/layerzero/TestLZMisc.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.22; import { LZCommon } from "./LZCommon.t.sol"; diff --git a/test/layerzero/TestLZSendPacket.t.sol b/test/layerzero/TestLZSendPacket.t.sol index e69de29..e66e676 100644 --- a/test/layerzero/TestLZSendPacket.t.sol +++ b/test/layerzero/TestLZSendPacket.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import { LZCommon } from "./LZCommon.t.sol"; + +import { ExecutorConfig } from "LayerZero-v2/messagelib/contracts/SendLibBase.sol"; +import { Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { Packet } from "LayerZero-v2/protocol/contracts/interfaces/ISendLib.sol"; +import { PacketV1Codec } from "LayerZero-v2/protocol/contracts/messagelib/libs/PacketV1Codec.sol"; +import { GUID } from "LayerZero-v2/protocol/contracts/libs/GUID.sol"; + +contract TestLZSendPacket is LZCommon { + + event ExecutorFeePaid(address executor, uint256 fee); + event PacketSent(bytes encodedPayload, bytes options, address sendLibrary); + + function _set_init_config() internal { + uint32[] memory remoteEids = new uint32[](2); + remoteEids[0] = remoteEid; + remoteEids[1] = localEid; + + address sendLibrary = endpoint.getSendLibrary(address(layerZeroEscrow), remoteEid); + layerZeroEscrow.initConfig(sendLibrary, remoteEids); + } + + function setUp() public override { + super.setUp(); + } + + function test_send_packet(address target, bytes calldata message, uint64 deadline) external { + _set_init_config(); + + + uint64 nonce = 1; + uint32 dstEid = remoteEid; + bytes32 receiver = bytes32(uint256(uint160(target))); + address sender = address(layerZeroEscrow); + + bytes32 guid = GUID.generate(nonce, localEid, sender, dstEid, receiver); + + Packet memory packet = Packet({ + nonce: nonce, + srcEid: localEid, + sender: sender, + dstEid: remoteEid, + receiver: receiver, + guid: guid, + message: message + }); + + vm.expectEmit(); + emit ExecutorFeePaid(address(layerZeroEscrow), 0); + + vm.expectEmit(); + emit PacketSent( + PacketV1Codec.encode(packet), + hex"0003", + address(sendULN) + ); + layerZeroEscrow.sendPacket(bytes32(uint256(remoteEid)), abi.encodePacked(bytes32(uint256(uint160(target)))), message, deadline); + } + + function test_revert_no_init_config_send_packet(address target, bytes calldata message, uint64 deadline) external { + vm.expectRevert(); + layerZeroEscrow.sendPacket(bytes32(uint256(remoteEid)), abi.encodePacked(bytes32(uint256(uint160(target)))), message, deadline); + } +} \ No newline at end of file diff --git a/test/layerzero/mock/MockDVN.sol b/test/layerzero/mock/MockDVN.sol new file mode 100644 index 0000000..80279eb --- /dev/null +++ b/test/layerzero/mock/MockDVN.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: IncentivizedLayerZeroEscrow +pragma solidity ^0.8.22; + +import { ILayerZeroDVN } from "LayerZero-v2/messagelib/contracts/uln/interfaces/ILayerZeroDVN.sol"; + +/** + * @notice Mock DVN + */ +contract MockDVN is ILayerZeroDVN { + function assignJob(AssignJobParam calldata /* _param */, bytes calldata /* _options */) external payable returns (uint256 fee) { + return 0; + } + + function getFee( + uint32 /* _dstEid */, + uint64 /* _confirmations */, + address /* _sender */, + bytes calldata /* _options */ + ) external pure returns (uint256 fee) { + return 0; + } +} \ No newline at end of file diff --git a/test/layerzero/mock/MockLayerZeroEscrow.sol b/test/layerzero/mock/MockLayerZeroEscrow.sol index e9df6ec..da540c0 100644 --- a/test/layerzero/mock/MockLayerZeroEscrow.sol +++ b/test/layerzero/mock/MockLayerZeroEscrow.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: IncentivizedLayerZeroEscrow -pragma solidity ^0.8.13; +pragma solidity ^0.8.22; import { IncentivizedLayerZeroEscrow } from "../../../src/apps/layerzero/IncentivizedLayerZeroEscrow.sol"; From 56397654e4362b5620142e890398a3b87bea970a Mon Sep 17 00:00:00 2001 From: ajimeno04 Date: Sun, 2 Jun 2024 20:32:57 +0200 Subject: [PATCH 38/61] Update deployment contract for new LayerZero escrow contracts --- script/Deploy.sol | 14 ++++++++++---- script/bridge_contracts.json | 12 ++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/script/Deploy.sol b/script/Deploy.sol index b9820f1..3aa4dd3 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -29,6 +29,7 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { // define a list of AMB mappings so we can get their addresses. mapping(string => mapping(string => address)) bridgeContract; + mapping(string => mapping(string => address)) lzEndpoint; constructor() { @@ -98,20 +99,22 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { } else if (versionHash == keccak256(abi.encodePacked("LayerZero"))) { address layerZeroBridgeContract = bridgeContract[version][currentChainKey]; + address lzEndpointV2 = lzEndpoint[version][currentChainKey]; // Fetch LZ_ENDPOINT_V2 from bridge contracts bytes32 salt = deploySalts[layerZeroBridgeContract]; require(layerZeroBridgeContract != address(0), "bridge cannot be address(0)"); + require(lzEndpointV2 != address(0), "LZ endpoint cannot be address(0)"); address expectedAddress = _getAddress( abi.encodePacked( type(IncentivizedLayerZeroEscrow).creationCode, - abi.encode(vm.envAddress("SEND_LOST_GAS_TO"), layerZeroBridgeContract) + abi.encode(vm.envAddress("SEND_LOST_GAS_TO"), lzEndpointV2) ), salt ); if (expectedAddress.codehash != bytes32(0)) return expectedAddress; - IncentivizedLayerZeroEscrow layerZeroEscrow = new IncentivizedLayerZeroEscrow{salt: salt}(vm.envAddress("SEND_LOST_GAS_TO"), layerZeroBridgeContract); + IncentivizedLayerZeroEscrow layerZeroEscrow = new IncentivizedLayerZeroEscrow{salt: salt}(vm.envAddress("SEND_LOST_GAS_TO"), lzEndpointV2); incentive = address(layerZeroEscrow); } else { revert IncentivesVersionNotFound(); @@ -126,8 +129,7 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { return incentive; } - modifier load_config() { - +modifier load_config() { string memory pathRoot = vm.projectRoot(); pathToAmbConfig = string.concat(pathRoot, "/script/bridge_contracts.json"); @@ -146,6 +148,10 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { // Decode the address address _bridgeContract = vm.parseJsonAddress(bridge_config, string.concat(".", bridge, ".", chain, ".bridge")); bridgeContract[bridge][chain] = _bridgeContract; + // Decode the LZ endpoint if it exists + if (keccak256(abi.encodePacked(bridge)) == keccak256(abi.encodePacked("LayerZero"))) { address _lzEndpointV2 = vm.parseJsonAddress(bridge_config, string.concat(".", bridge, ".", chain, ".LZ_ENDPOINT_V2")); + lzEndpoint[bridge][chain] = _lzEndpointV2; + } } } diff --git a/script/bridge_contracts.json b/script/bridge_contracts.json index 13c33e7..fa0d3fd 100644 --- a/script/bridge_contracts.json +++ b/script/bridge_contracts.json @@ -50,18 +50,18 @@ "LayerZero": { "basesepolia": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0x794d16F3a32FF13E1734D245f1c4C378622a53a5", - "ULN": "0x9eCf72299027e8AeFee5DC5351D6d92294F46d2b" + "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec", + "LZ_ENDPOINT_V2": "0x6EDCE65403992e310A62460808c4b910D972f10f" }, "optimismsepolia": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0xe2C449C0f8b0bF55655e4E92299d9d1c27bDE585", - "ULN": "0x420667429538adBF982aDa16C268ba561f097F74" + "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec", + "LZ_ENDPOINT_V2": "0x6EDCE65403992e310A62460808c4b910D972f10f" }, "blasttestnet": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0xe2FD2FEB02DD96eF1D3c0a8823a38C3005fDCB15", - "ULN": "0x8dF53a660a00C3D977d7E778fB7385ECf4482D16" + "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec", + "LZ_ENDPOINT_V2": "0x6EDCE65403992e310A62460808c4b910D972f10f" } } } \ No newline at end of file From 689a907afd3839dc9fc7b001e52ea8c47834efc2 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 3 Jun 2024 13:13:41 +0200 Subject: [PATCH 39/61] fix: correctly provide all of the package in rawMessage --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 16 ++-- test/layerzero/LZCommon.t.sol | 2 +- test/layerzero/TestLZVerifyPacket.t.sol | 96 +++++++++++++++++++ test/layerzero/mock/MockLayerZeroEscrow.sol | 2 +- 4 files changed, 105 insertions(+), 11 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 97e796a..112b2d6 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -187,17 +187,17 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero asset = address(0); } - function _verifyPacket(bytes calldata _packetHeader, bytes calldata _packet) internal view override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { - _assertHeader(_packetHeader); + function _verifyPacket(bytes calldata /* _packetHeader */, bytes calldata _packet) view internal override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { + _assertHeader(_packet.header()); // Check that we are the receiver - address receiver = _packetHeader.receiverB20(); + address receiver = _packet.receiverB20(); if (receiver != address(this)) revert IncorrectDestination(receiver); // Get the source chain. - uint32 srcEid = _packetHeader.srcEid(); + uint32 srcEid = _packet.srcEid(); - bytes32 _headerHash = keccak256(_packetHeader); + bytes32 _headerHash = keccak256(_packet); bytes32 _payloadHash = _packet.payloadHash(); // The ULN may not be constant since it depends on the srcEid. :( @@ -213,6 +213,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero if (!verifyable) { // LayerZero may have migrated to a new receive library. Check the timeout receive library. (address timeoutULN, ) = ENDPOINT.defaultReceiveLibraryTimeout(srcEid); + if (timeoutULN == address(0)) revert LZ_ULN_Verifying(); ULN = IReceiveUlnBase(timeoutULN); verifyable = ULN.verifiable(_config, _headerHash, _payloadHash); if (!verifyable) revert LZ_ULN_Verifying(); @@ -221,7 +222,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero // Get the source chain sourceIdentifier = bytes32(uint256(srcEid)); // Get the sender - implementationIdentifier = abi.encode(_packetHeader.sender()); + implementationIdentifier = abi.encode(_packet.sender()); // Get the message message_ = _packet.message(); } @@ -258,10 +259,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero require(allowExternalCall != 1, "Do not send ether to this address"); } - function _assertHeader(bytes calldata _packetHeader) internal view { - // assert packet header is of right size 81 - if (_packetHeader.length != 81) revert LZ_ULN_InvalidPacketHeader(); // assert packet header version is the same as ULN if (_packetHeader.version() != PacketV1Codec.PACKET_VERSION) revert LZ_ULN_InvalidPacketVersion(); // assert the packet is for this endpoint diff --git a/test/layerzero/LZCommon.t.sol b/test/layerzero/LZCommon.t.sol index 0fd9824..b8095c0 100644 --- a/test/layerzero/LZCommon.t.sol +++ b/test/layerzero/LZCommon.t.sol @@ -49,7 +49,7 @@ contract LZCommon is Test { // Set ULN Config UlnConfig memory baseConfig = UlnConfig({ - confirmations: 0, + confirmations: 1, requiredDVNCount: 1, optionalDVNCount: 0, optionalDVNThreshold: 0, diff --git a/test/layerzero/TestLZVerifyPacket.t.sol b/test/layerzero/TestLZVerifyPacket.t.sol index e69de29..b7e1e4d 100644 --- a/test/layerzero/TestLZVerifyPacket.t.sol +++ b/test/layerzero/TestLZVerifyPacket.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import { LZCommon } from "./LZCommon.t.sol"; + +import { Packet } from "LayerZero-v2/protocol/contracts/interfaces/ISendLib.sol"; +import { PacketV1Codec } from "LayerZero-v2/protocol/contracts/messagelib/libs/PacketV1Codec.sol"; +import { GUID } from "LayerZero-v2/protocol/contracts/libs/GUID.sol"; + +contract TestLZVerifyPacket is LZCommon { + using PacketV1Codec for bytes; + using PacketV1Codec for Packet; + + event ExecutorFeePaid(address executor, uint256 fee); + event PacketSent(bytes encodedPayload, bytes options, address sendLibrary); + + function _set_init_config() internal { + uint32[] memory remoteEids = new uint32[](2); + remoteEids[0] = remoteEid; + remoteEids[1] = localEid; + + address sendLibrary = endpoint.getSendLibrary(address(layerZeroEscrow), remoteEid); + layerZeroEscrow.initConfig(sendLibrary, remoteEids); + } + + function setUp() public override { + super.setUp(); + _set_init_config(); + } + + function test_revert_verify_packet_no_proof(bytes calldata message) external { + vm.assume(message.length > 0); + address target = address(layerZeroEscrow); + + uint64 nonce = 1; + uint32 dstEid = localEid; + bytes32 receiver = bytes32(uint256(uint160(target))); + address sender = address(layerZeroEscrow); + + bytes32 guid = GUID.generate(nonce, localEid, sender, dstEid, receiver); + + Packet memory packet = Packet({ + nonce: nonce, + srcEid: remoteEid, + sender: sender, + dstEid: dstEid, + receiver: receiver, + guid: guid, + message: message + }); + + bytes memory _packet = PacketV1Codec.encode(packet); + + vm.expectRevert(abi.encodeWithSignature("LZ_ULN_Verifying()")); + layerZeroEscrow.verifyPacket(hex"", _packet); + } + + function test_verify_packet(bytes calldata message) external { + vm.assume(message.length > 0); + address target = address(layerZeroEscrow); + + uint64 nonce = 1; + uint32 dstEid = localEid; + bytes32 receiver = bytes32(uint256(uint160(target))); + address sender = address(layerZeroEscrow); + + bytes32 guid = GUID.generate(nonce, localEid, sender, dstEid, receiver); + + Packet memory packet = Packet({ + nonce: nonce, + srcEid: remoteEid, + sender: sender, + dstEid: dstEid, + receiver: receiver, + guid: guid, + message: message + }); + + bytes memory _packet = packet.encode(); + + bytes32 ph = this.payloadHash(packet.encode()); + + vm.prank(address(mockDVN)); + receiveULN.verify( + packet.encodePacketHeader(), + ph, + 10 + ); + + layerZeroEscrow.verifyPacket(hex"", _packet); + } + + function payloadHash(bytes calldata pl) pure public returns(bytes32) { + return pl.payloadHash(); + } +} \ No newline at end of file diff --git a/test/layerzero/mock/MockLayerZeroEscrow.sol b/test/layerzero/mock/MockLayerZeroEscrow.sol index da540c0..d542593 100644 --- a/test/layerzero/mock/MockLayerZeroEscrow.sol +++ b/test/layerzero/mock/MockLayerZeroEscrow.sol @@ -10,7 +10,7 @@ contract MockLayerZeroEscrow is IncentivizedLayerZeroEscrow { constructor(address sendLostGasTo, address lzEndpointV2) IncentivizedLayerZeroEscrow(sendLostGasTo, lzEndpointV2) {} - function verifyPacket(bytes calldata _packetHeader, bytes calldata _packet) external view returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { + function verifyPacket(bytes calldata _packetHeader, bytes calldata _packet) view external returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { return _verifyPacket(_packetHeader, _packet); } From d81d6d8e63caac4f1b3d12b3e7b5ec30aff3daf6 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 3 Jun 2024 13:16:01 +0200 Subject: [PATCH 40/61] test: verify packages --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 2 +- test/layerzero/TestLZSendPacket.t.sol | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 112b2d6..d443617 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -197,7 +197,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero // Get the source chain. uint32 srcEid = _packet.srcEid(); - bytes32 _headerHash = keccak256(_packet); + bytes32 _headerHash = keccak256(_packet.header()); bytes32 _payloadHash = _packet.payloadHash(); // The ULN may not be constant since it depends on the srcEid. :( diff --git a/test/layerzero/TestLZSendPacket.t.sol b/test/layerzero/TestLZSendPacket.t.sol index e66e676..28abebc 100644 --- a/test/layerzero/TestLZSendPacket.t.sol +++ b/test/layerzero/TestLZSendPacket.t.sol @@ -3,8 +3,6 @@ pragma solidity ^0.8.22; import { LZCommon } from "./LZCommon.t.sol"; -import { ExecutorConfig } from "LayerZero-v2/messagelib/contracts/SendLibBase.sol"; -import { Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { Packet } from "LayerZero-v2/protocol/contracts/interfaces/ISendLib.sol"; import { PacketV1Codec } from "LayerZero-v2/protocol/contracts/messagelib/libs/PacketV1Codec.sol"; import { GUID } from "LayerZero-v2/protocol/contracts/libs/GUID.sol"; @@ -30,7 +28,6 @@ contract TestLZSendPacket is LZCommon { function test_send_packet(address target, bytes calldata message, uint64 deadline) external { _set_init_config(); - uint64 nonce = 1; uint32 dstEid = remoteEid; bytes32 receiver = bytes32(uint256(uint160(target))); @@ -42,7 +39,7 @@ contract TestLZSendPacket is LZCommon { nonce: nonce, srcEid: localEid, sender: sender, - dstEid: remoteEid, + dstEid: dstEid, receiver: receiver, guid: guid, message: message From dffd8056c93e8ce32d4285188052e93ce1ee4558 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 3 Jun 2024 13:27:03 +0200 Subject: [PATCH 41/61] feat: simplify contract very slightly --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index d443617..fdd57bb 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -214,8 +214,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero // LayerZero may have migrated to a new receive library. Check the timeout receive library. (address timeoutULN, ) = ENDPOINT.defaultReceiveLibraryTimeout(srcEid); if (timeoutULN == address(0)) revert LZ_ULN_Verifying(); - ULN = IReceiveUlnBase(timeoutULN); - verifyable = ULN.verifiable(_config, _headerHash, _payloadHash); + verifyable = IReceiveUlnBase(timeoutULN).verifiable(_config, _headerHash, _payloadHash); if (!verifyable) revert LZ_ULN_Verifying(); } From 60766c42e938bd4b01fcfb9573a6128f5d977e4b Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 3 Jun 2024 13:57:33 +0200 Subject: [PATCH 42/61] feat: more LZ testing --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 5 +- test/TestCommon.t.sol | 2 + test/layerzero/LZCommon.t.sol | 2 + test/layerzero/TestLZVerifyPacket.t.sol | 78 +++++++++++++++++++ test/layerzero/mock/MockDVN.sol | 3 + test/layerzero/mock/MockLayerZeroEscrow.sol | 2 + 6 files changed, 89 insertions(+), 3 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index fdd57bb..5481ef3 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -175,14 +175,13 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero * For a better quote, use the function overload. */ function estimateAdditionalCost() external view returns(address asset, uint256 amount) { - amount = _estimateAdditionalCost(chainId); - asset = address(0); + (asset, amount) = estimateAdditionalCost(chainId); } /** * @notice Get an exact quote. */ - function estimateAdditionalCost(uint256 destinationChainId) external view returns(address asset, uint256 amount) { + function estimateAdditionalCost(uint256 destinationChainId) public view returns(address asset, uint256 amount) { amount = _estimateAdditionalCost(uint32(destinationChainId)); asset = address(0); } diff --git a/test/TestCommon.t.sol b/test/TestCommon.t.sol index b5315c4..689b91f 100644 --- a/test/TestCommon.t.sol +++ b/test/TestCommon.t.sol @@ -20,6 +20,8 @@ interface ICansubmitMessage is IMessageEscrowStructs{ } contract TestCommon is Test, IMessageEscrowEvents, IMessageEscrowStructs { + + function test() public {} uint256 constant GAS_SPENT_ON_SOURCE = 6888; uint256 constant GAS_SPENT_ON_DESTINATION = 32073; diff --git a/test/layerzero/LZCommon.t.sol b/test/layerzero/LZCommon.t.sol index b8095c0..c39c8c0 100644 --- a/test/layerzero/LZCommon.t.sol +++ b/test/layerzero/LZCommon.t.sol @@ -12,6 +12,8 @@ import { MockLayerZeroEscrow } from "./mock/MockLayerZeroEscrow.sol"; import { MockDVN } from "./mock/MockDVN.sol"; contract LZCommon is Test { + + function test() public {} uint32 MAX_MESSAGE_SIZE = 65536; uint32 internal localEid; diff --git a/test/layerzero/TestLZVerifyPacket.t.sol b/test/layerzero/TestLZVerifyPacket.t.sol index b7e1e4d..4cfff23 100644 --- a/test/layerzero/TestLZVerifyPacket.t.sol +++ b/test/layerzero/TestLZVerifyPacket.t.sol @@ -90,7 +90,85 @@ contract TestLZVerifyPacket is LZCommon { layerZeroEscrow.verifyPacket(hex"", _packet); } + function test_revert_verify_wrong_chain(bytes calldata message) external { + vm.assume(message.length > 0); + address target = address(layerZeroEscrow); + + uint64 nonce = 1; + uint32 dstEid = remoteEid; + bytes32 receiver = bytes32(uint256(uint160(target))); + address sender = address(layerZeroEscrow); + + bytes32 guid = GUID.generate(nonce, dstEid, sender, dstEid, receiver); + + Packet memory packet = Packet({ + nonce: nonce, + srcEid: dstEid, + sender: sender, + dstEid: dstEid, + receiver: receiver, + guid: guid, + message: message + }); + + bytes memory _packet = packet.encode(); + + bytes32 ph = this.payloadHash(packet.encode()); + + vm.prank(address(mockDVN)); + receiveULN.verify( + packet.encodePacketHeader(), + ph, + 10 + ); + + vm.expectRevert(abi.encodeWithSignature("LZ_ULN_InvalidEid()")); + layerZeroEscrow.verifyPacket(hex"", _packet); + } + + // TODO: + function test_revert_verify_invalid_packet_version(bytes calldata message) external { + vm.assume(message.length > 0); + address target = address(layerZeroEscrow); + + uint64 nonce = 1; + uint32 dstEid = remoteEid; + bytes32 receiver = bytes32(uint256(uint160(target))); + address sender = address(layerZeroEscrow); + + bytes32 guid = GUID.generate(nonce, dstEid, sender, dstEid, receiver); + + Packet memory packet = Packet({ + nonce: nonce, + srcEid: dstEid, + sender: sender, + dstEid: dstEid, + receiver: receiver, + guid: guid, + message: message + }); + + bytes memory _packet = packet.encode(); + _packet = this.replaceVersion(0x02, _packet); + + bytes32 ph = this.payloadHash(packet.encode()); + + vm.prank(address(mockDVN)); + receiveULN.verify( + packet.encodePacketHeader(), + ph, + 10 + ); + + vm.expectRevert(abi.encodeWithSignature("LZ_ULN_InvalidPacketVersion()")); + layerZeroEscrow.verifyPacket(hex"", _packet); + } + function payloadHash(bytes calldata pl) pure public returns(bytes32) { return pl.payloadHash(); } + + function replaceVersion(bytes1 newVersion, bytes calldata pl) pure public returns(bytes memory) { + return abi.encodePacked(newVersion, pl[1:]); + } } \ No newline at end of file diff --git a/test/layerzero/mock/MockDVN.sol b/test/layerzero/mock/MockDVN.sol index 80279eb..017c86b 100644 --- a/test/layerzero/mock/MockDVN.sol +++ b/test/layerzero/mock/MockDVN.sol @@ -7,6 +7,9 @@ import { ILayerZeroDVN } from "LayerZero-v2/messagelib/contracts/uln/interfaces/ * @notice Mock DVN */ contract MockDVN is ILayerZeroDVN { + + function test() public {} + function assignJob(AssignJobParam calldata /* _param */, bytes calldata /* _options */) external payable returns (uint256 fee) { return 0; } diff --git a/test/layerzero/mock/MockLayerZeroEscrow.sol b/test/layerzero/mock/MockLayerZeroEscrow.sol index d542593..3379fa4 100644 --- a/test/layerzero/mock/MockLayerZeroEscrow.sol +++ b/test/layerzero/mock/MockLayerZeroEscrow.sol @@ -7,6 +7,8 @@ import { IncentivizedLayerZeroEscrow } from "../../../src/apps/layerzero/Incenti * @notice Mock Layer Zero Escrow */ contract MockLayerZeroEscrow is IncentivizedLayerZeroEscrow { + + function test() public {} constructor(address sendLostGasTo, address lzEndpointV2) IncentivizedLayerZeroEscrow(sendLostGasTo, lzEndpointV2) {} From f001dbfa3fb22e5e49efdf29f869f629c4546b98 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 3 Jun 2024 14:08:40 +0200 Subject: [PATCH 43/61] test: invalid receiver --- test/layerzero/TestLZVerifyPacket.t.sol | 37 ++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/test/layerzero/TestLZVerifyPacket.t.sol b/test/layerzero/TestLZVerifyPacket.t.sol index 4cfff23..4905445 100644 --- a/test/layerzero/TestLZVerifyPacket.t.sol +++ b/test/layerzero/TestLZVerifyPacket.t.sol @@ -126,7 +126,42 @@ contract TestLZVerifyPacket is LZCommon { layerZeroEscrow.verifyPacket(hex"", _packet); } - // TODO: + function test_revert_verify_invalid_receiver(bytes calldata message, address target) external { + vm.assume(target != address(layerZeroEscrow)); + vm.assume(message.length > 0); + + uint64 nonce = 1; + uint32 dstEid = localEid; + bytes32 receiver = bytes32(uint256(uint160(target))); + address sender = address(layerZeroEscrow); + + bytes32 guid = GUID.generate(nonce, dstEid, sender, dstEid, receiver); + + Packet memory packet = Packet({ + nonce: nonce, + srcEid: remoteEid, + sender: sender, + dstEid: dstEid, + receiver: receiver, + guid: guid, + message: message + }); + + bytes memory _packet = packet.encode(); + + bytes32 ph = this.payloadHash(packet.encode()); + + vm.prank(address(mockDVN)); + receiveULN.verify( + packet.encodePacketHeader(), + ph, + 10 + ); + + vm.expectRevert(abi.encodeWithSignature("IncorrectDestination(address)", receiver)); + layerZeroEscrow.verifyPacket(hex"", _packet); + } + function test_revert_verify_invalid_packet_version(bytes calldata message) external { vm.assume(message.length > 0); address target = address(layerZeroEscrow); From 3129a0ff346824e16dc55be27b6dc5b4b7c04891 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 3 Jun 2024 14:42:43 +0200 Subject: [PATCH 44/61] test: finsh LZ testing --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 7 ++--- test/layerzero/LZCommon.t.sol | 9 ++++++ test/layerzero/TestLZMisc.t.sol | 31 +++++++++++++++++++ test/layerzero/TestLZSendPacket.t.sol | 9 ------ test/layerzero/TestLZVerifyPacket.t.sol | 9 ------ 5 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 5481ef3..71df430 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -116,12 +116,11 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero } /** - * @notice Set ourself as executor on all (provided) remote chains. This is required before we anyone - * can send message out of that chain. + * @notice Set ourself as executor on all (provided) remote chains. This is required before anyone + * can send message out to that chain. * @dev sendLibrary is not checked. It is assumed that any endpoint will accept anything as long as it is somewhat sane. * @param sendLibrary Contract to set config on. * @param remoteEids List of remote Eids to set config on. - // TODO: read sendLibrary from Endpoint maybe. */ function initConfig(address sendLibrary, uint32[] calldata remoteEids) external { unchecked { @@ -161,7 +160,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero dstEid: uint32(destEid), receiver: bytes32(0), // Is unused by LZ. message: hex"", - options: LAYERZERO_OPTIONS, // TODO: Are these options important? + options: LAYERZERO_OPTIONS, payInLzToken: false }); diff --git a/test/layerzero/LZCommon.t.sol b/test/layerzero/LZCommon.t.sol index c39c8c0..648a50a 100644 --- a/test/layerzero/LZCommon.t.sol +++ b/test/layerzero/LZCommon.t.sol @@ -90,4 +90,13 @@ contract LZCommon is Test { // Setup our mock escrow layerZeroEscrow = new MockLayerZeroEscrow(SEND_LOST_GAS_TO, address(endpoint)); } + + function _set_init_config() internal { + uint32[] memory remoteEids = new uint32[](2); + remoteEids[0] = remoteEid; + remoteEids[1] = localEid; + + address sendLibrary = endpoint.getSendLibrary(address(layerZeroEscrow), remoteEid); + layerZeroEscrow.initConfig(sendLibrary, remoteEids); + } } diff --git a/test/layerzero/TestLZMisc.t.sol b/test/layerzero/TestLZMisc.t.sol index 7febd47..5e60b5a 100644 --- a/test/layerzero/TestLZMisc.t.sol +++ b/test/layerzero/TestLZMisc.t.sol @@ -6,9 +6,40 @@ import { LZCommon } from "./LZCommon.t.sol"; import { Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; contract TestLZMisc is LZCommon { + function setUp() public override { + super.setUp(); + } + function test_allowInitializePath(Origin calldata origin) external { bool result = layerZeroEscrow.allowInitializePath(origin); assertEq(result, false, "allowInitializePath returns true"); } + + function test_estimate_additional_cost() external { + _set_init_config(); + vm.expectCall( + address(layerZeroEscrow), + abi.encodeCall(layerZeroEscrow.getFee, (localEid, address(layerZeroEscrow), 0, hex"")) + ); + layerZeroEscrow.estimateAdditionalCost(); + } + + function test_revert_estimate_additional_cost_no_config() external { + vm.expectCall( + address(0), + abi.encodeCall(layerZeroEscrow.getFee, (localEid, address(layerZeroEscrow), 0, hex"")) + ); + vm.expectRevert(); + layerZeroEscrow.estimateAdditionalCost(); + } + + function test_estimate_additional_cost_remote() external { + _set_init_config(); + vm.expectCall( + address(layerZeroEscrow), + abi.encodeCall(layerZeroEscrow.getFee, (remoteEid, address(layerZeroEscrow), 0, hex"")) + ); + layerZeroEscrow.estimateAdditionalCost(remoteEid); + } } \ No newline at end of file diff --git a/test/layerzero/TestLZSendPacket.t.sol b/test/layerzero/TestLZSendPacket.t.sol index 28abebc..91f74f0 100644 --- a/test/layerzero/TestLZSendPacket.t.sol +++ b/test/layerzero/TestLZSendPacket.t.sol @@ -12,15 +12,6 @@ contract TestLZSendPacket is LZCommon { event ExecutorFeePaid(address executor, uint256 fee); event PacketSent(bytes encodedPayload, bytes options, address sendLibrary); - function _set_init_config() internal { - uint32[] memory remoteEids = new uint32[](2); - remoteEids[0] = remoteEid; - remoteEids[1] = localEid; - - address sendLibrary = endpoint.getSendLibrary(address(layerZeroEscrow), remoteEid); - layerZeroEscrow.initConfig(sendLibrary, remoteEids); - } - function setUp() public override { super.setUp(); } diff --git a/test/layerzero/TestLZVerifyPacket.t.sol b/test/layerzero/TestLZVerifyPacket.t.sol index 4905445..1e7bde7 100644 --- a/test/layerzero/TestLZVerifyPacket.t.sol +++ b/test/layerzero/TestLZVerifyPacket.t.sol @@ -14,15 +14,6 @@ contract TestLZVerifyPacket is LZCommon { event ExecutorFeePaid(address executor, uint256 fee); event PacketSent(bytes encodedPayload, bytes options, address sendLibrary); - function _set_init_config() internal { - uint32[] memory remoteEids = new uint32[](2); - remoteEids[0] = remoteEid; - remoteEids[1] = localEid; - - address sendLibrary = endpoint.getSendLibrary(address(layerZeroEscrow), remoteEid); - layerZeroEscrow.initConfig(sendLibrary, remoteEids); - } - function setUp() public override { super.setUp(); _set_init_config(); From 1f723abd536639d30333d0709a5352736c198b82 Mon Sep 17 00:00:00 2001 From: ajimeno04 Date: Tue, 4 Jun 2024 13:55:15 +0200 Subject: [PATCH 45/61] update condig and deploy --- script/Deploy.sol | 11 ++--------- script/bridge_contracts.json | 9 +++------ 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/script/Deploy.sol b/script/Deploy.sol index 3aa4dd3..fbe1a31 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -29,7 +29,6 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { // define a list of AMB mappings so we can get their addresses. mapping(string => mapping(string => address)) bridgeContract; - mapping(string => mapping(string => address)) lzEndpoint; constructor() { @@ -99,22 +98,20 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { } else if (versionHash == keccak256(abi.encodePacked("LayerZero"))) { address layerZeroBridgeContract = bridgeContract[version][currentChainKey]; - address lzEndpointV2 = lzEndpoint[version][currentChainKey]; // Fetch LZ_ENDPOINT_V2 from bridge contracts bytes32 salt = deploySalts[layerZeroBridgeContract]; require(layerZeroBridgeContract != address(0), "bridge cannot be address(0)"); - require(lzEndpointV2 != address(0), "LZ endpoint cannot be address(0)"); address expectedAddress = _getAddress( abi.encodePacked( type(IncentivizedLayerZeroEscrow).creationCode, - abi.encode(vm.envAddress("SEND_LOST_GAS_TO"), lzEndpointV2) + abi.encode(vm.envAddress("SEND_LOST_GAS_TO"), layerZeroBridgeContract) ), salt ); if (expectedAddress.codehash != bytes32(0)) return expectedAddress; - IncentivizedLayerZeroEscrow layerZeroEscrow = new IncentivizedLayerZeroEscrow{salt: salt}(vm.envAddress("SEND_LOST_GAS_TO"), lzEndpointV2); + IncentivizedLayerZeroEscrow layerZeroEscrow = new IncentivizedLayerZeroEscrow{salt: salt}(vm.envAddress("SEND_LOST_GAS_TO"), layerZeroBridgeContract); incentive = address(layerZeroEscrow); } else { revert IncentivesVersionNotFound(); @@ -148,10 +145,6 @@ modifier load_config() { // Decode the address address _bridgeContract = vm.parseJsonAddress(bridge_config, string.concat(".", bridge, ".", chain, ".bridge")); bridgeContract[bridge][chain] = _bridgeContract; - // Decode the LZ endpoint if it exists - if (keccak256(abi.encodePacked(bridge)) == keccak256(abi.encodePacked("LayerZero"))) { address _lzEndpointV2 = vm.parseJsonAddress(bridge_config, string.concat(".", bridge, ".", chain, ".LZ_ENDPOINT_V2")); - lzEndpoint[bridge][chain] = _lzEndpointV2; - } } } diff --git a/script/bridge_contracts.json b/script/bridge_contracts.json index fa0d3fd..23c9b29 100644 --- a/script/bridge_contracts.json +++ b/script/bridge_contracts.json @@ -50,18 +50,15 @@ "LayerZero": { "basesepolia": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec", - "LZ_ENDPOINT_V2": "0x6EDCE65403992e310A62460808c4b910D972f10f" + "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec" }, "optimismsepolia": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec", - "LZ_ENDPOINT_V2": "0x6EDCE65403992e310A62460808c4b910D972f10f" + "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec" }, "blasttestnet": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec", - "LZ_ENDPOINT_V2": "0x6EDCE65403992e310A62460808c4b910D972f10f" + "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec" } } } \ No newline at end of file From bcdd39c4c80267e5c2c11bf6e38a8b78d4711157 Mon Sep 17 00:00:00 2001 From: ajimeno04 Date: Wed, 5 Jun 2024 12:16:14 +0200 Subject: [PATCH 46/61] Bridge contract --- script/bridge_contracts.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/bridge_contracts.json b/script/bridge_contracts.json index 23c9b29..6cfd772 100644 --- a/script/bridge_contracts.json +++ b/script/bridge_contracts.json @@ -50,15 +50,15 @@ "LayerZero": { "basesepolia": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec" + "escrow": "0x9a7dc6f882adFca913A6A6FD3434a892Ddb657Fb" }, "optimismsepolia": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec" + "escrow": "0x9a7dc6f882adFca913A6A6FD3434a892Ddb657Fb" }, "blasttestnet": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0x495691fafcf2E0c8554bfD6eA67698BCD9795eec" + "escrow": "0x9a7dc6f882adFca913A6A6FD3434a892Ddb657Fb" } } } \ No newline at end of file From 287daaa2a844a734464b9a27dc0a12275a09e5d5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 5 Jun 2024 16:06:49 +0200 Subject: [PATCH 47/61] feat: initConfig script for LZ --- script/bridge_contracts.json | 6 +- script/layerzero/InitConfig.s.sol | 126 ++++++++++++++++++ .../layerzero/IncentivizedLayerZeroEscrow.sol | 2 +- 3 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 script/layerzero/InitConfig.s.sol diff --git a/script/bridge_contracts.json b/script/bridge_contracts.json index 6cfd772..191168e 100644 --- a/script/bridge_contracts.json +++ b/script/bridge_contracts.json @@ -50,15 +50,15 @@ "LayerZero": { "basesepolia": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0x9a7dc6f882adFca913A6A6FD3434a892Ddb657Fb" + "escrow": "0xC63E56cbe30b0D057ceaB70Eaf24A27dE8a3A835" }, "optimismsepolia": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0x9a7dc6f882adFca913A6A6FD3434a892Ddb657Fb" + "escrow": "0xC63E56cbe30b0D057ceaB70Eaf24A27dE8a3A835" }, "blasttestnet": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0x9a7dc6f882adFca913A6A6FD3434a892Ddb657Fb" + "escrow": "0xC63E56cbe30b0D057ceaB70Eaf24A27dE8a3A835" } } } \ No newline at end of file diff --git a/script/layerzero/InitConfig.s.sol b/script/layerzero/InitConfig.s.sol new file mode 100644 index 0000000..fee020c --- /dev/null +++ b/script/layerzero/InitConfig.s.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import { BaseMultiChainDeployer} from "../BaseMultiChainDeployer.s.sol"; + +import { IncentivizedLayerZeroEscrow } from "../../src/apps/layerzero/IncentivizedLayerZeroEscrow.sol"; + +import { ILayerZeroEndpointV2 } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +contract InitConfigLayerZero is BaseMultiChainDeployer { + using stdJson for string; + + error IncentivesNotFound(); + + string bridge_config; + + bytes32 constant KECCACK_OF_NOTHING = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + + // define a list of AMB mappings so we can get their addresses. + mapping(string => address) escrow; + mapping(string => uint32 eid) chainEid; + + function _setInitConfig(string[] memory counterPartyChains) internal { + IncentivizedLayerZeroEscrow lzescrow = IncentivizedLayerZeroEscrow(payable(escrow[currentChainKey])); + ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(lzescrow.ENDPOINT()); + endpoint.eid(); + + // Get all eids. + uint32[] memory remoteEids = getEids(counterPartyChains); + + // Get 1 example of the send library. + address sendLibrary = endpoint.getSendLibrary(address(lzescrow), remoteEids[0]); + + lzescrow.initConfig(sendLibrary, remoteEids); + } + + function _initConfig(string[] memory chains, string[] memory counterPartyChains) iter_chains_string(chains) broadcast internal { + _setInitConfig(counterPartyChains); + } + + function _loadEids(string[] memory chains) iter_chains_string(chains) internal { + IncentivizedLayerZeroEscrow lzescrow = IncentivizedLayerZeroEscrow(payable(escrow[currentChainKey])); + ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(lzescrow.ENDPOINT()); + chainEid[currentChainKey] = endpoint.eid(); + } + + function _loadAllEids(string[] memory chains, string[] memory counterPartyChains) internal { + _loadEids(combineString(chains, counterPartyChains)); + } + + function initConfig(string[] memory chains, string[] memory counterPartyChains) load_config external { + _loadAllEids(chains, counterPartyChains); + _initConfig(chains, counterPartyChains); + } + + //-- Helpers --// + function getEids(string[] memory chains) public view returns(uint32[] memory eids) { + eids = new uint32[](chains.length); + for (uint256 i = 0; i < chains.length; ++i) { + eids[i] = chainEid[chains[i]]; + } + } + + function combineString(string[] memory a, string[] memory b) public pure returns (string[] memory all_chains) { + all_chains = new string[](a.length + b.length); + uint256 i = 0; + for (i = 0; i < a.length; ++i) { + all_chains[i] = a[i]; + } + uint256 j = 0; + for (uint256 p = 0; p < b.length; ++p) { + string memory selected = b[p]; + bool found = false; + for (uint256 q = 0; q < a.length; ++q) { + if (keccak256(abi.encode(a[q])) == keccak256(abi.encode(selected))) { + found = true; + break; + } + } + if (!found) { + all_chains[i+j] = selected; + ++j; + } + } + string[] memory all_chains_copy = all_chains; + all_chains = new string[](i+j); + for (i = 0; i < all_chains.length; ++i) { + all_chains[i] = all_chains_copy[i]; + } + } + + function filter(string[] memory a, string memory val) public pure returns(string[] memory filtered) { + filtered = new string[](a.length - 1); + uint256 j = 0; + for (uint256 i = 0; i < a.length; ++i) { + string memory currentElement = a[i]; + if (keccak256(abi.encodePacked(val)) == keccak256(abi.encodePacked(currentElement))) continue; + filtered[j] = currentElement; + ++j; + } + require(j == a.length - 1, "Invalid Index"); // May be because val is replicated in a. + } + + modifier load_config() { + string memory pathRoot = vm.projectRoot(); + string memory pathToAmbConfig = string.concat(pathRoot, "/script/bridge_contracts.json"); + + bridge_config = vm.readFile(pathToAmbConfig); + + string memory bridge = "LayerZero"; + // Get the chains this bridge supports. + string[] memory availableBridgesChains = vm.parseJsonKeys(bridge_config, string.concat(".", bridge)); + for (uint256 j = 0; j < availableBridgesChains.length; ++j) { + string memory chain = availableBridgesChains[j]; + // Decode the address + address escrowContract = vm.parseJsonAddress(bridge_config, string.concat(".", bridge, ".", chain, ".escrow")); + escrow[chain] = escrowContract; + } + + _; + } +} + diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 71df430..cbb4387 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -86,7 +86,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero bytes constant LAYERZERO_OPTIONS = abi.encodePacked(TYPE_3); /** @notice The Layer Zero Endpoint. It is the destination for packages & configuration */ - ILayerZeroEndpointV2 immutable ENDPOINT; + ILayerZeroEndpointV2 public immutable ENDPOINT; /** @notice chainid is immutable on LayerZero endpoint, so we read it and store it likewise. */ uint32 public immutable chainId; From 1f9ae2fa996767f28c0d09cae7557266c62ac3f0 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 5 Jun 2024 18:00:14 +0200 Subject: [PATCH 48/61] feat: add broadcast to git ignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 320fbc5..4154e60 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ out/ lcov.info # Ignores development broadcast logs -!/broadcast +broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ From fc4bcb34c9aa6a7469049129a4cdf3d4fb869a25 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 5 Jun 2024 18:00:51 +0200 Subject: [PATCH 49/61] feat: small fix for LZ contracts --- script/bridge_contracts.json | 8 ++------ src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/script/bridge_contracts.json b/script/bridge_contracts.json index 191168e..4fbd3a0 100644 --- a/script/bridge_contracts.json +++ b/script/bridge_contracts.json @@ -50,15 +50,11 @@ "LayerZero": { "basesepolia": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0xC63E56cbe30b0D057ceaB70Eaf24A27dE8a3A835" + "escrow": "0xDb93559e30F5A3845438DDcf7Ca8A2D6D9005d30" }, "optimismsepolia": { "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0xC63E56cbe30b0D057ceaB70Eaf24A27dE8a3A835" - }, - "blasttestnet": { - "bridge": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "escrow": "0xC63E56cbe30b0D057ceaB70Eaf24A27dE8a3A835" + "escrow": "0xDb93559e30F5A3845438DDcf7Ca8A2D6D9005d30" } } } \ No newline at end of file diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index cbb4387..49b763d 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -244,7 +244,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero ); allowExternalCall = 1; // Set the cost of the sendPacket to msg.value - costOfsendPacketInNativeToken = uint128(msg.value - receipt.fee.nativeFee); + costOfsendPacketInNativeToken = uint128(receipt.fee.nativeFee); return costOfsendPacketInNativeToken; } From b464270aee692acf5468d9a1b2eb6a47049a5d7d Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 5 Jun 2024 18:01:01 +0200 Subject: [PATCH 50/61] feat: submit message script --- script/SimpleApplication.sol | 50 ++++++++++++++++++++++++++++++++++++ script/SubmitMessage.s.sol | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 script/SimpleApplication.sol create mode 100644 script/SubmitMessage.s.sol diff --git a/script/SimpleApplication.sol b/script/SimpleApplication.sol new file mode 100644 index 0000000..b86bc43 --- /dev/null +++ b/script/SimpleApplication.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { ICrossChainReceiver } from "../src/interfaces/ICrossChainReceiver.sol"; +import { IIncentivizedMessageEscrow } from "../src/interfaces/IIncentivizedMessageEscrow.sol"; + +/** + * @title Example application contract + */ +contract SimpleApplication is ICrossChainReceiver { + + event Event(bytes message); + + IIncentivizedMessageEscrow immutable MESSAGE_ESCROW; + + constructor(address messageEscrow_) { + MESSAGE_ESCROW = IIncentivizedMessageEscrow(messageEscrow_); + } + + function submitMessage( + bytes32 destinationIdentifier, + bytes calldata destinationAddress, + bytes calldata message, + IIncentivizedMessageEscrow.IncentiveDescription calldata incentive, + uint64 deadline + ) external payable returns(uint256 gasRefund, bytes32 messageIdentifier) { + (gasRefund, messageIdentifier) = MESSAGE_ESCROW.submitMessage{value: msg.value}( + destinationIdentifier, + destinationAddress, + message, + incentive, + deadline + ); + } + + function setRemoteImplementation(bytes32 destinationIdentifier, bytes calldata implementation) external { + MESSAGE_ESCROW.setRemoteImplementation(destinationIdentifier, implementation); + } + + function receiveAck(bytes32 /* destinationIdentifier */, bytes32 /* messageIdentifier */, bytes calldata acknowledgement) external { + emit Event(acknowledgement); + } + + function receiveMessage(bytes32 /* sourceIdentifierbytes */, bytes32 /* messageIdentifier */, bytes calldata /* fromApplication */, bytes calldata message) external returns(bytes calldata acknowledgement) { + emit Event(message); + return message; + } + + receive() external payable {} +} diff --git a/script/SubmitMessage.s.sol b/script/SubmitMessage.s.sol new file mode 100644 index 0000000..8b1698b --- /dev/null +++ b/script/SubmitMessage.s.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import { BaseMultiChainDeployer} from "./BaseMultiChainDeployer.s.sol"; + +// Import all the Apps for deployment here. +import { IMessageEscrowStructs } from "../src/interfaces/IMessageEscrowStructs.sol"; +import { IIncentivizedMessageEscrow } from "../src/interfaces/IIncentivizedMessageEscrow.sol"; + +import { SimpleApplication } from "./SimpleApplication.sol"; + +contract SubmitMessage is BaseMultiChainDeployer { + function submitMessage(address app, bytes32 destinationIdentifier, bytes memory destinationAddress, string memory message, address refundGasTo) external broadcast { + IMessageEscrowStructs.IncentiveDescription memory incentive = IMessageEscrowStructs.IncentiveDescription({ + maxGasDelivery: 200000, + maxGasAck: 200000, + refundGasTo: refundGasTo, + priceOfDeliveryGas: 1 gwei, + priceOfAckGas: 1 gwei, + targetDelta: 0 + }); + + uint256 incentiveValue = 200000 * 1 gwei * 2; + + SimpleApplication(payable(app)).submitMessage{value: 2807712706467 + incentiveValue}( + destinationIdentifier, + destinationAddress, + abi.encodePacked(message), + incentive, + 0 + ); + } + + function setRemoteImplementation(address app, bytes32 destinationIdentifier, bytes calldata implementation) broadcast external { + SimpleApplication(payable(app)).setRemoteImplementation(destinationIdentifier, implementation); + } + + function deploySimpleApplication(string[] memory chains, address escrow) iter_chains_string(chains) broadcast external returns(address app) { + app = address(new SimpleApplication{salt: bytes32(0)}(escrow)); + } +} + From 869d3f7636732936e063a46927175bde5ded573b Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 6 Jun 2024 13:03:24 +0200 Subject: [PATCH 51/61] feat: process message script --- script/layerzero/LzTryExecute.s.sol | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 script/layerzero/LzTryExecute.s.sol diff --git a/script/layerzero/LzTryExecute.s.sol b/script/layerzero/LzTryExecute.s.sol new file mode 100644 index 0000000..446609a --- /dev/null +++ b/script/layerzero/LzTryExecute.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import { BaseMultiChainDeployer} from "../BaseMultiChainDeployer.s.sol"; + +import { IncentivizedLayerZeroEscrow } from "../../src/apps/layerzero/IncentivizedLayerZeroEscrow.sol"; + +contract LZProcessMessage is BaseMultiChainDeployer { + function processMessage( + address escrow, + bytes calldata rawMessage, + bytes32 feeRecipient + ) external broadcast { + IncentivizedLayerZeroEscrow(payable(escrow)).processPacket{value: 2806592784579}(hex"", rawMessage, feeRecipient); + } +} + From d092375651a387768686f618334a682676a099ef Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 6 Jun 2024 13:03:36 +0200 Subject: [PATCH 52/61] feat: set proofValidPeriod to 1 month --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 49b763d..8706051 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -112,7 +112,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero } function _proofValidPeriod(bytes32 /* destinationIdentifier */) override internal pure returns(uint64 timestamp) { - return 0; // TODO: Set to something like 1 month. + return 30 days; } /** From f4a44584a565ca0e17d557dc6251a9c5f5508b4c Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 6 Jun 2024 13:14:06 +0200 Subject: [PATCH 53/61] test: fix test with proof valid period --- test/layerzero/TestLZSendPacket.t.sol | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/layerzero/TestLZSendPacket.t.sol b/test/layerzero/TestLZSendPacket.t.sol index 91f74f0..0c6c76a 100644 --- a/test/layerzero/TestLZSendPacket.t.sol +++ b/test/layerzero/TestLZSendPacket.t.sol @@ -16,9 +16,12 @@ contract TestLZSendPacket is LZCommon { super.setUp(); } - function test_send_packet(address target, bytes calldata message, uint64 deadline) external { + function test_send_packet(address target, bytes calldata message, uint32 deadline) external { + vm.assume(deadline < 30 days); _set_init_config(); + deadline = deadline + uint32(block.timestamp); + uint64 nonce = 1; uint32 dstEid = remoteEid; bytes32 receiver = bytes32(uint256(uint160(target))); @@ -47,9 +50,4 @@ contract TestLZSendPacket is LZCommon { ); layerZeroEscrow.sendPacket(bytes32(uint256(remoteEid)), abi.encodePacked(bytes32(uint256(uint160(target)))), message, deadline); } - - function test_revert_no_init_config_send_packet(address target, bytes calldata message, uint64 deadline) external { - vm.expectRevert(); - layerZeroEscrow.sendPacket(bytes32(uint256(remoteEid)), abi.encodePacked(bytes32(uint256(uint160(target)))), message, deadline); - } } \ No newline at end of file From 69a790518837b9f727d6730df48c874e0779b5b4 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 6 Jun 2024 15:29:10 +0200 Subject: [PATCH 54/61] feat: add additional estimateAdditionalCost view function --- src/apps/mock/IncentivizedMockEscrow.sol | 5 +++++ src/apps/mock/OnRecvIncentivizedMockEscrow.sol | 5 +++++ src/apps/polymer/APolymerEscrow.sol | 5 +++++ src/apps/wormhole/IncentivizedWormholeEscrow.sol | 5 +++++ src/interfaces/IIncentivizedMessageEscrow.sol | 8 ++++++++ .../processMessage/Reentry.call.t.sol | 2 +- test/layerzero/TestLZMisc.t.sol | 2 +- 7 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/apps/mock/IncentivizedMockEscrow.sol b/src/apps/mock/IncentivizedMockEscrow.sol index ca8f741..32cc41f 100644 --- a/src/apps/mock/IncentivizedMockEscrow.sol +++ b/src/apps/mock/IncentivizedMockEscrow.sol @@ -30,6 +30,11 @@ contract IncentivizedMockEscrow is IncentivizedMessageEscrow, Ownable2Step { amount = costOfMessages; } + function estimateAdditionalCost(bytes32 /* destinationChainIdentifier */) external view returns(address asset, uint256 amount) { + asset = address(0); + amount = costOfMessages; + } + function collectPayments() external { unchecked { payable(owner()).transfer(accumulator - 1); diff --git a/src/apps/mock/OnRecvIncentivizedMockEscrow.sol b/src/apps/mock/OnRecvIncentivizedMockEscrow.sol index 933946f..4489555 100644 --- a/src/apps/mock/OnRecvIncentivizedMockEscrow.sol +++ b/src/apps/mock/OnRecvIncentivizedMockEscrow.sol @@ -38,6 +38,11 @@ contract OnRecvIncentivizedMockEscrow is IncentivizedMessageEscrow { amount = 0; } + function estimateAdditionalCost(bytes32 /* destinationChainIdentifier */) external pure returns(address asset, uint256 amount) { + asset = address(0); + amount = 0; + } + function _proofValidPeriod(bytes32 /* destinationIdentifier */) override internal pure returns(uint64) { return 0; } diff --git a/src/apps/polymer/APolymerEscrow.sol b/src/apps/polymer/APolymerEscrow.sol index 198bd14..63fc47e 100644 --- a/src/apps/polymer/APolymerEscrow.sol +++ b/src/apps/polymer/APolymerEscrow.sol @@ -25,6 +25,11 @@ abstract contract APolymerEscrow is IncentivizedMessageEscrow { amount = 0; } + function estimateAdditionalCost(bytes32 /* destinationChainIdentifier */) external pure returns(address asset, uint256 amount) { + asset = address(0); + amount = 0; + } + function _uniqueSourceIdentifier() internal view override returns (bytes32 sourceIdentifier) { return sourceIdentifier = bytes32(block.chainid); } diff --git a/src/apps/wormhole/IncentivizedWormholeEscrow.sol b/src/apps/wormhole/IncentivizedWormholeEscrow.sol index a6620c3..50038ac 100644 --- a/src/apps/wormhole/IncentivizedWormholeEscrow.sol +++ b/src/apps/wormhole/IncentivizedWormholeEscrow.sol @@ -45,6 +45,11 @@ contract IncentivizedWormholeEscrow is IncentivizedMessageEscrow, WormholeVerifi amount = WORMHOLE.messageFee(); } + function estimateAdditionalCost(bytes32 /* destinationChainIdentifier */) external view returns(address asset, uint256 amount) { + asset = address(0); + amount = WORMHOLE.messageFee(); + } + /** @notice Wormhole proofs are valid until the guardian set is changed. The new guradian set may sign a new VAA */ function _proofValidPeriod(bytes32 /* destinationIdentifier */) override internal pure returns(uint64) { return 0; diff --git a/src/interfaces/IIncentivizedMessageEscrow.sol b/src/interfaces/IIncentivizedMessageEscrow.sol index c84a10c..d33a903 100644 --- a/src/interfaces/IIncentivizedMessageEscrow.sol +++ b/src/interfaces/IIncentivizedMessageEscrow.sol @@ -36,6 +36,14 @@ interface IIncentivizedMessageEscrow is IMessageEscrowStructs, IMessageEscrowErr * @return amount The number of assets to pay. */ function estimateAdditionalCost() external view returns(address asset, uint256 amount); + + /** + * @notice Estimates the additional cost to the messaging router to validate the message + * @param destinationChainIdentifier Destination chain. Some messaging protocols have chain based costs. + * @return asset The asset the token is in. If native token, returns address(0); + * @return amount The number of assets to pay. + */ + function estimateAdditionalCost(bytes32 destinationChainIdentifier) external view returns(address asset, uint256 amount); function timeoutMessage( bytes32 sourceIdentifier, diff --git a/test/IncentivizedMessageEscrow/processMessage/Reentry.call.t.sol b/test/IncentivizedMessageEscrow/processMessage/Reentry.call.t.sol index 62c5305..7f2455b 100644 --- a/test/IncentivizedMessageEscrow/processMessage/Reentry.call.t.sol +++ b/test/IncentivizedMessageEscrow/processMessage/Reentry.call.t.sol @@ -61,7 +61,7 @@ contract CallReentryTest is TestCommon, ICrossChainReceiver { messageIdentifier, _DESTINATION_ADDRESS_APPLICATION, feeRecipient, - uint48(0xfcb6), // Gas used + uint48(0xfccc), // Gas used uint64(1), uint8(1) ) diff --git a/test/layerzero/TestLZMisc.t.sol b/test/layerzero/TestLZMisc.t.sol index 5e60b5a..445914b 100644 --- a/test/layerzero/TestLZMisc.t.sol +++ b/test/layerzero/TestLZMisc.t.sol @@ -40,6 +40,6 @@ contract TestLZMisc is LZCommon { address(layerZeroEscrow), abi.encodeCall(layerZeroEscrow.getFee, (remoteEid, address(layerZeroEscrow), 0, hex"")) ); - layerZeroEscrow.estimateAdditionalCost(remoteEid); + layerZeroEscrow.estimateAdditionalCost(bytes32(uint256(remoteEid))); } } \ No newline at end of file From bb8c4d90465b00b4281bc9ef5df65f569ae9ba13 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 6 Jun 2024 17:56:06 +0200 Subject: [PATCH 55/61] feat: final LZ changes & code documentation --- .../layerzero/IncentivizedLayerZeroEscrow.sol | 107 ++++++++++++------ test/layerzero/TestLZVerifyPacket.t.sol | 2 +- 2 files changed, 75 insertions(+), 34 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 8706051..6bdde7a 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -13,6 +13,8 @@ import { IReceiveUlnBase, UlnConfig, Verification } from "./interfaces/IUlnBase. /** * @notice Always returns 0 to any job. * @dev We have set ourself as the executor. As a result, we need to implement the executor interfaces. + * Ideally we would revert when the sender is not us. However, that assumes that people would trust random + * contracts and set them as their executor. That shouldn't happen. As a result, we save the gas and don't check. */ contract ExecutorZero is ILayerZeroExecutor { function assignJob( @@ -51,6 +53,10 @@ contract ExecutorZero is ILayerZeroExecutor { * application. While permissionwise, it is 1/N, the economic security is 1/1. * * @dev This contract only allows messages smaller than or equal to 65536 bytes to be sent. + * + * Before using a deployed version of this contract `initConfig` has to be called to set ourself as + * the executor. This has to be done for every remote chain & ULN. + * * This implementation works by breaking the LZ endpoint flow. It relies on the * `.verfiyable` check on the ULN. When a cross-chain message is verified (step 2) * `commitVerification` is called and it deletes the storage for the verification: https://github.com/LayerZero-Labs/LayerZero-v2/blob/1fde89479fdc68b1a54cda7f19efa84483fcacc4/messagelib/contracts/uln/uln302/ReceiveUln302.sol#L56 @@ -63,9 +69,14 @@ contract ExecutorZero is ILayerZeroExecutor { */ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero { using PacketV1Codec for bytes; - uint32 CONFIG_TYPE_EXECUTOR = 1; - uint32 MAX_MESSAGE_SIZE = 65536; + /** @notice Executor config type. We use this when we set ourself as the executor on the endpoint. */ + uint32 private constant CONFIG_TYPE_EXECUTOR = 1; + + /** @notice Only allow messages of size 65536 bytes and smaller. */ + uint32 constant MAX_MESSAGE_SIZE = 65536; + + /** @notice LZ Config type struct for setting an executor. */ struct ConfigTypeExecutor { uint32 maxMessageSize; address executorAddress; @@ -73,7 +84,8 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero // Errors specific to this contract. error LayerZeroCannotBeAddress0(); - error IncorrectDestination(address actual); + error IncorrectDestination(); + error NoReceive(); // Errors inherited from LZ. error LZ_ULN_Verifying(); @@ -81,9 +93,10 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero error LZ_ULN_InvalidPacketVersion(); error LZ_ULN_InvalidEid(); - uint16 internal constant TYPE_3 = 3; + /** @notice LZ messaging options. This is the option type and has to be set. */ + uint16 private constant TYPE_3 = 3; /** @notice Set the LayerZero options. Needs to be 2 bytes with a version for the optionsSplit Library to process. */ - bytes constant LAYERZERO_OPTIONS = abi.encodePacked(TYPE_3); + bytes private constant LAYERZERO_OPTIONS = abi.encodePacked(TYPE_3); /** @notice The Layer Zero Endpoint. It is the destination for packages & configuration */ ILayerZeroEndpointV2 public immutable ENDPOINT; @@ -91,8 +104,8 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero /** @notice chainid is immutable on LayerZero endpoint, so we read it and store it likewise. */ uint32 public immutable chainId; - /** @notice Only allow LZ to send value to this contract */ - uint8 allowExternalCall = 1; + /** @notice Only allow LZ to send value to this contract. */ + uint8 internal allowExternalCall = 1; /** * @param sendLostGasTo Address to get gas that could not get sent to the recipitent. @@ -100,26 +113,38 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero */ constructor(address sendLostGasTo, address lzEndpointV2) IncentivizedMessageEscrow(sendLostGasTo) { if (lzEndpointV2 == address(0)) revert LayerZeroCannotBeAddress0(); - // Load the LZ endpoint. This is the contract we will be sending events to. ENDPOINT = ILayerZeroEndpointV2(lzEndpointV2); // Set chainId. - chainId = ENDPOINT.eid(); + chainId = ENDPOINT.eid(); } + /** @notice LayerZero identifies chains based on "eid"s. */ function _uniqueSourceIdentifier() override internal view returns(bytes32) { return bytes32(uint256(chainId)); } + /** + * @notice LayerZero proofs are by default non-expiring. However, the administrator can set + * a new receiveLibrary. When they do this, they invalidate the previous receiveLibrary and + * any associated proofs. As a result, the owners of the endpoint can determine when and if + * proofs should be invalidated. + * On one hand, you could arguemt that this warrant a timeout of 0, since these messages could + * be recovered and ordinary usage would imply unlimited. However, since the structure of + * LayerZero generally does not encorage 'recovery', it has been set to 30 days ≈ 1 month. + */ function _proofValidPeriod(bytes32 /* destinationIdentifier */) override internal pure returns(uint64 timestamp) { return 30 days; } /** * @notice Set ourself as executor on all (provided) remote chains. This is required before anyone - * can send message out to that chain. + * can send message to any chain.. * @dev sendLibrary is not checked. It is assumed that any endpoint will accept anything as long as it is somewhat sane. - * @param sendLibrary Contract to set config on. + * The reference LZ endpoint requires that sendLibrary is a owner approved once and that fits the requirement. + * This call also sets maxMessageSize to MAX_MESSAGE_SIZE. + * The reason why we can't read the sendLibrary is because it may depend on the EID. + * @param sendLibrary sendLibrary to configure to use this contract as executor. * @param remoteEids List of remote Eids to set config on. */ function initConfig(address sendLibrary, uint32[] calldata remoteEids) external { @@ -147,9 +172,11 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero /** * @notice Block any calls from the LZ endpoint so that no messages can ever get "verified" on the endpoint. - * This is very important, as otherwise, the package status can progress on the LZ endpoint which causes - * `verifiyable` which we rely on to be able to switch from true to false by commiting the proof to the endpoint. - * While this function is not intended for this use case, it should work. + * This contract relies on a `verifiyable` call on the LZ receiveULN. In an ordinary config, when + * `verifiyable` returns true, the package state can progress by calling `commitVerification` and + * `verifiyable` switched from true to false. This breaks our flow. The LZ Endpoint calls `allowInitializePath` + * during this flow and this function is intended to break that. + * As a result, when `verifiyable` switches from false => true it cannot be switched true => false. */ function allowInitializePath(Origin calldata /* _origin */) external pure returns(bool) { return false; @@ -159,7 +186,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero MessagingParams memory params = MessagingParams({ dstEid: uint32(destEid), receiver: bytes32(0), // Is unused by LZ. - message: hex"", + message: hex"", // Is sent to the executor as length. We don't care about it so set it as small as possible. options: LAYERZERO_OPTIONS, payInLzToken: false }); @@ -174,24 +201,30 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero * For a better quote, use the function overload. */ function estimateAdditionalCost() external view returns(address asset, uint256 amount) { - (asset, amount) = estimateAdditionalCost(chainId); + (asset, amount) = estimateAdditionalCost(bytes32(uint256(chainId))); } /** - * @notice Get an exact quote. + * @notice Get an exact quote. LayerZero charges based on the destination chain. */ - function estimateAdditionalCost(uint256 destinationChainId) public view returns(address asset, uint256 amount) { - amount = _estimateAdditionalCost(uint32(destinationChainId)); + function estimateAdditionalCost(bytes32 destinationChainId) public view returns(address asset, uint256 amount) { + amount = _estimateAdditionalCost(uint32(uint256(destinationChainId))); asset = address(0); } - function _verifyPacket(bytes calldata /* _packetHeader */, bytes calldata _packet) view internal override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { + /** + * @notice Verification of LayerZero packages. This function takes the whole LZ package + * as _packet rather than splitting the package in two. + * The function works by getting the defaultReceiveLibrary from the endpoint. We never set a specific receiveLibrary + * so we use the default. Getting the defaultReceiveLibrary directly is slightly cheaper. + * + * On the receiveLibrary, `verifiable` is called to check if it has been verified. + * If not, it is checked if a timeoutLibrary (the receiveLibrary has recently been changed) is available + * and then `verifiable` is checked on the timeoutLibrary. + */ + function _verifyPacket(bytes calldata /* context */, bytes calldata _packet) view internal override returns(bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) { _assertHeader(_packet.header()); - // Check that we are the receiver - address receiver = _packet.receiverB20(); - if (receiver != address(this)) revert IncorrectDestination(receiver); - // Get the source chain. uint32 srcEid = _packet.srcEid(); @@ -210,8 +243,8 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero bool verifyable = ULN.verifiable(_config, _headerHash, _payloadHash); if (!verifyable) { // LayerZero may have migrated to a new receive library. Check the timeout receive library. - (address timeoutULN, ) = ENDPOINT.defaultReceiveLibraryTimeout(srcEid); - if (timeoutULN == address(0)) revert LZ_ULN_Verifying(); + (address timeoutULN, uint256 expiry) = ENDPOINT.defaultReceiveLibraryTimeout(srcEid); + if (timeoutULN == address(0) || expiry < block.timestamp) revert LZ_ULN_Verifying(); verifyable = IReceiveUlnBase(timeoutULN).verifiable(_config, _headerHash, _payloadHash); if (!verifyable) revert LZ_ULN_Verifying(); } @@ -224,6 +257,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero message_ = _packet.message(); } + /** @notice Delivers a package to the LZ endpoint. */ function _sendPacket(bytes32 destinationChainIdentifier, bytes memory destinationImplementation, bytes memory message, uint64 /* deadline */) internal override returns(uint128 costOfsendPacketInNativeToken) { MessagingParams memory params = MessagingParams({ @@ -234,15 +268,15 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero payInLzToken: false }); - // Handoff package to LZ. - // We are getting a refund on any excess value we sent. We can get the native fee by subtracting it from - // the value we sent. - allowExternalCall = 2; + // Handoff package to LZ. We are getting a refund on any excess value we sent. Then + // receipt.fee.nativeFee is the fee we paid. + allowExternalCall = 2; // Allow refunds from LZ MessagingReceipt memory receipt = ENDPOINT.send{value: msg.value}( params, address(this) ); - allowExternalCall = 1; + allowExternalCall = 1; // Disallow other refunds. + // Set the cost of the sendPacket to msg.value costOfsendPacketInNativeToken = uint128(receipt.fee.nativeFee); @@ -252,14 +286,21 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero // Allow LZ refunds to come in while disallowing randoms from sending to this contract. // It won't stop abuses but it is the best we can do. receive() external payable { - // allowExternalCall is hot so it shouldn't be that expensive to read. - require(allowExternalCall != 1, "Do not send ether to this address"); + // allowExternalCall is hot so it shouldn't be expensive to read. + if (allowExternalCall == 1) revert NoReceive(); } + /** + * @notice Validate the package header. Similar to the LZ version except + * This function doesn't check length (it is enforced by library) and + * it checks if we are the receiver. + */ function _assertHeader(bytes calldata _packetHeader) internal view { // assert packet header version is the same as ULN if (_packetHeader.version() != PacketV1Codec.PACKET_VERSION) revert LZ_ULN_InvalidPacketVersion(); // assert the packet is for this endpoint if (_packetHeader.dstEid() != chainId) revert LZ_ULN_InvalidEid(); + // Check that we are the receiver + if (_packetHeader.receiverB20() != address(this)) revert IncorrectDestination(); } } \ No newline at end of file diff --git a/test/layerzero/TestLZVerifyPacket.t.sol b/test/layerzero/TestLZVerifyPacket.t.sol index 1e7bde7..e933a6e 100644 --- a/test/layerzero/TestLZVerifyPacket.t.sol +++ b/test/layerzero/TestLZVerifyPacket.t.sol @@ -149,7 +149,7 @@ contract TestLZVerifyPacket is LZCommon { 10 ); - vm.expectRevert(abi.encodeWithSignature("IncorrectDestination(address)", receiver)); + vm.expectRevert(abi.encodeWithSignature("IncorrectDestination()")); layerZeroEscrow.verifyPacket(hex"", _packet); } From 040e17547f423967cb3e7d57ffcb1c118c639ae6 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 17 Jun 2024 11:54:22 +0200 Subject: [PATCH 56/61] feat: set license to MIT --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 2 +- src/apps/layerzero/README.md | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 6bdde7a..8621aab 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: DO-NOT-USE +// SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import { ILayerZeroEndpointV2, MessagingParams, MessagingFee, MessagingReceipt, Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; diff --git a/src/apps/layerzero/README.md b/src/apps/layerzero/README.md index f0a8306..9936fcc 100644 --- a/src/apps/layerzero/README.md +++ b/src/apps/layerzero/README.md @@ -1,5 +1,3 @@ # Wormhole Generalised Incentives -This is a Layer Zero implementation of Generalised Incentives. - -The contracts are not allowed to be used in production. \ No newline at end of file +This is a Layer Zero implementation of Generalised Incentives. \ No newline at end of file From 0d9f2ba8324e83b4787291f42508910f18a66b5d Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 17 Jun 2024 11:57:12 +0200 Subject: [PATCH 57/61] fix: remove unused import and error --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 6bdde7a..94cb04b 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.22; import { ILayerZeroEndpointV2, MessagingParams, MessagingFee, MessagingReceipt, Origin } from "LayerZero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { ILayerZeroExecutor } from "LayerZero-v2/messagelib/contracts/interfaces/ILayerZeroExecutor.sol"; -import { IMessageLibManager, SetConfigParam } from "LayerZero-v2/protocol/contracts/interfaces/IMessageLibManager.sol"; +import { SetConfigParam } from "LayerZero-v2/protocol/contracts/interfaces/IMessageLibManager.sol"; import { PacketV1Codec } from "LayerZero-v2/protocol/contracts/messagelib/libs/PacketV1Codec.sol"; import { IncentivizedMessageEscrow } from "../../IncentivizedMessageEscrow.sol"; @@ -89,7 +89,6 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero // Errors inherited from LZ. error LZ_ULN_Verifying(); - error LZ_ULN_InvalidPacketHeader(); error LZ_ULN_InvalidPacketVersion(); error LZ_ULN_InvalidEid(); From 2cf24c618015b34512f41563e8ee375fea01c553 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 17 Jun 2024 11:59:22 +0200 Subject: [PATCH 58/61] fix: typos --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 14 +++++++------- src/apps/layerzero/interfaces/IUlnBase.sol | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 6bdde7a..6daa5cd 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -37,7 +37,7 @@ contract ExecutorZero is ILayerZeroExecutor { } /** - * @title Incentivized LayerZero Messag Escrow + * @title Incentivized LayerZero Message Escrow * @notice Provides an alternative pathway to incentivize LayerZero message relaying. * While Layer Zero has a native way to incentivize message relaying, it lacks: * - Gas refunds of unspent gas. @@ -129,7 +129,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero * a new receiveLibrary. When they do this, they invalidate the previous receiveLibrary and * any associated proofs. As a result, the owners of the endpoint can determine when and if * proofs should be invalidated. - * On one hand, you could arguemt that this warrant a timeout of 0, since these messages could + * On one hand, you could argument that this warrant a timeout of 0, since these messages could * be recovered and ordinary usage would imply unlimited. However, since the structure of * LayerZero generally does not encorage 'recovery', it has been set to 30 days ≈ 1 month. */ @@ -172,11 +172,11 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero /** * @notice Block any calls from the LZ endpoint so that no messages can ever get "verified" on the endpoint. - * This contract relies on a `verifiyable` call on the LZ receiveULN. In an ordinary config, when - * `verifiyable` returns true, the package state can progress by calling `commitVerification` and - * `verifiyable` switched from true to false. This breaks our flow. The LZ Endpoint calls `allowInitializePath` + * This contract relies on a `verifiable` call on the LZ receiveULN. In an ordinary config, when + * `verifiable` returns true, the package state can progress by calling `commitVerification` and + * `verifiable` switched from true to false. This breaks our flow. The LZ Endpoint calls `allowInitializePath` * during this flow and this function is intended to break that. - * As a result, when `verifiyable` switches from false => true it cannot be switched true => false. + * As a result, when `verifiable` switches from false => true it cannot be switched true => false. */ function allowInitializePath(Origin calldata /* _origin */) external pure returns(bool) { return false; @@ -209,7 +209,7 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero */ function estimateAdditionalCost(bytes32 destinationChainId) public view returns(address asset, uint256 amount) { amount = _estimateAdditionalCost(uint32(uint256(destinationChainId))); - asset = address(0); + asset = address(0); } /** diff --git a/src/apps/layerzero/interfaces/IUlnBase.sol b/src/apps/layerzero/interfaces/IUlnBase.sol index 0ecb06b..8033481 100644 --- a/src/apps/layerzero/interfaces/IUlnBase.sol +++ b/src/apps/layerzero/interfaces/IUlnBase.sol @@ -9,8 +9,8 @@ struct UlnConfig { uint8 requiredDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) uint8 optionalDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) uint8 optionalDVNThreshold; // (0, optionalDVNCount] - address[] requiredDVNs; // no duplicates. sorted an an ascending order. allowed overlap with optionalDVNs - address[] optionalDVNs; // no duplicates. sorted an an ascending order. allowed overlap with requiredDVNs + address[] requiredDVNs; // no duplicates. sorted in an ascending order. allowed overlap with optionalDVNs + address[] optionalDVNs; // no duplicates. sorted in an ascending order. allowed overlap with requiredDVNs } struct Verification { From 15bdf09ebca83447c792506e399610d31ae1f1ae Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 21 Jun 2024 12:44:01 +0200 Subject: [PATCH 59/61] fix: Last types of verifiable --- src/apps/layerzero/IncentivizedLayerZeroEscrow.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol index 7000c4d..5ad1249 100644 --- a/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol +++ b/src/apps/layerzero/IncentivizedLayerZeroEscrow.sol @@ -58,14 +58,14 @@ contract ExecutorZero is ILayerZeroExecutor { * the executor. This has to be done for every remote chain & ULN. * * This implementation works by breaking the LZ endpoint flow. It relies on the - * `.verfiyable` check on the ULN. When a cross-chain message is verified (step 2) + * `.verifiable` check on the ULN. When a cross-chain message is verified (step 2) * `commitVerification` is called and it deletes the storage for the verification: https://github.com/LayerZero-Labs/LayerZero-v2/blob/1fde89479fdc68b1a54cda7f19efa84483fcacc4/messagelib/contracts/uln/uln302/ReceiveUln302.sol#L56 - * this exactly `verfiyable: true -> false`. + * this exactly `verifiable: true -> false`. * We break this making the subcall `EndpointV2::verify` revert on _initializable: * https://github.com/LayerZero-Labs/LayerZero-v2/blob/1fde89479fdc68b1a54cda7f19efa84483fcacc4/protocol/contracts/EndpointV2.sol#L340 * That is the purpose of `allowInitializePath`. * - * Then we can use `verfiyable` to check if a message has been verified by DVNs. + * Then we can use `verifiable` to check if a message has been verified by DVNs. */ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero { using PacketV1Codec for bytes; @@ -239,13 +239,13 @@ contract IncentivizedLayerZeroEscrow is IncentivizedMessageEscrow, ExecutorZero // Verify the message on the LZ ultra light node. // Without any protection, this is a DoS vector. It is protected by setting allowInitializePath to return false // As a result, once this returns true it should return true perpetually. - bool verifyable = ULN.verifiable(_config, _headerHash, _payloadHash); - if (!verifyable) { + bool verifiable = ULN.verifiable(_config, _headerHash, _payloadHash); + if (!verifiable) { // LayerZero may have migrated to a new receive library. Check the timeout receive library. (address timeoutULN, uint256 expiry) = ENDPOINT.defaultReceiveLibraryTimeout(srcEid); if (timeoutULN == address(0) || expiry < block.timestamp) revert LZ_ULN_Verifying(); - verifyable = IReceiveUlnBase(timeoutULN).verifiable(_config, _headerHash, _payloadHash); - if (!verifyable) revert LZ_ULN_Verifying(); + verifiable = IReceiveUlnBase(timeoutULN).verifiable(_config, _headerHash, _payloadHash); + if (!verifiable) revert LZ_ULN_Verifying(); } // Get the source chain From db0c96e97b3247fe5835abd1478a8145f46e2f69 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 21 Jun 2024 13:12:14 +0200 Subject: [PATCH 60/61] feat: further types --- src/IncentivizedMessageEscrow.sol | 38 +++++++++---------- .../mock/OnRecvIncentivizedMockEscrow.sol | 6 +-- src/apps/polymer/vIBCEscrow.sol | 4 +- .../wormhole/IncentivizedWormholeEscrow.sol | 4 +- .../external/callworm/WormholeVerifier.sol | 6 +-- .../wormhole/external/wormhole/Messages.sol | 6 +-- .../wormhole/libraries/external/BytesLib.sol | 10 ++--- src/interfaces/IMessageEscrowEvents.sol | 2 +- 8 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/IncentivizedMessageEscrow.sol b/src/IncentivizedMessageEscrow.sol index 8024a6d..119ff2a 100644 --- a/src/IncentivizedMessageEscrow.sol +++ b/src/IncentivizedMessageEscrow.sol @@ -21,14 +21,14 @@ import "./MessagePayload.sol"; * and verify messages. There are 4 functions that an integration has to implement. * Any implementation of this contract, allows applications to deliver a message to ::submitMessage * along with the respective incentives. - * The integration (this contract) will handle transfering the message to the destination and + * The integration (this contract) will handle transferring the message to the destination and * returning an ack from the destination to the integrating application. * * The incentive is released when an ack from the destination chain is delivered to this contract. * * Beyond making relayer incentives stronger, this contract also implements several quality of life features: * - Refund unused gas. - * - Seperate gas payments for call and ack. + * - Separate gas payments for call and ack. * - Simple implementation of new messaging protocols. * * Applications integration with Generalised Incentives have to be aware that Acks are replayable. @@ -94,7 +94,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes * @notice Verifies the authenticity of a message. * @dev Should be overwritten by the specific messaging protocol verification structure. * onRecv. implementations should collect acks so _verifyPacket returns true after acks have been executed once. - * @param messagingProtocolContext Some context that is useful for verifing the message. + * @param messagingProtocolContext Some context that is useful for verifying the message. * It should not contain the message but instead verification context like signatures, header, etc. * Context may not be needed for verifying the message and can be prepended to rawMessage. * @param rawMessage Some kind of package, initially untrusted. Should contain the message as a slice @@ -123,7 +123,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes * @param destinationIdentifier The destination chain for the message. * @param destinationImplementation The destination escrow contract. * @param message The message. Contains relevant escrow context. - * @param deadline A timestamp that the message should be delivered before. If the AMB does not nativly + * @param deadline A timestamp that the message should be delivered before. If the AMB does not natively * support a timeout on their messages this parameter should be ignored. If 0 is provided, parse it as MAX. * @return costOfsendPacketInNativeToken An additional cost to emitting messages in NATIVE tokens. */ @@ -334,8 +334,8 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes * @param destinationIdentifier 32 bytes that identifies the destination chain. * @param destinationAddress The destination application encoded in 65 bytes: First byte is the length and last 64 is the destination application. * @param message The message to be sent to the destination. Please ensure the message is block-unique. - * This means that you don't send the same message twice in a single block. If you need to do that, add a nonce or noice. - * @param incentive The incentive to attatch to the bounty. The price of this incentive has to be paid, + * This means that you don't send the same message twice in a single block. If you need to do that, add a nonce or noise. + * @param incentive The incentive to attach to the bounty. The price of this incentive has to be paid, * any excess is refunded to refundGasTo. (not msg.sender) * @param deadline After this date, do not allow relayers to execute the message on the destination chain. If set to 0, disable timeouts. * Not all AMBs may support disabling the deadline. If acks are required it is recommended to set the deadline sometime in the future. @@ -477,7 +477,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes revert NotImplementedError(); } - // Check if there is a mis-match between the cost and the value of the message. + // Check if there is a mismatch between the cost and the value of the message. if (uint128(msg.value) != cost) { if (uint128(msg.value) > cost) { // Send the unused gas back to the the user. @@ -670,7 +670,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes // First check if the application trusts the implementation on the destination chain. bytes32 expectedDestinationImplementationHash = implementationAddressHash[fromApplication][destinationIdentifier]; // Check that the application approves the source implementation - // For acks, this should always be the case except when a fradulent applications sends a message to this contract. + // For acks, this should always be the case except when a fraudulent applications sends a message to this contract. if (expectedDestinationImplementationHash != keccak256(destinationImplementationIdentifier)) revert InvalidImplementationAddress(); // Deliver the ack to the application. @@ -825,7 +825,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes } /** - * @notice Verifies the input parameters are contained messageIdentfier and that the other arguments are valid. + * @notice Verifies the input parameters are contained messageIdentifier and that the other arguments are valid. * The usage of this function is intended when no parameters of a message can be trusted and we have to verify them. * This is the case when we receive a timeout, as the timeout had to be emitted without any verification * on the remote chain, for us to then verify since we know when a message identifier is good AND how to compute it. @@ -851,7 +851,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes fromApplication = address(uint160(bytes20(message[FROM_APPLICATION_START_EVM:FROM_APPLICATION_END]))); bytes32 expectedDestinationImplementationHash = implementationAddressHash[fromApplication][destinationIdentifier]; // Check that the application approves of the remote implementation. - // For timeouts, this could fail because of fradulent sender or bad data. + // For timeouts, this could fail because of fraudulent sender or bad data. if (expectedDestinationImplementationHash != keccak256(implementationIdentifier)) revert InvalidImplementationAddress(); // Do we need to check deadline again? @@ -926,7 +926,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes ackFee = gasSpentOnSource * priceOfAckGas; // deliveryFee + ackFee < 2**144 + 2**144 = 2**145 actualFee = deliveryFee + ackFee; - // (priceOfDeliveryGas * maxGasDelivery + priceOfDeliveryGas * maxGasAck) has been caculated before (escrowBounty) < (2**48 * 2**96) + (2**48 * 2**96) = 2**144 + 2**144 = 2**145 + // (priceOfDeliveryGas * maxGasDelivery + priceOfDeliveryGas * maxGasAck) has been calculated before (escrowBounty) < (2**48 * 2**96) + (2**48 * 2**96) = 2**144 + 2**144 = 2**145 uint256 maxDeliveryFee = maxGasDelivery * priceOfDeliveryGas; uint256 maxAckFee = maxGasAck * priceOfAckGas; uint256 maxFee = maxDeliveryFee + maxAckFee; @@ -1018,7 +1018,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes /** * @notice Sets a bounty for a message - * @dev Does not check if enough incentives have been provided, this is delegated as responsiblity + * @dev Does not check if enough incentives have been provided, this is delegated as responsibility * of the caller of this function. * @param fromApplication The application that called the contract. Should generally be msg.sender. Is used to separate storage between applications. * @param destinationIdentifier The destination chain. Combined with fromApplication, this specifics a unique remote escrow implementation. @@ -1044,7 +1044,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes /** * @notice Allows anyone to re-execute an ack which didn't properly execute. - * @dev No applciation should rely on this function. It should only be used incase an application has faulty logic. + * @dev No application should rely on this function. It should only be used incase an application has faulty logic. * Example: Faulty logic results in wrong enforcement on gas limit => out of gas? * * This function allows replaying acks. @@ -1094,21 +1094,21 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes bytes calldata implementationIdentifier, bytes calldata receiveAckWithContext ) external payable virtual { - // Has the package previously been executed? (otherwise timeout might be more appropiate) + // Has the package previously been executed? (otherwise timeout might be more appropriate) // Load the messageIdentifier from receiveAckWithContext. - // This makes it slighly easier to retry messages. + // This makes it slightly easier to retry messages. bytes32 messageIdentifier = bytes32(receiveAckWithContext[MESSAGE_IDENTIFIER_START:MESSAGE_IDENTIFIER_END]); bytes32 storedAckHash = _messageDelivered[sourceIdentifier][implementationIdentifier][messageIdentifier]; - // First, check if there is actually an appropiate hash at the message identifier. + // First, check if there is actually an appropriate hash at the message identifier. // Then, check if the storedAckHash & the source target (sourceIdentifier & implementationIdentifier) matches the executed one. if (storedAckHash == bytes32(0) || storedAckHash != keccak256(receiveAckWithContext)) revert CannotRetryWrongMessage(storedAckHash, keccak256(receiveAckWithContext)); // Send the package again. uint128 cost = _sendPacket(sourceIdentifier, implementationIdentifier, receiveAckWithContext, 0); - // Check if there is a mis-match between the cost and the value of the message. + // Check if there is a mismatch between the cost and the value of the message. if (uint128(msg.value) != cost) { if (uint128(msg.value) > cost) { // Send the unused gas back to the the user. @@ -1166,7 +1166,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes // When the message arrives, the usual incentive check ensures only 1 message can arrive. Since the incentive check is based on // messageIdentifier, we need to verify it. // Remember, the messageIdentifier is actually untrusted. So it is trivial to pass the above check. However, any way to pass - // the above check fradulently would result in messageIdentifier being wrong and unable to be reproduced on the source chain. + // the above check fraudulently would result in messageIdentifier being wrong and unable to be reproduced on the source chain. // Load the deadline from the message. uint64 deadline = uint64(bytes8(message[CTX0_DEADLINE_START:CTX0_DEADLINE_END])); @@ -1197,7 +1197,7 @@ abstract contract IncentivizedMessageEscrow is IIncentivizedMessageEscrow, Bytes 0 ); - // Check if there is a mis-match between the cost and the value of the message. + // Check if there is a mismatch between the cost and the value of the message. if (uint128(msg.value) != cost) { if (uint128(msg.value) > cost) { // Send the unused gas back to the the user. diff --git a/src/apps/mock/OnRecvIncentivizedMockEscrow.sol b/src/apps/mock/OnRecvIncentivizedMockEscrow.sol index 4489555..587e23c 100644 --- a/src/apps/mock/OnRecvIncentivizedMockEscrow.sol +++ b/src/apps/mock/OnRecvIncentivizedMockEscrow.sol @@ -116,9 +116,9 @@ contract OnRecvIncentivizedMockEscrow is IncentivizedMessageEscrow { bytes32 feeRecipient ) onlyMessagingProtocol external { uint256 gasLimit = gasleft(); - VerifiedMessageHashContext storage _verfiedMessageHashContext = isVerifiedMessageHash[keccak256(rawMessage)]; - _verfiedMessageHashContext.chainIdentifier = chainIdentifier; - _verfiedMessageHashContext.implementationIdentifier = destinationImplementationIdentifier; + VerifiedMessageHashContext storage _verifiedMessageHashContext = isVerifiedMessageHash[keccak256(rawMessage)]; + _verifiedMessageHashContext.chainIdentifier = chainIdentifier; + _verifiedMessageHashContext.implementationIdentifier = destinationImplementationIdentifier; _handleAck(chainIdentifier, destinationImplementationIdentifier, rawMessage, feeRecipient, gasLimit); } diff --git a/src/apps/polymer/vIBCEscrow.sol b/src/apps/polymer/vIBCEscrow.sol index ffaee8b..590570e 100644 --- a/src/apps/polymer/vIBCEscrow.sol +++ b/src/apps/polymer/vIBCEscrow.sol @@ -104,7 +104,7 @@ contract IncentivizedPolymerEscrow is APolymerEscrow, IbcReceiverBase, IbcReceiv } function onCloseIbcChannel(bytes32 channelId, string calldata, bytes32) external virtual onlyIbcDispatcher { - // logic to determin if the channel should be closed + // logic to determine if the channel should be closed bool channelFound = false; for (uint256 i = 0; i < connectedChannels.length; i++) { if (connectedChannels[i] == channelId) { @@ -158,7 +158,7 @@ contract IncentivizedPolymerEscrow is APolymerEscrow, IbcReceiverBase, IbcReceiv // Get the payload by removing the implementation identifier. bytes calldata rawMessage = ack.data[POLYMER_PACKAGE_PAYLOAD_START:]; - // Set a verificaiton context so we can recover the ack. + // Set a verification context so we can recover the ack. isVerifiedMessageHash[keccak256(rawMessage)] = VerifiedMessageHashContext({ chainIdentifier: packet.src.channelId, implementationIdentifier: destinationImplementationIdentifier diff --git a/src/apps/wormhole/IncentivizedWormholeEscrow.sol b/src/apps/wormhole/IncentivizedWormholeEscrow.sol index 50038ac..62ef596 100644 --- a/src/apps/wormhole/IncentivizedWormholeEscrow.sol +++ b/src/apps/wormhole/IncentivizedWormholeEscrow.sol @@ -10,7 +10,7 @@ import { IWormhole } from "./interfaces/IWormhole.sol"; /** * @title Incentivized Wormhole Message Escrow * @notice Incentivizes Wormhole messages through Generalised Incentives. - * Wormhole does not have any native way of relaying messages, this implemention adds one. + * Wormhole does not have any native way of relaying messages, this implementation adds one. * * When using Wormhole with Generalised Incentives and you don't want to lose message, be very careful regarding * emitting messages to destinationChainIdentifiers that does not exist. Wormhole has no way to verify if a @@ -50,7 +50,7 @@ contract IncentivizedWormholeEscrow is IncentivizedMessageEscrow, WormholeVerifi amount = WORMHOLE.messageFee(); } - /** @notice Wormhole proofs are valid until the guardian set is changed. The new guradian set may sign a new VAA */ + /** @notice Wormhole proofs are valid until the guardian set is changed. The new guardian set may sign a new VAA */ function _proofValidPeriod(bytes32 /* destinationIdentifier */) override internal pure returns(uint64) { return 0; } diff --git a/src/apps/wormhole/external/callworm/WormholeVerifier.sol b/src/apps/wormhole/external/callworm/WormholeVerifier.sol index b06d1b4..339aa62 100644 --- a/src/apps/wormhole/external/callworm/WormholeVerifier.sol +++ b/src/apps/wormhole/external/callworm/WormholeVerifier.sol @@ -23,7 +23,7 @@ contract WormholeVerifier is GettersGetter { constructor(address wormholeState) payable GettersGetter(wormholeState) {} - /// @dev parseAndVerifyVM serves to parse an encodedVM and wholy validate it for consumption + /// @dev parseAndVerifyVM serves to parse an encodedVM and wholly validate it for consumption function parseAndVerifyVM(bytes calldata encodedVM) public view returns ( SmallStructs.SmallVM memory vm, bytes calldata payload, @@ -91,7 +91,7 @@ contract WormholeVerifier is GettersGetter { /** - * @dev verifySignatures serves to validate arbitrary sigatures against an arbitrary guardianSet + * @dev verifySignatures serves to validate arbitrary signatures against an arbitrary guardianSet * - it intentionally does not solve for expectations within guardianSet (you should use verifyVM if you need these protections) * - it intentioanlly does not solve for quorum (you should use verifyVM if you need these protections) * - it intentionally returns true when signatures is an empty set (you should use verifyVM if you need these protections) @@ -215,7 +215,7 @@ contract WormholeVerifier is GettersGetter { } /** - * @dev quorum serves solely to determine the number of signatures required to acheive quorum + * @dev quorum serves solely to determine the number of signatures required to achieve quorum */ function quorum(uint numGuardians) public pure virtual returns (uint numSignaturesRequiredForQuorum) { unchecked { diff --git a/src/apps/wormhole/external/wormhole/Messages.sol b/src/apps/wormhole/external/wormhole/Messages.sol index e702caf..07ccb1d 100644 --- a/src/apps/wormhole/external/wormhole/Messages.sol +++ b/src/apps/wormhole/external/wormhole/Messages.sol @@ -12,7 +12,7 @@ import "./libraries/external/BytesLib.sol"; contract Messages is Getters { using BytesLib for bytes; - /// @dev parseAndVerifyVM serves to parse an encodedVM and wholy validate it for consumption + /// @dev parseAndVerifyVM serves to parse an encodedVM and wholly validate it for consumption function parseAndVerifyVM(bytes calldata encodedVM) public view returns (Structs.VM memory vm, bool valid, string memory reason) { vm = parseVM(encodedVM); /// setting checkHash to false as we can trust the hash field in this case given that parseVM computes and then sets the hash field above @@ -103,7 +103,7 @@ contract Messages is Getters { /** - * @dev verifySignatures serves to validate arbitrary sigatures against an arbitrary guardianSet + * @dev verifySignatures serves to validate arbitrary signatures against an arbitrary guardianSet * - it intentionally does not solve for expectations within guardianSet (you should use verifyVM if you need these protections) * - it intentioanlly does not solve for quorum (you should use verifyVM if you need these protections) * - it intentionally returns true when signatures is an empty set (you should use verifyVM if you need these protections) @@ -208,7 +208,7 @@ contract Messages is Getters { } /** - * @dev quorum serves solely to determine the number of signatures required to acheive quorum + * @dev quorum serves solely to determine the number of signatures required to achieve quorum */ function quorum(uint numGuardians) public pure virtual returns (uint numSignaturesRequiredForQuorum) { // The max number of guardians is 255 diff --git a/src/apps/wormhole/external/wormhole/libraries/external/BytesLib.sol b/src/apps/wormhole/external/wormhole/libraries/external/BytesLib.sol index 58b8f51..6ff3eef 100644 --- a/src/apps/wormhole/external/wormhole/libraries/external/BytesLib.sol +++ b/src/apps/wormhole/external/wormhole/libraries/external/BytesLib.sol @@ -421,14 +421,14 @@ library BytesLib { } { // if any of these checks fails then arrays are not equal if iszero(eq(mload(mc), mload(cc))) { - // unsuccess: + // unsuccessful: success := 0 cb := 0 } } } default { - // unsuccess: + // unsuccessful: success := 0 } } @@ -466,7 +466,7 @@ library BytesLib { fslot := mul(div(fslot, 0x100), 0x100) if iszero(eq(fslot, mload(add(_postBytes, 0x20)))) { - // unsuccess: + // unsuccessful: success := 0 } } @@ -491,7 +491,7 @@ library BytesLib { mc := add(mc, 0x20) } { if iszero(eq(sload(sc), mload(mc))) { - // unsuccess: + // unsuccessful: success := 0 cb := 0 } @@ -500,7 +500,7 @@ library BytesLib { } } default { - // unsuccess: + // unsuccessful: success := 0 } } diff --git a/src/interfaces/IMessageEscrowEvents.sol b/src/interfaces/IMessageEscrowEvents.sol index bd7a5fa..f1e8484 100644 --- a/src/interfaces/IMessageEscrowEvents.sol +++ b/src/interfaces/IMessageEscrowEvents.sol @@ -28,7 +28,7 @@ interface IMessageEscrowEvents { // To save gas, this event does not emit the full incentive scheme. // Instead, the new gas prices are emitted. As a result, the relayer can collect all bountyIncreased - // and then use the maximum. (since the maxmimum is enforced in the smart contract) + // and then use the maximum. (since the maximum is enforced in the smart contract) event BountyIncreased( bytes32 indexed messageIdentifier, uint96 newDeliveryGasPrice, From 948ad86a99eb079ed5507080b084ce177e1a2add Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 24 Jun 2024 15:32:17 +0200 Subject: [PATCH 61/61] feat: update ackee audit --- ...catalyst-generalised-incentives-report.pdf | Bin 1321400 -> 1476083 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/audit/ackee-blockchain-catalyst-generalised-incentives-report.pdf b/audit/ackee-blockchain-catalyst-generalised-incentives-report.pdf index 5ff2f0b9bd9fef8643dc898d61211debceb4f015..6f1dd14d1145be1cf215a71505ab049517fdeb53 100644 GIT binary patch delta 134768 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
$^1^VfX2S#yVLjQr_l%o?t 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 I{sx}^0iG-2hyVZp delta 83614 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 Ums%tY