Stratax Contracts

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

Oracle Does Not Check for Stale Prices Leading to Incorrect Position Valuations

Author Revealed upon completion

Description

  • The Stratax protocol relies on Chainlink price feeds to calculate leverage ratios, determine position health, and execute swaps at appropriate exchange rates. The oracle's getPrice() function retrieves the latest price data from Chainlink aggregators for critical operations including opening leveraged positions and unwinding them.

  • The getPrice() function does not validate whether the returned price data is stale by checking the updatedAt timestamp or comparing answeredInRound with roundId. During Chainlink oracle outages, network congestion, or when price feeds stop updating due to extreme market conditions, the function continues to return outdated prices. This causes the protocol to create leveraged positions with incorrect collateral ratios, calculate wrong swap amounts, and potentially liquidate healthy positions or fail to liquidate underwater positions.

// File: StrataxOracle.sol, Lines 66-75
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(); // ❌ Ignores updatedAt, roundId, answeredInRound
require(answer > 0, "Invalid price from oracle");
price = uint256(answer);
}

Risk

Likelihood: High

  • Chainlink oracles experience regular outages and delayed updates during periods of network congestion, L2 sequencer downtime, or when market conditions exceed update thresholds

  • Historical data shows Chainlink feeds can become stale for hours during Black Swan events when accurate pricing is most critical (e.g., LUNA collapse, FTX implosion, ETH flash crashes)

  • The protocol has no fallback mechanism or grace period - any staleness immediately affects all position calculations

  • Multi-collateral protocols increase exposure as each token pair has independent oracle feeds that can fail at different times

Impact: Critical

  • Users create leveraged positions using incorrect prices, immediately placing them at risk of liquidation when real prices update

  • Positions calculated with stale high prices appear healthy but are actually underwater, preventing timely liquidations and creating protocol bad debt

  • Flash loan calculations use wrong exchange rates, causing transactions to fail or execute unfavorable swaps that lose user funds

  • Attackers can monitor for stale prices and exploit the discrepancy by opening positions that are profitable when prices update, extracting value from the protocol

  • Protocol insolvency risk increases as accumulated bad debt from mispriced positions exceeds available collateral

Proof of Concept

function test_StalePriceAcceptedCausesIncorrectPositions() public {
// Setup: Mock a Chainlink price feed with stale data
address mockPriceFeed = address(0x9999);
vm.mockCall(
mockPriceFeed,
abi.encodeWithSignature("decimals()"),
abi.encode(uint8(8))
);
// Simulate: ETH price was $3,000 at 10:00 AM
uint256 staleTimestamp = block.timestamp - 3 hours; // 3 hours old!
vm.mockCall(
mockPriceFeed,
abi.encodeWithSignature("latestRoundData()"),
abi.encode(
uint80(100), // roundId
int256(3000e8), // answer: $3,000 (STALE)
staleTimestamp, // startedAt: 3 hours ago
staleTimestamp, // updatedAt: 3 hours ago
uint80(100) // answeredInRound
)
);
strataxOracle.setPriceFeed(WETH, mockPriceFeed);
// Oracle accepts stale price without any validation
uint256 price = strataxOracle.getPrice(WETH);
assertEq(price, 3000e8, "Stale price accepted");
console.log("Returned price: $3,000 (but real price is $2,000!)");
console.log("Price is 3 hours old but oracle accepts it");
// Real scenario: ETH crashed to $2,000 in last 3 hours
// User opens 5x leveraged position thinking ETH = $3,000
// Actual ETH = $2,000
// Position is immediately 33% underwater
// User will be liquidated as soon as oracle updates
}

Recommended Mitigation

// File: StrataxOracle.sol
+ /// @notice Maximum age of price data before considered stale (1 hour)
+ uint256 public constant STALENESS_THRESHOLD = 3600;
+
+ /// @notice Minimum valid price to prevent oracle errors
+ uint256 public constant MIN_VALID_PRICE = 1e4; // $0.0001 with 8 decimals
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(uint256(answer) >= MIN_VALID_PRICE, "Price suspiciously low");
+ require(answeredInRound >= roundId, "Stale price: round mismatch");
+ require(updatedAt > 0, "Round not complete");
+ require(block.timestamp - updatedAt <= STALENESS_THRESHOLD, "Price data is stale");
price = uint256(answer);
}

Support

FAQs

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

Give us feedback!