QuantAMM

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

Weights update fails due to oracle stale threshold one second shortage. Oversight causes impermanent loss and Arbitrage Risks.

Summary

In the UpdateWeightRunner contract, there's a function performUpdate function which calls _getData private view (staticCall) function to update weights. The _getData function has oracle data staleness threshold checks implementations to ensure that oracle data is fresh and ready to use. However, those checks has an oversight which makes protocol inefficient and buggy. The issue is a tiny mis-comparison of staleness threshold, Comparison missed one second of oracle staleness threshold which causes potential issues.

Vulnerability Details

If the weight update fails due to a one-second shortage in the oracle stale threshold, it could lead to several potential issues, particularly in a protocol like QuantAMM that depends on timely and accurate weight updates:

UpdateWeightRunner::_getData:

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];
uint256 oracleLength = optimisedOracles.length;
uint256 numAssetOracles;
outputData = new int256[](oracleLength);
uint256 oracleStalenessThreshold = IQuantAMMWeightedPool(_pool).getOracleStalenessThreshold();
for (uint256 i; i < oracleLength;) {
// Asset is base asset
OracleData memory oracleResult;
oracleResult = _getOracleData(OracleWrapper(optimisedOracles[i]));
@> // @info: should be >= not >
@> if (oracleResult.timestamp > block.timestamp - oracleStalenessThreshold) {
outputData[i] = oracleResult.data;
} else {
unchecked {
numAssetOracles = poolBackupOracles[_pool][i].length;
}
for (uint256 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;
} else if (j == numAssetOracles - 1) {
// All oracle results for this data point are stale. Should rarely happen in practice with proper backup oracles.
revert("No fresh oracle values available");
}
unchecked {
++j;
}
}
outputData[i] = oracleResult.data;
}
unchecked {
++i;
}
}
}

As we can see clearly that, if check has a flaw of in comparison, the comparison operator > used instead of >= due to which there's comes a shortage of one-second.

PoC

Let's see a scenario...

  • let's say, the oracleStalenessThreshold is 1 hours

  • oracle data last updated at 10 AM

  • The current timestamp is of 11 AM

  • According to the if check condition: oracleResult.timestamp > block.timestamp - oracleStalenessThreshold

  • 10 AM > 11 AM - 1 hours

  • 10 AM > 10 AM = False

  • revert: No fresh oracle values available

Impact

  1. Missed Opportunity for Optimal Rebalancing

    • Effect: Delays in updating weights may prevent the pool from adjusting to market conditions effectively. If the weights don't reflect recent price movements, the pool could:

      • Underperform compared to an optimally rebalanced pool.

      • Expose LPs to increased impermanent loss.

    • Example: If the price of an asset spikes, failing to update weights in time means the pool doesn't capitalize on the opportunity to rebalance and mitigate risk.

  2. Economic Arbitrage Risks

    • Effect: Stale weights could misprice assets within the pool, allowing arbitrageurs to exploit the difference between pool prices and the market price because the update was recently failed.

    • Example: If an oracle is stale for even a second, arbitrageurs might profit by buying underpriced assets or selling overpriced ones, leading to a loss of value for LPs.

  3. Weight Drift

    • Effect: The pool's weights might drift significantly from the intended trajectory, resulting in misaligned portfolio composition.

    • Impact: This can reduce the pool's effectiveness in achieving diversification and expose LPs to unintended risk concentrations.

  4. Potential Protocol Downtime

    • Effect: If weight updates consistently fail due to oracle data issues, the pool might enter a semi-stalled state where weights remain static. This reduces trust in the protocol and limits its functionality.

    • Example: Users might be unable to make informed decisions about depositing, withdrawing, or trading due to uncertainty in the pool's performance.

  5. Compounded Delays

    • Effect: A small initial delay might cascade into larger timing issues. Weight updates are typically scheduled periodically, and missing one window might mean waiting for the next.

    • Impact: This could compound losses or inefficiencies over time

Tools Used

  1. Manual Review

  2. Phind AI Buddy (Asked its severity)

Recommendations

The Mitigation is so simple. Update the if checks and replace > operator with >= operator as we did below:

UpdateWeightRunner::_getData:

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];
uint256 oracleLength = optimisedOracles.length;
uint256 numAssetOracles;
outputData = new int256[](oracleLength);
uint256 oracleStalenessThreshold = IQuantAMMWeightedPool(_pool).getOracleStalenessThreshold();
for (uint256 i; i < oracleLength;) {
// Asset is base asset
OracleData memory oracleResult;
oracleResult = _getOracleData(OracleWrapper(optimisedOracles[i]));
- if (oracleResult.timestamp > block.timestamp - oracleStalenessThreshold) {
+ if (oracleResult.timestamp >= block.timestamp - oracleStalenessThreshold) {
outputData[i] = oracleResult.data;
} else {
unchecked {
numAssetOracles = poolBackupOracles[_pool][i].length;
}
for (uint256 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) {
+ if (oracleResult.timestamp > block.timestamp - oracleStalenessThreshold) {
// Oracle has fresh values
break;
} else if (j == numAssetOracles - 1) {
// All oracle results for this data point are stale. Should rarely happen in practice with proper backup oracles.
revert("No fresh oracle values available");
}
unchecked {
++j;
}
}
outputData[i] = oracleResult.data;
}
unchecked {
++i;
}
}
}
Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas / Admin is trusted / Pool creation is trusted / User mistake / Suppositions

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelyhood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

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

Give us feedback!