QuantAMM

QuantAMM
49,600 OP
View results
Submission Details
Severity: low
Invalid

Stale oracle prices impact fee calculations in `UpliftOnlyExample` contract

Summary

This issue is similar to issue #M04 of the Cyfrin report, but it not only impacts the calculation of pool weight updates but also affects fee calculations in the UpliftOnlyExample contract.

Vulnerability Details

Detail from the Cyfrin audit report:

The UpdateWeightRunner::_getData function performs staleness checks on oracle prices to ensure price data is fresh. However, when the optimized (primary) oracle returns stale data and there are no backup oracles configured, the function silently accepts and returns the stale price data instead of reverting.

This occurs because the logic checks for staleness on the primary oracle, but if stale, it moves to a backup oracle check section. When no backup oracles exist, this section is skipped due to array length checks, and the function proceeds to use the stale data from the primary oracle.

function _getData(address _pool, bool internalCall) private view returns (int256[] memory outputData) {
...
outputData = new int256[](oracleLength);
uint oracleStalenessThreshold = IQuantAMMWeightedPool(_pool).getOracleStalenessThreshold();
for (uint i; i < oracleLength; ) {
OracleData memory oracleResult;
oracleResult = _getOracleData(OracleWrapper(optimisedOracles[i]));
if (oracleResult.timestamp > block.timestamp - oracleStalenessThreshold) {
outputData[i] = oracleResult.data;
} else {
unchecked {
numAssetOracles = poolBackupOracles[_pool][i].length;
}
// `for` loop is skipped when no backup oracle
for (uint j = 1 ; j < numAssetOracles; ) {
oracleResult = _getOracleData(
OracleWrapper(poolBackupOracles[_pool][i][j])
);
if (oracleResult.timestamp > block.timestamp - oracleStalenessThreshold) {
break;
} else if (j == numAssetOracles - 1) {
revert("No fresh oracle values available");
}
unchecked {
++j;
}
}
// @audit BUG - here it accepts the same stale data that was rejected before
outputData[i] = oracleResult.data;
}
unchecked {
++i;
}
}
}

Impact

The UpliftOnlyExample contract retrieves price data by calling the UpdateWeightRunner::getData function to calculate the notional value of the LP token in USD. This value is then used to calculate fees when users remove liquidity. Therefore, stale oracle prices could lead to incorrect fee calculations.

Recommendations

Consider reverting in the else block when numAssetOracles == 1:

function _getData(address _pool, bool internalCall) private view returns (int256[] memory outputData) {
...
outputData = new int256[](oracleLength);
uint oracleStalenessThreshold = IQuantAMMWeightedPool(_pool).getOracleStalenessThreshold();
for (uint i; i < oracleLength; ) {
OracleData memory oracleResult;
oracleResult = _getOracleData(OracleWrapper(optimisedOracles[i]));
if (oracleResult.timestamp > block.timestamp - oracleStalenessThreshold) {
outputData[i] = oracleResult.data;
} else {
unchecked {
numAssetOracles = poolBackupOracles[_pool][i].length;
}
+ if (numAssetOracles == 1) {
+ revert("No fresh oracle values available");
+ }
for (uint j = 1 ; j < numAssetOracles; ) {
...
}
outputData[i] = oracleResult.data;
}
unchecked {
++i;
}
}
}
Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

invalid_stale_price_when_no_backup_oracles_set

Cyfrin audit: 7.2.4 Stale Oracle prices accepted when no backup oracles available

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!