Ineffective slippage mechanism when redeeming proportionally
A trade will continue to be executed regardless of how bad the slippage is since the minimum amount returned by the `TwoTokenPoolUtils._getMinExitAmounts` function does not work effectively. Thus, a trade might incur significant slippage, resulting in the vault receiving fewer tokens in return, leading to losses for the vault shareholders.\\nThe `params.minPrimary` and `params.minSecondary` are calculated automatically based on the share of the Curve pool with a small discount within the `Curve2TokenConvexHelper._executeSettlement` function (Refer to Line 124 below)\\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\\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) / // @audit-info poolSlippageLimitPercent = 9975, # 0.25%\\n            (totalPoolSupply * uint256(VaultConstants.VAULT_PERCENT_BASIS)); // @audit-info VAULT_PERCENT_BASIS = 1e4 = 10000\\n        minSecondary = (poolContext.secondaryBalance * poolClaim * \\n            strategyContext.vaultSettings.poolSlippageLimitPercent) / \\n            (totalPoolSupply * uint256(VaultConstants.VAULT_PERCENT_BASIS));\\n    }\\n```\\n\\nWhen LP tokens are redeemed proportionally via the Curve Pool's `remove_liquidity` function, the tokens received are based on the share of the Curve pool as the source code.\\n```\\n@external\\n@nonreentrant('lock')\\ndef remove_liquidity(\\n    _amount: uint256,\\n    _min_amounts: uint256[N_COINS],\\n) -> uint256[N_COINS]:\\n    """\\n    @notice Withdraw coins from the pool\\n    @dev Withdrawal amounts are based on current deposit ratios\\n    @param _amount Quantity of LP tokens to burn in the withdrawal\\n    @param _min_amounts Minimum amounts of underlying coins to receive\\n    @return List of amounts of coins that were withdrawn\\n    """\\n    amounts: uint256[N_COINS] = self._balances()\\n    lp_token: address = self.lp_token\\n    total_supply: uint256 = ERC20(lp_token).totalSupply()\\n    CurveToken(lp_token).burnFrom(msg.sender, _amount)  # dev: insufficient funds\\n\\n    for i in range(N_COINS):\\n        value: uint256 = amounts[i] * _amount / total_supply\\n        assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected"\\n\\n        amounts[i] = value\\n        if i == 0:\\n            raw_call(msg.sender, b"", value=value)\\n        else:\\n            assert ERC20(self.coins[1]).transfer(msg.sender, value)\\n\\n    log RemoveLiquidity(msg.sender, amounts, empty(uint256[N_COINS]), total_supply - _amount)\\n\\n    return amounts\\n```\\n\\nAssume a Curve Pool with the following state:\\nConsists of 200 US Dollars worth of tokens (100 DAI and 100 USDC). DAI is the primary token\\nDAI <> USDC price is 1:1\\nTotal Supply = 100 LP Pool Tokens\\nAssume that 50 LP Pool Tokens will be claimed during vault settlement.\\n`TwoTokenPoolUtils._getMinExitAmounts` function will return `49.875 DAI` as `params.minPrimary` and `49.875 USDC` as `params.minSecondary` based on the following calculation\\n```\\nminPrimary = (poolContext.primaryBalance * poolClaim * strategyContext.vaultSettings.poolSlippageLimitPercent /  (totalPoolSupply * uint256(VaultConstants.VAULT_PERCENT_BASIS)\\nminPrimary = (100 DAI * 50 LP_TOKEN * 99.75% /  (100 LP_TOKEN * 100%)\\n\\nRewrite for clarity (ignoring rounding error):\\nminPrimary = 100 DAI * (50 LP_TOKEN/100 LP_TOKEN) * (99.75%/100%) = 49.875 DAI\\n\\nminSecondary = same calculation = 49.875 USDC\\n```\\n\\nCurve Pool's `remove_liquidity` function will return `50 DAI` and `50 USDC` if 50 LP Pool Tokens are redeemed.\\nNote that `TwoTokenPoolUtils._getMinExitAmounts` function performs the calculation based on the spot balance of the pool similar to the approach of the Curve Pool's `remove_liquidity` function. However, the `TwoTokenPoolUtils._getMinExitAmounts` function applied a discount to the returned result, while the Curve Pool's `remove_liquidity` function did not.\\nAs such, the number of tokens returned by Curve Pool's `remove_liquidity` function will always be larger than the number of tokens returned by the `TwoTokenPoolUtils._getMinExitAmounts` function regardless of the on-chain economic condition or the pool state (e.g. imbalance). Thus, the minimum amounts (minAmounts) pass into the Curve Pool's `remove_liquidity` function will never be triggered under any circumstance.\\n```\\na = Curve Pool's remove_liquidity => x DAI\\nb = TwoTokenPoolUtils._getMinExitAmounts => (x DAI - 0.25% discount)\\na > b => true (for all instances)\\n```\\n\\nThus, the `TwoTokenPoolUtils._getMinExitAmounts` function is not effective in determining the slippage when redeeming proportionally.
When redeeming proportional, theTwoTokenPoolUtils._getMinExitAmounts function can be removed. Instead, give the caller the flexibility to define the slippage/minimum amount (params.minPrimary and params.minSecondary). 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 proportional trade (remove_liquidity) is to call the Curve's Pool `calc_token_amount` function off-chain and reduce the values returned by the allowed slippage amount.\\nNote that `calc_token_amount` cannot be used solely on-chain for computing the minimum amount because the result can be manipulated because it uses spot balances for computation.\\nSidenote: Removing `TwoTokenPoolUtils._getMinExitAmounts` function also removes the built-in spot price and oracle price validation. Thus, the caller must remember to define the slippage. Otherwise, the vault settlement will risk being sandwiched. Alternatively, shift the `strategyContext._checkPriceLimit(oraclePrice, spotPrice)` code outside the `TwoTokenPoolUtils._getMinExitAmounts` function.
A trade will always be executed even if it returns fewer than expected assets since the minimum amount returned by the `TwoTokenPoolUtils._getMinExitAmounts` function does not work effectively. Thus, a trade might incur unexpected 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