Vulnerability: Two token vault will be broken if it comprises tokens with different decimals
Severity: high

Description:
A two token vault that comprises tokens with different decimals will have many of its key functions broken. For instance, rewards cannot be reinvested and vault cannot be settled.\\nThe `Stable2TokenOracleMath._getSpotPrice` function is used to compute the spot price of two tokens.\\n```\\nFile: Stable2TokenOracleMath.sol\\nlibrary Stable2TokenOracleMath {\\n    using TypeConvert for int256;\\n    using Stable2TokenOracleMath for StableOracleContext;\\n\\n    function _getSpotPrice(\\n        StableOracleContext memory oracleContext, \\n        TwoTokenPoolContext memory poolContext, \\n        uint256 primaryBalance,\\n        uint256 secondaryBalance,\\n        uint256 tokenIndex\\n    ) internal view returns (uint256 spotPrice) {\\n        require(tokenIndex < 2); /// @dev invalid token index\\n\\n        /// Apply scale factors\\n        uint256 scaledPrimaryBalance = primaryBalance * poolContext.primaryScaleFactor \\n            / BalancerConstants.BALANCER_PRECISION;\\n        uint256 scaledSecondaryBalance = secondaryBalance * poolContext.secondaryScaleFactor \\n            / BalancerConstants.BALANCER_PRECISION;\\n\\n        /// @notice poolContext balances are always in BALANCER_PRECISION (1e18)\\n        (uint256 balanceX, uint256 balanceY) = tokenIndex == 0 ?\\n            (scaledPrimaryBalance, scaledSecondaryBalance) :\\n            (scaledSecondaryBalance, scaledPrimaryBalance);\\n\\n        uint256 invariant = StableMath._calculateInvariant(\\n            oracleContext.ampParam, StableMath._balances(balanceX, balanceY), true // round up\\n        );\\n\\n        spotPrice = StableMath._calcSpotPrice({\\n            amplificationParameter: oracleContext.ampParam,\\n            invariant: invariant,\\n            balanceX: balanceX, \\n            balanceY: balanceY\\n        });\\n\\n        /// Apply secondary scale factor in reverse\\n        uint256 scaleFactor = tokenIndex == 0 ?\\n            poolContext.secondaryScaleFactor * BalancerConstants.BALANCER_PRECISION / poolContext.primaryScaleFactor :\\n            poolContext.primaryScaleFactor * BalancerConstants.BALANCER_PRECISION / poolContext.secondaryScaleFactor;\\n        spotPrice = spotPrice * BalancerConstants.BALANCER_PRECISION / scaleFactor;\\n    }\\n```\\n\\nTwo tokens (USDC and DAI) with different decimals will be used below to illustrate the issue:\\nUSDC/DAI Spot Price\\nAssume that the primary token is DAI (18 decimals) and the secondary token is USDC (6 decimals). As such, the scaling factors would be as follows. The token rate is ignored and set to 1 for simplicity.\\nPrimary Token (DAI)'s scaling factor = 1e18\\n`scaling factor = FixedPoint.ONE (1e18) * decimals difference to reach 18 decimals (1e0) * token rate (1)\\nscaling factor = 1e18`\\nSecondary Token (USDC)'s scaling factor = 1e30\\n`scaling factor = FixedPoint.ONE (1e18) * decimals difference to reach 18 decimals (1e12) * token rate (1)\\nscaling factor = 1e18 * 1e12 = 1e30`\\nAssume that the `primaryBalance` is 100 DAI (100e18), and the `secondaryBalance` is 100 USDC (100e6). Line 25 - 28 of the `_getSpotPrice` function will normalize the tokens balances to 18 decimals as follows:\\n`scaledPrimaryBalance` will be 100e18 (It remains the same as no scaling is needed because DAI is already denominated in 18 decimals)\\n`scaledPrimaryBalance = primaryBalance * poolContext.primaryScaleFactor / BalancerConstants.BALANCER_PRECISION;\\n`scaledPrimaryBalance` = 100e18 * 1e18 / 1e18\\n`scaledPrimaryBalance` = 100e18`\\n`scaledSecondaryBalance` will upscale to 100e18\\n`scaledSecondaryBalance` = `scaledSecondaryBalance` * poolContext.primaryScaleFactor / BalancerConstants.BALANCER_PRECISION;\\n`scaledSecondaryBalance` = 100e6 * 1e30 / 1e18\\n`scaledSecondaryBalance` = 100e18\\nThe `StableMath._calcSpotPrice` function at Line 39 returns the spot price of Y/X. In this example, `balanceX` is DAI, and `balanceY` is USDC. Thus, the spot price will be USDC/DAI. This means the amount of USDC I will get for each DAI.\\nWithin Balancer, all stable math calculations within the Balancer's pools are performed in `1e18`. With both the primary and secondary balances normalized to 18 decimals, they can be safely passed to the `StableMath._calculateInvariant` and `StableMath._calcSpotPrice` functions to compute the spot price. Assuming that the price of USDC and DAI is perfectly symmetric (1 DAI can be exchanged for exactly 1 USDC, and vice versa), the spot price returned from the `StableMath._calcSpotPrice` will be `1e18`. Note that the spot price returned by the `StableMath._calcSpotPrice` function will be denominated in 18 decimals.\\nIn Line 47-50 within the `Stable2TokenOracleMath._getSpotPrice` function, it attempts to downscale the spot price to normalize it back to the original decimals and token rate (e.g. stETH back to wstETH) of the token.\\nThe `scaleFactor` at Line 47 will be evaluated as follows:\\n```\\nscaleFactor = poolContext.secondaryScaleFactor * BalancerConstants.BALANCER_PRECISION / poolContext.primaryScaleFactor\\nscaleFactor = 1e30 * 1e18 / 1e18\\nscaleFactor = 1e30\\n```\\n\\nFinally, the spot price will be scaled in reverse order and it will be evaluated to `1e6` as shown below:\\n```\\nspotPrice = spotPrice * BalancerConstants.BALANCER_PRECISION / scaleFactor;\\nspotPrice = 1e18 * 1e18 / 1e30\\nspotPrice = 1e6\\n```\\n\\nDAI/USDC Spot Price\\nIf it is the opposite where the primary token is USDC (6 decimals) and the secondary token is DAI (18 decimals), the calculation of the spot price will be as follows:\\nThe `scaleFactor` at Line 47 will be evaluated to as follows:\\n```\\nscaleFactor = poolContext.secondaryScaleFactor * BalancerConstants.BALANCER_PRECISION / poolContext.primaryScaleFactor\\nscaleFactor = 1e18 * 1e18 / 1e30\\nscaleFactor = 1e6\\n```\\n\\nFinally, the spot price will be scaled in reverse order and it will be evaluated to `1e30` as shown below:\\n```\\nspotPrice = spotPrice * BalancerConstants.BALANCER_PRECISION / scaleFactor;\\nspotPrice = 1e18 * 1e18 / 1e6\\nspotPrice = 1e30\\n```\\n\\nNote about the spot price\\nAssuming that the spot price of USDC and DAI is 1:1. As shown above, if the decimals of two tokens are not the same, the final spot price will end up either 1e6 (USDC/DAI) or 1e30 (DAI/USDC). However, if the decimals of two tokens (e.g. wstETH and WETH) are the same, this issue stays hidden as the `scaleFactor` in Line 47 will always be 1e18 as both `secondaryScaleFactor` and `primaryScaleFactor` cancel out each other.\\nIt was observed that the spot price returned from the `Stable2TokenOracleMath._getSpotPrice` function is being compared with the oracle price from the `TwoTokenPoolUtils._getOraclePairPrice` function to determine if the pool has been manipulated within many functions.\\n```\\nuint256 oraclePrice = poolContext._getOraclePairPrice(strategyContext.tradingModule);\\n```\\n\\nBased on the implementation of the `TwoTokenPoolUtils._getOraclePairPrice` function , the `oraclePrice` returned by this function is always denominated in 18 decimals regardless of the decimals of the underlying tokens. For instance, assume the spot price of USDC (6 decimals) and DAI (18 decimals) is 1:1. The spot price returned by this oracle function for USDC/DAI will be `1e18` and DAI/USDC will be `1e18`.\\nIn many functions, the spot price returned from the `Stable2TokenOracleMath._getSpotPrice` function is compared with the oracle price via the `Stable2TokenOracleMath._checkPriceLimit`. Following is one such example. The `oraclePrice` will be `1e18`, while the `spotPrice` will be either `1e6` or `1e30` in our example. This will cause the `_checkPriceLimit` to always revert because of the large discrepancy between the two prices.\\n```\\nFile: Stable2TokenOracleMath.sol\\n    function _getMinExitAmounts(\\n        StableOracleContext calldata oracleContext,\\n        TwoTokenPoolContext calldata poolContext,\\n        StrategyContext calldata strategyContext,\\n        uint256 oraclePrice,\\n        uint256 bptAmount\\n    ) internal view returns (uint256 minPrimary, uint256 minSecondary) {\\n        // Oracle price is always specified in terms of primary, so tokenIndex == 0 for primary\\n        // Validate the spot price to make sure the pool is not being manipulated\\n        uint256 spotPrice = _getSpotPrice({\\n            oracleContext: oracleContext,\\n            poolContext: poolContext,\\n            primaryBalance: poolContext.primaryBalance,\\n            secondaryBalance: poolContext.secondaryBalance,\\n            tokenIndex: 0\\n        });\\n        _checkPriceLimit(strategyContext, oraclePrice, spotPrice);\\n```\\n\\nOther affected functions include the following:\\nStable2TokenOracleMath._validateSpotPriceAndPairPrice\\nStable2TokenOracleMath._getTimeWeightedPrimaryBalance

Impact:
A vault supporting tokens with two different decimals will have many of its key functions will be broken as the `_checkPriceLimit` will always revert. For instance, rewards cannot be reinvested and vaults cannot be settled since they rely on the `_checkPriceLimit` function.\\nIf the reward cannot be reinvested, the strategy tokens held by the users will not appreciate. If the vault cannot be settled, the vault debt cannot be repaid to Notional and the gain cannot be realized. Loss of assets for both users and Notional

Recommended mitigation:
Issue Two token vault will be broken if it comprises tokens with different decimals\\nWithin the `Stable2TokenOracleMath._getSpotPrice`, normalize the spot price back to 1e18 before returning the result. This ensures that it can be compared with the oracle price, which is denominated in 1e18 precision.\\nThis has been implemented in the spot price function (Boosted3TokenPoolUtils._getSpotPriceWithInvariant) of another pool (Boosted3Token). However, it was not consistently applied in `TwoTokenPool`.