diff --git a/foundry.toml b/foundry.toml index 0b5bf8c8..cb03c433 100644 --- a/foundry.toml +++ b/foundry.toml @@ -21,6 +21,7 @@ remappings = [ "yieldbox/=gitmodule/tap-yieldbox/contracts/", "tapioca-lbp/=gitmodule/tapioca-lbp/contracts/", "permitc/=gitmodule/permitc/src/", + "test/=test/", ] diff --git a/test/LZSetup/TestHelper.sol b/test/LZSetup/TestHelper.sol index 0e11a016..3b7607e2 100644 --- a/test/LZSetup/TestHelper.sol +++ b/test/LZSetup/TestHelper.sol @@ -25,15 +25,16 @@ import {EndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/EndpointV2 import {ExecutorOptions} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; import {PacketV1Codec} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; import {Origin} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; - import {OApp} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/OApp.sol"; import {OptionsBuilder} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol"; - import {OptionsHelper} from "./mocks/OptionsHelper.sol"; import {SendUln302Mock as SendUln302} from "./mocks/SendUln302Mock.sol"; import {SimpleMessageLibMock} from "./mocks/SimpleMessageLibMock.sol"; import "./mocks/ExecutorFeeLibMock.sol"; +// solhint-disable-next-line +import "forge-std/console.sol"; + contract TestHelper is Test, OptionsHelper { using OptionsBuilder for bytes; diff --git a/test/LZSetup/mocks/OFTAdapterMock.sol b/test/LZSetup/mocks/OFTAdapterMock.sol index 04ceb836..c505dba5 100644 --- a/test/LZSetup/mocks/OFTAdapterMock.sol +++ b/test/LZSetup/mocks/OFTAdapterMock.sol @@ -1,32 +1,36 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -import {OFTAdapter} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTAdapter.sol"; +// import {OFTAdapter} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTAdapter.sol"; -contract OFTAdapterMock is OFTAdapter { - constructor(address _token, address _lzEndpoint, address _owner) OFTAdapter(_token, _lzEndpoint, _owner) {} +// contract OFTAdapterMock is OFTAdapter { +// constructor(address _token, address _lzEndpoint, address _owner) OFTAdapter(_token, _lzEndpoint, _owner) {} - // @dev expose internal functions for testing purposes - function debit(uint256 _amountToSendLD, uint256 _minAmountToCreditLD, uint32 _dstEid) - public - returns (uint256 amountDebitedLD, uint256 amountToCreditLD) - { - return _debit(msg.sender, _amountToSendLD, _minAmountToCreditLD, _dstEid); - } +// // @dev expose internal functions for testing purposes +// function debit(uint256 _amountToSendLD, uint256 _minAmountToCreditLD, uint32 _dstEid) +// public +// returns (uint256 amountDebitedLD, uint256 amountToCreditLD) +// { +// return _debit(_amountToSendLD, _minAmountToCreditLD, _dstEid); +// } - function debitView(uint256 _amountToSendLD, uint256 _minAmountToCreditLD, uint32 _dstEid) - public - view - returns (uint256 amountDebitedLD, uint256 amountToCreditLD) - { - return _debitView(_amountToSendLD, _minAmountToCreditLD, _dstEid); - } +// function debitView(uint256 _amountToSendLD, uint256 _minAmountToCreditLD, uint32 _dstEid) +// public +// view +// returns (uint256 amountDebitedLD, uint256 amountToCreditLD) +// { +// return _debitView(_amountToSendLD, _minAmountToCreditLD, _dstEid); +// } - function credit(address _to, uint256 _amountToCreditLD, uint32 _srcEid) public returns (uint256 amountReceivedLD) { - return _credit(_to, _amountToCreditLD, _srcEid); - } +// function credit(address _to, uint256 _amountToCreditLD, uint32 _srcEid) public returns (uint256 amountReceivedLD) { +// return _credit(_to, _amountToCreditLD, _srcEid); +// } - function removeDust(uint256 _amountLD) public view returns (uint256 amountLD) { - return _removeDust(_amountLD); - } -} +// function increaseOutboundAmount(uint256 _amount) public { +// outboundAmount += _amount; +// } + +// function removeDust(uint256 _amountLD) public view returns (uint256 amountLD) { +// return _removeDust(_amountLD); +// } +// } diff --git a/test/LZSetup/mocks/OFTMock.sol b/test/LZSetup/mocks/OFTMock.sol index 03275dee..e38a7d9d 100644 --- a/test/LZSetup/mocks/OFTMock.sol +++ b/test/LZSetup/mocks/OFTMock.sol @@ -45,11 +45,12 @@ contract OFTMock is OFT { return _credit(_to, _amountToCreditLD, _srcEid); } - function buildMsgAndOptions(SendParam calldata _sendParam, uint256 _amountToCreditLD) - public - view - returns (bytes memory message, bytes memory options) - { + function buildMsgAndOptions( + SendParam calldata _sendParam, + bytes calldata _extraOptions, + bytes calldata _composeMsg, + uint256 _amountToCreditLD + ) public view returns (bytes memory message, bytes memory options) { return _buildMsgAndOptions(_sendParam, _amountToCreditLD); } } diff --git a/test/btt/Magnetar/Magnetar_burst.t.sol b/test/btt/Magnetar/Magnetar_burst.t.sol new file mode 100644 index 00000000..92bcf0d7 --- /dev/null +++ b/test/btt/Magnetar/Magnetar_burst.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +import { + MagnetarAction, + MagnetarModule, + MagnetarCall, + IMagnetarModuleExtender +} from "tapioca-periph/interfaces/periph/IMagnetar.sol"; +import {MagnetarBaseTest, Magnetar} from "test/btt/MagnetarBaseTest.sol"; +import {MagnetarStorage} from "contracts/Magnetar/MagnetarStorage.sol"; + +contract Magnetar_burst is MagnetarBaseTest { + function test_RevertWhen_CurrentAddressIsNotWhitelisted() external { + // it should revert + MagnetarCall[] memory magnetarCall = new MagnetarCall[](1); + magnetarCall[0] = MagnetarCall({id: 0, target: address(0x1), value: 0, call: hex""}); + + vm.prank(adminAddr); + cluster.updateContract(0, address(magnetar), false); + vm.expectRevert( + abi.encodeWithSelector(MagnetarStorage.Magnetar_TargetNotWhitelisted.selector, address(magnetar)) + ); + magnetar.burst(magnetarCall); + } + + function test_RevertWhen_Paused() external { + // it should revert + vm.prank(adminAddr); + magnetar.setPause(true); + + MagnetarCall[] memory magnetarCall = new MagnetarCall[](1); + magnetarCall[0] = MagnetarCall({id: 0, target: address(0x1), value: 0, call: hex""}); + + vm.expectRevert("Pausable: paused"); + magnetar.burst(magnetarCall); + } + + modifier whenCallingUsingMagnetarExtender() { + vm.prank(adminAddr); + magnetar.setMagnetarModuleExtender(IMagnetarModuleExtender(address(magnetarExtender))); + _; + } + + function test_RevertWhen_SuccessReturnsFalse() external whenCallingUsingMagnetarExtender { + // it should revert + MagnetarCall[] memory magnetarCall = new MagnetarCall[](1); + magnetarCall[0] = MagnetarCall({id: 100, target: address(0x1), value: 0, call: hex""}); + + vm.expectRevert("Invalid action id"); + magnetar.burst(magnetarCall); + } + + function test_RevertWhen_ActionNotValid() external { + // it should revert + MagnetarCall[] memory magnetarCall = new MagnetarCall[](1); + magnetarCall[0] = MagnetarCall({id: 50, target: address(0x1), value: 0, call: hex""}); + + vm.expectRevert(abi.encodeWithSelector(Magnetar.Magnetar_ActionNotValid.selector, 50, hex"")); + magnetar.burst(magnetarCall); + } + + function test_RevertWhen_MsgValueNotMatchingAccumulator() external { + // it should revert + vm.prank(adminAddr); + magnetar.setMagnetarModuleExtender(IMagnetarModuleExtender(address(magnetarExtender))); + + MagnetarCall[] memory magnetarCall = new MagnetarCall[](1); + magnetarCall[0] = MagnetarCall({id: 200, target: address(0x1), value: 0, call: hex""}); + + vm.expectRevert(abi.encodeWithSelector(Magnetar.Magnetar_ValueMismatch.selector, 1e18, 0)); + magnetar.burst{value: 1e18}(magnetarCall); + } +} diff --git a/test/btt/Magnetar/Magnetar_burst.tree b/test/btt/Magnetar/Magnetar_burst.tree new file mode 100644 index 00000000..ae6b8770 --- /dev/null +++ b/test/btt/Magnetar/Magnetar_burst.tree @@ -0,0 +1,12 @@ +Magnetar_burst +├── when current address is not whitelisted +│ └── it should revert +├── when paused +│ └── it should revert +├── when calling using MagnetarExtender +│ └── when success returns false +│ └── it should revert +├── when action not valid +│ └── it should revert +└── when msg value not matching accumulator + └── it should revert \ No newline at end of file diff --git a/test/btt/Magnetar/Magnetar_permitOperation.t.sol b/test/btt/Magnetar/Magnetar_permitOperation.t.sol new file mode 100644 index 00000000..22f256ca --- /dev/null +++ b/test/btt/Magnetar/Magnetar_permitOperation.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +import {ERC20PermitApprovalMsg, ERC20PermitStruct} from "tapioca-periph/interfaces/periph/ITapiocaOmnichainEngine.sol"; +import {MagnetarAction, MagnetarCall} from "tapioca-periph/interfaces/periph/IMagnetar.sol"; +import {MagnetarBaseTest, Magnetar} from "test/btt/MagnetarBaseTest.sol"; +import {MagnetarStorage} from "contracts/Magnetar/MagnetarStorage.sol"; + +contract Magnetar_permitOperation is MagnetarBaseTest { + function test_RevertWhen_TargetIsNotWhitelisted() external { + // it should revert + cluster.updateContract(0, address(aToeOFT), false); + + MagnetarCall[] memory magnetarCall = new MagnetarCall[](1); + magnetarCall[0] = MagnetarCall({id: 0, target: address(aToeOFT), value: 0, call: hex""}); + vm.expectRevert( + abi.encodeWithSelector(MagnetarStorage.Magnetar_TargetNotWhitelisted.selector, address(aToeOFT)) + ); + magnetar.burst(magnetarCall); + } + + function test_RevertWhen_SelectorIsNotValid() external { + // it should revert + MagnetarCall[] memory magnetarCall = new MagnetarCall[](1); + magnetarCall[0] = MagnetarCall({id: 0, target: address(aToeOFT), value: 0, call: hex""}); + vm.expectRevert(abi.encodeWithSelector(Magnetar.Magnetar_ActionNotValid.selector, 0, hex"")); + magnetar.burst(magnetarCall); + } + + function test_WhenSelectorIsPermit() external { + // it should work + cluster.updateContract(0, address(this), true); + ERC20PermitStruct memory permit_ = + ERC20PermitStruct({owner: aliceAddr, spender: address(this), value: 1e18, nonce: 0, deadline: 1 days}); + + bytes32 digest_ = aToeOFT.getTypedDataHash(permit_); + ERC20PermitApprovalMsg memory permitApproval_ = + getERC20PermitData(permit_, digest_, address(aToeOFT), alicePKey); + + aToeOFT.permit( + permit_.owner, + permit_.spender, + permit_.value, + permit_.deadline, + permitApproval_.v, + permitApproval_.r, + permitApproval_.s + ); + + MagnetarCall[] memory magnetarCall = new MagnetarCall[](1); + magnetarCall[0] = MagnetarCall({ + id: 0, + target: address(aToeOFT), + value: 0, + call: abi.encodeWithSelector( + aToeOFT.permit.selector, + permit_.owner, + permit_.spender, + permit_.value, + permit_.deadline, + permitApproval_.v, + permitApproval_.r, + permitApproval_.s + ) + }); + magnetar.burst(magnetarCall); + assertEq(aToeOFT.allowance(aliceAddr, address(this)), 1e18); + } + + function test_WhenSelectorIsRevoke() external { + // it should work + } + + function test_WhenSelectorIsPermitAll() external { + // it should work + } + + function test_WhenSelectorIsRevokeAll() external { + // it should work + } +} diff --git a/test/btt/Magnetar/Magnetar_permitOperation.tree b/test/btt/Magnetar/Magnetar_permitOperation.tree new file mode 100644 index 00000000..97689f01 --- /dev/null +++ b/test/btt/Magnetar/Magnetar_permitOperation.tree @@ -0,0 +1,13 @@ +Magnetar_permitOperation +├── when target is not whitelisted +│ └── it should revert +├── when selector is not valid +│ └── it should revert +├── when selector is permit +│ └── it should work +├── when selector is revoke +│ └── it should work +├── when selector is permitAll +│ └── it should work +└── when selector is revokeAll + └── it should work \ No newline at end of file diff --git a/test/btt/MagnetarBaseTest.sol b/test/btt/MagnetarBaseTest.sol new file mode 100644 index 00000000..6d95d1c7 --- /dev/null +++ b/test/btt/MagnetarBaseTest.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +/** + * Core + */ +import {TapiocaOmnichainEngineHelper} from "contracts/tapiocaOmnichainEngine/extension/TapiocaOmnichainEngineHelper.sol"; +import {ERC20PermitApprovalMsg, ERC20PermitStruct} from "tapioca-periph/interfaces/periph/ITapiocaOmnichainEngine.sol"; +import {TapiocaOmnichainExtExec} from "contracts/tapiocaOmnichainEngine/extension/TapiocaOmnichainExtExec.sol"; +import {MagnetarCollateralModule} from "tapioca-periph/Magnetar/modules/MagnetarCollateralModule.sol"; +import {MagnetarYieldBoxModule} from "contracts/Magnetar/modules/MagnetarYieldBoxModule.sol"; +import {MagnetarOptionModule} from "contracts/Magnetar/modules/MagnetarOptionModule.sol"; +import {MagnetarMintModule} from "contracts/Magnetar/modules/MagnetarMintModule.sol"; +import {MagnetarBaseModule} from "contracts/Magnetar/modules/MagnetarBaseModule.sol"; +import {IMagnetarHelper} from "contracts/interfaces/periph/IMagnetarHelper.sol"; +import {MagnetarHelper} from "contracts/Magnetar/MagnetarHelper.sol"; +import {Pearlmit, IPearlmit} from "contracts/pearlmit/Pearlmit.sol"; +import {Cluster, ICluster} from "contracts/Cluster/Cluster.sol"; +import {Magnetar} from "contracts/Magnetar/Magnetar.sol"; + +/** + * Test + */ +import {MagnetarExtenderMock} from "test/mocks/MagnetarExtenderMock.sol"; +import {ToeTokenReceiverMock} from "test/mocks/ToeTokenMock/ToeTokenReceiverMock.sol"; +import {ToeTokenSenderMock} from "test/mocks/ToeTokenMock/ToeTokenSenderMock.sol"; +import {ToeTokenMock} from "test/mocks/ToeTokenMock/ToeTokenMock.sol"; +import {TestHelper} from "test/LZSetup/TestHelper.sol"; + +contract MagnetarBaseTest is TestHelper { + // Address mapping + uint256 internal adminPKey = 0x1; + address public adminAddr = vm.addr(adminPKey); + uint256 internal alicePKey = 0x2; + address public aliceAddr = vm.addr(alicePKey); + + // Core contracts + Magnetar public magnetar; + address payable collateralModule; + address payable yieldBoxModule; + address payable optionModule; + address payable mintModule; + + // Peripheral contracts + TapiocaOmnichainEngineHelper public toeHelper; + MagnetarExtenderMock public magnetarExtender; + TapiocaOmnichainExtExec public toeExtExec; + IMagnetarHelper public magnetarHelper; + Pearlmit public pearlmit; + Cluster public cluster; + + // Tokens + ToeTokenMock aToeOFT; + ToeTokenMock bToeOFT; + + // Constants + uint32 public EID_A = 1; + address public ENDPOINT_A; + + uint32 public EID_B = 2; + address public ENDPOINT_B; + + function setUp() public virtual override { + vm.label(adminAddr, "admin"); + vm.label(aliceAddr, "alice"); + + // Peripheral + magnetarHelper = IMagnetarHelper(address(new MagnetarHelper())); + pearlmit = new Pearlmit("Pearlmit", "1", adminAddr, 0); + toeHelper = new TapiocaOmnichainEngineHelper(); + toeExtExec = new TapiocaOmnichainExtExec(); + cluster = new Cluster(0, adminAddr); + + // Core + collateralModule = payable(new MagnetarCollateralModule(IPearlmit(address(pearlmit)), address(toeHelper))); + yieldBoxModule = payable(new MagnetarYieldBoxModule(IPearlmit(address(pearlmit)), address(toeHelper))); + optionModule = payable(new MagnetarOptionModule(IPearlmit(address(pearlmit)), address(toeHelper))); + mintModule = payable(new MagnetarMintModule(IPearlmit(address(pearlmit)), address(toeHelper))); + magnetarExtender = new MagnetarExtenderMock(); + + magnetar = new Magnetar( + ICluster(address(cluster)), + adminAddr, + collateralModule, + mintModule, + optionModule, + yieldBoxModule, + IPearlmit(address(pearlmit)), + address(toeHelper), + magnetarHelper + ); + + // Lz setup + + setUpEndpoints(3, LibraryType.UltraLightNode); + ENDPOINT_A = address(endpoints[EID_A]); + ENDPOINT_B = address(endpoints[EID_B]); + + aToeOFT = new ToeTokenMock( + address(endpoints[EID_A]), + adminAddr, + address(toeExtExec), + address( + new ToeTokenSenderMock( + "", "", address(endpoints[EID_A]), address(this), address(0), IPearlmit(address(pearlmit)), cluster + ) + ), + address( + new ToeTokenReceiverMock( + "", "", address(endpoints[EID_A]), address(this), address(0), IPearlmit(address(pearlmit)), cluster + ) + ), + IPearlmit(address(pearlmit)), + cluster + ); + + // Setup + vm.startPrank(adminAddr); + cluster.updateContract(0, address(magnetar), true); + cluster.updateContract(0, address(aToeOFT), true); + vm.stopPrank(); + } + + /** + * @dev Helper to build an ERC20PermitApprovalMsg. + * @param _permit The permit data. + * @param _digest The typed data digest. + * @param _token The token contract to receive the permit. + * @param _pkSigner The private key signer. + */ + function getERC20PermitData(ERC20PermitStruct memory _permit, bytes32 _digest, address _token, uint256 _pkSigner) + internal + pure + returns (ERC20PermitApprovalMsg memory permitApproval_) + { + (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(_pkSigner, _digest); + + permitApproval_ = ERC20PermitApprovalMsg({ + token: _token, + owner: _permit.owner, + spender: _permit.spender, + value: _permit.value, + deadline: _permit.deadline, + v: v_, + r: r_, + s: s_ + }); + } +} diff --git a/test/mocks/MagnetarExtenderMock.sol b/test/mocks/MagnetarExtenderMock.sol new file mode 100644 index 00000000..865b2ded --- /dev/null +++ b/test/mocks/MagnetarExtenderMock.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +import {IMagnetarModuleExtender, MagnetarCall} from "tapioca-periph/interfaces/periph/IMagnetar.sol"; + +contract MagnetarExtenderMock is IMagnetarModuleExtender { + function isValidActionId(uint8 actionId) external view returns (bool) { + // we want this to pass the Magnetar check to call with ID 100, then force revert on `handleAction` + if (actionId == 100) { + return true; + } + // Action 200 does nothing + if (actionId == 200) { + return true; + } + return false; + } + + function handleAction(MagnetarCall calldata call) external payable { + if (call.id == 100) { + revert("Invalid action id"); + } + } +} diff --git a/test/mocks/ToeTokenMock/ToeTokenMock.sol b/test/mocks/ToeTokenMock/ToeTokenMock.sol index ccf0f07c..0fa53f3d 100644 --- a/test/mocks/ToeTokenMock/ToeTokenMock.sol +++ b/test/mocks/ToeTokenMock/ToeTokenMock.sol @@ -5,7 +5,14 @@ pragma solidity 0.8.22; import { MessagingReceipt, OFTReceipt, SendParam } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol"; +import { + MessagingReceipt, + OFTReceipt, + SendParam, + MessagingFee +} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol"; import {ERC20Permit, ERC20} from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; +import {OFTCore} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFT.sol"; // Tapioca import {BaseTapiocaOmnichainEngine} from "tapioca-periph/tapiocaOmnichainEngine/BaseTapiocaOmnichainEngine.sol"; @@ -54,33 +61,73 @@ contract ToeTokenMock is BaseTapiocaOmnichainEngine, ModuleManager, ERC20Permit function sendPacket(LZSendParam calldata _lzSendParam, bytes calldata _composeMsg) public payable - returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) + returns ( + MessagingReceipt memory msgReceipt, + OFTReceipt memory oftReceipt, + bytes memory msgSent, + bytes memory options + ) { - (msgReceipt, oftReceipt) = abi.decode( + (msgReceipt, oftReceipt, msgSent, options) = abi.decode( _executeModule( uint8(Module.ToeTokenSender), abi.encodeCall(TapiocaOmnichainSender.sendPacket, (_lzSendParam, _composeMsg)), false ), - (MessagingReceipt, OFTReceipt) + (MessagingReceipt, OFTReceipt, bytes, bytes) ); } + /** + * @dev See `TapiocaOmnichainSender.sendPacket` + */ function sendPacketFrom(address _from, LZSendParam calldata _lzSendParam, bytes calldata _composeMsg) public payable - returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) + returns ( + MessagingReceipt memory msgReceipt, + OFTReceipt memory oftReceipt, + bytes memory msgSent, + bytes memory options + ) { - (msgReceipt, oftReceipt) = abi.decode( + (msgReceipt, oftReceipt, msgSent, options) = abi.decode( _executeModule( uint8(Module.ToeTokenSender), abi.encodeCall(TapiocaOmnichainSender.sendPacketFrom, (_from, _lzSendParam, _composeMsg)), false ), - (MessagingReceipt, OFTReceipt) + (MessagingReceipt, OFTReceipt, bytes, bytes) ); } + /** + * See `OFTCore::send()` + * @dev override default `send` behavior to add `whenNotPaused` modifier + */ + function send(SendParam calldata _sendParam, MessagingFee calldata _fee, address _refundAddress) + external + payable + override(OFTCore) + returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) + { + // @dev Applies the token transfers regarding this send() operation. + // - 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); + + // @dev Builds the options and OFT message to quote in the endpoint. + (bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam, amountReceivedLD); + + // @dev Sends the message to the LayerZero endpoint and returns the LayerZero msg receipt. + msgReceipt = _lzSend(_sendParam.dstEid, message, options, _fee, _refundAddress); + // @dev Formulate the OFT receipt. + oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); + + emit OFTSent(msgReceipt.guid, _sendParam.dstEid, msg.sender, amountSentLD, amountReceivedLD); + } + function getTypedDataHash(ERC20PermitStruct calldata _permitData) public view returns (bytes32) { bytes32 permitTypeHash_ = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");