Stratax Contracts

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

Oracle Staleness: `getPrice()` Never Validates `updatedAt` Timestamp

Author Revealed upon completion

Root + Impact

Description

  • StrataxOracle.getPrice() calls latestRoundData() but discards all return values except answer, never comparing updatedAt against block.timestamp.

  • Chainlink prices can become stale during network congestion, sequencer downtime on L2s, or feed deprecation. The stale price flows directly into calculateOpenParams and calculateUnwindParams, which determine borrow amounts, flash loan sizes, and collateral withdrawal amounts.

// StrataxOracle.sol - getPrice()
function getPrice(address _token) public view returns (uint256 price) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[_token]);
@> (, int256 answer,,,) = priceFeed.latestRoundData(); // updatedAt silently discarded
require(answer > 0, "Invalid price from oracle");
price = uint256(answer);
}

Risk

Likelihood:

  • Chainlink feeds become stale during network congestion, L2 sequencer downtime, or feed deprecation — this is a documented, recurring event

  • No heartbeat or deviation threshold check exists, so any lag between Chainlink updates goes undetected

Impact:

  • Stale price causes incorrect collateral valuation in calculateOpenParams, leading to over-borrowing and positions that are immediately liquidatable at the real market price

  • During unwind, stale price causes under-withdrawal of collateral, resulting in direct fund loss for the user

  • All positions share one Aave account, so a stale price degrading one position's health factor affects all positions simultaneously

Proof of Concept

```solidity
function testStalePriceExploit() public {
// Mock Chainlink returning stale price (updatedAt = 2 hours ago)
mockFeed.setRoundData(1, 3000e8, block.timestamp - 2 hours, block.timestamp - 2 hours, 1);
// Real market price has dropped to $2,000
// getPrice() still returns $3,000 — no staleness check
@> uint256 price = oracle.getPrice(WETH);
assertEq(price, 3000e8); // Stale price accepted without revert
// Position created with inflated collateral value
// Immediately liquidatable at real market price
}
```

Recommended Mitigation

```diff
- (, int256 answer,,,) = priceFeed.latestRoundData();
+ (uint80 roundId, int256 answer,, uint256 updatedAt,) = priceFeed.latestRoundData();
require(answer > 0, "Invalid price from oracle");
+ require(updatedAt > 0, "Round not complete");
+ require(block.timestamp - updatedAt <= MAX_STALENESS, "Stale price");
+ require(roundId > 0, "Invalid round");
```
Add a configurable `MAX_STALENESS` constant per feed (e.g. 3600 for ETH/USD, 86400 for less volatile pairs).

Support

FAQs

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

Give us feedback!