Stratax Contracts

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

M01. Chainlink Oracle Has No Staleness Check on updatedAt

Author Revealed upon completion

Root + Impact

Description

  • StrataxOracle.getPrice() calls latestRoundData() and discards the updatedAt timestamp. No comparison against block.timestamp is made. If a Chainlink price feed stops updating (keeper failure, network congestion, deprecated feed), the oracle silently returns the last known price indefinitely.

  • Stale prices propagate directly into leverage calculations in calculateOpenParams and into the collateral withdrawal formula in _executeUnwindOperation, both of which are executed on-chain with real funds.

// src/StrataxOracle.sol:70-73
function getPrice(address _token) public view returns (uint256 price) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeedAddress);
// @> updatedAt (4th return value) is silently discarded
(, int256 answer,,,) = priceFeed.latestRoundData();
require(answer > 0, "Invalid price from oracle");
price = uint256(answer);
}

Risk

Likelihood:

  • Chainlink feeds have historically paused during extreme market volatility (e.g., March 2020 ETH crash) — exactly when stale prices are most dangerous

  • Feeds on L2 networks stop updating entirely when the sequencer is offline, a predictable and recurring event on Arbitrum and Optimism

Impact:

  • A stale inflated debt token price causes _executeUnwindOperation to over-calculate collateralToWithdraw, withdrawing more collateral than necessary and potentially leaving the remaining position below the liquidation threshold

  • A stale deflated collateral price causes calculateOpenParams to under-estimate position size, producing positions that open at unsafe effective leverage

Proof of Concept

The following test demonstrates that the oracle accepts an answer whose updatedAt is arbitrarily old without reverting.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {StrataxOracle} from "../../src/StrataxOracle.sol";
contract MockStaleFeed {
int256 public answer;
uint256 public updatedAt;
constructor(int256 _answer, uint256 _updatedAt) {
answer = _answer;
updatedAt = _updatedAt;
}
function decimals() external pure returns (uint8) { return 8; }
function latestRoundData() external view returns (
uint80, int256, uint256, uint256, uint80
) {
// @> Returns a price with updatedAt set 30 days in the past
return (1, answer, 0, updatedAt, 1);
}
}
contract StalenessCheckTest is Test {
StrataxOracle oracle;
address token = address(0xBEEF);
function setUp() public {
oracle = new StrataxOracle();
// Feed whose last update was 30 days ago
MockStaleFeed staleFeed = new MockStaleFeed(
3000e8, // price: $3000
block.timestamp - 30 days // @> 30 days stale
);
oracle.setPriceFeed(token, address(staleFeed));
}
function test_stale_price_accepted_without_revert() public view {
// @> This succeeds even though the price is 30 days old
uint256 price = oracle.getPrice(token);
assertEq(price, 3000e8, "Stale price returned without revert");
// No staleness revert — protocol accepts the price as current
}
}

The mock feed returns a 30-day-old answer. getPrice returns it successfully without reverting, confirming the absence of any updatedAt validation.

Recommended Mitigation

Add per-feed maximum staleness thresholds stored at _setPriceFeed time and validated in getPrice. Use Chainlink's published heartbeat values (ETH/USD: 3600s, USDC/USD: 86400s).

// src/StrataxOracle.sol
+ mapping(address => uint256) public maxStaleness; // token => seconds
function _setPriceFeed(address _token, address _priceFeed) internal {
// ...existing validation...
priceFeeds[_token] = _priceFeed;
+ // default staleness — caller should override via dedicated setter
+ maxStaleness[_token] = 3600;
}
function getPrice(address _token) public view returns (uint256 price) {
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(block.timestamp - updatedAt <= maxStaleness[_token], "Stale price");
price = uint256(answer);
}

Support

FAQs

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

Give us feedback!