Platform: Code4rena
Start Date: 02/06/2023
Pot Size: $100,000 USDC
Total HM: 15
Participants: 75
Period: 7 days
Judge: Picodes
Total Solo HM: 5
Id: 249
League: ETH
Rank: 47/75
Findings: 1
Award: $31.80
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Madalad
Also found by: Aymen0909, Bauchibred, Breeje, DadeKuma, Hama, LaScaloneta, Madalad, MohammedRizwan, bin2chen, dwward3n, erictee, etherhood, kutugu, peanuts, piyushshukla, rvierdiiev, saneryee, tallo, turvy_fuzz, whimints
31.7954 USDC - $31.80
https://github.com/code-423n4/2023-06-stader/blob/HEAD/contracts/StaderOracle.sol#L637-L651
The retrieved price from the oracle can be stale value or outdated and used anyways as a valid data. The usage of such data can impact on how the further logics of that price are implemented.
File: StaderOracle.sol function getPORFeedData() internal view returns ( uint256, uint256, uint256 ) { (, int256 totalETHBalanceInInt, , , ) = AggregatorV3Interface(staderConfig.getETHBalancePORFeedProxy()) .latestRoundData(); (, int256 totalETHXSupplyInInt, , , ) = AggregatorV3Interface(staderConfig.getETHXSupplyPORFeedProxy()) .latestRoundData(); return (uint256(totalETHBalanceInInt), uint256(totalETHXSupplyInInt), block.number); }
Neither round completeness or the quoted timestamp are checked to ensure that the reported price is not stale.
function latestRoundData() external view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound )
That's the reason Chainlink recommends using their data feeds along with some controls to prevent mismatches with the retrieved data.
Chainlink oracles updates the value when one of the 2 Trigger happens:
Deviation Threshold
exceeds. Deviation Threshold is the specific amount of deviation in prices which happens after which a new aggregation round will start and the price will be updated.Heartbeat Threshold
exceeds. Heartbeat Threshold is a specific amount of time from the last update after which a new aggregation round will start and the price will be updated.Link: https://docs.chain.link/architecture-overview/architecture-decentralized-model/
But in getPORFeedData
, Heartbeat Threshold is not implemented to check whether the last observed price was stale or not.
Chainlink in there docs have clearly written: "Your application should track the latestTimestamp variable or use the updatedAt value from the latestRoundData() function to make sure that the latest answer is recent enough for your application to use it. If your application detects that the reported answer is not updated within the heartbeat or within time limits that you determine are acceptable for your application, pause operation or switch to an alternate operation mode while identifying the cause of the delay."
So by adding the require check as recommended, it can stop the protocol from using the stale price which is even recommended by chainlink.
Link: https://docs.chain.link/data-feeds/#check-the-timestamp-of-the-latest-answer
VS Code
As Chainlink recommends:
Your application should track the latestTimestamp variable or use the updatedAt value from the latestRoundData() function to make sure that the latest answer is recent enough for your application to use it. If your application detects that the reported answer is not updated within the heartbeat or within time limits that you determine are acceptable for your application, pause operation or switch to an alternate operation mode while identifying the cause of the delay.
Recommended Mitigation:
getPORFeedData()
method to mitigate the issue after setting the HEARTBEAT_PERIOD
as set by chainlink.File: PriceOracle.sol function getPORFeedData() internal view returns ( uint256, uint256, uint256 ) { - (, int256 totalETHBalanceInInt, , , ) = AggregatorV3Interface(staderConfig.getETHBalancePORFeedProxy()) + (uint80 roundID, int256 totalETHBalanceInInt, , uint256 updatedAt, uint80 answeredInRound) = AggregatorV3Interface(staderConfig.getETHBalancePORFeedProxy()) .latestRoundData(); + require(totalETHBalanceInInt > 0, "invalid price"); + require(block.timestamp - updatedAt < HEARTBEAT_PERIOD, "Chainlink: Stale Price"); + require(answeredInRound >= roundID, "Chainlink: Stale Price"); - (, int256 totalETHXSupplyInInt, , , ) = AggregatorV3Interface(staderConfig.getETHXSupplyPORFeedProxy()) + (uint80 roundID, int256 totalETHXSupplyInInt, , uint256 updatedAt, uint80 answeredInRound) = AggregatorV3Interface(staderConfig.getETHXSupplyPORFeedProxy()) .latestRoundData(); + require(totalETHXSupplyInInt > 0, "invalid price"); + require(block.timestamp - updatedAt < HEARTBEAT_PERIOD, "Chainlink: Stale Price"); + require(answeredInRound >= roundID, "Chainlink: Stale Price"); return (uint256(totalETHBalanceInInt), uint256(totalETHXSupplyInInt), block.number); }
Oracle
#0 - c4-judge
2023-06-10T14:44:41Z
Picodes marked the issue as duplicate of #15
#1 - c4-judge
2023-07-02T10:50:52Z
Picodes marked the issue as satisfactory