Stratax Contracts

First Flight #57
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: medium
Valid

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

### 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.
Updates

Lead Judging Commences

izuman Lead Judge 16 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Price feed has no staleness check

StrataxOracle contract fails to check if the price is stale, which can mess up swap calculations.

Support

FAQs

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

Give us feedback!