Stratax Contracts

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

Stale Chainlink Oracle Price Data Used in Leverage Calculations Due to Missing Staleness and Validity Checks in `StrataxOracle::getPrice()`

Author Revealed upon completion

Root + Impact

Description

  • The StrataxOracle::getPrice() function is used throughout the protocol to fetch token prices for leverage calculations, position opening, and unwinding. It calls Chainlink's latestRoundData() but only validates that answer > 0, discarding all other return values that are critical for detecting stale or invalid price data.

  • The function ignores updatedAt, roundId, answeredInRound, and startedAt — all of which are necessary to determine whether the price feed is current and complete. When a Chainlink oracle goes stale (due to network congestion, oracle node failures, or feed deprecation), the last known price continues to be returned indefinitely.

// Root cause in the codebase with @> marks to highlight the relevant sectionfunction 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(); // All staleness fields discarded
@> require(answer > 0, "Invalid price from oracle"); // Only check: answer > 0
price = uint256(answer);
}

Risk

Likelihood:

  • Chainlink oracle staleness events occur during periods of network congestion, gas spikes, or oracle infrastructure issues — these are documented real-world events that have affected DeFi protocols on Ethereum mainnet

  • The protocol targets Ethereum mainnet and "all EVM-compatible chains with Aave V3, 1inch, and Chainlink deployed," and some L2 chains have experienced extended oracle delays

    Impact:

  • Stale prices fed into calculateOpenParams() produce incorrect flash loan amounts and borrow amounts, resulting in positions with wrong leverage that may be immediately liquidatable on Aave

  • Stale prices fed into calculateUnwindParams() and _executeUnwindOperation() produce incorrect collateral withdrawal amounts, potentially leaving users with less collateral than expected or causing the unwind to fail

  • An attacker aware of a stale oracle can exploit the price discrepancy to extract value — e.g., opening a leveraged position using an outdated favorable price, then unwinding at the real (current) price

Proof of Concept

// The getPrice function is called in these critical paths:
// 1. calculateOpenParams() — Lines 395-396, 401-402
details.collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(details.collateralToken);
details.borrowTokenPrice = IStrataxOracle(strataxOracle).getPrice(details.borrowToken);
// 2. calculateUnwindParams() — Lines 461-462
uint256 debtTokenPrice = IStrataxOracle(strataxOracle).getPrice(_borrowToken);
uint256 collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(_collateralToken);
// 3. _executeUnwindOperation() — Lines 570-571
uint256 debtTokenPrice = IStrataxOracle(strataxOracle).getPrice(_asset);
uint256 collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(unwindParams.collateralToken);
// If the Chainlink feed for WETH/USD returns a stale price of $3,000 when the real
// price is $2,500, the protocol will calculate incorrect leverage parameters:
// - Flash loan amount will be based on inflated collateral value
// - Borrow amount will be too high relative to actual collateral worth
// - The Aave position will have a lower real health factor than calculated
// - The position may be immediately liquidated by Aave at the real price

Recommended Mitigation

- remove this code
+ add this code+ uint256 public constant MAX_STALENESS = 3600; // 1 hour
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(updatedAt > 0, "Round not complete");
+ require(answeredInRound >= roundId, "Stale price data");
+ require(block.timestamp - updatedAt <= MAX_STALENESS, "Price feed is stale");

Support

FAQs

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

Give us feedback!