Slippage/Minimum amount does not work during single-side redemption
The slippage or minimum amount of tokens to be received is set to a value much smaller than expected due to the use of `TwoTokenPoolUtils._getMinExitAmounts` function to automatically compute the slippage or minimum amount on behalf of the callers during a single-sided redemption. As a result, the vault will continue to redeem the pool tokens even if the trade incurs significant slippage, resulting in the vault receiving fewer tokens in return, leading to losses for the vault shareholders.\\nThe `Curve2TokenConvexHelper._executeSettlement` function is called by the following functions:\\n`Curve2TokenConvexHelper.settleVault`\\n`Curve2TokenConvexHelper.settleVault` function is called within the `Curve2TokenConvexVault.settleVaultNormal` and `Curve2TokenConvexVault.settleVaultPostMaturity` functions\\n`Curve2TokenConvexHelper.settleVaultEmergency`\\n`Curve2TokenConvexHelper.settleVaultEmergency` is called by `Curve2TokenConvexVault.settleVaultEmergency`\\nIn summary, the `Curve2TokenConvexHelper._executeSettlement` function is called during vault settlement.\\nAn important point to note here is that within the `Curve2TokenConvexHelper._executeSettlement` function, the `params.minPrimary` and `params.minSecondary` are automatically computed and overwritten by the `TwoTokenPoolUtils._getMinExitAmounts` function (Refer to Line 124 below). Therefore, if the caller attempts to define the `params.minPrimary` and `params.minSecondary`, they will be discarded and overwritten. The `params.minPrimary` and `params.minSecondary` is for slippage control when redeeming the Curve's LP tokens.\\n```\\nFile: Curve2TokenConvexHelper.sol\\n    function _executeSettlement(\\n        StrategyContext calldata strategyContext,\\n        Curve2TokenPoolContext calldata poolContext,\\n        uint256 maturity,\\n        uint256 poolClaimToSettle,\\n        uint256 redeemStrategyTokenAmount,\\n        RedeemParams memory params\\n    ) private {\\n        (uint256 spotPrice, uint256 oraclePrice) = poolContext._getSpotPriceAndOraclePrice(strategyContext);\\n\\n        /// @notice params.minPrimary and params.minSecondary are not required to be passed in by the caller\\n        /// for this strategy vault\\n        (params.minPrimary, params.minSecondary) = poolContext.basePool._getMinExitAmounts({\\n            strategyContext: strategyContext,\\n            oraclePrice: oraclePrice,\\n            spotPrice: spotPrice,\\n            poolClaim: poolClaimToSettle\\n        });\\n```\\n\\nThe `TwoTokenPoolUtils._getMinExitAmounts` function calculates the minimum amount on the share of the pool with a small discount.\\nAssume a Curve Pool with the following configuration:\\nConsist of two tokens (DAI and USDC). DAI is primary token, USDC is secondary token.\\nPool holds 200 US Dollars worth of tokens (50 DAI and 150 USDC).\\nDAI <> USDC price is 1:1\\ntotalSupply = 100 LP Pool Tokens\\nAssume that 50 LP Pool Tokens will be claimed during vault settlement.\\n```\\nminPrimary = (poolContext.primaryBalance * poolClaim * strategyContext.vaultSettings.poolSlippageLimitPercent /  (totalPoolSupply * uint256(VaultConstants.VAULT_PERCENT_BASIS)\\nminPrimary = (50 DAI * 50 LP_TOKEN * 99.75% /  (100 LP_TOKEN * 100%)\\n\\nRewrite for clarity (ignoring rounding error):\\nminPrimary = 50 DAI * (50 LP_TOKEN/100 LP_TOKEN) * (99.75%/100%) = 24.9375 DAI\\n\\nminSecondary = same calculation = 74.8125 USDC\\n```\\n\\n`TwoTokenPoolUtils._getMinExitAmounts` function will return `24.9375 DAI` as `params.minPrimary` and `74.8125 USDC` as `params.minSecondary`.\\n```\\nFile: TwoTokenPoolUtils.sol\\n    /// @notice calculates the expected primary and secondary amounts based on\\n    /// the given spot price and oracle price\\n    function _getMinExitAmounts(\\n        TwoTokenPoolContext calldata poolContext,\\n        StrategyContext calldata strategyContext,\\n        uint256 spotPrice,\\n        uint256 oraclePrice,\\n        uint256 poolClaim\\n    ) internal view returns (uint256 minPrimary, uint256 minSecondary) {\\n        strategyContext._checkPriceLimit(oraclePrice, spotPrice);\\n\\n        // min amounts are calculated based on the share of the Balancer pool with a small discount applied\\n        uint256 totalPoolSupply = poolContext.poolToken.totalSupply();\\n        minPrimary = (poolContext.primaryBalance * poolClaim * \\n            strategyContext.vaultSettings.poolSlippageLimitPercent) / \\n            (totalPoolSupply * uint256(VaultConstants.VAULT_PERCENT_BASIS));\\n        minSecondary = (poolContext.secondaryBalance * poolClaim * \\n            strategyContext.vaultSettings.poolSlippageLimitPercent) / \\n            (totalPoolSupply * uint256(VaultConstants.VAULT_PERCENT_BASIS));\\n    }\\n```\\n\\nWhen settling the vault, it is possible to instruct the vault to redeem the Curve's LP tokens single-sided or proportionally. Settle vault functions will trigger a chain of functions that will eventually call the `Curve2TokenConvexHelper._unstakeAndExitPool` function that is responsible for redeeming the Curve's LP tokens.\\nWithin the `Curve2TokenConvexHelper._unstakeAndExitPool` function, if the `params.secondaryTradeParams.length` is zero, the redemption will be single-sided (refer to Line 242 below). Otherwise, the redemption will be executed proportionally (refer to Line 247 below). For a single-sided redemption, only the `params.minPrimary` will be used.\\n```\\nFile: Curve2TokenPoolUtils.sol\\n    function _unstakeAndExitPool(\\n        Curve2TokenPoolContext memory poolContext,\\n        ConvexStakingContext memory stakingContext,\\n        uint256 poolClaim,\\n        RedeemParams memory params\\n    ) internal returns (uint256 primaryBalance, uint256 secondaryBalance) {\\n        // Withdraw pool tokens back to the vault for redemption\\n        bool success = stakingContext.rewardPool.withdrawAndUnwrap(poolClaim, false); // claimRewards = false\\n        if (!success) revert Errors.UnstakeFailed();\\n\\n        if (params.secondaryTradeParams.length == 0) {\\n            // Redeem single-sided\\n            primaryBalance = ICurve2TokenPool(address(poolContext.curvePool)).remove_liquidity_one_coin(\\n                poolClaim, int8(poolContext.basePool.primaryIndex), params.minPrimary\\n            );\\n        } else {\\n            // Redeem proportionally\\n            uint256[2] memory minAmounts;\\n            minAmounts[poolContext.basePool.primaryIndex] = params.minPrimary;\\n            minAmounts[poolContext.basePool.secondaryIndex] = params.minSecondary;\\n            uint256[2] memory exitBalances = ICurve2TokenPool(address(poolContext.curvePool)).remove_liquidity(\\n                poolClaim, minAmounts\\n            );\\n\\n            (primaryBalance, secondaryBalance) \\n                = (exitBalances[poolContext.basePool.primaryIndex], exitBalances[poolContext.basePool.secondaryIndex]);\\n        }\\n    }\\n```\\n\\nAssume that the caller decided to perform a single-sided redemption of 50 LP Pool Tokens, using the earlier example. In this case,\\n`poolClaim` = 50 LP Pool Tokens\\n`params.minPrimary` = 24.9375 DAI\\n`params.minSecondary` = 74.8125 USDC\\nThe data passed into the `remove_liquidity_one_coin` will be as follows:\\n```\\n@notice Withdraw a single coin from the pool\\n@param _token_amount Amount of LP tokens to burn in the withdrawal\\n@param i Index value of the coin to withdraw\\n@param _min_amount Minimum amount of coin to receive\\n@return Amount of coin received\\ndef remove_liquidity_one_coin(\\n    _token_amount: uint256,\\n    i: int128,\\n    _min_amount: uint256\\n) -> uint256:\\n```\\n\\n```\\nremove_liquidity_one_coin(poolClaim, int8(poolContext.basePool.primaryIndex), params.minPrimary);\\nremove_liquidity_one_coin(50 LP_TOKEN, Index 0=DAI, 24.9375 DAI);\\n```\\n\\nAssume the pool holds 200 US dollars worth of tokens (50 DAI and 150 USDC), and the total supply is 100 LP Tokens. The pool's state is imbalanced, so any trade will result in significant slippage.\\nIntuitively (ignoring the slippage & fee), redeeming 50 LP Tokens should return approximately 100 US dollars worth of tokens, which means around 100 DAI. Thus, the slippage or minimum amount should ideally be around 100 DAI (+/- 5%).\\nHowever, the trade will be executed in the above example even if the vault receives only 25 DAI because the `params.minPrimary` is set to `24.9375 DAI`. This could result in a loss of around 75 DAI due to slippage (about 75% slippage rate) in the worst-case scenario.
When performing a single-side redemption, avoid using the `TwoTokenPoolUtils._getMinExitAmounts` function to automatically compute the slippage or minimum amount of tokens to receive on behalf of the caller. Instead, give the caller the flexibility to define the slippage (params.minPrimary). To prevent the caller from setting a slippage that is too large, consider restricting the slippage to an acceptable range.\\nThe proper way of computing the minimum amount of tokens to receive from a single-side trade (remove_liquidity_one_coin) is to call the Curve Pool's `calc_withdraw_one_coin` function off-chain to calculate the amount received when withdrawing a single LP Token, and then apply an acceptable discount.\\nNote that the `calc_withdraw_one_coin` function cannot be used solely on-chain for computing the minimum amount because the result can be manipulated since it uses spot balances for computation.
The slippage or minimum amount of tokens to be received is set to a value much smaller than expected. Thus, the vault will continue to redeem the pool tokens even if the trade incurs significant slippage, resulting in the vault receiving fewer tokens in return, leading to losses for the vault shareholders.
```\\nFile: Curve2TokenConvexHelper.sol\\n    function _executeSettlement(\\n        StrategyContext calldata strategyContext,\\n        Curve2TokenPoolContext calldata poolContext,\\n        uint256 maturity,\\n        uint256 poolClaimToSettle,\\n        uint256 redeemStrategyTokenAmount,\\n        RedeemParams memory params\\n    ) private {\\n        (uint256 spotPrice, uint256 oraclePrice) = poolContext._getSpotPriceAndOraclePrice(strategyContext);\\n\\n        /// @notice params.minPrimary and params.minSecondary are not required to be passed in by the caller\\n        /// for this strategy vault\\n        (params.minPrimary, params.minSecondary) = poolContext.basePool._getMinExitAmounts({\\n            strategyContext: strategyContext,\\n            oraclePrice: oraclePrice,\\n            spotPrice: spotPrice,\\n            poolClaim: poolClaimToSettle\\n        });\\n```\\n