QuantAMM

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

Oracle Staleness Threshold Design in QUANTAMM Pools Fails to Account for Per-Oracle Variances

Summary

The QUANTAMM protocol implements a single oracle staleness threshold per pool rather than per oracle. This design choice fails to account for the varying update frequencies (heartbeats) of different oracles across different chains and assets, potentially leading to either premature staleness flags for frequently updated oracles or delayed staleness detection for infrequently updated ones.

Vulnerability Details

The issue stems from the protocol's implementation of oracle staleness threshold at the pool level rather than the oracle level:

struct QuantAMMWeightedPoolImmutableData {
IERC20[] tokens;
uint oracleStalenessThreshold; // Single threshold for all oracles in pool
// ...
}

The threshold is used uniformly for all oracles in UpdateWeightRunner._getData():
UpdateWeightRunner.sol#L360-L361

function _getData(address _pool, bool internalCall) private view returns (int256[] memory outputData) {
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;
}
// ...
}
}

Pprotocol supports different EVM chains(OP Mainnet, Arbitrum, Ethereum Mainnet and Base), let's consider the ETH/USD oracles on different chains:

  • Ethereum Mainnet:
    ETH/USD: ~1 hour heartbeat

  • Optimism:
    ETH/USD: ~20 minutes heartbeat

  • Arbitrum:
    ETH/USD: ~24 hours heartbeat

Even on the same chain, different tokens have varying heartbeats—the intervals at which their price data is updated on-chain. For example:

  • The DAI/USD oracle on Ethereum updates approximately every 1 hour.

  • The USDT/USD oracle on Ethereum has a significantly longer heartbeat, around 24 hours.

This variation in heartbeat frequencies creates a significant challenge:

  • If the pool's staleness threshold is set too low (e.g., 1 hour), oracles with longer heartbeats (like USDT/USD) may frequently and erroneously be marked as stale.

  • Conversely, if the threshold is set too high (e.g., 24 hours), it undermines the value of more frequent updates for oracles with shorter heartbeats (like DAI/USD).

This discrepancy highlights the need for staleness thresholds tailored to each oracle rather than a uniform value across the pool.

Impact

Inconsistent Price Validation: The uniform threshold may incorrectly flag prices as stale or valid, leading to:

  • False positives: Marking valid prices as stale for oracles with longer heartbeats

  • False negatives: Accepting outdated prices for oracles with shorter heartbeats

Tools Used

Manual Review

Recommendations

  1. Implement per-oracle staleness thresholds:

struct OracleConfig {
address oracle;
uint256 stalenessThreshold;
}
struct QuantAMMWeightedPoolImmutableData {
IERC20[] tokens;
OracleConfig[] oracleConfigs; // Per-oracle configuration
// ...
}
  1. Update the staleness check logic:

function _getData(address _pool, bool internalCall) private view returns (int256[] memory outputData) {
OracleConfig[] memory configs = IQuantAMMWeightedPool(_pool).getOracleConfigs();
for (uint i; i < oracleLength; ) {
OracleData memory oracleResult = _getOracleData(OracleWrapper(optimisedOracles[i]));
if (oracleResult.timestamp > block.timestamp - configs[i].stalenessThreshold) {
outputData[i] = oracleResult.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.