Stratax Contracts

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

`StrataxOracle.getPrice()` missing stale price check — uses potentially outdated Chainlink data for critical calculations

Author Revealed upon completion

Root + Impact

The StrataxOracle.getPrice() function retrieves price data from Chainlink via latestRoundData() but completely discards the updatedAt and answeredInRound return values. This means there is zero validation that the price data is current. Stale prices lead to incorrect position sizing during opens (risking immediate liquidation) and incorrect collateral withdrawal during unwinds.

Description

  • At line 70 of StrataxOracle.sol, the getPrice() function destructures latestRoundData() as (, int256 answer,,,) — discarding roundId, startedAt, updatedAt, and answeredInRound. The only validation is require(answer > 0), which passes for prices that are hours or days old.

  • Ironically, the same contract has a getRoundData() function (L98-108) that DOES return all Chainlink fields — but getPrice(), the one actually called by Stratax.sol in 6 locations (L395, L402, L461, L462, L570, L571), ignores them.

  • Stale prices affect both critical paths: calculateOpenParams() (borrow amount computation) and _executeUnwindOperation() (collateral withdrawal calculation).

// StrataxOracle.sol L64-74
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);
// @> latestRoundData returns (roundId, answer, startedAt, updatedAt, answeredInRound)
// @> updatedAt and answeredInRound are completely DISCARDED
(, int256 answer,,,) = priceFeed.latestRoundData();
require(answer > 0, "Invalid price from oracle");
// @> No staleness check — updatedAt could be hours or days old
// @> No round completeness check — answeredInRound could be < roundId
price = uint256(answer);
}

Risk

Likelihood:

  • Chainlink price feeds can become stale during network congestion, extreme gas prices, or oracle node issues. This has occurred historically: June 2020 (ETH flash crash), March 2023 (USDC depeg stress).

  • The getPrice() function is called in 6 locations across Stratax.sol (L395, L402, L461, L462, L570, L571), covering both open and unwind critical paths.

Impact:

  • During position creation: stale price leads to incorrect borrowAmount calculation. Aave evaluates position health using its own fresh oracle — if Stratax's stale price significantly diverges, the position may be created with a dangerously low health factor, risking immediate liquidation.

  • During position unwinding: stale price leads to incorrect collateralToWithdraw calculation. If calculated too high, Aave's withdraw() reverts (health factor violation). If too low, less collateral is recovered than expected.

Proof of Concept

The following scenario demonstrates how stale oracle data can lead to incorrect position health. When Chainlink's price feed hasn't been updated due to network congestion, getPrice() returns the old price without any warning. Since Aave uses its own oracle internally, the position can be created with parameters based on stale data while Aave evaluates with current prices — leading to an unexpectedly low health factor and immediate liquidation risk.

// Scenario demonstrating stale price impact:
//
// 1. ETH price was $3,000 yesterday (Chainlink updatedAt = 24 hours ago)
// 2. ETH price is now $2,500 (Chainlink hasn't updated due to congestion)
// 3. User calls createLeveragedPosition()
// 4. calculateOpenParams() calls getPrice() → returns stale $3,000
// 5. borrowAmount calculated based on $3,000 collateral value → too high
// 6. aavePool.borrow() evaluates with Aave's own oracle ($2,500) → HF < expected
// 7. Position created with dangerously low health factor → immediate liquidation risk
//
// Standard staleness checks (ALL MISSING from StrataxOracle):
// require(updatedAt > 0, "Round not complete");
// require(answeredInRound >= roundId, "Stale price data");
// require(block.timestamp - updatedAt < STALENESS_THRESHOLD, "Price too stale");

Recommended Mitigation

The fix captures all five return values from latestRoundData() and adds three standard Chainlink staleness checks: (1) verifying the round is complete via updatedAt > 0, (2) ensuring answeredInRound >= roundId to confirm the answer was finalized, and (3) enforcing a maximum age for the price data (1 hour threshold shown, configurable based on asset volatility). This is the industry-standard approach used by protocols like Aave and Compound.

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 < 3600, "Price data too stale");
price = uint256(answer);
}

Support

FAQs

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

Give us feedback!