Curve vault will undervalue or overvalue the LP Pool tokens if it comprises tokens with different decimals
A Curve vault that comprises tokens with different decimals will undervalue or overvalue the LP Pool tokens. As a result, users might be liquidated prematurely or be able to borrow more than they are allowed. Additionally, the vault settlement process might break.\\nThe `TwoTokenPoolUtils._getTimeWeightedPrimaryBalance` function, which is utilized by the Curve vault, is used to compute the total value of the LP Pool tokens (poolClaim) denominated in the primary token.\\n```\\nFile: TwoTokenPoolUtils.sol\\n    function _getTimeWeightedPrimaryBalance(\\n        TwoTokenPoolContext memory poolContext,\\n        StrategyContext memory strategyContext,\\n        uint256 poolClaim,\\n        uint256 oraclePrice,\\n        uint256 spotPrice\\n    ) internal view returns (uint256 primaryAmount) {\\n        // Make sure spot price is within oracleDeviationLimit of pairPrice\\n        strategyContext._checkPriceLimit(oraclePrice, spotPrice);\\n        \\n        // Get shares of primary and secondary balances with the provided poolClaim\\n        uint256 totalSupply = poolContext.poolToken.totalSupply();\\n        uint256 primaryBalance = poolContext.primaryBalance * poolClaim / totalSupply;\\n        uint256 secondaryBalance = poolContext.secondaryBalance * poolClaim / totalSupply;\\n\\n        // Value the secondary balance in terms of the primary token using the oraclePairPrice\\n        uint256 secondaryAmountInPrimary = secondaryBalance * strategyContext.poolClaimPrecision / oraclePrice;\\n\\n        // Make sure primaryAmount is reported in primaryPrecision\\n        uint256 primaryPrecision = 10 ** poolContext.primaryDecimals;\\n        primaryAmount = (primaryBalance + secondaryAmountInPrimary) * primaryPrecision / strategyContext.poolClaimPrecision;\\n    }\\n```\\n\\nIf a leverage vault supports a Curve Pool that contains two tokens with different decimals, the math within the `TwoTokenPoolUtils._getTimeWeightedPrimaryBalance` function would not work, and the value returned from it will be incorrect. Consider the following two scenarios:\\nIf primary token's decimals (e.g. 18) > secondary token's decimals (e.g. 6)\\nTo illustrate the issue, assume the following:\\nThe leverage vault supports the DAI-USDC Curve Pool, and its primary token of the vault is DAI.\\nDAI's decimals are 18, while USDC's decimals are 6.\\nCurve Pool's total supply is 100\\nThe Curve Pool holds 100 DAI and 100 USDC\\nFor the sake of simplicity, the price of DAI and USDC is 1:1. Thus, the `oraclePrice` within the function will be `1 * 10^18`. Note that the oracle price is always scaled up to 18 decimals within the vault.\\nThe caller of the `TwoTokenPoolUtils._getTimeWeightedPrimaryBalance` function wanted to compute the total value of 50 LP Pool tokens.\\n```\\nprimaryBalance = poolContext.primaryBalance * poolClaim / totalSupply; // 100 DAI * 50 / 100\\nsecondaryBalance = poolContext.secondaryBalance * poolClaim / totalSupply; // 100 USDC * 50 / 100\\n```\\n\\nThe `primaryBalance` will be `50 DAI`. `50 DAI` denominated in WEI will be `50 * 10^18` since the decimals of DAI are 18.\\nThe `secondaryBalance` will be `50 USDC`. `50 USDC` denominated in WEI will be `50 * 10^6` since the decimals of USDC are 6.\\nNext, the code logic attempts to value the secondary balance (50 USDC) in terms of the primary token (DAI) using the oracle price (1 * 10^18).\\n```\\nsecondaryAmountInPrimary = secondaryBalance * strategyContext.poolClaimPrecision / oraclePrice;\\nsecondaryAmountInPrimary = 50 USDC * 10^18 / (1 * 10^18)\\nsecondaryAmountInPrimary = (50 * 10^6) * 10^18 / (1 * 10^18)\\nsecondaryAmountInPrimary = 50 * 10^6\\n```\\n\\n50 USDC should be worth 50 DAI (50 * 10^18). However, the `secondaryAmountInPrimary` shows that it is only worth 0.00000000005 DAI (50 * 10^6).\\n```\\nprimaryAmount = (primaryBalance + secondaryAmountInPrimary) * primaryPrecision / strategyContext.poolClaimPrecision;\\nprimaryAmount = [(50 * 10^18) + (50 * 10^6)] * 10^18 / 10^18\\nprimaryAmount = [(50 * 10^18) + (50 * 10^6)] // cancel out the 10^18\\nprimaryAmount = 50 DAI + 0.00000000005 DAI = 50.00000000005 DAI\\n```\\n\\n50 LP Pool tokens should be worth 100 DAI. However, the `TwoTokenPoolUtils._getTimeWeightedPrimaryBalance` function shows that it is only worth 50.00000000005 DAI, which undervalues the LP Pool tokens.\\nIf primary token's decimals (e.g. 6) < secondary token's decimals (e.g. 18)\\nTo illustrate the issue, assume the following:\\nThe leverage vault supports the DAI-USDC Curve Pool, and its primary token of the vault is USDC.\\nUSDC's decimals are 6, while DAI's decimals are 18.\\nCurve Pool's total supply is 100\\nThe Curve Pool holds 100 USDC and 100 DAI\\nFor the sake of simplicity, the price of DAI and USDC is 1:1. Thus, the `oraclePrice` within the function will be `1 * 10^18`. Note that the oracle price is always scaled up to 18 decimals within the vault.\\nThe caller of the `TwoTokenPoolUtils._getTimeWeightedPrimaryBalance` function wanted to compute the total value of 50 LP Pool tokens.\\n```\\nprimaryBalance = poolContext.primaryBalance * poolClaim / totalSupply; // 100 USDC * 50 / 100\\nsecondaryBalance = poolContext.secondaryBalance * poolClaim / totalSupply; // 100 DAI * 50 / 100\\n```\\n\\nThe `primaryBalance` will be `50 USDC`. `50 USDC` denominated in WEI will be `50 * 10^6` since the decimals of USDC are 6.\\nThe `secondaryBalance` will be `50 DAI`. `50 DAI` denominated in WEI will be `50 * 10^18` since the decimals of DAI are 18.\\nNext, the code logic attempts to value the secondary balance (50 DAI) in terms of the primary token (USDC) using the oracle price (1 * 10^18).\\n```\\nsecondaryAmountInPrimary = secondaryBalance * strategyContext.poolClaimPrecision / oraclePrice;\\nsecondaryAmountInPrimary = 50 DAI * 10^18 / (1 * 10^18)\\nsecondaryAmountInPrimary = (50 * 10^18) * 10^18 / (1 * 10^18)\\nsecondaryAmountInPrimary = 50 * 10^18\\n```\\n\\n50 DAI should be worth 50 USDC (50 * 10^6). However, the `secondaryAmountInPrimary` shows that it is worth 50,000,000,000,000 USDC (50 * 10^18).\\n```\\nprimaryAmount = (primaryBalance + secondaryAmountInPrimary) * primaryPrecision / strategyContext.poolClaimPrecision;\\nprimaryAmount = [(50 * 10^6) + (50 * 10^18)] * 10^6 / 10^18\\nprimaryAmount = [(50 * 10^6) + (50 * 10^18)] / 10^12\\nprimaryAmount = 50,000,000.00005 = 50 million\\n```\\n\\n50 LP Pool tokens should be worth 100 USDC. However, the `TwoTokenPoolUtils._getTimeWeightedPrimaryBalance` function shows that it is worth 50 million USDC, which overvalues the LP Pool tokens.\\nIn summary, if a leverage vault has two tokens with different decimals:\\nIf primary token's decimals (e.g. 18) > secondary token's decimals (e.g. 6), then `TwoTokenPoolUtils._getTimeWeightedPrimaryBalance` function will undervalue the LP Pool tokens\\nIf primary token's decimals (e.g. 6) < secondary token's decimals (e.g. 18), then `TwoTokenPoolUtils._getTimeWeightedPrimaryBalance` function will overvalue the LP Pool tokens
When valuing the secondary balance in terms of the primary token using the oracle price, the result should be scaled up or down the decimals of the primary token accordingly if the decimals of the two tokens are different.\\nThe root cause of this issue is in the following portion of the code, which attempts to add the `primaryBalance` and `secondaryAmountInPrimary` before multiplying with the `primaryPrecision`. The `primaryBalance` and `secondaryAmountInPrimary` might not be denominated in the same decimals. Therefore, they cannot be added together without scaling them if the decimals of two tokens are different.\\n```\\nprimaryAmount = (primaryBalance + secondaryAmountInPrimary) * primaryPrecision / strategyContext.poolClaimPrecision;\\n```\\n\\nConsider implementing the following changes to ensure that the math within the `_getTimeWeightedPrimaryBalance` function work with tokens with different decimals. The below approach will scale the secondary token to match the primary token's precision before performing further computation.\\n```\\nfunction _getTimeWeightedPrimaryBalance(\\n TwoTokenPoolContext memory poolContext,\\n StrategyContext memory strategyContext,\\n uint256 poolClaim,\\n uint256 oraclePrice,\\n uint256 spotPrice\\n) internal view returns (uint256 primaryAmount) {\\n // Make sure spot price is within oracleDeviationLimit of pairPrice\\n strategyContext._checkPriceLimit(oraclePrice, spotPrice);\\n \\n // Get shares of primary and secondary balances with the provided poolClaim\\n uint256 totalSupply = poolContext.poolToken.totalSupply();\\n uint256 primaryBalance = poolContext.primaryBalance * poolClaim / totalSupply;\\n uint256 secondaryBalance = poolContext.secondaryBalance * poolClaim / totalSupply;\\n\\n// Add the line below\\n // Scale secondary balance to primaryPrecision\\n// Add the line below\\n uint256 primaryPrecision = 10 ** poolContext.primaryDecimals;\\n// Add the line below\\n uint256 secondaryPrecision = 10 ** poolContext.secondaryDecimals;\\n// Add the line below\\n secondaryBalance = secondaryBalance * primaryPrecision / secondaryPrecision\\n   \\n // Value the secondary balance in terms of the primary token using the oraclePairPrice\\n uint256 secondaryAmountInPrimary = secondaryBalance * strategyContext.poolClaimPrecision / oraclePrice;\\n \\n// Remove the line below\\n // Make sure primaryAmount is reported in primaryPrecision\\n// Remove the line below\\n uint256 primaryPrecision = 10 ** poolContext.primaryDecimals;\\n// Remove the line below\\n primaryAmount = (primaryBalance // Add the line below\\n secondaryAmountInPrimary) * primaryPrecision / strategyContext.poolClaimPrecision;\\n// Add the line below\\n primaryAmount = primaryBalance // Add the line below\\n secondaryAmountInPrimary\\n}\\n```\\n\\nThe `poolContext.primaryBalance` or `poolClaim` are not scaled up to `strategyContext.poolClaimPrecision`. Thus, the `primaryBalance` is not scaled in any form. Thus, I do not see the need to perform any conversion at the last line of the `_getTimeWeightedPrimaryBalance` function.\\n```\\nuint256 primaryBalance = poolContext.primaryBalance * poolClaim / totalSupply;\\n```\\n\\nThe following attempts to run through the examples in the previous section showing that the updated function produces valid results after the changes.\\nIf primary token's decimals (e.g. 18) > secondary token's decimals (e.g. 6)\\n```\\nPrimary Balance = 50 DAI (18 Deci), Secondary Balance = 50 USDC (6 Deci)\\n\\nsecondaryBalance = secondaryBalance * primaryPrecision / secondaryPrecision\\nsecondaryBalance = 50 USDC * 10^18 / 10^6\\nsecondaryBalance = (50 * 10^6) * 10^18 / 10^6 = (50 * 10^18)\\n\\nsecondaryAmountInPrimary = secondaryBalance * strategyContext.poolClaimPrecision / oraclePrice;\\nsecondaryAmountInPrimary = (50 * 10^18) * 10^18 / (1 * 10^18)\\nsecondaryAmountInPrimary = (50 * 10^18) * 10^18 / (1 * 10^18)\\nsecondaryAmountInPrimary = 50 * 10^18\\n\\nprimaryAmount = primaryBalance + secondaryAmountInPrimary\\nprimaryAmount = (50 * 10^18) + (50 * 10^18) = (100 * 10^18) = 100 DAI\\n```\\n\\nIf primary token's decimals (e.g. 6) < secondary token's decimals (e.g. 18)\\n```\\nPrimary Balance = 50 USDC (6 Deci), Secondary Balance = 50 DAI (18 Deci)\\n\\nsecondaryBalance = secondaryBalance * primaryPrecision / secondaryPrecision\\nsecondaryBalance = 50 DAI * 10^6 / 10^18\\nsecondaryBalance = (50 * 10^18) * 10^6 / 10^18 = (50 * 10^6)\\n\\nsecondaryAmountInPrimary = secondaryBalance * strategyContext.poolClaimPrecision / oraclePrice;\\nsecondaryAmountInPrimary = (50 * 10^6) * 10^18 / (1 * 10^18)\\nsecondaryAmountInPrimary = (50 * 10^6) * 10^18 / (1 * 10^18)\\nsecondaryAmountInPrimary = 50 * 10^6\\n\\nprimaryAmount = primaryBalance + secondaryAmountInPrimary\\nprimaryAmount = (50 * 10^6) + (50 * 10^6) = (100 * 10^6) = 100 USDC\\n```\\n\\nIf primary token's decimals (e.g. 6) == secondary token's decimals (e.g. 6)\\n```\\nPrimary Balance = 50 USDC (6 Deci), Secondary Balance = 50 USDT (6 Deci)\\n\\nsecondaryBalance = secondaryBalance * primaryPrecision / secondaryPrecision\\nsecondaryBalance = 50 USDT * 10^6 / 10^6\\nsecondaryBalance = (50 * 10^6) * 10^6 / 10^6 = (50 * 10^6)\\n\\nsecondaryAmountInPrimary = secondaryBalance * strategyContext.poolClaimPrecision / oraclePrice;\\nsecondaryAmountInPrimary = (50 * 10^6) * 10^18 / (1 * 10^18)\\nsecondaryAmountInPrimary = (50 * 10^6) * 10^18 / (1 * 10^18)\\nsecondaryAmountInPrimary = 50 * 10^6\\n\\nprimaryAmount = primaryBalance + secondaryAmountInPrimary\\nprimaryAmount = (50 * 10^6) + (50 * 10^6) = (100 * 10^6) = 100 USDC\\n```\\n\\n`strategyContext.poolClaimPrecision` set to `CurveConstants.CURVE_PRECISION`, which is `1e18`. `oraclePrice` is always in `1e18` precision.
A vault supporting tokens with two different decimals will undervalue or overvalue the LP Pool tokens.\\nThe affected `TwoTokenPoolUtils._getTimeWeightedPrimaryBalance` function is called within the `Curve2TokenPoolUtils._convertStrategyToUnderlying` function that is used for valuing strategy tokens in terms of the primary balance. As a result, the strategy tokens will be overvalued or undervalued\\nFollowing are some of the impacts of this issue:\\nIf the strategy tokens are overvalued or undervalued, the users might be liquidated prematurely or be able to borrow more than they are allowed to since the `Curve2TokenPoolUtils._convertStrategyToUnderlying` function is indirectly used for computing the collateral ratio of an account within Notional's `VaultConfiguration.calculateCollateralRatio` function.\\n`expectedUnderlyingRedeemed` is computed based on the `Curve2TokenPoolUtils._convertStrategyToUnderlying` function. If the `expectedUnderlyingRedeemed` is incorrect, it will break the vault settlement process.
```\\nFile: TwoTokenPoolUtils.sol\\n    function _getTimeWeightedPrimaryBalance(\\n        TwoTokenPoolContext memory poolContext,\\n        StrategyContext memory strategyContext,\\n        uint256 poolClaim,\\n        uint256 oraclePrice,\\n        uint256 spotPrice\\n    ) internal view returns (uint256 primaryAmount) {\\n        // Make sure spot price is within oracleDeviationLimit of pairPrice\\n        strategyContext._checkPriceLimit(oraclePrice, spotPrice);\\n        \\n        // Get shares of primary and secondary balances with the provided poolClaim\\n        uint256 totalSupply = poolContext.poolToken.totalSupply();\\n        uint256 primaryBalance = poolContext.primaryBalance * poolClaim / totalSupply;\\n        uint256 secondaryBalance = poolContext.secondaryBalance * poolClaim / totalSupply;\\n\\n        // Value the secondary balance in terms of the primary token using the oraclePairPrice\\n        uint256 secondaryAmountInPrimary = secondaryBalance * strategyContext.poolClaimPrecision / oraclePrice;\\n\\n        // Make sure primaryAmount is reported in primaryPrecision\\n        uint256 primaryPrecision = 10 ** poolContext.primaryDecimals;\\n        primaryAmount = (primaryBalance + secondaryAmountInPrimary) * primaryPrecision / strategyContext.poolClaimPrecision;\\n    }\\n```\\n