Summary
ChainlinkOracle lacks price deviation checks, allowing manipulated price data to be accepted without validation against historical or expected values.
Vulnerability Details
The getData() function accepts any non-zero price without validating price movements:
function _getData() internal view override returns (int216, uint40) {
(, int data, , uint timestamp, ) =
priceFeed.latestRoundData();
require(data > 0, "INVLDDATA");
data = data * int(10 ** normalizationFactor);
return (int216(data), uint40(timestamp));
}
}
prices can be arbitrarily manipulated:
Proof of concept
function testPriceManipulation() public {
int256 initialPrice = 1000e8;
int256 manipulatedPrice = 500e8;
console.log("Initial price:", uint256(int256(initialPrice)));
(int216 price1,) = oracle.getData();
assertEq(price1, 1000e18, "Initial price should be 1000");
mockFeed.setPrice(manipulatedPrice);
console.log("Manipulated price:", uint256(int256(manipulatedPrice)));
(int216 price2,) = oracle.getData();
assertEq(price2, 500e18, "Should accept manipulated price");
}
}
results:
[PASS] testPriceManipulation() (gas: 35864)
Logs:
Initial price: 100000000000
Manipulated price: 50000000000
Traces:
[35864] ChainlinkOracleTest::testPriceManipulation()
├─ [0] console::log("Initial price:", 100000000000 [1e11]) [staticcall]
│ └─ ← [Stop]
├─ [14656] ChainlinkOracle::getData() [staticcall]
│ ├─ [10855] MockChainlinkFeed::latestRoundData() [staticcall]
│ │ └─ ← [Return] 1, 100000000000 [1e11], 0, 1, 1
│ └─ ← [Return] 1000000000000000000000 [1e21], 1
├─ [0] VM::assertEq(1000000000000000000000 [1e21], 1000000000000000000000 [1e21], "Initial price should be 1000") [staticcall]
│ └─ ← [Return]
├─ [3154] MockChainlinkFeed::setPrice(50000000000 [5e10])
│ └─ ← [Stop]
├─ [0] console::log("Manipulated price:", 50000000000 [5e10]) [staticcall]
│ └─ ← [Stop]
├─ [2156] ChainlinkOracle::getData() [staticcall]
│ ├─ [855] MockChainlinkFeed::latestRoundData() [staticcall]
│ │ └─ ← [Return] 1, 50000000000 [5e10], 0, 1, 1
│ └─ ← [Return] 500000000000000000000 [5e20], 1
├─ [0] VM::assertEq(500000000000000000000 [5e20], 500000000000000000000 [5e20], "Should accept manipulated price") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Impact
Unchecked price deviations enable:
Flash loan attacks via price manipulation
Malicious liquidations through artificial price movements
Protocol insolvency from trades at manipulated prices
Tools Used
Foundry testing framework
Manual code review
Recommendations
Add deviation threshold checks:
function getData() public view returns (int216, uint40) {
(,int data, , uint timestamp,) = priceFeed.latestRoundData();
require(data > 0, "INVLDDATA");
int256 prevPrice = getPreviousRoundPrice();
int256 maxDeviation = (prevPrice * 10) / 100;
require(abs(data - prevPrice) <= maxDeviation, "INVALID_DEVIATION");
data = data * int(10 ** normalizationFactor);
return (int216(data), uint40(timestamp));
}