From 68ec313b34b534c01e2b4d24b0e4d241338d81c6 Mon Sep 17 00:00:00 2001 From: polymorpher Date: Sun, 20 Oct 2024 07:26:37 -0700 Subject: [PATCH] fix ci error --- test/FoodMemeFactory.t.sol | 2 +- test/helper/OptionsHelper.sol | 140 ++++++ test/helper/TestHelperOz5.sol | 484 +++++++++++++++++++++ test/helper/mocks/DVNFeeLibMock.sol | 129 ++++++ test/helper/mocks/DVNMock.sol | 330 ++++++++++++++ test/helper/mocks/EndpointV2Mock.sol | 369 ++++++++++++++++ test/helper/mocks/ExecutorFeeLibMock.sol | 194 +++++++++ test/helper/mocks/ExecutorMock.sol | 148 +++++++ test/helper/mocks/MultiSigMock.sol | 106 +++++ test/helper/mocks/PriceFeedMock.sol | 257 +++++++++++ test/helper/mocks/ReceiveUln302Mock.sol | 88 ++++ test/helper/mocks/SendUln302Mock.sol | 111 +++++ test/helper/mocks/SimpleMessageLibMock.sol | 163 +++++++ test/helper/mocks/WorkerMock.sol | 167 +++++++ 14 files changed, 2687 insertions(+), 1 deletion(-) create mode 100644 test/helper/OptionsHelper.sol create mode 100644 test/helper/TestHelperOz5.sol create mode 100644 test/helper/mocks/DVNFeeLibMock.sol create mode 100644 test/helper/mocks/DVNMock.sol create mode 100644 test/helper/mocks/EndpointV2Mock.sol create mode 100644 test/helper/mocks/ExecutorFeeLibMock.sol create mode 100644 test/helper/mocks/ExecutorMock.sol create mode 100644 test/helper/mocks/MultiSigMock.sol create mode 100644 test/helper/mocks/PriceFeedMock.sol create mode 100644 test/helper/mocks/ReceiveUln302Mock.sol create mode 100644 test/helper/mocks/SendUln302Mock.sol create mode 100644 test/helper/mocks/SimpleMessageLibMock.sol create mode 100644 test/helper/mocks/WorkerMock.sol diff --git a/test/FoodMemeFactory.t.sol b/test/FoodMemeFactory.t.sol index bbddfce..ca184ca 100644 --- a/test/FoodMemeFactory.t.sol +++ b/test/FoodMemeFactory.t.sol @@ -17,7 +17,7 @@ import {MessagingFee, MessagingReceipt} from "@layerzerolabs/oft-evm/OFTCore.sol import {OFTMsgCodec} from "@layerzerolabs/oft-evm/libs/OFTMsgCodec.sol"; import {OFTComposeMsgCodec} from "@layerzerolabs/oft-evm/libs/OFTComposeMsgCodec.sol"; // DevTools imports -import {TestHelperOz5} from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol"; +import {TestHelperOz5} from "./helper/TestHelperOz5.sol"; contract FoodMemeFactoryTest is TestHelperOz5 { uint256 DEPLOYER_PRIVATE_KEY = vm.envUint("DEPLOYER_PRIVATE_KEY"); diff --git a/test/helper/OptionsHelper.sol b/test/helper/OptionsHelper.sol new file mode 100644 index 0000000..0a609cd --- /dev/null +++ b/test/helper/OptionsHelper.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import {ExecutorOptions} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; +import {UlnOptions} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/libs/UlnOptions.sol"; + +contract UlnOptionsMock { + using UlnOptions for bytes; + + function decode(bytes calldata _options) + public + pure + returns (bytes memory executorOptions, bytes memory dvnOptions) + { + return UlnOptions.decode(_options); + } +} + +contract OptionsHelper { + /// @dev For backwards compatibility reasons, we'll keep this initialization here + /// @dev Any new tests should use the _setUpUlnOptions function below + UlnOptionsMock ulnOptions = new UlnOptionsMock(); + + function _setUpUlnOptions() internal { + ulnOptions = new UlnOptionsMock(); + } + + function _parseExecutorLzReceiveOption(bytes memory _options) internal view returns (uint256 gas, uint256 value) { + (bool exist, bytes memory option) = + _getExecutorOptionByOptionType(_options, ExecutorOptions.OPTION_TYPE_LZRECEIVE); + require(exist, "OptionsHelper: lzReceive option not found"); + (gas, value) = this.decodeLzReceiveOption(option); + } + + function _parseExecutorNativeDropOption(bytes memory _options) + internal + view + returns (uint256 amount, bytes32 receiver) + { + (bool exist, bytes memory option) = + _getExecutorOptionByOptionType(_options, ExecutorOptions.OPTION_TYPE_NATIVE_DROP); + require(exist, "OptionsHelper: nativeDrop option not found"); + (amount, receiver) = this.decodeNativeDropOption(option); + } + + function _parseExecutorLzComposeOption(bytes memory _options) + internal + view + returns (uint16 index, uint256 gas, uint256 value) + { + (bool exist, bytes memory option) = + _getExecutorOptionByOptionType(_options, ExecutorOptions.OPTION_TYPE_LZCOMPOSE); + require(exist, "OptionsHelper: lzCompose option not found"); + return this.decodeLzComposeOption(option); + } + + function _executorOptionExists(bytes memory _options, uint8 _executorOptionType) + internal + view + returns (bool exist) + { + (exist,) = _getExecutorOptionByOptionType(_options, _executorOptionType); + } + + function _getExecutorOptionByOptionType(bytes memory _options, uint8 _executorOptionType) + internal + view + returns (bool exist, bytes memory option) + { + (bytes memory executorOpts,) = ulnOptions.decode(_options); + + uint256 cursor; + + // Used to accumulate the total gas and value for the chained executor options + uint128 executorGas; + uint128 executorValue; + + // Accumulated payload + bytes memory payload = new bytes(0); + + while (cursor < executorOpts.length) { + (uint8 optionType, bytes memory op, uint256 nextCursor) = this.nextExecutorOption(executorOpts, cursor); + + // There are 3 kinds of executor options -- lzReceive, nativeDrop, lzCompose. + if (optionType == _executorOptionType) { + uint128 gas; + uint128 value; + bytes32 receiver; + uint16 index; + if (optionType == ExecutorOptions.OPTION_TYPE_LZRECEIVE) { + (gas, value) = this.decodeLzReceiveOption(op); + executorGas += gas; + executorValue += value; + payload = abi.encodePacked(executorGas, executorValue); + } else if (optionType == ExecutorOptions.OPTION_TYPE_NATIVE_DROP) { + // Since there is a receiver in the nativeDrop options, do we do this differently? + (value, receiver) = this.decodeNativeDropOption(op); + executorValue += value; + payload = abi.encodePacked(executorValue, receiver); + } else if (optionType == ExecutorOptions.OPTION_TYPE_LZCOMPOSE) { + (index, gas, value) = this.decodeLzComposeOption(op); + executorGas += gas; + executorValue += value; + payload = abi.encodePacked(index, executorGas, executorValue); + } + } + cursor = nextCursor; + } + + if (payload.length == 0) { + return (false, payload); + } + return (true, payload); + } + + function nextExecutorOption(bytes calldata _options, uint256 _cursor) + external + pure + returns (uint8 optionType, bytes calldata option, uint256 cursor) + { + return ExecutorOptions.nextExecutorOption(_options, _cursor); + } + + function decodeLzReceiveOption(bytes calldata _option) external pure returns (uint128 gas, uint128 value) { + return ExecutorOptions.decodeLzReceiveOption(_option); + } + + function decodeNativeDropOption(bytes calldata _option) external pure returns (uint128 amount, bytes32 receiver) { + return ExecutorOptions.decodeNativeDropOption(_option); + } + + function decodeLzComposeOption(bytes calldata _option) + external + pure + returns (uint16 index, uint128 gas, uint128 value) + { + return ExecutorOptions.decodeLzComposeOption(_option); + } +} diff --git a/test/helper/TestHelperOz5.sol b/test/helper/TestHelperOz5.sol new file mode 100644 index 0000000..f2566a3 --- /dev/null +++ b/test/helper/TestHelperOz5.sol @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.18; + +// Forge +import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; + +// Oz +import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; + +// Msg Lib +import {UlnConfig, SetDefaultUlnConfigParam} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; +import { + SetDefaultExecutorConfigParam, + ExecutorConfig +} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; + +// Protocol +import {IMessageLib} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLib.sol"; +import {ExecutorOptions} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; +import {PacketV1Codec} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import { + Origin, + ILayerZeroEndpointV2 +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +// @dev oz4/5 breaking change... +import {ReceiveUln302Mock as ReceiveUln302, IReceiveUlnE2} from "./mocks/ReceiveUln302Mock.sol"; +import {DVNMock as DVN, ExecuteParam, IDVN} from "./mocks/DVNMock.sol"; +import {DVNFeeLibMock as DVNFeeLib} from "./mocks/DVNFeeLibMock.sol"; +import {ExecutorMock as Executor, IExecutor} from "./mocks/ExecutorMock.sol"; +import {PriceFeedMock as PriceFeed, ILayerZeroPriceFeed} from "./mocks/PriceFeedMock.sol"; +import {EndpointV2Mock as EndpointV2} from "./mocks//EndpointV2Mock.sol"; + +// Misc. Mocks +import {OptionsHelper} from "./OptionsHelper.sol"; +import {SendUln302Mock as SendUln302} from "./mocks/SendUln302Mock.sol"; +import {SimpleMessageLibMock} from "./mocks/SimpleMessageLibMock.sol"; +import {ExecutorFeeLibMock as ExecutorFeeLib} from "./mocks/ExecutorFeeLibMock.sol"; + +interface IOAppSetPeer { + function setPeer(uint32 _eid, bytes32 _peer) external; + function endpoint() external view returns (ILayerZeroEndpointV2 iEndpoint); +} + +/** + * @title TestHelperOz5 + * @notice Helper contract for setting up and managing LayerZero test environments. + * @dev Extends Foundry's Test contract and provides utility functions for setting up mock endpoints and OApps. + */ +contract TestHelperOz5 is Test, OptionsHelper { + enum LibraryType { + UltraLightNode, + SimpleMessageLib + } + + struct EndpointSetup { + EndpointV2[] endpointList; + uint32[] eidList; + address[] sendLibs; + address[] receiveLibs; + address[] signers; + PriceFeed priceFeed; + } + + struct LibrarySetup { + SendUln302 sendUln; + ReceiveUln302 receiveUln; + Executor executor; + DVN dvn; + ExecutorFeeLib executorLib; + DVNFeeLib dvnLib; + } + + struct ConfigParams { + IExecutor.DstConfigParam[] executorConfigParams; + IDVN.DstConfigParam[] dvnConfigParams; + } + + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + using PacketV1Codec for bytes; + + mapping(uint32 => mapping(bytes32 => DoubleEndedQueue.Bytes32Deque)) packetsQueue; // dstEid => dstUA => guids queue + mapping(bytes32 => bytes) packets; // guid => packet bytes + mapping(bytes32 => bytes) optionsLookup; // guid => options + + mapping(uint32 => address) endpoints; // eid => endpoint + + uint256 public constant TREASURY_GAS_CAP = 1000000000000; + uint256 public constant TREASURY_GAS_FOR_FEE_CAP = 100000; + + uint128 public executorValueCap = 0.1 ether; + + EndpointSetup internal endpointSetup; + LibrarySetup internal libSetup; + + /// @dev Initializes test environment setup, to be overridden by specific tests. + function setUp() public virtual { + _setUpUlnOptions(); + } + + /** + * @dev set executorValueCap if more than 0.1 ether is necessary + * @dev this must be called prior to setUpEndpoints() if the value is to be used + * @param _valueCap amount executor can pass as msg.value to lzReceive() + */ + function setExecutorValueCap(uint128 _valueCap) public { + executorValueCap = _valueCap; + } + + /** + * @notice Sets up endpoints for testing. + * @param _endpointNum The number of endpoints to create. + * @param _libraryType The type of message library to use (UltraLightNode or SimpleMessageLib). + */ + function setUpEndpoints(uint8 _endpointNum, LibraryType _libraryType) public { + endpointSetup.endpointList = new EndpointV2[](_endpointNum); + endpointSetup.eidList = new uint32[](_endpointNum); + endpointSetup.sendLibs = new address[](_endpointNum); + endpointSetup.receiveLibs = new address[](_endpointNum); + endpointSetup.signers = new address[](1); + endpointSetup.signers[0] = vm.addr(1); + + { + // deploy endpoints + for (uint8 i = 0; i < _endpointNum; i++) { + uint32 eid = i + 1; + endpointSetup.eidList[i] = eid; + endpointSetup.endpointList[i] = new EndpointV2(eid, address(this)); + registerEndpoint(endpointSetup.endpointList[i]); + } + } + + // @dev oz4/5 breaking change... constructor init + endpointSetup.priceFeed = new PriceFeed(address(this)); + + for (uint8 i = 0; i < _endpointNum; i++) { + if (_libraryType == LibraryType.UltraLightNode) { + address endpointAddr = address(endpointSetup.endpointList[i]); + + libSetup.sendUln = + new SendUln302(payable(this), endpointAddr, TREASURY_GAS_CAP, TREASURY_GAS_FOR_FEE_CAP); + libSetup.receiveUln = new ReceiveUln302(endpointAddr); + endpointSetup.endpointList[i].registerLibrary(address(libSetup.sendUln)); + endpointSetup.endpointList[i].registerLibrary(address(libSetup.receiveUln)); + endpointSetup.sendLibs[i] = address(libSetup.sendUln); + endpointSetup.receiveLibs[i] = address(libSetup.receiveUln); + + { + address[] memory admins = new address[](1); + admins[0] = address(this); + + address[] memory messageLibs = new address[](2); + messageLibs[0] = address(libSetup.sendUln); + messageLibs[1] = address(libSetup.receiveUln); + + libSetup.executor = new Executor( + endpointAddr, address(0x0), messageLibs, address(endpointSetup.priceFeed), address(this), admins + ); + + libSetup.executorLib = new ExecutorFeeLib(); + libSetup.executor.setWorkerFeeLib(address(libSetup.executorLib)); + + libSetup.dvn = + new DVN(i + 1, messageLibs, address(endpointSetup.priceFeed), endpointSetup.signers, 1, admins); + libSetup.dvnLib = new DVNFeeLib(1e18); + libSetup.dvn.setWorkerFeeLib(address(libSetup.dvnLib)); + } + + ConfigParams memory configParams; + configParams.executorConfigParams = new IExecutor.DstConfigParam[](_endpointNum); + configParams.dvnConfigParams = new IDVN.DstConfigParam[](_endpointNum); + + for (uint8 j = 0; j < _endpointNum; j++) { + if (i == j) continue; + uint32 dstEid = j + 1; + + address[] memory defaultDVNs = new address[](1); + address[] memory optionalDVNs = new address[](0); + defaultDVNs[0] = address(libSetup.dvn); + + SetDefaultUlnConfigParam[] memory ulnParams = new SetDefaultUlnConfigParam[](1); + UlnConfig memory ulnConfig = UlnConfig( + 100, uint8(defaultDVNs.length), uint8(optionalDVNs.length), 0, defaultDVNs, optionalDVNs + ); + + { + ulnParams[0] = SetDefaultUlnConfigParam(dstEid, ulnConfig); + libSetup.sendUln.setDefaultUlnConfigs(ulnParams); + libSetup.receiveUln.setDefaultUlnConfigs(ulnParams); + } + + { + SetDefaultExecutorConfigParam[] memory execParams = new SetDefaultExecutorConfigParam[](1); + ExecutorConfig memory execConfig = ExecutorConfig(10000, address(libSetup.executor)); + execParams[0] = SetDefaultExecutorConfigParam(dstEid, execConfig); + libSetup.sendUln.setDefaultExecutorConfigs(execParams); + } + + // executor config + configParams.executorConfigParams[j] = IExecutor.DstConfigParam({ + dstEid: dstEid, + lzReceiveBaseGas: 5000, + lzComposeBaseGas: 5000, + multiplierBps: 10000, + floorMarginUSD: 1e10, + nativeCap: executorValueCap + }); + + // dvn config + configParams.dvnConfigParams[j] = + IDVN.DstConfigParam({dstEid: dstEid, gas: 5000, multiplierBps: 10000, floorMarginUSD: 1e10}); + + uint128 denominator = endpointSetup.priceFeed.getPriceRatioDenominator(); + ILayerZeroPriceFeed.UpdatePrice[] memory prices = new ILayerZeroPriceFeed.UpdatePrice[](1); + prices[0] = + ILayerZeroPriceFeed.UpdatePrice(dstEid, ILayerZeroPriceFeed.Price(1 * denominator, 1, 1)); + endpointSetup.priceFeed.setPrice(prices); + } + + libSetup.executor.setDstConfig(configParams.executorConfigParams); + libSetup.dvn.setDstConfig(configParams.dvnConfigParams); + } else if (_libraryType == LibraryType.SimpleMessageLib) { + SimpleMessageLibMock messageLib = + new SimpleMessageLibMock(payable(this), address(endpointSetup.endpointList[i])); + endpointSetup.endpointList[i].registerLibrary(address(messageLib)); + endpointSetup.sendLibs[i] = address(messageLib); + endpointSetup.receiveLibs[i] = address(messageLib); + } else { + revert("invalid library type"); + } + } + + // config up + for (uint8 i = 0; i < _endpointNum; i++) { + EndpointV2 endpoint = endpointSetup.endpointList[i]; + for (uint8 j = 0; j < _endpointNum; j++) { + if (i == j) continue; + endpoint.setDefaultSendLibrary(j + 1, endpointSetup.sendLibs[i]); + endpoint.setDefaultReceiveLibrary(j + 1, endpointSetup.receiveLibs[i], 0); + } + } + } + + /** + * @notice Sets up mock OApp contracts for testing. + * @param _oappCreationCode The bytecode for creating OApp contracts. + * @param _startEid The starting endpoint ID for OApp setup. + * @param _oappNum The number of OApps to set up. + * @return oapps An array of addresses for the deployed OApps. + */ + function setupOApps(bytes memory _oappCreationCode, uint8 _startEid, uint8 _oappNum) + public + returns (address[] memory oapps) + { + oapps = new address[](_oappNum); + for (uint8 eid = _startEid; eid < _startEid + _oappNum; eid++) { + address oapp = _deployOApp(_oappCreationCode, abi.encode(address(endpoints[eid]), address(this), true)); + oapps[eid - _startEid] = oapp; + } + // config + wireOApps(oapps); + } + + /** + * @notice Configures the peers between multiple OApp instances. + * @dev Sets each OApp as a peer to every other OApp in the provided array, except itself. + * @param oapps An array of addresses representing the deployed OApp instances. + */ + function wireOApps(address[] memory oapps) public { + uint256 size = oapps.length; + for (uint256 i = 0; i < size; i++) { + IOAppSetPeer localOApp = IOAppSetPeer(oapps[i]); + for (uint256 j = 0; j < size; j++) { + if (i == j) continue; + IOAppSetPeer remoteOApp = IOAppSetPeer(oapps[j]); + uint32 remoteEid = (remoteOApp.endpoint()).eid(); + localOApp.setPeer(remoteEid, addressToBytes32(address(remoteOApp))); + } + } + } + + /** + * @notice Deploys an OApp contract using provided bytecode and constructor arguments. + * @dev This internal function uses low-level `create` for deploying a new contract. + * @param _oappBytecode The bytecode of the OApp contract to be deployed. + * @param _constructorArgs The encoded constructor arguments for the OApp contract. + * @return addr The address of the newly deployed OApp contract. + */ + function _deployOApp(bytes memory _oappBytecode, bytes memory _constructorArgs) internal returns (address addr) { + bytes memory bytecode = bytes.concat(abi.encodePacked(_oappBytecode), _constructorArgs); + assembly { + addr := create(0, add(bytecode, 0x20), mload(bytecode)) + if iszero(extcodesize(addr)) { revert(0, 0) } + } + } + + /** + * @notice Schedules a packet for delivery, storing it in the packets queue. + * @dev Adds the packet to the front of the queue and stores its options for later retrieval. + * @param _packetBytes The packet data to be scheduled. + * @param _options The options associated with the packet, used during delivery. + */ + function schedulePacket(bytes calldata _packetBytes, bytes calldata _options) public { + uint32 dstEid = _packetBytes.dstEid(); + bytes32 dstAddress = _packetBytes.receiver(); + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[dstEid][dstAddress]; + // front in, back out + bytes32 guid = _packetBytes.guid(); + queue.pushFront(guid); + packets[guid] = _packetBytes; + optionsLookup[guid] = _options; + } + + /** + * @notice Verifies and processes packets destined for a specific chain and user address. + * @dev Calls an overloaded version of verifyPackets with default values for packet amount and composer address. + * @param _dstEid The destination chain's endpoint ID. + * @param _dstAddress The destination address in bytes32 format. + */ + function verifyPackets(uint32 _dstEid, bytes32 _dstAddress) public { + verifyPackets(_dstEid, _dstAddress, 0, address(0x0)); + } + + /** + * @dev verify packets to destination chain's OApp address. + * @param _dstEid The destination endpoint ID. + * @param _dstAddress The destination address. + */ + function verifyPackets(uint32 _dstEid, address _dstAddress) public { + verifyPackets(_dstEid, bytes32(uint256(uint160(_dstAddress))), 0, address(0x0)); + } + + /** + * @dev dst UA receive/execute packets + * @dev will NOT work calling this directly with composer IF the composed payload is different from the lzReceive msg payload + */ + function verifyPackets(uint32 _dstEid, bytes32 _dstAddress, uint256 _packetAmount, address _composer) public { + require(endpoints[_dstEid] != address(0), "endpoint not yet registered"); + + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; + uint256 pendingPacketsSize = queue.length(); + uint256 numberOfPackets; + if (_packetAmount == 0) { + numberOfPackets = queue.length(); + } else { + numberOfPackets = pendingPacketsSize > _packetAmount ? _packetAmount : pendingPacketsSize; + } + while (numberOfPackets > 0) { + numberOfPackets--; + // front in, back out + bytes32 guid = queue.popBack(); + bytes memory packetBytes = packets[guid]; + this.assertGuid(packetBytes, guid); + this.validatePacket(packetBytes); + + bytes memory options = optionsLookup[guid]; + if (_executorOptionExists(options, ExecutorOptions.OPTION_TYPE_NATIVE_DROP)) { + (uint256 amount, bytes32 receiver) = _parseExecutorNativeDropOption(options); + address to = address(uint160(uint256(receiver))); + (bool sent,) = to.call{value: amount}(""); + require(sent, "Failed to send Ether"); + } + if (_executorOptionExists(options, ExecutorOptions.OPTION_TYPE_LZRECEIVE)) { + this.lzReceive(packetBytes, options); + } + if (_composer != address(0) && _executorOptionExists(options, ExecutorOptions.OPTION_TYPE_LZCOMPOSE)) { + this.lzCompose(packetBytes, options, guid, _composer); + } + } + } + + function lzReceive(bytes calldata _packetBytes, bytes memory _options) external payable { + EndpointV2 endpoint = EndpointV2(endpoints[_packetBytes.dstEid()]); + (uint256 gas, uint256 value) = OptionsHelper._parseExecutorLzReceiveOption(_options); + + Origin memory origin = Origin(_packetBytes.srcEid(), _packetBytes.sender(), _packetBytes.nonce()); + endpoint.lzReceive{value: value, gas: gas}( + origin, _packetBytes.receiverB20(), _packetBytes.guid(), _packetBytes.message(), bytes("") + ); + } + + function lzCompose(bytes calldata _packetBytes, bytes memory _options, bytes32 _guid, address _composer) + external + payable + { + this.lzCompose( + _packetBytes.dstEid(), _packetBytes.receiverB20(), _options, _guid, _composer, _packetBytes.message() + ); + } + + // @dev the verifyPackets does not know the composeMsg if it is NOT the same as the original lzReceive payload + // Can call this directly from your test to lzCompose those types of packets + function lzCompose( + uint32 _dstEid, + address _from, + bytes memory _options, + bytes32 _guid, + address _to, + bytes calldata _composerMsg + ) external payable { + EndpointV2 endpoint = EndpointV2(endpoints[_dstEid]); + (uint16 index, uint256 gas, uint256 value) = _parseExecutorLzComposeOption(_options); + endpoint.lzCompose{value: value, gas: gas}(_from, _to, _guid, index, _composerMsg, bytes("")); + } + + function validatePacket(bytes calldata _packetBytes) external { + uint32 dstEid = _packetBytes.dstEid(); + EndpointV2 endpoint = EndpointV2(endpoints[dstEid]); + (address receiveLib,) = endpoint.getReceiveLibrary(_packetBytes.receiverB20(), _packetBytes.srcEid()); + ReceiveUln302 dstUln = ReceiveUln302(receiveLib); + + (uint64 major,,) = IMessageLib(receiveLib).version(); + if (major == 3) { + // it is ultra light node + bytes memory config = dstUln.getConfig(_packetBytes.srcEid(), _packetBytes.receiverB20(), 2); // CONFIG_TYPE_ULN + DVN dvn = DVN(abi.decode(config, (UlnConfig)).requiredDVNs[0]); + + bytes memory packetHeader = _packetBytes.header(); + bytes32 payloadHash = keccak256(_packetBytes.payload()); + + // sign + bytes memory signatures; + bytes memory verifyCalldata = + abi.encodeWithSelector(IReceiveUlnE2.verify.selector, packetHeader, payloadHash, 100); + { + bytes32 hash = dvn.hashCallData(dstEid, address(dstUln), verifyCalldata, block.timestamp + 1000); + bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, ethSignedMessageHash); // matches dvn signer + signatures = abi.encodePacked(r, s, v); + } + ExecuteParam[] memory params = new ExecuteParam[](1); + params[0] = ExecuteParam(dstEid, address(dstUln), verifyCalldata, block.timestamp + 1000, signatures); + dvn.execute(params); + + // commit verification + bytes memory callData = + abi.encodeWithSelector(IReceiveUlnE2.commitVerification.selector, packetHeader, payloadHash); + { + bytes32 hash = dvn.hashCallData(dstEid, address(dstUln), callData, block.timestamp + 1000); + bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, ethSignedMessageHash); // matches dvn signer + signatures = abi.encodePacked(r, s, v); + } + params[0] = ExecuteParam(dstEid, address(dstUln), callData, block.timestamp + 1000, signatures); + dvn.execute(params); + } else { + SimpleMessageLibMock(payable(receiveLib)).validatePacket(_packetBytes); + } + } + + function assertGuid(bytes calldata packetBytes, bytes32 guid) external pure { + bytes32 packetGuid = packetBytes.guid(); + require(packetGuid == guid, "guid not match"); + } + + function registerEndpoint(EndpointV2 endpoint) public { + endpoints[endpoint.eid()] = address(endpoint); + } + + function hasPendingPackets(uint16 _dstEid, bytes32 _dstAddress) public view returns (bool flag) { + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; + return queue.length() > 0; + } + + function getNextInflightPacket(uint16 _dstEid, bytes32 _dstAddress) + public + view + returns (bytes memory packetBytes) + { + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; + if (queue.length() > 0) { + bytes32 guid = queue.back(); + packetBytes = packets[guid]; + } + } + + function addressToBytes32(address _addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } + + receive() external payable {} +} diff --git a/test/helper/mocks/DVNFeeLibMock.sol b/test/helper/mocks/DVNFeeLibMock.sol new file mode 100644 index 0000000..b44cfec --- /dev/null +++ b/test/helper/mocks/DVNFeeLibMock.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Transfer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; + +import {ILayerZeroPriceFeed} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/ILayerZeroPriceFeed.sol"; + +import {IDVN} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/interfaces/IDVN.sol"; +import {IDVNFeeLib} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/interfaces/IDVNFeeLib.sol"; +import {DVNOptions} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/libs/DVNOptions.sol"; + +contract DVNFeeLibMock is Ownable, IDVNFeeLib { + using DVNOptions for bytes; + + uint16 internal constant EXECUTE_FIXED_BYTES = 68; // encoded: funcSigHash + params -> 4 + (32 * 2) + uint16 internal constant SIGNATURE_RAW_BYTES = 65; // not encoded + // callData(updateHash) = 132 (4 + 32 * 4), padded to 32 = 160 and encoded as bytes with an 64 byte overhead = 224 + uint16 internal constant UPDATE_HASH_BYTES = 224; + + uint256 private immutable nativeDecimalsRate; + + // @dev oz4/5 breaking change... Ownable constructor + constructor(uint256 _nativeDecimalsRate) Ownable(msg.sender) { + nativeDecimalsRate = _nativeDecimalsRate; + } + + // ================================ OnlyOwner ================================ + function withdrawToken(address _token, address _to, uint256 _amount) external onlyOwner { + // transfers native if _token is address(0x0) + Transfer.nativeOrToken(_token, _to, _amount); + } + + // ========================= External ========================= + /// @dev get fee function that can change state. e.g. paying priceFeed + /// @param _params fee params + /// @param _dstConfig dst config + /// @param //_options options + function getFeeOnSend(FeeParams calldata _params, IDVN.DstConfig calldata _dstConfig, bytes calldata _options) + external + payable + returns (uint256) + { + if (_dstConfig.gas == 0) revert DVN_EidNotSupported(_params.dstEid); + + _decodeDVNOptions(_options); // todo: validate options + + uint256 callDataSize = _getCallDataSize(_params.quorum); + + // for future versions where priceFeed charges a fee + // uint256 priceFeedFee = ILayerZeroPriceFeed(_params.priceFeed).getFee(_params.dstEid, callDataSize, _dstConfig.gas); + // (uint256 fee, , , uint128 nativePriceUSD) = ILayerZeroPriceFeed(_params.priceFeed).estimateFeeOnSend{ + // value: priceFeedFee + // }(_params.dstEid, callDataSize, _dstConfig.gas); + + (uint256 fee,,, uint128 nativePriceUSD) = + ILayerZeroPriceFeed(_params.priceFeed).estimateFeeOnSend(_params.dstEid, callDataSize, _dstConfig.gas); + + return _applyPremium( + fee, _dstConfig.multiplierBps, _params.defaultMultiplierBps, _dstConfig.floorMarginUSD, nativePriceUSD + ); + } + + // ========================= View ========================= + /// @dev get fee view function + /// @param _params fee params + /// @param _dstConfig dst config + /// @param //_options options + function getFee(FeeParams calldata _params, IDVN.DstConfig calldata _dstConfig, bytes calldata _options) + external + view + returns (uint256) + { + if (_dstConfig.gas == 0) revert DVN_EidNotSupported(_params.dstEid); + + _decodeDVNOptions(_options); // validate options + + uint256 callDataSize = _getCallDataSize(_params.quorum); + (uint256 fee,,, uint128 nativePriceUSD) = + ILayerZeroPriceFeed(_params.priceFeed).estimateFeeByEid(_params.dstEid, callDataSize, _dstConfig.gas); + return _applyPremium( + fee, _dstConfig.multiplierBps, _params.defaultMultiplierBps, _dstConfig.floorMarginUSD, nativePriceUSD + ); + } + + // ========================= Internal ========================= + function _getCallDataSize(uint256 _quorum) internal pure returns (uint256) { + uint256 totalSignatureBytes = _quorum * SIGNATURE_RAW_BYTES; + if (totalSignatureBytes % 32 != 0) { + totalSignatureBytes = totalSignatureBytes - (totalSignatureBytes % 32) + 32; + } + // getFee should charge on execute(updateHash) + // totalSignatureBytesPadded also has 64 overhead for bytes + return uint256(EXECUTE_FIXED_BYTES) + UPDATE_HASH_BYTES + totalSignatureBytes + 64; + } + + function _applyPremium(uint256 _fee, uint16 _bps, uint16 _defaultBps, uint128 _marginUSD, uint128 _nativePriceUSD) + internal + view + returns (uint256) + { + uint16 multiplierBps = _bps == 0 ? _defaultBps : _bps; + + uint256 feeWithMultiplier = (_fee * multiplierBps) / 10000; + if (_nativePriceUSD == 0 || _marginUSD == 0) { + return feeWithMultiplier; + } + + uint256 feeWithFloorMargin = _fee + (_marginUSD * nativeDecimalsRate) / _nativePriceUSD; + + return feeWithFloorMargin > feeWithMultiplier ? feeWithFloorMargin : feeWithMultiplier; + } + + function _decodeDVNOptions(bytes calldata _options) internal pure returns (uint256) { + uint256 cursor; + while (cursor < _options.length) { + (uint8 optionType,, uint256 newCursor) = _options.nextDVNOption(cursor); + cursor = newCursor; + revert DVN_UnsupportedOptionType(optionType); + } + if (cursor != _options.length) revert DVNOptions.DVN_InvalidDVNOptions(cursor); + + return 0; // todo: precrime fee model + } + + // send funds here to pay for price feed directly + receive() external payable {} +} diff --git a/test/helper/mocks/DVNMock.sol b/test/helper/mocks/DVNMock.sol new file mode 100644 index 0000000..1aa570e --- /dev/null +++ b/test/helper/mocks/DVNMock.sol @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: LZBL-1.2 +pragma solidity ^0.8.0; + +pragma solidity ^0.8.20; + +import {ILayerZeroUltraLightNodeV2} from + "@layerzerolabs/lz-evm-v1-0.7/contracts/interfaces/ILayerZeroUltraLightNodeV2.sol"; + +import {WorkerMock as Worker} from "./WorkerMock.sol"; + +import {MultiSigMock as MultiSig} from "./MultiSigMock.sol"; +import {IDVN} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/interfaces/IDVN.sol"; +import {IDVNFeeLib} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/interfaces/IDVNFeeLib.sol"; +import {IReceiveUlnE2} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/interfaces/IReceiveUlnE2.sol"; + +struct ExecuteParam { + uint32 vid; + address target; + bytes callData; + uint256 expiration; + bytes signatures; +} + +contract DVNMock is Worker, MultiSig, IDVN { + // to uniquely identify this DVN instance + // set to endpoint v1 eid if available OR endpoint v2 eid % 30_000 + uint32 public immutable vid; + + mapping(uint32 dstEid => DstConfig) public dstConfig; + mapping(bytes32 executableHash => bool used) public usedHashes; + + error DVN_OnlySelf(); + error DVN_InvalidRole(bytes32 role); + error DVN_InstructionExpired(); + error DVN_InvalidTarget(address target); + error DVN_InvalidVid(uint32 vid); + error DVN_InvalidSignatures(); + error DVN_DuplicatedHash(bytes32 executableHash); + + event VerifySignaturesFailed(uint256 idx); + event ExecuteFailed(uint256 _index, bytes _data); + event HashAlreadyUsed(ExecuteParam param, bytes32 _hash); + // same as DVNFeePaid, but for ULNv2 + event VerifierFeePaid(uint256 fee); + + // ========================= Constructor ========================= + + /// @dev DVN doesn't have a roleAdmin (address(0x0)) + /// @dev Supports all of ULNv2, ULN301, ULN302 and more + /// @param _vid unique identifier for this DVN instance + /// @param _messageLibs array of message lib addresses that are granted the MESSAGE_LIB_ROLE + /// @param _priceFeed price feed address + /// @param _signers array of signer addresses for multisig + /// @param _quorum quorum for multisig + /// @param _admins array of admin addresses that are granted the ADMIN_ROLE + constructor( + uint32 _vid, + address[] memory _messageLibs, + address _priceFeed, + address[] memory _signers, + uint64 _quorum, + address[] memory _admins + ) Worker(_messageLibs, _priceFeed, 12000, address(0x0), _admins) MultiSig(_signers, _quorum) { + vid = _vid; + } + + // ========================= Modifier ========================= + + /// @dev depending on role, restrict access to only self or admin + /// @dev ALLOWLIST, DENYLIST, MESSAGE_LIB_ROLE can only be granted/revoked by self + /// @dev ADMIN_ROLE can only be granted/revoked by admin + /// @dev reverts if not one of the above roles + /// @param _role role to check + modifier onlySelfOrAdmin(bytes32 _role) { + if (_role == ALLOWLIST || _role == DENYLIST || _role == MESSAGE_LIB_ROLE) { + // self required + if (address(this) != msg.sender) { + revert DVN_OnlySelf(); + } + } else if (_role == ADMIN_ROLE) { + // admin required + _checkRole(ADMIN_ROLE); + } else { + revert DVN_InvalidRole(_role); + } + _; + } + + modifier onlySelf() { + if (address(this) != msg.sender) { + revert DVN_OnlySelf(); + } + _; + } + + // ========================= OnlySelf ========================= + + /// @dev set signers for multisig + /// @dev function sig 0x31cb6105 + /// @param _signer signer address + /// @param _active true to add, false to remove + function setSigner(address _signer, bool _active) external onlySelf { + _setSigner(_signer, _active); + } + + /// @dev set quorum for multisig + /// @dev function sig 0x8585c945 + /// @param _quorum to set + function setQuorum(uint64 _quorum) external onlySelf { + _setQuorum(_quorum); + } + + // ========================= OnlySelf / OnlyAdmin ========================= + + /// @dev overrides AccessControl to allow self/admin to grant role' + /// @dev function sig 0x2f2ff15d + /// @param _role role to grant + /// @param _account account to grant role to + function grantRole(bytes32 _role, address _account) public override onlySelfOrAdmin(_role) { + _grantRole(_role, _account); + } + + /// @dev overrides AccessControl to allow self/admin to revoke role + /// @dev function sig 0xd547741f + /// @param _role role to revoke + /// @param _account account to revoke role from + function revokeRole(bytes32 _role, address _account) public override onlySelfOrAdmin(_role) { + _revokeRole(_role, _account); + } + + // ========================= OnlyQuorum ========================= + + /// @notice function for quorum to change admin without going through execute function + /// @dev calldata in the case is abi.encode new admin address + function quorumChangeAdmin(ExecuteParam calldata _param) external { + if (_param.expiration <= block.timestamp) { + revert DVN_InstructionExpired(); + } + if (_param.target != address(this)) { + revert DVN_InvalidTarget(_param.target); + } + if (_param.vid != vid) { + revert DVN_InvalidVid(_param.vid); + } + + // generate and validate hash + bytes32 hash = hashCallData(_param.vid, _param.target, _param.callData, _param.expiration); + (bool sigsValid,) = verifySignatures(hash, _param.signatures); + if (!sigsValid) { + revert DVN_InvalidSignatures(); + } + if (usedHashes[hash]) { + revert DVN_DuplicatedHash(hash); + } + + usedHashes[hash] = true; + _grantRole(ADMIN_ROLE, abi.decode(_param.callData, (address))); + } + + // ========================= OnlyAdmin ========================= + + /// @param _params array of DstConfigParam + function setDstConfig(DstConfigParam[] calldata _params) external onlyRole(ADMIN_ROLE) { + for (uint256 i = 0; i < _params.length; ++i) { + DstConfigParam calldata param = _params[i]; + dstConfig[param.dstEid] = DstConfig(param.gas, param.multiplierBps, param.floorMarginUSD); + } + emit SetDstConfig(_params); + } + + /// @dev takes a list of instructions and executes them in order + /// @dev if any of the instructions fail, it will emit an error event and continue to execute the rest of the instructions + /// @param _params array of ExecuteParam, includes target, callData, expiration, signatures + function execute(ExecuteParam[] calldata _params) external onlyRole(ADMIN_ROLE) { + for (uint256 i = 0; i < _params.length; ++i) { + ExecuteParam calldata param = _params[i]; + // 1. skip if invalid vid + if (param.vid != vid) { + continue; + } + + // 2. skip if expired + if (param.expiration <= block.timestamp) { + continue; + } + + // generate and validate hash + bytes32 hash = hashCallData(param.vid, param.target, param.callData, param.expiration); + + // 3. check signatures + (bool sigsValid,) = verifySignatures(hash, param.signatures); + if (!sigsValid) { + emit VerifySignaturesFailed(i); + continue; + } + + // 4. should check hash + bool shouldCheckHash = _shouldCheckHash(bytes4(param.callData)); + if (shouldCheckHash) { + if (usedHashes[hash]) { + emit HashAlreadyUsed(param, hash); + continue; + } else { + usedHashes[hash] = true; // prevent reentry and replay attack + } + } + + (bool success, bytes memory rtnData) = param.target.call(param.callData); + if (!success) { + if (shouldCheckHash) { + // need to unset the usedHash otherwise it cant be used + usedHashes[hash] = false; + } + // emit an event in any case + emit ExecuteFailed(i, rtnData); + } + } + } + + /// @dev to support ULNv2 + /// @dev the withdrawFee function for ULN30X is built in the Worker contract + /// @param _lib message lib address + /// @param _to address to withdraw to + /// @param _amount amount to withdraw + function withdrawFeeFromUlnV2(address _lib, address payable _to, uint256 _amount) external onlyRole(ADMIN_ROLE) { + if (!hasRole(MESSAGE_LIB_ROLE, _lib)) { + revert Worker_OnlyMessageLib(); + } + ILayerZeroUltraLightNodeV2(_lib).withdrawNative(_to, _amount); + } + + // ========================= OnlyMessageLib ========================= + + /// @dev for ULN301, ULN302 and more to assign job + /// @dev dvn network can reject job from _sender by adding/removing them from allowlist/denylist + /// @param _param assign job param + /// @param _options dvn options + function assignJob(AssignJobParam calldata _param, bytes calldata _options) + external + payable + onlyRole(MESSAGE_LIB_ROLE) + onlyAcl(_param.sender) + returns (uint256 totalFee) + { + IDVNFeeLib.FeeParams memory feeParams = IDVNFeeLib.FeeParams( + priceFeed, _param.dstEid, _param.confirmations, _param.sender, quorum, defaultMultiplierBps + ); + totalFee = IDVNFeeLib(workerFeeLib).getFeeOnSend(feeParams, dstConfig[_param.dstEid], _options); + } + + /// @dev to support ULNv2 + /// @dev dvn network can reject job from _sender by adding/removing them from allowlist/denylist + /// @param _dstEid destination EndpointId + /// @param //_outboundProofType outbound proof type + /// @param _confirmations block confirmations + /// @param _sender message sender address + function assignJob(uint16 _dstEid, uint16, /*_outboundProofType*/ uint64 _confirmations, address _sender) + external + onlyRole(MESSAGE_LIB_ROLE) + onlyAcl(_sender) + returns (uint256 totalFee) + { + IDVNFeeLib.FeeParams memory params = + IDVNFeeLib.FeeParams(priceFeed, _dstEid, _confirmations, _sender, quorum, defaultMultiplierBps); + // ULNV2 does not have dvn options + totalFee = IDVNFeeLib(workerFeeLib).getFeeOnSend(params, dstConfig[_dstEid], bytes("")); + emit VerifierFeePaid(totalFee); + } + + // ========================= View ========================= + + /// @dev getFee can revert if _sender doesn't pass ACL + /// @param _dstEid destination EndpointId + /// @param _confirmations block confirmations + /// @param _sender message sender address + /// @param _options dvn options + /// @return fee fee in native amount + function getFee(uint32 _dstEid, uint64 _confirmations, address _sender, bytes calldata _options) + external + view + onlyAcl(_sender) + returns (uint256 fee) + { + IDVNFeeLib.FeeParams memory params = + IDVNFeeLib.FeeParams(priceFeed, _dstEid, _confirmations, _sender, quorum, defaultMultiplierBps); + return IDVNFeeLib(workerFeeLib).getFee(params, dstConfig[_dstEid], _options); + } + + /// @dev to support ULNv2 + /// @dev getFee can revert if _sender doesn't pass ACL + /// @param _dstEid destination EndpointId + /// @param //_outboundProofType outbound proof type + /// @param _confirmations block confirmations + /// @param _sender message sender address + function getFee(uint16 _dstEid, uint16, /*_outboundProofType*/ uint64 _confirmations, address _sender) + public + view + onlyAcl(_sender) + returns (uint256 fee) + { + IDVNFeeLib.FeeParams memory params = + IDVNFeeLib.FeeParams(priceFeed, _dstEid, _confirmations, _sender, quorum, defaultMultiplierBps); + return IDVNFeeLib(workerFeeLib).getFee(params, dstConfig[_dstEid], bytes("")); + } + + /// @param _target target address + /// @param _callData call data + /// @param _expiration expiration timestamp + /// @return hash of above + function hashCallData(uint32 _vid, address _target, bytes calldata _callData, uint256 _expiration) + public + pure + returns (bytes32) + { + return keccak256(abi.encodePacked(_vid, _target, _expiration, _callData)); + } + + // ========================= Internal ========================= + + /// @dev to save gas, we don't check hash for some functions (where replaying won't change the state) + /// @dev for example, some administrative functions like changing signers, the contract should check hash to double spending + /// @dev should ensure that all onlySelf functions have unique functionSig + /// @param _functionSig function signature + /// @return true if should check hash + function _shouldCheckHash(bytes4 _functionSig) internal pure returns (bool) { + // never check for these selectors to save gas + return _functionSig != IReceiveUlnE2.verify.selector // 0x0223536e, replaying won't change the state + && _functionSig != ILayerZeroUltraLightNodeV2.updateHash.selector; // 0x704316e5, replaying will be revert at uln + } +} diff --git a/test/helper/mocks/EndpointV2Mock.sol b/test/helper/mocks/EndpointV2Mock.sol new file mode 100644 index 0000000..2d564e6 --- /dev/null +++ b/test/helper/mocks/EndpointV2Mock.sol @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// @dev oz4/5 breaking change... Ownable constructor +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import { + MessagingFee, + MessagingParams, + MessagingReceipt, + Origin, + ILayerZeroEndpointV2 +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import {ISendLib, Packet} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import {ILayerZeroReceiver} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol"; +import {Errors} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Errors.sol"; +import {GUID} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; +import {Transfer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; +import {MessagingChannel} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingChannel.sol"; +import {MessagingComposer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingComposer.sol"; +import {MessageLibManager} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessageLibManager.sol"; +import {MessagingContext} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingContext.sol"; + +// LayerZero EndpointV2 is fully backward compatible with LayerZero Endpoint(V1), but it also supports additional +// features that Endpoint(V1) does not support now and may not in the future. We have also changed some terminology +// to clarify pre-existing language that might have been confusing. +// +// The following is a list of terminology changes: +// -chainId -> eid +// - Rationale: chainId was a term we initially used to describe an endpoint on a specific chain. Since +// LayerZero supports non-EVMs we could not map the classic EVM chainIds to the LayerZero chainIds, making it +// confusing for developers. With the addition of EndpointV2 and its backward compatible nature, we would have +// two chainIds per chain that has Endpoint(V1), further confusing developers. We have decided to change the +// name to Endpoint Id, or eid, for simplicity and clarity. +// -adapterParams -> options +// -userApplication -> oapp. Omnichain Application +// -srcAddress -> sender +// -dstAddress -> receiver +// - Rationale: The sender/receiver on EVM is the address. However, on non-EVM chains, the sender/receiver could +// represented as a public key, or some other identifier. The term sender/receiver is more generic +// -payload -> message. +// - Rationale: The term payload is used in the context of a packet, which is a combination of the message and GUID +contract EndpointV2Mock is + ILayerZeroEndpointV2, + MessagingChannel, + MessageLibManager, + MessagingComposer, + MessagingContext +{ + address public lzToken; + + mapping(address oapp => address delegate) public delegates; + + /// @param _eid the unique Endpoint Id for this deploy that all other Endpoints can use to send to it + // @dev oz4/5 breaking change... Ownable constructor + constructor(uint32 _eid, address _owner) Ownable(_owner) MessagingChannel(_eid) {} + + /// @dev MESSAGING STEP 0 + /// @notice This view function gives the application built on top of LayerZero the ability to requests a quote + /// with the same parameters as they would to send their message. Since the quotes are given on chain there is a + /// race condition in which the prices could change between the time the user gets their quote and the time they + /// submit their message. If the price moves up and the user doesn't send enough funds the transaction will revert, + /// if the price goes down the _refundAddress provided by the app will be refunded the difference. + /// @param _params the messaging parameters + /// @param _sender the sender of the message + function quote(MessagingParams calldata _params, address _sender) external view returns (MessagingFee memory) { + // lzToken must be set to support payInLzToken + if (_params.payInLzToken && lzToken == address(0x0)) revert Errors.LZ_LzTokenUnavailable(); + + // get the correct outbound nonce + uint64 nonce = outboundNonce[_sender][_params.dstEid][_params.receiver] + 1; + + // construct the packet with a GUID + Packet memory packet = Packet({ + nonce: nonce, + srcEid: eid, + sender: _sender, + dstEid: _params.dstEid, + receiver: _params.receiver, + guid: GUID.generate(nonce, eid, _sender, _params.dstEid, _params.receiver), + message: _params.message + }); + + // get the send library by sender and dst eid + // use _ to avoid variable shadowing + address _sendLibrary = getSendLibrary(_sender, _params.dstEid); + + return ISendLib(_sendLibrary).quote(packet, _params.options, _params.payInLzToken); + } + + /// @dev MESSAGING STEP 1 - OApp need to transfer the fees to the endpoint before sending the message + /// @param _params the messaging parameters + /// @param _refundAddress the address to refund both the native and lzToken + function send(MessagingParams calldata _params, address _refundAddress) + external + payable + sendContext(_params.dstEid, msg.sender) + returns (MessagingReceipt memory) + { + if (_params.payInLzToken && lzToken == address(0x0)) revert Errors.LZ_LzTokenUnavailable(); + + // send message + (MessagingReceipt memory receipt, address _sendLibrary) = _send(msg.sender, _params); + + // OApp can simulate with 0 native value it will fail with error including the required fee, which can be provided in the actual call + // this trick can be used to avoid the need to write the quote() function + // however, without the quote view function it will be hard to compose an oapp on chain + uint256 suppliedNative = _suppliedNative(); + uint256 suppliedLzToken = _suppliedLzToken(_params.payInLzToken); + _assertMessagingFee(receipt.fee, suppliedNative, suppliedLzToken); + + // handle lz token fees + _payToken(lzToken, receipt.fee.lzTokenFee, suppliedLzToken, _sendLibrary, _refundAddress); + + // handle native fees + _payNative(receipt.fee.nativeFee, suppliedNative, _sendLibrary, _refundAddress); + + return receipt; + } + + /// @dev internal function for sending the messages used by all external send methods + /// @param _sender the address of the application sending the message to the destination chain + /// @param _params the messaging parameters + function _send(address _sender, MessagingParams calldata _params) + internal + returns (MessagingReceipt memory, address) + { + // get the correct outbound nonce + uint64 latestNonce = _outbound(_sender, _params.dstEid, _params.receiver); + + // construct the packet with a GUID + Packet memory packet = Packet({ + nonce: latestNonce, + srcEid: eid, + sender: _sender, + dstEid: _params.dstEid, + receiver: _params.receiver, + guid: GUID.generate(latestNonce, eid, _sender, _params.dstEid, _params.receiver), + message: _params.message + }); + + // get the send library by sender and dst eid + address _sendLibrary = getSendLibrary(_sender, _params.dstEid); + + // messageLib always returns encodedPacket with guid + (MessagingFee memory fee, bytes memory encodedPacket) = + ISendLib(_sendLibrary).send(packet, _params.options, _params.payInLzToken); + + // Emit packet information for DVNs, Executors, and any other offchain infrastructure to only listen + // for this one event to perform their actions. + emit PacketSent(encodedPacket, _params.options, _sendLibrary); + + return (MessagingReceipt(packet.guid, latestNonce, fee), _sendLibrary); + } + + /// @dev MESSAGING STEP 2 - on the destination chain + /// @dev configured receive library verifies a message + /// @param _origin a struct holding the srcEid, nonce, and sender of the message + /// @param _receiver the receiver of the message + /// @param _payloadHash the payload hash of the message + function verify(Origin calldata _origin, address _receiver, bytes32 _payloadHash) external { + if (!isValidReceiveLibrary(_receiver, _origin.srcEid, msg.sender)) revert Errors.LZ_InvalidReceiveLibrary(); + + uint64 lazyNonce = lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]; + if (!_initializable(_origin, _receiver, lazyNonce)) revert Errors.LZ_PathNotInitializable(); + if (!_verifiable(_origin, _receiver, lazyNonce)) revert Errors.LZ_PathNotVerifiable(); + + // insert the message into the message channel + _inbound(_receiver, _origin.srcEid, _origin.sender, _origin.nonce, _payloadHash); + emit PacketVerified(_origin, _receiver, _payloadHash); + } + + /// @dev MESSAGING STEP 3 - the last step + /// @dev execute a verified message to the designated receiver + /// @dev the execution provides the execution context (caller, extraData) to the receiver. the receiver can optionally assert the caller and validate the untrusted extraData + /// @dev cant reentrant because the payload is cleared before execution + /// @param _origin the origin of the message + /// @param _receiver the receiver of the message + /// @param _guid the guid of the message + /// @param _message the message + /// @param _extraData the extra data provided by the executor. this data is untrusted and should be validated. + function lzReceive( + Origin calldata _origin, + address _receiver, + bytes32 _guid, + bytes calldata _message, + bytes calldata _extraData + ) external payable { + // clear the payload first to prevent reentrancy, and then execute the message + _clearPayload(_receiver, _origin.srcEid, _origin.sender, _origin.nonce, abi.encodePacked(_guid, _message)); + ILayerZeroReceiver(_receiver).lzReceive{value: msg.value}(_origin, _guid, _message, msg.sender, _extraData); + emit PacketDelivered(_origin, _receiver); + } + + /// @param _origin the origin of the message + /// @param _receiver the receiver of the message + /// @param _guid the guid of the message + /// @param _message the message + /// @param _extraData the extra data provided by the executor. + /// @param _reason the reason for failure + function lzReceiveAlert( + Origin calldata _origin, + address _receiver, + bytes32 _guid, + uint256 _gas, + uint256 _value, + bytes calldata _message, + bytes calldata _extraData, + bytes calldata _reason + ) external { + emit LzReceiveAlert(_receiver, msg.sender, _origin, _guid, _gas, _value, _message, _extraData, _reason); + } + + /// @dev Oapp uses this interface to clear a message. + /// @dev this is a PULL mode versus the PUSH mode of lzReceive + /// @dev the cleared message can be ignored by the app (effectively burnt) + /// @dev authenticated by oapp + /// @param _origin the origin of the message + /// @param _guid the guid of the message + /// @param _message the message + function clear(address _oapp, Origin calldata _origin, bytes32 _guid, bytes calldata _message) external { + _assertAuthorized(_oapp); + + bytes memory payload = abi.encodePacked(_guid, _message); + _clearPayload(_oapp, _origin.srcEid, _origin.sender, _origin.nonce, payload); + emit PacketDelivered(_origin, _oapp); + } + + /// @dev allows reconfiguration to recover from wrong configurations + /// @dev users should never approve the EndpointV2 contract to spend their non-layerzero tokens + /// @dev override this function if the endpoint is charging ERC20 tokens as native + /// @dev only owner + /// @param _lzToken the new layer zero token address + function setLzToken(address _lzToken) public virtual onlyOwner { + lzToken = _lzToken; + emit LzTokenSet(_lzToken); + } + + /// @dev recover the token sent to this contract by mistake + /// @dev only owner + /// @param _token the token to recover. if 0x0 then it is native token + /// @param _to the address to send the token to + /// @param _amount the amount to send + function recoverToken(address _token, address _to, uint256 _amount) external onlyOwner { + Transfer.nativeOrToken(_token, _to, _amount); + } + + /// @dev handling token payments on endpoint. the sender must approve the endpoint to spend the token + /// @dev internal function + /// @param _token the token to pay + /// @param _required the amount required + /// @param _supplied the amount supplied + /// @param _receiver the receiver of the token + function _payToken(address _token, uint256 _required, uint256 _supplied, address _receiver, address _refundAddress) + internal + { + if (_required > 0) { + Transfer.token(_token, _receiver, _required); + } + if (_required < _supplied) { + unchecked { + // refund the excess + Transfer.token(_token, _refundAddress, _supplied - _required); + } + } + } + + /// @dev handling native token payments on endpoint + /// @dev override this if the endpoint is charging ERC20 tokens as native + /// @dev internal function + /// @param _required the amount required + /// @param _supplied the amount supplied + /// @param _receiver the receiver of the native token + /// @param _refundAddress the address to refund the excess to + function _payNative(uint256 _required, uint256 _supplied, address _receiver, address _refundAddress) + internal + virtual + { + if (_required > 0) { + Transfer.native(_receiver, _required); + } + if (_required < _supplied) { + unchecked { + // refund the excess + Transfer.native(_refundAddress, _supplied - _required); + } + } + } + + /// @dev get the balance of the lzToken as the supplied lzToken fee if payInLzToken is true + function _suppliedLzToken(bool _payInLzToken) internal view returns (uint256 supplied) { + if (_payInLzToken) { + supplied = IERC20(lzToken).balanceOf(address(this)); + + // if payInLzToken is true, the supplied fee must be greater than 0 to prevent a race condition + // in which an oapp sending a message with lz token and the lz token is set to a new token between the tx + // being sent and the tx being mined. if the required lz token fee is 0 and the old lz token would be + // locked in the contract instead of being refunded + if (supplied == 0) revert Errors.LZ_ZeroLzTokenFee(); + } + } + + /// @dev override this if the endpoint is charging ERC20 tokens as native + function _suppliedNative() internal view virtual returns (uint256) { + return msg.value; + } + + /// @dev Assert the required fees and the supplied fees are enough + function _assertMessagingFee(MessagingFee memory _required, uint256 _suppliedNativeFee, uint256 _suppliedLzTokenFee) + internal + pure + { + if (_required.nativeFee > _suppliedNativeFee || _required.lzTokenFee > _suppliedLzTokenFee) { + revert Errors.LZ_InsufficientFee( + _required.nativeFee, _suppliedNativeFee, _required.lzTokenFee, _suppliedLzTokenFee + ); + } + } + + /// @dev override this if the endpoint is charging ERC20 tokens as native + /// @return 0x0 if using native. otherwise the address of the native ERC20 token + function nativeToken() external view virtual returns (address) { + return address(0x0); + } + + /// @notice delegate is authorized by the oapp to configure anything in layerzero + function setDelegate(address _delegate) external { + delegates[msg.sender] = _delegate; + emit DelegateSet(msg.sender, _delegate); + } + + // ========================= Internal ========================= + function _initializable(Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce) + internal + view + returns (bool) + { + return _lazyInboundNonce > 0 // allowInitializePath already checked + || ILayerZeroReceiver(_receiver).allowInitializePath(_origin); + } + + /// @dev bytes(0) payloadHash can never be submitted + function _verifiable(Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce) + internal + view + returns (bool) + { + return _origin.nonce > _lazyInboundNonce // either initializing an empty slot or reverifying + || inboundPayloadHash[_receiver][_origin.srcEid][_origin.sender][_origin.nonce] != EMPTY_PAYLOAD_HASH; // only allow reverifying if it hasn't been executed + } + + /// @dev assert the caller to either be the oapp or the delegate + function _assertAuthorized(address _oapp) internal view override(MessagingChannel, MessageLibManager) { + if (msg.sender != _oapp && msg.sender != delegates[_oapp]) revert Errors.LZ_Unauthorized(); + } + + // ========================= VIEW FUNCTIONS FOR OFFCHAIN ONLY ========================= + // Not involved in any state transition function. + // ==================================================================================== + function initializable(Origin calldata _origin, address _receiver) external view returns (bool) { + return _initializable(_origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]); + } + + function verifiable(Origin calldata _origin, address _receiver) external view returns (bool) { + return _verifiable(_origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]); + } +} diff --git a/test/helper/mocks/ExecutorFeeLibMock.sol b/test/helper/mocks/ExecutorFeeLibMock.sol new file mode 100644 index 0000000..31bbd44 --- /dev/null +++ b/test/helper/mocks/ExecutorFeeLibMock.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: LZBL-1.2 +pragma solidity ^0.8.22; + +// @dev oz4/5 breaking change... Ownable constructor +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {Transfer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; +import {ExecutorOptions} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; + +import {ILayerZeroPriceFeed} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/ILayerZeroPriceFeed.sol"; +import {IExecutor} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/IExecutor.sol"; +import {IExecutorFeeLib} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/IExecutorFeeLib.sol"; + +contract ExecutorFeeLibMock is Ownable, IExecutorFeeLib { + using ExecutorOptions for bytes; + + uint256 private immutable nativeDecimalsRate; + + // @dev oz4/5 breaking change... Ownable constructor + constructor() Ownable(msg.sender) { + nativeDecimalsRate = 1e18; + } + + // ================================ OnlyOwner ================================ + function withdrawToken(address _token, address _to, uint256 _amount) external onlyOwner { + // transfers native if _token is address(0x0) + Transfer.nativeOrToken(_token, _to, _amount); + } + + // ================================ External ================================ + function getFeeOnSend(FeeParams calldata _params, IExecutor.DstConfig calldata _dstConfig, bytes calldata _options) + external + returns (uint256 fee) + { + if (_dstConfig.lzReceiveBaseGas == 0) revert Executor_EidNotSupported(_params.dstEid); + + (uint256 totalDstAmount, uint256 totalGas) = _decodeExecutorOptions( + _isV1Eid(_params.dstEid), + _dstConfig.lzReceiveBaseGas, + _dstConfig.lzComposeBaseGas, + _dstConfig.nativeCap, + _options + ); + + // for future versions where priceFeed charges a fee + (uint256 totalGasFee, uint128 priceRatio, uint128 priceRatioDenominator, uint128 nativePriceUSD) = + ILayerZeroPriceFeed(_params.priceFeed).estimateFeeOnSend(_params.dstEid, _params.calldataSize, totalGas); + + fee = _applyPremiumToGas( + totalGasFee, + _dstConfig.multiplierBps, + _params.defaultMultiplierBps, + _dstConfig.floorMarginUSD, + nativePriceUSD + ); + fee += _convertAndApplyPremiumToValue( + totalDstAmount, priceRatio, priceRatioDenominator, _params.defaultMultiplierBps + ); + } + + // ================================ View ================================ + function getFee(FeeParams calldata _params, IExecutor.DstConfig calldata _dstConfig, bytes calldata _options) + external + view + returns (uint256 fee) + { + if (_dstConfig.lzReceiveBaseGas == 0) revert Executor_EidNotSupported(_params.dstEid); + + (uint256 totalDstAmount, uint256 totalGas) = _decodeExecutorOptions( + _isV1Eid(_params.dstEid), + _dstConfig.lzReceiveBaseGas, + _dstConfig.lzComposeBaseGas, + _dstConfig.nativeCap, + _options + ); + + (uint256 totalGasFee, uint128 priceRatio, uint128 priceRatioDenominator, uint128 nativePriceUSD) = + ILayerZeroPriceFeed(_params.priceFeed).estimateFeeByEid(_params.dstEid, _params.calldataSize, totalGas); + + fee = _applyPremiumToGas( + totalGasFee, + _dstConfig.multiplierBps, + _params.defaultMultiplierBps, + _dstConfig.floorMarginUSD, + nativePriceUSD + ); + fee += _convertAndApplyPremiumToValue( + totalDstAmount, priceRatio, priceRatioDenominator, _params.defaultMultiplierBps + ); + } + + // ================================ Internal ================================ + // @dev decode executor options into dstAmount and totalGas + function _decodeExecutorOptions( + bool _v1Eid, + uint64 _lzReceiveBaseGas, + uint64 _lzComposeBaseGas, + uint128 _nativeCap, + bytes calldata _options + ) internal pure returns (uint256 dstAmount, uint256 totalGas) { + if (_options.length == 0) { + revert Executor_NoOptions(); + } + + uint256 cursor = 0; + bool ordered = false; + totalGas = _lzReceiveBaseGas; // lz receive only called once + + bool v1Eid = _v1Eid; // stack too deep + uint256 lzReceiveGas; + while (cursor < _options.length) { + (uint8 optionType, bytes calldata option, uint256 newCursor) = _options.nextExecutorOption(cursor); + cursor = newCursor; + + if (optionType == ExecutorOptions.OPTION_TYPE_LZRECEIVE) { + (uint128 gas, uint128 value) = ExecutorOptions.decodeLzReceiveOption(option); + + // endpoint v1 does not support lzReceive with value + if (v1Eid && value > 0) revert Executor_UnsupportedOptionType(optionType); + + dstAmount += value; + lzReceiveGas += gas; + } else if (optionType == ExecutorOptions.OPTION_TYPE_NATIVE_DROP) { + (uint128 nativeDropAmount,) = ExecutorOptions.decodeNativeDropOption(option); + dstAmount += nativeDropAmount; + } else if (optionType == ExecutorOptions.OPTION_TYPE_LZCOMPOSE) { + // endpoint v1 does not support lzCompose + if (v1Eid) revert Executor_UnsupportedOptionType(optionType); + + (, uint128 gas, uint128 value) = ExecutorOptions.decodeLzComposeOption(option); + if (gas == 0) revert Executor_ZeroLzComposeGasProvided(); + + dstAmount += value; + // lz compose can be called multiple times, based on unique index + // to simplify the quoting, we add lzComposeBaseGas for each lzComposeOption received + // if the same index has multiple compose options, the gas will be added multiple times + totalGas += gas + _lzComposeBaseGas; + } else if (optionType == ExecutorOptions.OPTION_TYPE_ORDERED_EXECUTION) { + ordered = true; + } else { + revert Executor_UnsupportedOptionType(optionType); + } + } + if (cursor != _options.length) revert Executor_InvalidExecutorOptions(cursor); + if (dstAmount > _nativeCap) revert Executor_NativeAmountExceedsCap(dstAmount, _nativeCap); + if (lzReceiveGas == 0) revert Executor_ZeroLzReceiveGasProvided(); + totalGas += lzReceiveGas; + + if (ordered) { + totalGas = (totalGas * 102) / 100; + } + } + + function _applyPremiumToGas( + uint256 _fee, + uint16 _bps, + uint16 _defaultBps, + uint128 _marginUSD, + uint128 _nativePriceUSD + ) internal view returns (uint256) { + uint16 multiplierBps = _bps == 0 ? _defaultBps : _bps; + + uint256 feeWithMultiplier = (_fee * multiplierBps) / 10000; + + if (_nativePriceUSD == 0 || _marginUSD == 0) { + return feeWithMultiplier; + } + uint256 feeWithMargin = (_marginUSD * nativeDecimalsRate) / _nativePriceUSD + _fee; + return feeWithMargin > feeWithMultiplier ? feeWithMargin : feeWithMultiplier; + } + + // includes value and nativeDrop + function _convertAndApplyPremiumToValue(uint256 _value, uint128 _ratio, uint128 _denom, uint16 _defaultBps) + internal + pure + returns (uint256 fee) + { + if (_value > 0) { + fee = (((_value * _ratio) / _denom) * _defaultBps) / 10000; + } + } + + function _isV1Eid(uint32 /*_eid*/ ) internal pure virtual returns (bool) { + return false; + } + + // function _isV1Eid(uint32 _eid) internal pure virtual returns (bool) { + // // v1 eid is < 30000 + // return _eid < 30000; + // } + + // send funds here to pay for price feed directly + receive() external payable {} +} diff --git a/test/helper/mocks/ExecutorMock.sol b/test/helper/mocks/ExecutorMock.sol new file mode 100644 index 0000000..9aef618 --- /dev/null +++ b/test/helper/mocks/ExecutorMock.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: LZBL-1.2 +pragma solidity ^0.8.0; + +import { + ILayerZeroEndpointV2, + Origin +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import {PacketV1Codec} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; + +import {IUltraLightNode301} from + "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/uln301/interfaces/IUltraLightNode301.sol"; +import {IExecutor} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/IExecutor.sol"; +import {IExecutorFeeLib} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/IExecutorFeeLib.sol"; + +// @dev oz4/5 breaking change... path +import {WorkerMock as Worker} from "./WorkerMock.sol"; +// @dev oz4/5 breaking change... upgradeable reentrancy +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +contract ExecutorMock is Worker, ReentrancyGuard, IExecutor { + using PacketV1Codec for bytes; + + mapping(uint32 dstEid => DstConfig) public dstConfig; + + // endpoint v2 + address public endpoint; + uint32 public localEid; + + // endpoint v1 + address public receiveUln301; + + constructor( + address _endpoint, + address _receiveUln301, + address[] memory _messageLibs, + address _priceFeed, + address _roleAdmin, + address[] memory _admins + ) Worker(_messageLibs, _priceFeed, 12000, _roleAdmin, _admins) { + endpoint = _endpoint; + localEid = ILayerZeroEndpointV2(_endpoint).eid(); + receiveUln301 = _receiveUln301; + } + + // --- Admin --- + function setDstConfig(DstConfigParam[] memory _params) external onlyRole(ADMIN_ROLE) { + for (uint256 i = 0; i < _params.length; i++) { + DstConfigParam memory param = _params[i]; + dstConfig[param.dstEid] = DstConfig( + param.lzReceiveBaseGas, + param.multiplierBps, + param.floorMarginUSD, + param.nativeCap, + param.lzComposeBaseGas + ); + } + emit DstConfigSet(_params); + } + + function nativeDrop( + Origin calldata _origin, + uint32 _dstEid, + address _oapp, + NativeDropParams[] calldata _nativeDropParams, + uint256 _nativeDropGasLimit + ) external payable onlyRole(ADMIN_ROLE) nonReentrant { + _nativeDrop(_origin, _dstEid, _oapp, _nativeDropParams, _nativeDropGasLimit); + } + + function nativeDropAndExecute301( + Origin calldata _origin, + NativeDropParams[] calldata _nativeDropParams, + uint256 _nativeDropGasLimit, + bytes calldata _packet, + uint256 _gasLimit + ) external payable onlyRole(ADMIN_ROLE) nonReentrant { + _nativeDrop(_origin, _packet.dstEid(), _packet.receiverB20(), _nativeDropParams, _nativeDropGasLimit); + IUltraLightNode301(receiveUln301).commitVerification(_packet, _gasLimit); + } + + function execute301(bytes calldata _packet, uint256 _gasLimit) external onlyRole(ADMIN_ROLE) nonReentrant { + IUltraLightNode301(receiveUln301).commitVerification(_packet, _gasLimit); + } + + function nativeDropAndExecute302( + NativeDropParams[] calldata _nativeDropParams, + uint256 _nativeDropGasLimit, + ExecutionParams calldata _executionParams + ) external payable onlyRole(ADMIN_ROLE) nonReentrant { + uint256 spent = _nativeDrop( + _executionParams.origin, localEid, _executionParams.receiver, _nativeDropParams, _nativeDropGasLimit + ); + + uint256 value = msg.value - spent; + // ignore the execution result + ILayerZeroEndpointV2(endpoint).lzReceive{value: value, gas: _executionParams.gasLimit}( + _executionParams.origin, + _executionParams.receiver, + _executionParams.guid, + _executionParams.message, + _executionParams.extraData + ); + } + + // --- Message Lib --- + function assignJob(uint32 _dstEid, address _sender, uint256 _calldataSize, bytes calldata _options) + external + onlyRole(MESSAGE_LIB_ROLE) + onlyAcl(_sender) + returns (uint256 fee) + { + IExecutorFeeLib.FeeParams memory params = + IExecutorFeeLib.FeeParams(priceFeed, _dstEid, _sender, _calldataSize, defaultMultiplierBps); + fee = IExecutorFeeLib(workerFeeLib).getFeeOnSend(params, dstConfig[_dstEid], _options); + } + + // --- Only ACL --- + function getFee(uint32 _dstEid, address _sender, uint256 _calldataSize, bytes calldata _options) + external + view + onlyAcl(_sender) + whenNotPaused + returns (uint256 fee) + { + IExecutorFeeLib.FeeParams memory params = + IExecutorFeeLib.FeeParams(priceFeed, _dstEid, _sender, _calldataSize, defaultMultiplierBps); + fee = IExecutorFeeLib(workerFeeLib).getFee(params, dstConfig[_dstEid], _options); + } + + function _nativeDrop( + Origin calldata _origin, + uint32 _dstEid, + address _oapp, + NativeDropParams[] calldata _nativeDropParams, + uint256 _nativeDropGasLimit + ) internal returns (uint256 spent) { + bool[] memory success = new bool[](_nativeDropParams.length); + for (uint256 i = 0; i < _nativeDropParams.length; i++) { + NativeDropParams memory param = _nativeDropParams[i]; + + (bool sent,) = param.receiver.call{value: param.amount, gas: _nativeDropGasLimit}(""); + + success[i] = sent; + spent += param.amount; + } + emit NativeDropApplied(_origin, _dstEid, _oapp, _nativeDropParams, success); + } +} diff --git a/test/helper/mocks/MultiSigMock.sol b/test/helper/mocks/MultiSigMock.sol new file mode 100644 index 0000000..4df6700 --- /dev/null +++ b/test/helper/mocks/MultiSigMock.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +abstract contract MultiSigMock { + enum Errors { + NoError, + SignatureError, + DuplicatedSigner, + SignerNotInCommittee + } + + mapping(address signer => bool active) public signers; + uint64 public signerSize; + uint64 public quorum; + + error MultiSig_OnlySigner(); + error MultiSig_QuorumIsZero(); + error MultiSig_SignersSizeIsLessThanQuorum(uint64 signersSize, uint64 quorum); + error MultiSig_UnorderedSigners(); + error MultiSig_StateAlreadySet(address signer, bool active); + + event UpdateSigner(address _signer, bool _active); + event UpdateQuorum(uint64 _quorum); + + modifier onlySigner() { + if (!signers[msg.sender]) { + revert MultiSig_OnlySigner(); + } + _; + } + + constructor(address[] memory _signers, uint64 _quorum) { + if (_quorum == 0) { + revert MultiSig_QuorumIsZero(); + } + if (_signers.length < _quorum) { + revert MultiSig_SignersSizeIsLessThanQuorum(uint64(_signers.length), _quorum); + } + address lastSigner = address(0); + for (uint256 i = 0; i < _signers.length; i++) { + address signer = _signers[i]; + if (signer <= lastSigner) { + revert MultiSig_UnorderedSigners(); + } + signers[signer] = true; + lastSigner = signer; + } + signerSize = uint64(_signers.length); + quorum = _quorum; + } + + function _setSigner(address _signer, bool _active) internal { + if (signers[_signer] == _active) { + revert MultiSig_StateAlreadySet(_signer, _active); + } + signers[_signer] = _active; + uint64 _signerSize = _active ? signerSize + 1 : signerSize - 1; + uint64 _quorum = quorum; + if (_signerSize < _quorum) { + revert MultiSig_SignersSizeIsLessThanQuorum(_signerSize, _quorum); + } + signerSize = _signerSize; + emit UpdateSigner(_signer, _active); + } + + function _setQuorum(uint64 _quorum) internal { + if (_quorum == 0) { + revert MultiSig_QuorumIsZero(); + } + uint64 _signerSize = signerSize; + if (_signerSize < _quorum) { + revert MultiSig_SignersSizeIsLessThanQuorum(_signerSize, _quorum); + } + quorum = _quorum; + emit UpdateQuorum(_quorum); + } + + function verifySignatures(bytes32 _hash, bytes calldata _signatures) public view returns (bool, Errors) { + if (_signatures.length != uint256(quorum) * 65) { + return (false, Errors.SignatureError); + } + + bytes32 messageDigest = _getEthSignedMessageHash(_hash); + + address lastSigner = address(0); // There cannot be a signer with address 0. + for (uint256 i = 0; i < quorum; i++) { + bytes calldata signature = _signatures[i * 65:(i + 1) * 65]; + // @dev oz4/5 breaking change... return value from tryRecover + (address currentSigner, ECDSA.RecoverError error, /*bytes32(signature.length)*/ ) = + ECDSA.tryRecover(messageDigest, signature); + + if (error != ECDSA.RecoverError.NoError) return (false, Errors.SignatureError); + if (currentSigner <= lastSigner) return (false, Errors.DuplicatedSigner); // prevent duplicate signatures + if (!signers[currentSigner]) return (false, Errors.SignerNotInCommittee); // signature is not in committee + lastSigner = currentSigner; + } + return (true, Errors.NoError); + } + + function _getEthSignedMessageHash(bytes32 _messageHash) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)); + } +} diff --git a/test/helper/mocks/PriceFeedMock.sol b/test/helper/mocks/PriceFeedMock.sol new file mode 100644 index 0000000..a6f9d06 --- /dev/null +++ b/test/helper/mocks/PriceFeedMock.sol @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +// @dev oz4/5 breaking change... Ownable constructor +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {ILayerZeroEndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import {Transfer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; + +import {ILayerZeroPriceFeed} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/ILayerZeroPriceFeed.sol"; + +// PriceFeed is updated based on v1 eids +// v2 eids will fall to the convention of v1 eid + 30,000 +contract PriceFeedMock is ILayerZeroPriceFeed, Ownable { + uint128 internal PRICE_RATIO_DENOMINATOR; + + // sets pricing + mapping(address updater => bool active) public priceUpdater; + + mapping(uint32 dstEid => Price) internal _defaultModelPrice; + ArbitrumPriceExt internal _arbitrumPriceExt; + + uint128 internal _nativePriceUSD; // uses PRICE_RATIO_DENOMINATOR + + // upgrade: arbitrum compression - percentage of callDataSize after brotli compression + uint128 public ARBITRUM_COMPRESSION_PERCENT; + + ILayerZeroEndpointV2 public endpoint; + + // ============================ Constructor =================================== + + // @dev oz4/5 breaking change... Ownable constructor + constructor(address _priceUpdater) Ownable(msg.sender) { + priceUpdater[_priceUpdater] = true; + PRICE_RATIO_DENOMINATOR = 1e20; + ARBITRUM_COMPRESSION_PERCENT = 47; + } + + // ============================ Modifier ====================================== + + // owner is always approved + modifier onlyPriceUpdater() { + if (owner() != msg.sender) { + if (!priceUpdater[msg.sender]) { + revert LZ_PriceFeed_OnlyPriceUpdater(); + } + } + _; + } + + // ============================ OnlyOwner ===================================== + + function setPriceUpdater(address _addr, bool _active) external onlyOwner { + priceUpdater[_addr] = _active; + } + + function setPriceRatioDenominator(uint128 _denominator) external onlyOwner { + PRICE_RATIO_DENOMINATOR = _denominator; + } + + function setArbitrumCompressionPercent(uint128 _compressionPercent) external onlyOwner { + ARBITRUM_COMPRESSION_PERCENT = _compressionPercent; + } + + function setEndpoint(address _endpoint) external onlyOwner { + endpoint = ILayerZeroEndpointV2(_endpoint); + } + + function withdrawFee(address _to, uint256 _amount) external onlyOwner { + Transfer.native(_to, _amount); + } + + // ============================ OnlyPriceUpdater ===================================== + + function setPrice(UpdatePrice[] calldata _price) external onlyPriceUpdater { + for (uint256 i = 0; i < _price.length; i++) { + UpdatePrice calldata _update = _price[i]; + _setPrice(_update.eid, _update.price); + } + } + + function setPriceForArbitrum(UpdatePriceExt calldata _update) external onlyPriceUpdater { + _setPrice(_update.eid, _update.price); + + uint64 gasPerL2Tx = _update.extend.gasPerL2Tx; + uint32 gasPerL1CalldataByte = _update.extend.gasPerL1CallDataByte; + + _arbitrumPriceExt.gasPerL2Tx = gasPerL2Tx; + _arbitrumPriceExt.gasPerL1CallDataByte = gasPerL1CalldataByte; + } + + function setNativeTokenPriceUSD(uint128 _nativeTokenPriceUSD) external onlyPriceUpdater { + _nativePriceUSD = _nativeTokenPriceUSD; + } + + // ============================ External ===================================== + + function estimateFeeOnSend(uint32 _dstEid, uint256 _callDataSize, uint256 _gas) + external + payable + returns (uint256, uint128, uint128, uint128) + { + uint256 fee = getFee(_dstEid, _callDataSize, _gas); + if (msg.value < fee) revert LZ_PriceFeed_InsufficientFee(msg.value, fee); + return _estimateFeeByEid(_dstEid, _callDataSize, _gas); + } + + // ============================ View ========================================== + + // get fee for calling estimateFeeOnSend + function getFee(uint32, /*_dstEid*/ uint256, /*_callDataSize*/ uint256 /*_gas*/ ) public pure returns (uint256) { + return 0; + } + + function getPriceRatioDenominator() external view returns (uint128) { + return PRICE_RATIO_DENOMINATOR; + } + + // NOTE: to be reverted when endpoint is in sendContext + function nativeTokenPriceUSD() external view returns (uint128) { + return _nativePriceUSD; + } + + // NOTE: to be reverted when endpoint is in sendContext + function arbitrumPriceExt() external view returns (ArbitrumPriceExt memory) { + return _arbitrumPriceExt; + } + + // NOTE: to be reverted when endpoint is in sendContext + function getPrice(uint32 _dstEid) external view returns (Price memory price) { + price = _defaultModelPrice[_dstEid]; + } + + // NOTE: to be reverted when endpoint is in sendContext + function estimateFeeByEid(uint32 _dstEid, uint256 _callDataSize, uint256 _gas) + external + view + returns (uint256, uint128, uint128, uint128) + { + return _estimateFeeByEid(_dstEid, _callDataSize, _gas); + } + + // NOTE: to be reverted when endpoint is in sendContext + // NOTE: to support legacy + function getPrice(uint16 _dstEid) external view returns (Price memory price) { + price = _defaultModelPrice[_dstEid]; + } + + // NOTE: to be reverted when endpoint is in sendContext + // NOTE: to support legacy + function estimateFeeByChain(uint16 _dstEid, uint256 _callDataSize, uint256 _gas) + external + view + returns (uint256 fee, uint128 priceRatio) + { + if (_dstEid == 110 || _dstEid == 10143 || _dstEid == 20143) { + return _estimateFeeWithArbitrumModel(_dstEid, _callDataSize, _gas); + } else if (_dstEid == 111 || _dstEid == 10132 || _dstEid == 20132) { + return _estimateFeeWithOptimismModel(_dstEid, _callDataSize, _gas); + } else { + return _estimateFeeWithDefaultModel(_dstEid, _callDataSize, _gas); + } + } + + // ============================ Internal ========================================== + + function _setPrice(uint32 _dstEid, Price memory _price) internal { + uint128 priceRatio = _price.priceRatio; + uint64 gasPriceInUnit = _price.gasPriceInUnit; + uint32 gasPerByte = _price.gasPerByte; + _defaultModelPrice[_dstEid] = Price(priceRatio, gasPriceInUnit, gasPerByte); + } + + function _getL1LookupId(uint32 _l2Eid) internal pure returns (uint32) { + uint32 l2Eid = _l2Eid % 30_000; + if (l2Eid == 111) { + return 101; + } else if (l2Eid == 10132) { + return 10121; // ethereum-goerli + } else if (l2Eid == 20132) { + return 20121; // ethereum-goerli + } + revert LZ_PriceFeed_UnknownL2Eid(l2Eid); + } + + function _estimateFeeWithDefaultModel(uint32 _dstEid, uint256 _callDataSize, uint256 _gas) + internal + view + returns (uint256 fee, uint128 priceRatio) + { + Price storage remotePrice = _defaultModelPrice[_dstEid]; + + // assuming the _gas includes (1) the 21,000 overhead and (2) not the calldata gas + uint256 gasForCallData = _callDataSize * remotePrice.gasPerByte; + uint256 remoteFee = (gasForCallData + _gas) * remotePrice.gasPriceInUnit; + return ((remoteFee * remotePrice.priceRatio) / PRICE_RATIO_DENOMINATOR, remotePrice.priceRatio); + } + + function _estimateFeeByEid(uint32 _dstEid, uint256 _callDataSize, uint256 _gas) + internal + view + returns (uint256 fee, uint128 priceRatio, uint128 priceRatioDenominator, uint128 priceUSD) + { + uint32 dstEid = _dstEid % 30_000; + if (dstEid == 110 || dstEid == 10143 || dstEid == 20143) { + (fee, priceRatio) = _estimateFeeWithArbitrumModel(dstEid, _callDataSize, _gas); + } else if (dstEid == 111 || dstEid == 10132 || dstEid == 20132) { + (fee, priceRatio) = _estimateFeeWithOptimismModel(dstEid, _callDataSize, _gas); + } else { + (fee, priceRatio) = _estimateFeeWithDefaultModel(dstEid, _callDataSize, _gas); + } + priceRatioDenominator = PRICE_RATIO_DENOMINATOR; + priceUSD = _nativePriceUSD; + } + + function _estimateFeeWithOptimismModel(uint32 _dstEid, uint256 _callDataSize, uint256 _gas) + internal + view + returns (uint256 fee, uint128 priceRatio) + { + uint32 ethereumId = _getL1LookupId(_dstEid); + + // L1 fee + Price storage ethereumPrice = _defaultModelPrice[ethereumId]; + uint256 gasForL1CallData = (_callDataSize * ethereumPrice.gasPerByte) + 3188; // 2100 + 68 * 16 + uint256 l1Fee = gasForL1CallData * ethereumPrice.gasPriceInUnit; + + // L2 fee + Price storage optimismPrice = _defaultModelPrice[_dstEid]; + uint256 gasForL2CallData = _callDataSize * optimismPrice.gasPerByte; + uint256 l2Fee = (gasForL2CallData + _gas) * optimismPrice.gasPriceInUnit; + + uint256 l1FeeInSrcPrice = (l1Fee * ethereumPrice.priceRatio) / PRICE_RATIO_DENOMINATOR; + uint256 l2FeeInSrcPrice = (l2Fee * optimismPrice.priceRatio) / PRICE_RATIO_DENOMINATOR; + uint256 gasFee = l1FeeInSrcPrice + l2FeeInSrcPrice; + return (gasFee, optimismPrice.priceRatio); + } + + function _estimateFeeWithArbitrumModel(uint32 _dstEid, uint256 _callDataSize, uint256 _gas) + internal + view + returns (uint256 fee, uint128 priceRatio) + { + Price storage arbitrumPrice = _defaultModelPrice[_dstEid]; + + // L1 fee + uint256 gasForL1CallData = + ((_callDataSize * ARBITRUM_COMPRESSION_PERCENT) / 100) * _arbitrumPriceExt.gasPerL1CallDataByte; + // L2 Fee + uint256 gasForL2CallData = _callDataSize * arbitrumPrice.gasPerByte; + uint256 gasFee = + (_gas + _arbitrumPriceExt.gasPerL2Tx + gasForL1CallData + gasForL2CallData) * arbitrumPrice.gasPriceInUnit; + + return ((gasFee * arbitrumPrice.priceRatio) / PRICE_RATIO_DENOMINATOR, arbitrumPrice.priceRatio); + } +} diff --git a/test/helper/mocks/ReceiveUln302Mock.sol b/test/helper/mocks/ReceiveUln302Mock.sol new file mode 100644 index 0000000..dfd89fc --- /dev/null +++ b/test/helper/mocks/ReceiveUln302Mock.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: LZBL-1.2 +pragma solidity ^0.8.0; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {PacketV1Codec} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import {SetConfigParam} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; +import { + ILayerZeroEndpointV2, + Origin +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import {IReceiveUlnE2} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/interfaces/IReceiveUlnE2.sol"; +import {ReceiveUlnBase} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/ReceiveUlnBase.sol"; +import {ReceiveLibBaseE2} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/ReceiveLibBaseE2.sol"; +import {UlnConfig} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; + +contract ReceiveUln302Mock is IReceiveUlnE2, ReceiveUlnBase, ReceiveLibBaseE2 { + using PacketV1Codec for bytes; + + /// @dev CONFIG_TYPE_ULN=2 here to align with SendUln302/ReceiveUln302/ReceiveUln301 + uint32 internal constant CONFIG_TYPE_ULN = 2; + + error LZ_ULN_InvalidConfigType(uint32 configType); + + // @dev oz4/5 breaking change... Ownable constructor + constructor(address _endpoint) Ownable(msg.sender) ReceiveLibBaseE2(_endpoint) {} + + function supportsInterface(bytes4 _interfaceId) public view override returns (bool) { + return _interfaceId == type(IReceiveUlnE2).interfaceId || super.supportsInterface(_interfaceId); + } + + // ============================ OnlyEndpoint =================================== + + // only the ULN config on the receive side + function setConfig(address _oapp, SetConfigParam[] calldata _params) external override onlyEndpoint { + for (uint256 i = 0; i < _params.length; i++) { + SetConfigParam calldata param = _params[i]; + _assertSupportedEid(param.eid); + if (param.configType == CONFIG_TYPE_ULN) { + _setUlnConfig(param.eid, _oapp, abi.decode(param.config, (UlnConfig))); + } else { + revert LZ_ULN_InvalidConfigType(param.configType); + } + } + } + + // ============================ External =================================== + + /// @dev dont need to check endpoint verifiable here to save gas, as it will reverts if not verifiable. + function commitVerification(bytes calldata _packetHeader, bytes32 _payloadHash) external { + _assertHeader(_packetHeader, localEid); + + // cache these values to save gas + address receiver = _packetHeader.receiverB20(); + uint32 srcEid = _packetHeader.srcEid(); + + UlnConfig memory config = getUlnConfig(receiver, srcEid); + _verifyAndReclaimStorage(config, keccak256(_packetHeader), _payloadHash); + + Origin memory origin = Origin(srcEid, _packetHeader.sender(), _packetHeader.nonce()); + // endpoint will revert if nonce <= lazyInboundNonce + ILayerZeroEndpointV2(endpoint).verify(origin, receiver, _payloadHash); + } + + /// @dev for dvn to verify the payload + function verify(bytes calldata _packetHeader, bytes32 _payloadHash, uint64 _confirmations) external { + _verify(_packetHeader, _payloadHash, _confirmations); + } + + // ============================ View =================================== + + function getConfig(uint32 _eid, address _oapp, uint32 _configType) external view override returns (bytes memory) { + if (_configType == CONFIG_TYPE_ULN) { + return abi.encode(getUlnConfig(_oapp, _eid)); + } else { + revert LZ_ULN_InvalidConfigType(_configType); + } + } + + function isSupportedEid(uint32 _eid) external view override returns (bool) { + return _isSupportedEid(_eid); + } + + function version() external pure override returns (uint64 major, uint8 minor, uint8 endpointVersion) { + return (3, 0, 2); + } +} diff --git a/test/helper/mocks/SendUln302Mock.sol b/test/helper/mocks/SendUln302Mock.sol new file mode 100644 index 0000000..e36b08b --- /dev/null +++ b/test/helper/mocks/SendUln302Mock.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: LZBL-1.2 +pragma solidity ^0.8.22; + +// @dev oz4/5 breaking change... Ownable constructor +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +// structs +import {Packet} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import {MessagingFee} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import {ExecutorConfig} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; +import {UlnConfig} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; + +// contracts +import {SendUlnBase} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/SendUlnBase.sol"; +import {SendLibBaseE2, WorkerOptions} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBaseE2.sol"; +import {SetConfigParam} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; + +import {TestHelperOz5} from "../TestHelperOz5.sol"; + +contract SendUln302Mock is SendUlnBase, SendLibBaseE2 { + // offchain packets schedule + TestHelperOz5 public testHelper; + + uint32 internal constant CONFIG_TYPE_EXECUTOR = 1; + uint32 internal constant CONFIG_TYPE_ULN = 2; + + error LZ_ULN_InvalidConfigType(uint32 configType); + + constructor( + address payable _verifyHelper, + address _endpoint, + uint256 _treasuryGasCap, + uint256 _treasuryGasForFeeCap + ) Ownable(msg.sender) SendLibBaseE2(_endpoint, _treasuryGasCap, _treasuryGasForFeeCap) { + testHelper = TestHelperOz5(_verifyHelper); + } + + // ============================ OnlyEndpoint =================================== + + // on the send side the user can config both the executor and the ULN + function setConfig(address _oapp, SetConfigParam[] calldata _params) external override onlyEndpoint { + for (uint256 i = 0; i < _params.length; i++) { + SetConfigParam calldata param = _params[i]; + _assertSupportedEid(param.eid); + if (param.configType == CONFIG_TYPE_EXECUTOR) { + _setExecutorConfig(param.eid, _oapp, abi.decode(param.config, (ExecutorConfig))); + } else if (param.configType == CONFIG_TYPE_ULN) { + _setUlnConfig(param.eid, _oapp, abi.decode(param.config, (UlnConfig))); + } else { + revert LZ_ULN_InvalidConfigType(param.configType); + } + } + } + + // ============================ View =================================== + + function getConfig(uint32 _eid, address _oapp, uint32 _configType) external view override returns (bytes memory) { + if (_configType == CONFIG_TYPE_EXECUTOR) { + return abi.encode(getExecutorConfig(_oapp, _eid)); + } else if (_configType == CONFIG_TYPE_ULN) { + return abi.encode(getUlnConfig(_oapp, _eid)); + } else { + revert LZ_ULN_InvalidConfigType(_configType); + } + } + + function version() external pure override returns (uint64 major, uint8 minor, uint8 endpointVersion) { + return (3, 0, 2); + } + + function isSupportedEid(uint32 _eid) external view override returns (bool) { + return _isSupportedEid(_eid); + } + + // ============================ Internal =================================== + + function _quoteVerifier(address _sender, uint32 _dstEid, WorkerOptions[] memory _options) + internal + view + override + returns (uint256) + { + return _quoteDVNs(_sender, _dstEid, _options); + } + + function _payVerifier(Packet calldata _packet, WorkerOptions[] memory _options) + internal + override + returns (uint256 otherWorkerFees, bytes memory encodedPacket) + { + (otherWorkerFees, encodedPacket) = _payDVNs(fees, _packet, _options); + } + + function _splitOptions(bytes calldata _options) + internal + pure + override + returns (bytes memory, WorkerOptions[] memory) + { + return _splitUlnOptions(_options); + } + + function send(Packet calldata _packet, bytes calldata _options, bool _payInLzToken) + public + override + returns (MessagingFee memory fee, bytes memory encodedPacket) + { + (fee, encodedPacket) = super.send(_packet, _options, _payInLzToken); + testHelper.schedulePacket(encodedPacket, _options); + } +} diff --git a/test/helper/mocks/SimpleMessageLibMock.sol b/test/helper/mocks/SimpleMessageLibMock.sol new file mode 100644 index 0000000..e49267f --- /dev/null +++ b/test/helper/mocks/SimpleMessageLibMock.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: LZBL-1.2 +pragma solidity ^0.8.22; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {IMessageLib, MessageLibType} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLib.sol"; +import {Packet} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import { + ILayerZeroEndpointV2, + MessagingFee, + Origin +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import {Errors} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Errors.sol"; +import {PacketV1Codec} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import {Transfer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; + +import {TestHelperOz5} from "../TestHelperOz5.sol"; + +contract SimpleMessageLibMock is Ownable, ERC165 { + // offchain packets schedule + TestHelperOz5 public testHelper; + + using SafeERC20 for IERC20; + using PacketV1Codec for bytes; + + address public immutable endpoint; + address public immutable treasury; + uint32 public immutable localEid; + uint8 public constant PACKET_VERSION = 1; + + address public whitelistCaller; + + uint256 public lzTokenFee; + uint256 public nativeFee; + + bytes public defaultOption; + + error OnlyEndpoint(); + error OnlyWhitelistCaller(); + error InvalidEndpoint(address expected, address actual); + error ToIsAddressZero(); + error LzTokenIsAddressZero(); + error TransferFailed(); + + // only the endpoint can call SEND() and setConfig() + modifier onlyEndpoint() { + if (endpoint != msg.sender) { + revert OnlyEndpoint(); + } + _; + } + + // @dev oz4/5 breaking change... Ownable constructor + constructor(address payable _verifyHelper, address _endpoint) Ownable(msg.sender) { + testHelper = TestHelperOz5(_verifyHelper); + endpoint = _endpoint; + treasury = address(0x0); + localEid = ILayerZeroEndpointV2(_endpoint).eid(); + lzTokenFee = 99; + nativeFee = 100; + // defaultOption = Options.encodeLegacyOptionsType1(200000); + } + + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return interfaceId == type(IMessageLib).interfaceId || super.supportsInterface(interfaceId); + } + + // no validation logic at all + function validatePacket(bytes calldata packetBytes) external { + if (whitelistCaller != address(0x0) && msg.sender != whitelistCaller) { + revert OnlyWhitelistCaller(); + } + Origin memory origin = Origin(packetBytes.srcEid(), packetBytes.sender(), packetBytes.nonce()); + ILayerZeroEndpointV2(endpoint).verify(origin, packetBytes.receiverB20(), keccak256(packetBytes.payload())); + } + + // ------------------ onlyEndpoint ------------------ + function send(Packet calldata _packet, bytes memory _options, bool _payInLzToken) + external + onlyEndpoint + returns (MessagingFee memory fee, bytes memory encodedPacket, bytes memory options) + { + encodedPacket = PacketV1Codec.encode(_packet); + + options = _options.length == 0 ? defaultOption : _options; + _handleMessagingParamsHook(encodedPacket, options); + + fee = MessagingFee(nativeFee, _payInLzToken ? lzTokenFee : 0); + } + + // ------------------ onlyOwner ------------------ + function setDefaultOption(bytes memory _defaultOption) external onlyOwner { + defaultOption = _defaultOption; + } + + function setMessagingFee(uint256 _nativeFee, uint256 _lzTokenFee) external onlyOwner { + nativeFee = _nativeFee; + lzTokenFee = _lzTokenFee; + } + + function setWhitelistCaller(address _whitelistCaller) external onlyOwner { + whitelistCaller = _whitelistCaller; + } + + function withdrawFee(address _to, uint256 _amount) external onlyOwner { + if (_to == address(0x0)) { + revert ToIsAddressZero(); + } + + address altTokenAddr = ILayerZeroEndpointV2(endpoint).nativeToken(); + + // transfers native if altTokenAddr == address(0x0) + Transfer.nativeOrToken(altTokenAddr, _to, _amount); + } + + function withdrawLzTokenFee(address _to, uint256 _amount) external onlyOwner { + if (_to == address(0x0)) { + revert ToIsAddressZero(); + } + address lzToken = ILayerZeroEndpointV2(endpoint).lzToken(); + if (lzToken == address(0x0)) { + revert LzTokenIsAddressZero(); + } + IERC20(lzToken).safeTransfer(_to, _amount); + } + + // ------------------ View ------------------ + function quote(Packet calldata, /*_packet*/ bytes calldata, /*_options*/ bool _payInLzToken) + external + view + returns (MessagingFee memory) + { + return MessagingFee(nativeFee, _payInLzToken ? lzTokenFee : 0); + } + + function isSupportedEid(uint32) external pure returns (bool) { + return true; + } + + function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) { + return (0, 0, 2); + } + + function messageLibType() external pure returns (MessageLibType) { + return MessageLibType.SendAndReceive; + } + + // ------------------ Internal ------------------ + // function _handleMessagingParamsHook(bytes memory _encodedPacket, bytes memory _options) internal virtual {} + + function _handleMessagingParamsHook(bytes memory _encodedPacket, bytes memory _options) internal virtual { + testHelper.schedulePacket(_encodedPacket, _options); + } + + fallback() external payable { + revert Errors.LZ_NotImplemented(); + } + + receive() external payable {} +} diff --git a/test/helper/mocks/WorkerMock.sol b/test/helper/mocks/WorkerMock.sol new file mode 100644 index 0000000..e159172 --- /dev/null +++ b/test/helper/mocks/WorkerMock.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: LZBL-1.2 +pragma solidity ^0.8.0; + +// @dev oz4/5 breaking change... path +import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +import {ISendLib} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import {Transfer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; + +import {IWorker} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/IWorker.sol"; + +abstract contract WorkerMock is AccessControl, Pausable, IWorker { + bytes32 internal constant MESSAGE_LIB_ROLE = keccak256("MESSAGE_LIB_ROLE"); + bytes32 internal constant ALLOWLIST = keccak256("ALLOWLIST"); + bytes32 internal constant DENYLIST = keccak256("DENYLIST"); + bytes32 internal constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + + address public workerFeeLib; + + uint64 public allowlistSize; + uint16 public defaultMultiplierBps; + address public priceFeed; + + mapping(uint32 eid => uint8[] optionTypes) internal supportedOptionTypes; + + /// @param _messageLibs array of message lib addresses that are granted the MESSAGE_LIB_ROLE + /// @param _priceFeed price feed address + /// @param _defaultMultiplierBps default multiplier for worker fee + /// @param _roleAdmin address that is granted the DEFAULT_ADMIN_ROLE (can grant and revoke all roles) + /// @param _admins array of admin addresses that are granted the ADMIN_ROLE + constructor( + address[] memory _messageLibs, + address _priceFeed, + uint16 _defaultMultiplierBps, + address _roleAdmin, + address[] memory _admins + ) { + defaultMultiplierBps = _defaultMultiplierBps; + priceFeed = _priceFeed; + + if (_roleAdmin != address(0x0)) { + _grantRole(DEFAULT_ADMIN_ROLE, _roleAdmin); // _roleAdmin can grant and revoke all roles + } + + for (uint256 i = 0; i < _messageLibs.length; ++i) { + _grantRole(MESSAGE_LIB_ROLE, _messageLibs[i]); + } + + for (uint256 i = 0; i < _admins.length; ++i) { + _grantRole(ADMIN_ROLE, _admins[i]); + } + } + + // ========================= Modifier ========================= + + modifier onlyAcl(address _sender) { + if (!hasAcl(_sender)) { + revert Worker_NotAllowed(); + } + _; + } + + /// @dev Access control list using allowlist and denylist + /// @dev 1) if one address is in the denylist -> deny + /// @dev 2) else if address in the allowlist OR allowlist is empty (allows everyone)-> allow + /// @dev 3) else deny + /// @param _sender address to check + function hasAcl(address _sender) public view returns (bool) { + if (hasRole(DENYLIST, _sender)) { + return false; + } else if (allowlistSize == 0 || hasRole(ALLOWLIST, _sender)) { + return true; + } else { + return false; + } + } + + // ========================= OnyDefaultAdmin ========================= + + /// @dev flag to pause execution of workers (if used with whenNotPaused modifier) + /// @param _paused true to pause, false to unpause + function setPaused(bool _paused) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_paused) { + _pause(); + } else { + _unpause(); + } + } + + // ========================= OnlyAdmin ========================= + + /// @param _priceFeed price feed address + function setPriceFeed(address _priceFeed) external onlyRole(ADMIN_ROLE) { + priceFeed = _priceFeed; + emit SetPriceFeed(_priceFeed); + } + + /// @param _workerFeeLib worker fee lib address + function setWorkerFeeLib(address _workerFeeLib) external onlyRole(ADMIN_ROLE) { + workerFeeLib = _workerFeeLib; + emit SetWorkerLib(_workerFeeLib); + } + + /// @param _multiplierBps default multiplier for worker fee + function setDefaultMultiplierBps(uint16 _multiplierBps) external onlyRole(ADMIN_ROLE) { + defaultMultiplierBps = _multiplierBps; + emit SetDefaultMultiplierBps(_multiplierBps); + } + + /// @dev supports withdrawing fee from ULN301, ULN302 and more + /// @param _lib message lib address + /// @param _to address to withdraw fee to + /// @param _amount amount to withdraw + function withdrawFee(address _lib, address _to, uint256 _amount) external onlyRole(ADMIN_ROLE) { + if (!hasRole(MESSAGE_LIB_ROLE, _lib)) revert Worker_OnlyMessageLib(); + ISendLib(_lib).withdrawFee(_to, _amount); + emit Withdraw(_lib, _to, _amount); + } + + /// @dev supports withdrawing token from the contract + /// @param _token token address + /// @param _to address to withdraw token to + /// @param _amount amount to withdraw + function withdrawToken(address _token, address _to, uint256 _amount) external onlyRole(ADMIN_ROLE) { + // transfers native if _token is address(0x0) + Transfer.nativeOrToken(_token, _to, _amount); + } + + function setSupportedOptionTypes(uint32 _eid, uint8[] calldata _optionTypes) external onlyRole(ADMIN_ROLE) { + supportedOptionTypes[_eid] = _optionTypes; + } + + // ========================= View Functions ========================= + function getSupportedOptionTypes(uint32 _eid) external view returns (uint8[] memory) { + return supportedOptionTypes[_eid]; + } + + // ========================= Internal Functions ========================= + + /// @dev overrides AccessControl to allow for counting of allowlistSize + /// @param _role role to grant + /// @param _account address to grant role to + function _grantRole(bytes32 _role, address _account) internal override returns (bool) { + if (_role == ALLOWLIST && !hasRole(_role, _account)) { + ++allowlistSize; + } + super._grantRole(_role, _account); + return true; + } + + /// @dev overrides AccessControl to allow for counting of allowlistSize + /// @param _role role to revoke + /// @param _account address to revoke role from + function _revokeRole(bytes32 _role, address _account) internal override returns (bool) { + if (_role == ALLOWLIST && hasRole(_role, _account)) { + --allowlistSize; + } + super._revokeRole(_role, _account); + return true; + } + + /// @dev overrides AccessControl to disable renouncing of roles + function renounceRole(bytes32, /*role*/ address /*account*/ ) public pure override { + revert Worker_RoleRenouncingDisabled(); + } +}