QuantAMM

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

Critical Precision Loss in MultiHopOracle Price Calculations

Summary

The MultiHopOracle contract exhibits severe precision loss when processing multiple small values in sequence or handling chained operations with inversions. Testing shows losses of up to 100% in some scenarios, rendering the oracle completely unreliable for certain token pairs.

Vulnerability Details

Location: pkg/pool-quantamm/contracts/MultiHopOracle.sol

The vulnerability manifests in two critical scenarios:

  1. Multiple Small Values:

// Sequential operations in the loop
for (uint i = 1; i < oracleLength; ) {
HopConfig memory oracleConfig = oracles[i];
(int216 oracleRes, uint40 oracleTimestamp) = oracleConfig.oracle.getData();
if (oracleConfig.invert) {
data = (data * 10 ** 18) / oracleRes;
} else {
data = (data * oracleRes) / 10 ** 18;
}
unchecked {
++i;
}
}

Complete loss of precision (100%) when handling very small values in sequence.

  1. Chained Operations with Inversion:

// First oracle inversion (if needed)
if (firstOracle.invert) {
data = 10 ** 36 / data;
}
// Subsequent oracle inversions
if (oracleConfig.invert) {
data = (data * 10 ** 18) / oracleRes;
} else {
data = (data * oracleRes) / 10 ** 18;
}

Massive overestimation due to precision loss in inverted calculations, especially when combining multiple operations.

Proof of Concept:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "../../../contracts/mock/MockChainlinkOracles.sol";
import "../../../contracts/MultiHopOracle.sol";
contract MultiHopOraclePrecisionLossTest is Test {
MockChainlinkOracle internal chainlinkOracle1;
MockChainlinkOracle internal chainlinkOracle2;
MultiHopOracle internal multiHopOracle;
// Helper function to deploy the oracles - similar to original test
function deployOracles(
int216 fixedValue1,
int216 fixedValue2,
uint delay1,
uint delay2,
bool[] memory invert
) internal returns (MultiHopOracle) {
chainlinkOracle1 = new MockChainlinkOracle(fixedValue1, delay1);
chainlinkOracle2 = new MockChainlinkOracle(fixedValue2, delay2);
MultiHopOracle.HopConfig[] memory hops = new MultiHopOracle.HopConfig[]();
hops[0] = MultiHopOracle.HopConfig({ oracle: OracleWrapper(address(chainlinkOracle1)), invert: invert[0] });
hops[1] = MultiHopOracle.HopConfig({ oracle: OracleWrapper(address(chainlinkOracle2)), invert: invert[1] });
return new MultiHopOracle(hops);
}
function testPrecisionLossWithMultipleSmallValues() public {
// Test with extremely small decimal values
// Using 1e-15 * 1e-15 which should equal 1e-30
// But due to precision loss with intermediate 1e18 scaling, it won't
int216 value1 = 1e3; // 1e-15 with 18 decimals (1e3)
int216 value2 = 1e3; // 1e-15 with 18 decimals (1e3)
uint delay = 3600;
bool[] memory invert = new bool[]();
invert[0] = false;
invert[1] = false;
multiHopOracle = deployOracles(value1, value2, delay, delay, invert);
vm.warp(block.timestamp + delay);
(int216 actualResult,) = multiHopOracle.getData();
// Expected result: 1e-30 with 18 decimals precision
// 1e-30 * 1e18 = 1e-12
int216 expectedResult = 1; // 1e-12 represented with 18 decimals
assertFalse(actualResult == expectedResult, "Should show precision loss");
int216 difference = expectedResult - actualResult;
emit log_named_int("Expected Result", int(expectedResult));
emit log_named_int("Actual Result", int(actualResult));
emit log_named_int("Difference", int(difference));
// Calculate relative error
int216 percentageError = (difference * 100e18) / expectedResult;
emit log_named_int("Percentage Error (scaled by 1e18)", int(percentageError));
}
function testPrecisionLossInChainedOperations() public {
// Test precision loss with extreme values:
// Starting with a very large value (1e20) and dividing by a very small value (1e-16)
// This should stress the precision limits of the calculations
int216 value1 = 1e20; // Very large value
int216 value2 = 1e2; // Very small value (will be inverted)
uint delay = 3600;
bool[] memory invert = new bool[]();
invert[0] = false;
invert[1] = true; // Invert second value
multiHopOracle = deployOracles(value1, value2, delay, delay, invert);
vm.warp(block.timestamp + delay);
(int216 actualResult,) = multiHopOracle.getData();
// Expected: 1e20 * (1/1e2) = 1e18
int216 expectedResult = 1e18;
assertFalse(actualResult == expectedResult, "Should show precision loss");
int216 difference = expectedResult - actualResult;
emit log_named_int("Expected Result", int(expectedResult));
emit log_named_int("Actual Result", int(actualResult));
emit log_named_int("Difference", int(difference));
// Calculate percentage error: (difference / expectedResult) * 100
int216 percentageError = (difference * 100e18) / expectedResult;
emit log_named_int("Percentage Error (scaled by 1e18)", int(percentageError));
}
}

Test Results:

1. Multiple Small Values Test:
Expected: 1 (1e-12 with 18 decimals)
Actual: 0
Loss: 100%
2. Chained Operations Test:
Expected: 1000000000000000000
Actual: 1000000000000000000000000000000000000
Error: ~1e41 magnitude deviation

Impact

Severity: HIGH

  1. Technical Impact:

    • Complete loss of precision (100%) in small value calculations

    • Massive overestimation in chained operations with inversion

    • Affects all multi-hop oracle paths with small values or inversions

  2. Economic Impact:

    • Incorrect pricing for micro-tokens

    • Unreliable price feeds for inverted pairs

    • Potential arbitrage opportunities

    • Strategy miscalculations leading to losses

Tools Used

  • Foundry testing framework

  • Custom precision loss test suite

  • Manual code review

  • Mathematical analysis of fixed-point arithmetic operations

Recommendations

  1. Implement Precision-Safe Calculations:

contract MultiHopOracle {
uint256 constant PRECISION_SCALAR = 1e9;
function getData() public view returns (int216 data, uint256 timestamp) {
// ... existing code ...
// Higher precision intermediate calculations
uint256 scaledResult = uint256(data) * uint256(oracleRes) * PRECISION_SCALAR;
data = int216(scaledResult / (10 ** 18 * PRECISION_SCALAR));
// ... existing code ...
}
}
  1. Add Value Range Validation:

// Minimum/maximum thresholds
int216 constant MIN_VALUE = 1e3; // 1e-15 with 18 decimals
int216 constant MAX_VALUE = 1e40; // Based on int216 limits
function validateValue(int216 value) internal pure {
require(abs(value) >= MIN_VALUE || value == 0, "Value too small");
require(abs(value) <= MAX_VALUE, "Value too large");
}
  1. Architectural Changes:

    • Use a precision-focused math library

    • Implement circuit breakers for extreme values

    • Add monitoring for precision loss events

    • Consider alternative scaling approaches for small values

References

Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_MultiHopOracle_getData_invert_precision_loss_low_decimals

Likelihood: Informational/Very Low, admin should use a price feed with 18 decimals and this feed should compare a assets with a very small value and an asset with a biggest amount to have the smallest price possible. Admin wouldn't do that intentionally, but one token could collapse, and with multiple hop, it increases a bit the probability. Impact: High, complete loss of precision. Probability near 0 but not 0: deserve a Low

Support

FAQs

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