QuantAMM

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

Improper Staleness Threshold Configuration Causing Price Inaccuracy and Transaction Reverts

Summary

Each pool currently uses a single oracleStalenessThreshold for all its assets and the oracles associated with them.

The staleness threshold should reflect the heartbeat of the oracle’s price feed. This information can be found in Chainlink’s list of Ethereum mainnet price feeds by selecting the “Show More Details” option, which reveals the “Heartbeat” column for each feed.

This approach doesn’t work well for assets with varying volatility, such as BTC (volatile) and USD (stable). As a result, stable assets face frequent reverts when the threshold is set too low, while volatile assets may use outdated prices when the threshold is set too high.

Note:

ruleOracleStalenessThreshold has been defined in the UpdateWeightRunner.sol code to map each oracle address to a staleness threshold, but it is not being used anywhere in the code.

Vulnerability Details

1. The user calls the performUpdate() function in UpdateWeightRunner.sol, which triggers the following call chain to get oracle data:

• performUpdate() → _performUpdateAndGetData() → _getUpdatedWeightsAndOracleData() → _getData()

2. In the _getData() function, it calls pool.getOracleStalenessThreshold() and applies this single oracleStalenessThreshold to check the staleness for all oracles in the loop:

https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/UpdateWeightRunner.sol#L354-L361

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];
uint oracleLength = optimisedOracles.length;
uint numAssetOracles;
outputData = new int256[](oracleLength);
uint oracleStalenessThreshold = IQuantAMMWeightedPool(_pool).getOracleStalenessThreshold();
for (uint i; i < oracleLength; ) {
// Asset is base asset
OracleData memory oracleResult;
oracleResult = _getOracleData(OracleWrapper(optimisedOracles[i]));
if (oracleResult.timestamp > block.timestamp - oracleStalenessThreshold) {
outputData[i] = oracleResult.data;

However, this threshold is used to compare the timestamps of all assets and all oracles, regardless of their asset type or individual staleness threshold.

Impact

If the pool’s staleness threshold is set too high, many stale prices will be included in weight calculations, potentially causing losses for users. Conversely, if the threshold is set too low, assets with longer update intervals (higher heartbeat) will often have their prices marked as stale, leading to frequent reverts.

Tools Used

vscode

Recommendations

• Use the ruleOracleStalenessThreshold mapping in the code to allow each oracle to have its own staleness threshold, better matching the volatility and update frequency of the assets.

• Update the _getData() function to account for individual oracle thresholds rather than applying a single global threshold to all oracles in the pool. This would improve accuracy and reduce the risk of stale price data.

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

invalid_oracle_same_threshold_for_assets_in_pool

This is by design, staleness is a strategy aspect: it requires all data to have been updated within n minutes. No more precision needed.

Support

FAQs

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

Give us feedback!