Summary
The protocol applies a threshold for determining stale prices across all tokens in its oracle system, This approach fails to account for the fact that different assets require different stale price thresholds, as a result stale prices might be considered valid, or a valid price will be considered stale while it is not, leading to inaccurate valuation of collateral and other assets.
Vulnerability Details
File: https://github.com/Cyfrin/2024-12-quantamm/blob/main/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L712
The protocol's oracle system currently uses a fixed stale price threshold for example in test: 3600 seconds / 1 hour
for all tokens.
However, this approach doesn't work well for tokens that need shorter thresholds to maintain accurate price data.
For example:
For tokens like LINK and AVAX, the system will incorrectly accept stale prices, which could lead to valuation issues.
On the other hand, for tokens like XRP, the _getData function will revert because it accepts only prices within a 1-hour range.
function getQuantAMMWeightedPoolImmutableData()
external view returns(QuantAMMWeightedPoolImmutableData memory data)
{
data.tokens = _vault.getPoolTokens(address(this));
|>> data.oracleStalenessThreshold = oracleStalenessThreshold;
data.poolRegistry = poolRegistry;
data.ruleParameters = ruleParameters;
data.lambda = lambda;
data.epsilonMax = epsilonMax;
data.absoluteWeightGuardRail = absoluteWeightGuardRail;
data.maxTradeSizeRatio = maxTradeSizeRatio;
data.updateInterval = updateInterval;
}
As we see once oracleStalenessThreshold is set no way to change it.
Impact
The system can accept a stale prices if token threshold is less than 1 hour or reject fresh price if token threshold is 24 hours.
Recommendations
Add this in: https://github.com/Cyfrin/2024-12-quantamm/blob/main/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol
+ mapping(address => uint256) private tokenStalePriceThresholds;
// Function to get stale price threshold for a specific token
+ function getStalePriceThreshold(address token) public view returns(uint256) {
+ return tokenStalePriceThresholds[token];
+ }
Add this in: https://github.com/Cyfrin/2024-12-quantamm/blob/main/pkg/pool-quantamm/contracts/UpdateWeightRunner.sol#L347-L391
// @notice Get the data for a pool from the oracles and return it in the same order as the assets in the pool
// @param _pool Pool to get data for
// @param internalCall Internal call flag to detect if the function was called internally for emission and permissions
function _getData(address _pool, bool internalCall) private view returns(int256[] memory outputData) {
require(internalCall || (approvedPoolActions[_pool] & MASK_POOL_GET_DATA > 0), "Not allowed to get data");
//optimised == happy path, optimised into a different array to save gas
address[] memory optimisedOracles = poolOracles[_pool];
uint256 oracleLength = optimisedOracles.length;
uint256 numAssetOracles;
outputData = new int256[](oracleLength);
+ uint256 oracleStalenessThreshold;
- uint256 oracleStalenessThreshold = IQuantAMMWeightedPool(_pool).getOracleStalenessThreshold();
for (uint256 i; i < oracleLength; ) {
// Asset is base asset
OracleData memory oracleResult;
oracleResult = _getOracleData(OracleWrapper(optimisedOracles[i]));
+ oracleStalenessThreshold = IQuantAMMWeightedPool(_pool).getStalePriceThreshold(optimisedOracles[i]);
if (oracleResult.timestamp > block.timestamp - oracleStalenessThreshold) {
outputData[i] = oracleResult.data;
} else {
// Snip code.
}
unchecked {
++i;
}
}
}