Skip to content

fix(ovault-evm): override _sendLocal in VaultComposerSyncNative to deliver native ETH#1939

Open
shunkakinoki wants to merge 1 commit intoLayerZero-Labs:mainfrom
shunkakinoki:fix/vault-composer-native-send-local
Open

fix(ovault-evm): override _sendLocal in VaultComposerSyncNative to deliver native ETH#1939
shunkakinoki wants to merge 1 commit intoLayerZero-Labs:mainfrom
shunkakinoki:fix/vault-composer-native-send-local

Conversation

@shunkakinoki
Copy link

Problem

VaultComposerSyncNative correctly overrides _sendRemote to unwrap WETH → ETH before a cross-chain send via Stargate PoolNative. However, it was missing the same override for _sendLocal (same-chain delivery, dstEid == VAULT_EID).

The base VaultComposerSync._sendLocal does:

address erc20 = _oft == ASSET_OFT ? ASSET_ERC20 : SHARE_ERC20;
IERC20(erc20).safeTransfer(recipient, amount); // transfers WETH, not ETH

This caused the lzCompose redeem path (e.g. Katana vbWETH → Ethereum native ETH) to deliver WETH instead of native ETH to the recipient when dstEid == VAULT_EID.

Confirmed via Tenderly trace on mainnet tx 0x634177bc2e23536080ca93ae73f4e626a3bd84ce912b314b62aa524b8b08b645:

_sendLocal(_oft=ASSET_OFT, dstEid=30101)
  → safeTransfer(WETH, recipient, 1693000000000000)  ← wrong, should be native ETH

Fix

Override _sendLocal in VaultComposerSyncNative to call WETH.withdraw() then transfer native ETH for the ASSET_OFT path. Share-path local sends (_oft == SHARE_OFT) fall through to super._sendLocal unchanged.

The receive() function already permits ETH from ASSET_ERC20 (WETH), so the withdraw() call is handled correctly.

Test plan

  • Unit test: redeem vbWETH shares with dstEid == VAULT_EID via lzCompose, verify recipient receives native ETH not WETH
  • Confirm _sendLocal share path (_oft == SHARE_OFT) still calls super._sendLocal unchanged
  • Confirm _sendRemote path is unaffected

🤖 Generated with Claude Code

…liver 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().
Copilot AI review requested due to automatic review settings March 12, 2026 18:02
@cursor
Copy link

cursor bot commented Mar 12, 2026

PR Summary

Medium Risk
Changes the local payout path from ERC20 transfers to native ETH calls, which affects fund delivery semantics and introduces call/revert behavior that must be exercised in tests.

Overview
Fixes same-chain (dstEid == VAULT_EID) redemption delivery in VaultComposerSyncNative by overriding _sendLocal to unwrap WETH to ETH and transfer native ETH to the recipient when sending the asset (_oft == ASSET_OFT).

Adds NativeTransferFailed to IVaultComposerSyncNative and falls back to super._sendLocal for the share path, leaving cross-chain _sendRemote behavior unchanged.

Written by Cursor Bugbot for commit f0a9c2e. Configure here.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes same-chain (dstEid == VAULT_EID) asset delivery for VaultComposerSyncNative so that local redemptions deliver native ETH (by unwrapping WETH) rather than incorrectly transferring WETH as an ERC20.

Changes:

  • Add VaultComposerSyncNative._sendLocal override to unwrap WETH → ETH and transfer native ETH for the ASSET_OFT path.
  • Add a new custom error NativeTransferFailed() for failed native ETH transfers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/ovault-evm/contracts/interfaces/IVaultComposerSyncNative.sol Adds NativeTransferFailed() error used by the native local-send logic.
packages/ovault-evm/contracts/VaultComposerSyncNative.sol Overrides _sendLocal to unwrap WETH and send native ETH on same-chain asset delivery.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +105 to +107
IWETH(ASSET_ERC20).withdraw(_sendParam.amountLD);
(bool success, ) = payable(address(uint160(uint256(_sendParam.to)))).call{ value: _sendParam.amountLD }("");
if (!success) revert NativeTransferFailed();
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_sendParam.to is decoded as a bytes32 “address” everywhere else via bytes32ToAddress() (see VaultComposerSync._sendLocal). Here it’s converted with address(uint160(uint256(_sendParam.to))), which is inconsistent and can be error-prone if the bytes32 encoding ever changes or includes non-address data. Prefer using the same bytes32ToAddress() helper for consistency (and add the needed using OFTComposeMsgCodec for bytes32; in this contract since using directives aren’t inherited).

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +110
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);
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new local-send branch (unwrap WETH + native ETH transfer) isn’t covered by existing tests: current native unit tests cover dstEid == VAULT_EID on the deposit/share path, but not the redeem/asset path where _oft == ASSET_OFT and _sendLocal is hit. Please add a unit test that redeems and sends with dstEid == VAULT_EID and asserts the recipient’s ETH balance increases (and that they do not receive WETH).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants