QuantAMM

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

Improper oracle staleness check allows stale prices affecting weights

Summary

The protocol relies on oracle data for prices, which are used for calculating weights at intervals, however, the staleness checks placed inside the UpdateWeightRunner::_getData are flawed and allows stale prices, this would lead to incorret weight calculations and even loss of funds.

Vulnerability Details

The UpdateWeightRunner::performUpdate is called at intervals to update the weights used by the pool.
In order to calculate weights accurately, the oracle data is fetched inside the UpdateWeightRunner::_getUpdatedWeightsAndOracleData function via UpdateWeightRunner::_getData.

function _getUpdatedWeightsAndOracleData(
address _pool,
int256[] memory _currentWeights,
PoolRuleSettings memory _ruleSettings
) private returns (int256[] memory updatedWeights, int256[] memory data) {
data = _getData(_pool, true); <@ - oracle data is collected here
updatedWeights = rules[_pool].CalculateNewWeights(
_currentWeights,
data,
_pool,
_ruleSettings.ruleParameters,
_ruleSettings.lambda,
_ruleSettings.epsilonMax,
_ruleSettings.absoluteWeightGuardRail
);
poolRuleSettings[_pool].timingSettings.lastPoolUpdateRun = uint40(block.timestamp);
}

The way we check for staleness is first query the pool's oracleStaleness threshold in UpdateWeightRunner::_getData

uint oracleStalenessThreshold = IQuantAMMWeightedPool(_pool).getOracleStalenessThreshold();

and then compare to determine the staleness.

(oracleResult.timestamp > block.timestamp - oracleStalenessThreshold)

However, the logic to have a single oracleStalenessThreshold is inherently flawed for Multi hop oracles.
The chainlink oracle, according to the price feed docs updates the prices when either deviation crosses the given % or when the heartbeat threshold is reached.
So, for example in case of a DAI / USD feed on ethereum network the deviation is 0.25% and heartbeat is 3600 seconds.
Similarly, wstETH / USD feed shows deviation of 0.5% and heartbeat of 86400 seconds.
A lot of pairs have a different deviation and heartbeats.

So let's say we want to keep a staleness threshold, we have to consider the heartbeat factor as the UpdateWeightRunner::_getData function deals in timestamps and it's fair to assume it's a scenario where the oracle should have been updated.
The way MultiHopOracle::_getData calculates timestamp is to store and return the minimum timestamp.

function _getData() internal view override returns (int216 data, uint40 timestamp) {
// ... Rest of the code ...
for (uint i = 1; i < oracleLength; ) {
HopConfig memory oracleConfig = oracles[i];
(int216 oracleRes, uint40 oracleTimestamp) = oracleConfig.oracle.getData();
if (oracleTimestamp < timestamp) {
timestamp = oracleTimestamp; <@ - Minimuma timestamp stored
}
// ... Rest of the code ...
}
}

However, it's not possible to accurately determine a staleness threshold as the pairs used in the hops will have different heartbeats.
So let's say there's a pair with lower heartbeat and another pair with a higher heartbeat, now there can be two scenarios in this case:-

  1. Threshold is determined as per the lower heartbeat - However, it might happen that pair with higher heartbeat did not got updated and returned stale value, which would not be caught.

  2. Threshold is determined as per the higher heartbeat - However, the threshold here would fail to consider the staleness of the value of the lower heartbeat pair.

There can be multiple combinations of tokens used for multi hop oracles, and hence, it's inherently not possible to determine a fair threshold and the logic used here is inherently flawed.

Impact

  1. Allows stale prices for certain tokens.

  2. The weights will be calculated by using stale prices and can even lead to loss of funds as the rule uses stale values.

Tools Used

Manual Review

Recommendations

It is recommended to keep staleness check for each token inside the MultiHopOracle contract.

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!