QuantAMM

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

Inconsistent Negative Price Handling Between Oracle Implementations

Summary

The protocol has inconsistent handling of negative prices between oracle implementations. While MultiHopOracle correctly allows negative prices (which is valid for synthetic tokens, inverse tokens, and price-difference tracking tokens), ChainlinkOracle incorrectly blocks them with a require(data > 0) check. This inconsistency creates confusion about negative price support and prevents uniform handling of legitimate negative price feeds across the protocol.

Vulnerability Details

ERC20 token price feeds can be negative in several legitimate scenarios:

  • Synthetic tokens tracking price differences

  • Inverse token products

  • Derivative tokens

  • Spread-based tokens

The MultiHopOracle correctly allows these negative prices:

function _getData() internal view override returns (int216 data, uint40 timestamp) {
HopConfig memory firstOracle = oracles[0];
(data, timestamp) = firstOracle.oracle.getData();
if (firstOracle.invert) {
data = 10 ** 36 / data; // Correctly allows negative values
}
// ... subsequent calculations preserve negative values
}

While ChainlinkOracle blocks them:

function _getData() internal view override returns (int216, uint40) {
(, int data, , uint timestamp, ) = priceFeed.latestRoundData();
require(data > 0, "INVLDDATA"); // Incorrectly blocks negative prices
data = data * int(10 ** normalizationFactor);
return (int216(data), uint40(timestamp));
}

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../../../contracts/ChainlinkOracle.sol";
import "../../../contracts/MultiHopOracle.sol";
import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
contract MockNegativePriceFeed is AggregatorV3Interface {
function decimals() external pure returns (uint8) {
return 8;
}
function description() external pure returns (string memory) {
return "Mock Negative Price Feed";
}
function version() external pure returns (uint256) {
return 1;
}
function getRoundData(uint80) external pure returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) {
return (0, 0, 0, 0, 0);
}
function latestRoundData() external view returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) {
return (1, -5000000000, block.timestamp, block.timestamp, 1); // -50 USD with 8 decimals
}
}
contract QuantammOracleInconsistencyTest is Test {
ChainlinkOracle public chainlinkOracle;
MultiHopOracle public multiHopOracle;
MockNegativePriceFeed public mockFeed;
function setUp() public {
mockFeed = new MockNegativePriceFeed();
}
function testChainlinkOracleBlocksNegativePrices() public {
// Setup ChainlinkOracle with mock negative price feed
chainlinkOracle = new ChainlinkOracle(address(mockFeed));
// Expect revert when trying to get negative price
vm.expectRevert("INVLDDATA");
chainlinkOracle.getData();
}
function testMultiHopOracleAllowsNegativePrices() public {
// Setup ChainlinkOracle wrapper for MultiHopOracle
chainlinkOracle = new ChainlinkOracle(address(mockFeed));
// Setup MultiHopOracle with single hop configuration
MultiHopOracle.HopConfig[] memory hops = new MultiHopOracle.HopConfig[]();
hops[0] = MultiHopOracle.HopConfig({
oracle: OracleWrapper(address(mockFeed)),
invert: false
});
multiHopOracle = new MultiHopOracle(hops);
// Get price from MultiHopOracle
(int216 price, uint40 timestamp) = multiHopOracle.getData();
// Verify negative price is preserved
assertEq(price, -50e18, "MultiHopOracle should preserve negative price");
assertTrue(timestamp > 0, "Timestamp should be set");
}
}

Impact

Severity: HIGH

  1. Technical Impact:

    • Inconsistent oracle behavior across the protocol

    • Blocks legitimate negative price feeds in ChainlinkOracle

    • Creates confusion about negative price support

    • Prevents uniform price feed handling

  2. Business Impact:

    • Cannot use ChainlinkOracle for legitimate negative price feeds

    • Requires complex oracle paths to support negative prices

    • Inconsistent support for synthetic/inverse products

    • Architectural confusion about price sign handling

Tools Used

  • Foundry testing framework

  • Manual code review

  • Analysis of oracle implementations

  • Cross-reference of Chainlink price feed specifications

Recommendations

  1. Remove positive price requirement from ChainlinkOracle to match MultiHopOracle's behavior:

function _getData() internal view override returns (int216, uint40) {
(, int data, , uint timestamp, ) = priceFeed.latestRoundData();
// Only check for zero price
require(data != 0, "Zero price not supported");
data = data * int(10 ** normalizationFactor);
return (int216(data), uint40(timestamp));
}
  1. Add consistent validation across oracles:

    • Document negative price support clearly

    • Add uniform bounds checking if needed

    • Maintain consistent behavior across oracle implementations

    • Consider adding events for price validation failures

References

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

invalid_getData_negative_or_zero_price

Multihop will call ChainlinkOracle and the check is in it: `require(data > 0, "INVLDDATA");` MultiHop is just here to combine Chainlinks feed when there is no direct USD price feed for a token.

Support

FAQs

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

Give us feedback!