Summary
The ChainlinkOracle contract lacks timestamp validation for price data, accepting stale prices that could be hours old. This creates significant risks for protocols relying on accurate, real-time price data.
Vulnerability Details
The `getData()` function in ChainlinkOracle.sol (lines 27-34) retrieves price data without checking timestamp freshness:
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));
}
}
The function only validates that the price is positive but fails to ensure the data is recent.
Proof of Concept
function testStaleData() public {
vm.warp(block.timestamp + 1 days);
mockFeed.setStaleData(2 hours);
(int216 price, uint40 timestamp) = oracle.getData();
console.log("Current timestamp:", block.timestamp);
console.log("Oracle timestamp:", timestamp);
console.log("Time difference:", block.timestamp - timestamp);
console.log("Price returned:", uint256(int256(price)));
assertTrue(block.timestamp - timestamp >= 2 hours, "Data should be stale");
}
Results:
[PASS] testStaleData() (gas: 34540)
Logs:
Current timestamp: 86401
Oracle timestamp: 79201
Time difference: 7200
Price returned: 1000000000000000000000
Traces:
[34540] ChainlinkOracleTest::testStaleData()
├─ [0] VM::warp(86401 [8.64e4])
│ └─ ← [Return]
├─ [5390] MockChainlinkFeed::setStaleData(7200)
│ └─ ← [Stop]
├─ [10156] ChainlinkOracle::getData() [staticcall]
│ ├─ [8855] MockChainlinkFeed::latestRoundData() [staticcall]
│ │ └─ ← [Return] 1, 100000000000 [1e11], 0, 79201 [7.92e4], 1
│ └─ ← [Return] 1000000000000000000000 [1e21], 79201 [7.92e4]
├─ [0] console::log("Current timestamp:", 86401 [8.64e4]) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Oracle timestamp:", 79201 [7.92e4]) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Time difference:", 7200) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Price returned:", 1000000000000000000000 [1e21]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::assertTrue(true, "Data should be stale") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Impact
This vulnerability allows stale prices to be used for critical operations:
Liquidations could occur at outdated prices
Users could open/close positions at incorrect valuations
Protocol could become insolvent due to mispriced positions
In volatile markets, even slightly outdated prices can lead to significant losses.
Tools Used
Foundry forge test
Manual code review
Recommendations
Add a maximum staleness threshold check:
function getData() public view returns (int216, uint40) {
(,int data, , uint timestamp,) = priceFeed.latestRoundData();
require(data > 0, "INVLDDATA");
require(block.timestamp - timestamp <= 1 hours, "STALE");
data = data * int(10 ** normalizationFactor);
return (int216(data), uint40(timestamp));
}