Rely On Balancer Oracle Which Is Not Updated Frequently
The vault relies on Balancer Oracle which is not updated frequently.\\nNote: This issue affects the MetaStable2 balancer leverage vault\\nThe issue is that this pool only handled ~1.5 transactions per day based on the last 5 days' data. In terms of average, the price will only be updated once every 16 hours. There are also many days that there is only 1 transaction. The following shows the number of transactions for each day within the audit period.\\n5 Oct 2022 - 3 transactions\\n4 Oct 2022 - 1 transaction\\n3 Oct 2022 - 1 transaction\\n2 Oct 2022 - 2 transactions\\n1 Oct 2022 - 1 transaction\\nNote that the price will only be updated whenever a transaction (e.g. swap) within the Balancer pool is triggered. Due to the lack of updates, the price provided by Balancer Oracle will not reflect the true value of the assets. Considering the stETH/ETH Balancer pool, the price of the stETH or ETH provided will not reflect the true value in the market.\\n```\\nFile: TwoTokenPoolUtils.sol\\n    /// @notice Gets the oracle price pair price between two tokens using a weighted\\n    /// average between a chainlink oracle and the balancer TWAP oracle.\\n    /// @param poolContext oracle context variables\\n    /// @param oracleContext oracle context variables\\n    /// @param tradingModule address of the trading module\\n    /// @return oraclePairPrice oracle price for the pair in 18 decimals\\n    function _getOraclePairPrice(\\n        TwoTokenPoolContext memory poolContext,\\n        OracleContext memory oracleContext, \\n        ITradingModule tradingModule\\n    ) internal view returns (uint256 oraclePairPrice) {\\n        // NOTE: this balancer price is denominated in 18 decimal places\\n        uint256 balancerWeightedPrice;\\n        if (oracleContext.balancerOracleWeight > 0) {\\n            uint256 balancerPrice = BalancerUtils._getTimeWeightedOraclePrice(\\n                address(poolContext.basePool.pool),\\n                IPriceOracle.Variable.PAIR_PRICE,\\n                oracleContext.oracleWindowInSeconds\\n            );\\n\\n            if (poolContext.primaryIndex == 1) {\\n                // If the primary index is the second token, we need to invert\\n                // the balancer price.\\n                balancerPrice = BalancerConstants.BALANCER_PRECISION_SQUARED / balancerPrice;\\n            }\\n\\n            balancerWeightedPrice = balancerPrice * oracleContext.balancerOracleWeight;\\n        }\\n\\n        uint256 chainlinkWeightedPrice;\\n        if (oracleContext.balancerOracleWeight < BalancerConstants.BALANCER_ORACLE_WEIGHT_PRECISION) {\\n            (int256 rate, int256 decimals) = tradingModule.getOraclePrice(\\n                poolContext.primaryToken, poolContext.secondaryToken\\n            );\\n            require(rate > 0);\\n            require(decimals >= 0);\\n\\n            if (uint256(decimals) != BalancerConstants.BALANCER_PRECISION) {\\n                rate = (rate * int256(BalancerConstants.BALANCER_PRECISION)) / decimals;\\n            }\\n\\n            // No overflow in rate conversion, checked above\\n            chainlinkWeightedPrice = uint256(rate) * \\n                (BalancerConstants.BALANCER_ORACLE_WEIGHT_PRECISION - oracleContext.balancerOracleWeight);\\n        }\\n\\n        oraclePairPrice = (balancerWeightedPrice + chainlinkWeightedPrice) / \\n            BalancerConstants.BALANCER_ORACLE_WEIGHT_PRECISION;\\n    }\\n```\\n\\n```\\nFile: BalancerUtils.sol\\n    function _getTimeWeightedOraclePrice(\\n        address pool,\\n        IPriceOracle.Variable variable,\\n        uint256 secs\\n    ) internal view returns (uint256) {\\n        IPriceOracle.OracleAverageQuery[]\\n            memory queries = new IPriceOracle.OracleAverageQuery[](1);\\n\\n        queries[0].variable = variable;\\n        queries[0].secs = secs;\\n        queries[0].ago = 0; // now\\n\\n        // Gets the balancer time weighted average price denominated in the first token\\n        return IPriceOracle(pool).getTimeWeightedAverage(queries)[0];\\n    }\\n```\\n
Although it is not possible to obtain a price pair that truly reflects the true value of an asset in the real world, the vault should attempt to minimize inaccuracy and slippage as much as possible. This can be done by choosing and using a more accurate Oracle that is updated more frequently instead of using the Balancer Oracle that is infrequently updated.\\nChainlink should be used as the primary Oracle for price pair. If a secondary Oracle is needed for a price pair, consider using Teller Oracle instead of Balancer Oracle. Some example of how Chainlink and Tellor works together in a live protocol can be found here\\nObtaining the time-weight average price of BTP LP token from Balancer Oracle is fine as the Balancer pool is the source of truth. However, getting the price of ETH or stETH from Balancer Oracle would not be a good option.\\nOn a side note, it was observed that the weightage of the price pair is Balancer Oracle - 60% and Chainlink - 40%. Thus, this theoretically will reduce the impact of inaccurate prices provided by Balancer Oracle by around half. However, the team should still consider using a better Oracle as almost all the functions within the vault depends on the accurate price of underlying assets to operate.\\nNote: For the stETH/ETH balancer leverage vault, the price pair is computed based on a weighted average of Balancer Oracle and Chainlink. Based on the test script, the weightage is Balancer Oracle - 60% and Chainlink - 40%.\\n```\\nFile: BalancerEnvironment.py\\n            "maxRewardTradeSlippageLimitPercent": 5e6,\\n            "balancerOracleWeight": 0.6e4, # 60%\\n            "settlementCoolDownInMinutes": 60 * 6, # 6 hour settlement cooldown\\n```\\n
The price provided by the function will not reflect the true value of the assets. It might be overvalued or undervalued. The affected function is being used in almost all functions within the vault. For instance, this function is part of the critical `_convertStrategyToUnderlying` function that computes the value of the strategy token in terms of its underlying assets. As a result, it might cause the following:\\nVault Settlement - Vault settlement requires computing the underlying value of the strategy tokens. It involves dealing with a large number of assets, and thus even a slight slippage in the price will be significantly amplified.\\nDeleverage/Liquidation of Account - If the price provided does not reflect the true value, users whose debt ratio is close to the liquidation threshold might be pre-maturely deleveraged/liquidated since their total asset value might be undervalued.\\nBorrowing - If the price provided does not reflect the true value, it might be possible that the assets of some users might be overvalued, and thus they are able to over-borrow from the vault.
```\\nFile: TwoTokenPoolUtils.sol\\n    /// @notice Gets the oracle price pair price between two tokens using a weighted\\n    /// average between a chainlink oracle and the balancer TWAP oracle.\\n    /// @param poolContext oracle context variables\\n    /// @param oracleContext oracle context variables\\n    /// @param tradingModule address of the trading module\\n    /// @return oraclePairPrice oracle price for the pair in 18 decimals\\n    function _getOraclePairPrice(\\n        TwoTokenPoolContext memory poolContext,\\n        OracleContext memory oracleContext, \\n        ITradingModule tradingModule\\n    ) internal view returns (uint256 oraclePairPrice) {\\n        // NOTE: this balancer price is denominated in 18 decimal places\\n        uint256 balancerWeightedPrice;\\n        if (oracleContext.balancerOracleWeight > 0) {\\n            uint256 balancerPrice = BalancerUtils._getTimeWeightedOraclePrice(\\n                address(poolContext.basePool.pool),\\n                IPriceOracle.Variable.PAIR_PRICE,\\n                oracleContext.oracleWindowInSeconds\\n            );\\n\\n            if (poolContext.primaryIndex == 1) {\\n                // If the primary index is the second token, we need to invert\\n                // the balancer price.\\n                balancerPrice = BalancerConstants.BALANCER_PRECISION_SQUARED / balancerPrice;\\n            }\\n\\n            balancerWeightedPrice = balancerPrice * oracleContext.balancerOracleWeight;\\n        }\\n\\n        uint256 chainlinkWeightedPrice;\\n        if (oracleContext.balancerOracleWeight < BalancerConstants.BALANCER_ORACLE_WEIGHT_PRECISION) {\\n            (int256 rate, int256 decimals) = tradingModule.getOraclePrice(\\n                poolContext.primaryToken, poolContext.secondaryToken\\n            );\\n            require(rate > 0);\\n            require(decimals >= 0);\\n\\n            if (uint256(decimals) != BalancerConstants.BALANCER_PRECISION) {\\n                rate = (rate * int256(BalancerConstants.BALANCER_PRECISION)) / decimals;\\n            }\\n\\n            // No overflow in rate conversion, checked above\\n            chainlinkWeightedPrice = uint256(rate) * \\n                (BalancerConstants.BALANCER_ORACLE_WEIGHT_PRECISION - oracleContext.balancerOracleWeight);\\n        }\\n\\n        oraclePairPrice = (balancerWeightedPrice + chainlinkWeightedPrice) / \\n            BalancerConstants.BALANCER_ORACLE_WEIGHT_PRECISION;\\n    }\\n```\\n