Native ETH not received when removing liquidity from Curve V2 pools
Native ETH was not received when removing liquidity from Curve V2 pools due to the mishandling of Native ETH and WETH, leading to a loss of assets.\\nCurve V2 pool will always wrap to WETH and send to leverage vault unless the `use_eth` is explicitly set to `True`. Otherwise, it will default to `False`. The following implementation of the `remove_liquidity_one_coin` function taken from one of the Curve V2 pools shows that unless the `use_eth` is set to `True`, the `WETH.deposit()` will be triggered to wrap the ETH, and WETH will be transferred back to the caller. The same is true for the `remove_liquidity` function, but it is omitted for brevity.\\n```\\n@external\\n@nonreentrant('lock')\\ndef remove_liquidity_one_coin(token_amount: uint256, i: uint256, min_amount: uint256,\\n                              use_eth: bool = False, receiver: address = msg.sender) -> uint256:\\n    A_gamma: uint256[2] = self._A_gamma()\\n\\n    dy: uint256 = 0\\n    D: uint256 = 0\\n    p: uint256 = 0\\n    xp: uint256[N_COINS] = empty(uint256[N_COINS])\\n    future_A_gamma_time: uint256 = self.future_A_gamma_time\\n    dy, p, D, xp = self._calc_withdraw_one_coin(A_gamma, token_amount, i, (future_A_gamma_time > 0), True)\\n    assert dy >= min_amount, "Slippage"\\n\\n    if block.timestamp >= future_A_gamma_time:\\n        self.future_A_gamma_time = 1\\n\\n    self.balances[i] -= dy\\n    CurveToken(self.token).burnFrom(msg.sender, token_amount)\\n\\n    coin: address = self.coins[i]\\n    if use_eth and coin == WETH20:\\n        raw_call(receiver, b"", value=dy)\\n    else:\\n        if coin == WETH20:\\n            WETH(WETH20).deposit(value=dy)\\n        response: Bytes[32] = raw_call(\\n            coin,\\n            _abi_encode(receiver, dy, method_id=method_id("transfer(address,uint256)")),\\n            max_outsize=32,\\n        )\\n        if len(response) != 0:\\n            assert convert(response, bool)\\n```\\n\\nNotional's Leverage Vault only works with Native ETH. It was found that the `remove_liquidity_one_coin` and `remove_liquidity` functions are executed without explicitly setting the `use_eth` parameter to `True`. Thus, WETH instead of Native ETH will be returned during remove liquidity. As a result, these WETH will not be accounted for in the vault and result in a loss of assets.\\n```\\nFile: Curve2TokenConvexVault.sol\\n    function _unstakeAndExitPool(\\n..SNIP..\\n        ICurve2TokenPool pool = ICurve2TokenPool(CURVE_POOL);\\n        exitBalances = new uint256[](2);\\n        if (isSingleSided) {\\n            // Redeem single-sided\\n            exitBalances[_PRIMARY_INDEX] = pool.remove_liquidity_one_coin(\\n                poolClaim, int8(_PRIMARY_INDEX), _minAmounts[_PRIMARY_INDEX]\\n            );\\n        } else {\\n            // Redeem proportionally, min amounts are rewritten to a fixed length array\\n            uint256[2] memory minAmounts;\\n            minAmounts[0] = _minAmounts[0];\\n            minAmounts[1] = _minAmounts[1];\\n\\n            uint256[2] memory _exitBalances = pool.remove_liquidity(poolClaim, minAmounts);\\n            exitBalances[0] = _exitBalances[0];\\n            exitBalances[1] = _exitBalances[1];\\n        }\\n```\\n
If one of the pool tokens is ETH, consider setting `is_eth` to true when calling `remove_liquidity_one_coin` and `remove_liquidity` functions to ensure that Native ETH is sent back to the vault.
Following are some of the impacts due to the mishandling of Native ETH and WETH during liquidity removal in Curve pools, leading to loss of assets:\\nWithin the `redeemFromNotional`, if the vaults consist of ETH, the `_UNDERLYING_IS_ETH` will be set to true. In this case, the code will attempt to call `transfer` to `transfer` Native ETH, which will fail as Native ETH is not received and users/Notional are unable to redeem.\\n`File: BaseStrategyVault.sol\\n175:     function redeemFromNotional(\\n..SNIP..\\n199:         if (_UNDERLYING_IS_ETH) {\\n200:             if (transferToReceiver > 0) payable(receiver).transfer(transferToReceiver);\\n201:             if (transferToNotional > 0) payable(address(NOTIONAL)).transfer(transferToNotional);\\n202:         } else {\\n..SNIP..`\\nWETH will be received instead of Native ETH during the emergency exit. During vault restoration, WETH is not re-entered into the pool as only Native ETH residing in the vault will be transferred to the pool. Leverage vault only works with Native ETH, and if one of the pool tokens is WETH, it will be converted to Native ETH (0x0 or 0xEeeee) during deployment/initialization. Thus, the WETH is stuck in the vault. This causes the value per share to drop significantly. (Reference)\\n`File: SingleSidedLPVaultBase.sol\\n480:     function emergencyExit(\\n481:         uint256 claimToExit, bytes calldata /* data */\\n482:     ) external override onlyRole(EMERGENCY_EXIT_ROLE) {\\n483:         StrategyVaultState memory state = VaultStorage.getStrategyVaultState();\\n484:         if (claimToExit == 0) claimToExit = state.totalPoolClaim;\\n485: \\n486:         // By setting min amounts to zero, we will accept whatever tokens come from the pool\\n487:         // in a proportional exit. Front running will not have an effect since no trading will\\n488:         // occur during a proportional exit.\\n489:         _unstakeAndExitPool(claimToExit, new uint256[](NUM_TOKENS()), true);\\n490: \\n491:         state.totalPoolClaim = state.totalPoolClaim - claimToExit;\\n492:         state.setStrategyVaultState();`
```\\n@external\\n@nonreentrant('lock')\\ndef remove_liquidity_one_coin(token_amount: uint256, i: uint256, min_amount: uint256,\\n                              use_eth: bool = False, receiver: address = msg.sender) -> uint256:\\n    A_gamma: uint256[2] = self._A_gamma()\\n\\n    dy: uint256 = 0\\n    D: uint256 = 0\\n    p: uint256 = 0\\n    xp: uint256[N_COINS] = empty(uint256[N_COINS])\\n    future_A_gamma_time: uint256 = self.future_A_gamma_time\\n    dy, p, D, xp = self._calc_withdraw_one_coin(A_gamma, token_amount, i, (future_A_gamma_time > 0), True)\\n    assert dy >= min_amount, "Slippage"\\n\\n    if block.timestamp >= future_A_gamma_time:\\n        self.future_A_gamma_time = 1\\n\\n    self.balances[i] -= dy\\n    CurveToken(self.token).burnFrom(msg.sender, token_amount)\\n\\n    coin: address = self.coins[i]\\n    if use_eth and coin == WETH20:\\n        raw_call(receiver, b"", value=dy)\\n    else:\\n        if coin == WETH20:\\n            WETH(WETH20).deposit(value=dy)\\n        response: Bytes[32] = raw_call(\\n            coin,\\n            _abi_encode(receiver, dy, method_id=method_id("transfer(address,uint256)")),\\n            max_outsize=32,\\n        )\\n        if len(response) != 0:\\n            assert convert(response, bool)\\n```\\n