From f0a9c2eb1fceeb677049cbd68742028daa1126ea Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 13 Mar 2026 03:01:31 +0900 Subject: [PATCH] fix(ovault-evm): override _sendLocal in VaultComposerSyncNative to deliver native ETH The base VaultComposerSync._sendLocal calls IERC20(ASSET_ERC20).safeTransfer() which transfers WETH directly to the recipient. VaultComposerSyncNative correctly overrides _sendRemote to unwrap WETH before a cross-chain send, but was missing the same override for _sendLocal (same-chain delivery, dstEid == VAULT_EID). This caused the lzCompose redeem path (Katana vbWETH -> Ethereum native ETH) to deliver WETH instead of ETH when the destination is on the same chain as the vault. Fix: override _sendLocal to call WETH.withdraw() before transferring native ETH to the recipient. Share-path local sends are unchanged via super._sendLocal(). --- .../contracts/VaultComposerSyncNative.sol | 25 +++++++++++++++++++ .../interfaces/IVaultComposerSyncNative.sol | 1 + 2 files changed, 26 insertions(+) diff --git a/packages/ovault-evm/contracts/VaultComposerSyncNative.sol b/packages/ovault-evm/contracts/VaultComposerSyncNative.sol index b9842b3fd2..5ff2c28d89 100644 --- a/packages/ovault-evm/contracts/VaultComposerSyncNative.sol +++ b/packages/ovault-evm/contracts/VaultComposerSyncNative.sol @@ -85,6 +85,31 @@ contract VaultComposerSyncNative is VaultComposerSync, IVaultComposerSyncNative ); } + /** + * @dev Unwrap WETH and transfer native ETH directly to recipient on the same chain + * @dev Overrides base _sendLocal which would incorrectly transfer WETH (ERC20) instead of native ETH + * @dev The base VaultComposerSync._sendLocal does `IERC20(ASSET_ERC20).safeTransfer(recipient, amount)` + * which transfers WETH. For the native variant, the vault redeems WETH but the recipient + * expects native ETH, so we must call WETH.withdraw() first. + * @param _oft The OFT contract address to determine asset vs share path + * @param _sendParam The parameters for the send operation + */ + function _sendLocal( + address _oft, + SendParam memory _sendParam, + address _refundAddress, + uint256 _msgValue + ) internal override virtual { + if (_oft == ASSET_OFT) { + /// @dev Vault redeems WETH; unwrap to native ETH before delivering to recipient + IWETH(ASSET_ERC20).withdraw(_sendParam.amountLD); + (bool success, ) = payable(address(uint160(uint256(_sendParam.to)))).call{ value: _sendParam.amountLD }(""); + if (!success) revert NativeTransferFailed(); + } else { + super._sendLocal(_oft, _sendParam, _refundAddress, _msgValue); + } + } + /** * @dev Unwrap WETH when sending to Stargate PoolNative and send via OFT * @dev Overriden to unwrap WETH when sending to Stargate PoolNative diff --git a/packages/ovault-evm/contracts/interfaces/IVaultComposerSyncNative.sol b/packages/ovault-evm/contracts/interfaces/IVaultComposerSyncNative.sol index d12d65efb2..a68231b1d0 100644 --- a/packages/ovault-evm/contracts/interfaces/IVaultComposerSyncNative.sol +++ b/packages/ovault-evm/contracts/interfaces/IVaultComposerSyncNative.sol @@ -7,6 +7,7 @@ interface IVaultComposerSyncNative { error AssetOFTTokenNotNative(); // 0xd61c4b4a error AmountExceedsMsgValue(); // 0x0f971d59 error ETHTransferNotFromAsset(); // 0x02cadbeb + error NativeTransferFailed(); // 0x26b2aba5 /** * @notice Deposits Native token (ETH) from the caller into the vault and sends them to the recipient