diff --git a/packages/ovault-composer/contracts/OVaultComposer.sol b/packages/ovault-composer/contracts/OVaultComposer.sol index d2c3cf0ec3..806580b797 100644 --- a/packages/ovault-composer/contracts/OVaultComposer.sol +++ b/packages/ovault-composer/contracts/OVaultComposer.sol @@ -8,6 +8,7 @@ import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.s import { IOFT, SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; import { IOAppCore } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; +import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; import { IOVaultComposer, FailedMessage, FailedState } from "./interfaces/IOVaultComposer.sol"; @@ -22,6 +23,7 @@ contract OVaultComposer is IOVaultComposer, ReentrancyGuard { address public immutable SHARE_OFT; address public immutable OVAULT; address public immutable ENDPOINT; + uint32 public immutable COMPOSER_EID; mapping(bytes32 guid => FailedMessage) public failedMessages; @@ -36,6 +38,7 @@ contract OVaultComposer is IOVaultComposer, ReentrancyGuard { SHARE_OFT = IOVault(_ovault).SHARE_OFT(); ASSET_OFT = IOVault(_ovault).ASSET_OFT(); ENDPOINT = address(IOAppCore(ASSET_OFT).endpoint()); + COMPOSER_EID = ILayerZeroEndpointV2(ENDPOINT).eid(); // Approve the adapter to spend the share tokens held by this contract IERC20(share).approve(OVAULT, type(uint256).max); @@ -129,6 +132,10 @@ contract OVaultComposer is IOVaultComposer, ReentrancyGuard { /// @dev Dirty swapping amountLD and minAmountLD to 1e18 and 0 to avoid Slippage issue on the target OFT quoteSend() function validateTargetOFTConfig(address _oft, SendParam memory _sendParam) external view { + if (COMPOSER_EID == _sendParam.dstEid) { + return; + } + _sendParam.amountLD = 1e18; _sendParam.minAmountLD = 0; @@ -138,6 +145,18 @@ contract OVaultComposer is IOVaultComposer, ReentrancyGuard { /// @dev External call for try...catch logic in lzCompose() function send(address _oft, SendParam calldata _sendParam) external payable nonReentrant { if (msg.sender != address(this)) revert OnlySelf(msg.sender); + if (_sendParam.dstEid == COMPOSER_EID) { + address _receiver = _sendParam.to.bytes32ToAddress(); + uint256 _amountLD = _sendParam.amountLD; + IERC20 token = IERC20(IOFT(_oft).token()); + token.transfer(_receiver, _amountLD); + if (msg.value > 0) { + (bool sent, ) = _receiver.call{ value: msg.value }(""); + require(sent, "Failed to send Ether"); + } + emit SentOnHub(_receiver, _oft, _amountLD); + return; + } _send(_oft, _sendParam); } diff --git a/packages/ovault-composer/contracts/interfaces/IOVaultComposer.sol b/packages/ovault-composer/contracts/interfaces/IOVaultComposer.sol index 841979f0f5..a03f89e5b2 100644 --- a/packages/ovault-composer/contracts/interfaces/IOVaultComposer.sol +++ b/packages/ovault-composer/contracts/interfaces/IOVaultComposer.sol @@ -22,6 +22,7 @@ interface IOVaultComposer is IOAppComposer { /// ========================== EVENTS ===================================== event DecodeFailed(bytes32 indexed guid, address indexed oft, bytes message); event Sent(bytes32 indexed guid, address indexed oft); + event SentOnHub(address indexed receiver, address indexed oft, uint256 amountLD); event SendFailed(bytes32 indexed guid, address indexed oft); event Refunded(bytes32 indexed guid, address indexed oft); event Retried(bytes32 indexed guid, address indexed oft); diff --git a/packages/ovault-composer/test/composer/OVaultComposer_Unit.t.sol b/packages/ovault-composer/test/composer/OVaultComposer_Unit.t.sol index e0c447668e..7bb951955c 100644 --- a/packages/ovault-composer/test/composer/OVaultComposer_Unit.t.sol +++ b/packages/ovault-composer/test/composer/OVaultComposer_Unit.t.sol @@ -87,6 +87,51 @@ contract OVaultComposerUnitTest is OVaultComposerBaseTest { assertEq(shareOFT_arb.totalSupply(), 0); } + function test_lzCompose_pass_on_hub() public { + bytes32 guid = _randomGUID(); + assetOFT_arb.mint(address(OVaultComposerArb), TOKENS_TO_SEND); + + SendParam memory internalSendParam = SendParam( + OVaultComposerArb.COMPOSER_EID(), + addressToBytes32(userA), + TOKENS_TO_SEND, + 0, + OPTIONS_LZRECEIVE_2M, + "", + "" + ); + + bytes memory composeMsg = _createComposePayload(ETH_EID, internalSendParam, TOKENS_TO_SEND, userA); + + vm.expectEmit(true, true, true, true, address(assetOFT_arb)); + emit IERC20.Transfer(address(OVaultComposerArb), address(oVault_arb), TOKENS_TO_SEND); + + vm.expectEmit(true, true, true, true, address(shareOFT_arb)); + emit IERC20.Transfer(address(0), address(OVaultComposerArb), TOKENS_TO_SEND); + + vm.expectEmit(true, true, true, true, address(oVault_arb)); + emit IERC4626Adapter.Deposit( + address(OVaultComposerArb), + address(OVaultComposerArb), + TOKENS_TO_SEND, + TOKENS_TO_SEND + ); + + vm.expectEmit(true, true, true, true, address(OVaultComposerArb)); + emit IOVaultComposer.SentOnHub(userA, address(shareOFT_arb), TOKENS_TO_SEND); + + assertEq(assetOFT_arb.totalSupply(), assetOFT_arb.balanceOf(address(OVaultComposerArb)), TOKENS_TO_SEND); + assertEq(shareOFT_arb.totalSupply(), 0); + + vm.prank(arbEndpoint); + OVaultComposerArb.lzCompose{ value: 1 ether }(address(assetOFT_arb), guid, composeMsg, arbExecutor, ""); + + assertEq(uint256(OVaultComposerArb.failedGuidState(guid)), uint256(FailedState.NotFound)); + + assertEq(assetOFT_arb.totalSupply(), assetOFT_arb.balanceOf(address(oVault_arb)), TOKENS_TO_SEND); + assertEq(shareOFT_arb.totalSupply(), shareOFT_arb.balanceOf(address(userA)), TOKENS_TO_SEND); + } + function test_lzCompose_fail_invalid_payload() public { bytes32 guid = _randomGUID(); assetOFT_arb.mint(address(OVaultComposerArb), TOKENS_TO_SEND);