fix(ovault-evm): override _sendLocal in VaultComposerSyncNative to deliver native ETH#1939
Conversation
…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().
PR SummaryMedium Risk Overview Adds Written by Cursor Bugbot for commit f0a9c2e. Configure here. |
There was a problem hiding this comment.
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._sendLocaloverride to unwrap WETH → ETH and transfer native ETH for theASSET_OFTpath. - 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.
| IWETH(ASSET_ERC20).withdraw(_sendParam.amountLD); | ||
| (bool success, ) = payable(address(uint160(uint256(_sendParam.to)))).call{ value: _sendParam.amountLD }(""); | ||
| if (!success) revert NativeTransferFailed(); |
There was a problem hiding this comment.
_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).
| 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); | ||
| } |
There was a problem hiding this comment.
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).
Problem
VaultComposerSyncNativecorrectly overrides_sendRemoteto 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._sendLocaldoes:This caused the
lzComposeredeem path (e.g. Katana vbWETH → Ethereum native ETH) to deliver WETH instead of native ETH to the recipient whendstEid == VAULT_EID.Confirmed via Tenderly trace on mainnet tx
0x634177bc2e23536080ca93ae73f4e626a3bd84ce912b314b62aa524b8b08b645:Fix
Override
_sendLocalinVaultComposerSyncNativeto callWETH.withdraw()then transfer native ETH for theASSET_OFTpath. Share-path local sends (_oft == SHARE_OFT) fall through tosuper._sendLocalunchanged.The
receive()function already permits ETH fromASSET_ERC20(WETH), so thewithdraw()call is handled correctly.Test plan
dstEid == VAULT_EIDvialzCompose, verify recipient receives native ETH not WETH_sendLocalshare path (_oft == SHARE_OFT) still callssuper._sendLocalunchanged_sendRemotepath is unaffected🤖 Generated with Claude Code