Stratax Contracts

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

`StrataxOracle::getPrice` does not check for stale Chainlink data

Author Revealed upon completion

StrataxOracle::getPrice does not check for stale Chainlink data

Description

StrataxOracle::getPrice calls latestRoundData but doesn't check the updatedAt timestamp, performing no staleness check:

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();
require(answer > 0, "Invalid price from oracle");
price = uint256(answer);
}

If the Chainlink feed becomes stale (e.g., the feed hasn't been updated due to network congestion, sequencer downtime), the oracle will return the last known price — which could be arbitrarily outdated — without any indication that the data is stale.

This affects every price-dependent calculation throughout Stratax:

calculateOpenParams (lines 402, 410) — Stale prices produce incorrect flashLoanAmount and borrowAmount, opening positions with wrong leverage or insufficient collateral:

function calculateOpenParams(TradeDetails memory details)
public
view
returns (uint256 flashLoanAmount, uint256 borrowAmount)
{
// ...
@> details.collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(details.collateralToken);
@> details.borrowTokenPrice = IStrataxOracle(strataxOracle).getPrice(details.borrowToken);
// ...
}

calculateUnwindParams (lines 472-473) — Stale prices miscalculate collateralToWithdraw, causing the unwind to withdraw too much or too little collateral. These calls also have no zero-price validation:

function calculateUnwindParams(address _collateralToken, address _borrowToken)
public
view
returns (uint256 collateralToWithdraw, uint256 debtAmount)
{
// ...
@> uint256 debtTokenPrice = IStrataxOracle(strataxOracle).getPrice(_borrowToken);
@> uint256 collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(_collateralToken);
collateralToWithdraw = (debtTokenPrice * debtAmount * 10 ** IERC20(_collateralToken).decimals())
/ (collateralTokenPrice * 10 ** IERC20(_borrowToken).decimals());
// ...
}

_executeUnwindOperation (lines 585-586) — Stale prices during the actual flash loan callback produce an incorrect collateralToWithdraw, directly affecting how much collateral is withdrawn from Aave mid-execution:

function _executeUnwindOperation(address _asset, uint256 _amount, uint256 _premium, bytes calldata _params)
internal
returns (bool)
{
// ...
uint256 debtTokenPrice = IStrataxOracle(strataxOracle).getPrice(_asset);
uint256 collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(unwindParams.collateralToken);
@> require(debtTokenPrice > 0 && collateralTokenPrice > 0, "Invalid prices");
// ...
}

Risk

Likelihood:

  • The ETH/USD Chainlink feed on Ethereum mainnet has a 1-hour heartbeat and a 0.5% deviation threshold (data.chain.link). If the price moves less than 0.5%, the on-chain answer is only refreshed once per hour. Since getPrice doesn't check the updatedAt return value, the contract cannot distinguish a 5-second-old price from a 59-minute-old one.

  • True staleness beyond the heartbeat (i.e., no update at all for multiple hours) occurs during feed deprecations or infrastructure issues. Chainlink has an ongoing feed deprecation process (docs), and missing staleness checks have been exploited or flagged in multiple real-world audits.

Impact:

  • calculateOpenParams computes flashLoanAmount and borrowAmount from the stale price, so a user opening a leveraged position receives incorrect leverage — either over-borrowing (creating an immediately liquidatable position on Aave) or under-borrowing (wasting user collateral on less leverage than intended).

  • calculateUnwindParams and _executeUnwindOperation derive collateralToWithdraw from the stale price. If the stale price overstates collateral value, too little collateral is withdrawn to fully repay the flash loan, causing the unwind transaction to revert or leaving residual debt. If it understates collateral value, excess collateral is withdrawn, and the surplus is effectively lost to the position owner.

Recommended Mitigation

Add a staleness check in StrataxOracle::getPrice using the updatedAt timestamp and a configurable heartbeat threshold:

+ mapping(address => uint256) public heartbeats;
+
+ function setHeartbeat(address _token, uint256 _heartbeat) external onlyOwner {
+ heartbeats[_token] = _heartbeat;
+ }
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();
+ (, int256 answer,, uint256 updatedAt,) = priceFeed.latestRoundData();
require(answer > 0, "Invalid price from oracle");
+ require(block.timestamp - updatedAt <= heartbeats[_token], "Stale price data");
price = uint256(answer);
}

Support

FAQs

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

Give us feedback!