Stratax Contracts

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

[H-01] Missing Oracle Freshness Validation Enables Over-Leverage, Unwind DoS, and Increased Liquidation Risk

Author Revealed upon completion
### Description
`StrataxOracle.getPrice()` does not validate the `updatedAt` field returned by Chainlink’s `latestRoundData()`. As a result, Stratax accepts stale price data indefinitely. This leads to three critical impact vectors sharing the same root cause:
* Over-leverage window during stale feed
* Unwind DoS during oracle freeze
* Increased liquidation risk due to cross-oracle mismatch
Because Stratax is a leveraged protocol that performs flash-loan-based leverage sizing and debt unwinding using oracle prices, stale price acceptance directly undermines solvency guarantees.
### Root Cause
`getPrice()` implementation:
```solidity
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);
}
```
The function does not validate that `updatedAt != 0`, `block.timestamp - updatedAt <= MAX_DELAY`, and `answeredInRound >= roundId`. Therefore, stale oracle rounds are accepted as valid prices.
### Impact
1. Over-Leverage Window
If collateral price crashes but oracle has not updated: Collateral value is overestimated, borrow sizing is inflated, position becomes immediately unsafe on Aave, and health factor < 1 shortly after opening
2. Unwind DoS During Oracle Freeze
During `_executeUnwindOperation()`:
```solidity
collateralToWithdraw =
(_amount * debtTokenPrice * ...) /
(collateralTokenPrice * ... * liqThreshold);
```
If collateral price is stale and too high, while the real price is lower. Then insufficient collateral is withdrawn to cover swap + flash loan repayment. This causes `require(returnAmount >= totalDebt, "Insufficient funds");` to revert.
If oracle remains frozen, position cannot be unwound. Therefore, a liveness failure.
3. Increased Liquidation Risk
Stratax uses its own oracle for sizing, while Aave uses its own internal oracle.
If Stratax oracle is stale but Aave oracle updates:
* Stratax assumes position is safe
* Aave evaluates lower collateral value
* Liquidation becomes possible
* This creates cross-oracle inconsistency risk.
### Recommended Mitigation
Reconfigure the `getPrice()` function as shown below;
```diff
function getPrice(address _token) public view returns (uint256 price) {
address priceFeedAddress = priceFeeds[_token];
require(priceFeedAddress != address(0), "Price feed not set");
AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeedAddress);
+ // Capture updatedAt
+ (, int256 answer, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(answer > 0, "Invalid price");
+ // Check if the price is older than a specific threshold (e.g., 1 hour)
+ // The threshold depends on the specific token's heartbeat
+ require(block.timestamp - updatedAt < 3600, "Stale price");
price = uint256(answer);
}
```
### Proof of Concept
Create a test file `test/unit/StrataxPoC.t.sol`. Then add the following and run: forge test --match-test test_GetPriceAcceptsStaleData -vv --fork-url $ETH_RPC_URL
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import {StrataxOracle} from "src/StrataxOracle.sol";
contract StrataxStalenessPoC is Test {
StrataxOracle oracle;
// Mainnet ETH/USD Chainlink Feed
address constant ETH_FEED = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
string ETH_RPC_URL = vm.envString("ETH_RPC_URL");
function setUp() public {
// Create a fork of Ethereum Mainnet
vm.createSelectFork(ETH_RPC_URL);
oracle = new StrataxOracle();
oracle.setPriceFeed(WETH, ETH_FEED);
}
function test_GetPriceAcceptsStaleData() public {
// 1. Get the current "fresh" data from the real feed
(,,, uint256 updatedAt, ) = oracle.getRoundData(WETH);
uint256 priceBefore = oracle.getPrice(WETH);
console.log("Current Oracle UpdatedAt:", updatedAt);
console.log("Current Price:", priceBefore);
// 2. Warp time forward by 7 days (604,800 seconds)
// The real ETH heartbeat is 1 hour (3600s). After 7 days, this price is VERY stale.
vm.warp(block.timestamp + 604800);
// 3. Call the affected contract directly
// This SHOULD revert if the contract had staleness checks.
uint256 priceAfter = oracle.getPrice(WETH);
// 4. Assert that the contract still returns the price from 7 days ago
assertEq(priceBefore, priceAfter);
console.log("Time has passed, but getPrice() still returns:", priceAfter);
console.log("The contract failed to detect the data is 7 days old.");
}
}
```
NB: Before running this test, create .env and your ETH_RPC_URL.
I mistakenly deleted the template and tried undoing it but it didn't work.

Support

FAQs

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

Give us feedback!