Stratax Contracts

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

Missing Oracle Staleness Validation in StrataxOracle "getPrice" Allows Use of Outdated Prices

Author Revealed upon completion

Root + Impact

Description

The StrataxOracle "getPrice" function is expected to return a fresh, reliable price from Chainlink by calling latestRoundData() and validating the response before it is used in critical financial calculations such as determining borrow amounts during position opening and collateral withdrawal amounts during position unwinding.

However, the function discards the updatedAt, roundId, and answeredInRound fields from the Chainlink response and only checks that answer > 0. This means a price that hasn't been updated in hours or even days — due to Chainlink network congestion, feed deprecation, or multisig failures — will be silently accepted and used in _executeUnwindOperation and calculateOpenParams, potentially causing the contract to miscalculate collateral-to-debt ratios and resulting in users withdrawing too much or too little collateral when unwinding leveraged positions.

// Root cause in the codebase with @> marks to highlight the relevant section
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 is ignored
require(answer > 0, "Invalid price from oracle");
price = uint256(answer);
}

Risk

Likelihood:


Chainlink price feeds have documented instances of delayed updates during high network congestion, L2 sequencer downtime, or feed deprecation. When a feed's updatedAt timestamp falls significantly behind block.timestamp, every call to StrataxOracle::getPrice will return the outdated price without any revert, since the function only validates answer > 0 and discards all freshness metadata. This is not an edge case — Chainlink's own documentation explicitly recommends checking updatedAt and answeredInRound for this reason.


Impact:


During position unwinding in _executeUnwindOperation, a stale price causes the collateralToWithdraw calculation to be incorrect. If the stale price overvalues the collateral token relative to the debt token, the contract withdraws less collateral than needed to cover the flash loan repayment via swap, causing the require(returnAmount >= totalDebt) check to fail and locking the user out of unwinding their position. Conversely, if the stale price undervalues the collateral, the contract withdraws more collateral than necessary, leaving excess value in the swap return that gets re-supplied to Aave rather than returned to the user — a direct loss of user funds. During position opening via calculateOpenParams, a stale price produces an incorrect borrowAmount, which either causes the Aave borrow to revert (if too high) or results in a position with less leverage than intended (if too low).

Proof of Concept

function test_POC_NoOracleStalenessCheck() public {
// Warp to a realistic timestamp to avoid underflow
vm.warp(365 days); // Set block.timestamp to 1 year from epoch
// Setup: Mock the Chainlink price feed to return stale data
// We'll simulate a price that hasn't been updated in 30 days
uint80 roundId = 1;
int256 price = 2000e8; // $2000 with 8 decimals
uint256 startedAt = block.timestamp - 30 days;
uint256 updatedAt = block.timestamp - 30 days; // STALE: 30 days old!
uint80 answeredInRound = 1;
// Mock the latestRoundData call to return stale data
vm.mockCall(
WETH_PRICE_FEED,
abi.encodeWithSignature("latestRoundData()"),
abi.encode(roundId, price, startedAt, updatedAt, answeredInRound)
);
// The contract should reject this stale data, but it doesn't!
// This call succeeds even though the price is 30 days old
uint256 returnedPrice = strataxOracle.getPrice(WETH);
// Show impact: If real price is $1500 but stale oracle says $2000,
// the unwind withdraws wrong collateral amount
// (This means ~25% less collateral withdrawn than needed)
uint256 stalePrice = 2000e8;
uint256 realPrice = 1500e8;
uint256 debtAmount = 1e18; // 1 ETH of debt
// What contract calculates with stale price
uint256 staleCollateral = (stalePrice * debtAmount) / realPrice;
// What it SHOULD calculate
uint256 correctCollateral = (realPrice * debtAmount) / realPrice;
// Stale price causes over/under-estimation
assertTrue(staleCollateral != correctCollateral, "Stale price causes incorrect calculation");
// Assert: The contract accepts the stale price without any validation
assertEq(returnedPrice, uint256(price), "Contract accepted stale oracle data!");
// POC SUCCESS: A 30-day-old price was accepted without any staleness check
// If there was a staleness check, this test would have reverted
}

Recommended Mitigation

+ add this code
(uint80 roundId, int256 answer,, uint256 updatedAt, uint80 answeredInRound) = priceFeed.latestRoundData();
require(answer > 0, "Invalid price from oracle");
require(updatedAt > block.timestamp - MAX_STALENESS, "Stale price data");
require(answeredInRound >= roundId, "Stale round");

Support

FAQs

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

Give us feedback!