Stratax Contracts

First Flight #57
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: medium
Likelihood: medium

Missing Stale Price Check in Oracle Allows Use of Outdated Prices

Author Revealed upon completion

`StrataxOracle.getPrice()` does not validate the `updatedAt` timestamp from Chainlink’s `latestRoundData()`, allowing the protocol to operate with arbitrarily stale prices that no longer reflect market conditions, leading to incorrect position calculations and potential loss of funds.

Description

  • When retrieving price data from Chainlink oracles, the protocol should verify that the price is recent enough to be reliable. Chainlink price feeds can become stale during network congestion, oracle downtime, or when the price does not deviate enough to trigger an update.

  • In `StrataxOracle.getPrice()`, the function calls `latestRoundData()` which returns five values including `updatedAt` (the timestamp of the last price update). However, the function only checks that `answer > 0` and completely ignores `updatedAt`, `roundId`, and `answeredInRound`. A price that was last updated 24 hours ago is accepted the same as a price updated 1 second ago.

function getPrice(address _token) public view returns (uint256 price) {
address priceFeedAddress = priceFeeds[_token];
require(priceFeedAddress != address(0), "Price feed not set for token");
AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeedAddress);
(, int256 answer,,,) = priceFeed.latestRoundData(); // @> updatedAt, roundId, answeredInRound all ignored
require(answer > 0, "Invalid price from oracle"); // @> Only checks > 0, no staleness validation
price = uint256(answer);
}

This stale price is then used in critical calculations in `calculateOpenParams()`, `calculateUnwindParams()`, and `_executeUnwindOperation()` to determine flash loan amounts, borrow amounts, and collateral withdrawal amounts.

Risk

Likelihood: Medium

  • Chainlink oracle downtime has occurred historically during extreme market events (e.g., LUNA/UST crash, network congestion events). During these events, `latestRoundData()` returns the last known price rather than the current market price

  • The protocol deploys on “all EVM-compatible chains with Aave V3, 1inch, and Chainlink deployed” — L2 sequencer downtime on chains like Arbitrum or Optimism causes stale prices without an L2 sequencer uptime feed check.

Impact: Medieum

  • Funds are directly at risk. A stale price that overvalues collateral allows the owner to open positions with more leverage than the actual market conditions support, creating positions that are immediately at risk of liquidation once the real price is used by Aave.

  • A stale price in `_executeUnwindOperation()` causes incorrect `collateralToWithdraw` calculations — either withdrawing too much collateral (degrading health factor of other positions) or too little (leaving funds stuck).

Proof of Concept

Commande For Run : forge test --mt testStalePriceAccepted --fork-url https://ethereum-rpc.publicnode.com -vvv

function testStalePriceAccepted() public {
// Mock a stale ETH price — not updated for 24 hours
vm.mockCall(
WETH_PRICE_FEED,
abi.encodeWithSignature("latestRoundData()"),
abi.encode(
uint80(1), // roundId
int256(200000000000), // answer: $2000 (8 decimals) — stale price
block.timestamp - 24 hours, // startedAt: 24h ago
block.timestamp - 24 hours, // updatedAt: NOT UPDATED FOR 24H
uint80(1) // answeredInRound
)
);
// The stale price is accepted without any validation
uint256 price = strataxOracle.getPrice(WETH);
// Returns $2000 even though the real market price could be $1000
assertEq(price, 200000000000, "Stale price accepted without verification");
}

Recommended Mitigation

function getPrice(address _token) public view returns (uint256 price) {
address priceFeedAddress = priceFeeds[_token];
require(priceFeedAddress != address(0), "Price feed not set for token");
AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeedAddress);
- (, int256 answer,,,) = priceFeed.latestRoundData();
+ (uint80 roundId, int256 answer,, uint256 updatedAt, uint80 answeredInRound) = priceFeed.latestRoundData();
require(answer > 0, "Invalid price from oracle");
+ require(block.timestamp - updatedAt < MAX_STALENESS, "Stale price from oracle");
+ require(answeredInRound >= roundId, "Incomplete round");
price = uint256(answer);
}
Add a configurable staleness threshold constant:
+ uint256 public constant MAX_STALENESS = 3600; // 1 hour

Support

FAQs

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

Give us feedback!