diff --git a/packages/layerzero-v2/evm/messagelib/contracts/Executor.sol b/packages/layerzero-v2/evm/messagelib/contracts/Executor.sol index 7eb75d6..7e64f06 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/Executor.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/Executor.sol @@ -64,7 +64,7 @@ contract Executor is WorkerUpgradeable, ReentrancyGuardUpgradeable, Proxied, IEx // endpoint v2 address public endpoint; - uint32 public localEid; + uint32 public localEidV2; // endpoint v1 address public receiveUln301; @@ -80,7 +80,7 @@ contract Executor is WorkerUpgradeable, ReentrancyGuardUpgradeable, Proxied, IEx __ReentrancyGuard_init(); __Worker_init(_messageLibs, _priceFeed, 12000, _roleAdmin, _admins); endpoint = _endpoint; - localEid = ILayerZeroEndpointV2(_endpoint).eid(); + localEidV2 = ILayerZeroEndpointV2(_endpoint).eid(); receiveUln301 = _receiveUln301; } @@ -130,13 +130,13 @@ contract Executor is WorkerUpgradeable, ReentrancyGuardUpgradeable, Proxied, IEx function execute302(ExecutionParams calldata _executionParams) external payable onlyRole(ADMIN_ROLE) nonReentrant { try - ILayerZeroEndpointV2(endpoint).lzReceive{ value: msg.value, gas: _executionParams.gasLimit }( - _executionParams.origin, - _executionParams.receiver, - _executionParams.guid, - _executionParams.message, - _executionParams.extraData - ) + ILayerZeroEndpointV2(endpoint).lzReceive{ value: msg.value, gas: _executionParams.gasLimit }( + _executionParams.origin, + _executionParams.receiver, + _executionParams.guid, + _executionParams.message, + _executionParams.extraData + ) { // do nothing } catch (bytes memory reason) { @@ -163,14 +163,14 @@ contract Executor is WorkerUpgradeable, ReentrancyGuardUpgradeable, Proxied, IEx uint256 _gasLimit ) external payable onlyRole(ADMIN_ROLE) nonReentrant { try - ILayerZeroEndpointV2(endpoint).lzCompose{ value: msg.value, gas: _gasLimit }( - _from, - _to, - _guid, - _index, - _message, - _extraData - ) + ILayerZeroEndpointV2(endpoint).lzCompose{ value: msg.value, gas: _gasLimit }( + _from, + _to, + _guid, + _index, + _message, + _extraData + ) { // do nothing } catch (bytes memory reason) { @@ -195,7 +195,7 @@ contract Executor is WorkerUpgradeable, ReentrancyGuardUpgradeable, Proxied, IEx ) external payable onlyRole(ADMIN_ROLE) nonReentrant { uint256 spent = _nativeDrop( _executionParams.origin, - localEid, + localEidV2, _executionParams.receiver, _nativeDropParams, _nativeDropGasLimit @@ -203,13 +203,13 @@ contract Executor is WorkerUpgradeable, ReentrancyGuardUpgradeable, Proxied, IEx uint256 value = msg.value - spent; try - ILayerZeroEndpointV2(endpoint).lzReceive{ value: value, gas: _executionParams.gasLimit }( - _executionParams.origin, - _executionParams.receiver, - _executionParams.guid, - _executionParams.message, - _executionParams.extraData - ) + ILayerZeroEndpointV2(endpoint).lzReceive{ value: value, gas: _executionParams.gasLimit }( + _executionParams.origin, + _executionParams.receiver, + _executionParams.guid, + _executionParams.message, + _executionParams.extraData + ) { // do nothing } catch (bytes memory reason) { @@ -243,6 +243,19 @@ contract Executor is WorkerUpgradeable, ReentrancyGuardUpgradeable, Proxied, IEx fee = IExecutorFeeLib(workerFeeLib).getFeeOnSend(params, dstConfig[_dstEid], _options); } + // assignJob for CmdLib + function assignJob( + address _sender, + bytes calldata _options + ) external onlyRole(MESSAGE_LIB_ROLE) onlyAcl(_sender) whenNotPaused returns (uint256 fee) { + IExecutorFeeLib.FeeParamsForRead memory params = IExecutorFeeLib.FeeParamsForRead( + priceFeed, + _sender, + defaultMultiplierBps + ); + fee = IExecutorFeeLib(workerFeeLib).getFeeOnSend(params, dstConfig[localEidV2], _options); + } + // --- Only ACL --- function getFee( uint32 _dstEid, @@ -260,6 +273,18 @@ contract Executor is WorkerUpgradeable, ReentrancyGuardUpgradeable, Proxied, IEx fee = IExecutorFeeLib(workerFeeLib).getFee(params, dstConfig[_dstEid], _options); } + function getFee( + address _sender, + bytes calldata _options + ) external view onlyAcl(_sender) whenNotPaused returns (uint256 fee) { + IExecutorFeeLib.FeeParamsForRead memory params = IExecutorFeeLib.FeeParamsForRead( + priceFeed, + _sender, + defaultMultiplierBps + ); + fee = IExecutorFeeLib(workerFeeLib).getFee(params, dstConfig[localEidV2], _options); + } + function _nativeDrop( Origin calldata _origin, uint32 _dstEid, diff --git a/packages/layerzero-v2/evm/messagelib/contracts/ExecutorFeeLib.sol b/packages/layerzero-v2/evm/messagelib/contracts/ExecutorFeeLib.sol index c74d191..8ab1cf3 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/ExecutorFeeLib.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/ExecutorFeeLib.sol @@ -5,18 +5,20 @@ 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 { ExecutorOptions } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; import { ILayerZeroPriceFeed } from "./interfaces/ILayerZeroPriceFeed.sol"; import { IExecutor } from "./interfaces/IExecutor.sol"; import { IExecutorFeeLib } from "./interfaces/IExecutorFeeLib.sol"; +import { ExecutorOptions } from "./libs/ExecutorOptions.sol"; contract ExecutorFeeLib is Ownable, IExecutorFeeLib { using ExecutorOptions for bytes; uint256 private immutable nativeDecimalsRate; + uint32 private immutable localEidV2; // endpoint-v2 only, for read call - constructor(uint256 _nativeDecimalsRate) { + constructor(uint32 _localEidV2, uint256 _nativeDecimalsRate) { + localEidV2 = _localEidV2; nativeDecimalsRate = _nativeDecimalsRate; } @@ -31,10 +33,28 @@ contract ExecutorFeeLib is Ownable, IExecutorFeeLib { FeeParams calldata _params, IExecutor.DstConfig calldata _dstConfig, bytes calldata _options - ) external returns (uint256 fee) { + ) external view returns (uint256 fee) { + fee = getFee(_params, _dstConfig, _options); + } + + function getFeeOnSend( + FeeParamsForRead calldata _params, + IExecutor.DstConfig calldata _dstConfig, + bytes calldata _options + ) external view returns (uint256 fee) { + fee = getFee(_params, _dstConfig, _options); + } + + // ================================ View ================================ + function getFee( + FeeParams calldata _params, + IExecutor.DstConfig calldata _dstConfig, + bytes calldata _options + ) public view returns (uint256 fee) { if (_dstConfig.lzReceiveBaseGas == 0) revert Executor_EidNotSupported(_params.dstEid); - (uint256 totalDstAmount, uint256 totalGas) = _decodeExecutorOptions( + (uint256 totalValue, uint256 totalGas, ) = _decodeExecutorOptions( + false, _isV1Eid(_params.dstEid), _dstConfig.lzReceiveBaseGas, _dstConfig.lzComposeBaseGas, @@ -42,30 +62,29 @@ contract ExecutorFeeLib is Ownable, IExecutorFeeLib { _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); + ) = ILayerZeroPriceFeed(_params.priceFeed).estimateFeeByEid(_params.dstEid, _params.calldataSize, totalGas); uint16 multiplierBps = _dstConfig.multiplierBps == 0 ? _params.defaultMultiplierBps : _dstConfig.multiplierBps; fee = _applyPremiumToGas(totalGasFee, multiplierBps, _dstConfig.floorMarginUSD, nativePriceUSD); - fee += _convertAndApplyPremiumToValue(totalDstAmount, priceRatio, priceRatioDenominator, multiplierBps); + fee += _convertAndApplyPremiumToValue(totalValue, priceRatio, priceRatioDenominator, multiplierBps); } - // ================================ View ================================ function getFee( - FeeParams calldata _params, + FeeParamsForRead calldata _params, IExecutor.DstConfig calldata _dstConfig, bytes calldata _options - ) external view returns (uint256 fee) { - if (_dstConfig.lzReceiveBaseGas == 0) revert Executor_EidNotSupported(_params.dstEid); + ) public view returns (uint256 fee) { + if (_dstConfig.lzReceiveBaseGas == 0) revert Executor_EidNotSupported(localEidV2); - (uint256 totalDstAmount, uint256 totalGas) = _decodeExecutorOptions( - _isV1Eid(_params.dstEid), + (uint256 totalValue, uint256 totalGas, uint32 calldataSize) = _decodeExecutorOptions( + true, + false, // endpoint v2 only _dstConfig.lzReceiveBaseGas, _dstConfig.lzComposeBaseGas, _dstConfig.nativeCap, @@ -77,74 +96,108 @@ contract ExecutorFeeLib is Ownable, IExecutorFeeLib { uint128 priceRatio, uint128 priceRatioDenominator, uint128 nativePriceUSD - ) = ILayerZeroPriceFeed(_params.priceFeed).estimateFeeByEid(_params.dstEid, _params.calldataSize, totalGas); + ) = ILayerZeroPriceFeed(_params.priceFeed).estimateFeeByEid(localEidV2, calldataSize, totalGas); uint16 multiplierBps = _dstConfig.multiplierBps == 0 ? _params.defaultMultiplierBps : _dstConfig.multiplierBps; fee = _applyPremiumToGas(totalGasFee, multiplierBps, _dstConfig.floorMarginUSD, nativePriceUSD); - fee += _convertAndApplyPremiumToValue(totalDstAmount, priceRatio, priceRatioDenominator, multiplierBps); + fee += _convertAndApplyPremiumToValue(totalValue, priceRatio, priceRatioDenominator, multiplierBps); } // ================================ Internal ================================ // @dev decode executor options into dstAmount and totalGas function _decodeExecutorOptions( + bool _isRead, bool _v1Eid, uint64 _lzReceiveBaseGas, uint64 _lzComposeBaseGas, uint128 _nativeCap, bytes calldata _options - ) internal pure returns (uint256 dstAmount, uint256 totalGas) { + ) internal pure returns (uint256 totalValue, uint256 totalGas, uint32 calldataSize) { + ExecutorOptionsAgg memory aggOptions = _parseExecutorOptions(_options, _isRead, _v1Eid, _nativeCap); + totalValue = aggOptions.totalValue; + calldataSize = aggOptions.calldataSize; + + // lz receive only called once + // 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 = _lzReceiveBaseGas + aggOptions.totalGas + _lzComposeBaseGas * aggOptions.numLzCompose; + if (aggOptions.ordered) { + totalGas = (totalGas * 102) / 100; + } + } + + struct ExecutorOptionsAgg { + uint256 totalValue; + uint256 totalGas; + bool ordered; + uint32 calldataSize; + uint256 numLzCompose; + } + + function _parseExecutorOptions( + bytes calldata _options, + bool _isRead, + bool _v1Eid, + uint128 _nativeCap + ) internal pure returns (ExecutorOptionsAgg memory options) { 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; + uint32 calldataSize; while (cursor < _options.length) { (uint8 optionType, bytes calldata option, uint256 newCursor) = _options.nextExecutorOption(cursor); cursor = newCursor; if (optionType == ExecutorOptions.OPTION_TYPE_LZRECEIVE) { + // lzRead does not support lzReceive option + if (_isRead) revert Executor_UnsupportedOptionType(optionType); (uint128 gas, uint128 value) = ExecutorOptions.decodeLzReceiveOption(option); // endpoint v1 does not support lzReceive with value - if (v1Eid && value > 0) revert Executor_UnsupportedOptionType(optionType); + if (_v1Eid && value > 0) revert Executor_UnsupportedOptionType(optionType); - dstAmount += value; + options.totalValue += value; lzReceiveGas += gas; } else if (optionType == ExecutorOptions.OPTION_TYPE_NATIVE_DROP) { + // lzRead does not support nativeDrop option + if (_isRead) revert Executor_UnsupportedOptionType(optionType); + (uint128 nativeDropAmount, ) = ExecutorOptions.decodeNativeDropOption(option); - dstAmount += nativeDropAmount; + options.totalValue += nativeDropAmount; } else if (optionType == ExecutorOptions.OPTION_TYPE_LZCOMPOSE) { // endpoint v1 does not support lzCompose - if (v1Eid) revert Executor_UnsupportedOptionType(optionType); + 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; + options.totalValue += value; + options.totalGas += gas; + options.numLzCompose++; } else if (optionType == ExecutorOptions.OPTION_TYPE_ORDERED_EXECUTION) { - ordered = true; + options.ordered = true; + } else if (optionType == ExecutorOptions.OPTION_TYPE_LZREAD) { + if (!_isRead) revert Executor_UnsupportedOptionType(optionType); + + (uint128 gas, uint32 size, uint128 value) = ExecutorOptions.decodeLzReadOption(option); + options.totalValue += value; + lzReceiveGas += gas; + calldataSize += size; } else { revert Executor_UnsupportedOptionType(optionType); } } if (cursor != _options.length) revert Executor_InvalidExecutorOptions(cursor); - if (dstAmount > _nativeCap) revert Executor_NativeAmountExceedsCap(dstAmount, _nativeCap); + if (options.totalValue > _nativeCap) revert Executor_NativeAmountExceedsCap(options.totalValue, _nativeCap); if (lzReceiveGas == 0) revert Executor_ZeroLzReceiveGasProvided(); - totalGas += lzReceiveGas; - - if (ordered) { - totalGas = (totalGas * 102) / 100; - } + if (_isRead && calldataSize == 0) revert Executor_ZeroCalldataSizeProvided(); + options.totalGas += lzReceiveGas; + options.calldataSize = calldataSize; } function _applyPremiumToGas( @@ -179,6 +232,10 @@ contract ExecutorFeeLib is Ownable, IExecutorFeeLib { return _eid < 30000; } + function version() external pure returns (uint64 major, uint8 minor) { + return (1, 1); + } + // send funds here to pay for price feed directly receive() external payable {} } diff --git a/packages/layerzero-v2/evm/messagelib/contracts/PriceFeed.sol b/packages/layerzero-v2/evm/messagelib/contracts/PriceFeed.sol index be3b494..0ef84b9 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/PriceFeed.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/PriceFeed.sol @@ -10,6 +10,17 @@ import { Transfer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Trans import { ILayerZeroPriceFeed } from "./interfaces/ILayerZeroPriceFeed.sol"; +enum ModelType { + DEFAULT, + ARB_STACK, + OP_STACK +} + +struct SetEidToModelTypeParam { + uint32 dstEid; + ModelType modelType; +} + // PriceFeed is updated based on v1 eids // v2 eids will fall to the convention of v1 eid + 30,000 contract PriceFeed is ILayerZeroPriceFeed, OwnableUpgradeable, Proxied { @@ -28,6 +39,9 @@ contract PriceFeed is ILayerZeroPriceFeed, OwnableUpgradeable, Proxied { ILayerZeroEndpointV2 public endpoint; + // for the destination endpoint id, return the fee model type + mapping(uint32 => ModelType) public eidToModelType; + // ============================ Constructor =================================== function initialize(address _priceUpdater) public proxied initializer { @@ -63,6 +77,13 @@ contract PriceFeed is ILayerZeroPriceFeed, OwnableUpgradeable, Proxied { ARBITRUM_COMPRESSION_PERCENT = _compressionPercent; } + // set the fee ModelType for the destination eid + function setEidToModelType(SetEidToModelTypeParam[] calldata _params) external onlyOwner { + for (uint i = 0; i < _params.length; i++) { + eidToModelType[_params[i].dstEid] = _params[i].modelType; + } + } + function setEndpoint(address _endpoint) external onlyOwner { endpoint = ILayerZeroEndpointV2(_endpoint); } @@ -154,10 +175,19 @@ contract PriceFeed is ILayerZeroPriceFeed, OwnableUpgradeable, Proxied { uint256 _callDataSize, uint256 _gas ) external view returns (uint256 fee, uint128 priceRatio) { + // legacy if-statement uses very little gas, can keep using it until future upgrade 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); + } + + // fee model type is configured per eid + ModelType _modelType = eidToModelType[_dstEid]; + if (_modelType == ModelType.OP_STACK) { + return _estimateFeeWithOptimismModel(_dstEid, _callDataSize, _gas); + } else if (_modelType == ModelType.ARB_STACK) { + return _estimateFeeWithArbitrumModel(_dstEid, _callDataSize, _gas); } else { return _estimateFeeWithDefaultModel(_dstEid, _callDataSize, _gas); } @@ -172,7 +202,7 @@ contract PriceFeed is ILayerZeroPriceFeed, OwnableUpgradeable, Proxied { _defaultModelPrice[_dstEid] = Price(priceRatio, gasPriceInUnit, gasPerByte); } - function _getL1LookupId(uint32 _l2Eid) internal pure returns (uint32) { + function _getL1LookupIdForOptimismModel(uint32 _l2Eid) internal view returns (uint32) { uint32 l2Eid = _l2Eid % 30_000; if (l2Eid == 111) { return 101; @@ -181,7 +211,15 @@ contract PriceFeed is ILayerZeroPriceFeed, OwnableUpgradeable, Proxied { } else if (l2Eid == 20132) { return 20121; // ethereum-goerli } - revert LZ_PriceFeed_UnknownL2Eid(l2Eid); + + if (eidToModelType[l2Eid] != ModelType.OP_STACK) revert LZ_PriceFeed_NotAnOPStack(_l2Eid); + if (l2Eid < 10000) { + return 101; + } else if (l2Eid < 20000) { + return 10161; // ethereum-sepolia + } else { + return 20121; // ethereum-goerli + } } function _estimateFeeWithDefaultModel( @@ -207,9 +245,18 @@ contract PriceFeed is ILayerZeroPriceFeed, OwnableUpgradeable, Proxied { (fee, priceRatio) = _estimateFeeWithArbitrumModel(dstEid, _callDataSize, _gas); } else if (dstEid == 111 || dstEid == 10132 || dstEid == 20132) { (fee, priceRatio) = _estimateFeeWithOptimismModel(dstEid, _callDataSize, _gas); + } + + // lookup map stuff + ModelType _modelType = eidToModelType[dstEid]; + if (_modelType == ModelType.OP_STACK) { + (fee, priceRatio) = _estimateFeeWithOptimismModel(dstEid, _callDataSize, _gas); + } else if (_modelType == ModelType.ARB_STACK) { + (fee, priceRatio) = _estimateFeeWithArbitrumModel(dstEid, _callDataSize, _gas); } else { (fee, priceRatio) = _estimateFeeWithDefaultModel(dstEid, _callDataSize, _gas); } + priceRatioDenominator = PRICE_RATIO_DENOMINATOR; priceUSD = _nativePriceUSD; } @@ -219,7 +266,7 @@ contract PriceFeed is ILayerZeroPriceFeed, OwnableUpgradeable, Proxied { uint256 _callDataSize, uint256 _gas ) internal view returns (uint256 fee, uint128 priceRatio) { - uint32 ethereumId = _getL1LookupId(_dstEid); + uint32 ethereumId = _getL1LookupIdForOptimismModel(_dstEid); // L1 fee Price storage ethereumPrice = _defaultModelPrice[ethereumId]; diff --git a/packages/layerzero-v2/evm/messagelib/contracts/SimpleReadExecutor.sol b/packages/layerzero-v2/evm/messagelib/contracts/SimpleReadExecutor.sol new file mode 100644 index 0000000..dc22d9f --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/contracts/SimpleReadExecutor.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import { Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import { ILayerZeroReadExecutor } from "./interfaces/ILayerZeroReadExecutor.sol"; +import { ExecutorOptions } from "./libs/ExecutorOptions.sol"; + +struct ExecutionParams { + address receiver; + Origin origin; + bytes32 guid; + bytes message; + bytes extraData; + uint256 gasLimit; +} + +interface ILayerZeroEndpointV2 { + function eid() external view returns (uint32); + + function lzReceive( + Origin calldata _origin, + address _receiver, + bytes32 _guid, + bytes calldata _message, + bytes calldata _extraData + ) external payable; + + function lzReceiveAlert( + Origin calldata _origin, + address _receiver, + bytes32 _guid, + uint256 _gas, + uint256 _value, + bytes calldata _message, + bytes calldata _extraData, + bytes calldata _reason + ) external; +} + +contract SimpleReadExecutor is ILayerZeroReadExecutor { + using ExecutorOptions for bytes; + + address public immutable endpoint; + + uint128 public gasPerByte; + uint128 public gasPrice; + + constructor(address _endpoint) { + endpoint = _endpoint; + } + + function configGas(uint128 _gasPerByte, uint128 _gasPrice) external { + gasPerByte = _gasPerByte; + gasPrice = _gasPrice; + } + + function assignJob(address _sender, bytes calldata _options) external returns (uint256) { + return getFee(_sender, _options); + } + + function execute(ExecutionParams calldata _executionParams) external payable { + try + ILayerZeroEndpointV2(endpoint).lzReceive{ value: msg.value, gas: _executionParams.gasLimit }( + _executionParams.origin, + _executionParams.receiver, + _executionParams.guid, + _executionParams.message, + _executionParams.extraData + ) + { + // do nothing + } catch (bytes memory reason) { + ILayerZeroEndpointV2(endpoint).lzReceiveAlert( + _executionParams.origin, + _executionParams.receiver, + _executionParams.guid, + _executionParams.gasLimit, + msg.value, + _executionParams.message, + _executionParams.extraData, + reason + ); + } + } + + function mustExecute(ExecutionParams calldata _executionParams) external payable { + ILayerZeroEndpointV2(endpoint).lzReceive{ value: msg.value, gas: _executionParams.gasLimit }( + _executionParams.origin, + _executionParams.receiver, + _executionParams.guid, + _executionParams.message, + _executionParams.extraData + ); + } + + // ========================= View ========================= + + function getFee(address /*_sender*/, bytes calldata _options) public view returns (uint256) { + // For simplify, we only support one execute option, and must be LZREAD type + (uint8 optionType, bytes calldata option, ) = _options.nextExecutorOption(0); + require(optionType == ExecutorOptions.OPTION_TYPE_LZREAD, "SimpleReadExecutor: not LZREAD option"); + (uint128 gas, uint32 calldataSize, uint128 value) = option.decodeLzReadOption(); + // calculate fee + return (gas + calldataSize * gasPerByte) * gasPrice + value; + } + + receive() external payable virtual {} +} diff --git a/packages/layerzero-v2/evm/messagelib/contracts/interfaces/IExecutor.sol b/packages/layerzero-v2/evm/messagelib/contracts/interfaces/IExecutor.sol index 4ce6a73..939576e 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/interfaces/IExecutor.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/interfaces/IExecutor.sol @@ -6,8 +6,9 @@ import { Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/I import { IWorker } from "./IWorker.sol"; import { ILayerZeroExecutor } from "./ILayerZeroExecutor.sol"; +import { ILayerZeroReadExecutor } from "./ILayerZeroReadExecutor.sol"; -interface IExecutor is IWorker, ILayerZeroExecutor { +interface IExecutor is IWorker, ILayerZeroExecutor, ILayerZeroReadExecutor { struct DstConfigParam { uint32 dstEid; uint64 lzReceiveBaseGas; diff --git a/packages/layerzero-v2/evm/messagelib/contracts/interfaces/IExecutorFeeLib.sol b/packages/layerzero-v2/evm/messagelib/contracts/interfaces/IExecutorFeeLib.sol index 1e8a0cd..5aee62a 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/interfaces/IExecutorFeeLib.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/interfaces/IExecutorFeeLib.sol @@ -13,12 +13,19 @@ interface IExecutorFeeLib { uint16 defaultMultiplierBps; } + struct FeeParamsForRead { + address priceFeed; + address sender; + uint16 defaultMultiplierBps; + } + error Executor_NoOptions(); error Executor_NativeAmountExceedsCap(uint256 amount, uint256 cap); error Executor_UnsupportedOptionType(uint8 optionType); error Executor_InvalidExecutorOptions(uint256 cursor); error Executor_ZeroLzReceiveGasProvided(); error Executor_ZeroLzComposeGasProvided(); + error Executor_ZeroCalldataSizeProvided(); error Executor_EidNotSupported(uint32 eid); function getFeeOnSend( @@ -32,4 +39,18 @@ interface IExecutorFeeLib { IExecutor.DstConfig calldata _dstConfig, bytes calldata _options ) external view returns (uint256 fee); + + function getFeeOnSend( + FeeParamsForRead calldata _params, + IExecutor.DstConfig calldata _dstConfig, + bytes calldata _options + ) external returns (uint256 fee); + + function getFee( + FeeParamsForRead calldata _params, + IExecutor.DstConfig calldata _dstConfig, + bytes calldata _options + ) external view returns (uint256 fee); + + function version() external view returns (uint64 major, uint8 minor); } diff --git a/packages/layerzero-v2/evm/messagelib/contracts/interfaces/ILayerZeroPriceFeed.sol b/packages/layerzero-v2/evm/messagelib/contracts/interfaces/ILayerZeroPriceFeed.sol index 6aea0de..f486ff4 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/interfaces/ILayerZeroPriceFeed.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/interfaces/ILayerZeroPriceFeed.sol @@ -37,7 +37,7 @@ interface ILayerZeroPriceFeed { error LZ_PriceFeed_OnlyPriceUpdater(); error LZ_PriceFeed_InsufficientFee(uint256 provided, uint256 required); - error LZ_PriceFeed_UnknownL2Eid(uint32 l2Eid); + error LZ_PriceFeed_NotAnOPStack(uint32 l2Eid); function nativeTokenPriceUSD() external view returns (uint128); diff --git a/packages/layerzero-v2/evm/messagelib/contracts/interfaces/ILayerZeroReadExecutor.sol b/packages/layerzero-v2/evm/messagelib/contracts/interfaces/ILayerZeroReadExecutor.sol new file mode 100644 index 0000000..d25532b --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/contracts/interfaces/ILayerZeroReadExecutor.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +interface ILayerZeroReadExecutor { + // @notice query price and assign jobs at the same time + // @param _sender - the source sending contract address. executors may apply price discrimination to senders + // @param _options - optional parameters for extra service plugins, e.g. sending dust tokens at the destination chain + function assignJob(address _sender, bytes calldata _options) external returns (uint256 fee); + + // @notice query the executor price for executing the payload on this chain + // @param _sender - the source sending contract address. executors may apply price discrimination to senders + // @param _options - optional parameters for extra service plugins, e.g. sending dust tokens + function getFee(address _sender, bytes calldata _options) external view returns (uint256 fee); +} diff --git a/packages/layerzero-v2/evm/messagelib/contracts/libs/ExecutorOptions.sol b/packages/layerzero-v2/evm/messagelib/contracts/libs/ExecutorOptions.sol new file mode 100644 index 0000000..433ef90 --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/contracts/libs/ExecutorOptions.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/CalldataBytesLib.sol"; + +library ExecutorOptions { + using CalldataBytesLib for bytes; + + uint8 internal constant WORKER_ID = 1; + + uint8 internal constant OPTION_TYPE_LZRECEIVE = 1; + uint8 internal constant OPTION_TYPE_NATIVE_DROP = 2; + uint8 internal constant OPTION_TYPE_LZCOMPOSE = 3; + uint8 internal constant OPTION_TYPE_ORDERED_EXECUTION = 4; + uint8 internal constant OPTION_TYPE_LZREAD = 5; + + error Executor_InvalidLzReceiveOption(); + error Executor_InvalidNativeDropOption(); + error Executor_InvalidLzComposeOption(); + error Executor_InvalidLzReadOption(); + + /// @dev decode the next executor option from the options starting from the specified cursor + /// @param _options [executor_id][executor_option][executor_id][executor_option]... + /// executor_option = [option_size][option_type][option] + /// option_size = len(option_type) + len(option) + /// executor_id: uint8, option_size: uint16, option_type: uint8, option: bytes + /// @param _cursor the cursor to start decoding from + /// @return optionType the type of the option + /// @return option the option of the executor + /// @return cursor the cursor to start decoding the next executor option + function nextExecutorOption( + bytes calldata _options, + uint256 _cursor + ) internal pure returns (uint8 optionType, bytes calldata option, uint256 cursor) { + unchecked { + // skip worker id + cursor = _cursor + 1; + + // read option size + uint16 size = _options.toU16(cursor); + cursor += 2; + + // read option type + optionType = _options.toU8(cursor); + + // startCursor and endCursor are used to slice the option from _options + uint256 startCursor = cursor + 1; // skip option type + uint256 endCursor = cursor + size; + option = _options[startCursor:endCursor]; + cursor += size; + } + } + + function decodeLzReceiveOption(bytes calldata _option) internal pure returns (uint128 gas, uint128 value) { + if (_option.length != 16 && _option.length != 32) revert Executor_InvalidLzReceiveOption(); + gas = _option.toU128(0); + value = _option.length == 32 ? _option.toU128(16) : 0; + } + + function decodeNativeDropOption(bytes calldata _option) internal pure returns (uint128 amount, bytes32 receiver) { + if (_option.length != 48) revert Executor_InvalidNativeDropOption(); + amount = _option.toU128(0); + receiver = _option.toB32(16); + } + + function decodeLzComposeOption( + bytes calldata _option + ) internal pure returns (uint16 index, uint128 gas, uint128 value) { + if (_option.length != 18 && _option.length != 34) revert Executor_InvalidLzComposeOption(); + index = _option.toU16(0); + gas = _option.toU128(2); + value = _option.length == 34 ? _option.toU128(18) : 0; + } + + function decodeLzReadOption( + bytes calldata _option + ) internal pure returns (uint128 gas, uint32 calldataSize, uint128 value) { + if (_option.length != 20 && _option.length != 36) revert Executor_InvalidLzReadOption(); + gas = _option.toU128(0); + calldataSize = _option.toU32(16); + value = _option.length == 36 ? _option.toU128(20) : 0; + } + + function encodeLzReceiveOption(uint128 _gas, uint128 _value) internal pure returns (bytes memory) { + return _value == 0 ? abi.encodePacked(_gas) : abi.encodePacked(_gas, _value); + } + + function encodeNativeDropOption(uint128 _amount, bytes32 _receiver) internal pure returns (bytes memory) { + return abi.encodePacked(_amount, _receiver); + } + + function encodeLzComposeOption(uint16 _index, uint128 _gas, uint128 _value) internal pure returns (bytes memory) { + return _value == 0 ? abi.encodePacked(_index, _gas) : abi.encodePacked(_index, _gas, _value); + } + + function encodeLzReadOption( + uint128 _gas, + uint32 _calldataSize, + uint128 _value + ) internal pure returns (bytes memory) { + return _value == 0 ? abi.encodePacked(_gas, _calldataSize) : abi.encodePacked(_gas, _calldataSize, _value); + } +} diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVN.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVN.sol index 1160eaa..aef9e63 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVN.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVN.sol @@ -6,6 +6,7 @@ import { ILayerZeroUltraLightNodeV2 } from "@layerzerolabs/lz-evm-v1-0.7/contrac import { Worker } from "../../Worker.sol"; import { MultiSig } from "./MultiSig.sol"; +import { ReadLib1002 } from "../readlib/ReadLib1002.sol"; import { IDVN } from "../interfaces/IDVN.sol"; import { IDVNFeeLib } from "../interfaces/IDVNFeeLib.sol"; import { IReceiveUlnE2 } from "../interfaces/IReceiveUlnE2.sol"; @@ -22,6 +23,7 @@ contract DVN 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; + uint32 public immutable localEidV2; // endpoint-v2 only, for read call mapping(uint32 dstEid => DstConfig) public dstConfig; mapping(bytes32 executableHash => bool used) public usedHashes; @@ -44,6 +46,7 @@ contract DVN is Worker, MultiSig, IDVN { /// @dev DVN doesn't have a roleAdmin (address(0x0)) /// @dev Supports all of ULNv2, ULN301, ULN302 and more + /// @param _localEidV2 local endpoint-v2 eid /// @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 @@ -51,6 +54,7 @@ contract DVN is Worker, MultiSig, IDVN { /// @param _quorum quorum for multisig /// @param _admins array of admin addresses that are granted the ADMIN_ROLE constructor( + uint32 _localEidV2, uint32 _vid, address[] memory _messageLibs, address _priceFeed, @@ -59,6 +63,7 @@ contract DVN is Worker, MultiSig, IDVN { address[] memory _admins ) Worker(_messageLibs, _priceFeed, 12000, address(0x0), _admins) MultiSig(_signers, _quorum) { vid = _vid; + localEidV2 = _localEidV2; } // ========================= Modifier ========================= @@ -272,6 +277,25 @@ contract DVN is Worker, MultiSig, IDVN { emit VerifierFeePaid(totalFee); } + /// @dev to support CmdLib + // @param _packetHeader - version + nonce + path + // @param _cmd - the command to be executed to obtain the payload + // @param _options - options + function assignJob( + address _sender, + bytes calldata /*_packetHeader*/, + bytes calldata _cmd, + bytes calldata _options + ) external payable onlyRole(MESSAGE_LIB_ROLE) onlyAcl(_sender) returns (uint256 fee) { + IDVNFeeLib.FeeParamsForRead memory feeParams = IDVNFeeLib.FeeParamsForRead( + priceFeed, + _sender, + quorum, + defaultMultiplierBps + ); + fee = IDVNFeeLib(workerFeeLib).getFeeOnSend(feeParams, dstConfig[localEidV2], _cmd, _options); + } + // ========================= View ========================= /// @dev getFee can revert if _sender doesn't pass ACL @@ -294,7 +318,7 @@ contract DVN is Worker, MultiSig, IDVN { quorum, defaultMultiplierBps ); - return IDVNFeeLib(workerFeeLib).getFee(params, dstConfig[_dstEid], _options); + fee = IDVNFeeLib(workerFeeLib).getFee(params, dstConfig[_dstEid], _options); } /// @dev to support ULNv2 @@ -317,7 +341,26 @@ contract DVN is Worker, MultiSig, IDVN { quorum, defaultMultiplierBps ); - return IDVNFeeLib(workerFeeLib).getFee(params, dstConfig[_dstEid], bytes("")); + fee = IDVNFeeLib(workerFeeLib).getFee(params, dstConfig[_dstEid], bytes("")); + } + + /// @dev to support CmdLib + // @param _packetHeader - version + nonce + path + // @param _cmd - the command to be executed to obtain the payload + // @param _options - options + function getFee( + address _sender, + bytes calldata /*_packetHeader*/, + bytes calldata _cmd, + bytes calldata _options + ) external view onlyAcl(_sender) returns (uint256 fee) { + IDVNFeeLib.FeeParamsForRead memory feeParams = IDVNFeeLib.FeeParamsForRead( + priceFeed, + _sender, + quorum, + defaultMultiplierBps + ); + fee = IDVNFeeLib(workerFeeLib).getFee(feeParams, dstConfig[localEidV2], _cmd, _options); } /// @param _target target address @@ -344,6 +387,7 @@ contract DVN is Worker, MultiSig, IDVN { // never check for these selectors to save gas return _functionSig != IReceiveUlnE2.verify.selector && // 0x0223536e, replaying won't change the state + _functionSig != ReadLib1002.verify.selector && // 0xab750e75, replaying won't change the state _functionSig != ILayerZeroUltraLightNodeV2.updateHash.selector; // 0x704316e5, replaying will be revert at uln } } diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVNFeeLib.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVNFeeLib.sol index 9001e6c..b235a72 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVNFeeLib.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/DVNFeeLib.sol @@ -9,27 +9,96 @@ import { ILayerZeroPriceFeed } from "../../interfaces/ILayerZeroPriceFeed.sol"; import { IDVN } from "../interfaces/IDVN.sol"; import { IDVNFeeLib } from "../interfaces/IDVNFeeLib.sol"; import { DVNOptions } from "../libs/DVNOptions.sol"; +import { ReadCmdCodecV1 } from "../libs/ReadCmdCodecV1.sol"; +import { SupportedCmdTypesLib, SupportedCmdTypes, BitMap256 } from "../libs/SupportedCmdTypes.sol"; contract DVNFeeLib is Ownable, IDVNFeeLib { using DVNOptions for bytes; - uint16 internal constant EXECUTE_FIXED_BYTES = 68; // encoded: funcSigHash + params -> 4 + (32 * 2) + struct SetSupportedCmdTypesParam { + uint32 targetEid; + BitMap256 types; + } + + struct BlockTimeConfig { + uint32 avgBlockTime; // milliseconds + uint64 blockNum; // the block number of the reference timestamp + uint64 timestamp; // second, the reference timestamp of the block number + uint32 maxPastRetention; // second, the max retention time the DVN will accept read requests/compute from the past time + uint32 maxFutureRetention; // second, the max retention time the DVN will accept read requests/compute from the future time + } + + uint16 internal constant BPS_BASE = 10000; + + // encoded( execute(ExecuteParam[]) ): funcSigHash + params -> 4 + 32(Offset of the array) + 32(array size) + 32(first element start offset)\ + // + 32(vid) + 32(target) + 32(calldata-offset) + 32(expiration) + 32(signatures-offset) = 260 + uint16 internal constant EXECUTE_FIXED_BYTES = 260; 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; + // verify(bytes calldata _packetHeader, bytes32 _payloadHash, uint64 _confirmations)\ + // 4 + 32(header offset) + 32(payloadHash) + 32(confirmations, 8 -> 32 padded) + 32(header-size) + 96(81 -> header-padded) = 228, + // padded to multiples of 32 = 256, encoded as bytes with an 32 byte for the bytes size = 288 + uint16 internal constant VERIFY_BYTES_ULN = 288; + // verify(bytes calldata _packetHeader, bytes32 _cmdHash, bytes32 _payloadHash)\ + // 4 + 32(header offset) + 32(cmdHash) + 32(payloadHash) + 32(header-size) + 96(81 -> header-padded) = 228, + // padded to multiples of 32 = 256, encoded as bytes with an 32 byte for the bytes size = 288 + uint16 internal constant VERIFY_BYTES_CMD_LIB = 288; - uint256 private immutable nativeDecimalsRate; + uint256 internal immutable nativeDecimalsRate; + uint32 internal immutable localEidV2; // endpoint-v2 only, for read call - constructor(uint256 _nativeDecimalsRate) { + SupportedCmdTypes internal supportedCmdTypes; + + uint120 internal evmCallRequestV1FeeUSD; + uint120 internal evmCallComputeV1ReduceFeeUSD; + uint16 internal evmCallComputeV1MapBps; + + mapping(uint32 dstEid => BlockTimeConfig) public dstBlockTimeConfigs; + + constructor(uint32 _localEidV2, uint256 _nativeDecimalsRate) { + localEidV2 = _localEidV2; nativeDecimalsRate = _nativeDecimalsRate; } // ================================ OnlyOwner ================================ + function setSupportedCmdTypes(SetSupportedCmdTypesParam[] calldata _params) external onlyOwner { + for (uint256 i = 0; i < _params.length; i++) { + supportedCmdTypes.cmdTypes[_params[i].targetEid] = _params[i].types; + } + } + + function getSupportedCmdTypes(uint32 _targetEid) external view returns (BitMap256) { + return supportedCmdTypes.cmdTypes[_targetEid]; + } + + function setDstBlockTimeConfigs( + uint32[] calldata dstEids, + BlockTimeConfig[] calldata _blockConfigs + ) external onlyOwner { + if (dstEids.length != _blockConfigs.length) revert DVN_INVALID_INPUT_LENGTH(); + for (uint256 i = 0; i < dstEids.length; i++) { + dstBlockTimeConfigs[dstEids[i]] = _blockConfigs[i]; + } + } + function withdrawToken(address _token, address _to, uint256 _amount) external onlyOwner { // transfers native if _token is address(0x0) Transfer.nativeOrToken(_token, _to, _amount); } + function setCmdFees( + uint120 _evmCallRequestV1FeeUSD, + uint120 _evmCallComputeV1ReduceFeeUSD, + uint16 _evmCallComputeV1MapBps + ) external onlyOwner { + evmCallRequestV1FeeUSD = _evmCallRequestV1FeeUSD; + evmCallComputeV1ReduceFeeUSD = _evmCallComputeV1ReduceFeeUSD; + evmCallComputeV1MapBps = _evmCallComputeV1MapBps; + } + + function getCmdFees() external view returns (uint120, uint120, uint16) { + return (evmCallRequestV1FeeUSD, evmCallComputeV1ReduceFeeUSD, evmCallComputeV1MapBps); + } + // ========================= External ========================= /// @dev get fee function that can change state. e.g. paying priceFeed /// @param _params fee params @@ -40,24 +109,38 @@ contract DVNFeeLib is Ownable, IDVNFeeLib { IDVN.DstConfig calldata _dstConfig, bytes calldata _options ) external payable returns (uint256) { - if (_dstConfig.gas == 0) revert DVN_EidNotSupported(_params.dstEid); + return getFee(_params, _dstConfig, _options); + } - _decodeDVNOptions(_options); // todo: validate options + function getFeeOnSend( + FeeParamsForRead calldata _params, + IDVN.DstConfig calldata _dstConfig, + bytes calldata _cmd, + bytes calldata _options + ) external payable returns (uint256 fee) { + fee = getFee(_params, _dstConfig, _cmd, _options); + } - uint256 callDataSize = _getCallDataSize(_params.quorum); + // ========================= 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 + ) public view returns (uint256) { + if (_dstConfig.gas == 0) revert DVN_EidNotSupported(_params.dstEid); - // 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); + _decodeDVNOptions(_options); // validate options - (uint256 fee, , , uint128 nativePriceUSD) = ILayerZeroPriceFeed(_params.priceFeed).estimateFeeOnSend( + uint256 callDataSize = _getCallDataSize(_params.quorum); + (uint256 fee, , , uint128 nativePriceUSD) = ILayerZeroPriceFeed(_params.priceFeed).estimateFeeByEid( _params.dstEid, callDataSize, _dstConfig.gas ); - return _applyPremium( fee, @@ -68,29 +151,30 @@ contract DVNFeeLib is Ownable, IDVNFeeLib { ); } - // ========================= View ========================= - /// @dev get fee view function - /// @param _params fee params - /// @param _dstConfig dst config - /// @param //_options options function getFee( - FeeParams calldata _params, + FeeParamsForRead calldata _params, IDVN.DstConfig calldata _dstConfig, + bytes calldata _cmd, bytes calldata _options - ) external view returns (uint256) { - if (_dstConfig.gas == 0) revert DVN_EidNotSupported(_params.dstEid); + ) public view returns (uint256) { + if (_dstConfig.gas == 0) revert DVN_EidNotSupported(localEidV2); _decodeDVNOptions(_options); // validate options - uint256 callDataSize = _getCallDataSize(_params.quorum); + uint256 callDataSize = _getReadCallDataSize(_params.quorum); (uint256 fee, , , uint128 nativePriceUSD) = ILayerZeroPriceFeed(_params.priceFeed).estimateFeeByEid( - _params.dstEid, + localEidV2, callDataSize, _dstConfig.gas ); + + // cmdFeeUSD -> cmdFee native final + uint256 cmdFeeUSD = _estimateCmdFee(_cmd); + uint256 cmdFee = (cmdFeeUSD * nativeDecimalsRate) / nativePriceUSD; + return _applyPremium( - fee, + fee + cmdFee, _dstConfig.multiplierBps, _params.defaultMultiplierBps, _dstConfig.floorMarginUSD, @@ -100,13 +184,24 @@ contract DVNFeeLib is Ownable, IDVNFeeLib { // ========================= Internal ========================= function _getCallDataSize(uint256 _quorum) internal pure returns (uint256) { + return _getCallDataSizeByQuorumAndVerifyBytes(_quorum, VERIFY_BYTES_ULN); + } + + function _getReadCallDataSize(uint256 _quorum) internal pure returns (uint256) { + return _getCallDataSizeByQuorumAndVerifyBytes(_quorum, VERIFY_BYTES_CMD_LIB); + } + + function _getCallDataSizeByQuorumAndVerifyBytes( + uint256 _quorum, + uint256 verifyBytes + ) 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; + // totalSignatureBytesPadded also has 32 as size of the bytes + return uint256(EXECUTE_FIXED_BYTES) + verifyBytes + totalSignatureBytes + 32; } function _applyPremium( @@ -140,6 +235,54 @@ contract DVNFeeLib is Ownable, IDVNFeeLib { return 0; // todo: precrime fee model } + function _estimateCmdFee(bytes calldata _cmd) internal view returns (uint256 fee) { + ReadCmdCodecV1.Cmd memory cmd = ReadCmdCodecV1.decode(_cmd, _assertCmdTypeSupported); + fee = cmd.numEvmCallRequestV1 * evmCallRequestV1FeeUSD; + if (cmd.evmCallComputeV1Map) { + fee += (fee * evmCallComputeV1MapBps) / BPS_BASE; + } + if (cmd.evmCallComputeV1Reduce) { + fee += evmCallComputeV1ReduceFeeUSD; + } + } + + function _assertCmdTypeSupported( + uint32 _targetEid, + bool _isBlockNum, + uint64 _blockNumOrTimestamp, + uint8 _cmdType + ) internal view { + supportedCmdTypes.assertSupported(_targetEid, _cmdType); + if (supportedCmdTypes.isSupported(_targetEid, SupportedCmdTypesLib.CMD_V1__TIMESTAMP_VALIDATE)) { + BlockTimeConfig memory blockCnf = dstBlockTimeConfigs[_targetEid]; + uint64 timestamp = _blockNumOrTimestamp; + if (_isBlockNum) { + // convert the blockNum to the timestamp + if (_blockNumOrTimestamp > blockCnf.blockNum) { + timestamp = + blockCnf.timestamp + + ((_blockNumOrTimestamp - blockCnf.blockNum) * blockCnf.avgBlockTime) / + 1000; + } else { + timestamp = + blockCnf.timestamp - + ((blockCnf.blockNum - _blockNumOrTimestamp) * blockCnf.avgBlockTime) / + 1000; + } + } + if ( + timestamp + blockCnf.maxPastRetention < block.timestamp || + timestamp > block.timestamp + blockCnf.maxFutureRetention + ) { + revert DVN_TimestampOutOfRange(_targetEid, timestamp); + } + } + } + + function version() external pure returns (uint64 major, uint8 minor) { + return (1, 1); + } + // send funds here to pay for price feed directly receive() external payable {} } diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/MultiSig.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/MultiSig.sol index 375abad..6c591fd 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/MultiSig.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/MultiSig.sol @@ -3,8 +3,11 @@ pragma solidity ^0.8.20; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; abstract contract MultiSig { + using EnumerableSet for EnumerableSet.AddressSet; + enum Errors { NoError, SignatureError, @@ -12,8 +15,7 @@ abstract contract MultiSig { SignerNotInCommittee } - mapping(address signer => bool active) public signers; - uint64 public signerSize; + EnumerableSet.AddressSet internal signerSet; uint64 public quorum; error MultiSig_OnlySigner(); @@ -21,12 +23,14 @@ abstract contract MultiSig { error MultiSig_SignersSizeIsLessThanQuorum(uint64 signersSize, uint64 quorum); error MultiSig_UnorderedSigners(); error MultiSig_StateAlreadySet(address signer, bool active); + error MultiSig_StateNotSet(address signer, bool active); + error MultiSig_InvalidSigner(); event UpdateSigner(address _signer, bool _active); event UpdateQuorum(uint64 _quorum); modifier onlySigner() { - if (!signers[msg.sender]) { + if (!isSigner(msg.sender)) { revert MultiSig_OnlySigner(); } _; @@ -36,33 +40,41 @@ abstract contract MultiSig { 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(); + if (signer == address(0)) { + revert MultiSig_InvalidSigner(); } - signers[signer] = true; - lastSigner = signer; + signerSet.add(signer); + } + + uint64 _signerSize = uint64(signerSet.length()); + if (_signerSize < _quorum) { + revert MultiSig_SignersSizeIsLessThanQuorum(_signerSize, _quorum); } - signerSize = uint64(_signers.length); + quorum = _quorum; } function _setSigner(address _signer, bool _active) internal { - if (signers[_signer] == _active) { - revert MultiSig_StateAlreadySet(_signer, _active); + if (_active) { + if (_signer == address(0)) { + revert MultiSig_InvalidSigner(); + } + if (!signerSet.add(_signer)) { + revert MultiSig_StateAlreadySet(_signer, _active); + } + } else { + if (!signerSet.remove(_signer)) { + revert MultiSig_StateNotSet(_signer, _active); + } } - signers[_signer] = _active; - uint64 _signerSize = _active ? signerSize + 1 : signerSize - 1; + + uint64 _signerSize = uint64(signerSet.length()); uint64 _quorum = quorum; if (_signerSize < _quorum) { revert MultiSig_SignersSizeIsLessThanQuorum(_signerSize, _quorum); } - signerSize = _signerSize; emit UpdateSigner(_signer, _active); } @@ -70,7 +82,7 @@ abstract contract MultiSig { if (_quorum == 0) { revert MultiSig_QuorumIsZero(); } - uint64 _signerSize = signerSize; + uint64 _signerSize = uint64(signerSet.length()); if (_signerSize < _quorum) { revert MultiSig_SignersSizeIsLessThanQuorum(_signerSize, _quorum); } @@ -87,12 +99,13 @@ abstract contract MultiSig { address lastSigner = address(0); // There cannot be a signer with address 0. for (uint256 i = 0; i < quorum; i++) { + // the quorum is guaranteed not to be zero in the constructor and setter bytes calldata signature = _signatures[i * 65:(i + 1) * 65]; (address currentSigner, ECDSA.RecoverError error) = 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 + if (currentSigner <= lastSigner) return (false, Errors.DuplicatedSigner); // prevent duplicate signatures, the signers must be ordered to sign the digest + if (!isSigner(currentSigner)) return (false, Errors.SignerNotInCommittee); // signature is not in committee lastSigner = currentSigner; } return (true, Errors.NoError); @@ -101,4 +114,22 @@ abstract contract MultiSig { function _getEthSignedMessageHash(bytes32 _messageHash) internal pure returns (bytes32) { return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)); } + + // ============================================== View ============================================== + function getSigners() public view returns (address[] memory) { + return signerSet.values(); + } + + // compatibility with the previous version + function signers(address _signer) public view returns (bool) { + return isSigner(_signer); + } + + function isSigner(address _signer) public view returns (bool) { + return signerSet.contains(_signer); + } + + function signerSize() public view returns (uint256) { + return signerSet.length(); + } } diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/SimpleReadDVN.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/SimpleReadDVN.sol new file mode 100644 index 0000000..c130311 --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/SimpleReadDVN.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import { BitMap256 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/BitMaps.sol"; + +import { ILayerZeroReadDVN } from "../interfaces/ILayerZeroReadDVN.sol"; +import { ReadCmdCodecV1 } from "../libs/ReadCmdCodecV1.sol"; +import { ReadLib1002 } from "../readlib/ReadLib1002.sol"; +import { SupportedCmdTypes } from "../libs/SupportedCmdTypes.sol"; + +contract SimpleReadDVN is ILayerZeroReadDVN { + struct SetSupportedCmdTypesParam { + uint32 targetEid; + BitMap256 types; + } + + uint128 internal constant DENOMINATOR = 10 ** 18; + uint128 internal constant NATIVE_DECIMALS = 10 ** 18; + + uint16 internal constant BPS_BASE = 10000; + + address payable public immutable readLib; + + // the usd fee should be usd * DENOMINATOR + uint128 internal evmCallRequestV1FeeUSD; + uint128 internal evmCallComputeV1MapFeeUSD; + uint128 internal evmCallComputeV1ReduceFeeUSD; + + uint128 internal nativePriceUSD; // usd * DENOMINATOR + + SupportedCmdTypes internal supportedCmdTypes; + + constructor(address payable _readLib) { + readLib = _readLib; + } + + function setSupportedCmdTypes(SetSupportedCmdTypesParam[] calldata _params) external { + for (uint256 i = 0; i < _params.length; i++) { + supportedCmdTypes.cmdTypes[_params[i].targetEid] = _params[i].types; + } + } + + function assignJob( + address /*_sender*/, + bytes calldata /*_packetHeader*/, + bytes calldata _cmd, + bytes calldata /*_options*/ + ) external payable returns (uint256) { + uint256 cmdFeeUSD = _estimateCmdFee(_cmd); + uint256 cmdFee = (cmdFeeUSD * NATIVE_DECIMALS) / nativePriceUSD; + + return cmdFee; + } + + function verify(bytes calldata _packetHeader, bytes32 _cmdHash, bytes32 _payloadHash) external { + ReadLib1002(readLib).verify(_packetHeader, _cmdHash, _payloadHash); + } + + // ========================= View ========================= + + function getFee( + address /*_sender*/, + bytes calldata /*_packetHeader*/, + bytes calldata _cmd, + bytes calldata /*_options*/ + ) external view returns (uint256) { + // cmdFeeUSD -> cmdFee native + uint256 cmdFeeUSD = _estimateCmdFee(_cmd); + uint256 cmdFee = (cmdFeeUSD * NATIVE_DECIMALS) / nativePriceUSD; + + return cmdFee; + } + + function setCmdFees( + uint128 _evmCallReqV1FeeUSD, + uint128 _evmCallComputeV1MapFeeUSD, + uint128 _evmCallComputeV1ReduceFeeUSD, + uint128 _nativePriceUSD + ) external { + evmCallRequestV1FeeUSD = _evmCallReqV1FeeUSD; + evmCallComputeV1MapFeeUSD = _evmCallComputeV1MapFeeUSD; + evmCallComputeV1ReduceFeeUSD = _evmCallComputeV1ReduceFeeUSD; + nativePriceUSD = _nativePriceUSD; + } + + function getCmdFees() external view returns (uint128, uint128, uint128, uint128) { + return (evmCallRequestV1FeeUSD, evmCallComputeV1MapFeeUSD, evmCallComputeV1ReduceFeeUSD, nativePriceUSD); + } + + function _estimateCmdFee(bytes calldata _cmd) internal view returns (uint256 fee) { + ReadCmdCodecV1.Cmd memory cmd = ReadCmdCodecV1.decode(_cmd, _assertCmdTypeSupported); + fee = cmd.numEvmCallRequestV1 * evmCallRequestV1FeeUSD; + if (cmd.evmCallComputeV1Map) { + fee += evmCallComputeV1MapFeeUSD * cmd.numEvmCallRequestV1; + } + if (cmd.evmCallComputeV1Reduce) { + fee += evmCallComputeV1ReduceFeeUSD; + } + } + + function _assertCmdTypeSupported( + uint32 _targetEid, + bool /*_isBlockNum*/, + uint64 /*_blockNumOrTimestamp*/, + uint8 _cmdType + ) internal view { + supportedCmdTypes.assertSupported(_targetEid, _cmdType); + } + + receive() external payable virtual {} +} diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/CCIP/CCIPDVNAdapter.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/CCIP/CCIPDVNAdapter.sol index 3ad1337..5a5ec8b 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/CCIP/CCIPDVNAdapter.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/CCIP/CCIPDVNAdapter.sol @@ -22,10 +22,10 @@ import { ICCIPDVNAdapterFeeLib } from "../../../interfaces/adapters/ICCIPDVNAdap contract CCIPDVNAdapter is DVNAdapterBase, IAny2EVMMessageReceiver, ICCIPDVNAdapter { address private constant NATIVE_GAS_TOKEN_ADDRESS = address(0); - IRouterClient public router; + IRouterClient public immutable router; - mapping(uint32 dstEid => DstConfig config) public dstConfig; - mapping(uint64 chainSelector => bytes peer) public peers; + mapping(uint32 dstEid => DstConfig) public dstConfig; + mapping(uint64 srcChainSelector => SrcConfig) public srcConfig; constructor(address[] memory _admins, address _router) DVNAdapterBase(msg.sender, _admins, 12000) { router = IRouterClient(_router); @@ -38,25 +38,25 @@ contract CCIPDVNAdapter is DVNAdapterBase, IAny2EVMMessageReceiver, ICCIPDVNAdap for (uint256 i = 0; i < _params.length; i++) { DstConfigParam calldata param = _params[i]; - delete peers[dstConfig[param.dstEid].chainSelector]; // delete old peer in case chain by dstEid is updated - peers[param.chainSelector] = param.peer; + uint32 eid = param.eid % 30000; - dstConfig[param.dstEid] = DstConfig({ - chainSelector: param.chainSelector, - multiplierBps: param.multiplierBps, - gas: param.gas, - peer: param.peer - }); + // set once per chainSelector + // only one adapter per dvn that services both endpoint v1 and v2 + // we standardize the eid stored here with mod 30000 + if (dstConfig[eid].chainSelector == 0) { + dstConfig[eid].chainSelector = param.chainSelector; + dstConfig[eid].peer = param.peer; + srcConfig[param.chainSelector].eid = eid; + srcConfig[param.chainSelector].peer = param.peer; + } + + dstConfig[eid].multiplierBps = param.multiplierBps; + dstConfig[eid].gas = param.gas; } emit DstConfigSet(_params); } - function setRouter(address _router) external onlyRole(ADMIN_ROLE) { - router = IRouterClient(_router); - emit RouterSet(_router); - } - // ========================= OnlyMessageLib ========================= function assignJob( AssignJobParam calldata _param, @@ -71,7 +71,7 @@ contract CCIPDVNAdapter is DVNAdapterBase, IAny2EVMMessageReceiver, ICCIPDVNAdap defaultMultiplierBps ); - DstConfig memory config = dstConfig[_param.dstEid]; + DstConfig memory config = dstConfig[_param.dstEid % 30000]; bytes memory data = _encode(receiveLib, _param.packetHeader, _param.payloadHash); Client.EVM2AnyMessage memory message = _createCCIPMessage(data, config.peer, config.gas); @@ -95,9 +95,11 @@ contract CCIPDVNAdapter is DVNAdapterBase, IAny2EVMMessageReceiver, ICCIPDVNAdap function ccipReceive(Client.Any2EVMMessage calldata _message) external { if (msg.sender != address(router)) revert CCIPDVNAdapter_InvalidRouter(msg.sender); - _assertPeer(_message.sourceChainSelector, _message.sender); + SrcConfig memory config = srcConfig[_message.sourceChainSelector]; + + _assertPeer(_message.sourceChainSelector, _message.sender, config.peer); - _decodeAndVerify(_message.data); + _decodeAndVerify(config.eid, _message.data); } // ========================= View ========================= @@ -114,7 +116,7 @@ contract CCIPDVNAdapter is DVNAdapterBase, IAny2EVMMessageReceiver, ICCIPDVNAdap defaultMultiplierBps ); - DstConfig memory config = dstConfig[_dstEid]; + DstConfig memory config = dstConfig[_dstEid % 30000]; bytes memory data = _encodeEmpty(); Client.EVM2AnyMessage memory message = _createCCIPMessage(data, config.peer, config.gas); @@ -141,9 +143,8 @@ contract CCIPDVNAdapter is DVNAdapterBase, IAny2EVMMessageReceiver, ICCIPDVNAdap }); } - function _assertPeer(uint64 _sourceChainSelector, bytes memory _sourceAddress) private view { - bytes memory sourcePeer = peers[_sourceChainSelector]; - if (keccak256(_sourceAddress) != keccak256(sourcePeer)) { + function _assertPeer(uint64 _sourceChainSelector, bytes memory _sourceAddress, bytes memory peer) private pure { + if (keccak256(_sourceAddress) != keccak256(peer)) { revert CCIPDVNAdapter_UntrustedPeer(_sourceChainSelector, _sourceAddress); } } diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/DVNAdapterBase.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/DVNAdapterBase.sol index d41300d..1aca44a 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/DVNAdapterBase.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/DVNAdapterBase.sol @@ -80,7 +80,9 @@ abstract contract DVNAdapterBase is Worker, ILayerZeroDVN { DVNAdapterMessageCodec.encode(bytes32(0), new bytes(DVNAdapterMessageCodec.PACKET_HEADER_SIZE), bytes32(0)); } - function _decodeAndVerify(bytes calldata _payload) internal { + function _decodeAndVerify(uint32 _srcEid, bytes calldata _payload) internal { + require((DVNAdapterMessageCodec.srcEid(_payload) % 30000) == _srcEid, "DVNAdapterBase: invalid srcEid"); + (address receiveLib, bytes memory packetHeader, bytes32 payloadHash) = DVNAdapterMessageCodec.decode(_payload); IReceiveUln(receiveLib).verify(packetHeader, payloadHash, MAX_CONFIRMATIONS); diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/axelar/AxelarDVNAdapter.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/axelar/AxelarDVNAdapter.sol index 2514ef7..d255e6c 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/axelar/AxelarDVNAdapter.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/axelar/AxelarDVNAdapter.sol @@ -30,24 +30,34 @@ interface ISendLibBase { /// refer to https://docs.axelar.dev/dev/general-message-passing/recovery#manually-execute-a-transfer /// @dev As the Gas is estimated off-chain, we need to update the gas fee periodically on-chain by calling `setNativeGasFee` with the new fee. contract AxelarDVNAdapter is DVNAdapterBase, AxelarExecutable, IAxelarDVNAdapter { - mapping(string axelarChain => string peer) public peers; // by chain name + mapping(string srcChainName => SrcConfig) public srcConfig; // by chain name mapping(uint32 dstEid => DstConfig) public dstConfig; // by dstEid // set default multiplier to 2.5x constructor( address[] memory _admins, address _gateway - ) AxelarExecutable(_gateway) DVNAdapterBase(msg.sender, _admins, 12000) {} + ) AxelarExecutable(_gateway) DVNAdapterBase(msg.sender, _admins, 10000) {} // ========================= OnlyAdmin ========================= function setDstConfig(DstConfigParam[] calldata _params) external onlyRole(ADMIN_ROLE) { for (uint256 i = 0; i < _params.length; i++) { DstConfigParam calldata param = _params[i]; - delete peers[dstConfig[param.dstEid].chainName]; // delete old peer in case chain name by dstEid is updated - peers[param.chainName] = param.peer; // update peer + uint32 eid = param.eid % 30000; - dstConfig[param.dstEid] = DstConfig(param.chainName, param.peer, param.multiplierBps, param.nativeGasFee); // update config by dstEid + // set once per chainName + // only one adapter per dvn that services both endpoint v1 and v2 + // we standardize the eid stored here with mod 30000 + if (bytes(dstConfig[eid].chainName).length == 0) { + dstConfig[eid].chainName = param.chainName; + dstConfig[eid].peer = param.peer; + srcConfig[param.chainName].eid = eid; + srcConfig[param.chainName].peer = param.peer; + } + + dstConfig[eid].multiplierBps = param.multiplierBps; + dstConfig[eid].nativeGasFee = param.nativeGasFee; } emit DstConfigSet(_params); @@ -63,7 +73,7 @@ contract AxelarDVNAdapter is DVNAdapterBase, AxelarExecutable, IAxelarDVNAdapter function setNativeGasFee(NativeGasFeeParam[] calldata _params) external onlyRole(ADMIN_ROLE) { for (uint256 i = 0; i < _params.length; i++) { NativeGasFeeParam calldata param = _params[i]; - dstConfig[param.dstEid].nativeGasFee = param.nativeGasFee; + dstConfig[param.dstEid % 30000].nativeGasFee = param.nativeGasFee; } emit NativeGasFeeSet(_params); } @@ -88,7 +98,7 @@ contract AxelarDVNAdapter is DVNAdapterBase, AxelarExecutable, IAxelarDVNAdapter sender: _param.sender, defaultMultiplierBps: defaultMultiplierBps }); - DstConfig memory config = dstConfig[_param.dstEid]; + DstConfig memory config = dstConfig[_param.dstEid % 30000]; bytes memory payload = _encode(receiveLib, _param.packetHeader, _param.payloadHash); @@ -117,7 +127,7 @@ contract AxelarDVNAdapter is DVNAdapterBase, AxelarExecutable, IAxelarDVNAdapter defaultMultiplierBps ); - totalFee = IAxelarDVNAdapterFeeLib(workerFeeLib).getFee(feeLibParam, dstConfig[_dstEid], _options); + totalFee = IAxelarDVNAdapterFeeLib(workerFeeLib).getFee(feeLibParam, dstConfig[_dstEid % 30000], _options); } // ========================= Internal ========================= @@ -126,15 +136,16 @@ contract AxelarDVNAdapter is DVNAdapterBase, AxelarExecutable, IAxelarDVNAdapter string calldata _sourceAddress, bytes calldata _payload ) internal override { + SrcConfig memory config = srcConfig[_sourceChain]; + // assert peer is the same as the source chain - _assertPeer(_sourceChain, _sourceAddress); + _assertPeer(_sourceChain, _sourceAddress, config.peer); - _decodeAndVerify(_payload); + _decodeAndVerify(config.eid, _payload); } - function _assertPeer(string memory _sourceChain, string memory _sourceAddress) private view { - string memory sourcePeer = peers[_sourceChain]; - if (keccak256(bytes(_sourceAddress)) != keccak256(bytes(sourcePeer))) { + function _assertPeer(string memory _sourceChain, string memory _sourceAddress, string memory peer) private pure { + if (keccak256(bytes(_sourceAddress)) != keccak256(bytes(peer))) { revert AxelarDVNAdapter_UntrustedPeer(_sourceChain, _sourceAddress); } } diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/axelar/AxelarDVNAdapterFeeLib.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/axelar/AxelarDVNAdapterFeeLib.sol index ea6d1b7..be5df3e 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/axelar/AxelarDVNAdapterFeeLib.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/axelar/AxelarDVNAdapterFeeLib.sol @@ -75,22 +75,22 @@ contract AxelarDVNAdapterFeeLib is OwnableUpgradeable, Proxied, IAxelarDVNAdapte if (_dstConfig.nativeGasFee == 0) revert AxelarDVNAdapter_EidNotSupported(_param.dstEid); if (_options.length > 0) revert AxelarDVNAdapter_OptionsUnsupported(); - uint256 axelarFee = _getAxelarFee(_dstConfig.nativeGasFee); - totalFee = _applyPremium(_dstConfig.multiplierBps, _param.defaultMultiplierBps, axelarFee); + totalFee = _applyPremium(_dstConfig.multiplierBps, _param.defaultMultiplierBps, _dstConfig.nativeGasFee); + uint256 feeToAxelar = _getAxelarFeeWithBuffer(_dstConfig.nativeGasFee); // withdraw from uln to fee lib if not enough balance uint256 balance = address(this).balance; - if (balance < axelarFee) { + if (balance < feeToAxelar) { dvn.withdrawToFeeLib(_sendLib); // revert if still not enough balance = address(this).balance; - if (balance < axelarFee) revert AxelarDVNAdapter_InsufficientBalance(balance, axelarFee); + if (balance < feeToAxelar) revert AxelarDVNAdapter_InsufficientBalance(balance, feeToAxelar); } // pay axelar gas service - gasService.payNativeGasForContractCall{ value: axelarFee }( - address(this), // sender + gasService.payNativeGasForContractCall{ value: feeToAxelar }( + msg.sender, // sender _dstConfig.chainName, // destinationChain _dstConfig.peer, // destinationAddress _payload, // payload @@ -102,16 +102,15 @@ contract AxelarDVNAdapterFeeLib is OwnableUpgradeable, Proxied, IAxelarDVNAdapte Param calldata _param, IAxelarDVNAdapter.DstConfig calldata _dstConfig, bytes calldata _options - ) external view returns (uint256 totalFee) { + ) external pure returns (uint256 totalFee) { if (_dstConfig.nativeGasFee == 0) revert AxelarDVNAdapter_EidNotSupported(_param.dstEid); if (_options.length > 0) revert AxelarDVNAdapter_OptionsUnsupported(); - uint256 axelarFee = _getAxelarFee(_dstConfig.nativeGasFee); - totalFee = _applyPremium(_dstConfig.multiplierBps, _param.defaultMultiplierBps, axelarFee); + totalFee = _applyPremium(_dstConfig.multiplierBps, _param.defaultMultiplierBps, _dstConfig.nativeGasFee); } // ================================ Internal ================================ - function _getAxelarFee(uint256 _nativeGasFee) internal view returns (uint256) { + function _getAxelarFeeWithBuffer(uint256 _nativeGasFee) internal view returns (uint256) { return (_nativeGasFee * nativeGasFeeMultiplierBps) / BPS_DENOMINATOR; } diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/libs/DVNAdapterMessageCodec.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/libs/DVNAdapterMessageCodec.sol index 1b6bdb1..42b3f72 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/libs/DVNAdapterMessageCodec.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/dvn/adapters/libs/DVNAdapterMessageCodec.sol @@ -12,6 +12,7 @@ library DVNAdapterMessageCodec { uint256 private constant RECEIVE_LIB_OFFSET = 0; uint256 private constant PAYLOAD_HASH_OFFSET = 32; uint256 private constant PACKET_HEADER_OFFSET = 64; + uint256 private constant SRC_EID_OFFSET = 73; // 64 + 1 + 8 uint256 internal constant PACKET_HEADER_SIZE = 81; // version(uint8) + nonce(uint64) + path(uint32,bytes32,uint32,bytes32) uint256 internal constant MESSAGE_SIZE = 32 + 32 + PACKET_HEADER_SIZE; // receive_lib(bytes32) + payloadHash(bytes32) + packetHeader @@ -33,4 +34,8 @@ library DVNAdapterMessageCodec { payloadHash = bytes32(_message[PAYLOAD_HASH_OFFSET:PACKET_HEADER_OFFSET]); packetHeader = _message[PACKET_HEADER_OFFSET:]; } + + function srcEid(bytes calldata _message) internal pure returns (uint32) { + return uint32(bytes4(_message[SRC_EID_OFFSET:SRC_EID_OFFSET + 4])); + } } diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/IDVN.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/IDVN.sol index 2927a1d..1d4be0e 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/IDVN.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/IDVN.sol @@ -4,8 +4,9 @@ pragma solidity >=0.8.0; import { IWorker } from "../../interfaces/IWorker.sol"; import { ILayerZeroDVN } from "./ILayerZeroDVN.sol"; +import { ILayerZeroReadDVN } from "./ILayerZeroReadDVN.sol"; -interface IDVN is IWorker, ILayerZeroDVN { +interface IDVN is IWorker, ILayerZeroDVN, ILayerZeroReadDVN { struct DstConfigParam { uint32 dstEid; uint64 gas; diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/IDVNFeeLib.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/IDVNFeeLib.sol index b3eeb26..c79b810 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/IDVNFeeLib.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/IDVNFeeLib.sol @@ -14,8 +14,17 @@ interface IDVNFeeLib { uint16 defaultMultiplierBps; } + struct FeeParamsForRead { + address priceFeed; + address sender; + uint64 quorum; + uint16 defaultMultiplierBps; + } + error DVN_UnsupportedOptionType(uint8 optionType); error DVN_EidNotSupported(uint32 eid); + error DVN_TimestampOutOfRange(uint32 eid, uint64 timestamp); + error DVN_INVALID_INPUT_LENGTH(); function getFeeOnSend( FeeParams calldata _params, @@ -28,4 +37,20 @@ interface IDVNFeeLib { IDVN.DstConfig calldata _dstConfig, bytes calldata _options ) external view returns (uint256 fee); + + function getFeeOnSend( + FeeParamsForRead calldata _params, + IDVN.DstConfig calldata _dstConfig, + bytes calldata _cmd, + bytes calldata _options + ) external payable returns (uint256 fee); + + function getFee( + FeeParamsForRead calldata _params, + IDVN.DstConfig calldata _dstConfig, + bytes calldata _cmd, + bytes calldata _options + ) external view returns (uint256 fee); + + function version() external view returns (uint64 major, uint8 minor); } diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/ILayerZeroReadDVN.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/ILayerZeroReadDVN.sol new file mode 100644 index 0000000..73aad83 --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/ILayerZeroReadDVN.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +interface ILayerZeroReadDVN { + // @notice query price and assign jobs at the same time + // @param _packetHeader - version + nonce + path + // @param _cmd - the command to be executed to obtain the payload + // @param _options - options + function assignJob( + address _sender, + bytes calldata _packetHeader, + bytes calldata _cmd, + bytes calldata _options + ) external payable returns (uint256 fee); + + // @notice query the dvn fee for relaying block information to the destination chain + // @param _packetHeader - version + nonce + path + // @param _cmd - the command to be executed to obtain the payload + // @param _options - options + function getFee( + address _sender, + bytes calldata _packetHeader, + bytes calldata _cmd, + bytes calldata _options + ) external view returns (uint256 fee); +} diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/adapters/IAxelarDVNAdapter.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/adapters/IAxelarDVNAdapter.sol index 59f785d..974a9a6 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/adapters/IAxelarDVNAdapter.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/adapters/IAxelarDVNAdapter.sol @@ -19,7 +19,7 @@ interface IAxelarDVNAdapter { } struct DstConfigParam { - uint32 dstEid; + uint32 eid; string chainName; string peer; uint16 multiplierBps; @@ -33,6 +33,11 @@ interface IAxelarDVNAdapter { uint256 nativeGasFee; } + struct SrcConfig { + uint32 eid; + string peer; + } + event DstConfigSet(DstConfigParam[] params); event NativeGasFeeSet(NativeGasFeeParam[] params); event MultiplierSet(MultiplierParam[] params); diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/adapters/ICCIPDVNAdapter.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/adapters/ICCIPDVNAdapter.sol index 66e152a..08f0350 100644 --- a/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/adapters/ICCIPDVNAdapter.sol +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/interfaces/adapters/ICCIPDVNAdapter.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.0; interface ICCIPDVNAdapter { struct DstConfigParam { - uint32 dstEid; + uint32 eid; uint16 multiplierBps; uint64 chainSelector; uint256 gas; @@ -22,8 +22,12 @@ interface ICCIPDVNAdapter { uint256 gas; } + struct SrcConfig { + uint32 eid; + bytes peer; + } + event DstConfigSet(DstConfigParam[] params); - event RouterSet(address router); error CCIPDVNAdapter_UntrustedPeer(uint64 chainSelector, bytes peer); error CCIPDVNAdapter_InvalidRouter(address router); diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/libs/ReadCmdCodecV1.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/libs/ReadCmdCodecV1.sol new file mode 100644 index 0000000..b4df12c --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/libs/ReadCmdCodecV1.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import { SupportedCmdTypes, SupportedCmdTypesLib } from "./SupportedCmdTypes.sol"; + +library ReadCmdCodecV1 { + uint16 internal constant CMD_VERSION = 1; + uint8 internal constant REQUEST_VERSION = 1; + uint16 internal constant RESOLVER_TYPE_SINGLE_VIEW_EVM_CALL = 1; + uint8 internal constant COMPUTE_VERSION = 1; + uint16 internal constant COMPUTE_TYPE_SINGLE_VIEW_EVM_CALL = 1; + + uint8 internal constant COMPUTE_SETTING_MAP_ONLY = 0; + uint8 internal constant COMPUTE_SETTING_REDUCE_ONLY = 1; + uint8 internal constant COMPUTE_SETTING_MAP_AND_REDUCE = 2; + + error InvalidCmd(); + error InvalidVersion(); + error InvalidType(); + + struct Cmd { + uint16 numEvmCallRequestV1; + bool evmCallComputeV1Map; + bool evmCallComputeV1Reduce; + } + + function decode( + bytes calldata _cmd, + function(uint32, bool, uint64, uint8) view _assertCmdTypeSupported + ) internal view returns (Cmd memory cmd) { + uint256 cursor = 0; + // decode the header in scope, depress stack too deep + { + uint16 cmdVersion = uint16(bytes2(_cmd[cursor:cursor + 2])); + cursor += 2; + if (cmdVersion != CMD_VERSION) revert InvalidVersion(); + + cursor += 2; // skip appCmdLabel + + uint16 requestCount = uint16(bytes2(_cmd[cursor:cursor + 2])); + cursor += 2; + + // there is only one request type in this version, so total request count should be the same as numEvmCallRequestV1 + if (requestCount == 0) revert InvalidCmd(); + cmd.numEvmCallRequestV1 = requestCount; + } + + // decode the requests + for (uint16 i = 0; i < cmd.numEvmCallRequestV1; i++) { + uint8 requestVersion = uint8(_cmd[cursor]); + cursor += 1; + if (requestVersion != REQUEST_VERSION) revert InvalidVersion(); + + // skip appRequestLabel + cursor += 2; + + uint16 resolverType = uint16(bytes2(_cmd[cursor:cursor + 2])); + cursor += 2; + + if (resolverType == RESOLVER_TYPE_SINGLE_VIEW_EVM_CALL) { + uint16 requestSize = uint16(bytes2(_cmd[cursor:cursor + 2])); + cursor += 2; + + // decode the request in scope, depress stack too deep + { + uint256 requestCursor = cursor; + uint32 targetEid = uint32(bytes4(_cmd[requestCursor:requestCursor + 4])); + requestCursor += 4; + + bool isBlockNum = uint8(_cmd[requestCursor]) == 1; + requestCursor += 1; + + uint64 blockNumOrTimestamp = uint64(bytes8(_cmd[requestCursor:requestCursor + 8])); + + _assertCmdTypeSupported( + targetEid, + isBlockNum, + blockNumOrTimestamp, + SupportedCmdTypesLib.CMD_V1__REQUEST_V1__EVM_CALL + ); + } + + if (cursor + requestSize > _cmd.length) revert InvalidCmd(); + cursor += requestSize; + } else { + revert InvalidType(); + } + } + + // decode the compute if it exists + if (cursor < _cmd.length) { + uint8 computeVersion = uint8(_cmd[cursor]); + cursor += 1; + if (computeVersion != COMPUTE_VERSION) revert InvalidVersion(); + + uint16 computeType = uint16(bytes2(_cmd[cursor:cursor + 2])); + cursor += 2; + if (computeType != COMPUTE_TYPE_SINGLE_VIEW_EVM_CALL) revert InvalidType(); + + uint8 computeSetting = uint8(_cmd[cursor]); + cursor += 1; + + if (computeSetting == COMPUTE_SETTING_MAP_ONLY) { + cmd.evmCallComputeV1Map = true; + } else if (computeSetting == COMPUTE_SETTING_REDUCE_ONLY) { + cmd.evmCallComputeV1Reduce = true; + } else if (computeSetting == COMPUTE_SETTING_MAP_AND_REDUCE) { + cmd.evmCallComputeV1Map = true; + cmd.evmCallComputeV1Reduce = true; + } else { + revert InvalidType(); + } + + uint32 targetEid = uint32(bytes4(_cmd[cursor:cursor + 4])); + cursor += 4; + + bool isBlockNum = uint8(_cmd[cursor]) == 1; + cursor += 1; + + uint64 blockNumOrTimestamp = uint64(bytes8(_cmd[cursor:cursor + 8])); + cursor += 8; + + _assertCmdTypeSupported( + targetEid, + isBlockNum, + blockNumOrTimestamp, + SupportedCmdTypesLib.CMD_V1__COMPUTE_V1__EVM_CALL + ); + + // assert the remaining length: confirmations(2), to(20) + cursor += 22; + } + if (cursor != _cmd.length) revert InvalidCmd(); + } +} diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/libs/SupportedCmdTypes.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/libs/SupportedCmdTypes.sol new file mode 100644 index 0000000..789f989 --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/libs/SupportedCmdTypes.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { BitMap256 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/BitMaps.sol"; + +struct SupportedCmdTypes { + mapping(uint32 => BitMap256) cmdTypes; // support options +} + +using SupportedCmdTypesLib for SupportedCmdTypes global; + +library SupportedCmdTypesLib { + // the max number of supported command types is 256 + uint8 internal constant CMD_V1__REQUEST_V1__EVM_CALL = 0; + uint8 internal constant CMD_V1__COMPUTE_V1__EVM_CALL = 1; + uint8 internal constant CMD_V1__TIMESTAMP_VALIDATE = 2; // validate timestamp, to check if the timestamp is out of range + // more types can be added here in the future + + error UnsupportedTargetEid(); + + function assertSupported(SupportedCmdTypes storage _self, uint32 _targetEid, uint8 _type) internal view { + if (!isSupported(_self, _targetEid, _type)) revert UnsupportedTargetEid(); + } + + function isSupported(SupportedCmdTypes storage _self, uint32 _targetEid, uint8 _type) internal view returns (bool) { + return _self.cmdTypes[_targetEid].get(_type); + } +} diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/readlib/ReadLib1002.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/readlib/ReadLib1002.sol new file mode 100644 index 0000000..e73856f --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/readlib/ReadLib1002.sol @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import { ERC165, IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +import { ILayerZeroEndpointV2, MessagingFee, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { IMessageLib, MessageLibType, SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLib.sol"; +import { ISendLib, Packet } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.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 { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol"; + +import { ILayerZeroReadExecutor } from "../../interfaces/ILayerZeroReadExecutor.sol"; +import { ILayerZeroReadDVN } from "../interfaces/ILayerZeroReadDVN.sol"; +import { ILayerZeroTreasury } from "../../interfaces/ILayerZeroTreasury.sol"; + +import { UlnOptions } from "../libs/UlnOptions.sol"; +import { DVNOptions } from "../libs/DVNOptions.sol"; +import { SafeCall } from "../../libs/SafeCall.sol"; + +import { MessageLibBase } from "../../MessageLibBase.sol"; +import { ReadLibBase, ReadLibConfig } from "./ReadLibBase.sol"; + +contract ReadLib1002 is ISendLib, ERC165, ReadLibBase, MessageLibBase { + using PacketV1Codec for bytes; + using SafeCall for address; + + uint32 internal constant CONFIG_TYPE_READ_LID_CONFIG = 1; + + uint16 internal constant TREASURY_MAX_COPY = 32; + + uint256 internal immutable treasuryGasLimit; + + mapping(address oapp => mapping(uint32 eid => mapping(uint64 nonce => bytes32 cmdHash))) public cmdHashLookup; + mapping(bytes32 headerHash => mapping(bytes32 cmdHash => mapping(address dvn => bytes32 payloadHash))) + public hashLookup; + + // accumulated fees for workers and treasury + mapping(address worker => uint256 fee) public fees; + uint256 internal treasuryNativeFeeCap; + address internal treasury; + + event PayloadVerified(address dvn, bytes header, bytes32 cmdHash, bytes32 payloadHash); + event ExecutorFeePaid(address executor, uint256 fee); + event DVNFeePaid(address[] requiredDVNs, address[] optionalDVNs, uint256[] fees); + event NativeFeeWithdrawn(address worker, address receiver, uint256 amount); + event LzTokenFeeWithdrawn(address lzToken, address receiver, uint256 amount); + event TreasurySet(address treasury); + event TreasuryNativeFeeCapSet(uint256 newTreasuryNativeFeeCap); + + error LZ_RL_InvalidReceiver(); + error LZ_RL_InvalidPacketHeader(); + error LZ_RL_InvalidCmdHash(); + error LZ_RL_InvalidPacketVersion(); + error LZ_RL_InvalidEid(); + error LZ_RL_Verifying(); + error LZ_RL_InvalidConfigType(uint32 configType); + error LZ_RL_InvalidAmount(uint256 requested, uint256 available); + error LZ_RL_NotTreasury(); + error LZ_RL_CannotWithdrawAltToken(); + + constructor( + address _endpoint, + uint256 _treasuryGasLimit, + uint256 _treasuryGasForFeeCap + ) MessageLibBase(_endpoint, ILayerZeroEndpointV2(_endpoint).eid()) { + treasuryGasLimit = _treasuryGasLimit; + treasuryNativeFeeCap = _treasuryGasForFeeCap; + } + + function supportsInterface(bytes4 _interfaceId) public view override(ERC165, IERC165) returns (bool) { + return + _interfaceId == type(IMessageLib).interfaceId || + _interfaceId == type(ISendLib).interfaceId || + super.supportsInterface(_interfaceId); + } + + // ============================ OnlyOwner =================================== + + function setTreasury(address _treasury) external onlyOwner { + treasury = _treasury; + emit TreasurySet(_treasury); + } + + /// @dev the new value can not be greater than the old value, i.e. down only + function setTreasuryNativeFeeCap(uint256 _newTreasuryNativeFeeCap) external onlyOwner { + // assert the new value is no greater than the old value + if (_newTreasuryNativeFeeCap > treasuryNativeFeeCap) + revert LZ_RL_InvalidAmount(_newTreasuryNativeFeeCap, treasuryNativeFeeCap); + treasuryNativeFeeCap = _newTreasuryNativeFeeCap; + emit TreasuryNativeFeeCapSet(_newTreasuryNativeFeeCap); + } + + // ============================ OnlyEndpoint =================================== + + function send( + Packet calldata _packet, + bytes calldata _options, + bool _payInLzToken + ) external onlyEndpoint returns (MessagingFee memory, bytes memory) { + // the receiver must be the same as the sender + if (AddressCast.toBytes32(_packet.sender) != _packet.receiver) revert LZ_RL_InvalidReceiver(); + + // pay worker and treasury + (bytes memory encodedPacket, uint256 totalNativeFee) = _payWorkers(_packet, _options); + (uint256 treasuryNativeFee, uint256 lzTokenFee) = _payTreasury( + _packet.sender, + _packet.dstEid, + totalNativeFee, + _payInLzToken + ); + totalNativeFee += treasuryNativeFee; + + // store the cmdHash for verification in order to prevent reorg attack + cmdHashLookup[_packet.sender][_packet.dstEid][_packet.nonce] = keccak256(_packet.message); + + return (MessagingFee(totalNativeFee, lzTokenFee), encodedPacket); + } + + function setConfig(address _oapp, SetConfigParam[] calldata _params) external onlyEndpoint { + for (uint256 i = 0; i < _params.length; i++) { + SetConfigParam calldata param = _params[i]; + _assertSupportedEid(param.eid); + if (param.configType == CONFIG_TYPE_READ_LID_CONFIG) { + _setReadLibConfig(param.eid, _oapp, abi.decode(param.config, (ReadLibConfig))); + } else { + revert LZ_RL_InvalidConfigType(param.configType); + } + } + } + + // ============================ External =================================== + /// @dev The verification will be done in the same chain where the packet is sent. + /// @dev dont need to check endpoint verifiable here to save gas, as it will reverts if not verifiable. + /// @param _packetHeader - the srcEid should be the localEid and the dstEid should be the channel id. + /// The original packet header in PacketSent event should be processed to flip the srcEid and dstEid. + function commitVerification(bytes calldata _packetHeader, bytes32 _cmdHash, bytes32 _payloadHash) external { + // assert packet header is of right size 81 + if (_packetHeader.length != 81) revert LZ_RL_InvalidPacketHeader(); + // assert packet header version is the same + if (_packetHeader.version() != PacketV1Codec.PACKET_VERSION) revert LZ_RL_InvalidPacketVersion(); + // assert the packet is for this endpoint + if (_packetHeader.dstEid() != localEid) revert LZ_RL_InvalidEid(); + + // cache these values to save gas + address receiver = _packetHeader.receiverB20(); + uint32 srcEid = _packetHeader.srcEid(); // channel id + uint64 nonce = _packetHeader.nonce(); + + // reorg protection. to allow reverification, the cmdHash cant be removed + if (cmdHashLookup[receiver][srcEid][nonce] != _cmdHash) revert LZ_RL_InvalidCmdHash(); + + ReadLibConfig memory config = getReadLibConfig(receiver, srcEid); + _verifyAndReclaimStorage(config, keccak256(_packetHeader), _cmdHash, _payloadHash); + + // endpoint will revert if nonce <= lazyInboundNonce + Origin memory origin = Origin(srcEid, _packetHeader.sender(), nonce); + ILayerZeroEndpointV2(endpoint).verify(origin, receiver, _payloadHash); + } + + /// @dev DVN verifies the payload with the packet header and command hash + /// @param _packetHeader - the packet header is needed for event only, which can be conveniently for off-chain to track the packet state. + function verify(bytes calldata _packetHeader, bytes32 _cmdHash, bytes32 _payloadHash) external { + hashLookup[keccak256(_packetHeader)][_cmdHash][msg.sender] = _payloadHash; + emit PayloadVerified(msg.sender, _packetHeader, _cmdHash, _payloadHash); + } + + function withdrawFee(address _to, uint256 _amount) external { + uint256 fee = fees[msg.sender]; + if (_amount > fee) revert LZ_RL_InvalidAmount(_amount, fee); + unchecked { + fees[msg.sender] = fee - _amount; + } + + // transfers native if nativeToken == address(0x0) + address nativeToken = ILayerZeroEndpointV2(endpoint).nativeToken(); + Transfer.nativeOrToken(nativeToken, _to, _amount); + emit NativeFeeWithdrawn(msg.sender, _to, _amount); + } + + // ============================ Treasury =================================== + + /// @dev _lzToken is a user-supplied value because lzToken might change in the endpoint before all lzToken can be taken out + function withdrawLzTokenFee(address _lzToken, address _to, uint256 _amount) external { + if (msg.sender != treasury) revert LZ_RL_NotTreasury(); + + // lz token cannot be the same as the native token + if (ILayerZeroEndpointV2(endpoint).nativeToken() == _lzToken) revert LZ_RL_CannotWithdrawAltToken(); + + Transfer.token(_lzToken, _to, _amount); + + emit LzTokenFeeWithdrawn(_lzToken, _to, _amount); + } + + // ============================ View =================================== + + function quote( + Packet calldata _packet, + bytes calldata _options, + bool _payInLzToken + ) external view returns (MessagingFee memory) { + // split workers options + (bytes memory executorOptions, bytes memory dvnOptions) = UlnOptions.decode(_options); + + address sender = _packet.sender; + uint32 dstEid = _packet.dstEid; + + // quote the executor and dvns + ReadLibConfig memory config = getReadLibConfig(sender, dstEid); + uint256 nativeFee = _quoteDVNs( + config, + sender, + PacketV1Codec.encodePacketHeader(_packet), + _packet.message, + dvnOptions + ); + nativeFee += ILayerZeroReadExecutor(config.executor).getFee(sender, executorOptions); + + // quote treasury + (uint256 treasuryNativeFee, uint256 lzTokenFee) = _quoteTreasury(sender, dstEid, nativeFee, _payInLzToken); + nativeFee += treasuryNativeFee; + + return MessagingFee(nativeFee, lzTokenFee); + } + + function verifiable( + ReadLibConfig calldata _config, + bytes32 _headerHash, + bytes32 _cmdHash, + bytes32 _payloadHash + ) external view returns (bool) { + return _checkVerifiable(_config, _headerHash, _cmdHash, _payloadHash); + } + + function getConfig(uint32 _eid, address _oapp, uint32 _configType) external view returns (bytes memory) { + if (_configType == CONFIG_TYPE_READ_LID_CONFIG) { + return abi.encode(getReadLibConfig(_oapp, _eid)); + } else { + revert LZ_RL_InvalidConfigType(_configType); + } + } + + function getTreasuryAndNativeFeeCap() external view returns (address, uint256) { + return (treasury, treasuryNativeFeeCap); + } + + function isSupportedEid(uint32 _eid) external view returns (bool) { + return _isSupportedEid(_eid); + } + + function messageLibType() external pure returns (MessageLibType) { + return MessageLibType.SendAndReceive; + } + + function version() external pure returns (uint64 major, uint8 minor, uint8 endpointVersion) { + return (10, 0, 2); + } + + // ============================ Internal =================================== + + /// 1/ handle executor + /// 2/ handle other workers + function _payWorkers( + Packet calldata _packet, + bytes calldata _options + ) internal returns (bytes memory encodedPacket, uint256 totalNativeFee) { + // split workers options + (bytes memory executorOptions, bytes memory dvnOptions) = UlnOptions.decode(_options); + + // handle executor + ReadLibConfig memory config = getReadLibConfig(_packet.sender, _packet.dstEid); + totalNativeFee = _payExecutor(config.executor, _packet.sender, executorOptions); + + // handle dvns + (uint256 dvnFee, bytes memory packetBytes) = _payDVNs(config, _packet, dvnOptions); + totalNativeFee += dvnFee; + + encodedPacket = packetBytes; + } + + function _payDVNs( + ReadLibConfig memory _config, + Packet calldata _packet, + bytes memory _options + ) internal returns (uint256 totalFee, bytes memory encodedPacket) { + bytes memory packetHeader = PacketV1Codec.encodePacketHeader(_packet); + bytes memory payload = PacketV1Codec.encodePayload(_packet); + + uint256[] memory dvnFees; + (totalFee, dvnFees) = _assignDVNJobs(_config, _packet.sender, packetHeader, _packet.message, _options); + + encodedPacket = abi.encodePacked(packetHeader, payload); + emit DVNFeePaid(_config.requiredDVNs, _config.optionalDVNs, dvnFees); + } + + function _assignDVNJobs( + ReadLibConfig memory _config, + address _sender, + bytes memory _packetHeader, + bytes calldata _cmd, + bytes memory _options + ) internal returns (uint256 totalFee, uint256[] memory dvnFees) { + (bytes[] memory optionsArray, uint8[] memory dvnIds) = DVNOptions.groupDVNOptionsByIdx(_options); + + uint8 dvnsLength = _config.requiredDVNCount + _config.optionalDVNCount; + dvnFees = new uint256[](dvnsLength); + for (uint8 i = 0; i < dvnsLength; ++i) { + address dvn = i < _config.requiredDVNCount + ? _config.requiredDVNs[i] + : _config.optionalDVNs[i - _config.requiredDVNCount]; + + bytes memory options = ""; + for (uint256 j = 0; j < dvnIds.length; ++j) { + if (dvnIds[j] == i) { + options = optionsArray[j]; + break; + } + } + + dvnFees[i] = ILayerZeroReadDVN(dvn).assignJob(_sender, _packetHeader, _cmd, options); + if (dvnFees[i] > 0) { + fees[dvn] += dvnFees[i]; + totalFee += dvnFees[i]; + } + } + } + + function _quoteDVNs( + ReadLibConfig memory _config, + address _sender, + bytes memory _packetHeader, + bytes calldata _cmd, + bytes memory _options + ) internal view returns (uint256 totalFee) { + (bytes[] memory optionsArray, uint8[] memory dvnIndices) = DVNOptions.groupDVNOptionsByIdx(_options); + + // here we merge 2 list of dvns into 1 to allocate the indexed dvn options to the right dvn + uint8 dvnsLength = _config.requiredDVNCount + _config.optionalDVNCount; + for (uint8 i = 0; i < dvnsLength; ++i) { + address dvn = i < _config.requiredDVNCount + ? _config.requiredDVNs[i] + : _config.optionalDVNs[i - _config.requiredDVNCount]; + + bytes memory options = ""; + // it is a double loop here. however, if the list is short, the cost is very acceptable. + for (uint256 j = 0; j < dvnIndices.length; ++j) { + if (dvnIndices[j] == i) { + options = optionsArray[j]; + break; + } + } + totalFee += ILayerZeroReadDVN(dvn).getFee(_sender, _packetHeader, _cmd, options); + } + } + + function _payTreasury( + address _sender, + uint32 _dstEid, + uint256 _totalNativeFee, + bool _payInLzToken + ) internal returns (uint256 treasuryNativeFee, uint256 lzTokenFee) { + if (treasury != address(0x0)) { + bytes memory callData = abi.encodeCall( + ILayerZeroTreasury.payFee, + (_sender, _dstEid, _totalNativeFee, _payInLzToken) + ); + (bool success, bytes memory result) = treasury.safeCall(treasuryGasLimit, 0, TREASURY_MAX_COPY, callData); + + (treasuryNativeFee, lzTokenFee) = _parseTreasuryResult(_totalNativeFee, _payInLzToken, success, result); + // fee should be in lzTokenFee if payInLzToken, otherwise in native + if (treasuryNativeFee > 0) { + fees[treasury] += treasuryNativeFee; + } + } + } + + /// @dev this interface should be DoS-free if the user is paying with native. properties + /// 1/ treasury can return an overly high lzToken fee + /// 2/ if treasury returns an overly high native fee, it will be capped by maxNativeFee, + /// which can be reasoned with the configurations + /// 3/ the owner can not configure the treasury in a way that force this function to revert + function _quoteTreasury( + address _sender, + uint32 _dstEid, + uint256 _totalNativeFee, + bool _payInLzToken + ) internal view returns (uint256 nativeFee, uint256 lzTokenFee) { + // treasury must be set, and it has to be a contract + if (treasury != address(0x0)) { + bytes memory callData = abi.encodeCall( + ILayerZeroTreasury.getFee, + (_sender, _dstEid, _totalNativeFee, _payInLzToken) + ); + (bool success, bytes memory result) = treasury.safeStaticCall( + treasuryGasLimit, + TREASURY_MAX_COPY, + callData + ); + + return _parseTreasuryResult(_totalNativeFee, _payInLzToken, success, result); + } + } + + function _parseTreasuryResult( + uint256 _totalNativeFee, + bool _payInLzToken, + bool _success, + bytes memory _result + ) internal view returns (uint256 nativeFee, uint256 lzTokenFee) { + // failure, charges nothing + if (!_success || _result.length < TREASURY_MAX_COPY) return (0, 0); + + // parse the result + uint256 treasureFeeQuote = abi.decode(_result, (uint256)); + if (_payInLzToken) { + lzTokenFee = treasureFeeQuote; + } else { + // pay in native + // we must prevent high-treasuryFee Dos attack + // nativeFee = min(treasureFeeQuote, maxNativeFee) + // opportunistically raise the maxNativeFee to be the same as _totalNativeFee + // can't use the _totalNativeFee alone because the oapp can use custom workers to force the fee to 0. + // maxNativeFee = max (_totalNativeFee, treasuryNativeFeeCap) + uint256 maxNativeFee = _totalNativeFee > treasuryNativeFeeCap ? _totalNativeFee : treasuryNativeFeeCap; + + // min (treasureFeeQuote, nativeFeeCap) + nativeFee = treasureFeeQuote > maxNativeFee ? maxNativeFee : treasureFeeQuote; + } + } + + function _verifyAndReclaimStorage( + ReadLibConfig memory _config, + bytes32 _headerHash, + bytes32 _cmdHash, + bytes32 _payloadHash + ) internal { + if (!_checkVerifiable(_config, _headerHash, _cmdHash, _payloadHash)) { + revert LZ_RL_Verifying(); + } + + // iterate the required DVNs + if (_config.requiredDVNCount > 0) { + for (uint8 i = 0; i < _config.requiredDVNCount; ++i) { + delete hashLookup[_headerHash][_cmdHash][_config.requiredDVNs[i]]; + } + } + + // iterate the optional DVNs + if (_config.optionalDVNCount > 0) { + for (uint8 i = 0; i < _config.optionalDVNCount; ++i) { + delete hashLookup[_headerHash][_cmdHash][_config.optionalDVNs[i]]; + } + } + } + + /// @dev for verifiable view function + /// @dev checks if this verification is ready to be committed to the endpoint + function _checkVerifiable( + ReadLibConfig memory _config, + bytes32 _headerHash, + bytes32 _cmdHash, + bytes32 _payloadHash + ) internal view returns (bool) { + // iterate the required DVNs + if (_config.requiredDVNCount > 0) { + for (uint8 i = 0; i < _config.requiredDVNCount; ++i) { + if (!_verified(_config.requiredDVNs[i], _headerHash, _cmdHash, _payloadHash)) { + // return if any of the required DVNs haven't signed + return false; + } + } + if (_config.optionalDVNCount == 0) { + // returns early if all required DVNs have signed and there are no optional DVNs + return true; + } + } + + // then it must require optional validations + uint8 threshold = _config.optionalDVNThreshold; + for (uint8 i = 0; i < _config.optionalDVNCount; ++i) { + if (_verified(_config.optionalDVNs[i], _headerHash, _cmdHash, _payloadHash)) { + // increment the optional count if the optional DVN has signed + threshold--; + if (threshold == 0) { + // early return if the optional threshold has hit + return true; + } + } + } + + // return false as a catch-all + return false; + } + + function _verified( + address _dvn, + bytes32 _headerHash, + bytes32 _cmdHash, + bytes32 _expectedPayloadHash + ) internal view returns (bool verified) { + verified = hashLookup[_headerHash][_cmdHash][_dvn] == _expectedPayloadHash; + } + + function _payExecutor( + address _executor, + address _sender, + bytes memory _executorOptions + ) internal returns (uint256 executorFee) { + executorFee = ILayerZeroReadExecutor(_executor).assignJob(_sender, _executorOptions); + if (executorFee > 0) { + fees[_executor] += executorFee; + } + emit ExecutorFeePaid(_executor, executorFee); + } + + receive() external payable {} +} diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/readlib/ReadLib1002View.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/readlib/ReadLib1002View.sol new file mode 100644 index 0000000..b319e83 --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/readlib/ReadLib1002View.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import { Proxied } from "hardhat-deploy/solc_0.8/proxy/Proxied.sol"; +import { PacketV1Codec } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import { Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { EndpointV2ViewUpgradeable } from "@layerzerolabs/lz-evm-protocol-v2/contracts/EndpointV2ViewUpgradeable.sol"; + +import { ReadLibConfig } from "./ReadLibBase.sol"; +import { ReadLib1002 } from "./ReadLib1002.sol"; + +enum VerificationState { + Verifying, + Verifiable, + Verified, + NotInitializable, + Reorged +} + +contract ReadLib1002View is EndpointV2ViewUpgradeable, Proxied { + using PacketV1Codec for bytes; + + ReadLib1002 public readLib; + uint32 internal localEid; + + function initialize(address _endpoint, address payable _readLib) external proxied initializer { + __EndpointV2View_init(_endpoint); + readLib = ReadLib1002(_readLib); + localEid = endpoint.eid(); + } + + /// @dev get a verifiable payload hash based on the payloadHashLookup from the DVNs + function getVerifiablePayloadHash( + address _receiver, + uint32 _srcEid, + bytes32 _headerHash, + bytes32 _cmdHash + ) public view returns (bytes32) { + ReadLibConfig memory config = readLib.getReadLibConfig(_receiver, _srcEid); + uint8 dvnsLength = config.requiredDVNCount + config.optionalDVNCount; + for (uint8 i = 0; i < dvnsLength; ++i) { + address dvn = i < config.requiredDVNCount + ? config.requiredDVNs[i] + : config.optionalDVNs[i - config.requiredDVNCount]; + + bytes32 payloadHash = readLib.hashLookup(_headerHash, _cmdHash, dvn); + if (readLib.verifiable(config, _headerHash, _cmdHash, payloadHash)) { + return payloadHash; + } + } + return EMPTY_PAYLOAD_HASH; // not found + } + + /// @dev a verifiable requires it to be endpoint verifiable and committable + function verifiable(bytes calldata _packetHeader, bytes32 _cmdHash) external view returns (VerificationState) { + address receiver = _packetHeader.receiverB20(); + uint32 srcEid = _packetHeader.srcEid(); + + Origin memory origin = Origin(srcEid, _packetHeader.sender(), _packetHeader.nonce()); + + // check endpoint initializable + if (!initializable(origin, receiver)) { + return VerificationState.NotInitializable; + } + + // check endpoint verifiable. if false, that means it is executed and can not be verified + if (!endpoint.verifiable(origin, receiver)) { + return VerificationState.Verified; + } + + // get the verifiable payload hash + bytes32 payloadHash = getVerifiablePayloadHash(receiver, srcEid, keccak256(_packetHeader), _cmdHash); + + if (payloadHash == EMPTY_PAYLOAD_HASH) { + // if payload hash is not empty, it is verified + if (endpoint.inboundPayloadHash(receiver, srcEid, origin.sender, origin.nonce) != EMPTY_PAYLOAD_HASH) { + return VerificationState.Verified; + } + + // otherwise, it is verifying + return VerificationState.Verifying; + } + + // check if the cmdHash matches + if (readLib.cmdHashLookup(receiver, srcEid, origin.nonce) != _cmdHash) { + return VerificationState.Reorged; + } + + // check if the payload hash matches + // endpoint allows re-verify, check if this payload has already been verified + if (endpoint.inboundPayloadHash(receiver, origin.srcEid, origin.sender, origin.nonce) == payloadHash) { + return VerificationState.Verified; + } + + return VerificationState.Verifiable; + } +} diff --git a/packages/layerzero-v2/evm/messagelib/contracts/uln/readlib/ReadLibBase.sol b/packages/layerzero-v2/evm/messagelib/contracts/uln/readlib/ReadLibBase.sol new file mode 100644 index 0000000..8347f51 --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/contracts/uln/readlib/ReadLibBase.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +struct ReadLibConfig { + address executor; + // we store the length of required DVNs and optional DVNs instead of using DVN.length directly to save gas + uint8 requiredDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) + uint8 optionalDVNCount; // 0 indicate DEFAULT, NIL_DVN_COUNT indicate NONE (to override the value of default) + uint8 optionalDVNThreshold; // (0, optionalDVNCount] + address[] requiredDVNs; // no duplicates. sorted an an ascending order. allowed overlap with optionalDVNs + address[] optionalDVNs; // no duplicates. sorted an an ascending order. allowed overlap with requiredDVNs +} + +struct SetDefaultReadLibConfigParam { + uint32 eid; + ReadLibConfig config; +} + +/// @dev includes the utility functions for checking ReadLib states and logics +abstract contract ReadLibBase is Ownable { + address internal constant DEFAULT_CONFIG = address(0); + // reserved values for + uint8 internal constant DEFAULT = 0; + uint8 internal constant NIL_DVN_COUNT = type(uint8).max; + // 127 to prevent total number of DVNs (127 * 2) exceeding uint8.max (255) + // by limiting the total size, it would help constraint the design of DVNOptions + uint8 private constant MAX_COUNT = (type(uint8).max - 1) / 2; + + mapping(address oapp => mapping(uint32 eid => ReadLibConfig config)) internal readLibConfigs; + + error LZ_RL_Unsorted(); + error LZ_RL_InvalidRequiredDVNCount(); + error LZ_RL_InvalidOptionalDVNCount(); + error LZ_RL_AtLeastOneDVN(); + error LZ_RL_InvalidOptionalDVNThreshold(); + error LZ_RL_UnsupportedEid(uint32 eid); + error LZ_RL_InvalidExecutor(); + + event DefaultReadLibConfigsSet(SetDefaultReadLibConfigParam[] params); + event ReadLibConfigSet(address oapp, uint32 eid, ReadLibConfig config); + + // ============================ OnlyOwner =================================== + + /// @dev about the DEFAULT ReadLib config + /// 1) its values are all LITERAL (e.g. 0 is 0). whereas in the oapp ReadLib config, 0 (default value) points to the default ReadLib config + /// this design enables the oapp to point to DEFAULT config without explicitly setting the config + /// 2) its configuration is more restrictive than the oapp ReadLib config that + /// a) it must not use NIL value, where NIL is used only by oapps to indicate the LITERAL 0 + /// b) it must have at least one DVN and executor + function setDefaultReadLibConfigs(SetDefaultReadLibConfigParam[] calldata _params) external onlyOwner { + for (uint256 i = 0; i < _params.length; ++i) { + SetDefaultReadLibConfigParam calldata param = _params[i]; + + // 2.a must not use NIL + if (param.config.requiredDVNCount == NIL_DVN_COUNT) revert LZ_RL_InvalidRequiredDVNCount(); + if (param.config.optionalDVNCount == NIL_DVN_COUNT) revert LZ_RL_InvalidOptionalDVNCount(); + + // 2.b must have at least one dvn and executor + _assertAtLeastOneDVN(param.config); + if (param.config.executor == address(0x0)) revert LZ_RL_InvalidExecutor(); + + _setConfig(DEFAULT_CONFIG, param.eid, param.config); + } + emit DefaultReadLibConfigsSet(_params); + } + + // ============================ View =================================== + // @dev assuming most oapps use default, we get default as memory and custom as storage to save gas + function getReadLibConfig(address _oapp, uint32 _remoteEid) public view returns (ReadLibConfig memory rtnConfig) { + ReadLibConfig storage defaultConfig = readLibConfigs[DEFAULT_CONFIG][_remoteEid]; + ReadLibConfig storage customConfig = readLibConfigs[_oapp][_remoteEid]; + + address executor = customConfig.executor; + rtnConfig.executor = executor != address(0x0) ? executor : defaultConfig.executor; + + if (customConfig.requiredDVNCount == DEFAULT) { + if (defaultConfig.requiredDVNCount > 0) { + // copy only if count > 0. save gas + rtnConfig.requiredDVNs = defaultConfig.requiredDVNs; + rtnConfig.requiredDVNCount = defaultConfig.requiredDVNCount; + } // else, do nothing + } else { + if (customConfig.requiredDVNCount != NIL_DVN_COUNT) { + rtnConfig.requiredDVNs = customConfig.requiredDVNs; + rtnConfig.requiredDVNCount = customConfig.requiredDVNCount; + } // else, do nothing + } + + if (customConfig.optionalDVNCount == DEFAULT) { + if (defaultConfig.optionalDVNCount > 0) { + // copy only if count > 0. save gas + rtnConfig.optionalDVNs = defaultConfig.optionalDVNs; + rtnConfig.optionalDVNCount = defaultConfig.optionalDVNCount; + rtnConfig.optionalDVNThreshold = defaultConfig.optionalDVNThreshold; + } + } else { + if (customConfig.optionalDVNCount != NIL_DVN_COUNT) { + rtnConfig.optionalDVNs = customConfig.optionalDVNs; + rtnConfig.optionalDVNCount = customConfig.optionalDVNCount; + rtnConfig.optionalDVNThreshold = customConfig.optionalDVNThreshold; + } + } + + // the final value must have at least one dvn + // it is possible that some default config result into 0 dvns + _assertAtLeastOneDVN(rtnConfig); + } + + /// @dev Get the readLib config without the default config for the given remoteEid. + function getAppReadLibConfig(address _oapp, uint32 _remoteEid) external view returns (ReadLibConfig memory) { + return readLibConfigs[_oapp][_remoteEid]; + } + + // ============================ Internal =================================== + function _setReadLibConfig(uint32 _remoteEid, address _oapp, ReadLibConfig memory _param) internal { + _setConfig(_oapp, _remoteEid, _param); + + // get ReadLib config again as a catch all to ensure the config is valid + getReadLibConfig(_oapp, _remoteEid); + emit ReadLibConfigSet(_oapp, _remoteEid, _param); + } + + /// @dev a supported Eid must have a valid default readLib config, which has at least one dvn + function _isSupportedEid(uint32 _remoteEid) internal view returns (bool) { + ReadLibConfig storage defaultConfig = readLibConfigs[DEFAULT_CONFIG][_remoteEid]; + return defaultConfig.requiredDVNCount > 0 || defaultConfig.optionalDVNThreshold > 0; + } + + function _assertSupportedEid(uint32 _remoteEid) internal view { + if (!_isSupportedEid(_remoteEid)) revert LZ_RL_UnsupportedEid(_remoteEid); + } + + // ============================ Private =================================== + + function _assertAtLeastOneDVN(ReadLibConfig memory _config) private pure { + if (_config.requiredDVNCount == 0 && _config.optionalDVNThreshold == 0) revert LZ_RL_AtLeastOneDVN(); + } + + /// @dev this private function is used in both setDefaultReadLibConfigs and setReadLibConfig + function _setConfig(address _oapp, uint32 _eid, ReadLibConfig memory _param) private { + // @dev required dvns + // if dvnCount == NONE, dvns list must be empty + // if dvnCount == DEFAULT, dvn list must be empty + // otherwise, dvnList.length == dvnCount and assert the list is valid + if (_param.requiredDVNCount == NIL_DVN_COUNT || _param.requiredDVNCount == DEFAULT) { + if (_param.requiredDVNs.length != 0) revert LZ_RL_InvalidRequiredDVNCount(); + } else { + if (_param.requiredDVNs.length != _param.requiredDVNCount || _param.requiredDVNCount > MAX_COUNT) + revert LZ_RL_InvalidRequiredDVNCount(); + _assertNoDuplicates(_param.requiredDVNs); + } + + // @dev optional dvns + // if optionalDVNCount == NONE, optionalDVNs list must be empty and threshold must be 0 + // if optionalDVNCount == DEFAULT, optionalDVNs list must be empty and threshold must be 0 + // otherwise, optionalDVNs.length == optionalDVNCount, threshold > 0 && threshold <= optionalDVNCount and assert the list is valid + + // example use case: an oapp uses the DEFAULT 'required' but + // a) use a custom 1/1 dvn (practically a required dvn), or + // b) use a custom 2/3 dvn + if (_param.optionalDVNCount == NIL_DVN_COUNT || _param.optionalDVNCount == DEFAULT) { + if (_param.optionalDVNs.length != 0) revert LZ_RL_InvalidOptionalDVNCount(); + if (_param.optionalDVNThreshold != 0) revert LZ_RL_InvalidOptionalDVNThreshold(); + } else { + if (_param.optionalDVNs.length != _param.optionalDVNCount || _param.optionalDVNCount > MAX_COUNT) + revert LZ_RL_InvalidOptionalDVNCount(); + if (_param.optionalDVNThreshold == 0 || _param.optionalDVNThreshold > _param.optionalDVNCount) + revert LZ_RL_InvalidOptionalDVNThreshold(); + _assertNoDuplicates(_param.optionalDVNs); + } + // don't assert valid count here, as it needs to be validated along side default config + + readLibConfigs[_oapp][_eid] = _param; + } + + function _assertNoDuplicates(address[] memory _dvns) private pure { + address lastDVN = address(0); + for (uint256 i = 0; i < _dvns.length; i++) { + address dvn = _dvns[i]; + if (dvn <= lastDVN) revert LZ_RL_Unsorted(); // to ensure no duplicates + lastDVN = dvn; + } + } +} diff --git a/packages/layerzero-v2/evm/messagelib/test/DVN.t.sol b/packages/layerzero-v2/evm/messagelib/test/DVN.t.sol index 599078d..2591eb7 100644 --- a/packages/layerzero-v2/evm/messagelib/test/DVN.t.sol +++ b/packages/layerzero-v2/evm/messagelib/test/DVN.t.sol @@ -420,17 +420,47 @@ contract DVNTest is Test { dvn.assignJob(ILayerZeroDVN.AssignJobParam(0, "", "", 0, sender), ""); } + function test_Revert_AssignJob_Read_NotByMessageLib() public { + vm.expectRevert(); + dvn.assignJob(address(1), "", "", ""); + } + + function test_Revert_AssignJob_Read_NotAcl_Denied() public { + // set deniedSender to denylist + address deniedSender = address(1); + vm.prank(address(dvn)); + dvn.grantRole(DENYLIST, deniedSender); + + vm.expectRevert(IWorker.Worker_NotAllowed.selector); + vm.prank(address(fixtureV2.sendUln302)); + dvn.assignJob(deniedSender, "", "", ""); + } + + function test_Revert_AssignJob_Read_NotAcl_NotInAllowList() public { + // set allowed sender to allowlist + address allowedSender = address(1); + vm.prank(address(dvn)); + dvn.grantRole(ALLOWLIST, allowedSender); + + address sender = address(2); + vm.expectRevert(IWorker.Worker_NotAllowed.selector); + vm.prank(address(fixtureV2.sendUln302)); + dvn.assignJob(sender, "", "", ""); + } + function test_GetFee() public { // mock feeLib getFee address workerFeeLib = dvn.workerFeeLib(); - vm.mockCall(workerFeeLib, abi.encodeWithSelector(IDVNFeeLib.getFee.selector), abi.encode(100)); + string memory sig = "getFee((address,uint32,uint64,address,uint64,uint16),(uint64,uint16,uint128),bytes)"; + vm.mockCall(workerFeeLib, abi.encodeWithSignature(sig), abi.encode(100)); assertEq(dvn.getFee(0, 0, address(0), ""), 100, "fee is mocked by 100"); } function test_GetFee_UlnV2() public { // mock feeLib getFee address workerFeeLib = dvn.workerFeeLib(); - vm.mockCall(workerFeeLib, abi.encodeWithSelector(IDVNFeeLib.getFee.selector), abi.encode(100)); + string memory sig = "getFee((address,uint32,uint64,address,uint64,uint16),(uint64,uint16,uint128),bytes)"; + vm.mockCall(workerFeeLib, abi.encodeWithSignature(sig), abi.encode(100)); assertEq(dvn.getFee(0, 0, 0, address(0)), 100, "fee is mocked by 100"); } @@ -455,6 +485,35 @@ contract DVNTest is Test { dvn.getFee(0, 0, sender, ""); } + function test_GetFee_Read() public { + // mock feeLib getFee for Read + address workerFeeLib = dvn.workerFeeLib(); + string memory sig = "getFee((address,address,uint64,uint16),(uint64,uint16,uint128),bytes,bytes)"; + vm.mockCall(workerFeeLib, abi.encodeWithSignature(sig), abi.encode(100)); + assertEq(dvn.getFee(address(0), "", "", ""), 100, "fee is mocked by 100"); + } + + function test_Revert_GetFee_Read_NotAcl_Denied() public { + // set deniedSender to denylist + address deniedSender = address(1); + vm.prank(address(dvn)); + dvn.grantRole(DENYLIST, deniedSender); + + vm.expectRevert(IWorker.Worker_NotAllowed.selector); + dvn.getFee(deniedSender, "", "", ""); + } + + function test_Revert_GetFee_Read_NotAcl_NotInAllowList() public { + // set allowed sender to allowlist + address allowedSender = address(1); + vm.prank(address(dvn)); + dvn.grantRole(ALLOWLIST, allowedSender); + + address sender = address(2); + vm.expectRevert(IWorker.Worker_NotAllowed.selector); + dvn.getFee(sender, "", "", ""); + } + function test_WithdrawFee() public { // mock vm.mockCall( diff --git a/packages/layerzero-v2/evm/messagelib/test/DVNFeeLib.t.sol b/packages/layerzero-v2/evm/messagelib/test/DVNFeeLib.t.sol index c3b7062..4594524 100644 --- a/packages/layerzero-v2/evm/messagelib/test/DVNFeeLib.t.sol +++ b/packages/layerzero-v2/evm/messagelib/test/DVNFeeLib.t.sol @@ -2,23 +2,25 @@ pragma solidity ^0.8.0; -import { Test } from "forge-std/Test.sol"; +import { Test, console } from "forge-std/Test.sol"; import { IDVN } from "../contracts/uln/interfaces/IDVN.sol"; import { IDVNFeeLib } from "../contracts/uln/interfaces/IDVNFeeLib.sol"; import { DVNFeeLib } from "../contracts/uln/dvn/DVNFeeLib.sol"; +import { DVN, ExecuteParam } from "../contracts/uln/dvn/DVN.sol"; +import { SupportedCmdTypes, BitMap256 } from "../contracts/uln/libs/SupportedCmdTypes.sol"; +import { ReadLib1002 } from "../contracts/uln/readlib/ReadLib1002.sol"; +import { ReceiveUln302 } from "../contracts/uln/uln302/ReceiveUln302.sol"; import { PriceFeedMock } from "./mocks/PriceFeedMock.sol"; +import { CmdUtil } from "./util/CmdUtil.sol"; -contract DVNFeeLibTest is Test { - uint16 constant EXECUTE_FIXED_BYTES = 68; - uint16 constant SIGNATURE_RAW_BYTES = 65; - uint16 constant UPDATE_HASH_BYTES = 224; - +contract DVNFeeLibTest is Test, DVNFeeLib { DVNFeeLib dvnFeeLib; PriceFeedMock priceFeed; IDVN.DstConfig config; uint16 defaultMultiplierBps = 12000; + uint32 localEid = 100; uint32 dstEid = 101; uint256 gasFee = 100; uint128 priceRatio = 1e10; @@ -30,9 +32,20 @@ contract DVNFeeLibTest is Test { address oapp = address(0); uint64 confirmations = 15; + uint120 internal OneUSD = 1e10; + uint120 internal REQUEST_PER_JOB = OneUSD; + uint120 internal REDUCE_PER_JOB = OneUSD; + uint16 internal MAP_PER_REQ_JOB = 1000; // 10% + + constructor() DVNFeeLib(100, 1e18) {} + function setUp() public { priceFeed = new PriceFeedMock(); - dvnFeeLib = new DVNFeeLib(1e18); + + dvnFeeLib = new DVNFeeLib(localEid, 1e18); + dvnFeeLib.setCmdFees(REQUEST_PER_JOB, REDUCE_PER_JOB, MAP_PER_REQ_JOB); // 1u, 1u, 1000 + setDVNFeeLibSupportedCmdTypes(dstEid, 3); + priceFeed.setup(gasFee, priceRatio, nativePriceUSD); } @@ -124,4 +137,304 @@ contract DVNFeeLibTest is Test { assertEq(actual, expected); } + + function test_getFee_read_defaultMultiplier() public { + config = IDVN.DstConfig(gas, 0, 0); + + uint8 requestNum = 5; + uint256 expected = (((requestNum * REQUEST_PER_JOB * 1e18) / nativePriceUSD + gasFee) * defaultMultiplierBps) / + 10000; + IDVNFeeLib.FeeParamsForRead memory params = IDVNFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + quorum, + defaultMultiplierBps + ); + bytes memory cmd = buildCmd(requestNum, false, 0); + uint256 actual = dvnFeeLib.getFee(params, config, cmd, ""); + assertEq(actual, expected); + + // with map only, request_fee * (1+MAP_PER_REQ_JOB) + expected = + (((((requestNum * REQUEST_PER_JOB * 1e18) / nativePriceUSD) * (10000 + MAP_PER_REQ_JOB)) / 10000 + gasFee) * + defaultMultiplierBps) / + 10000; + cmd = buildCmd(requestNum, true, 0); + actual = dvnFeeLib.getFee(params, config, cmd, ""); + assertEq(actual, expected); + + // with reduce only, request_fee + REDUCE_PER_JOB + expected = + ((((((requestNum * REQUEST_PER_JOB + REDUCE_PER_JOB) * 1e18) / nativePriceUSD)) + gasFee) * + defaultMultiplierBps) / + 10000; + cmd = buildCmd(requestNum, true, 1); + actual = dvnFeeLib.getFee(params, config, cmd, ""); + assertEq(actual, expected); + + // with map and reduce, request_fee * (1+MAP_PER_REQ_JOB) + REDUCE_PER_JOB + expected = + ((((((requestNum * REQUEST_PER_JOB * (10000 + MAP_PER_REQ_JOB)) / 10000 + REDUCE_PER_JOB) * 1e18) / + nativePriceUSD) + gasFee) * defaultMultiplierBps) / + 10000; + cmd = buildCmd(requestNum, true, 2); + actual = dvnFeeLib.getFee(params, config, cmd, ""); + assertEq(actual, expected); + } + + function test_getFee_read_specificMultiplier() public { + config = IDVN.DstConfig(gas, multiplierBps, 0); + + uint8 requestNum = 5; + uint256 expected = (((requestNum * REQUEST_PER_JOB * 1e18) / nativePriceUSD + gasFee) * multiplierBps) / 10000; + IDVNFeeLib.FeeParamsForRead memory params = IDVNFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + quorum, + defaultMultiplierBps + ); + bytes memory cmd = buildCmd(requestNum, false, 0); + uint256 actual = dvnFeeLib.getFee(params, config, cmd, ""); + assertEq(actual, expected); + } + + function test_setSupportedCmdTypes() public { + DVNFeeLib.SetSupportedCmdTypesParam[] memory params = new DVNFeeLib.SetSupportedCmdTypesParam[](1); + params[0] = DVNFeeLib.SetSupportedCmdTypesParam(1, BitMap256.wrap(1)); + dvnFeeLib.setSupportedCmdTypes(params); + + uint256 bm = BitMap256.unwrap(dvnFeeLib.getSupportedCmdTypes(1)); + assertEq(bm, 1); + } + + function test_getReadCallDataSize() public { + uint256[] memory quorums = new uint256[](4); + quorums[0] = 1; + quorums[1] = 64; + quorums[2] = 128; + quorums[3] = 200; + + for (uint256 i = 0; i < quorums.length; i++) { + uint256 qum = quorums[i]; + bytes memory header = new bytes(81); + bytes memory callData = abi.encodeWithSelector(ReadLib1002.verify.selector, header, bytes32(0), bytes32(0)); // verify(bytes calldata _packetHeader, bytes32 _cmdHash, bytes32 _payloadHash) + bytes memory signatures = new bytes(65 * qum); + ExecuteParam[] memory params = new ExecuteParam[](1); + params[0] = ExecuteParam(0, address(0), callData, 0, signatures); + uint256 expected = abi.encodeWithSelector(DVN.execute.selector, params).length; // dvn.execute(params) + uint256 actual = _getReadCallDataSize(qum); + assertEq(actual, expected); + } + } + + function test_getCallDataSize() public { + uint256[] memory quorums = new uint256[](4); + quorums[0] = 1; + quorums[1] = 64; + quorums[2] = 128; + quorums[3] = 200; + + for (uint256 i = 0; i < quorums.length; i++) { + uint256 qum = quorums[i]; + bytes memory header = new bytes(81); + bytes memory callData = abi.encodeWithSelector( + ReceiveUln302.verify.selector, + header, + bytes32(0), + uint64(0) + ); // verify(bytes calldata _packetHeader, bytes32 _payloadHash, uint64 _confirmations) + bytes memory signatures = new bytes(65 * qum); + ExecuteParam[] memory params = new ExecuteParam[](1); + params[0] = ExecuteParam(0, address(0), callData, 0, signatures); + uint256 expected = abi.encodeWithSelector(DVN.execute.selector, params).length; // dvn.execute(params) + uint256 actual = _getCallDataSize(qum); + assertEq(actual, expected); + } + } + + function test_revert_request_TimestampOutOfReach() public { + setBlock(1000, 1000); + setDVNFeeLibSupportedCmdTypes(dstEid, 7); // 1 + 2 + 4 + config = IDVN.DstConfig(gas, 0, 0); + + IDVNFeeLib.FeeParamsForRead memory params = IDVNFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + quorum, + defaultMultiplierBps + ); + + setFeeLibBlockConfig(dstEid, 500, 500, 90); + + // request timestamp out of reach + uint64 pinTimestamp = uint64(block.timestamp) - 100; // set it out of reach + bytes memory cmd = buildCmd(1, false, 0, false, pinTimestamp, false, uint64(block.timestamp)); + vm.expectRevert(abi.encodeWithSelector(DVN_TimestampOutOfRange.selector, dstEid, pinTimestamp)); + dvnFeeLib.getFee(params, config, cmd, ""); + + pinTimestamp = uint64(block.timestamp) + 100; // set it out of reach + cmd = buildCmd(1, false, 0, false, pinTimestamp, false, uint64(block.timestamp)); + vm.expectRevert(abi.encodeWithSelector(DVN_TimestampOutOfRange.selector, dstEid, pinTimestamp)); + dvnFeeLib.getFee(params, config, cmd, ""); + } + + function test_revert_compute_TimestampOutOfReach() public { + setBlock(1000, 1000); + setDVNFeeLibSupportedCmdTypes(dstEid, 7); // 1 + 2 + 4 + config = IDVN.DstConfig(gas, 0, 0); + + IDVNFeeLib.FeeParamsForRead memory params = IDVNFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + quorum, + defaultMultiplierBps + ); + + setFeeLibBlockConfig(dstEid, 500, 500, 90); + + // request timestamp out of reach + uint64 pinTimestamp = uint64(block.timestamp) - 100; // set it out of reach + bytes memory cmd = buildCmd(1, true, 2, false, uint64(block.timestamp), false, pinTimestamp); + vm.expectRevert(abi.encodeWithSelector(DVN_TimestampOutOfRange.selector, dstEid, pinTimestamp)); + dvnFeeLib.getFee(params, config, cmd, ""); + + pinTimestamp = uint64(block.timestamp) + 100; // set it out of reach + cmd = buildCmd(1, true, 2, false, uint64(block.timestamp), false, pinTimestamp); + vm.expectRevert(abi.encodeWithSelector(DVN_TimestampOutOfRange.selector, dstEid, pinTimestamp)); + dvnFeeLib.getFee(params, config, cmd, ""); + } + + function test_revert_request_BlockOutOfReach() public { + setBlock(1000, 1000); + setDVNFeeLibSupportedCmdTypes(dstEid, 7); // 1 + 2 + 4 + config = IDVN.DstConfig(gas, 0, 0); + + IDVNFeeLib.FeeParamsForRead memory params = IDVNFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + quorum, + defaultMultiplierBps + ); + + setFeeLibBlockConfig(dstEid, 500, 500, 90); // 90 sec retention + + // request block number out of reach + uint64 pinBlockNum = uint64(block.number) - 99; // set it out of reach + bytes memory cmd = buildCmd(1, false, 0, true, pinBlockNum, true, uint64(block.number)); + vm.expectRevert(abi.encodeWithSelector(DVN_TimestampOutOfRange.selector, dstEid, pinBlockNum)); + dvnFeeLib.getFee(params, config, cmd, ""); + + pinBlockNum = uint64(block.number) + 99; // set it out of reach + cmd = buildCmd(1, false, 0, true, pinBlockNum, true, uint64(block.number)); + vm.expectRevert(abi.encodeWithSelector(DVN_TimestampOutOfRange.selector, dstEid, pinBlockNum)); + dvnFeeLib.getFee(params, config, cmd, ""); + } + + function test_revert_compute_BlockOutOfReach() public { + setBlock(1000, 1000); + setDVNFeeLibSupportedCmdTypes(dstEid, 7); // 1 + 2 + 4 + config = IDVN.DstConfig(gas, 0, 0); + + IDVNFeeLib.FeeParamsForRead memory params = IDVNFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + quorum, + defaultMultiplierBps + ); + + setFeeLibBlockConfig(dstEid, 500, 500, 90); // 90 sec retention + + // request block number out of reach + uint64 pinBlockNum = uint64(block.number) - 99; // set it out of reach + bytes memory cmd = buildCmd(1, true, 2, true, uint64(block.number), true, pinBlockNum); + vm.expectRevert(abi.encodeWithSelector(DVN_TimestampOutOfRange.selector, dstEid, pinBlockNum)); + dvnFeeLib.getFee(params, config, cmd, ""); + + pinBlockNum = uint64(block.number) + 99; // set it out of reach + cmd = buildCmd(1, true, 2, true, uint64(block.number), true, pinBlockNum); + vm.expectRevert(abi.encodeWithSelector(DVN_TimestampOutOfRange.selector, dstEid, pinBlockNum)); + dvnFeeLib.getFee(params, config, cmd, ""); + } + + function test_success_with_valid_blockNum() public { + setBlock(1000, 1000); + setDVNFeeLibSupportedCmdTypes(dstEid, 7); // 1 + 2 + 4 + config = IDVN.DstConfig(gas, 0, 0); + + IDVNFeeLib.FeeParamsForRead memory params = IDVNFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + quorum, + defaultMultiplierBps + ); + + setFeeLibBlockConfig(dstEid, 500, 500, 90); // 90 sec retention + + // request block number out of reach + uint64 pinBlockNum = uint64(block.number) - 10; // set it within reach + bytes memory cmd = buildCmd(1, true, 2, true, pinBlockNum, true, pinBlockNum); + uint256 fee = dvnFeeLib.getFee(params, config, cmd, ""); + assertGt(fee, 0); + } + + // ---------------------------- Test Helpers ---------------------------- + function buildCmd( + uint256 requestNum, + bool hasComputeSetting, + uint8 computeSetting + ) internal view returns (bytes memory) { + return buildCmd(requestNum, hasComputeSetting, computeSetting, false, 0, false, 0); + } + + function buildCmd( + uint256 requestNum, + bool hasComputeSetting, + uint8 computeSetting, + bool requestIsBlockNum, + uint64 requestBlockNumOrTimestamp, + bool computeIsBlockNum, + uint64 computeBlockNumOrTimestamp + ) internal view returns (bytes memory) { + CmdUtil.EVMCallRequestV1[] memory requests = new CmdUtil.EVMCallRequestV1[](requestNum); + for (uint256 i = 0; i < requestNum; i++) { + CmdUtil.EVMCallRequestV1 memory request = CmdUtil.EVMCallRequestV1({ + targetEid: dstEid, + appRequestLabel: 0, + isBlockNum: requestIsBlockNum, + blockNumOrTimestamp: requestBlockNumOrTimestamp, + callData: new bytes(10), + confirmations: 0, + to: oapp + }); + requests[i] = request; + } + CmdUtil.EVMCallComputeV1 memory compute = CmdUtil.EVMCallComputeV1({ + computeSetting: computeSetting, + to: oapp, + isBlockNum: computeIsBlockNum, + blockNumOrTimestamp: computeBlockNumOrTimestamp, + confirmations: 0, + targetEid: hasComputeSetting ? dstEid : 0 + }); + return CmdUtil.encode(0, requests, compute); + } + + function setDVNFeeLibSupportedCmdTypes(uint32 _targetEid, uint256 _cmdTypes) public { + SetSupportedCmdTypesParam[] memory params = new SetSupportedCmdTypesParam[](1); + params[0] = SetSupportedCmdTypesParam(_targetEid, BitMap256.wrap(_cmdTypes)); + dvnFeeLib.setSupportedCmdTypes(params); + } + + function setBlock(uint64 _blockNum, uint64 _timestamp) public { + vm.roll(_blockNum); + vm.warp(_timestamp); + } + + function setFeeLibBlockConfig(uint32 _dstEid, uint64 _blockNum, uint64 _timestamp, uint32 _maxRetention) public { + uint32[] memory _dstEids = new uint32[](1); + _dstEids[0] = _dstEid; + DVNFeeLib.BlockTimeConfig[] memory _snapshots = new DVNFeeLib.BlockTimeConfig[](1); + _snapshots[0] = DVNFeeLib.BlockTimeConfig(1000, _blockNum, _timestamp, _maxRetention, _maxRetention); // 1 sec per block + dvnFeeLib.setDstBlockTimeConfigs(_dstEids, _snapshots); + } } diff --git a/packages/layerzero-v2/evm/messagelib/test/ExecutorFeeLib.t.sol b/packages/layerzero-v2/evm/messagelib/test/ExecutorFeeLib.t.sol index ab991e4..c71881d 100644 --- a/packages/layerzero-v2/evm/messagelib/test/ExecutorFeeLib.t.sol +++ b/packages/layerzero-v2/evm/messagelib/test/ExecutorFeeLib.t.sol @@ -15,6 +15,7 @@ contract ExecutorFeeLibTest is Test { PriceFeedMock priceFeed; IExecutor.DstConfig config; uint16 defaultMultiplierBps = 12000; + uint32 srcEid = 10000; uint32 dstEid = 30000; uint256 gasFee = 100; uint128 priceRatio = 1e10; @@ -34,13 +35,14 @@ contract ExecutorFeeLibTest is Test { uint8 internal constant OPTION_TYPE_NATIVE_DROP = 2; uint8 internal constant OPTION_TYPE_LZCOMPOSE = 3; uint8 internal constant OPTION_TYPE_ORDERED_EXECUTION = 4; - uint8 internal constant OPTION_TYPE_INVALID = 5; + uint8 internal constant OPTION_TYPE_LZ_READ = 5; + uint8 internal constant OPTION_TYPE_INVALID = 99; uint8 internal constant WORKER_ID = 1; function setUp() public { priceFeed = new PriceFeedMock(); - executorFeeLib = new ExecutorFeeLib(1e18); + executorFeeLib = new ExecutorFeeLib(srcEid, 1e18); priceFeed.setup(gasFee, priceRatio, nativePriceUSD); config = IExecutor.DstConfig(baseGas, multiplierBps, floorMarginUSD, nativeDropCap, 0); } @@ -120,6 +122,86 @@ contract ExecutorFeeLibTest is Test { assertEq(actual, expected); } + function test_getFee_lzReadOption_defaultMultiplier() public { + config = IExecutor.DstConfig(baseGas, 0, 0, nativeDropCap, 0); + uint256 dstFee = (dstAmount * priceRatio) / priceFeed.getPriceRatioDenominator(); + uint32 dataSize = 100; + + uint256 expected = ((gasFee + dstFee) * defaultMultiplierBps) / 10000; + IExecutorFeeLib.FeeParamsForRead memory params = IExecutorFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + defaultMultiplierBps + ); + bytes memory executorOption = abi.encodePacked(OPTION_TYPE_LZ_READ, dstGas, dataSize, dstAmount); + uint256 actual = executorFeeLib.getFee( + params, + config, + abi.encodePacked(WORKER_ID, uint16(executorOption.length), executorOption) + ); + + assertEq(actual, expected); + } + + function test_getFee_lzReadOption_specificMultiplier() public { + config = IExecutor.DstConfig(baseGas, multiplierBps, 0, nativeDropCap, 0); + uint256 dstFee = (dstAmount * priceRatio) / priceFeed.getPriceRatioDenominator(); + uint32 dataSize = 100; + + uint256 expected = ((gasFee + dstFee) * multiplierBps) / 10000; + IExecutorFeeLib.FeeParamsForRead memory params = IExecutorFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + multiplierBps + ); + bytes memory executorOption = abi.encodePacked(OPTION_TYPE_LZ_READ, dstGas, dataSize, dstAmount); + uint256 actual = executorFeeLib.getFee( + params, + config, + abi.encodePacked(WORKER_ID, uint16(executorOption.length), executorOption) + ); + + assertEq(actual, expected); + } + + function test_getFee_lzReadOption_dataSize_0_revert() public { + config = IExecutor.DstConfig(baseGas, 0, 0, nativeDropCap, 0); + uint32 dataSize = 0; + + vm.expectRevert(abi.encodeWithSelector(IExecutorFeeLib.Executor_ZeroCalldataSizeProvided.selector)); + + IExecutorFeeLib.FeeParamsForRead memory params = IExecutorFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + defaultMultiplierBps + ); + bytes memory executorOption = abi.encodePacked(OPTION_TYPE_LZ_READ, dstGas, dataSize, dstAmount); + executorFeeLib.getFee( + params, + config, + abi.encodePacked(WORKER_ID, uint16(executorOption.length), executorOption) + ); + } + + function test_getFee_lzReadOption_lzReceiveGas_0_revert() public { + config = IExecutor.DstConfig(baseGas, 0, 0, nativeDropCap, 0); + uint32 dataSize = 100; + + vm.expectRevert(abi.encodeWithSelector(IExecutorFeeLib.Executor_ZeroLzReceiveGasProvided.selector)); + + IExecutorFeeLib.FeeParamsForRead memory params = IExecutorFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + defaultMultiplierBps + ); + bytes memory executorOption = abi.encodePacked(OPTION_TYPE_LZ_READ, uint128(0), dataSize, dstAmount); + executorFeeLib.getFee( + params, + config, + abi.encodePacked(WORKER_ID, uint16(executorOption.length), executorOption) + ); + } + function test_getFee_lzComposeOption_floorMargin() public { uint256 floorMargin = (floorMarginUSD * 1e18) / priceFeed.nativeTokenPriceUSD(); uint256 dstFee = (dstAmount * priceRatio) / priceFeed.getPriceRatioDenominator(); @@ -178,7 +260,7 @@ contract ExecutorFeeLibTest is Test { assertEq(actual, expected); } - function test_getFee_nativeDropAmountExceedsCap_revert() public { + function test_getFee_nativeAmountExceedsCap_revert() public { uint128 nativeDropAmount = nativeDropCap + 1; vm.expectRevert( @@ -195,7 +277,7 @@ contract ExecutorFeeLibTest is Test { calldataSize, defaultMultiplierBps ); - bytes memory executorOption = abi.encodePacked(OPTION_TYPE_NATIVE_DROP, nativeDropAmount, nativeDropReceiver); + bytes memory executorOption = abi.encodePacked(OPTION_TYPE_LZRECEIVE, dstGas, nativeDropAmount); executorFeeLib.getFee( params, config, @@ -236,6 +318,59 @@ contract ExecutorFeeLibTest is Test { assertEq(actual, expected); } + function test_getFee_Read_UnsupportedOptionType_With_LzReceiveOption_revert() public { + IExecutorFeeLib.FeeParamsForRead memory params = IExecutorFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + defaultMultiplierBps + ); + vm.expectRevert( + abi.encodeWithSelector(IExecutorFeeLib.Executor_UnsupportedOptionType.selector, OPTION_TYPE_LZRECEIVE) // for read getFee, not allow lzReceive options + ); + bytes memory executorOption = abi.encodePacked(OPTION_TYPE_LZRECEIVE, dstGas, dstAmount); + executorFeeLib.getFee( + params, + config, + abi.encodePacked(WORKER_ID, uint16(executorOption.length), executorOption) + ); + } + + function test_getFee_Read_UnsupportedOptionType_With_NativeDropOptions_revert() public { + IExecutorFeeLib.FeeParamsForRead memory params = IExecutorFeeLib.FeeParamsForRead( + address(priceFeed), + oapp, + defaultMultiplierBps + ); + vm.expectRevert( + abi.encodeWithSelector(IExecutorFeeLib.Executor_UnsupportedOptionType.selector, OPTION_TYPE_NATIVE_DROP) // for read getFee, not allow native drop options + ); + bytes memory executorOption = abi.encodePacked(OPTION_TYPE_NATIVE_DROP, dstAmount, address(this)); + executorFeeLib.getFee( + params, + config, + abi.encodePacked(WORKER_ID, uint16(executorOption.length), executorOption) + ); + } + + function test_getFee_UnsupportedOptionType_ReadOptions_revert() public { + IExecutorFeeLib.FeeParams memory params = IExecutorFeeLib.FeeParams( + address(priceFeed), + 101, + oapp, + calldataSize, + defaultMultiplierBps + ); + vm.expectRevert( + abi.encodeWithSelector(IExecutorFeeLib.Executor_UnsupportedOptionType.selector, OPTION_TYPE_LZ_READ) // for cross-chain getFee, not allow read options + ); + bytes memory executorOption = abi.encodePacked(OPTION_TYPE_LZ_READ, dstGas, calldataSize, dstAmount); + executorFeeLib.getFee( + params, + config, + abi.encodePacked(WORKER_ID, uint16(executorOption.length), executorOption) + ); + } + function test_getFee_UnsupportedOptionType_EndpointV1_LzReceiveWithValue_revert() public { // LzReceive with value IExecutorFeeLib.FeeParams memory params = IExecutorFeeLib.FeeParams( @@ -298,7 +433,7 @@ contract ExecutorFeeLibTest is Test { assertEq(actual, expected); } - function test_getFeeOnSend_EndpoitnV1_LzReceive() public { + function test_getFeeOnSend_EndpointV1_LzReceive() public { // LzReceive IExecutorFeeLib.FeeParams memory params = IExecutorFeeLib.FeeParams( address(priceFeed), diff --git a/packages/layerzero-v2/evm/messagelib/test/MultiSig.t.sol b/packages/layerzero-v2/evm/messagelib/test/MultiSig.t.sol index d71e4a7..3fcd64d 100644 --- a/packages/layerzero-v2/evm/messagelib/test/MultiSig.t.sol +++ b/packages/layerzero-v2/evm/messagelib/test/MultiSig.t.sol @@ -16,18 +16,35 @@ contract MultiSigTest is MultiSig, Test { return signers; } + function test_getSigners() public { + address[] memory signers = this.getSigners(); + assertEq(signers[0], vm.addr(2)); + assertEq(signers[1], vm.addr(3)); + assertEq(signers.length, 2); + } + + function test_isSigner() public { + assertEq(isSigner(vm.addr(2)), true); + assertEq(isSigner(vm.addr(3)), true); + assertEq(isSigner(vm.addr(4)), false); + } + function test_setSigner() public { // only two signers - assertEq(signers[vm.addr(2)], true); - assertEq(signers[vm.addr(3)], true); - assertEq(signers[vm.addr(4)], false); - assertEq(signerSize, 2); + assertEq(signers(vm.addr(2)), true); + assertEq(signers(vm.addr(3)), true); + assertEq(signers(vm.addr(4)), false); + assertEq(signerSize(), 2); // add a new signer address newSigner = vm.addr(4); bool active = true; _setSigner(newSigner, active); - assertEq(signerSize, 3); + assertEq(signerSize(), 3); + + // cant add address(0) as a signer + vm.expectRevert(abi.encodeWithSelector(MultiSig_InvalidSigner.selector)); + _setSigner(address(0), active); // cant add a signer twice vm.expectRevert(abi.encodeWithSelector(MultiSig_StateAlreadySet.selector, newSigner, active)); @@ -35,7 +52,7 @@ contract MultiSigTest is MultiSig, Test { // remove a signer _setSigner(newSigner, !active); - assertEq(signerSize, 2); + assertEq(signerSize(), 2); // signer size must be >= quorum after removing a signer vm.expectRevert(abi.encodeWithSelector(MultiSig_SignersSizeIsLessThanQuorum.selector, uint64(1), uint64(2))); diff --git a/packages/layerzero-v2/evm/messagelib/test/ReceiveUln302View.t.sol b/packages/layerzero-v2/evm/messagelib/test/ReceiveUln302View.t.sol index 8a89684..0940eb1 100644 --- a/packages/layerzero-v2/evm/messagelib/test/ReceiveUln302View.t.sol +++ b/packages/layerzero-v2/evm/messagelib/test/ReceiveUln302View.t.sol @@ -27,6 +27,7 @@ contract ReceiveUln302ViewTest is Test { uint32 internal EID; bool internal initializable = true; + bool internal revertInitializable = false; function setUp() public { fixtureV2 = Setup.loadFixtureV2(Constant.EID_ETHEREUM); @@ -143,7 +144,60 @@ contract ReceiveUln302ViewTest is Test { assertEq(uint256(status), uint256(VerificationState.NotInitializable)); } + function test_Verifiable_NotInitializable_contract_doesnt_exist() public { + // wire to itself + Setup.wireFixtureV2WithRemote(fixtureV2, EID); + + Packet memory packet = PacketUtil.newPacket( + 1, + EID, + address(this), + EID, + address(0), + abi.encodePacked("message") + ); + bytes memory encodedPacket = PacketV1Codec.encode(packet); + bytes memory header = BytesLib.slice(encodedPacket, 0, 81); + bytes32 payloadHash = keccak256(BytesLib.slice(encodedPacket, 81, encodedPacket.length - 81)); + + // dvn sign + vm.prank(address(fixtureV2.dvn)); + receiveUln302.verify(header, payloadHash, 1); + + VerificationState status = receiveUln302View.verifiable(header, payloadHash); + assertEq(uint256(status), uint256(VerificationState.NotInitializable)); + } + + function test_Verifiable_NotInitializable_contract_revert() public { + // wire to itself + Setup.wireFixtureV2WithRemote(fixtureV2, EID); + + Packet memory packet = PacketUtil.newPacket( + 1, + EID, + address(this), + EID, + address(this), + abi.encodePacked("message") + ); + bytes memory encodedPacket = PacketV1Codec.encode(packet); + bytes memory header = BytesLib.slice(encodedPacket, 0, 81); + bytes32 payloadHash = keccak256(BytesLib.slice(encodedPacket, 81, encodedPacket.length - 81)); + + // dvn sign + vm.prank(address(fixtureV2.dvn)); + receiveUln302.verify(header, payloadHash, 1); + + // set app to revert initializable + revertInitializable = true; + + VerificationState status = receiveUln302View.verifiable(header, payloadHash); + assertEq(uint256(status), uint256(VerificationState.NotInitializable)); + } + function allowInitializePath(Origin calldata) external view returns (bool) { + require(!revertInitializable); + return initializable; } } diff --git a/packages/layerzero-v2/evm/messagelib/test/SendUln301.t.sol b/packages/layerzero-v2/evm/messagelib/test/SendUln301.t.sol index 35285ac..1342007 100644 --- a/packages/layerzero-v2/evm/messagelib/test/SendUln301.t.sol +++ b/packages/layerzero-v2/evm/messagelib/test/SendUln301.t.sol @@ -83,7 +83,7 @@ contract SendUln301Test is Test { uint256 mockExecutorFee = 2; vm.mockCall( address(fixtureV1.executor), - abi.encodeWithSelector(fixtureV1.executor.getFee.selector), + abi.encodeWithSignature("getFee(uint32,address,uint256,bytes)"), abi.encode(mockExecutorFee) ); diff --git a/packages/layerzero-v2/evm/messagelib/test/SendUln302.t.sol b/packages/layerzero-v2/evm/messagelib/test/SendUln302.t.sol index a26703d..3f14054 100644 --- a/packages/layerzero-v2/evm/messagelib/test/SendUln302.t.sol +++ b/packages/layerzero-v2/evm/messagelib/test/SendUln302.t.sol @@ -64,7 +64,7 @@ contract SendUln302Test is Test { uint256 mockExecutorFee = 2; vm.mockCall( address(fixtureV2.executor), - abi.encodeWithSelector(fixtureV2.executor.getFee.selector), + abi.encodeWithSignature("getFee(uint32,address,uint256,bytes)"), abi.encode(mockExecutorFee) ); diff --git a/packages/layerzero-v2/evm/messagelib/test/DVNOptions.t.sol b/packages/layerzero-v2/evm/messagelib/test/libtests/DVNOptions.t.sol similarity index 97% rename from packages/layerzero-v2/evm/messagelib/test/DVNOptions.t.sol rename to packages/layerzero-v2/evm/messagelib/test/libtests/DVNOptions.t.sol index 90f6f7c..a1bd765 100644 --- a/packages/layerzero-v2/evm/messagelib/test/DVNOptions.t.sol +++ b/packages/layerzero-v2/evm/messagelib/test/libtests/DVNOptions.t.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.0; import { Test } from "forge-std/Test.sol"; -import { DVNOptions } from "../contracts/uln/libs/DVNOptions.sol"; +import { DVNOptions } from "../../contracts/uln/libs/DVNOptions.sol"; -import { OptionsUtil } from "./util/OptionsUtil.sol"; +import { OptionsUtil } from "../util/OptionsUtil.sol"; contract DVNOptionsTest is Test { using OptionsUtil for bytes; diff --git a/packages/layerzero-v2/evm/messagelib/test/libtests/ExecutorOptions.t.sol b/packages/layerzero-v2/evm/messagelib/test/libtests/ExecutorOptions.t.sol new file mode 100644 index 0000000..9004ffd --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/test/libtests/ExecutorOptions.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; + +import { ExecutorOptions } from "../../contracts/libs/ExecutorOptions.sol"; + +contract ExecutorOptionsTest is Test { + function test_nextExecutorOption() public { + bytes memory option = ExecutorOptions.encodeLzReceiveOption(1, 2); + bytes memory options = abi.encodePacked( + ExecutorOptions.WORKER_ID, + uint16(option.length + 1), // option type + option length + ExecutorOptions.OPTION_TYPE_LZRECEIVE, + option + ); + + (uint8 optionType, bytes memory actualOption, uint256 cursor) = ExecutorOptionsWrapper.nextExecutorOption( + options, + 0 + ); + assertEq(optionType, ExecutorOptions.OPTION_TYPE_LZRECEIVE); + assertEq(actualOption, option); + assertEq(cursor, options.length); + } + + function test_lzReceiveOption() public { + bytes memory option = ExecutorOptions.encodeLzReceiveOption(1, 0); + assertEq(option.length, 16); + + (uint128 gas, uint128 value) = ExecutorOptionsWrapper.decodeLzReceiveOption(option); + assertEq(gas, 1); + assertEq(value, 0); + + option = ExecutorOptions.encodeLzReceiveOption(1, 2); + assertEq(option.length, 32); + + (gas, value) = ExecutorOptionsWrapper.decodeLzReceiveOption(option); + assertEq(gas, 1); + assertEq(value, 2); + } + + function test_lzReadOptions() public { + bytes memory option = ExecutorOptions.encodeLzReadOption(1, 2, 0); + assertEq(option.length, 20); + + (uint128 gas, uint32 calldataSize, uint128 value) = ExecutorOptionsWrapper.decodeLzReadOption(option); + assertEq(gas, 1); + assertEq(calldataSize, 2); + assertEq(value, 0); + + option = ExecutorOptions.encodeLzReadOption(1, 2, 3); + assertEq(option.length, 36); + + (gas, calldataSize, value) = ExecutorOptionsWrapper.decodeLzReadOption(option); + assertEq(gas, 1); + assertEq(calldataSize, 2); + assertEq(value, 3); + } + + function test_nativeDropOption() public { + bytes memory option = ExecutorOptions.encodeNativeDropOption(1, bytes32(uint256(0x1234))); + assertEq(option.length, 48); + + (uint128 value, bytes32 receiver) = ExecutorOptionsWrapper.decodeNativeDropOption(option); + assertEq(value, 1); + assertEq(receiver, bytes32(uint256(0x1234))); + } + + function test_lzComposeOption() public { + bytes memory option = ExecutorOptions.encodeLzComposeOption(0, 1, 0); + assertEq(option.length, 18); + + (uint16 index, uint128 gas, uint128 value) = ExecutorOptionsWrapper.decodeLzComposeOption(option); + assertEq(index, 0); + assertEq(gas, 1); + assertEq(value, 0); + + option = ExecutorOptions.encodeLzComposeOption(0, 1, 2); + assertEq(option.length, 34); + + (index, gas, value) = ExecutorOptionsWrapper.decodeLzComposeOption(option); + assertEq(index, 0); + assertEq(gas, 1); + assertEq(value, 2); + } +} + +// Wrap the library functions inorder to get calldata bytes +library ExecutorOptionsWrapper { + function nextExecutorOption( + bytes calldata _options, + uint256 _cursor + ) external pure returns (uint8 optionType, bytes memory 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); + } + + function decodeLzReadOption( + bytes calldata _option + ) external pure returns (uint128 gas, uint32 calldataSize, uint128 value) { + return ExecutorOptions.decodeLzReadOption(_option); + } +} diff --git a/packages/layerzero-v2/evm/messagelib/test/libtests/ReadCmdCodecV1.t.sol b/packages/layerzero-v2/evm/messagelib/test/libtests/ReadCmdCodecV1.t.sol new file mode 100644 index 0000000..bbe6a81 --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/test/libtests/ReadCmdCodecV1.t.sol @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import { Test, console } from "forge-std/Test.sol"; + +import { ReadCmdCodecV1 } from "../../contracts/uln/libs/ReadCmdCodecV1.sol"; +import { SupportedCmdTypes, SupportedCmdTypesLib, BitMap256 } from "../../contracts/uln/libs/SupportedCmdTypes.sol"; + +contract ReadCmdCodecV1Test is Test { + uint32 internal supportedEid = 666; + ReadCodecV1Wrapper internal cmdCodec; + + uint16 internal constant CMD_VERSION = 1; + uint16 internal constant APP_LABEL = 0; + uint16 internal constant REQUEST_COUNT_1 = 1; + uint16 internal constant REQUEST_COUNT_2 = 2; + + uint8 internal constant REQUEST_VERSION = 1; + uint16 internal constant REQUEST_LABEL = 0; + uint16 internal constant REQUEST_RESOLVER_TYPE_EVM_CALL = 1; + bytes internal requestData = abi.encodePacked(uint8(1), uint64(100), "other request data"); // isBlockNum, blockNumOrTimestamp, other data + uint16 internal requestSize = uint16(requestData.length) + 4; // 4 bytes for targetEid + // request data + + uint8 internal constant COMPUTE_VERSION = 1; + uint16 internal constant COMPUTE_TYPE_EVM_CALL = 1; + uint8 internal constant COMPUTE_SETTING_MAP_ONLY = 0; + uint8 internal constant COMPUTE_SETTING_REDUCE_ONLY = 1; + uint8 internal constant COMPUTE_SETTING_MAP_AND_REDUCE = 2; + bytes internal computeData = new bytes(31); + // compute data + + function setUp() public { + cmdCodec = new ReadCodecV1Wrapper(supportedEid, 3); + } + + function test_revert_less_than_2_bytes() public { + bytes memory cmd = hex"00"; + vm.expectRevert(); + cmdCodec.decode(cmd); + } + + function test_revert_invalid_cmd_version() public { + bytes memory cmd = hex"dead"; + vm.expectRevert(ReadCmdCodecV1.InvalidVersion.selector); + cmdCodec.decode(cmd); + } + + function test_revert_invalid_req_type() public { + uint16 wrongRequestType = 666; + bytes memory cmd = abi.encodePacked(CMD_VERSION, APP_LABEL, REQUEST_COUNT_1, wrongRequestType); + vm.expectRevert(ReadCmdCodecV1.InvalidVersion.selector); + cmdCodec.decode(cmd); + } + + function test_revert_zero_request_count() public { + bytes memory cmd = abi.encodePacked(CMD_VERSION, APP_LABEL, uint16(0)); + vm.expectRevert(ReadCmdCodecV1.InvalidCmd.selector); + cmdCodec.decode(cmd); + } + + function test_revert_invalid_req_resolver_type() public { + uint16 wrongResolverType = 666; + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + wrongResolverType + ); + vm.expectRevert(ReadCmdCodecV1.InvalidType.selector); + cmdCodec.decode(cmd); + } + + function test_revert_unsupported_eid() public { + uint32 unsupportedEid = 2; + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + unsupportedEid, + requestData + ); + vm.expectRevert(SupportedCmdTypesLib.UnsupportedTargetEid.selector); + cmdCodec.decode(cmd); + } + + function test_revert_invalid_req_size() public { + uint16 exceedReqSize = requestSize + 1; + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + exceedReqSize, + supportedEid, + requestData + ); + vm.expectRevert(ReadCmdCodecV1.InvalidCmd.selector); + cmdCodec.decode(cmd); + } + + function test_revert_invalid_compute_version() public { + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData, + COMPUTE_VERSION + 666 + ); + vm.expectRevert(ReadCmdCodecV1.InvalidVersion.selector); + cmdCodec.decode(cmd); + } + + function test_revert_unsupported_compute_type() public { + uint16 wrongComputeType = 666; + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData, + COMPUTE_VERSION, + wrongComputeType + ); + vm.expectRevert(ReadCmdCodecV1.InvalidType.selector); + cmdCodec.decode(cmd); + } + + function test_revert_unsupported_compute_setting() public { + uint8 wrongComputeSetting = 222; + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData, + COMPUTE_VERSION, + COMPUTE_TYPE_EVM_CALL, + wrongComputeSetting + ); + vm.expectRevert(ReadCmdCodecV1.InvalidType.selector); + cmdCodec.decode(cmd); + } + + function test_revert_unsupported_compute_target_eid() public { + uint32 unsupportedEid = 2; + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData, + COMPUTE_VERSION, + COMPUTE_TYPE_EVM_CALL, + COMPUTE_SETTING_MAP_ONLY, + unsupportedEid, + computeData + ); + vm.expectRevert(SupportedCmdTypesLib.UnsupportedTargetEid.selector); + cmdCodec.decode(cmd); + } + + function test_revert_invalid_compute_data_size() public { + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData, + COMPUTE_VERSION, + COMPUTE_TYPE_EVM_CALL, + COMPUTE_SETTING_MAP_ONLY, + supportedEid, + new bytes(32) // more than 31 + ); + vm.expectRevert(ReadCmdCodecV1.InvalidCmd.selector); + cmdCodec.decode(cmd); + + cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData, + COMPUTE_VERSION, + COMPUTE_TYPE_EVM_CALL, + COMPUTE_SETTING_MAP_ONLY, + supportedEid, + new bytes(30) // less than 31 + ); + vm.expectRevert(ReadCmdCodecV1.InvalidCmd.selector); + cmdCodec.decode(cmd); + } + + function test_revert_unused_bytes_in_end() public { + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData, + COMPUTE_VERSION, + COMPUTE_TYPE_EVM_CALL, + COMPUTE_SETTING_MAP_ONLY, + supportedEid, + computeData, + hex"deadbeef" + ); + vm.expectRevert(ReadCmdCodecV1.InvalidCmd.selector); + cmdCodec.decode(cmd); + } + + function test_success_1_req_map_only() public { + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData, + COMPUTE_VERSION, + COMPUTE_TYPE_EVM_CALL, + COMPUTE_SETTING_MAP_ONLY, + supportedEid, + computeData + ); + ReadCmdCodecV1.Cmd memory ret = cmdCodec.decode(cmd); + assertEq(ret.numEvmCallRequestV1, 1); + assertEq(ret.evmCallComputeV1Map, true); + assertEq(ret.evmCallComputeV1Reduce, false); + } + + function test_success_1_req_reduce_only() public { + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData, + COMPUTE_VERSION, + COMPUTE_TYPE_EVM_CALL, + COMPUTE_SETTING_REDUCE_ONLY, + supportedEid, + computeData + ); + ReadCmdCodecV1.Cmd memory ret = cmdCodec.decode(cmd); + assertEq(ret.numEvmCallRequestV1, 1); + assertEq(ret.evmCallComputeV1Map, false); + assertEq(ret.evmCallComputeV1Reduce, true); + } + + function test_success_1_req_map_and_reduce() public { + bytes memory cmd = abi.encodePacked( + CMD_VERSION, + APP_LABEL, + REQUEST_COUNT_1, + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData, + COMPUTE_VERSION, + COMPUTE_TYPE_EVM_CALL, + COMPUTE_SETTING_MAP_AND_REDUCE, + supportedEid, + computeData + ); + ReadCmdCodecV1.Cmd memory ret = cmdCodec.decode(cmd); + assertEq(ret.numEvmCallRequestV1, 1); + assertEq(ret.evmCallComputeV1Map, true); + assertEq(ret.evmCallComputeV1Reduce, true); + } + + function test_success_2_req_map_reduce() public { + bytes memory cmd = abi.encodePacked(CMD_VERSION, APP_LABEL, REQUEST_COUNT_2); + cmd = abi.encodePacked( + cmd, + // request 1 + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData + ); + cmd = abi.encodePacked( + cmd, + // request 2 + REQUEST_VERSION, + REQUEST_LABEL, + REQUEST_RESOLVER_TYPE_EVM_CALL, + requestSize, + supportedEid, + requestData + ); + cmd = abi.encodePacked( + cmd, + // compute + COMPUTE_VERSION, + COMPUTE_TYPE_EVM_CALL, + COMPUTE_SETTING_MAP_AND_REDUCE, + supportedEid, + computeData + ); + ReadCmdCodecV1.Cmd memory ret = cmdCodec.decode(cmd); + assertEq(ret.numEvmCallRequestV1, 2); + assertEq(ret.evmCallComputeV1Map, true); + assertEq(ret.evmCallComputeV1Reduce, true); + } +} + +// Wrap the library functions inorder to get calldata bytes +contract ReadCodecV1Wrapper { + SupportedCmdTypes internal supportedCmdTypes; + + constructor(uint32 dstEid, uint256 bm) { + supportedCmdTypes.cmdTypes[dstEid] = BitMap256.wrap(bm); + } + + function decode(bytes calldata _cmd) external view returns (ReadCmdCodecV1.Cmd memory cmd) { + return ReadCmdCodecV1.decode(_cmd, _assertCmdTypeSupported); + } + + function _assertCmdTypeSupported( + uint32 _targetEid, + bool /*_isBlockNum*/, + uint64 /*_blockNumOrTimestamp*/, + uint8 _cmdType + ) internal view { + supportedCmdTypes.assertSupported(_targetEid, _cmdType); + } +} diff --git a/packages/layerzero-v2/evm/messagelib/test/UlnOptions.t.sol b/packages/layerzero-v2/evm/messagelib/test/libtests/UlnOptions.t.sol similarity index 97% rename from packages/layerzero-v2/evm/messagelib/test/UlnOptions.t.sol rename to packages/layerzero-v2/evm/messagelib/test/libtests/UlnOptions.t.sol index 2c9c16a..749a0af 100644 --- a/packages/layerzero-v2/evm/messagelib/test/UlnOptions.t.sol +++ b/packages/layerzero-v2/evm/messagelib/test/libtests/UlnOptions.t.sol @@ -7,9 +7,9 @@ import { BytesLib } from "solidity-bytes-utils/contracts/BytesLib.sol"; import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol"; -import { UlnOptions as UlnOptionsImpl } from "../contracts/uln/libs/UlnOptions.sol"; +import { UlnOptions as UlnOptionsImpl } from "../../contracts/uln/libs/UlnOptions.sol"; -import { OptionsUtil } from "./util/OptionsUtil.sol"; +import { OptionsUtil } from "../util/OptionsUtil.sol"; library UlnOptions { function decode( diff --git a/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLib102.E2E.t.sol b/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLib102.E2E.t.sol new file mode 100644 index 0000000..1ad524c --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLib102.E2E.t.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import { Test, console } from "forge-std/Test.sol"; + +import { EndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/EndpointV2.sol"; +import { MessagingParams, MessagingReceipt, MessagingFee, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { Packet } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import { SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; +import { PacketV1Codec } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; + +import { ReadLibConfig } from "../../contracts/uln/readlib/ReadLibBase.sol"; +import { ReadLib1002 } from "../../contracts/uln/readlib/ReadLib1002.sol"; +import { ExecuteParam } from "../../contracts/uln/dvn/DVN.sol"; + +import { SetupRead } from "../util/SetupRead.sol"; +import { PacketUtil } from "../util/Packet.sol"; +import { Constant } from "../util/Constant.sol"; +import { OptionsUtil } from "../util/OptionsUtil.sol"; + +contract ReadLib1002E2ETest is Test { + using OptionsUtil for bytes; + + uint32 internal constant CONFIG_TYPE_CMD_LID_CONFIG = 1; + + SetupRead.FixtureRead internal fixture; + ReadLib1002 internal readLib; + EndpointV2 internal endpointV2; + uint32 internal EID; + uint32 internal CID = 666; + + uint256 internal constant ALICE_KEY = 0x0ac101; + address internal ALICE; + + event PayloadSigned(address dvn, bytes header, uint256 confirmations, bytes32 proofHash); + event PacketSent(bytes encodedPayload, bytes options, address sendLibrary); + event PacketVerified(Origin origin, address receiver, bytes32 payloadHash); + + function setUp() public { + fixture = SetupRead.loadFixture(Constant.EID_ETHEREUM); + readLib = fixture.cmdLib; + endpointV2 = fixture.endpointV2; + EID = fixture.eid; + ALICE = vm.addr(ALICE_KEY); + } + + function test_Quote() public { + SetupRead.wireFixtureV2WithChannel(fixture, CID); + + bytes memory options = OptionsUtil.newOptions().addExecutorLzReadOption(200000, 100, 0); + + // mock dvn fee + uint256 mockDVNFee = 1; + uint256 mockOptionalDVNFee = 2; + + ReadLibConfig memory readLibConfig; + address dvn1 = address(0xd1); + address dvn2 = address(0xd2); + readLibConfig = ReadLibConfig(address(fixture.executor), 1, 1, 1, new address[](1), new address[](1)); + readLibConfig.requiredDVNs[0] = dvn1; // dvn 1 + readLibConfig.optionalDVNs[0] = dvn2; // dvn 2 + SetConfigParam[] memory cfParams = new SetConfigParam[](1); + cfParams[0] = SetConfigParam(CID, CONFIG_TYPE_CMD_LID_CONFIG, abi.encode(readLibConfig)); + + vm.prank(address(endpointV2)); + readLib.setConfig(address(fixture.oapp), cfParams); + + vm.mockCall( + address(dvn1), + abi.encodeWithSignature("getFee(address,bytes,bytes,bytes)"), + abi.encode(mockDVNFee) + ); + vm.mockCall( + address(dvn2), + abi.encodeWithSignature("getFee(address,bytes,bytes,bytes)"), + abi.encode(mockOptionalDVNFee) + ); + + // mock executor fee + uint256 mockExecutorFee = 3; + vm.mockCall( + address(fixture.executor), + abi.encodeWithSignature("getFee(address,bytes)"), + abi.encode(mockExecutorFee) + ); + + // mock treasury fee + uint256 mockTreasuryFee = 4; + vm.mockCall( + address(fixture.treasury), + abi.encodeWithSelector(fixture.treasury.getFee.selector), + abi.encode(mockTreasuryFee) + ); + + MessagingFee memory quoteFee = fixture.oapp.quote(CID, false, options); + assertEq(quoteFee.nativeFee, 1 + 2 + 3 + 4); + assertEq(quoteFee.lzTokenFee, 0); + } + + function test_Send() public { + // wire to itself + SetupRead.wireFixtureV2WithChannel(fixture, CID); + + Packet memory packetSent = PacketUtil.newPacket( + 1, + EID, + address(fixture.oapp), + CID, + address(fixture.oapp), + fixture.oapp.cmd() + ); + bytes memory encodedPacket = PacketV1Codec.encode(packetSent); + bytes memory options = OptionsUtil.newOptions().addExecutorLzReadOption(200000, 100, 0); + + MessagingFee memory quoteFee = fixture.oapp.quote(CID, false, options); + require(quoteFee.nativeFee > 0, "quoteFee.nativeFee must be greater than 0"); + require(quoteFee.lzTokenFee == 0, "quoteFee.lzTokenFee must be 0"); + + vm.expectEmit(true, true, true, true, address(endpointV2)); + emit PacketSent(encodedPacket, options, address(fixture.cmdLib)); + + vm.deal(ALICE, quoteFee.nativeFee); + vm.prank(ALICE); + MessagingReceipt memory receipt = fixture.oapp.send{ value: quoteFee.nativeFee }(CID, false, options); + assertEq(receipt.nonce, 1); + assertEq(receipt.fee.nativeFee, quoteFee.nativeFee); + assertEq(receipt.fee.lzTokenFee, quoteFee.lzTokenFee); + } + + function test_Send_LzTokenFee() public { + // wire to itself + SetupRead.wireFixtureV2WithChannel(fixture, CID); + + Packet memory packetSent = PacketUtil.newPacket( + 1, + EID, + address(fixture.oapp), + CID, + address(fixture.oapp), + fixture.oapp.cmd() + ); + bytes memory encodedPacket = PacketV1Codec.encode(packetSent); + bytes memory options = OptionsUtil.newOptions().addExecutorLzReadOption(200000, 100, 0); + + MessagingFee memory quoteFee = fixture.oapp.quote(CID, true, options); + require(quoteFee.nativeFee > 0, "quoteFee.nativeFee must be greater than 0"); + require(quoteFee.lzTokenFee > 0, "LzTokenFee should be greater than 0"); + + // pay lzTokenFee + fixture.lzToken.transfer(address(endpointV2), quoteFee.lzTokenFee); + + vm.expectEmit(true, true, true, true, address(endpointV2)); + emit PacketSent(encodedPacket, options, address(fixture.cmdLib)); + + vm.deal(ALICE, quoteFee.nativeFee); + vm.prank(ALICE); + MessagingReceipt memory receipt = fixture.oapp.send{ value: quoteFee.nativeFee }(CID, true, options); + assertEq(receipt.nonce, 1); + assertEq(receipt.fee.nativeFee, quoteFee.nativeFee); + assertEq(receipt.fee.lzTokenFee, quoteFee.lzTokenFee); + } + + function test_receive() public { + // wire to itself + SetupRead.wireFixtureV2WithChannel(fixture, CID); + + // send packet + bytes memory options = OptionsUtil.newOptions().addExecutorLzReadOption(200000, 100, 0); + MessagingFee memory quoteFee = fixture.oapp.quote(CID, false, options); + fixture.oapp.send{ value: quoteFee.nativeFee }(CID, false, options); + + Packet memory packetSent = PacketUtil.newPacket( + 1, + EID, + address(fixture.oapp), + CID, + address(fixture.oapp), + fixture.oapp.cmd() + ); + // flip the packet eid + uint32 srcEid = packetSent.srcEid; + packetSent.srcEid = packetSent.dstEid; + packetSent.dstEid = srcEid; + // dvn verify + bytes memory resolvedMessage = "resolved message"; + verifyAndCommit(packetSent, resolvedMessage, fixture); + + Origin memory origin = PacketUtil.getOrigin(packetSent); + address receiver = address(uint160(uint256(packetSent.receiver))); + bytes32 guid = packetSent.guid; + endpointV2.lzReceive(origin, receiver, guid, resolvedMessage, ""); + + assertEq(fixture.oapp.ack(), 1); + } + + // -------------- helper functions -------------- + function verifyAndCommit( + Packet memory _packet, + bytes memory _resolvedMessage, + SetupRead.FixtureRead memory _fixture + ) internal { + bytes memory packetHeader = PacketV1Codec.encodePacketHeader(_packet); + bytes32 cmdHash = keccak256(_fixture.oapp.cmd()); + bytes32 payloadHash = keccak256(abi.encodePacked(_packet.guid, _resolvedMessage)); + bytes memory verifyCallData = abi.encodeWithSelector( + ReadLib1002.verify.selector, + packetHeader, + cmdHash, + payloadHash + ); + vm.prank(address(_fixture.dvn)); + _fixture.dvn.setSigner(ALICE, true); + bytes32 callDataHash = _fixture.dvn.hashCallData( + _fixture.eid % 30000, + address(readLib), + verifyCallData, + block.timestamp + 500 + ); + bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", callDataHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ALICE_KEY, ethSignedMessageHash); + bytes memory signature = abi.encodePacked(r, s, v); + + ExecuteParam[] memory executeParams = new ExecuteParam[](1); + executeParams[0] = ExecuteParam( + _fixture.eid % 30000, + address(readLib), + verifyCallData, + block.timestamp + 500, + signature + ); + _fixture.dvn.execute(executeParams); + + // commit verification + _fixture.cmdLib.commitVerification(packetHeader, cmdHash, payloadHash); + } +} diff --git a/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLib102.Treasury.t.sol b/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLib102.Treasury.t.sol new file mode 100644 index 0000000..ac32885 --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLib102.Treasury.t.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import { EndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/EndpointV2.sol"; + +import { ReadLib1002 } from "../../contracts/uln/readlib/ReadLib1002.sol"; +import { Treasury } from "../../contracts/Treasury.sol"; + +contract ReadLib1002TreasuryTest is Test, ReadLib1002 { + address internal constant ALICE = address(0xaaaa); + address internal constant MALICIOUS = address(0xabcd); + + address internal constant TREASURY_DEFAULT = address(0x111111); + + constructor() ReadLib1002(address(new EndpointV2(1, address(this))), 10000, 20000) { + treasury = TREASURY_DEFAULT; + } + + function test_setTreasury() public { + address newTreasury = address(0x1234); + vm.prank(address(MALICIOUS)); + vm.expectRevert("Ownable: caller is not the owner"); + this.setTreasury(newTreasury); + + vm.prank(owner()); + this.setTreasury(newTreasury); + + (address treasury_, ) = this.getTreasuryAndNativeFeeCap(); + assertEq(treasury_, newTreasury); + } + + function test_setTreasuryNativeFeeCap() public { + (, uint256 feeCap) = this.getTreasuryAndNativeFeeCap(); + + uint256 newNativeFeeCap = 100; + vm.prank(address(MALICIOUS)); + vm.expectRevert("Ownable: caller is not the owner"); + this.setTreasuryNativeFeeCap(newNativeFeeCap); + + // bigger than cap + newNativeFeeCap = feeCap + 1; + vm.expectRevert(abi.encodeWithSelector(LZ_RL_InvalidAmount.selector, newNativeFeeCap, feeCap)); + vm.prank(owner()); + this.setTreasuryNativeFeeCap(newNativeFeeCap); + + // smaller than cap + newNativeFeeCap = feeCap - 1; + vm.prank(owner()); + this.setTreasuryNativeFeeCap(newNativeFeeCap); + + (, uint256 nativeFeeCap) = this.getTreasuryAndNativeFeeCap(); + assertEq(nativeFeeCap, newNativeFeeCap); + } + + function test_quote_no_treasury() public { + vm.prank(owner()); + this.setTreasury(address(0)); + + (uint256 nativeFee, uint256 lzFee) = this.quoteTreasury(ALICE, 1, 10000, false); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + + (nativeFee, lzFee) = this.quoteTreasury(ALICE, 1, 10000, true); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + } + + function test_quote_treasury_return_unknown_data() public { + vm.mockCall(treasury, abi.encodeWithSelector(Treasury.getFee.selector), hex"dead"); + + uint256 totalNativeFee = 10000; + (uint256 nativeFee, uint256 lzFee) = this.quoteTreasury(ALICE, 1, totalNativeFee, false); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + + (nativeFee, lzFee) = this.quoteTreasury(ALICE, 1, totalNativeFee, true); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + } + + function test_quote_treasury_revert() public { + vm.mockCallRevert(treasury, abi.encodeWithSelector(Treasury.getFee.selector), abi.encode("revert")); + + uint256 totalNativeFee = 10000; + (uint256 nativeFee, uint256 lzFee) = this.quoteTreasury(ALICE, 1, totalNativeFee, false); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + + (nativeFee, lzFee) = this.quoteTreasury(ALICE, 1, totalNativeFee, true); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + } + + function test_quote_treasury_return_big_fee() public { + uint256 totalNativeFee = 10000; + uint256 maxFee = totalNativeFee > treasuryNativeFeeCap ? totalNativeFee : treasuryNativeFeeCap; + + // bigger than totalNativeFee or treasuryNativeFeeCap + vm.mockCall(treasury, abi.encodeWithSelector(Treasury.getFee.selector), abi.encode(maxFee + 1)); + (uint256 nativeFee, uint256 lzFee) = this.quoteTreasury(ALICE, 1, totalNativeFee, false); + assertEq(nativeFee, maxFee); + assertEq(lzFee, 0); + + // for lzFee it can be bigger than maxFee + vm.mockCall(treasury, abi.encodeWithSelector(Treasury.getFee.selector), abi.encode(maxFee + 1)); + (nativeFee, lzFee) = this.quoteTreasury(ALICE, 1, totalNativeFee, true); + assertEq(nativeFee, 0); + assertEq(lzFee, maxFee + 1); + } + + function test_quote_success_treasury() public { + uint256 totalNativeFee = 10000; + uint256 treasuryFee = 100; + vm.mockCall(treasury, abi.encodeWithSelector(Treasury.getFee.selector), abi.encode(treasuryFee)); + + (uint256 nativeFee, uint256 lzFee) = this.quoteTreasury(ALICE, 1, totalNativeFee, false); + assertEq(nativeFee, treasuryFee); + assertEq(lzFee, 0); + + (nativeFee, lzFee) = this.quoteTreasury(ALICE, 1, totalNativeFee, true); + assertEq(nativeFee, 0); + assertEq(lzFee, treasuryFee); + } + + function test_pay_no_treasury() public { + vm.prank(owner()); + this.setTreasury(address(0)); + + (uint256 nativeFee, uint256 lzFee) = this.payTreasury(ALICE, 1, 10000, false); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + assertEq(fees[treasury], 0); + + (nativeFee, lzFee) = this.payTreasury(ALICE, 1, 10000, true); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + } + + function test_pay_treasury_return_unknown_data() public { + vm.mockCall(treasury, abi.encodeWithSelector(Treasury.payFee.selector), hex"dead"); + + uint256 totalNativeFee = 10000; + (uint256 nativeFee, uint256 lzFee) = this.payTreasury(ALICE, 1, totalNativeFee, false); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + assertEq(fees[treasury], 0); + + (nativeFee, lzFee) = this.payTreasury(ALICE, 1, totalNativeFee, true); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + } + + function test_pay_treasury_revert() public { + vm.mockCallRevert(treasury, abi.encodeWithSelector(Treasury.payFee.selector), abi.encode("revert")); + + uint256 totalNativeFee = 10000; + (uint256 nativeFee, uint256 lzFee) = this.payTreasury(ALICE, 1, totalNativeFee, false); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + assertEq(fees[treasury], 0); + + (nativeFee, lzFee) = this.payTreasury(ALICE, 1, totalNativeFee, true); + assertEq(nativeFee, 0); + assertEq(lzFee, 0); + } + + function test_pay_treasury_return_big_fee() public { + uint256 totalNativeFee = 10000; + uint256 maxFee = totalNativeFee > treasuryNativeFeeCap ? totalNativeFee : treasuryNativeFeeCap; + + // bigger than totalNativeFee or treasuryNativeFeeCap + vm.mockCall(treasury, abi.encodeWithSelector(Treasury.payFee.selector), abi.encode(maxFee + 1)); + (uint256 nativeFee, uint256 lzFee) = this.payTreasury(ALICE, 1, totalNativeFee, false); + assertEq(nativeFee, maxFee); + assertEq(lzFee, 0); + assertEq(fees[treasury], maxFee); + + // for lzFee it can be bigger than maxFee + vm.mockCall(treasury, abi.encodeWithSelector(Treasury.payFee.selector), abi.encode(maxFee + 1)); + (nativeFee, lzFee) = this.payTreasury(ALICE, 1, totalNativeFee, true); + assertEq(nativeFee, 0); + assertEq(lzFee, maxFee + 1); + } + + function test_success_pay_treasury() public { + uint256 totalNativeFee = 10000; + uint256 treasuryFee = 100; + vm.mockCall(treasury, abi.encodeWithSelector(Treasury.payFee.selector), abi.encode(treasuryFee)); + + (uint256 nativeFee, uint256 lzFee) = this.payTreasury(ALICE, 1, totalNativeFee, false); + assertEq(nativeFee, treasuryFee); + assertEq(lzFee, 0); + assertEq(fees[treasury], treasuryFee); + + (nativeFee, lzFee) = this.payTreasury(ALICE, 1, totalNativeFee, true); + assertEq(nativeFee, 0); + assertEq(lzFee, treasuryFee); + } + + function test_withdraw_lzToken() public { + address lzToken = address(0x1234); + + // revert if withdraw native alt token + // mock endpoint.lzToken() + vm.mockCall(address(endpoint), abi.encodeWithSelector(EndpointV2.nativeToken.selector), abi.encode(lzToken)); + vm.expectRevert(abi.encodeWithSelector(LZ_RL_CannotWithdrawAltToken.selector)); + vm.prank(treasury); + this.withdrawLzTokenFee(lzToken, treasury, 100); + + // clear mock + vm.clearMockedCalls(); + + // mock lzToken transfer + vm.mockCall(lzToken, abi.encodeWithSelector(ERC20.transfer.selector), abi.encode(true)); + + vm.prank(treasury); + vm.expectEmit(true, true, true, true, address(this)); + emit LzTokenFeeWithdrawn(lzToken, treasury, 100); + this.withdrawLzTokenFee(lzToken, treasury, 100); + } + + function test_revert_withdraw_lzToken_not_treasury() public { + address lzToken = address(0x1234); + vm.expectRevert(abi.encodeWithSelector(LZ_RL_NotTreasury.selector)); + vm.prank(MALICIOUS); + this.withdrawLzTokenFee(lzToken, MALICIOUS, 100); + } + + function test_revert_withdraw_lzToken_nativeToken() public { + address lzToken = address(0x1234); + // mock endpoint nativeToken is lzToken + vm.mockCall(address(endpoint), abi.encodeWithSelector(EndpointV2.nativeToken.selector), abi.encode(lzToken)); + + vm.expectRevert(abi.encodeWithSelector(LZ_RL_CannotWithdrawAltToken.selector)); + vm.prank(treasury); + this.withdrawLzTokenFee(lzToken, treasury, 100); + } + + // ---- expose internal functions for testing ---- + function quoteTreasury( + address _sender, + uint32 _dstEid, + uint256 _totalNativeFee, + bool _payInLzToken + ) public view returns (uint256 nativeFee, uint256 lzTokenFee) { + return _quoteTreasury(_sender, _dstEid, _totalNativeFee, _payInLzToken); + } + + function payTreasury( + address _sender, + uint32 _dstEid, + uint256 _totalNativeFee, + bool _payInLzToken + ) public returns (uint256 treasuryNativeFee, uint256 lzTokenFee) { + return _payTreasury(_sender, _dstEid, _totalNativeFee, _payInLzToken); + } +} diff --git a/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLib102.t.sol b/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLib102.t.sol new file mode 100644 index 0000000..2ad740d --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLib102.t.sol @@ -0,0 +1,542 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; + +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import { EndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/EndpointV2.sol"; +import { PacketV1Codec } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import { MessagingFee } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; +import { IMessageLib, MessageLibType } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLib.sol"; +import { ISendLib, Packet } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; + +import { ReadLibConfig, SetDefaultReadLibConfigParam } from "../../contracts/uln/readlib/ReadLibBase.sol"; +import { ReadLib1002 } from "../../contracts/uln/readlib/ReadLib1002.sol"; +import { Treasury } from "../../contracts/Treasury.sol"; +import { OptionsUtil } from "../util/OptionsUtil.sol"; + +contract CmdLibTest is Test, ReadLib1002 { + using OptionsUtil for bytes; + + address internal constant MALICIOUS = address(0xabcd); + + constructor() ReadLib1002(address(new EndpointV2(1, address(this))), 10000, 20000) {} + + function test_withdraw_native() public { + address worker = address(0xaaaa); + fees[worker] = 100; // set fee to WORKER + + // fail if withdraw too much + vm.expectRevert(abi.encodeWithSelector(LZ_RL_InvalidAmount.selector, 101, 100)); + vm.prank(worker); + this.withdrawFee(worker, 101); + + uint256 balanceBefore = address(worker).balance; + vm.prank(worker); + this.withdrawFee(worker, 100); + uint256 balanceAfter = address(worker).balance; + assertEq(balanceAfter - balanceBefore, 100); + + assertEq(fees[worker], 0); + } + + function test_withdraw_native_alt() public { + address worker = address(0xaaaa); + fees[worker] = 100; // set fee to WORKER + + // mock endpoint.nativeToken() + vm.mockCall( + address(endpoint), + abi.encodeWithSelector(EndpointV2.nativeToken.selector), + abi.encode(address(0x1234)) + ); + // mock token transfer + vm.mockCall(address(0x1234), abi.encodeWithSelector(ERC20.transfer.selector), abi.encode(true)); + + // fail if withdraw too much + vm.expectRevert(abi.encodeWithSelector(LZ_RL_InvalidAmount.selector, 101, 100)); + vm.prank(worker); + this.withdrawFee(worker, 101); + + vm.prank(worker); + this.withdrawFee(worker, 100); + + fees[worker] = 0; + } + + function test_supportsInterface() public { + assertEq(this.supportsInterface(type(IMessageLib).interfaceId), true); + assertEq(this.supportsInterface(type(ISendLib).interfaceId), true); + assertEq(this.supportsInterface(type(IERC165).interfaceId), true); + } + + function test_success_setConfig_getConfig() public { + uint32 dstEid = 100; + + // setDefaultReadLibConfigs + ReadLibConfig memory readConfig = buildReadLibConfig(address(0xdd)); + setDefaultReadLibConfigs(dstEid, readConfig); // set default config + + address oapp = address(0xaaaa); + + readConfig = buildReadLibConfig(address(0xdd22)); + SetConfigParam[] memory params = buildSetConfigParam(dstEid, CONFIG_TYPE_READ_LID_CONFIG, readConfig); + + vm.expectEmit(true, true, true, true, address(this)); + emit ReadLibConfigSet(oapp, dstEid, readConfig); + vm.prank(endpoint); + this.setConfig(oapp, params); + + // check if config is set + bytes memory configData = this.getConfig(dstEid, oapp, CONFIG_TYPE_READ_LID_CONFIG); + ReadLibConfig memory newReadConfig = abi.decode(configData, (ReadLibConfig)); + assertEq(newReadConfig.executor, address(0xe111)); + assertEq(newReadConfig.requiredDVNs[0], address(0xdd22)); + assertEq(newReadConfig.requiredDVNs.length, 1); + assertEq(newReadConfig.requiredDVNCount, 1); + assertEq(newReadConfig.optionalDVNs.length, 0); + assertEq(newReadConfig.optionalDVNCount, 0); + assertEq(newReadConfig.optionalDVNs.length, 0); + } + + function test_revert_getConfig_unknown_configType() public { + uint32 dstEid = 100; + address oapp = address(0xaaaa); + uint32 unknownConfigType = 100; + + vm.expectRevert(abi.encodeWithSelector(LZ_RL_InvalidConfigType.selector, unknownConfigType)); + this.getConfig(dstEid, oapp, unknownConfigType); + } + + function test_revert_setConfig_not_supported_eid() public { + uint32 dstEid = 100; + ReadLibConfig memory readConfig = buildReadLibConfig(address(0xdd)); + + address oapp = address(0xaaaa); + SetConfigParam[] memory params = buildSetConfigParam(dstEid, CONFIG_TYPE_READ_LID_CONFIG, readConfig); + + vm.expectRevert(abi.encodeWithSelector(LZ_RL_UnsupportedEid.selector, dstEid)); + vm.prank(endpoint); + this.setConfig(oapp, params); + } + + function test_revert_only_endpoint_setConfig() public { + ReadLibConfig memory readConfig = buildReadLibConfig(address(0xdd)); + + address oapp = address(0xaaaa); + SetConfigParam[] memory params = buildSetConfigParam(100, CONFIG_TYPE_READ_LID_CONFIG, readConfig); + + vm.expectRevert(abi.encodeWithSelector(LZ_MessageLib_OnlyEndpoint.selector)); + this.setConfig(oapp, params); + } + + function test_revert_setConfig_invalid_configType() public { + uint32 dstEid = 100; + ReadLibConfig memory readConfig = buildReadLibConfig(address(0xdd)); + + setDefaultReadLibConfigs(dstEid, readConfig); // set default config + + address oapp = address(0xaaaa); + uint32 unknownConfigType = 100; + SetConfigParam[] memory params = buildSetConfigParam(dstEid, unknownConfigType, readConfig); + + vm.expectRevert(abi.encodeWithSelector(LZ_RL_InvalidConfigType.selector, unknownConfigType)); + vm.prank(endpoint); + this.setConfig(oapp, params); + } + + function test_isSupportedEid() public { + uint32 dstEid = 100; + assertEq(this.isSupportedEid(dstEid), false); + + setDefaultReadLibConfigs(dstEid, buildReadLibConfig(address(0xdd))); // set default config + + assertEq(this.isSupportedEid(dstEid), true); + } + + function test_messageLibType() public { + assertTrue(this.messageLibType() == MessageLibType.SendAndReceive); + } + + function test_Version() public { + (uint64 major, uint64 minor, uint64 endpointVersion) = this.version(); + assertEq(major, 10); + assertEq(minor, 0); + assertEq(endpointVersion, 2); + } + + function test_quoteDVNs() public { + address dvn = address(0xd1); + address optionalDvn = address(0xd2); + ReadLibConfig memory readConfig = buildReadLibConfig(dvn, optionalDvn); + // mock dvn.getFee + vm.mockCall(dvn, abi.encodeWithSignature("getFee(address,bytes,bytes,bytes)"), abi.encode(100)); + vm.mockCall(optionalDvn, abi.encodeWithSignature("getFee(address,bytes,bytes,bytes)"), abi.encode(200)); + + uint256 totalFee = this.quoteDVNs(readConfig, address(0), "", "", ""); + assertEq(totalFee, 300); + } + + function test_payDVNs() public { + Packet memory packet = newPacket(1, 1, 1, address(0xa111), "cmd"); + address dvn = address(0xd1); + address optionalDvn = address(0xd2); + ReadLibConfig memory readConfig = buildReadLibConfig(dvn, optionalDvn); + // mock dvn.assignJob + vm.mockCall(dvn, abi.encodeWithSignature("assignJob(address,bytes,bytes,bytes)"), abi.encode(100)); + vm.mockCall(optionalDvn, abi.encodeWithSignature("assignJob(address,bytes,bytes,bytes)"), abi.encode(200)); + + (uint256 totalFee, ) = this.payDVNs(readConfig, packet, ""); + assertEq(totalFee, 300); + assertEq(fees[dvn], 100); + assertEq(fees[optionalDvn], 200); + } + + function test_payExecutor() public { + address executor = address(0xe111); + // mock executor.getFee + vm.mockCall(executor, abi.encodeWithSignature("assignJob(address,bytes)"), abi.encode(100)); + + uint256 executorFee = this.payExecutor(executor, address(0), ""); + assertEq(executorFee, 100); + assertEq(fees[executor], 100); + } + + function test_quote() public { + uint32 cid = 1; + // setup default config + address dvn = address(0xd1); + address optionalDvn = address(0xd2); + ReadLibConfig memory readConfig = buildReadLibConfig(dvn, optionalDvn); + // mock dvn.getFee + vm.mockCall(dvn, abi.encodeWithSignature("getFee(address,bytes,bytes,bytes)"), abi.encode(100)); + vm.mockCall(optionalDvn, abi.encodeWithSignature("getFee(address,bytes,bytes,bytes)"), abi.encode(200)); + vm.mockCall(readConfig.executor, abi.encodeWithSignature("getFee(address,bytes)"), abi.encode(300)); + setDefaultReadLibConfigs(cid, readConfig); + + treasury = address(0xe222); + // mock treasury.getFee + vm.mockCall(treasury, abi.encodeWithSelector(Treasury.getFee.selector), abi.encode(400)); + + Packet memory packet = newPacket(1, 1, cid, address(0xa111), "cmd"); + bytes memory options = OptionsUtil.newOptions().addExecutorLzReadOption(200000, 100, 0); + MessagingFee memory msgFee = this.quote(packet, options, false); + assertEq(msgFee.nativeFee, 100 + 200 + 300 + 400); + assertEq(msgFee.lzTokenFee, 0); + + // quote with lzToken + msgFee = this.quote(packet, options, true); + assertEq(msgFee.nativeFee, 100 + 200 + 300); + assertEq(msgFee.lzTokenFee, 400); + } + + function test_send() public { + uint32 cid = 1; + // setup default config + address dvn = address(0xd1); + address optionalDvn = address(0xd2); + ReadLibConfig memory readConfig = buildReadLibConfig(dvn, optionalDvn); + // mock dvn.getFee + vm.mockCall(dvn, abi.encodeWithSignature("assignJob(address,bytes,bytes,bytes)"), abi.encode(100)); + vm.mockCall(optionalDvn, abi.encodeWithSignature("assignJob(address,bytes,bytes,bytes)"), abi.encode(200)); + vm.mockCall(readConfig.executor, abi.encodeWithSignature("assignJob(address,bytes)"), abi.encode(300)); + setDefaultReadLibConfigs(cid, readConfig); + + treasury = address(0xe222); + // mock treasury.payFee + vm.mockCall(treasury, abi.encodeWithSelector(Treasury.payFee.selector), abi.encode(400)); + + Packet memory packet = newPacket(1, 1, cid, address(0xa111), "cmd"); + bytes memory options = OptionsUtil.newOptions().addExecutorLzReadOption(200000, 100, 0); + vm.prank(endpoint); + (MessagingFee memory msgFee, ) = this.send(packet, options, false); + assertEq(msgFee.nativeFee, 100 + 200 + 300 + 400); + assertEq(msgFee.lzTokenFee, 0); + + // send with lzToken + vm.prank(endpoint); + (msgFee, ) = this.send(packet, options, true); + assertEq(msgFee.nativeFee, 100 + 200 + 300); + assertEq(msgFee.lzTokenFee, 400); + + // check cmdHashLookup + assertEq(cmdHashLookup[packet.sender][cid][packet.nonce], keccak256(packet.message)); + } + + function test_revert_send_not_endpoint() public { + Packet memory packet = newPacket(1, 1, 1, address(0xa111), "cmd"); + bytes memory options = OptionsUtil.newOptions().addExecutorLzReadOption(200000, 100, 0); + vm.expectRevert(abi.encodeWithSelector(LZ_MessageLib_OnlyEndpoint.selector)); + + vm.prank(MALICIOUS); + this.send(packet, options, false); + } + + function test_revert_send_invalid_receiver() public { + Packet memory packet = newPacket(1, 1, 1, address(0xa111), "cmd"); + packet.receiver = bytes32(uint256(999)); // receiver != sender + bytes memory options = OptionsUtil.newOptions().addExecutorLzReadOption(200000, 100, 0); + vm.expectRevert(abi.encodeWithSelector(LZ_RL_InvalidReceiver.selector)); + + vm.prank(endpoint); + this.send(packet, options, false); + } + + function test_verify() public { + bytes memory header = "header"; + bytes32 cmdHash = keccak256("cmd"); + bytes32 payloadHash = keccak256("payload"); + vm.expectEmit(true, true, true, true, address(this)); + vm.prank(address(0xd1)); + emit PayloadVerified(address(0xd1), header, cmdHash, payloadHash); + this.verify(header, cmdHash, payloadHash); + } + + function test_checkVerifiable_only_required_dvn() public { + bytes32 headerHash = keccak256("header"); + bytes32 cmdHash = keccak256("cmd"); + bytes32 payloadHash = keccak256("payload"); + + ReadLibConfig memory readConfig = buildReadLibConfig(address(0xd1)); + // no one signed + bool verified = this.verifiable(readConfig, headerHash, cmdHash, payloadHash); + assertEq(verified, false); + + // the required dvn signed + hashLookup[headerHash][cmdHash][readConfig.requiredDVNs[0]] = payloadHash; + + verified = this.verifiable(readConfig, headerHash, cmdHash, payloadHash); + assertEq(verified, true); + } + + function test_checkVerifiable_optional_dvn_2_threshold() public { + bytes32 headerHash = keccak256("header"); + bytes32 cmdHash = keccak256("cmd"); + bytes32 payloadHash = keccak256("payload"); + + address[] memory dvns = new address[](2); + dvns[0] = address(0xd1); + dvns[1] = address(0xd2); + ReadLibConfig memory readConfig = ReadLibConfig(address(0xe111), 0, 2, 2, new address[](0), dvns); // 2 threshold + // the optional dvn1 signed + hashLookup[headerHash][cmdHash][dvns[0]] = payloadHash; + + bool verified = this.verifiable(readConfig, headerHash, cmdHash, payloadHash); + assertEq(verified, false); // still not enough + + // the optional dvn2 signed + hashLookup[headerHash][cmdHash][dvns[1]] = payloadHash; + + verified = this.verifiable(readConfig, headerHash, cmdHash, payloadHash); + assertEq(verified, true); + } + + function test_checkVerifiable_optional_dvn_1_threshold() public { + bytes32 headerHash = keccak256("header"); + bytes32 cmdHash = keccak256("cmd"); + bytes32 payloadHash = keccak256("payload"); + + address[] memory dvns = new address[](2); + dvns[0] = address(0xd1); + dvns[1] = address(0xd2); + ReadLibConfig memory readConfig = ReadLibConfig(address(0xe111), 0, 2, 1, new address[](0), dvns); // 1 threshold + // the optional dvn1 signed + hashLookup[headerHash][cmdHash][dvns[0]] = payloadHash; + + bool verified = this.verifiable(readConfig, headerHash, cmdHash, payloadHash); + assertEq(verified, true); // enough + + // the optional dvn2 signed + hashLookup[headerHash][cmdHash][dvns[1]] = payloadHash; + + verified = this.verifiable(readConfig, headerHash, cmdHash, payloadHash); + assertEq(verified, true); + } + + function test_checkVerifiable_with_required_and_optional() public { + bytes32 headerHash = keccak256("header"); + bytes32 cmdHash = keccak256("cmd"); + bytes32 payloadHash = keccak256("payload"); + + address dvn1 = address(0xd1); + address dvn2 = address(0xd2); + ReadLibConfig memory readConfig = buildReadLibConfig(dvn1, dvn2); + // the required dvn1 signed + hashLookup[headerHash][cmdHash][dvn1] = payloadHash; + + bool verified = this.verifiable(readConfig, headerHash, cmdHash, payloadHash); + assertEq(verified, false); // not enough + + // the optional dvn2 signed + hashLookup[headerHash][cmdHash][dvn2] = payloadHash; + + verified = this.verifiable(readConfig, headerHash, cmdHash, payloadHash); + assertEq(verified, true); + } + + function test_commitVerification() public { + uint32 cid = 666; + bytes memory cmd = "cmd"; + Packet memory packet = newPacket(1, cid, localEid, address(0xa111), cmd); // flip localEid and cid when receiving + bytes memory payload = PacketV1Codec.encodePayload(packet); + bytes memory header = PacketV1Codec.encodePacketHeader(packet); + bytes32 headerHash = keccak256(header); + bytes32 cmdHash = keccak256(cmd); + bytes32 payloadHash = keccak256(payload); + + // setup cmdHashLookup + cmdHashLookup[packet.sender][cid][packet.nonce] = cmdHash; + // mock endpoint.verify + vm.mockCall(address(endpoint), abi.encodeWithSelector(EndpointV2.verify.selector), abi.encode("")); + // set default config + ReadLibConfig memory readConfig = buildReadLibConfig(address(0xd1), address(0xd2)); + setDefaultReadLibConfigs(cid, readConfig); + + // verify by DVNs + vm.prank(address(0xd1)); + this.verify(header, cmdHash, payloadHash); + vm.prank(address(0xd2)); + this.verify(header, cmdHash, payloadHash); + // check hashLookup set + assertEq(hashLookup[headerHash][cmdHash][address(0xd1)], payloadHash); + assertEq(hashLookup[headerHash][cmdHash][address(0xd2)], payloadHash); + + this.commitVerification(header, cmdHash, payloadHash); + + // check hashLookup cleared + assertEq(hashLookup[headerHash][cmdHash][address(0xd1)], bytes32(0)); + assertEq(hashLookup[headerHash][cmdHash][address(0xd2)], bytes32(0)); + } + + function test_revert_commitVerification_invalid_header() public { + bytes32 cmdHash = keccak256("cmd"); + bytes32 payloadHash = keccak256("payload"); + + vm.expectRevert(abi.encodeWithSelector(LZ_RL_InvalidPacketHeader.selector)); + this.commitVerification("invalid header", cmdHash, payloadHash); + } + + function test_revert_commitVerification_invalid_packet_version() public { + bytes memory header = new bytes(81); + bytes32 cmdHash = keccak256("cmd"); + bytes32 payloadHash = keccak256("payload"); + + vm.expectRevert(abi.encodeWithSelector(LZ_RL_InvalidPacketVersion.selector)); + this.commitVerification(header, cmdHash, payloadHash); + } + + function test_revert_commitVerification_invalid_eid() public { + bytes memory cmd = "cmd"; + Packet memory packet = newPacket(1, 1, localEid + 666, address(0xa111), cmd); // invalid eid + bytes memory header = PacketV1Codec.encodePacketHeader(packet); + bytes32 cmdHash = keccak256(cmd); + bytes32 payloadHash = keccak256(PacketV1Codec.encodePayload(packet)); + + vm.expectRevert(abi.encodeWithSelector(LZ_RL_InvalidEid.selector)); + this.commitVerification(header, cmdHash, payloadHash); + } + + function test_revert_commitVerification_invalid_cmdHash() public { + bytes memory cmd = "cmd"; + Packet memory packet = newPacket(1, 1, localEid, address(0xa111), cmd); + bytes memory header = PacketV1Codec.encodePacketHeader(packet); + bytes32 cmdHash = keccak256(cmd); + bytes32 payloadHash = keccak256(PacketV1Codec.encodePayload(packet)); + + vm.expectRevert(abi.encodeWithSelector(LZ_RL_InvalidCmdHash.selector)); + this.commitVerification(header, cmdHash, payloadHash); + } + + function test_revert_commitVerification_verifying() public { + bytes memory cmd = "cmd"; + Packet memory packet = newPacket(1, 1, localEid, address(0xa111), cmd); + bytes memory header = PacketV1Codec.encodePacketHeader(packet); + bytes32 cmdHash = keccak256(cmd); + bytes32 payloadHash = keccak256(PacketV1Codec.encodePayload(packet)); + + // setup cmdHashLookup + cmdHashLookup[packet.sender][1][packet.nonce] = cmdHash; + // set default cid config + ReadLibConfig memory readConfig = buildReadLibConfig(address(0xd1), address(0xd2)); + setDefaultReadLibConfigs(1, readConfig); + + vm.expectRevert(abi.encodeWithSelector(LZ_RL_Verifying.selector)); + this.commitVerification(header, cmdHash, payloadHash); + } + + // ----- helper functions ----- + function buildReadLibConfig(address dvn) internal pure returns (ReadLibConfig memory) { + address executor = address(0xe111); + address[] memory dvns = new address[](1); + dvns[0] = dvn; + return ReadLibConfig(executor, 1, 0, 0, dvns, new address[](0)); + } + + function buildReadLibConfig(address dvn, address optionalDvn) internal pure returns (ReadLibConfig memory) { + address executor = address(0xe111); + address[] memory dvns = new address[](1); + dvns[0] = dvn; + address[] memory optionalDvns = new address[](1); + optionalDvns[0] = optionalDvn; + return ReadLibConfig(executor, 1, 1, 1, dvns, optionalDvns); + } + + function buildSetConfigParam( + uint32 dstEid, + uint32 configType, + ReadLibConfig memory readConfig + ) internal pure returns (SetConfigParam[] memory) { + bytes memory configData = abi.encode(readConfig); + SetConfigParam[] memory params = new SetConfigParam[](1); + params[0] = SetConfigParam(dstEid, configType, configData); + return params; + } + + function setDefaultReadLibConfigs(uint32 dstEid, ReadLibConfig memory readConfig) internal { + SetDefaultReadLibConfigParam[] memory setDefaultParams = new SetDefaultReadLibConfigParam[](1); + setDefaultParams[0] = SetDefaultReadLibConfigParam(dstEid, readConfig); + vm.prank(owner()); + this.setDefaultReadLibConfigs(setDefaultParams); + } + + function newPacket( + uint64 _nonce, + uint32 _eid, + uint32 _cid, + address _oapp, + bytes memory _cmd + ) internal pure returns (Packet memory) { + return Packet(_nonce, _eid, _oapp, _cid, bytes32(uint256(uint160(_oapp))), bytes32(0), _cmd); + } + + // ----- wrap functions ----- + function quoteDVNs( + ReadLibConfig memory _config, + address _sender, + bytes memory _pckHeader, + bytes calldata _cmd, + bytes memory _options + ) public view returns (uint256) { + return _quoteDVNs(_config, _sender, _pckHeader, _cmd, _options); + } + + function payDVNs( + ReadLibConfig memory _config, + Packet calldata _packet, + bytes memory _options + ) public returns (uint256 totalFee, bytes memory encodedPacket) { + return _payDVNs(_config, _packet, _options); + } + + function payExecutor( + address _executor, + address _sender, + bytes memory _executorOptions + ) public returns (uint256 executorFee) { + return _payExecutor(_executor, _sender, _executorOptions); + } +} diff --git a/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLibBase.t.sol b/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLibBase.t.sol new file mode 100644 index 0000000..ac27d17 --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/test/readlib/ReadLibBase.t.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; + +import { ReadLibBase, ReadLibConfig, SetDefaultReadLibConfigParam } from "../../contracts/uln/readlib/ReadLibBase.sol"; +import { Constant } from "../util/Constant.sol"; + +contract ReadLibBaseTest is Test, ReadLibBase { + address private constant EXECUTOR = address(0x1); + uint32 private constant DST_EID = 1; + + function test_setInvalidDefaultReadLibConfig() public { + vm.startPrank(owner()); + SetDefaultReadLibConfigParam[] memory params = new SetDefaultReadLibConfigParam[](1); + + // nil dvns count + params[0] = SetDefaultReadLibConfigParam( + DST_EID, + ReadLibConfig( + EXECUTOR, + Constant.NIL_DVN_COUNT, + 0, + 0, + new address[](Constant.NIL_DVN_COUNT), + new address[](0) + ) + ); + vm.expectRevert(LZ_RL_InvalidRequiredDVNCount.selector); + this.setDefaultReadLibConfigs(params); + + // nil optional dvns count + params[0] = SetDefaultReadLibConfigParam( + DST_EID, + ReadLibConfig( + EXECUTOR, + 0, + Constant.NIL_DVN_COUNT, + 1, + new address[](0), + new address[](Constant.NIL_DVN_COUNT) + ) + ); + vm.expectRevert(LZ_RL_InvalidOptionalDVNCount.selector); + this.setDefaultReadLibConfigs(params); + + // no executor + params[0] = SetDefaultReadLibConfigParam( + DST_EID, + ReadLibConfig(address(0), 1, 0, 0, new address[](1), new address[](0)) + ); + vm.expectRevert(LZ_RL_InvalidExecutor.selector); + this.setDefaultReadLibConfigs(params); + + // no dvn + params[0] = SetDefaultReadLibConfigParam( + DST_EID, + ReadLibConfig(EXECUTOR, 0, 0, 0, new address[](0), new address[](0)) + ); + vm.expectRevert(LZ_RL_AtLeastOneDVN.selector); + this.setDefaultReadLibConfigs(params); + } + + function test_setDefaultReadLibConfig() public { + vm.startPrank(owner()); + ReadLibConfig memory param = _newReadLibConfig(EXECUTOR, 1, address(0x11), 1, address(0x22)); + SetDefaultReadLibConfigParam[] memory params = new SetDefaultReadLibConfigParam[](1); + params[0] = SetDefaultReadLibConfigParam(DST_EID, param); + this.setDefaultReadLibConfigs(params); + + // check default config + ReadLibConfig memory defaultConfig = readLibConfigs[DEFAULT_CONFIG][1]; + assertEq(defaultConfig.executor, EXECUTOR); + assertEq(defaultConfig.requiredDVNCount, 1); + assertEq(defaultConfig.optionalDVNCount, 1); + assertEq(defaultConfig.optionalDVNThreshold, 1); + assertEq(defaultConfig.requiredDVNs[0], address(0x11)); + assertEq(defaultConfig.optionalDVNs[0], address(0x22)); + } + + function test_setInvalidReadLibConfig() public { + // dvns.length > 0 but count == default(0) + ReadLibConfig memory param = ReadLibConfig(EXECUTOR, 0, 0, 0, new address[](1), new address[](0)); + vm.expectRevert(LZ_RL_InvalidRequiredDVNCount.selector); + _setReadLibConfig(DST_EID, address(2), param); + + // count != dvns.length + param = ReadLibConfig(EXECUTOR, 1, 0, 0, new address[](2), new address[](0)); + vm.expectRevert(LZ_RL_InvalidRequiredDVNCount.selector); + _setReadLibConfig(DST_EID, address(2), param); + + // dvns.length > MAX(127) + param = ReadLibConfig(EXECUTOR, 128, 0, 0, new address[](128), new address[](0)); + vm.expectRevert(LZ_RL_InvalidRequiredDVNCount.selector); + _setReadLibConfig(DST_EID, address(2), param); + + // duplicated dvns + param = ReadLibConfig(EXECUTOR, 2, 0, 0, new address[](2), new address[](0)); + vm.expectRevert(LZ_RL_Unsorted.selector); + _setReadLibConfig(DST_EID, address(2), param); + + // optionalDVNs.length > 0 but count == default(0) + param = ReadLibConfig(EXECUTOR, 0, 0, 0, new address[](0), new address[](1)); + vm.expectRevert(LZ_RL_InvalidOptionalDVNCount.selector); + _setReadLibConfig(DST_EID, address(2), param); + + // optionalDVNs.length > MAX(127) + param = ReadLibConfig(EXECUTOR, 0, 128, 1, new address[](0), new address[](128)); + vm.expectRevert(LZ_RL_InvalidOptionalDVNCount.selector); + _setReadLibConfig(DST_EID, address(2), param); + + // optionalDVNs.length < threshold + param = ReadLibConfig(EXECUTOR, 0, 1, 2, new address[](0), new address[](1)); + vm.expectRevert(LZ_RL_InvalidOptionalDVNThreshold.selector); + _setReadLibConfig(DST_EID, address(2), param); + + // optionalDVNs.length > 0 but threshold is 0 + param = ReadLibConfig(EXECUTOR, 0, 1, 0, new address[](0), new address[](1)); + vm.expectRevert(LZ_RL_AtLeastOneDVN.selector); + _setReadLibConfig(DST_EID, address(2), param); + } + + function test_setReadLibConfig() public { + ReadLibConfig memory param = _newReadLibConfig(EXECUTOR, 1, address(0x11), 1, address(0x22)); + _setReadLibConfig(DST_EID, address(2), param); + + // check custom config + ReadLibConfig memory customConfig = readLibConfigs[address(2)][1]; + assertEq(customConfig.executor, EXECUTOR); + assertEq(customConfig.requiredDVNCount, 1); + assertEq(customConfig.optionalDVNCount, 1); + assertEq(customConfig.optionalDVNThreshold, 1); + assertEq(customConfig.requiredDVNs[0], address(0x11)); + assertEq(customConfig.optionalDVNs[0], address(0x22)); + } + + function test_getReadLibConfig() public { + // no available dvn + vm.expectRevert(LZ_RL_AtLeastOneDVN.selector); + getReadLibConfig(address(1), 1); + + // set default config + readLibConfigs[DEFAULT_CONFIG][1] = _newReadLibConfig(EXECUTOR, 1, address(0x11), 1, address(0x22)); + + // use default uln config + ReadLibConfig memory config = getReadLibConfig(address(1), 1); + assertEq(config.executor, EXECUTOR); + assertEq(config.requiredDVNCount, 1); + assertEq(config.optionalDVNCount, 1); + assertEq(config.optionalDVNThreshold, 1); + assertEq(config.requiredDVNs[0], address(0x11)); + assertEq(config.optionalDVNs[0], address(0x22)); + + // set custom executor + readLibConfigs[address(1)][1].executor = address(0xabcd); + config = getReadLibConfig(address(1), 1); + assertEq(config.executor, address(0xabcd)); + + // set custom executor to nil + readLibConfigs[address(1)][1].executor = address(0); + config = getReadLibConfig(address(1), 1); + assertEq(config.executor, EXECUTOR); + + // set custom required dvns + readLibConfigs[address(1)][1].requiredDVNCount = 1; + readLibConfigs[address(1)][1].requiredDVNs[0] = address(0x33); + config = getReadLibConfig(address(1), 1); + assertEq(config.requiredDVNCount, 1); + assertEq(config.requiredDVNs[0], address(0x33)); + + // set custom dvns to nil + readLibConfigs[address(1)][1].requiredDVNCount = Constant.NIL_DVN_COUNT; + config = getReadLibConfig(address(1), 1); + assertEq(config.requiredDVNCount, 0); + assertEq(config.requiredDVNs.length, 0); + + // set custom optional dvns + readLibConfigs[address(1)][1].optionalDVNCount = 1; + readLibConfigs[address(1)][1].optionalDVNs[0] = address(0x44); + config = getReadLibConfig(address(1), 1); + assertEq(config.optionalDVNCount, 1); + assertEq(config.optionalDVNs[0], address(0x44)); + + // set custom optional dvns to nil + readLibConfigs[address(1)][1].optionalDVNCount = Constant.NIL_DVN_COUNT; + config = getReadLibConfig(address(1), 1); + assertEq(config.optionalDVNCount, 0); + assertEq(config.optionalDVNs.length, 0); + assertEq(config.optionalDVNThreshold, 0); + } + + function _newSingletonAddressArray(address _addr) internal pure returns (address[] memory) { + address[] memory addrs = new address[](1); + addrs[0] = _addr; + return addrs; + } + + function _newReadLibConfig( + address _executor, + uint8 _requiredCount, + address _dvn, + uint8 _optionalCount, + address _optionalDVN + ) internal pure returns (ReadLibConfig memory) { + address[] memory dvns = _dvn == address(0) ? new address[](0) : _newSingletonAddressArray(_dvn); + address[] memory optionalDVNs = _optionalDVN == address(0) + ? new address[](0) + : _newSingletonAddressArray(_optionalDVN); + uint8 optionalDVNThreshold = _optionalDVN == address(0) ? 0 : 1; + return ReadLibConfig(_executor, _requiredCount, _optionalCount, optionalDVNThreshold, dvns, optionalDVNs); + } +} diff --git a/packages/layerzero-v2/evm/messagelib/test/util/CmdUtil.sol b/packages/layerzero-v2/evm/messagelib/test/util/CmdUtil.sol new file mode 100644 index 0000000..6888ada --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/test/util/CmdUtil.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +library CmdUtil { + struct EVMCallRequestV1 { + uint16 appRequestLabel; // Label identifying the application or type of request (can be use in lzCompute) + uint32 targetEid; // Target endpoint ID (representing a target blockchain) + bool isBlockNum; // True if the request = block number, false if timestamp + uint64 blockNumOrTimestamp; // Block number or timestamp to use in the request + uint16 confirmations; // Number of block confirmations on top of the requested block number or timestamp before the view function can be called + address to; // Address of the target contract on the target chain + bytes callData; // Calldata for the contract call + } + + struct EVMCallComputeV1 { + uint8 computeSetting; // Compute setting (0 = map only, 1 = reduce only, 2 = map reduce) + uint32 targetEid; // Target endpoint ID (representing a target blockchain) + bool isBlockNum; // True if the request = block number, false if timestamp + uint64 blockNumOrTimestamp; // Block number or timestamp to use in the request + uint16 confirmations; // Number of block confirmations on top of the requested block number or timestamp before the view function can be called + address to; // Address of the target contract on the target chain + } + + uint16 internal constant CMD_VERSION = 1; + + uint8 internal constant REQUEST_VERSION = 1; + uint16 internal constant RESOLVER_TYPE_SINGLE_VIEW_EVM_CALL = 1; + + uint8 internal constant COMPUTE_VERSION = 1; + uint16 internal constant COMPUTE_TYPE_SINGLE_VIEW_EVM_CALL = 1; + + error InvalidVersion(); + error InvalidType(); + + function decode( + bytes calldata _cmd + ) + internal + pure + returns (uint16 appCmdLabel, EVMCallRequestV1[] memory evmCallRequests, EVMCallComputeV1 memory compute) + { + uint256 offset = 0; + uint16 cmdVersion = uint16(bytes2(_cmd[offset:offset + 2])); + offset += 2; + if (cmdVersion != CMD_VERSION) revert InvalidVersion(); + + appCmdLabel = uint16(bytes2(_cmd[offset:offset + 2])); + offset += 2; + + (evmCallRequests, offset) = decodeRequestsV1(_cmd, offset); + + // decode the compute if it exists + if (offset < _cmd.length) { + (compute, ) = decodeEVMCallComputeV1(_cmd, offset); + } + } + + function decodeRequestsV1( + bytes calldata _cmd, + uint256 _offset + ) internal pure returns (EVMCallRequestV1[] memory evmCallRequests, uint256 newOffset) { + newOffset = _offset; + uint16 requestCount = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + + evmCallRequests = new EVMCallRequestV1[](requestCount); + for (uint16 i = 0; i < requestCount; i++) { + uint8 requestVersion = uint8(_cmd[newOffset]); + newOffset += 1; + if (requestVersion != REQUEST_VERSION) revert InvalidVersion(); + + uint16 appRequestLabel = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + + uint16 resolverType = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + + if (resolverType == RESOLVER_TYPE_SINGLE_VIEW_EVM_CALL) { + (EVMCallRequestV1 memory request, uint256 nextOffset) = decodeEVMCallRequestV1( + _cmd, + newOffset, + appRequestLabel + ); + newOffset = nextOffset; + evmCallRequests[i] = request; + } else { + revert InvalidType(); + } + } + } + + function decodeEVMCallRequestV1( + bytes calldata _cmd, + uint256 _offset, + uint16 _appRequestLabel + ) internal pure returns (EVMCallRequestV1 memory request, uint256 newOffset) { + newOffset = _offset; + request.appRequestLabel = _appRequestLabel; + + uint16 requestSize = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + request.targetEid = uint32(bytes4(_cmd[newOffset:newOffset + 4])); + newOffset += 4; + request.isBlockNum = uint8(_cmd[newOffset]) == 1; + newOffset += 1; + request.blockNumOrTimestamp = uint64(bytes8(_cmd[newOffset:newOffset + 8])); + newOffset += 8; + request.confirmations = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + request.to = address(bytes20(_cmd[newOffset:newOffset + 20])); + newOffset += 20; + uint16 callDataSize = requestSize - 35; + request.callData = _cmd[newOffset:newOffset + callDataSize]; + newOffset += callDataSize; + } + + function decodeEVMCallComputeV1( + bytes calldata _cmd, + uint256 _offset + ) internal pure returns (EVMCallComputeV1 memory compute, uint256 newOffset) { + newOffset = _offset; + uint8 computeVersion = uint8(_cmd[newOffset]); + newOffset += 1; + if (computeVersion != COMPUTE_VERSION) revert InvalidVersion(); + uint16 computeType = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + if (computeType != COMPUTE_TYPE_SINGLE_VIEW_EVM_CALL) revert InvalidType(); + + compute.computeSetting = uint8(_cmd[newOffset]); + newOffset += 1; + compute.targetEid = uint32(bytes4(_cmd[newOffset:newOffset + 4])); + newOffset += 4; + compute.isBlockNum = uint8(_cmd[newOffset]) == 1; + newOffset += 1; + compute.blockNumOrTimestamp = uint64(bytes8(_cmd[newOffset:newOffset + 8])); + newOffset += 8; + compute.confirmations = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + compute.to = address(bytes20(_cmd[newOffset:newOffset + 20])); + newOffset += 20; + } + + function decodeCmdAppLabel(bytes calldata _cmd) internal pure returns (uint16) { + uint256 offset = 0; + uint16 cmdVersion = uint16(bytes2(_cmd[offset:offset + 2])); + offset += 2; + if (cmdVersion != CMD_VERSION) revert InvalidVersion(); + + return uint16(bytes2(_cmd[offset:offset + 2])); + } + + function decodeRequestV1AppRequestLabel(bytes calldata _request) internal pure returns (uint16) { + uint256 offset = 0; + uint8 requestVersion = uint8(_request[offset]); + offset += 1; + if (requestVersion != REQUEST_VERSION) revert InvalidVersion(); + + return uint16(bytes2(_request[offset:offset + 2])); + } + + function encode( + uint16 _appCmdLabel, + EVMCallRequestV1[] memory _evmCallRequests, + EVMCallComputeV1 memory _evmCallCompute + ) internal pure returns (bytes memory) { + bytes memory cmd = abi.encodePacked(CMD_VERSION, _appCmdLabel, uint16(_evmCallRequests.length)); + for (uint256 i = 0; i < _evmCallRequests.length; i++) { + cmd = appendEVMCallRequestV1(cmd, _evmCallRequests[i]); + } + if (_evmCallCompute.targetEid != 0) { + // if eid is 0, it means no compute + cmd = appendEVMCallComputeV1(cmd, _evmCallCompute); + } + return cmd; + } + + function appendEVMCallRequestV1( + bytes memory _cmd, + EVMCallRequestV1 memory _request + ) internal pure returns (bytes memory) { + bytes memory newCmd = abi.encodePacked( + _cmd, + REQUEST_VERSION, + _request.appRequestLabel, + RESOLVER_TYPE_SINGLE_VIEW_EVM_CALL, + uint16(_request.callData.length + 35), + _request.targetEid + ); + return + abi.encodePacked( + newCmd, + _request.isBlockNum, + _request.blockNumOrTimestamp, + _request.confirmations, + _request.to, + _request.callData + ); + } + + function appendEVMCallComputeV1( + bytes memory _cmd, + EVMCallComputeV1 memory _compute + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _cmd, + COMPUTE_VERSION, + COMPUTE_TYPE_SINGLE_VIEW_EVM_CALL, + _compute.computeSetting, + _compute.targetEid, + _compute.isBlockNum, + _compute.blockNumOrTimestamp, + _compute.confirmations, + _compute.to + ); + } +} diff --git a/packages/layerzero-v2/evm/messagelib/test/util/OptionsUtil.sol b/packages/layerzero-v2/evm/messagelib/test/util/OptionsUtil.sol index d416670..29240bb 100644 --- a/packages/layerzero-v2/evm/messagelib/test/util/OptionsUtil.sol +++ b/packages/layerzero-v2/evm/messagelib/test/util/OptionsUtil.sol @@ -17,6 +17,8 @@ library OptionsUtil { uint16 internal constant TYPE_2 = 2; // legacy options type 2 uint16 internal constant TYPE_3 = 3; + uint8 internal constant OPTION_TYPE_LZ_READ = 5; + function newOptions() internal pure returns (bytes memory) { return abi.encodePacked(TYPE_3); } @@ -30,6 +32,16 @@ library OptionsUtil { return addExecutorOption(_options, ExecutorOptions.OPTION_TYPE_LZRECEIVE, option); } + function addExecutorLzReadOption( + bytes memory _options, + uint128 _gas, + uint32 _size, + uint128 _value + ) internal pure returns (bytes memory) { + bytes memory option = encodeLzReadOption(_gas, _size, _value); + return addExecutorOption(_options, OPTION_TYPE_LZ_READ, option); + } + function addExecutorNativeDropOption( bytes memory _options, uint128 _amount, @@ -119,4 +131,8 @@ library OptionsUtil { function trimType(bytes memory _options) internal pure returns (bytes memory) { return _options.slice(2, _options.length - 2); } + + function encodeLzReadOption(uint128 _gas, uint32 _size, uint128 _value) internal pure returns (bytes memory) { + return _value == 0 ? abi.encodePacked(_gas, _size) : abi.encodePacked(_gas, _size, _value); + } } diff --git a/packages/layerzero-v2/evm/messagelib/test/util/Packet.sol b/packages/layerzero-v2/evm/messagelib/test/util/Packet.sol index b30841b..1807e47 100644 --- a/packages/layerzero-v2/evm/messagelib/test/util/Packet.sol +++ b/packages/layerzero-v2/evm/messagelib/test/util/Packet.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import { Packet } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import { Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; library PacketUtil { function newPacket( @@ -24,4 +25,8 @@ library PacketUtil { ); return Packet(_nonce, _srcEid, _sender, _dstEid, bytes32(uint256(uint160(_receiver))), guid, _message); } + + function getOrigin(Packet memory packet) internal pure returns (Origin memory origin) { + return Origin(packet.srcEid, bytes32(uint256(uint160(packet.sender))), packet.nonce); + } } diff --git a/packages/layerzero-v2/evm/messagelib/test/util/Setup.sol b/packages/layerzero-v2/evm/messagelib/test/util/Setup.sol index a25272a..b2df42e 100644 --- a/packages/layerzero-v2/evm/messagelib/test/util/Setup.sol +++ b/packages/layerzero-v2/evm/messagelib/test/util/Setup.sol @@ -263,12 +263,12 @@ library Setup { signers[0] = address(this); address[] memory admins = new address[](1); admins[0] = address(this); - DVN dvn = new DVN(eid, libs, priceFeed, signers, 1, admins); + DVN dvn = new DVN(eid, eid, libs, priceFeed, signers, 1, admins); IDVN.DstConfigParam[] memory dstConfigParams = new IDVN.DstConfigParam[](1); dstConfigParams[0] = IDVN.DstConfigParam({ dstEid: eid, gas: 5000, multiplierBps: 0, floorMarginUSD: 0 }); dvn.setDstConfig(dstConfigParams); - DVNFeeLib dvnFeeLib = new DVNFeeLib(1e18); + DVNFeeLib dvnFeeLib = new DVNFeeLib(eid, 1e18); dvn.setWorkerFeeLib(address(dvnFeeLib)); return dvn; @@ -282,7 +282,7 @@ library Setup { address priceFeed ) internal returns (Executor) { Executor executor = new Executor(); - ExecutorFeeLib executorFeeLib = new ExecutorFeeLib(1e18); + ExecutorFeeLib executorFeeLib = new ExecutorFeeLib(1, 1e18); { address[] memory admins = new address[](1); admins[0] = address(this); diff --git a/packages/layerzero-v2/evm/messagelib/test/util/SetupRead.sol b/packages/layerzero-v2/evm/messagelib/test/util/SetupRead.sol new file mode 100644 index 0000000..45d8a0e --- /dev/null +++ b/packages/layerzero-v2/evm/messagelib/test/util/SetupRead.sol @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import { EndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/EndpointV2.sol"; +import { MessagingParams, MessagingReceipt, MessagingFee, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import { IDVN } from "../../contracts/uln/interfaces/IDVN.sol"; +import { DVN } from "../../contracts/uln/dvn/DVN.sol"; +import { DVNFeeLib, BitMap256 } from "../../contracts/uln/dvn/DVNFeeLib.sol"; +import { Executor } from "../../contracts/Executor.sol"; +import { ExecutorFeeLib } from "../../contracts/ExecutorFeeLib.sol"; +import { IExecutor } from "../../contracts/interfaces/IExecutor.sol"; +import { PriceFeed } from "../../contracts/PriceFeed.sol"; +import { ILayerZeroPriceFeed } from "../../contracts/interfaces/ILayerZeroPriceFeed.sol"; +import { Treasury } from "../../contracts/Treasury.sol"; +import { TreasuryFeeHandler } from "../../contracts/uln/uln301/TreasuryFeeHandler.sol"; +import { ExecutorConfig, SetDefaultExecutorConfigParam } from "../../contracts/SendLibBase.sol"; +import { ReadLibConfig, SetDefaultReadLibConfigParam } from "../../contracts/uln/readlib/ReadLibBase.sol"; +import { SetDefaultExecutorParam } from "../../contracts/uln/uln301/ReceiveLibBaseE1.sol"; +import { ReadLib1002 } from "../../contracts/uln/readlib/ReadLib1002.sol"; + +import { TokenMock } from "../mocks/TokenMock.sol"; +import { Constant } from "./Constant.sol"; +import { OptionsUtil } from "./OptionsUtil.sol"; + +library SetupRead { + using OptionsUtil for bytes; + + uint120 internal constant OneUSD = 1e20; + + struct FixtureRead { + uint32 eid; + EndpointV2 endpointV2; + ReadLib1002 cmdLib; + Executor executor; + ExecutorFeeLib executorFeeLib; + DVN dvn; + address dvnSigner; // 1 signer + DVNFeeLib dvnFeeLib; + PriceFeed priceFeed; + Treasury treasury; + TokenMock lzToken; + ReadOApp oapp; + } + + function loadFixture(uint32 eid) internal returns (FixtureRead memory f) { + f.eid = eid; + // deploy endpointV2, sendUln302 + (f.endpointV2, f.cmdLib) = deployEndpointV2(eid, Constant.TREASURY_GAS_CAP, Constant.TREASURY_GAS_FOR_FEE_CAP); + // deploy priceFee + f.priceFeed = deployPriceFeed(eid); + // deploy dvn + (f.dvn, f.dvnFeeLib) = deployDVN(eid, address(f.cmdLib), address(f.priceFeed)); + // deploy executor + (f.executor, f.executorFeeLib) = deployExecutor( + eid, + address(f.endpointV2), + address(f.cmdLib), + address(f.priceFeed) + ); + // deploy treasury + f.treasury = deployTreasury(); + // deploy LZ token + f.lzToken = deployTokenMock(); + + // deploy oapp + f.oapp = new ReadOApp(address(f.endpointV2)); + + f.cmdLib.setTreasury(address(f.treasury)); + + f.endpointV2.registerLibrary(address(f.cmdLib)); + f.endpointV2.setLzToken(address(f.lzToken)); + } + + function wireFixtureV2WithChannel(FixtureRead memory f2, uint32 cid) internal { + // dvn feelib set supported channels + DVNFeeLib.SetSupportedCmdTypesParam[] memory supportedCmdTypes = new DVNFeeLib.SetSupportedCmdTypesParam[](1); + supportedCmdTypes[0] = DVNFeeLib.SetSupportedCmdTypesParam({ targetEid: cid, types: BitMap256.wrap(3) }); + f2.dvnFeeLib.setSupportedCmdTypes(supportedCmdTypes); + + // cmdLib set default config + address[] memory dvns = new address[](1); + dvns[0] = address(f2.dvn); + ReadLibConfig memory cmdLibConfig = ReadLibConfig( + address(f2.executor), + uint8(dvns.length), + 0, + 0, + dvns, + new address[](0) + ); + SetDefaultReadLibConfigParam[] memory ulnConfigParams = new SetDefaultReadLibConfigParam[](1); + ulnConfigParams[0] = SetDefaultReadLibConfigParam(cid, cmdLibConfig); + f2.cmdLib.setDefaultReadLibConfigs(ulnConfigParams); + + f2.endpointV2.setDefaultSendLibrary(cid, address(f2.cmdLib)); + f2.endpointV2.setDefaultReceiveLibrary(cid, address(f2.cmdLib), 0); + } + + function deployEndpointV2( + uint32 eid, + uint256 treasuryGasCap, + uint256 treasuryGasForFeeCap + ) internal returns (EndpointV2, ReadLib1002) { + // deploy endpointV2, sendUln302 + EndpointV2 endpointV2 = new EndpointV2(eid, address(this)); + ReadLib1002 cmdLib = new ReadLib1002(address(endpointV2), treasuryGasCap, treasuryGasForFeeCap); + return (endpointV2, cmdLib); + } + + function deployPriceFeed(uint32 eid) internal returns (PriceFeed) { + PriceFeed priceFeed = new PriceFeed(); + priceFeed.initialize(address(this)); + priceFeed.setNativeTokenPriceUSD(OneUSD); // 1 USD with 20 denominator + + // price feed + ILayerZeroPriceFeed.UpdatePrice memory updatePrice = ILayerZeroPriceFeed.UpdatePrice({ + eid: eid, + price: ILayerZeroPriceFeed.Price({ + priceRatio: priceFeed.getPriceRatioDenominator(), // 1:1 + gasPriceInUnit: 1e9, // 1 gwei + gasPerByte: 1000 + }) + }); + ILayerZeroPriceFeed.UpdatePrice[] memory updates = new ILayerZeroPriceFeed.UpdatePrice[](1); + updates[0] = updatePrice; + priceFeed.setPrice(updates); + + return priceFeed; + } + + function deployDVN(uint32 eid, address cmdLib, address priceFeed) internal returns (DVN, DVNFeeLib) { + address[] memory libs = new address[](4); + libs[0] = cmdLib; + address[] memory signers = new address[](1); + signers[0] = address(this); + address[] memory admins = new address[](1); + admins[0] = address(this); + DVN dvn = new DVN(eid, eid, libs, priceFeed, signers, 1, admins); + + IDVN.DstConfigParam[] memory dstConfigParams = new IDVN.DstConfigParam[](1); + dstConfigParams[0] = IDVN.DstConfigParam({ dstEid: eid, gas: 5000, multiplierBps: 0, floorMarginUSD: 0 }); + dvn.setDstConfig(dstConfigParams); + DVNFeeLib dvnFeeLib = new DVNFeeLib(eid, 1e18); + dvn.setWorkerFeeLib(address(dvnFeeLib)); + uint120 evmCallRequestV1FeeUSD = OneUSD; + uint120 evmCallComputeV1ReduceFeeUSD = OneUSD; + uint16 evmCallComputeV1MapBps = 1000; // 10% plug for each map call on request + dvnFeeLib.setCmdFees(evmCallRequestV1FeeUSD, evmCallComputeV1ReduceFeeUSD, evmCallComputeV1MapBps); + + return (dvn, dvnFeeLib); + } + + function deployExecutor( + uint32 eid, + address endpointV2, + address cmdLib, + address priceFeed + ) internal returns (Executor, ExecutorFeeLib) { + Executor executor = new Executor(); + ExecutorFeeLib executorFeeLib = new ExecutorFeeLib(EndpointV2(endpointV2).eid(), 1e18); + { + address[] memory admins = new address[](1); + admins[0] = address(this); + address[] memory libs = new address[](3); + libs[0] = cmdLib; + executor.initialize(endpointV2, address(0), libs, priceFeed, address(this), admins); + executor.setWorkerFeeLib(address(executorFeeLib)); + + IExecutor.DstConfigParam[] memory dstConfigParams = new IExecutor.DstConfigParam[](1); + dstConfigParams[0] = IExecutor.DstConfigParam({ + dstEid: eid, + lzReceiveBaseGas: 5000, + lzComposeBaseGas: 5000, + multiplierBps: 10000, + floorMarginUSD: 0, + nativeCap: 1 gwei + }); + executor.setDstConfig(dstConfigParams); + } + return (executor, executorFeeLib); + } + + function deployTreasury() internal returns (Treasury) { + Treasury treasury = new Treasury(); + treasury.setLzTokenEnabled(true); + treasury.setLzTokenFee(1e18); // 1 ZRO + treasury.setNativeFeeBP(1000); // 10% + return treasury; + } + + function deployTokenMock() internal returns (TokenMock) { + TokenMock lzToken = new TokenMock(); + return lzToken; + } +} + +contract ReadOApp { + using OptionsUtil for bytes; + + // copy cmd from test_success_1_req_map_and_reduce + bytes public constant cmd = + hex"000100000001010000000100080000029aaabbccdd010001020000029a00000000000000000000000000000000000000000000000000000000000000"; + + EndpointV2 public endpointV2; + + uint256 public ack; + + constructor(address _endpoint) { + endpointV2 = EndpointV2(_endpoint); + } + + function quote(uint32 _cid, bool _payInLzToken, bytes memory _options) public view returns (MessagingFee memory) { + MessagingParams memory msgParams = MessagingParams( + _cid, + bytes32(uint256(uint160(address(this)))), + cmd, + _options, + _payInLzToken + ); + return endpointV2.quote(msgParams, address(this)); + } + + function send( + uint32 _cid, + bool _payInLzToken, + bytes memory _options + ) public payable returns (MessagingReceipt memory) { + MessagingParams memory msgParams = MessagingParams( + _cid, + bytes32(uint256(uint160(address(this)))), + cmd, + _options, + _payInLzToken + ); + return endpointV2.send{ value: msg.value }(msgParams, address(this)); + } + + function lzReceive( + Origin calldata /*_origin*/, + bytes32 /*_guid*/, + bytes calldata /*_message*/, + address /*_executor*/, + bytes calldata /*_extraData*/ + ) public payable virtual { + require(msg.sender == address(endpointV2), "ReadOApp: Invalid sender"); + ack += 1; + } + + function allowInitializePath(Origin calldata /*origin*/) public view virtual returns (bool) { + return true; + } + + receive() external payable {} +} diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppCore.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppCore.sol index 966e5d5..1699f69 100644 --- a/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppCore.sol +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppCore.sol @@ -41,6 +41,19 @@ abstract contract OAppCore is IOAppCore, Ownable { * @dev Peer is a bytes32 to accommodate non-evm chains. */ function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner { + _setPeer(_eid, _peer); + } + + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @param _peer The address of the peer to be associated with the corresponding endpoint. + * + * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. + * @dev Set this to bytes32(0) to remove the peer address. + * @dev Peer is a bytes32 to accommodate non-evm chains. + */ + function _setPeer(uint32 _eid, bytes32 _peer) internal virtual { peers[_eid] = _peer; emit PeerSet(_eid, _peer); } diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppRead.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppRead.sol new file mode 100644 index 0000000..be0c52a --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppRead.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol"; + +import { OApp } from "./OApp.sol"; + +abstract contract OAppRead is OApp { + + constructor(address _endpoint, address _delegate) OApp(_endpoint, _delegate) {} + + // ------------------------------- + // Only Owner + function setReadChannel(uint32 _channelId, bool _active) public virtual onlyOwner { + _setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0)); + } +} diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppReceiver.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppReceiver.sol index c797f6f..a90ea28 100644 --- a/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppReceiver.sol +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppReceiver.sol @@ -15,7 +15,7 @@ abstract contract OAppReceiver is IOAppReceiver, OAppCore { // @dev The version of the OAppReceiver implementation. // @dev Version is bumped when changes are made to this contract. - uint64 internal constant RECEIVER_VERSION = 1; + uint64 internal constant RECEIVER_VERSION = 2; /** * @notice Retrieves the OApp version information. @@ -31,14 +31,24 @@ abstract contract OAppReceiver is IOAppReceiver, OAppCore { } /** - * @notice Retrieves the address responsible for 'sending' composeMsg's to the Endpoint. - * @return sender The address responsible for 'sending' composeMsg's to the Endpoint. + * @notice Indicates whether an address is an approved composeMsg sender to the Endpoint. + * @dev _origin The origin information containing the source endpoint and sender address. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address on the src chain. + * - nonce: The nonce of the message. + * @dev _message The lzReceive payload. + * @param _sender The sender address. + * @return isSender Is a valid sender. * - * @dev Applications can optionally choose to implement a separate composeMsg sender that is NOT the bridging layer. - * @dev The default sender IS the OApp implementer. + * @dev Applications can optionally choose to implement separate composeMsg senders that are NOT the bridging layer. + * @dev The default sender IS the OAppReceiver implementer. */ - function composeMsgSender() public view virtual returns (address sender) { - return address(this); + function isComposeMsgSender( + Origin calldata /*_origin*/, + bytes calldata /*_message*/, + address _sender + ) public view virtual returns (bool) { + return _sender == address(this); } /** diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/CmdCodecV1Mock.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/CmdCodecV1Mock.sol new file mode 100644 index 0000000..91f35fe --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/CmdCodecV1Mock.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { EVMCallRequestV1, EVMCallComputeV1, ReadCmdCodecV1 } from "../libs/ReadCmdCodecV1.sol"; + +contract CmdCodecV1Mock { + function decode( + bytes calldata _cmd + ) + external + pure + returns (uint16 appCmdLabel, EVMCallRequestV1[] memory evmRequests, EVMCallComputeV1 memory compute) + { + return ReadCmdCodecV1.decode(_cmd); + } + + function encode( + uint16 _appCmdLabel, + EVMCallRequestV1[] calldata _evmRequests + ) external pure returns (bytes memory) { + return ReadCmdCodecV1.encode(_appCmdLabel, _evmRequests); + } + + function encode( + uint16 _appCmdLabel, + EVMCallRequestV1[] calldata _evmRequests, + EVMCallComputeV1 calldata _evmCompute + ) external pure returns (bytes memory) { + return ReadCmdCodecV1.encode(_appCmdLabel, _evmRequests, _evmCompute); + } +} diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/LzReadCounter.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/LzReadCounter.sol new file mode 100644 index 0000000..e221067 --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/LzReadCounter.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { ILayerZeroEndpointV2, MessagingFee, MessagingReceipt, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { ReadCmdCodecV1, EVMCallComputeV1, EVMCallRequestV1 } from "../libs/ReadCmdCodecV1.sol"; +import { IOAppComputer } from "../interfaces/IOAppComputer.sol"; + +import { OAppRead } from "../OAppRead.sol"; + +contract LzReadCounter is OAppRead, IOAppComputer { + struct EvmReadRequest { + uint16 appRequestLabel; + uint32 targetEid; + bool isBlockNum; + uint64 blockNumOrTimestamp; + uint16 confirmations; + address to; + uint256 countAddition; // addition to add to the count when reading + } + + struct ComputeSetting { + uint8 computeSetting; + uint16 computeConfirmations; + uint64 blockNumOrTimestamp; + bool isBlockNum; + } + + uint8 internal constant COMPUTE_SETTING_MAP_ONLY = 0; + uint8 internal constant COMPUTE_SETTING_REDUCE_ONLY = 1; + uint8 internal constant COMPUTE_SETTING_MAP_REDUCE = 2; + uint8 internal constant COMPUTE_SETTING_NONE = 3; + + uint32 public immutable eid; + uint256 public count; + + constructor(address _endpoint) OAppRead(_endpoint, msg.sender) { + eid = ILayerZeroEndpointV2(_endpoint).eid(); + } + + // ------------------------------- + // Trigger Read + function triggerRead( + uint32 _channelId, // The read channel id + uint16 _appLabel, // The cmd app label + EvmReadRequest[] memory _requests, + ComputeSetting memory _computeSetting, + bytes calldata _options + ) external payable returns (MessagingReceipt memory receipt) { + bytes memory cmd = buildCmd(_appLabel, _requests, _computeSetting); + count += 1; // increase the count, for pin block testing + return _lzSend(_channelId, cmd, _options, MessagingFee(msg.value, 0), payable(msg.sender)); + } + + function clearCount() external { + count = 0; + } + + // ------------------------------- + // View + function quote( + uint32 _channelId, + uint16 _appLabel, + EvmReadRequest[] memory _requests, + ComputeSetting memory _computeSetting, + bytes calldata _options + ) public view returns (uint256 nativeFee, uint256 lzTokenFee) { + bytes memory cmd = buildCmd(_appLabel, _requests, _computeSetting); + MessagingFee memory fee = _quote(_channelId, cmd, _options, false); + return (fee.nativeFee, fee.lzTokenFee); + } + + function buildCmd( + uint16 appLabel, + EvmReadRequest[] memory _readRequests, + ComputeSetting memory _computeSetting + ) public view returns (bytes memory) { + require(_readRequests.length > 0, "LzReadCounter: empty requests"); + // build read requests + EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](_readRequests.length); + for (uint256 i = 0; i < _readRequests.length; i++) { + EvmReadRequest memory req = _readRequests[i]; + readRequests[i] = EVMCallRequestV1({ + appRequestLabel: req.appRequestLabel, + targetEid: req.targetEid, + isBlockNum: req.isBlockNum, + blockNumOrTimestamp: req.blockNumOrTimestamp, + confirmations: req.confirmations, + to: req.to, + callData: abi.encodeWithSelector(this.readCount.selector, req.countAddition) + }); + } + // build compute, on current contract + require(_computeSetting.computeSetting <= COMPUTE_SETTING_NONE, "LzReadCounter: invalid compute type"); + EVMCallComputeV1 memory evmCompute = EVMCallComputeV1({ + computeSetting: _computeSetting.computeSetting, + targetEid: _computeSetting.computeSetting == COMPUTE_SETTING_NONE ? 0 : eid, // 0(means no compute) for none, else use local eid + isBlockNum: _computeSetting.isBlockNum, + blockNumOrTimestamp: _computeSetting.blockNumOrTimestamp, + confirmations: _computeSetting.computeConfirmations, + to: address(this) + }); + bytes memory cmd = ReadCmdCodecV1.encode(appLabel, readRequests, evmCompute); + + return cmd; + } + + function readCount(uint256 countAddition) external view returns (uint256) { + require(countAddition != 9, "LzReadCounter: invalid count addition"); // This is only for testing + return count + countAddition; + } + + function lzMap(bytes calldata _request, bytes calldata _response) external pure returns (bytes memory) { + require(_response.length == 32, "LzReadCounter: invalid response length"); + uint16 requestId = ReadCmdCodecV1.decodeRequestV1AppRequestLabel(_request); + uint256 countNum = abi.decode(_response, (uint256)); + return abi.encode(countNum + 100 + requestId * 1000); // map behavior + } + + function lzReduce(bytes calldata _cmd, bytes[] calldata _responses) external pure returns (bytes memory) { + uint256 total = 0; + for (uint256 i = 0; i < _responses.length; i++) { + require(_responses[i].length == 32, "LzReadCounter: invalid response length"); + uint256 countNum = abi.decode(_responses[i], (uint256)); + total += countNum; + } + total += 10000; // reduce behavior + + uint16 cmdAppLabel = ReadCmdCodecV1.decodeCmdAppLabel(_cmd); + total += uint256(cmdAppLabel) * 100000; // cmdAppLabel behavior + + return abi.encode(total); + } + + // ------------------------------- + function _lzReceive( + Origin calldata /* _origin */, + bytes32 /* _guid */, + bytes calldata _message, + address /*_executor*/, + bytes calldata /*_extraData*/ + ) internal override { + require(_message.length % 32 == 0, "LzReadCounter: invalid message length"); + uint256 total = 0; + // loop read bytes32 of the message and decode it to uint256 then add it to the total + for (uint256 i = 0; i < _message.length; i += 32) { + total += abi.decode(_message[i:i + 32], (uint256)); + } + // reset count if it's too large + if (count >= 2 ** 128) { + count = 0; + } + count += total; + } + + // be able to receive ether + receive() external payable virtual {} + + fallback() external payable {} +} diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/OmniCounter.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/OmniCounter.sol index ec394b6..d4bc1ba 100644 --- a/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/OmniCounter.sol +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/OmniCounter.sol @@ -1,283 +1,15 @@ -// SPDX-License-Identifier: MIT - +// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; -import { ILayerZeroEndpointV2, MessagingFee, MessagingReceipt, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; -import { ILayerZeroComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol"; - -import { OApp } from "../OApp.sol"; -import { OptionsBuilder } from "../libs/OptionsBuilder.sol"; -import { OAppPreCrimeSimulator } from "../../precrime/OAppPreCrimeSimulator.sol"; - -library MsgCodec { - uint8 internal constant VANILLA_TYPE = 1; - uint8 internal constant COMPOSED_TYPE = 2; - uint8 internal constant ABA_TYPE = 3; - uint8 internal constant COMPOSED_ABA_TYPE = 4; - - uint8 internal constant MSG_TYPE_OFFSET = 0; - uint8 internal constant SRC_EID_OFFSET = 1; - uint8 internal constant VALUE_OFFSET = 5; - - function encode(uint8 _type, uint32 _srcEid) internal pure returns (bytes memory) { - return abi.encodePacked(_type, _srcEid); - } - - function encode(uint8 _type, uint32 _srcEid, uint256 _value) internal pure returns (bytes memory) { - return abi.encodePacked(_type, _srcEid, _value); - } - - function msgType(bytes calldata _message) internal pure returns (uint8) { - return uint8(bytes1(_message[MSG_TYPE_OFFSET:SRC_EID_OFFSET])); - } - - function srcEid(bytes calldata _message) internal pure returns (uint32) { - return uint32(bytes4(_message[SRC_EID_OFFSET:VALUE_OFFSET])); - } - - function value(bytes calldata _message) internal pure returns (uint256) { - return uint256(bytes32(_message[VALUE_OFFSET:])); - } -} - -contract OmniCounter is ILayerZeroComposer, OApp, OAppPreCrimeSimulator { - using MsgCodec for bytes; - using OptionsBuilder for bytes; - - uint256 public count; - uint256 public composedCount; - - address public admin; - uint32 public eid; - - mapping(uint32 srcEid => mapping(bytes32 sender => uint64 nonce)) private maxReceivedNonce; - bool private orderedNonce; - - // for global assertions - mapping(uint32 srcEid => uint256 count) public inboundCount; - mapping(uint32 dstEid => uint256 count) public outboundCount; - - constructor(address _endpoint, address _delegate) OApp(_endpoint, _delegate) { - admin = msg.sender; - eid = ILayerZeroEndpointV2(_endpoint).eid(); - } - - modifier onlyAdmin() { - require(msg.sender == admin, "only admin"); - _; - } - - // ------------------------------- - // Only Admin - function setAdmin(address _admin) external onlyAdmin { - admin = _admin; - } - - function withdraw(address payable _to, uint256 _amount) external onlyAdmin { - (bool success, ) = _to.call{ value: _amount }(""); - require(success, "OmniCounter: withdraw failed"); - } - - // ------------------------------- - // Send - function increment(uint32 _eid, uint8 _type, bytes calldata _options) external payable { - // bytes memory options = combineOptions(_eid, _type, _options); - _lzSend(_eid, MsgCodec.encode(_type, eid), _options, MessagingFee(msg.value, 0), payable(msg.sender)); - _incrementOutbound(_eid); - } - - // this is a broken function to skip incrementing outbound count - // so that preCrime will fail - function brokenIncrement(uint32 _eid, uint8 _type, bytes calldata _options) external payable onlyAdmin { - // bytes memory options = combineOptions(_eid, _type, _options); - _lzSend(_eid, MsgCodec.encode(_type, eid), _options, MessagingFee(msg.value, 0), payable(msg.sender)); - } - - function batchIncrement( - uint32[] calldata _eids, - uint8[] calldata _types, - bytes[] calldata _options - ) external payable { - require(_eids.length == _options.length && _eids.length == _types.length, "OmniCounter: length mismatch"); - - MessagingReceipt memory receipt; - uint256 providedFee = msg.value; - for (uint256 i = 0; i < _eids.length; i++) { - address refundAddress = i == _eids.length - 1 ? msg.sender : address(this); - uint32 dstEid = _eids[i]; - uint8 msgType = _types[i]; - // bytes memory options = combineOptions(dstEid, msgType, _options[i]); - receipt = _lzSend( - dstEid, - MsgCodec.encode(msgType, eid), - _options[i], - MessagingFee(providedFee, 0), - payable(refundAddress) - ); - _incrementOutbound(dstEid); - providedFee -= receipt.fee.nativeFee; - } - } - - // ------------------------------- - // View - function quote( - uint32 _eid, - uint8 _type, - bytes calldata _options - ) public view returns (uint256 nativeFee, uint256 lzTokenFee) { - // bytes memory options = combineOptions(_eid, _type, _options); - MessagingFee memory fee = _quote(_eid, MsgCodec.encode(_type, eid), _options, false); - return (fee.nativeFee, fee.lzTokenFee); - } - - // @dev enables preCrime simulator - // @dev routes the call down from the OAppPreCrimeSimulator, and up to the OApp - function _lzReceiveSimulate( - Origin calldata _origin, - bytes32 _guid, - bytes calldata _message, - address _executor, - bytes calldata _extraData - ) internal virtual override { - _lzReceive(_origin, _guid, _message, _executor, _extraData); - } - - // ------------------------------- - function _lzReceive( - Origin calldata _origin, - bytes32 _guid, - bytes calldata _message, - address /*_executor*/, - bytes calldata /*_extraData*/ - ) internal override { - _acceptNonce(_origin.srcEid, _origin.sender, _origin.nonce); - uint8 messageType = _message.msgType(); - - if (messageType == MsgCodec.VANILLA_TYPE) { - count++; - - //////////////////////////////// IMPORTANT ////////////////////////////////// - /// if you request for msg.value in the options, you should also encode it - /// into your message and check the value received at destination (example below). - /// if not, the executor could potentially provide less msg.value than you requested - /// leading to unintended behavior. Another option is to assert the executor to be - /// one that you trust. - ///////////////////////////////////////////////////////////////////////////// - require(msg.value >= _message.value(), "OmniCounter: insufficient value"); - - _incrementInbound(_origin.srcEid); - } else if (messageType == MsgCodec.COMPOSED_TYPE || messageType == MsgCodec.COMPOSED_ABA_TYPE) { - count++; - _incrementInbound(_origin.srcEid); - endpoint.sendCompose(address(this), _guid, 0, _message); - } else if (messageType == MsgCodec.ABA_TYPE) { - count++; - _incrementInbound(_origin.srcEid); - - // send back to the sender - _incrementOutbound(_origin.srcEid); - bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 10); - _lzSend( - _origin.srcEid, - MsgCodec.encode(MsgCodec.VANILLA_TYPE, eid, 10), - options, - MessagingFee(msg.value, 0), - payable(address(this)) - ); - } else { - revert("invalid message type"); - } - } - - function _incrementInbound(uint32 _srcEid) internal { - inboundCount[_srcEid]++; - } - - function _incrementOutbound(uint32 _dstEid) internal { - outboundCount[_dstEid]++; - } - - function lzCompose( - address _oApp, - bytes32 /*_guid*/, - bytes calldata _message, - address, - bytes calldata - ) external payable override { - require(_oApp == address(this), "!oApp"); - require(msg.sender == address(endpoint), "!endpoint"); - - uint8 msgType = _message.msgType(); - if (msgType == MsgCodec.COMPOSED_TYPE) { - composedCount += 1; - } else if (msgType == MsgCodec.COMPOSED_ABA_TYPE) { - composedCount += 1; - - uint32 srcEid = _message.srcEid(); - _incrementOutbound(srcEid); - bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); - _lzSend( - srcEid, - MsgCodec.encode(MsgCodec.VANILLA_TYPE, eid), - options, - MessagingFee(msg.value, 0), - payable(address(this)) - ); - } else { - revert("invalid message type"); - } - } - - // ------------------------------- - // Ordered OApp - // this demonstrates how to build an app that requires execution nonce ordering - // normally an app should decide ordered or not on contract construction - // this is just a demo - function setOrderedNonce(bool _orderedNonce) external onlyOwner { - orderedNonce = _orderedNonce; - } - - function _acceptNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) internal virtual { - uint64 currentNonce = maxReceivedNonce[_srcEid][_sender]; - if (orderedNonce) { - require(_nonce == currentNonce + 1, "OApp: invalid nonce"); - } - // update the max nonce anyway. once the ordered mode is turned on, missing early nonces will be rejected - if (_nonce > currentNonce) { - maxReceivedNonce[_srcEid][_sender] = _nonce; - } - } - - function nextNonce(uint32 _srcEid, bytes32 _sender) public view virtual override returns (uint64) { - if (orderedNonce) { - return maxReceivedNonce[_srcEid][_sender] + 1; - } else { - return 0; // path nonce starts from 1. if 0 it means that there is no specific nonce enforcement - } - } - - // TODO should override oApp version with added ordered nonce increment - // a governance function to skip nonce - function skipInboundNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) public virtual onlyOwner { - endpoint.skip(address(this), _srcEid, _sender, _nonce); - if (orderedNonce) { - maxReceivedNonce[_srcEid][_sender]++; - } - } - - function isPeer(uint32 _eid, bytes32 _peer) public view override returns (bool) { - return peers[_eid] == _peer; - } +// @dev Oz5 implementation +// import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - // @dev Batch send requires overriding this function from OAppSender because the msg.value contains multiple fees - function _payNative(uint256 _nativeFee) internal virtual override returns (uint256 nativeFee) { - if (msg.value < _nativeFee) revert NotEnoughNative(msg.value); - return _nativeFee; - } +import { OmniCounterAbstract, MsgCodec } from "./OmniCounterAbstract.sol"; - // be able to receive ether - receive() external payable virtual {} +contract OmniCounter is OmniCounterAbstract { + // @dev Oz4 implementation + constructor(address _endpoint, address _delegate) OmniCounterAbstract(_endpoint, _delegate) {} - fallback() external payable {} + // @dev Oz5 implementation + // constructor(address _endpoint, address _delegate) OmniCounterAbstract(_endpoint, _delegate) Ownable(_delegate) {} } diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/OmniCounterAbstract.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/OmniCounterAbstract.sol new file mode 100644 index 0000000..a4ec3ad --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/examples/OmniCounterAbstract.sol @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { ILayerZeroEndpointV2, MessagingFee, MessagingReceipt, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { ILayerZeroComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol"; + +import { OApp } from "../OApp.sol"; +import { OptionsBuilder } from "../libs/OptionsBuilder.sol"; +import { OAppPreCrimeSimulator } from "../../precrime/OAppPreCrimeSimulator.sol"; + +library MsgCodec { + uint8 internal constant VANILLA_TYPE = 1; + uint8 internal constant COMPOSED_TYPE = 2; + uint8 internal constant ABA_TYPE = 3; + uint8 internal constant COMPOSED_ABA_TYPE = 4; + + uint8 internal constant MSG_TYPE_OFFSET = 0; + uint8 internal constant SRC_EID_OFFSET = 1; + uint8 internal constant VALUE_OFFSET = 5; + + function encode(uint8 _type, uint32 _srcEid) internal pure returns (bytes memory) { + return abi.encodePacked(_type, _srcEid); + } + + function encode(uint8 _type, uint32 _srcEid, uint256 _value) internal pure returns (bytes memory) { + return abi.encodePacked(_type, _srcEid, _value); + } + + function msgType(bytes calldata _message) internal pure returns (uint8) { + return uint8(bytes1(_message[MSG_TYPE_OFFSET:SRC_EID_OFFSET])); + } + + function srcEid(bytes calldata _message) internal pure returns (uint32) { + return uint32(bytes4(_message[SRC_EID_OFFSET:VALUE_OFFSET])); + } + + function value(bytes calldata _message) internal pure returns (uint256) { + return uint256(bytes32(_message[VALUE_OFFSET:])); + } +} + +// @dev declared as abstract to provide backwards compatibility with Oz5/Oz4 +abstract contract OmniCounterAbstract is ILayerZeroComposer, OApp, OAppPreCrimeSimulator { + using MsgCodec for bytes; + using OptionsBuilder for bytes; + + uint256 public count; + uint256 public composedCount; + + address public admin; + uint32 public eid; + + mapping(uint32 srcEid => mapping(bytes32 sender => uint64 nonce)) private maxReceivedNonce; + bool private orderedNonce; + + // for global assertions + mapping(uint32 srcEid => uint256 count) public inboundCount; + mapping(uint32 dstEid => uint256 count) public outboundCount; + + constructor(address _endpoint, address _delegate) OApp(_endpoint, _delegate) { + admin = msg.sender; + eid = ILayerZeroEndpointV2(_endpoint).eid(); + } + + modifier onlyAdmin() { + require(msg.sender == admin, "only admin"); + _; + } + + // ------------------------------- + // Only Admin + function setAdmin(address _admin) external onlyAdmin { + admin = _admin; + } + + function withdraw(address payable _to, uint256 _amount) external onlyAdmin { + (bool success, ) = _to.call{ value: _amount }(""); + require(success, "OmniCounter: withdraw failed"); + } + + // ------------------------------- + // Send + function increment(uint32 _eid, uint8 _type, bytes calldata _options) external payable { + // bytes memory options = combineOptions(_eid, _type, _options); + _lzSend(_eid, MsgCodec.encode(_type, eid), _options, MessagingFee(msg.value, 0), payable(msg.sender)); + _incrementOutbound(_eid); + } + + // this is a broken function to skip incrementing outbound count + // so that preCrime will fail + function brokenIncrement(uint32 _eid, uint8 _type, bytes calldata _options) external payable onlyAdmin { + // bytes memory options = combineOptions(_eid, _type, _options); + _lzSend(_eid, MsgCodec.encode(_type, eid), _options, MessagingFee(msg.value, 0), payable(msg.sender)); + // _incrementOutbound(_eid); // mock method which intentionally does not increment outboundCount to cause a PreCrime Crime + } + + function batchIncrement( + uint32[] calldata _eids, + uint8[] calldata _types, + bytes[] calldata _options + ) external payable { + require(_eids.length == _options.length && _eids.length == _types.length, "OmniCounter: length mismatch"); + + MessagingReceipt memory receipt; + uint256 providedFee = msg.value; + for (uint256 i = 0; i < _eids.length; i++) { + address refundAddress = i == _eids.length - 1 ? msg.sender : address(this); + uint32 dstEid = _eids[i]; + uint8 msgType = _types[i]; + // bytes memory options = combineOptions(dstEid, msgType, _options[i]); + receipt = _lzSend( + dstEid, + MsgCodec.encode(msgType, eid), + _options[i], + MessagingFee(providedFee, 0), + payable(refundAddress) + ); + _incrementOutbound(dstEid); + providedFee -= receipt.fee.nativeFee; + } + } + + // ------------------------------- + // View + function quote( + uint32 _eid, + uint8 _type, + bytes calldata _options + ) public view returns (uint256 nativeFee, uint256 lzTokenFee) { + // bytes memory options = combineOptions(_eid, _type, _options); + MessagingFee memory fee = _quote(_eid, MsgCodec.encode(_type, eid), _options, false); + return (fee.nativeFee, fee.lzTokenFee); + } + + // @dev enables preCrime simulator + // @dev routes the call down from the OAppPreCrimeSimulator, and up to the OApp + function _lzReceiveSimulate( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual override { + _lzReceive(_origin, _guid, _message, _executor, _extraData); + } + + // ------------------------------- + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address /*_executor*/, + bytes calldata /*_extraData*/ + ) internal override { + _acceptNonce(_origin.srcEid, _origin.sender, _origin.nonce); + uint8 messageType = _message.msgType(); + + if (messageType == MsgCodec.VANILLA_TYPE) { + count++; + + //////////////////////////////// IMPORTANT ////////////////////////////////// + /// if you request for msg.value in the options, you should also encode it + /// into your message and check the value received at destination (example below). + /// if not, the executor could potentially provide less msg.value than you requested + /// leading to unintended behavior. Another option is to assert the executor to be + /// one that you trust. + ///////////////////////////////////////////////////////////////////////////// + require(msg.value >= _message.value(), "OmniCounter: insufficient value"); + + _incrementInbound(_origin.srcEid); + } else if (messageType == MsgCodec.COMPOSED_TYPE || messageType == MsgCodec.COMPOSED_ABA_TYPE) { + count++; + _incrementInbound(_origin.srcEid); + endpoint.sendCompose(address(this), _guid, 0, _message); + } else if (messageType == MsgCodec.ABA_TYPE) { + count++; + _incrementInbound(_origin.srcEid); + + // send back to the sender + _incrementOutbound(_origin.srcEid); + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 10); + _lzSend( + _origin.srcEid, + MsgCodec.encode(MsgCodec.VANILLA_TYPE, eid, 10), + options, + MessagingFee(msg.value, 0), + payable(address(this)) + ); + } else { + revert("invalid message type"); + } + } + + function _incrementInbound(uint32 _srcEid) internal { + inboundCount[_srcEid]++; + } + + function _incrementOutbound(uint32 _dstEid) internal { + outboundCount[_dstEid]++; + } + + function lzCompose( + address _oApp, + bytes32 /*_guid*/, + bytes calldata _message, + address, + bytes calldata + ) external payable override { + require(_oApp == address(this), "!oApp"); + require(msg.sender == address(endpoint), "!endpoint"); + + uint8 msgType = _message.msgType(); + if (msgType == MsgCodec.COMPOSED_TYPE) { + composedCount += 1; + } else if (msgType == MsgCodec.COMPOSED_ABA_TYPE) { + composedCount += 1; + + uint32 srcEid = _message.srcEid(); + _incrementOutbound(srcEid); + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + _lzSend( + srcEid, + MsgCodec.encode(MsgCodec.VANILLA_TYPE, eid), + options, + MessagingFee(msg.value, 0), + payable(address(this)) + ); + } else { + revert("invalid message type"); + } + } + + // ------------------------------- + // Ordered OApp + // this demonstrates how to build an app that requires execution nonce ordering + // normally an app should decide ordered or not on contract construction + // this is just a demo + function setOrderedNonce(bool _orderedNonce) external onlyOwner { + orderedNonce = _orderedNonce; + } + + function _acceptNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) internal virtual { + uint64 currentNonce = maxReceivedNonce[_srcEid][_sender]; + if (orderedNonce) { + require(_nonce == currentNonce + 1, "OApp: invalid nonce"); + } + // update the max nonce anyway. once the ordered mode is turned on, missing early nonces will be rejected + if (_nonce > currentNonce) { + maxReceivedNonce[_srcEid][_sender] = _nonce; + } + } + + function nextNonce(uint32 _srcEid, bytes32 _sender) public view virtual override returns (uint64) { + if (orderedNonce) { + return maxReceivedNonce[_srcEid][_sender] + 1; + } else { + return 0; // path nonce starts from 1. if 0 it means that there is no specific nonce enforcement + } + } + + // TODO should override oApp version with added ordered nonce increment + // a governance function to skip nonce + function skipInboundNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) public virtual onlyOwner { + endpoint.skip(address(this), _srcEid, _sender, _nonce); + if (orderedNonce) { + maxReceivedNonce[_srcEid][_sender]++; + } + } + + function isPeer(uint32 _eid, bytes32 _peer) public view override returns (bool) { + return peers[_eid] == _peer; + } + + // @dev Batch send requires overriding this function from OAppSender because the msg.value contains multiple fees + function _payNative(uint256 _nativeFee) internal virtual override returns (uint256 nativeFee) { + if (msg.value < _nativeFee) revert NotEnoughNative(msg.value); + return _nativeFee; + } + + // be able to receive ether + receive() external payable virtual {} + + fallback() external payable {} +} diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppComputer.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppComputer.sol new file mode 100644 index 0000000..9767642 --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppComputer.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { IOAppComputerReduce } from "./IOAppComputerReduce.sol"; +import { IOAppComputerMap } from "./IOAppComputerMap.sol"; + +interface IOAppComputer is IOAppComputerMap, IOAppComputerReduce {} diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppComputerMap.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppComputerMap.sol new file mode 100644 index 0000000..9514dad --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppComputerMap.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +interface IOAppComputerMap { + function lzMap(bytes calldata _request, bytes calldata _response) external view returns (bytes memory); +} diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppComputerReduce.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppComputerReduce.sol new file mode 100644 index 0000000..40e19f4 --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppComputerReduce.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +interface IOAppComputerReduce { + function lzReduce(bytes calldata _cmd, bytes[] calldata _responses) external view returns (bytes memory); +} diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppReceiver.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppReceiver.sol index 425f9f5..2dcb996 100644 --- a/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppReceiver.sol +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/interfaces/IOAppReceiver.sol @@ -5,11 +5,21 @@ import { ILayerZeroReceiver, Origin } from "@layerzerolabs/lz-evm-protocol-v2/co interface IOAppReceiver is ILayerZeroReceiver { /** - * @notice Retrieves the address responsible for 'sending' composeMsg's to the Endpoint. - * @return sender The address responsible for 'sending' composeMsg's to the Endpoint. + * @notice Indicates whether an address is an approved composeMsg sender to the Endpoint. + * @param _origin The origin information containing the source endpoint and sender address. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address on the src chain. + * - nonce: The nonce of the message. + * @param _message The lzReceive payload. + * @param _sender The sender address. + * @return isSender Is a valid sender. * * @dev Applications can optionally choose to implement a separate composeMsg sender that is NOT the bridging layer. - * @dev The default sender IS the OApp implementer. + * @dev The default sender IS the OAppReceiver implementer. */ - function composeMsgSender() external view returns (address sender); + function isComposeMsgSender( + Origin calldata _origin, + bytes calldata _message, + address _sender + ) external view returns (bool isSender); } diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/libs/OAppOptionsType3.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/libs/OAppOptionsType3.sol index 5b1159a..edb4447 100644 --- a/packages/layerzero-v2/evm/oapp/contracts/oapp/libs/OAppOptionsType3.sol +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/libs/OAppOptionsType3.sol @@ -26,6 +26,19 @@ abstract contract OAppOptionsType3 is IOAppOptionsType3, Ownable { * if you are only making a standard LayerZero message ie. lzReceive() WITHOUT sendCompose(). */ function setEnforcedOptions(EnforcedOptionParam[] calldata _enforcedOptions) public virtual onlyOwner { + _setEnforcedOptions(_enforcedOptions); + } + + /** + * @dev Sets the enforced options for specific endpoint and message type combinations. + * @param _enforcedOptions An array of EnforcedOptionParam structures specifying enforced options. + * + * @dev Provides a way for the OApp to enforce things like paying for PreCrime, AND/OR minimum dst lzReceive gas amounts etc. + * @dev These enforced options can vary as the potential options/execution on the remote may differ as per the msgType. + * eg. Amount of lzReceive() gas necessary to deliver a lzCompose() message adds overhead you dont want to pay + * if you are only making a standard LayerZero message ie. lzReceive() WITHOUT sendCompose(). + */ + function _setEnforcedOptions(EnforcedOptionParam[] memory _enforcedOptions) internal virtual { for (uint256 i = 0; i < _enforcedOptions.length; i++) { // @dev Enforced options are only available for optionType 3, as type 1 and 2 dont support combining. _assertOptionsType3(_enforcedOptions[i].options); @@ -75,8 +88,11 @@ abstract contract OAppOptionsType3 is IOAppOptionsType3, Ownable { * @dev Internal function to assert that options are of type 3. * @param _options The options to be checked. */ - function _assertOptionsType3(bytes calldata _options) internal pure virtual { - uint16 optionsType = uint16(bytes2(_options[0:2])); + function _assertOptionsType3(bytes memory _options) internal pure virtual { + uint16 optionsType; + assembly { + optionsType := mload(add(_options, 2)) + } if (optionsType != OPTION_TYPE_3) revert InvalidOptions(_options); } } diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/libs/ReadCmdCodecV1.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/libs/ReadCmdCodecV1.sol new file mode 100644 index 0000000..0943bfb --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/libs/ReadCmdCodecV1.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +struct EVMCallRequestV1 { + uint16 appRequestLabel; // Label identifying the application or type of request (can be use in lzCompute) + uint32 targetEid; // Target endpoint ID (representing a target blockchain) + bool isBlockNum; // True if the request = block number, false if timestamp + uint64 blockNumOrTimestamp; // Block number or timestamp to use in the request + uint16 confirmations; // Number of block confirmations on top of the requested block number or timestamp before the view function can be called + address to; // Address of the target contract on the target chain + bytes callData; // Calldata for the contract call +} + +struct EVMCallComputeV1 { + uint8 computeSetting; // Compute setting (0 = map only, 1 = reduce only, 2 = map reduce) + uint32 targetEid; // Target endpoint ID (representing a target blockchain) + bool isBlockNum; // True if the request = block number, false if timestamp + uint64 blockNumOrTimestamp; // Block number or timestamp to use in the request + uint16 confirmations; // Number of block confirmations on top of the requested block number or timestamp before the view function can be called + address to; // Address of the target contract on the target chain +} + +library ReadCmdCodecV1 { + using SafeCast for uint256; + + uint16 internal constant CMD_VERSION = 1; + + uint8 internal constant REQUEST_VERSION = 1; + uint16 internal constant RESOLVER_TYPE_SINGLE_VIEW_EVM_CALL = 1; + + uint8 internal constant COMPUTE_VERSION = 1; + uint16 internal constant COMPUTE_TYPE_SINGLE_VIEW_EVM_CALL = 1; + + error InvalidVersion(); + error InvalidType(); + + function decode( + bytes calldata _cmd + ) + internal + pure + returns (uint16 appCmdLabel, EVMCallRequestV1[] memory evmCallRequests, EVMCallComputeV1 memory compute) + { + uint256 offset = 0; + uint16 cmdVersion = uint16(bytes2(_cmd[offset:offset + 2])); + offset += 2; + if (cmdVersion != CMD_VERSION) revert InvalidVersion(); + + appCmdLabel = uint16(bytes2(_cmd[offset:offset + 2])); + offset += 2; + + (evmCallRequests, offset) = decodeRequestsV1(_cmd, offset); + + // decode the compute if it exists + if (offset < _cmd.length) { + (compute, ) = decodeEVMCallComputeV1(_cmd, offset); + } + } + + function decodeRequestsV1( + bytes calldata _cmd, + uint256 _offset + ) internal pure returns (EVMCallRequestV1[] memory evmCallRequests, uint256 newOffset) { + newOffset = _offset; + uint16 requestCount = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + + evmCallRequests = new EVMCallRequestV1[](requestCount); + for (uint16 i = 0; i < requestCount; i++) { + uint8 requestVersion = uint8(_cmd[newOffset]); + newOffset += 1; + if (requestVersion != REQUEST_VERSION) revert InvalidVersion(); + + uint16 appRequestLabel = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + + uint16 resolverType = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + + if (resolverType == RESOLVER_TYPE_SINGLE_VIEW_EVM_CALL) { + (EVMCallRequestV1 memory request, uint256 nextOffset) = decodeEVMCallRequestV1( + _cmd, + newOffset, + appRequestLabel + ); + newOffset = nextOffset; + evmCallRequests[i] = request; + } else { + revert InvalidType(); + } + } + } + + function decodeEVMCallRequestV1( + bytes calldata _cmd, + uint256 _offset, + uint16 _appRequestLabel + ) internal pure returns (EVMCallRequestV1 memory request, uint256 newOffset) { + newOffset = _offset; + request.appRequestLabel = _appRequestLabel; + + uint16 requestSize = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + request.targetEid = uint32(bytes4(_cmd[newOffset:newOffset + 4])); + newOffset += 4; + request.isBlockNum = uint8(_cmd[newOffset]) == 1; + newOffset += 1; + request.blockNumOrTimestamp = uint64(bytes8(_cmd[newOffset:newOffset + 8])); + newOffset += 8; + request.confirmations = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + request.to = address(bytes20(_cmd[newOffset:newOffset + 20])); + newOffset += 20; + uint16 callDataSize = requestSize - 35; + request.callData = _cmd[newOffset:newOffset + callDataSize]; + newOffset += callDataSize; + } + + function decodeEVMCallComputeV1( + bytes calldata _cmd, + uint256 _offset + ) internal pure returns (EVMCallComputeV1 memory compute, uint256 newOffset) { + newOffset = _offset; + uint8 computeVersion = uint8(_cmd[newOffset]); + newOffset += 1; + if (computeVersion != COMPUTE_VERSION) revert InvalidVersion(); + uint16 computeType = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + if (computeType != COMPUTE_TYPE_SINGLE_VIEW_EVM_CALL) revert InvalidType(); + + compute.computeSetting = uint8(_cmd[newOffset]); + newOffset += 1; + compute.targetEid = uint32(bytes4(_cmd[newOffset:newOffset + 4])); + newOffset += 4; + compute.isBlockNum = uint8(_cmd[newOffset]) == 1; + newOffset += 1; + compute.blockNumOrTimestamp = uint64(bytes8(_cmd[newOffset:newOffset + 8])); + newOffset += 8; + compute.confirmations = uint16(bytes2(_cmd[newOffset:newOffset + 2])); + newOffset += 2; + compute.to = address(bytes20(_cmd[newOffset:newOffset + 20])); + newOffset += 20; + } + + function decodeCmdAppLabel(bytes calldata _cmd) internal pure returns (uint16) { + uint256 offset = 0; + uint16 cmdVersion = uint16(bytes2(_cmd[offset:offset + 2])); + offset += 2; + if (cmdVersion != CMD_VERSION) revert InvalidVersion(); + + return uint16(bytes2(_cmd[offset:offset + 2])); + } + + function decodeRequestV1AppRequestLabel(bytes calldata _request) internal pure returns (uint16) { + uint256 offset = 0; + uint8 requestVersion = uint8(_request[offset]); + offset += 1; + if (requestVersion != REQUEST_VERSION) revert InvalidVersion(); + + return uint16(bytes2(_request[offset:offset + 2])); + } + + function encode( + uint16 _appCmdLabel, + EVMCallRequestV1[] memory _evmCallRequests, + EVMCallComputeV1 memory _evmCallCompute + ) internal pure returns (bytes memory) { + bytes memory cmd = encode(_appCmdLabel, _evmCallRequests); + if (_evmCallCompute.targetEid != 0) { + // if eid is 0, it means no compute + cmd = appendEVMCallComputeV1(cmd, _evmCallCompute); + } + return cmd; + } + + function encode( + uint16 _appCmdLabel, + EVMCallRequestV1[] memory _evmCallRequests + ) internal pure returns (bytes memory) { + bytes memory cmd = abi.encodePacked(CMD_VERSION, _appCmdLabel, _evmCallRequests.length.toUint16()); + for (uint256 i = 0; i < _evmCallRequests.length; i++) { + cmd = appendEVMCallRequestV1(cmd, _evmCallRequests[i]); + } + return cmd; + } + + // todo: optimize this with Buffer + function appendEVMCallRequestV1( + bytes memory _cmd, + EVMCallRequestV1 memory _request + ) internal pure returns (bytes memory) { + bytes memory newCmd = abi.encodePacked( + _cmd, + REQUEST_VERSION, + _request.appRequestLabel, + RESOLVER_TYPE_SINGLE_VIEW_EVM_CALL, + (_request.callData.length + 35).toUint16(), + _request.targetEid + ); + return + abi.encodePacked( + newCmd, + _request.isBlockNum, + _request.blockNumOrTimestamp, + _request.confirmations, + _request.to, + _request.callData + ); + } + + function appendEVMCallComputeV1( + bytes memory _cmd, + EVMCallComputeV1 memory _compute + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _cmd, + COMPUTE_VERSION, + COMPUTE_TYPE_SINGLE_VIEW_EVM_CALL, + _compute.computeSetting, + _compute.targetEid, + _compute.isBlockNum, + _compute.blockNumOrTimestamp, + _compute.confirmations, + _compute.to + ); + } +} diff --git a/packages/layerzero-v2/evm/oapp/contracts/oapp/utils/RateLimiter.sol b/packages/layerzero-v2/evm/oapp/contracts/oapp/utils/RateLimiter.sol new file mode 100644 index 0000000..1149701 --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/contracts/oapp/utils/RateLimiter.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title RateLimiter + * @dev Abstract contract for implementing rate limiting functionality. This contract provides a basic framework for + * rate limiting how often a function can be executed. It is designed to be inherited by other contracts requiring rate + * limiting capabilities to protect resources or services from excessive use. + * + * Example 1: Max rate limit reached at beginning of window. As time continues the amount of in flights comes down. + * + * Rate Limit Config: + * limit: 100 units + * window: 60 seconds + * + * Amount in Flight (units) vs. Time Graph (seconds) + * + * 100 | * - (Max limit reached at beginning of window) + * | * + * | * + * | * + * 50 | * (After 30 seconds only 50 units in flight) + * | * + * | * + * | * + * 0 +--|---|---|---|---|-->(After 60 seconds 0 units are in flight) + * 0 15 30 45 60 (seconds) + * + * Example 2: Max rate limit reached at beginning of window. As time continues the amount of in flights comes down + * allowing for more to be sent. At the 90 second mark, more in flights come in. + * + * Rate Limit Config: + * limit: 100 units + * window: 60 seconds + * + * Amount in Flight (units) vs. Time Graph (seconds) + * + * 100 | * - (Max limit reached at beginning of window) + * | * + * | * + * | * + * 50 | * * (50 inflight) + * | * * + * | * * + * | * * + * 0 +--|--|--|--|--|--|--|--|--|--> Time + * 0 15 30 45 60 75 90 105 120 (seconds) + * + * Example 3: Max rate limit reached at beginning of window. At the 15 second mark, the window gets updated to 60 + * seconds and the limit gets updated to 50 units. This scenario shows the direct depiction of "in flight" from the + * previous window affecting the current window. + * + * Initial Rate Limit Config: For first 15 seconds + * limit: 100 units + * window: 30 seconds + * + * Updated Rate Limit Config: Updated at 15 second mark + * limit: 50 units + * window: 60 seconds + * + * Amount in Flight (units) vs. Time Graph (seconds) + * 100 - * + * |* + * | * + * | * + * | * + * | * + * | * + * 75 - | * + * | * + * | * + * | * + * | * + * | * + * | * + * | * + * 50 - | 𐫰 <--(Slope changes at the 15 second mark because of the update. + * | ✧ * Window extended to 60 seconds and limit reduced to 50 units. + * | ✧ ︎ * Because amountInFlight/lastUpdated do not reset, 50 units are + * | ✧ * considered in flight from the previous window and the corresponding + * | ✧ ︎ * decay from the previous rate.) + * | ✧ * + * 25 - | ✧ * + * | ✧ * + * | ✧ * + * | ✧ * + * | ✧ * + * | ✧ * + * | ✧ * + * | ✧ * + * 0 - +---|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----> Time + * 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 (seconds) + * [ Initial 30 Second Window ] + * [ --------------- Extended 60 Second Window --------------- ] + */ +abstract contract RateLimiter { + /** + * @notice Rate Limit struct. + * @param amountInFlight The amount in the current window. + * @param lastUpdated Timestamp representing the last time the rate limit was checked or updated. + * @param limit This represents the maximum allowed amount within a given window. + * @param window Defines the duration of the rate limiting window. + */ + struct RateLimit { + uint256 amountInFlight; + uint256 lastUpdated; + uint256 limit; + uint256 window; + } + + /** + * @notice Rate Limit Configuration struct. + * @param dstEid The destination endpoint id. + * @param limit This represents the maximum allowed amount within a given window. + * @param window Defines the duration of the rate limiting window. + */ + struct RateLimitConfig { + uint32 dstEid; + uint256 limit; + uint256 window; + } + + /** + * @dev Mapping from destination endpoint id to RateLimit Configurations. + */ + mapping(uint32 dstEid => RateLimit limit) public rateLimits; + + /** + * @notice Emitted when _setRateLimits occurs. + * @param rateLimitConfigs An array of `RateLimitConfig` structs representing the rate limit configurations set. + * - `dstEid`: The destination endpoint id. + * - `limit`: This represents the maximum allowed amount within a given window. + * - `window`: Defines the duration of the rate limiting window. + */ + event RateLimitsChanged(RateLimitConfig[] rateLimitConfigs); + + /** + * @notice Error that is thrown when an amount exceeds the rate_limit. + */ + error RateLimitExceeded(); + + /** + * @notice Get the current amount that can be sent to this destination endpoint id for the given rate limit window. + * @param _dstEid The destination endpoint id. + * @return currentAmountInFlight The current amount that was sent. + * @return amountCanBeSent The amount that can be sent. + */ + function getAmountCanBeSent( + uint32 _dstEid + ) external view virtual returns (uint256 currentAmountInFlight, uint256 amountCanBeSent) { + RateLimit memory rl = rateLimits[_dstEid]; + return _amountCanBeSent(rl.amountInFlight, rl.lastUpdated, rl.limit, rl.window); + } + + /** + * @notice Sets the Rate Limit. + * @param _rateLimitConfigs A `RateLimitConfig` struct representing the rate limit configuration. + * - `dstEid`: The destination endpoint id. + * - `limit`: This represents the maximum allowed amount within a given window. + * - `window`: Defines the duration of the rate limiting window. + */ + function _setRateLimits(RateLimitConfig[] memory _rateLimitConfigs) internal virtual { + unchecked { + for (uint256 i = 0; i < _rateLimitConfigs.length; i++) { + RateLimit storage rl = rateLimits[_rateLimitConfigs[i].dstEid]; + + // @dev Ensure we checkpoint the existing rate limit as to not retroactively apply the new decay rate. + _checkAndUpdateRateLimit(_rateLimitConfigs[i].dstEid, 0); + + // @dev Does NOT reset the amountInFlight/lastUpdated of an existing rate limit. + rl.limit = _rateLimitConfigs[i].limit; + rl.window = _rateLimitConfigs[i].window; + } + } + emit RateLimitsChanged(_rateLimitConfigs); + } + + /** + * @notice Checks current amount in flight and amount that can be sent for a given rate limit window. + * @param _amountInFlight The amount in the current window. + * @param _lastUpdated Timestamp representing the last time the rate limit was checked or updated. + * @param _limit This represents the maximum allowed amount within a given window. + * @param _window Defines the duration of the rate limiting window. + * @return currentAmountInFlight The amount in the current window. + * @return amountCanBeSent The amount that can be sent. + */ + function _amountCanBeSent( + uint256 _amountInFlight, + uint256 _lastUpdated, + uint256 _limit, + uint256 _window + ) internal view virtual returns (uint256 currentAmountInFlight, uint256 amountCanBeSent) { + uint256 timeSinceLastDeposit = block.timestamp - _lastUpdated; + if (timeSinceLastDeposit >= _window) { + currentAmountInFlight = 0; + amountCanBeSent = _limit; + } else { + // @dev Presumes linear decay. + uint256 decay = (_limit * timeSinceLastDeposit) / _window; + currentAmountInFlight = _amountInFlight <= decay ? 0 : _amountInFlight - decay; + // @dev In the event the _limit is lowered, and the 'in-flight' amount is higher than the _limit, set to 0. + amountCanBeSent = _limit <= currentAmountInFlight ? 0 : _limit - currentAmountInFlight; + } + } + + /** + * @notice Verifies whether the specified amount falls within the rate limit constraints for the targeted + * endpoint ID. On successful verification, it updates amountInFlight and lastUpdated. If the amount exceeds + * the rate limit, the operation reverts. + * @param _dstEid The destination endpoint id. + * @param _amount The amount to check for rate limit constraints. + */ + function _checkAndUpdateRateLimit(uint32 _dstEid, uint256 _amount) internal virtual { + // @dev By default dstEid that have not been explicitly set will return amountCanBeSent == 0. + RateLimit storage rl = rateLimits[_dstEid]; + + (uint256 currentAmountInFlight, uint256 amountCanBeSent) = _amountCanBeSent( + rl.amountInFlight, + rl.lastUpdated, + rl.limit, + rl.window + ); + if (_amount > amountCanBeSent) revert RateLimitExceeded(); + + // @dev Update the storage to contain the new amount and current timestamp. + rl.amountInFlight = currentAmountInFlight + _amount; + rl.lastUpdated = block.timestamp; + } +} diff --git a/packages/layerzero-v2/evm/oapp/contracts/oft/OFT.sol b/packages/layerzero-v2/evm/oapp/contracts/oft/OFT.sol index c96364e..03d0c46 100644 --- a/packages/layerzero-v2/evm/oapp/contracts/oft/OFT.sol +++ b/packages/layerzero-v2/evm/oapp/contracts/oft/OFT.sol @@ -24,27 +24,13 @@ abstract contract OFT is OFTCore, ERC20 { address _delegate ) ERC20(_name, _symbol) OFTCore(decimals(), _lzEndpoint, _delegate) {} - /** - * @notice Retrieves interfaceID and the version of the OFT. - * @return interfaceId The interface ID. - * @return version The version. - * - * @dev interfaceId: This specific interface ID is '0x02e49c2c'. - * @dev version: Indicates a cross-chain compatible msg encoding with other OFTs. - * @dev If a new feature is added to the OFT cross-chain msg encoding, the version will be incremented. - * ie. localOFT version(x,1) CAN send messages to remoteOFT version(x,1) - */ - function oftVersion() external pure virtual returns (bytes4 interfaceId, uint64 version) { - return (type(IOFT).interfaceId, 1); - } - /** * @dev Retrieves the address of the underlying ERC20 implementation. * @return The address of the OFT token. * * @dev In the case of OFT, address(this) and erc20 are the same contract. */ - function token() external view returns (address) { + function token() public view returns (address) { return address(this); } @@ -60,6 +46,7 @@ abstract contract OFT is OFTCore, ERC20 { /** * @dev Burns tokens from the sender's specified balance. + * @param _from The address to debit the tokens from. * @param _amountLD The amount of tokens to send in local decimals. * @param _minAmountLD The minimum amount to send in local decimals. * @param _dstEid The destination chain ID. @@ -67,6 +54,7 @@ abstract contract OFT is OFTCore, ERC20 { * @return amountReceivedLD The amount received in local decimals on the remote. */ function _debit( + address _from, uint256 _amountLD, uint256 _minAmountLD, uint32 _dstEid @@ -77,7 +65,7 @@ abstract contract OFT is OFTCore, ERC20 { // therefore amountSentLD CAN differ from amountReceivedLD. // @dev Default OFT burns on src. - _burn(msg.sender, amountSentLD); + _burn(_from, amountSentLD); } /** @@ -92,6 +80,7 @@ abstract contract OFT is OFTCore, ERC20 { uint256 _amountLD, uint32 /*_srcEid*/ ) internal virtual override returns (uint256 amountReceivedLD) { + if (_to == address(0x0)) _to = address(0xdead); // _mint(...) does not support address(0x0) // @dev Default OFT mints on dst. _mint(_to, _amountLD); // @dev In the case of NON-default OFT, the _amountLD MIGHT not be == amountReceivedLD. diff --git a/packages/layerzero-v2/evm/oapp/contracts/oft/OFTAdapter.sol b/packages/layerzero-v2/evm/oapp/contracts/oft/OFTAdapter.sol index a3b4845..82d09ea 100644 --- a/packages/layerzero-v2/evm/oapp/contracts/oft/OFTAdapter.sol +++ b/packages/layerzero-v2/evm/oapp/contracts/oft/OFTAdapter.sol @@ -36,27 +36,13 @@ abstract contract OFTAdapter is OFTCore { innerToken = IERC20(_token); } - /** - * @notice Retrieves interfaceID and the version of the OFT. - * @return interfaceId The interface ID. - * @return version The version. - * - * @dev interfaceId: This specific interface ID is '0x02e49c2c'. - * @dev version: Indicates a cross-chain compatible msg encoding with other OFTs. - * @dev If a new feature is added to the OFT cross-chain msg encoding, the version will be incremented. - * ie. localOFT version(x,1) CAN send messages to remoteOFT version(x,1) - */ - function oftVersion() external pure virtual returns (bytes4 interfaceId, uint64 version) { - return (type(IOFT).interfaceId, 1); - } - /** * @dev Retrieves the address of the underlying ERC20 implementation. * @return The address of the adapted ERC-20 token. * * @dev In the case of OFTAdapter, address(this) and erc20 are NOT the same contract. */ - function token() external view returns (address) { + function token() public view returns (address) { return address(innerToken); } @@ -73,6 +59,7 @@ abstract contract OFTAdapter is OFTCore { /** * @dev Burns tokens from the sender's specified balance, ie. pull method. + * @param _from The address to debit from. * @param _amountLD The amount of tokens to send in local decimals. * @param _minAmountLD The minimum amount to send in local decimals. * @param _dstEid The destination chain ID. @@ -85,13 +72,14 @@ abstract contract OFTAdapter is OFTCore { * a pre/post balance check will need to be done to calculate the amountReceivedLD. */ function _debit( + address _from, uint256 _amountLD, uint256 _minAmountLD, uint32 _dstEid ) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) { (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid); // @dev Lock tokens by moving them into this contract from the caller. - innerToken.safeTransferFrom(msg.sender, address(this), amountSentLD); + innerToken.safeTransferFrom(_from, address(this), amountSentLD); } /** diff --git a/packages/layerzero-v2/evm/oapp/contracts/oft/OFTCore.sol b/packages/layerzero-v2/evm/oapp/contracts/oft/OFTCore.sol index fee9e9b..d5ccf67 100644 --- a/packages/layerzero-v2/evm/oapp/contracts/oft/OFTCore.sol +++ b/packages/layerzero-v2/evm/oapp/contracts/oft/OFTCore.sol @@ -56,6 +56,20 @@ abstract contract OFTCore is IOFT, OApp, OAppPreCrimeSimulator, OAppOptionsType3 decimalConversionRate = 10 ** (_localDecimals - sharedDecimals()); } + /** + * @notice Retrieves interfaceID and the version of the OFT. + * @return interfaceId The interface ID. + * @return version The version. + * + * @dev interfaceId: This specific interface ID is '0x02e49c2c'. + * @dev version: Indicates a cross-chain compatible msg encoding with other OFTs. + * @dev If a new feature is added to the OFT cross-chain msg encoding, the version will be incremented. + * ie. localOFT version(x,1) CAN send messages to remoteOFT version(x,1) + */ + function oftVersion() external pure virtual returns (bytes4 interfaceId, uint64 version) { + return (type(IOFT).interfaceId, 1); + } + /** * @dev Retrieves the shared decimals of the OFT. * @return The shared decimals of the OFT. @@ -66,7 +80,7 @@ abstract contract OFTCore is IOFT, OApp, OAppPreCrimeSimulator, OAppOptionsType3 * For tokens exceeding this totalSupply(), they will need to override the sharedDecimals function with something smaller. * ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615 */ - function sharedDecimals() public pure virtual returns (uint8) { + function sharedDecimals() public view virtual returns (uint8) { return 6; } @@ -165,6 +179,7 @@ abstract contract OFTCore is IOFT, OApp, OAppPreCrimeSimulator, OAppOptionsType3 // - amountSentLD is the amount in local decimals that was ACTUALLY sent/debited from the sender. // - amountReceivedLD is the amount in local decimals that will be received/credited to the recipient on the remote OFT instance. (uint256 amountSentLD, uint256 amountReceivedLD) = _debit( + msg.sender, _sendParam.amountLD, _sendParam.minAmountLD, _sendParam.dstEid @@ -349,6 +364,7 @@ abstract contract OFTCore is IOFT, OApp, OAppPreCrimeSimulator, OAppOptionsType3 /** * @dev Internal function to perform a debit operation. + * @param _from The address to debit. * @param _amountLD The amount to send in local decimals. * @param _minAmountLD The minimum amount to send in local decimals. * @param _dstEid The destination endpoint ID. @@ -359,6 +375,7 @@ abstract contract OFTCore is IOFT, OApp, OAppPreCrimeSimulator, OAppOptionsType3 * @dev Depending on OFT implementation the _amountLD could differ from the amountReceivedLD. */ function _debit( + address _from, uint256 _amountLD, uint256 _minAmountLD, uint32 _dstEid diff --git a/packages/layerzero-v2/evm/oapp/contracts/precrime/PreCrime.sol b/packages/layerzero-v2/evm/oapp/contracts/precrime/PreCrime.sol index f1e2b46..320eaad 100644 --- a/packages/layerzero-v2/evm/oapp/contracts/precrime/PreCrime.sol +++ b/packages/layerzero-v2/evm/oapp/contracts/precrime/PreCrime.sol @@ -135,7 +135,7 @@ abstract contract PreCrime is Ownable, IPreCrime { if (_packets.length > maxBatchSize) revert PacketOversize(maxBatchSize, _packets.length); // check packets nonce, sequence order - // packets should group by srcEid and sender, then sort by nonce ascending + // packets should ordered in ascending order by srcEid, sender, nonce if (_packets.length > 0) { uint32 srcEid; bytes32 sender; @@ -146,8 +146,12 @@ abstract contract PreCrime is Ownable, IPreCrime { // skip if not from trusted peer if (!IOAppPreCrimeSimulator(simulator).isPeer(packet.origin.srcEid, packet.origin.sender)) continue; - // start from a new chain or a new source oApp - if (packet.origin.srcEid != srcEid || packet.origin.sender != sender) { + if ( + packet.origin.srcEid < srcEid || (packet.origin.srcEid == srcEid && packet.origin.sender < sender) + ) { + revert PacketUnsorted(); + } else if (packet.origin.srcEid != srcEid || packet.origin.sender != sender) { + // start from a new chain or a new source oApp srcEid = packet.origin.srcEid; sender = packet.origin.sender; nonce = _getInboundNonce(srcEid, sender); diff --git a/packages/layerzero-v2/evm/oapp/test/OFT.t.sol b/packages/layerzero-v2/evm/oapp/test/OFT.t.sol index f7d3220..40be3c6 100644 --- a/packages/layerzero-v2/evm/oapp/test/OFT.t.sol +++ b/packages/layerzero-v2/evm/oapp/test/OFT.t.sol @@ -99,14 +99,15 @@ contract OFTTest is TestHelper { assertEq(interfaceId, expectedId); } - function test_send_oft() public { - uint256 tokensToSend = 1 ether; + function test_send_oft(uint256 tokensToSend) public { + vm.assume(tokensToSend > 0.001 ether && tokensToSend < 100 ether); // avoid reverting due to SlippageExceeded + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); SendParam memory sendParam = SendParam( bEid, addressToBytes32(userB), tokensToSend, - tokensToSend, + (tokensToSend * 9_900) / 10_000, // allow 1% slippage options, "", "" @@ -117,15 +118,20 @@ contract OFTTest is TestHelper { assertEq(bOFT.balanceOf(userB), initialBalance); vm.prank(userA); - aOFT.send{ value: fee.nativeFee }(sendParam, fee, payable(address(this))); + (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) = aOFT.send{ value: fee.nativeFee }( + sendParam, + fee, + payable(address(this)) + ); verifyPackets(bEid, addressToBytes32(address(bOFT))); - assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend); - assertEq(bOFT.balanceOf(userB), initialBalance + tokensToSend); + assertEq(msgReceipt.fee.nativeFee, fee.nativeFee); + assertEq(aOFT.balanceOf(userA), initialBalance - oftReceipt.amountSentLD); + assertEq(bOFT.balanceOf(userB), initialBalance + oftReceipt.amountReceivedLD); } - function test_send_oft_compose_msg() public { - uint256 tokensToSend = 1 ether; + function test_send_oft_compose_msg(uint256 tokensToSend) public { + vm.assume(tokensToSend > 0.001 ether && tokensToSend < 100 ether); // avoid reverting due to SlippageExceeded OFTComposerMock composer = new OFTComposerMock(); @@ -138,7 +144,7 @@ contract OFTTest is TestHelper { bEid, addressToBytes32(address(composer)), tokensToSend, - tokensToSend, + (tokensToSend * 9_900) / 10_000, // allow 1% slippage options, composeMsg, "" @@ -170,8 +176,8 @@ contract OFTTest is TestHelper { ); this.lzCompose(dstEid_, from_, options_, guid_, to_, composerMsg_); - assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend); - assertEq(bOFT.balanceOf(address(composer)), tokensToSend); + assertEq(aOFT.balanceOf(userA), initialBalance - oftReceipt.amountSentLD); + assertEq(bOFT.balanceOf(address(composer)), oftReceipt.amountReceivedLD); assertEq(composer.from(), from_); assertEq(composer.guid(), guid_); @@ -180,12 +186,12 @@ contract OFTTest is TestHelper { assertEq(composer.extraData(), composerMsg_); // default to setting the extraData to the message as well to test } - function test_oft_compose_codec() public { - uint64 nonce = 1; - uint32 srcEid = 2; - uint256 amountCreditLD = 3; - bytes memory composeMsg = hex"1234"; - + function test_oft_compose_codec( + uint64 nonce, + uint32 srcEid, + uint256 amountCreditLD, + bytes memory composeMsg + ) public { bytes memory message = OFTComposeMsgCodec.encode( nonce, srcEid, @@ -239,13 +245,12 @@ contract OFTTest is TestHelper { aOFT.debit(amountToSendLD, minAmountToCreditLD, dstEid); } - function test_toLD() public { - uint64 amountSD = 1000; + function test_toLD(uint64 amountSD) public { assertEq(amountSD * aOFT.decimalConversionRate(), aOFT.toLD(uint64(amountSD))); } - function test_toSD() public { - uint256 amountLD = 1000000; + function test_toSD(uint256 amountLD) public { + vm.assume(amountLD <= type(uint64).max); // avoid reverting due to overflow assertEq(amountLD / aOFT.decimalConversionRate(), aOFT.toSD(amountLD)); } @@ -337,15 +342,12 @@ contract OFTTest is TestHelper { composeMsg = OFTMsgCodec.composeMsg(message); } - function test_oft_build_msg() public { - uint32 dstEid = bEid; - bytes32 to = addressToBytes32(userA); - uint256 amountToSendLD = 1.23456789 ether; + function test_oft_build_msg(uint32 dstEid, bytes32 to, uint256 amountToSendLD, bytes memory composeMsg) public { + vm.assume(composeMsg.length > 0); // ensure there is a composed payload uint256 minAmountToCreditLD = aOFT.removeDust(amountToSendLD); // params for buildMsgAndOptions bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); - bytes memory composeMsg = hex"1234"; SendParam memory sendParam = SendParam( dstEid, to, @@ -370,10 +372,7 @@ contract OFTTest is TestHelper { assertEq(composeMsg_, expectedComposeMsg); } - function test_oft_build_msg_no_compose_msg() public { - uint32 dstEid = bEid; - bytes32 to = addressToBytes32(userA); - uint256 amountToSendLD = 1.23456789 ether; + function test_oft_build_msg_no_compose_msg(uint32 dstEid, bytes32 to, uint256 amountToSendLD) public { uint256 minAmountToCreditLD = aOFT.removeDust(amountToSendLD); // params for buildMsgAndOptions @@ -438,8 +437,7 @@ contract OFTTest is TestHelper { aOFT.setEnforcedOptions(enforcedOptions); // doesnt revert cus option type 3 } - function test_combine_options() public { - uint32 eid = 1; + function test_combine_options(uint32 eid, uint128 nativeDropGas, address user) public { uint16 msgType = 1; bytes memory enforcedOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); @@ -448,56 +446,55 @@ contract OFTTest is TestHelper { aOFT.setEnforcedOptions(enforcedOptionsArray); bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorNativeDropOption( - 1.2345 ether, - addressToBytes32(userA) + nativeDropGas, + addressToBytes32(user) ); bytes memory expectedOptions = OptionsBuilder .newOptions() .addExecutorLzReceiveOption(200000, 0) - .addExecutorNativeDropOption(1.2345 ether, addressToBytes32(userA)); + .addExecutorNativeDropOption(nativeDropGas, addressToBytes32(user)); bytes memory combinedOptions = aOFT.combineOptions(eid, msgType, extraOptions); assertEq(combinedOptions, expectedOptions); } - function test_combine_options_no_extra_options() public { - uint32 eid = 1; + function test_combine_options_no_extra_options(uint32 eid, uint128 gasLimit, uint128 nativeDrop) public { uint16 msgType = 1; - bytes memory enforcedOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + bytes memory enforcedOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(gasLimit, nativeDrop); EnforcedOptionParam[] memory enforcedOptionsArray = new EnforcedOptionParam[](1); enforcedOptionsArray[0] = EnforcedOptionParam(eid, msgType, enforcedOptions); aOFT.setEnforcedOptions(enforcedOptionsArray); - bytes memory expectedOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + bytes memory expectedOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(gasLimit, nativeDrop); bytes memory combinedOptions = aOFT.combineOptions(eid, msgType, ""); assertEq(combinedOptions, expectedOptions); } - function test_combine_options_no_enforced_options() public { - uint32 eid = 1; - uint16 msgType = 1; - + function test_combine_options_no_enforced_options( + uint32 eid, + uint16 msgType, + uint128 nativeDropGas, + address user + ) public { bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorNativeDropOption( - 1.2345 ether, - addressToBytes32(userA) + nativeDropGas, + addressToBytes32(user) ); bytes memory expectedOptions = OptionsBuilder.newOptions().addExecutorNativeDropOption( - 1.2345 ether, - addressToBytes32(userA) + nativeDropGas, + addressToBytes32(user) ); bytes memory combinedOptions = aOFT.combineOptions(eid, msgType, extraOptions); assertEq(combinedOptions, expectedOptions); } - function test_oapp_inspector_inspect() public { - uint32 dstEid = bEid; - bytes32 to = addressToBytes32(userA); - uint256 amountToSendLD = 1.23456789 ether; + function test_oapp_inspector_inspect(uint32 dstEid, address user, uint256 amountToSendLD) public { + bytes32 to = addressToBytes32(user); uint256 minAmountToCreditLD = aOFT.removeDust(amountToSendLD); // params for buildMsgAndOptions diff --git a/packages/layerzero-v2/evm/oapp/test/OmniCounter.t.sol b/packages/layerzero-v2/evm/oapp/test/OmniCounter.t.sol index 562c5ab..c39593a 100644 --- a/packages/layerzero-v2/evm/oapp/test/OmniCounter.t.sol +++ b/packages/layerzero-v2/evm/oapp/test/OmniCounter.t.sol @@ -74,25 +74,28 @@ contract OmniCounterTest is TestHelper { } // classic message passing A -> B - function test_increment() public { + function test_increment(uint8 numIncrements) public { + vm.assume(numIncrements > 0 && numIncrements < 10); // upper bound to ensure tests don't run too long uint256 counterBefore = bCounter.count(); bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); (uint256 nativeFee, ) = aCounter.quote(bEid, MsgCodec.VANILLA_TYPE, options); - aCounter.increment{ value: nativeFee }(bEid, MsgCodec.VANILLA_TYPE, options); - + for (uint8 i = 0; i < numIncrements; i++) { + aCounter.increment{ value: nativeFee }(bEid, MsgCodec.VANILLA_TYPE, options); + } assertEq(bCounter.count(), counterBefore, "shouldn't be increased until packet is verified"); // verify packet to bCounter manually verifyPackets(bEid, addressToBytes32(address(bCounter))); - assertEq(bCounter.count(), counterBefore + 1, "increment assertion failure"); + assertEq(bCounter.count(), counterBefore + numIncrements, "increment assertion failure"); } - function test_batchIncrement() public { + function test_batchIncrement(uint256 batchSize) public { + vm.assume(batchSize > 0 && batchSize < 10); + uint256 counterBefore = bCounter.count(); - uint256 batchSize = 5; uint32[] memory eids = new uint32[](batchSize); uint8[] memory types = new uint8[](batchSize); bytes[] memory options = new bytes[](batchSize); @@ -115,20 +118,21 @@ contract OmniCounterTest is TestHelper { assertEq(bCounter.count(), counterBefore + batchSize, "batchIncrement assertion failure"); } - function test_nativeDrop_increment() public { + function test_nativeDrop_increment(uint128 nativeDropGas) public { + vm.assume(nativeDropGas <= 100000000000000000); // avoid encountering Executor_NativeAmountExceedsCap uint256 balanceBefore = address(bCounter).balance; bytes memory options = OptionsBuilder .newOptions() .addExecutorLzReceiveOption(200000, 0) - .addExecutorNativeDropOption(1 gwei, addressToBytes32(address(bCounter))); + .addExecutorNativeDropOption(nativeDropGas, addressToBytes32(address(bCounter))); (uint256 nativeFee, ) = aCounter.quote(bEid, MsgCodec.VANILLA_TYPE, options); aCounter.increment{ value: nativeFee }(bEid, MsgCodec.VANILLA_TYPE, options); // verify packet to bCounter manually verifyPackets(bEid, addressToBytes32(address(bCounter))); - assertEq(address(bCounter).balance, balanceBefore + 1 gwei, "nativeDrop assertion failure"); + assertEq(address(bCounter).balance, balanceBefore + nativeDropGas, "nativeDrop assertion failure"); // transfer funds out address payable receiver = payable(address(0xABCD)); @@ -136,13 +140,13 @@ contract OmniCounterTest is TestHelper { // withdraw with non admin vm.startPrank(receiver); vm.expectRevert(); - bCounter.withdraw(receiver, 1 gwei); + bCounter.withdraw(receiver, nativeDropGas); vm.stopPrank(); // withdraw with admin - bCounter.withdraw(receiver, 1 gwei); + bCounter.withdraw(receiver, nativeDropGas); assertEq(address(bCounter).balance, 0, "withdraw assertion failure"); - assertEq(receiver.balance, 1 gwei, "withdraw assertion failure"); + assertEq(receiver.balance, nativeDropGas, "withdraw assertion failure"); } // classic message passing A -> B1 -> B2 diff --git a/packages/layerzero-v2/evm/oapp/test/RateLimiter.t.sol b/packages/layerzero-v2/evm/oapp/test/RateLimiter.t.sol new file mode 100644 index 0000000..353e16b --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/test/RateLimiter.t.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; +import "../contracts/oapp/utils/RateLimiter.sol"; + +contract RateLimiterImpl is RateLimiter { + constructor() {} + + function setRateLimits(RateLimitConfig[] memory _rateLimitConfigs) external { + _setRateLimits(_rateLimitConfigs); + } + + function checkAndUpdateRateLimit(uint32 _dstEid, uint256 _amount) external { + _checkAndUpdateRateLimit(_dstEid, _amount); + } +} + +contract RateLimiterTest is RateLimiterImpl, Test { + uint32 dstEid = 1; + uint256 sendLimit = 100 ether; + uint256 window = 1 hours; + uint256 amountInFlight; + uint256 amountCanBeSent; + RateLimiterImpl rateLimiterImpl; + + function setUp() public virtual { + vm.warp(0); + RateLimiter.RateLimitConfig[] memory rateLimitConfigs = new RateLimiter.RateLimitConfig[](1); + rateLimitConfigs[0] = RateLimiter.RateLimitConfig(dstEid, sendLimit, window); + + rateLimiterImpl = new RateLimiterImpl(); + rateLimiterImpl.setRateLimits(rateLimitConfigs); + } + + function test_max_rate_limit() public { + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit); + } + + function test_over_max_rate_limit() public { + vm.expectRevert(abi.encodeWithSelector(RateLimiter.RateLimitExceeded.selector)); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, 101 ether); + } + + function test_rate_limit_resets_after_window() public { + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit); + vm.warp(block.timestamp + 1 hours + 1 seconds); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit); + } + + function test_multiple_rate_limit_windows() public { + uint16[10] memory times = [1, 11, 233, 440, 666, 667, 778, 999, 1000, 3600]; + uint256 decay = 0; + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit); + for (uint256 i = 0; i < 10; i++) { + decay = (sendLimit * times[i]) / window; + vm.warp(times[i]); + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, sendLimit - decay); + assertEq(amountCanBeSent, decay); + } + } + + function test_rate_change_mid_window() public { + // Make sure you can send max limit + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, 0); + assertEq(amountCanBeSent, sendLimit); + + // Send max limit + vm.warp(0); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit); + + // Verify max in flight + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, sendLimit); + assertEq(amountCanBeSent, 0); + + // Expect revert when max in flight + vm.expectRevert(abi.encodeWithSelector(RateLimiter.RateLimitExceeded.selector)); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit); + + // Advance halfway through window + vm.warp(1800); + + // Verify amountInFlight/amountCanBeSent is half the sendLimit + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, sendLimit / 2); + assertEq(amountCanBeSent, sendLimit / 2); + + // update sendLimit to 2x + uint256 newLimit = 200 ether; + RateLimiter.RateLimitConfig[] memory rateLimitConfigs = new RateLimiter.RateLimitConfig[](1); + rateLimitConfigs[0] = RateLimiter.RateLimitConfig(dstEid, newLimit, window); + rateLimiterImpl.setRateLimits(rateLimitConfigs); + + // Verify amountInFlight is still half the sendLimit + // Verify amountCanBeSent is the newLimit - half the sendLimit + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, sendLimit / 2); + assertEq(amountCanBeSent, newLimit - sendLimit / 2); + + // Advance rest of the window + vm.warp(3600); + + // Verify new max limit can be sent + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, newLimit); + + // Expect revert when max in flight + vm.expectRevert(abi.encodeWithSelector(RateLimiter.RateLimitExceeded.selector)); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, 1 ether); + } + + function test_window_change_mid_window() public { + // Send max limit + vm.warp(0); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit); + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, sendLimit); + assertEq(amountCanBeSent, 0); + + // Advance 30 mins + vm.warp(1800); + + // Verify amountInFlight/amountCanBeSent is half the sendLimit + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, sendLimit / 2); + assertEq(amountCanBeSent, sendLimit / 2); + + // Update window to be 2x longer. + uint256 newWindow = 2 hours; + RateLimiter.RateLimitConfig[] memory rateLimitConfigs = new RateLimiter.RateLimitConfig[](1); + rateLimitConfigs[0] = RateLimiter.RateLimitConfig(dstEid, sendLimit, newWindow); + rateLimiterImpl.setRateLimits(rateLimitConfigs); + + // Verify amountInFlight/amountCanBeSent is still half the sendLimit + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, sendLimit / 2); + assertEq(amountCanBeSent, sendLimit / 2); + + // Expect anything more that half the sendLimit to revert + vm.expectRevert(abi.encodeWithSelector(RateLimiter.RateLimitExceeded.selector)); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit / 2 + 1 ether); + + // Advance another 30 mins + vm.warp(3600); + + // Verify amountInFlight is still 1/4 the sendLimit + // Verify amountCanBeSent is 3/4 the sendLimit + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, sendLimit / 4); + assertEq(amountCanBeSent, sendLimit - sendLimit / 4); + + // Advance another past the window + vm.warp(5400); + + // Verify max limit can be sent + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit); + + // Advance old window and make sure you cant send max limit because of newly set window + vm.warp(9000); + vm.expectRevert(abi.encodeWithSelector(RateLimiter.RateLimitExceeded.selector)); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit); + } + + function test_rate_and_window_change_mid_window() public { + // Send max limit + vm.warp(0); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, sendLimit); + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, sendLimit); + assertEq(amountCanBeSent, 0); + + // Advance 30 mins + vm.warp(1800); + + // Verify amountInFlight/amountCanBeSent is half the sendLimit + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, sendLimit / 2); + assertEq(amountCanBeSent, sendLimit / 2); + + // Update limit to 2x and window to 4x. + uint256 newLimit = 200 ether; + uint256 newWindow = 4 hours; + RateLimiter.RateLimitConfig[] memory rateLimitConfigs = new RateLimiter.RateLimitConfig[](1); + rateLimitConfigs[0] = RateLimiter.RateLimitConfig(dstEid, newLimit, newWindow); + rateLimiterImpl.setRateLimits(rateLimitConfigs); + + // The amountInFlight should be a 1/4 of the newLimit because the new rate limit provides capacity for 50 ETH/hour + // We sent 100 ETH an hour before the update. So one hour after the update, half of this capacity (50 ETH) is considered still in use + + // Verify amountInFlight is still half the sendLimit + // Verify amountCanBeSent is the newLimit - half the sendLimit + uint amountInFlightBeforeUpdate = sendLimit / 2; + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, amountInFlightBeforeUpdate); + assertEq(amountCanBeSent, newLimit - amountInFlightBeforeUpdate); + + // Advance another 30 mins + vm.warp(3600); + + // Verify amountInFlight is 1/4 the old sendLimit + // Verify amountCanBeSent is newLimit - 1/4 the old sendLimit + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + assertEq(amountInFlight, amountInFlightBeforeUpdate / 2); + assertEq(amountCanBeSent, newLimit - amountInFlightBeforeUpdate / 2); + + // Advance another 30 mins + vm.warp(5400); + // Verify new max limit can be sent + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, newLimit); + + // Verify max amount cant be sent for the rest of the window (4 hours left in window) + vm.expectRevert(abi.encodeWithSelector(RateLimiter.RateLimitExceeded.selector)); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, newLimit); + + // Advance another 60 mins + vm.warp(9000); + // Verify max amount cant be sent for the rest of the window (3 hours left in window) + vm.expectRevert(abi.encodeWithSelector(RateLimiter.RateLimitExceeded.selector)); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, newLimit); + + // Advance another 60 mins + vm.warp(12600); + // Verify max amount cant be sent for the rest of the window (2 hours left in window) + vm.expectRevert(abi.encodeWithSelector(RateLimiter.RateLimitExceeded.selector)); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, newLimit); + + // Advance another 60 mins + vm.warp(16200); + // Verify max amount cant be sent for the rest of the window (1 hours left in window) + vm.expectRevert(abi.encodeWithSelector(RateLimiter.RateLimitExceeded.selector)); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, newLimit); + + // Advance another 60 mins + vm.warp(19800); + (amountInFlight, amountCanBeSent) = rateLimiterImpl.getAmountCanBeSent(dstEid); + // Verify max amount can be sent when new window starts + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, newLimit); + + // Verify max inflight and cant send anymore at this point in time + vm.expectRevert(abi.encodeWithSelector(RateLimiter.RateLimitExceeded.selector)); + rateLimiterImpl.checkAndUpdateRateLimit(dstEid, 1 ether); + } +} diff --git a/packages/layerzero-v2/evm/oapp/test/TestHelper.sol b/packages/layerzero-v2/evm/oapp/test/TestHelper.sol index c9ada1a..4a3d3cc 100644 --- a/packages/layerzero-v2/evm/oapp/test/TestHelper.sol +++ b/packages/layerzero-v2/evm/oapp/test/TestHelper.sol @@ -123,11 +123,11 @@ contract TestHelper is Test, OptionsHelper { address(this), admins ); - ExecutorFeeLib executorLib = new ExecutorFeeLibMock(); + ExecutorFeeLib executorLib = new ExecutorFeeLibMock(1); executor.setWorkerFeeLib(address(executorLib)); - dvn = new DVN(i + 1, messageLibs, address(priceFeed), signers, 1, admins); - DVNFeeLib dvnLib = new DVNFeeLib(1e18); + dvn = new DVN(i + 1, i + 1, messageLibs, address(priceFeed), signers, 1, admins); + DVNFeeLib dvnLib = new DVNFeeLib(i + 1, 1e18); dvn.setWorkerFeeLib(address(dvnLib)); } diff --git a/packages/layerzero-v2/evm/oapp/test/lib/CmdCodecV1.t.sol b/packages/layerzero-v2/evm/oapp/test/lib/CmdCodecV1.t.sol new file mode 100644 index 0000000..622b73c --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/test/lib/CmdCodecV1.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.22; + +import { Test, console } from "forge-std/Test.sol"; +import { EVMCallRequestV1, EVMCallComputeV1, ReadCmdCodecV1 } from "../../contracts/oapp/libs/ReadCmdCodecV1.sol"; +import { CmdCodecV1Mock } from "../../contracts/oapp/examples/CmdCodecV1Mock.sol"; + +contract CmdCodecV1Test is Test { + CmdCodecV1Mock internal codec = new CmdCodecV1Mock(); + + function test_codec() public { + // requests + EVMCallRequestV1 memory evmCallRequest1 = EVMCallRequestV1({ + appRequestLabel: 1, + targetEid: 2, + isBlockNum: true, + blockNumOrTimestamp: 3, + confirmations: 4, + to: address(5), + callData: hex"1234" + }); + EVMCallRequestV1 memory evmCallRequest2 = EVMCallRequestV1({ + appRequestLabel: 2, + targetEid: 2, + isBlockNum: true, + blockNumOrTimestamp: 3, + confirmations: 4, + to: address(5), + callData: hex"5678" + }); + EVMCallRequestV1[] memory evmCallRequests = new EVMCallRequestV1[](2); + evmCallRequests[0] = evmCallRequest1; + evmCallRequests[1] = evmCallRequest2; + + // compute + EVMCallComputeV1 memory compute = EVMCallComputeV1({ + computeSetting: 1, + targetEid: 8, + isBlockNum: false, + blockNumOrTimestamp: 9, + confirmations: 10, + to: address(11) + }); + + uint16 appCmdLabel = 1; + bytes memory cmd = codec.encode(appCmdLabel, evmCallRequests, compute); + + ( + uint16 actualAppCmdLabel, + EVMCallRequestV1[] memory actualEvmCallRequests, + EVMCallComputeV1 memory actualCompute + ) = codec.decode(cmd); + + assertEq(actualAppCmdLabel, appCmdLabel, "AppCmdLabel should match"); + assertEVMCallRequestV1Eq(actualEvmCallRequests[0], evmCallRequest1); + assertEVMCallRequestV1Eq(actualEvmCallRequests[1], evmCallRequest2); + assertEVMCallComputeV1Eq(actualCompute, compute); + + // test no compute encode/decode + cmd = codec.encode(appCmdLabel, evmCallRequests); + + (actualAppCmdLabel, actualEvmCallRequests, actualCompute) = codec.decode(cmd); + assertEq(actualAppCmdLabel, appCmdLabel, "AppCmdLabel should match"); + assertEVMCallRequestV1Eq(actualEvmCallRequests[0], evmCallRequest1); + assertEVMCallRequestV1Eq(actualEvmCallRequests[1], evmCallRequest2); + + EVMCallComputeV1 memory emptyCompute; + assertEVMCallComputeV1Eq(actualCompute, emptyCompute); + } + + // ------------------------------- utils ------------------------------- + + function assertEVMCallRequestV1Eq(EVMCallRequestV1 memory a, EVMCallRequestV1 memory b) internal { + assertEq(a.appRequestLabel, b.appRequestLabel, "AppRequestLabel should match"); + assertEq(a.targetEid, b.targetEid, "TargetEid should match"); + assertEq(a.isBlockNum, b.isBlockNum, "IsBlockNum should match"); + assertEq(a.blockNumOrTimestamp, b.blockNumOrTimestamp, "BlockNumOrTimestamp should match"); + assertEq(a.confirmations, b.confirmations, "Confirmations should match"); + assertEq(a.to, b.to, "To should match"); + assertEq(a.callData, b.callData, "CallData should match"); + } + + function assertEVMCallComputeV1Eq(EVMCallComputeV1 memory a, EVMCallComputeV1 memory b) internal { + assertEq(a.computeSetting, b.computeSetting, "ComputeSetting should match"); + assertEq(a.targetEid, b.targetEid, "TargetEid should match"); + assertEq(a.isBlockNum, b.isBlockNum, "IsBlockNum should match"); + assertEq(a.blockNumOrTimestamp, b.blockNumOrTimestamp, "BlockNumOrTimestamp should match"); + assertEq(a.confirmations, b.confirmations, "Confirmations should match"); + assertEq(a.to, b.to, "To should match"); + } +} diff --git a/packages/layerzero-v2/evm/oapp/test/lib/OAppOptionsType3.t.sol b/packages/layerzero-v2/evm/oapp/test/lib/OAppOptionsType3.t.sol new file mode 100644 index 0000000..fc1711d --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/test/lib/OAppOptionsType3.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.22; + +import { Test } from "forge-std/Test.sol"; +import { OptionsType3Mock } from "./mock/OptionsType3Mock.sol"; +import { OptionsBuilder } from "../../contracts/oapp/libs/OptionsBuilder.sol"; +import { IOAppOptionsType3 } from "../../contracts/oapp/interfaces/IOAppOptionsType3.sol"; + +contract OAppOptionsType3Test is Test { + using OptionsBuilder for bytes; + + function test_constructor(uint128 lzReceiveGas, uint128 lzReceiveValue) public { + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(lzReceiveGas, lzReceiveValue); + OptionsType3Mock mock = new OptionsType3Mock(options, true); + bytes memory actualOptions = mock.enforcedOptions(1, 1); + assertEq(actualOptions, options, "OptionsType3Mock constructor should set enforced options"); + } + + function test_assertOptionsType3(uint128 lzReceiveGas, uint128 lzReceiveValue) public { + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(lzReceiveGas, lzReceiveValue); + OptionsType3Mock mock = new OptionsType3Mock(options, false); + mock.assertOptionsType3(options); + } + + function test_assertOptionsType3_fails(uint16 prefix, bytes memory remaining) public { + vm.assume(prefix != 3); + bytes memory options = abi.encodePacked(bytes2(prefix), remaining); + OptionsType3Mock mock = new OptionsType3Mock(options, false); + vm.expectRevert(abi.encodeWithSelector(IOAppOptionsType3.InvalidOptions.selector, options)); + mock.assertOptionsType3(options); + } +} diff --git a/packages/layerzero-v2/evm/oapp/test/lib/mock/OptionsType3Mock.sol b/packages/layerzero-v2/evm/oapp/test/lib/mock/OptionsType3Mock.sol new file mode 100644 index 0000000..fd86610 --- /dev/null +++ b/packages/layerzero-v2/evm/oapp/test/lib/mock/OptionsType3Mock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.22; + +import { OAppOptionsType3 } from "../../../contracts/oapp/libs/OAppOptionsType3.sol"; +import { EnforcedOptionParam } from "../../../contracts/oapp/interfaces/IOAppOptionsType3.sol"; + +contract OptionsType3Mock is OAppOptionsType3 { + constructor(bytes memory options, bool applyEnforcedOptions) { + if (applyEnforcedOptions) { + EnforcedOptionParam[] memory params = new EnforcedOptionParam[](1); + params[0] = EnforcedOptionParam(1, 1, options); + _setEnforcedOptions(params); // ensure enforced options can be set in the constructor + } + } + + function assertOptionsType3(bytes calldata options) public pure { + _assertOptionsType3(options); + } +} diff --git a/packages/layerzero-v2/evm/oapp/test/mocks/ExecutorFeeLibMock.sol b/packages/layerzero-v2/evm/oapp/test/mocks/ExecutorFeeLibMock.sol index b0b8365..aa8e61f 100644 --- a/packages/layerzero-v2/evm/oapp/test/mocks/ExecutorFeeLibMock.sol +++ b/packages/layerzero-v2/evm/oapp/test/mocks/ExecutorFeeLibMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.20; import { ExecutorFeeLib } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/ExecutorFeeLib.sol"; contract ExecutorFeeLibMock is ExecutorFeeLib { - constructor() ExecutorFeeLib(1e18) {} + constructor(uint32 _localEid) ExecutorFeeLib(_localEid, 1e18) {} function _isV1Eid(uint32 /*_eid*/) internal pure override returns (bool) { return false; diff --git a/packages/layerzero-v2/evm/oapp/test/mocks/OFTAdapterMock.sol b/packages/layerzero-v2/evm/oapp/test/mocks/OFTAdapterMock.sol index f60056d..61aa78b 100644 --- a/packages/layerzero-v2/evm/oapp/test/mocks/OFTAdapterMock.sol +++ b/packages/layerzero-v2/evm/oapp/test/mocks/OFTAdapterMock.sol @@ -12,7 +12,7 @@ contract OFTAdapterMock is OFTAdapter { uint256 _minAmountToCreditLD, uint32 _dstEid ) public returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { - return _debit(_amountToSendLD, _minAmountToCreditLD, _dstEid); + return _debit(msg.sender, _amountToSendLD, _minAmountToCreditLD, _dstEid); } function debitView( diff --git a/packages/layerzero-v2/evm/oapp/test/mocks/OFTMock.sol b/packages/layerzero-v2/evm/oapp/test/mocks/OFTMock.sol index c63538b..a1dd957 100644 --- a/packages/layerzero-v2/evm/oapp/test/mocks/OFTMock.sol +++ b/packages/layerzero-v2/evm/oapp/test/mocks/OFTMock.sol @@ -22,7 +22,7 @@ contract OFTMock is OFT { uint256 _minAmountToCreditLD, uint32 _dstEid ) public returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { - return _debit(_amountToSendLD, _minAmountToCreditLD, _dstEid); + return _debit(msg.sender, _amountToSendLD, _minAmountToCreditLD, _dstEid); } function debitView( diff --git a/packages/layerzero-v2/evm/protocol/contracts/EndpointV2ViewUpgradeable.sol b/packages/layerzero-v2/evm/protocol/contracts/EndpointV2ViewUpgradeable.sol index e459176..fbb13a2 100644 --- a/packages/layerzero-v2/evm/protocol/contracts/EndpointV2ViewUpgradeable.sol +++ b/packages/layerzero-v2/evm/protocol/contracts/EndpointV2ViewUpgradeable.sol @@ -28,7 +28,11 @@ contract EndpointV2ViewUpgradeable is Initializable { } function initializable(Origin memory _origin, address _receiver) public view returns (bool) { - return endpoint.initializable(_origin, _receiver); + try endpoint.initializable(_origin, _receiver) returns (bool _initializable) { + return _initializable; + } catch { + return false; + } } /// @dev check if a message is verifiable.