QuantAMM

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

Protocol uses same Oracle Staleness threshold for all the tokens causing problems in calculation of prices

Summary

Different tokens have different Oracle Staleness Threshold/Heartbeat as can be seen from the docs of Oracles . Using the same Oracle staleness check for different tokens in a pool will cause unnecessary reverts and stale datas .

Vulnerability Details

oracleStalenessThreshold state variable in QuantAMMWeightedPool is Maximum amount of seconds that can pass between two oracle updates . The problem is , there is only one oracleStalenessThreshold defined for a single pool .

https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPool.sol#L712

oracleStalenessThreshold = _oracleStalenessThreshold;

That means all the assets in a QuantAMMWeightedPool will be checked against same oracleStalenessThreshold . So whenever updates happen , data from the Oracle is checked against the same staleness threshold in UpdateWeightRunner as can be seen here :
https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/UpdateWeightRunner.sol#L347-L374

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;
} else {
unchecked {
numAssetOracles = poolBackupOracles[_pool][i].length;
}
for (uint j = 1 /*0 already done via optimised poolOracles*/; j < numAssetOracles; ) {
oracleResult = _getOracleData(
// poolBackupOracles[_pool][asset][oracle]
OracleWrapper(poolBackupOracles[_pool][i][j])
);
if (oracleResult.timestamp > block.timestamp - oracleStalenessThreshold) {
// Oracle has fresh values
break;

Let's see an example to demonstrate this problem , In Chainlink we can see different price feeds of tokens have different Heartbeats(or maximum amount of time that is allowed to pass before the data is updated) , for eg :

In Eth Mainnet -

  1. LINK/USD in Mainnet has heartbeat of 3600s

  2. BAL/USD in Mainnet has heartbeat of 86400s

So if one sets oracleStalenessThreshold in QuantAMMWeightedPool as 3600s , then BAL/USD price derivation would report as stale price for a long time and cause reverts , even though it is giving the most updated data and if oracleStalenessThreshold is set to 86400s, then LINK/USD will pass even if it is returning stale data .

Impact

Leads to incorrect price derivation for assets in the pool.

Tools Used

Prev Report = https://solodit.cyfrin.io/issues/m-6-the-redstonecoreoracle-has-a-constant-stale-price-threshold-this-is-dangerous-to-use-with-tokens-that-have-a-smaller-threshold-as-the-oracle-will-report-stale-prices-as-valid-sherlock-sentiment-v2-git

https://solodit.cyfrin.io/issues/m-1-using-the-same-heartbeat-for-multiple-price-feeds-causing-dos-sherlock-zerolend-one-git

Chainlink Docs = https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1&search=BAL

Recommendations

Set different oracle staleness check for different tokens to avoid price derivation problems i.e., use something like ruleOracleStalenessThreshold variable of UpdateWeightRunner which gives different thresholds for each oracle address :
https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/UpdateWeightRunner.sol#L190-L191

/// @notice mapping keyed of oracle address to staleness threshold in seconds. Created for gas efficincy.
mapping(address => uint) public ruleOracleStalenessThreshold;
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.