Summary
The Protocol Fetch the Price for ETH/USD and STETH/ETH from chainLink Price Feed to Identify the Price of WSTETH/USD. To validate the price return from chainlink priceFeed is not steal the protocol checks it against a 4 day timeout. which in fact is very large time as compare with HeartBeat PriceFeed of WETETH/ETH oracle which is one day.
Vulnerability Details
The Price timeout used for validity of Price return from chainLink price feed is very large uint256 constant FOUR_DAY_TIMEOUT = 345600
. when the Price return from ChainLink the protocol check wheatear its steal price or not using following function:
function checkForInvalidTimestampOrAnswer(
uint256 timestamp,
int256 answer,
uint256 currentTimestamp,
uint256 maxTimeout
) private pure returns (bool) {
if (timestamp == 0 || timestamp > currentTimestamp) return true;
if (currentTimestamp.sub(timestamp) > maxTimeout) return true;
if (answer <= 0) return true;
}
inside the getPrice
and getTwap
function as follows:
function getPrice(
address priceAggregatorAddress,
uint256 maxTimeout
) internal view returns (uint256 price) {
...
try priceAggregator.latestRoundData() returns (
uint80 roundId,
int256 answer,
uint256 ,
uint256 timestamp,
uint80
) {
if (roundId == 0) return 0;
@> if (checkForInvalidTimestampOrAnswer(timestamp, answer, block.timestamp, maxTimeout)) {
return 0;
}
...
}
function getTwap(
address priceAggregatorAddress,
uint256 maxTimeout,
uint256 lookback
) internal view returns (uint256 price) {
...
try priceAggregator.latestRoundData() returns (
uint80 roundId,
int256 answer,
uint256 ,
uint256 timestamp,
uint80
) {
if (roundId == 0) return 0;
@> if (checkForInvalidTimestampOrAnswer(timestamp, answer, block.timestamp, maxTimeout)) {
return 0;
}
TwapVariables memory t;
t.endTimestamp = block.timestamp.sub(lookback);
if (timestamp <= t.endTimestamp) {
return uint256(answer).mul(PRECISION).div(10 ** decimals);
} else {
t.lastTimestamp = block.timestamp;
while (timestamp > t.endTimestamp) {
t.cumulativePrice = t.cumulativePrice.add(
uint256(answer).mul(t.lastTimestamp.sub(timestamp))
);
roundId -= 1;
t.lastTimestamp = timestamp;
(answer, timestamp) = getRoundData(priceAggregator, roundId);
@> if (checkForInvalidTimestampOrAnswer(
timestamp,
answer,
t.lastTimestamp,
maxTimeout
)) {
return 0;
}
}
t.cumulativePrice = t.cumulativePrice.add(
uint256(answer).mul(t.lastTimestamp.sub(t.endTimestamp))
);
return t.cumulativePrice.mul(PRECISION).div(10 ** decimals).div(lookback);
}
} catch {
return 0;
}
}
Impact
The Price would validate as true against a steal Price.
Tools Used
Manual Review
Recommendations
Change the uint256 constant FOUR_DAY_TIMEOUT = 345600
to meet the HearBeat of chainLink Price feed which is 1 day :
- uint256 constant FOUR_DAY_TIMEOUT = 345600;
+ uint256 constant FOUR_DAY_TIMEOUT = 86400;