diff --git a/test/ETHx_OFT.t.sol b/test/ETHx_OFT.t.sol new file mode 100644 index 0000000..53f8b81 --- /dev/null +++ b/test/ETHx_OFT.t.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +// Mock imports +import { OFTMock } from "./mocks/OFTMock.sol"; +import { ERC20Mock } from "./mocks/ERC20Mock.sol"; +import { OFTComposerMock } from "./mocks/OFTComposerMock.sol"; + +// OApp imports +import { + IOAppOptionsType3, + EnforcedOptionParam +} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OAppOptionsType3.sol"; +import { OptionsBuilder } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol"; + +// OFT imports +import { IOFT, SendParam, OFTReceipt } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol"; +import { MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTCore.sol"; +import { OFTMsgCodec } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/libs/OFTMsgCodec.sol"; +import { OFTComposeMsgCodec } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/libs/OFTComposeMsgCodec.sol"; + +import { ETHx } from "../contracts/ETHx.sol"; +import { ETHxOFTMock } from "./mocks/ETHxOFTMock.sol"; + +// OZ imports +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +// Forge imports +import "forge-std/console.sol"; + +// DevTools imports +import { TestHelperOz5 } from "test-devtools-evm-foundry/contracts/TestHelperOz5.sol"; + +contract ETHxOFTTest is TestHelperOz5 { + using OptionsBuilder for bytes; + + uint32 aEid = 1; + uint32 bEid = 2; + + OFTMock aOFT; + ETHx erc20; + ETHxOFTMock bOFT; + + address public userA = address(0x1); + address public userB = address(0x2); + uint256 public initialBalance = 100 ether; + + function setUp() public virtual override { + vm.deal(userA, 1000 ether); + vm.deal(userB, 1000 ether); + + address erc20Mock = vm.addr(1001); + mockEthx(erc20Mock); + erc20 = ETHx(erc20Mock); + + super.setUp(); + setUpEndpoints(2, LibraryType.UltraLightNode); + + aOFT = OFTMock( + _deployOApp(type(OFTMock).creationCode, abi.encode("aOFT", "aOFT", address(endpoints[aEid]), address(this))) + ); + + bOFT = ETHxOFTMock( + _deployOApp(type(ETHxOFTMock).creationCode, abi.encode(erc20Mock, address(endpoints[bEid]), address(this))) + ); + + erc20.grantRole(erc20.PAUSER_ROLE(), address(bOFT)); + erc20.grantRole(erc20.MINTER_ROLE(), address(bOFT)); + erc20.grantRole(erc20.BURNER_ROLE(), address(bOFT)); + + // config and wire the ofts + address[] memory ofts = new address[](2); + ofts[0] = address(aOFT); + ofts[1] = address(bOFT); + this.wireOApps(ofts); + + // mint tokens + aOFT.mint(userA, initialBalance); + vm.prank(address(bOFT)); + bOFT.mint(userB, initialBalance); + } + + function testConstructor() public { + assertEq(aOFT.owner(), address(this)); + assertEq(bOFT.owner(), address(this)); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(userB), initialBalance); + + assertEq(aOFT.token(), address(aOFT)); + assertEq(bOFT.token(), address(erc20)); + } + + function testBalanceOf() public { + address user1 = vm.addr(0x1); + address user2 = vm.addr(0x2); + vm.prank(address(bOFT)); + erc20.mint(user1, 100); + assertEq(100, bOFT.balanceOf(user1)); + assertEq(0, bOFT.balanceOf(user2)); + } + + function testSendOft() public { + uint256 tokensToSend = 1 ether; + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0); + SendParam memory sendParam = + SendParam(bEid, addressToBytes32(userB), tokensToSend, tokensToSend, options, "", ""); + MessagingFee memory fee = aOFT.quoteSend(sendParam, false); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(userB), initialBalance); + + vm.prank(userA); + 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); + } + + function testSendOftComposeMsg() public { + uint256 tokensToSend = 1 ether; + + OFTComposerMock composer = new OFTComposerMock(); + + bytes memory options = + OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0).addExecutorLzComposeOption(0, 500_000, 0); + bytes memory composeMsg = hex"1234"; + SendParam memory sendParam = + SendParam(bEid, addressToBytes32(address(composer)), tokensToSend, tokensToSend, options, composeMsg, ""); + MessagingFee memory fee = aOFT.quoteSend(sendParam, false); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(address(composer)), 0); + + vm.prank(userA); + (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) = + aOFT.send{ value: fee.nativeFee }(sendParam, fee, payable(address(this))); + verifyPackets(bEid, addressToBytes32(address(bOFT))); + + // lzCompose params + uint32 dstEid_ = bEid; + address from_ = address(bOFT); + bytes memory options_ = options; + bytes32 guid_ = msgReceipt.guid; + address to_ = address(composer); + bytes memory composerMsg_ = OFTComposeMsgCodec.encode( + msgReceipt.nonce, aEid, oftReceipt.amountReceivedLD, abi.encodePacked(addressToBytes32(userA), composeMsg) + ); + this.lzCompose(dstEid_, from_, options_, guid_, to_, composerMsg_); + + assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend); + assertEq(bOFT.balanceOf(address(composer)), tokensToSend); + + assertEq(composer.from(), from_); + assertEq(composer.guid(), guid_); + assertEq(composer.message(), composerMsg_); + assertEq(composer.executor(), address(this)); + assertEq(composer.extraData(), composerMsg_); // default to setting the extraData to the message as well to test + } + + function mockEthx(address ethxMock) private { + ETHx implementation = new ETHx(); + bytes memory code = address(implementation).code; + vm.etch(ethxMock, code); + ETHx mock = ETHx(ethxMock); + mock.initialize(address(this)); + } +} diff --git a/test/ETHx_OFTAdapter.t.sol b/test/ETHx_OFTAdapter.t.sol index 2853da6..1fdde2f 100644 --- a/test/ETHx_OFTAdapter.t.sol +++ b/test/ETHx_OFTAdapter.t.sol @@ -5,9 +5,10 @@ import { Test } from "forge-std/Test.sol"; import { MessagingFee } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { EndpointV2Mock as EndpointV2 } from "test-devtools-evm-foundry/contracts/mocks/EndpointV2Mock.sol"; + import { ETHx_OFTAdapter } from "../contracts/ETHx_OFTAdapter.sol"; -import { EndpointV2Mock as EndpointV2 } from "./mocks/EndpointV2Mock.sol"; import { ERC20Mock } from "./mocks/ERC20Mock.sol"; import { ETHx_OFTAdapterMock } from "./mocks/ETHx_OFTAdapterMock.sol"; diff --git a/test/mocks/DVNFeeLibMock.sol b/test/mocks/DVNFeeLibMock.sol deleted file mode 100644 index 2ba5618..0000000 --- a/test/mocks/DVNFeeLibMock.sol +++ /dev/null @@ -1,147 +0,0 @@ -// SPDX-License-Identifier: LZBL-1.2 - -pragma solidity ^0.8.20; - -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { Transfer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; - -import { ILayerZeroPriceFeed } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/ILayerZeroPriceFeed.sol"; - -import { IDVN } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/interfaces/IDVN.sol"; -import { IDVNFeeLib } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/interfaces/IDVNFeeLib.sol"; -import { DVNOptions } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/libs/DVNOptions.sol"; - -contract DVNFeeLibMock is Ownable, IDVNFeeLib { - using DVNOptions for bytes; - - uint16 internal constant EXECUTE_FIXED_BYTES = 68; // encoded: funcSigHash + params -> 4 + (32 * 2) - uint16 internal constant SIGNATURE_RAW_BYTES = 65; // not encoded - // callData(updateHash) = 132 (4 + 32 * 4), padded to 32 = 160 and encoded as bytes with an 64 byte overhead = 224 - uint16 internal constant UPDATE_HASH_BYTES = 224; - - uint256 private immutable nativeDecimalsRate; - - // @dev oz4/5 breaking change... Ownable constructor - constructor(uint256 _nativeDecimalsRate) { - transferOwnership(msg.sender); - nativeDecimalsRate = _nativeDecimalsRate; - } - - // ================================ OnlyOwner ================================ - function withdrawToken(address _token, address _to, uint256 _amount) external onlyOwner { - // transfers native if _token is address(0x0) - Transfer.nativeOrToken(_token, _to, _amount); - } - - // ========================= External ========================= - /// @dev get fee function that can change state. e.g. paying priceFeed - /// @param _params fee params - /// @param _dstConfig dst config - /// @param //_options options - function getFeeOnSend( - FeeParams calldata _params, - IDVN.DstConfig calldata _dstConfig, - bytes calldata _options - ) - external - payable - returns (uint256) - { - if (_dstConfig.gas == 0) revert DVN_EidNotSupported(_params.dstEid); - - _decodeDVNOptions(_options); // todo: validate options - - uint256 callDataSize = _getCallDataSize(_params.quorum); - - // for future versions where priceFeed charges a fee - // uint256 priceFeedFee = ILayerZeroPriceFeed(_params.priceFeed).getFee(_params.dstEid, callDataSize, - // _dstConfig.gas); - // (uint256 fee, , , uint128 nativePriceUSD) = ILayerZeroPriceFeed(_params.priceFeed).estimateFeeOnSend{ - // value: priceFeedFee - // }(_params.dstEid, callDataSize, _dstConfig.gas); - - (uint256 fee,,, uint128 nativePriceUSD) = - ILayerZeroPriceFeed(_params.priceFeed).estimateFeeOnSend(_params.dstEid, callDataSize, _dstConfig.gas); - - return _applyPremium( - fee, _dstConfig.multiplierBps, _params.defaultMultiplierBps, _dstConfig.floorMarginUSD, nativePriceUSD - ); - } - - // ========================= View ========================= - /// @dev get fee view function - /// @param _params fee params - /// @param _dstConfig dst config - /// @param //_options options - function getFee( - FeeParams calldata _params, - IDVN.DstConfig calldata _dstConfig, - bytes calldata _options - ) - external - view - returns (uint256) - { - if (_dstConfig.gas == 0) revert DVN_EidNotSupported(_params.dstEid); - - _decodeDVNOptions(_options); // validate options - - uint256 callDataSize = _getCallDataSize(_params.quorum); - (uint256 fee,,, uint128 nativePriceUSD) = - ILayerZeroPriceFeed(_params.priceFeed).estimateFeeByEid(_params.dstEid, callDataSize, _dstConfig.gas); - return _applyPremium( - fee, _dstConfig.multiplierBps, _params.defaultMultiplierBps, _dstConfig.floorMarginUSD, nativePriceUSD - ); - } - - // ========================= Internal ========================= - function _getCallDataSize(uint256 _quorum) internal pure returns (uint256) { - uint256 totalSignatureBytes = _quorum * SIGNATURE_RAW_BYTES; - if (totalSignatureBytes % 32 != 0) { - totalSignatureBytes = totalSignatureBytes - (totalSignatureBytes % 32) + 32; - } - // getFee should charge on execute(updateHash) - // totalSignatureBytesPadded also has 64 overhead for bytes - return uint256(EXECUTE_FIXED_BYTES) + UPDATE_HASH_BYTES + totalSignatureBytes + 64; - } - - function _applyPremium( - uint256 _fee, - uint16 _bps, - uint16 _defaultBps, - uint128 _marginUSD, - uint128 _nativePriceUSD - ) - internal - view - returns (uint256) - { - uint16 multiplierBps = _bps == 0 ? _defaultBps : _bps; - - uint256 feeWithMultiplier = (_fee * multiplierBps) / 10_000; - if (_nativePriceUSD == 0 || _marginUSD == 0) { - return feeWithMultiplier; - } - - uint256 feeWithFloorMargin = _fee + (_marginUSD * nativeDecimalsRate) / _nativePriceUSD; - - return feeWithFloorMargin > feeWithMultiplier ? feeWithFloorMargin : feeWithMultiplier; - } - - function _decodeDVNOptions(bytes calldata _options) internal pure returns (uint256) { - uint256 cursor; - while (cursor < _options.length) { - (uint8 optionType,, uint256 newCursor) = _options.nextDVNOption(cursor); - cursor = newCursor; - revert DVN_UnsupportedOptionType(optionType); - } - if (cursor != _options.length) { - revert DVNOptions.DVN_InvalidDVNOptions(cursor); - } - - return 0; // todo: precrime fee model - } - - // send funds here to pay for price feed directly - receive() external payable { } -} diff --git a/test/mocks/EndpointV2Mock.sol b/test/mocks/EndpointV2Mock.sol deleted file mode 100644 index 80d08da..0000000 --- a/test/mocks/EndpointV2Mock.sol +++ /dev/null @@ -1,410 +0,0 @@ -// SPDX-License-Identifier: LZBL-1.2 - -pragma solidity ^0.8.20; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -// @dev oz4/5 breaking change... Ownable constructor -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - -import { - MessagingFee, - MessagingParams, - MessagingReceipt, - Origin, - ILayerZeroEndpointV2 -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; -import { ISendLib, Packet } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; -import { ILayerZeroReceiver } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol"; -import { Errors } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Errors.sol"; -import { GUID } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; -import { Transfer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; -import { MessagingChannel } from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingChannel.sol"; -import { MessagingComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingComposer.sol"; -import { MessageLibManager } from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessageLibManager.sol"; -import { MessagingContext } from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingContext.sol"; - -// LayerZero EndpointV2 is fully backward compatible with LayerZero Endpoint(V1), but it also supports additional -// features that Endpoint(V1) does not support now and may not in the future. We have also changed some terminology -// to clarify pre-existing language that might have been confusing. -// -// The following is a list of terminology changes: -// -chainId -> eid -// - Rationale: chainId was a term we initially used to describe an endpoint on a specific chain. Since -// LayerZero supports non-EVMs we could not map the classic EVM chainIds to the LayerZero chainIds, making it -// confusing for developers. With the addition of EndpointV2 and its backward compatible nature, we would have -// two chainIds per chain that has Endpoint(V1), further confusing developers. We have decided to change the -// name to Endpoint Id, or eid, for simplicity and clarity. -// -adapterParams -> options -// -userApplication -> oapp. Omnichain Application -// -srcAddress -> sender -// -dstAddress -> receiver -// - Rationale: The sender/receiver on EVM is the address. However, on non-EVM chains, the sender/receiver -// could -// represented as a public key, or some other identifier. The term sender/receiver is more generic -// -payload -> message. -// - Rationale: The term payload is used in the context of a packet, which is a combination of the message and -// GUID -contract EndpointV2Mock is - ILayerZeroEndpointV2, - MessagingChannel, - MessageLibManager, - MessagingComposer, - MessagingContext -{ - address public lzToken; - - mapping(address oapp => address delegate) public delegates; - - /// @param _eid the unique Endpoint Id for this deploy that all other Endpoints can use to send to it - // @dev oz4/5 breaking change... Ownable constructor - constructor(uint32 _eid, address _owner) MessagingChannel(_eid) { - transferOwnership(_owner); - } - - /// @dev MESSAGING STEP 0 - /// @notice This view function gives the application built on top of LayerZero the ability to requests a quote - /// with the same parameters as they would to send their message. Since the quotes are given on chain there is a - /// race condition in which the prices could change between the time the user gets their quote and the time they - /// submit their message. If the price moves up and the user doesn't send enough funds the transaction will revert, - /// if the price goes down the _refundAddress provided by the app will be refunded the difference. - /// @param _params the messaging parameters - /// @param _sender the sender of the message - function quote(MessagingParams calldata _params, address _sender) external view returns (MessagingFee memory) { - // lzToken must be set to support payInLzToken - if (_params.payInLzToken && lzToken == address(0x0)) revert Errors.LZ_LzTokenUnavailable(); - - // get the correct outbound nonce - uint64 nonce = outboundNonce[_sender][_params.dstEid][_params.receiver] + 1; - - // construct the packet with a GUID - Packet memory packet = Packet({ - nonce: nonce, - srcEid: eid, - sender: _sender, - dstEid: _params.dstEid, - receiver: _params.receiver, - guid: GUID.generate(nonce, eid, _sender, _params.dstEid, _params.receiver), - message: _params.message - }); - - // get the send library by sender and dst eid - // use _ to avoid variable shadowing - address _sendLibrary = getSendLibrary(_sender, _params.dstEid); - - return ISendLib(_sendLibrary).quote(packet, _params.options, _params.payInLzToken); - } - - /// @dev MESSAGING STEP 1 - OApp need to transfer the fees to the endpoint before sending the message - /// @param _params the messaging parameters - /// @param _refundAddress the address to refund both the native and lzToken - function send( - MessagingParams calldata _params, - address _refundAddress - ) - external - payable - sendContext(_params.dstEid, msg.sender) - returns (MessagingReceipt memory) - { - if (_params.payInLzToken && lzToken == address(0x0)) revert Errors.LZ_LzTokenUnavailable(); - - // send message - (MessagingReceipt memory receipt, address _sendLibrary) = _send(msg.sender, _params); - - // OApp can simulate with 0 native value it will fail with error including the required fee, which can be - // provided in the actual call - // this trick can be used to avoid the need to write the quote() function - // however, without the quote view function it will be hard to compose an oapp on chain - uint256 suppliedNative = _suppliedNative(); - uint256 suppliedLzToken = _suppliedLzToken(_params.payInLzToken); - _assertMessagingFee(receipt.fee, suppliedNative, suppliedLzToken); - - // handle lz token fees - _payToken(lzToken, receipt.fee.lzTokenFee, suppliedLzToken, _sendLibrary, _refundAddress); - - // handle native fees - _payNative(receipt.fee.nativeFee, suppliedNative, _sendLibrary, _refundAddress); - - return receipt; - } - - /// @dev internal function for sending the messages used by all external send methods - /// @param _sender the address of the application sending the message to the destination chain - /// @param _params the messaging parameters - function _send( - address _sender, - MessagingParams calldata _params - ) - internal - returns (MessagingReceipt memory, address) - { - // get the correct outbound nonce - uint64 latestNonce = _outbound(_sender, _params.dstEid, _params.receiver); - - // construct the packet with a GUID - Packet memory packet = Packet({ - nonce: latestNonce, - srcEid: eid, - sender: _sender, - dstEid: _params.dstEid, - receiver: _params.receiver, - guid: GUID.generate(latestNonce, eid, _sender, _params.dstEid, _params.receiver), - message: _params.message - }); - - // get the send library by sender and dst eid - address _sendLibrary = getSendLibrary(_sender, _params.dstEid); - - // messageLib always returns encodedPacket with guid - (MessagingFee memory fee, bytes memory encodedPacket) = - ISendLib(_sendLibrary).send(packet, _params.options, _params.payInLzToken); - - // Emit packet information for DVNs, Executors, and any other offchain infrastructure to only listen - // for this one event to perform their actions. - emit PacketSent(encodedPacket, _params.options, _sendLibrary); - - return (MessagingReceipt(packet.guid, latestNonce, fee), _sendLibrary); - } - - /// @dev MESSAGING STEP 2 - on the destination chain - /// @dev configured receive library verifies a message - /// @param _origin a struct holding the srcEid, nonce, and sender of the message - /// @param _receiver the receiver of the message - /// @param _payloadHash the payload hash of the message - function verify(Origin calldata _origin, address _receiver, bytes32 _payloadHash) external { - if (!isValidReceiveLibrary(_receiver, _origin.srcEid, msg.sender)) revert Errors.LZ_InvalidReceiveLibrary(); - - uint64 lazyNonce = lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]; - if (!_initializable(_origin, _receiver, lazyNonce)) revert Errors.LZ_PathNotInitializable(); - if (!_verifiable(_origin, _receiver, lazyNonce)) revert Errors.LZ_PathNotVerifiable(); - - // insert the message into the message channel - _inbound(_receiver, _origin.srcEid, _origin.sender, _origin.nonce, _payloadHash); - emit PacketVerified(_origin, _receiver, _payloadHash); - } - - /// @dev MESSAGING STEP 3 - the last step - /// @dev execute a verified message to the designated receiver - /// @dev the execution provides the execution context (caller, extraData) to the receiver. the receiver can - /// optionally assert the caller and validate the untrusted extraData - /// @dev cant reentrant because the payload is cleared before execution - /// @param _origin the origin of the message - /// @param _receiver the receiver of the message - /// @param _guid the guid of the message - /// @param _message the message - /// @param _extraData the extra data provided by the executor. this data is untrusted and should be validated. - function lzReceive( - Origin calldata _origin, - address _receiver, - bytes32 _guid, - bytes calldata _message, - bytes calldata _extraData - ) - external - payable - { - // clear the payload first to prevent reentrancy, and then execute the message - _clearPayload(_receiver, _origin.srcEid, _origin.sender, _origin.nonce, abi.encodePacked(_guid, _message)); - ILayerZeroReceiver(_receiver).lzReceive{ value: msg.value }(_origin, _guid, _message, msg.sender, _extraData); - emit PacketDelivered(_origin, _receiver); - } - - /// @param _origin the origin of the message - /// @param _receiver the receiver of the message - /// @param _guid the guid of the message - /// @param _message the message - /// @param _extraData the extra data provided by the executor. - /// @param _reason the reason for failure - function lzReceiveAlert( - Origin calldata _origin, - address _receiver, - bytes32 _guid, - uint256 _gas, - uint256 _value, - bytes calldata _message, - bytes calldata _extraData, - bytes calldata _reason - ) - external - { - emit LzReceiveAlert(_receiver, msg.sender, _origin, _guid, _gas, _value, _message, _extraData, _reason); - } - - /// @dev Oapp uses this interface to clear a message. - /// @dev this is a PULL mode versus the PUSH mode of lzReceive - /// @dev the cleared message can be ignored by the app (effectively burnt) - /// @dev authenticated by oapp - /// @param _origin the origin of the message - /// @param _guid the guid of the message - /// @param _message the message - function clear(address _oapp, Origin calldata _origin, bytes32 _guid, bytes calldata _message) external { - _assertAuthorized(_oapp); - - bytes memory payload = abi.encodePacked(_guid, _message); - _clearPayload(_oapp, _origin.srcEid, _origin.sender, _origin.nonce, payload); - emit PacketDelivered(_origin, _oapp); - } - - /// @dev allows reconfiguration to recover from wrong configurations - /// @dev users should never approve the EndpointV2 contract to spend their non-layerzero tokens - /// @dev override this function if the endpoint is charging ERC20 tokens as native - /// @dev only owner - /// @param _lzToken the new layer zero token address - function setLzToken(address _lzToken) public virtual onlyOwner { - lzToken = _lzToken; - emit LzTokenSet(_lzToken); - } - - /// @dev recover the token sent to this contract by mistake - /// @dev only owner - /// @param _token the token to recover. if 0x0 then it is native token - /// @param _to the address to send the token to - /// @param _amount the amount to send - function recoverToken(address _token, address _to, uint256 _amount) external onlyOwner { - Transfer.nativeOrToken(_token, _to, _amount); - } - - /// @dev handling token payments on endpoint. the sender must approve the endpoint to spend the token - /// @dev internal function - /// @param _token the token to pay - /// @param _required the amount required - /// @param _supplied the amount supplied - /// @param _receiver the receiver of the token - function _payToken( - address _token, - uint256 _required, - uint256 _supplied, - address _receiver, - address _refundAddress - ) - internal - { - if (_required > 0) { - Transfer.token(_token, _receiver, _required); - } - if (_required < _supplied) { - unchecked { - // refund the excess - Transfer.token(_token, _refundAddress, _supplied - _required); - } - } - } - - /// @dev handling native token payments on endpoint - /// @dev override this if the endpoint is charging ERC20 tokens as native - /// @dev internal function - /// @param _required the amount required - /// @param _supplied the amount supplied - /// @param _receiver the receiver of the native token - /// @param _refundAddress the address to refund the excess to - function _payNative( - uint256 _required, - uint256 _supplied, - address _receiver, - address _refundAddress - ) - internal - virtual - { - if (_required > 0) { - Transfer.native(_receiver, _required); - } - if (_required < _supplied) { - unchecked { - // refund the excess - Transfer.native(_refundAddress, _supplied - _required); - } - } - } - - /// @dev get the balance of the lzToken as the supplied lzToken fee if payInLzToken is true - function _suppliedLzToken(bool _payInLzToken) internal view returns (uint256 supplied) { - if (_payInLzToken) { - supplied = IERC20(lzToken).balanceOf(address(this)); - - // if payInLzToken is true, the supplied fee must be greater than 0 to prevent a race condition - // in which an oapp sending a message with lz token and the lz token is set to a new token between the tx - // being sent and the tx being mined. if the required lz token fee is 0 and the old lz token would be - // locked in the contract instead of being refunded - if (supplied == 0) revert Errors.LZ_ZeroLzTokenFee(); - } - } - - /// @dev override this if the endpoint is charging ERC20 tokens as native - function _suppliedNative() internal view virtual returns (uint256) { - return msg.value; - } - - /// @dev Assert the required fees and the supplied fees are enough - function _assertMessagingFee( - MessagingFee memory _required, - uint256 _suppliedNativeFee, - uint256 _suppliedLzTokenFee - ) - internal - pure - { - if (_required.nativeFee > _suppliedNativeFee || _required.lzTokenFee > _suppliedLzTokenFee) { - revert Errors.LZ_InsufficientFee( - _required.nativeFee, _suppliedNativeFee, _required.lzTokenFee, _suppliedLzTokenFee - ); - } - } - - /// @dev override this if the endpoint is charging ERC20 tokens as native - /// @return 0x0 if using native. otherwise the address of the native ERC20 token - function nativeToken() external view virtual returns (address) { - return address(0x0); - } - - /// @notice delegate is authorized by the oapp to configure anything in layerzero - function setDelegate(address _delegate) external { - delegates[msg.sender] = _delegate; - emit DelegateSet(msg.sender, _delegate); - } - - // ========================= Internal ========================= - function _initializable( - Origin calldata _origin, - address _receiver, - uint64 _lazyInboundNonce - ) - internal - view - returns (bool) - { - return _lazyInboundNonce > 0 // allowInitializePath already checked - || ILayerZeroReceiver(_receiver).allowInitializePath(_origin); - } - - /// @dev bytes(0) payloadHash can never be submitted - function _verifiable( - Origin calldata _origin, - address _receiver, - uint64 _lazyInboundNonce - ) - internal - view - returns (bool) - { - return _origin.nonce > _lazyInboundNonce // either initializing an empty slot or reverifying - || inboundPayloadHash[_receiver][_origin.srcEid][_origin.sender][_origin.nonce] != EMPTY_PAYLOAD_HASH; // only - // allow reverifying if it hasn't been executed - } - - /// @dev assert the caller to either be the oapp or the delegate - function _assertAuthorized(address _oapp) internal view override(MessagingChannel, MessageLibManager) { - if (msg.sender != _oapp && msg.sender != delegates[_oapp]) revert Errors.LZ_Unauthorized(); - } - - // ========================= VIEW FUNCTIONS FOR OFFCHAIN ONLY ========================= - // Not involved in any state transition function. - // ==================================================================================== - function initializable(Origin calldata _origin, address _receiver) external view returns (bool) { - return _initializable(_origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]); - } - - function verifiable(Origin calldata _origin, address _receiver) external view returns (bool) { - return _verifiable(_origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]); - } -} diff --git a/test/mocks/ExecutorFeeLibMock.sol b/test/mocks/ExecutorFeeLibMock.sol deleted file mode 100644 index 6f76159..0000000 --- a/test/mocks/ExecutorFeeLibMock.sol +++ /dev/null @@ -1,216 +0,0 @@ -// SPDX-License-Identifier: LZBL-1.2 -pragma solidity ^0.8.22; - -// @dev oz4/5 breaking change... Ownable constructor -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - -import { Transfer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; -import { ExecutorOptions } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; - -import { ILayerZeroPriceFeed } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/ILayerZeroPriceFeed.sol"; -import { IExecutor } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/IExecutor.sol"; -import { IExecutorFeeLib } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/IExecutorFeeLib.sol"; - -contract ExecutorFeeLibMock is Ownable, IExecutorFeeLib { - using ExecutorOptions for bytes; - - uint256 private immutable nativeDecimalsRate; - - // @dev oz4/5 breaking change... Ownable constructor - constructor() { - transferOwnership(msg.sender); - nativeDecimalsRate = 1e18; - } - - // ================================ OnlyOwner ================================ - function withdrawToken(address _token, address _to, uint256 _amount) external onlyOwner { - // transfers native if _token is address(0x0) - Transfer.nativeOrToken(_token, _to, _amount); - } - - // ================================ External ================================ - function getFeeOnSend( - FeeParams calldata _params, - IExecutor.DstConfig calldata _dstConfig, - bytes calldata _options - ) - external - returns (uint256 fee) - { - if (_dstConfig.lzReceiveBaseGas == 0) revert Executor_EidNotSupported(_params.dstEid); - - (uint256 totalDstAmount, uint256 totalGas) = _decodeExecutorOptions( - _isV1Eid(_params.dstEid), - _dstConfig.lzReceiveBaseGas, - _dstConfig.lzComposeBaseGas, - _dstConfig.nativeCap, - _options - ); - - // for future versions where priceFeed charges a fee - (uint256 totalGasFee, uint128 priceRatio, uint128 priceRatioDenominator, uint128 nativePriceUSD) = - ILayerZeroPriceFeed(_params.priceFeed).estimateFeeOnSend(_params.dstEid, _params.calldataSize, totalGas); - - fee = _applyPremiumToGas( - totalGasFee, - _dstConfig.multiplierBps, - _params.defaultMultiplierBps, - _dstConfig.floorMarginUSD, - nativePriceUSD - ); - fee += _convertAndApplyPremiumToValue( - totalDstAmount, priceRatio, priceRatioDenominator, _params.defaultMultiplierBps - ); - } - - // ================================ View ================================ - function getFee( - FeeParams calldata _params, - IExecutor.DstConfig calldata _dstConfig, - bytes calldata _options - ) - external - view - returns (uint256 fee) - { - if (_dstConfig.lzReceiveBaseGas == 0) revert Executor_EidNotSupported(_params.dstEid); - - (uint256 totalDstAmount, uint256 totalGas) = _decodeExecutorOptions( - _isV1Eid(_params.dstEid), - _dstConfig.lzReceiveBaseGas, - _dstConfig.lzComposeBaseGas, - _dstConfig.nativeCap, - _options - ); - - (uint256 totalGasFee, uint128 priceRatio, uint128 priceRatioDenominator, uint128 nativePriceUSD) = - ILayerZeroPriceFeed(_params.priceFeed).estimateFeeByEid(_params.dstEid, _params.calldataSize, totalGas); - - fee = _applyPremiumToGas( - totalGasFee, - _dstConfig.multiplierBps, - _params.defaultMultiplierBps, - _dstConfig.floorMarginUSD, - nativePriceUSD - ); - fee += _convertAndApplyPremiumToValue( - totalDstAmount, priceRatio, priceRatioDenominator, _params.defaultMultiplierBps - ); - } - - // ================================ Internal ================================ - // @dev decode executor options into dstAmount and totalGas - function _decodeExecutorOptions( - bool _v1Eid, - uint64 _lzReceiveBaseGas, - uint64 _lzComposeBaseGas, - uint128 _nativeCap, - bytes calldata _options - ) - internal - pure - returns (uint256 dstAmount, uint256 totalGas) - { - if (_options.length == 0) { - revert Executor_NoOptions(); - } - - uint256 cursor = 0; - bool ordered = false; - totalGas = _lzReceiveBaseGas; // lz receive only called once - - bool v1Eid = _v1Eid; // stack too deep - uint256 lzReceiveGas; - while (cursor < _options.length) { - (uint8 optionType, bytes calldata option, uint256 newCursor) = _options.nextExecutorOption(cursor); - cursor = newCursor; - - if (optionType == ExecutorOptions.OPTION_TYPE_LZRECEIVE) { - (uint128 gas, uint128 value) = ExecutorOptions.decodeLzReceiveOption(option); - - // endpoint v1 does not support lzReceive with value - if (v1Eid && value > 0) revert Executor_UnsupportedOptionType(optionType); - - dstAmount += value; - lzReceiveGas += gas; - } else if (optionType == ExecutorOptions.OPTION_TYPE_NATIVE_DROP) { - (uint128 nativeDropAmount,) = ExecutorOptions.decodeNativeDropOption(option); - dstAmount += nativeDropAmount; - } else if (optionType == ExecutorOptions.OPTION_TYPE_LZCOMPOSE) { - // endpoint v1 does not support lzCompose - if (v1Eid) revert Executor_UnsupportedOptionType(optionType); - - (, uint128 gas, uint128 value) = ExecutorOptions.decodeLzComposeOption(option); - if (gas == 0) revert Executor_ZeroLzComposeGasProvided(); - - dstAmount += value; - // lz compose can be called multiple times, based on unique index - // to simplify the quoting, we add lzComposeBaseGas for each lzComposeOption received - // if the same index has multiple compose options, the gas will be added multiple times - totalGas += gas + _lzComposeBaseGas; - } else if (optionType == ExecutorOptions.OPTION_TYPE_ORDERED_EXECUTION) { - ordered = true; - } else { - revert Executor_UnsupportedOptionType(optionType); - } - } - if (cursor != _options.length) revert Executor_InvalidExecutorOptions(cursor); - if (dstAmount > _nativeCap) revert Executor_NativeAmountExceedsCap(dstAmount, _nativeCap); - if (lzReceiveGas == 0) revert Executor_ZeroLzReceiveGasProvided(); - totalGas += lzReceiveGas; - - if (ordered) { - totalGas = (totalGas * 102) / 100; - } - } - - function _applyPremiumToGas( - uint256 _fee, - uint16 _bps, - uint16 _defaultBps, - uint128 _marginUSD, - uint128 _nativePriceUSD - ) - internal - view - returns (uint256) - { - uint16 multiplierBps = _bps == 0 ? _defaultBps : _bps; - - uint256 feeWithMultiplier = (_fee * multiplierBps) / 10_000; - - if (_nativePriceUSD == 0 || _marginUSD == 0) { - return feeWithMultiplier; - } - uint256 feeWithMargin = (_marginUSD * nativeDecimalsRate) / _nativePriceUSD + _fee; - return feeWithMargin > feeWithMultiplier ? feeWithMargin : feeWithMultiplier; - } - - // includes value and nativeDrop - function _convertAndApplyPremiumToValue( - uint256 _value, - uint128 _ratio, - uint128 _denom, - uint16 _defaultBps - ) - internal - pure - returns (uint256 fee) - { - if (_value > 0) { - fee = (((_value * _ratio) / _denom) * _defaultBps) / 10_000; - } - } - - function _isV1Eid(uint32 /*_eid*/ ) internal pure virtual returns (bool) { - return false; - } - - // function _isV1Eid(uint32 _eid) internal pure virtual returns (bool) { - // // v1 eid is < 30000 - // return _eid < 30000; - // } - - // send funds here to pay for price feed directly - receive() external payable { } -} diff --git a/test/mocks/OFTInspectorMock.sol b/test/mocks/OFTInspectorMock.sol deleted file mode 100644 index ada77e7..0000000 --- a/test/mocks/OFTInspectorMock.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: LZBL-1.2 -pragma solidity ^0.8.22; - -import { IOAppMsgInspector } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/interfaces/IOAppMsgInspector.sol"; - -contract OFTInspectorMock is IOAppMsgInspector { - function inspect(bytes calldata _message, bytes calldata _options) external pure returns (bool) { - revert InspectionFailed(_message, _options); - } -}