Stratax Contracts

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

H01. Caller-Supplied Prices Bypass Oracle in calculateOpenParams

Author Revealed upon completion

Root + Impact

Description

  • calculateOpenParams fetches oracle prices only when the caller passes zero for collateralTokenPrice or borrowTokenPrice. Any non-zero value supplied by the caller is accepted without cross-checking against the oracle. This allows a caller to use arbitrary prices to size positions — artificially inflating or deflating the computed flashLoanAmount and borrowAmount.

// src/Stratax.sol:392-404
// @> Price is only fetched from oracle if caller passes zero
// Any non-zero value is accepted blindly, no oracle validation
if (details.collateralTokenPrice == 0) {
require(strataxOracle != address(0), "Oracle not set");
details.collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(details.collateralToken);
}
require(details.collateralTokenPrice > 0, "Collateral token price must be > 0");
if (details.borrowTokenPrice == 0) {
require(strataxOracle != address(0), "Oracle not set");
details.borrowTokenPrice = IStrataxOracle(strataxOracle).getPrice(details.borrowToken);
}
require(details.borrowTokenPrice > 0, "Borrow token price must be > 0");

The calculateOpenParams function is a view used to size positions before calling createLeveragedPosition. A caller who supplies an inflated collateralTokenPrice or a deflated borrowTokenPrice receives output parameters (flashLoanAmount, borrowAmount) that differ from what the oracle would compute. The caller then passes those parameters to createLeveragedPosition, which executes the flash loan and borrow without any on-chain oracle validation of its own.

Additionally, the Stratax oracle and the Aave internal oracle are independent systems. Aave's liquidation engine uses its own oracle (from AaveOracle); Stratax uses StrataxOracle. These can diverge. A position sized using Stratax oracle prices may be immediately under- or over-collateralised by Aave's assessment.

Risk

Likelihood:

  • calculateOpenParams is a public view function; any caller can supply arbitrary collateralTokenPrice and borrowTokenPrice values

  • The function is intended as an off-chain helper, but its outputs are directly used as inputs to createLeveragedPosition — there is no separation enforced

  • Oracle divergence between StrataxOracle and the Aave oracle is always present in live markets; the gap widens during volatility

Impact:

  • A caller can pass collateralTokenPrice = 1 (near zero) to maximise borrowAmount in the formula, opening a position with a borrow far exceeding what the oracle would allow

  • With the inflated borrow, the 1inch swap receives more tokens than expected; the position opens with excess leverage, reducing the health factor below safe margins immediately after creation

  • An under-priced borrowTokenPrice causes the pre-flight flash-loan-repay check to fail, preventing valid positions from being opened (denial of service for position creation)

  • The Aave/Stratax oracle divergence means positions calculated as healthy by Stratax may be liquidated by Aave without any on-chain warning

Proof of Concept

The caller price bypass is demonstrable without a fork. The check if (details.collateralTokenPrice == 0) is the only oracle fetch gate:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {Stratax} from "../../src/Stratax.sol";
import {ConstantsEtMainnet} from "../Constants.t.sol";
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
contract CallerPriceBypassTest is Test, ConstantsEtMainnet {
Stratax stratax;
function setUp() public {
vm.createSelectFork(vm.envString("ETH_RPC_URL"));
Stratax impl = new Stratax();
UpgradeableBeacon beacon = new UpgradeableBeacon(address(impl), address(this));
bytes memory initData = abi.encodeWithSelector(
Stratax.initialize.selector,
AAVE_POOL, AAVE_PROTOCOL_DATA_PROVIDER,
address(0xdead), address(0), address(0)
);
BeaconProxy proxy = new BeaconProxy(address(beacon), initData);
stratax = Stratax(address(proxy));
}
function test_inflated_collateral_price_accepted() public view {
// Build a trade detail struct with a caller-supplied inflated collateral price
// True WETH price ≈ 3000e8; we supply 999_999_999e8 (arbitrarily large)
Stratax.TradeDetails memory details = Stratax.TradeDetails({
collateralToken: WETH,
collateralAmount: 1 ether,
collateralTokenPrice: 999_999_999e8, // @> arbitrarily inflated price — not checked against oracle
collateralTokenDec: 18,
borrowToken: USDC,
borrowTokenPrice: 1e8, // actual USDC price
borrowTokenDec: 6,
desiredLeverage: 2e4 // 2x leverage (LEVERAGE_PRECISION = 1e4)
});
// @> calculateOpenParams accepts the inflated price without any oracle cross-check
(uint256 flashLoanAmount, uint256 borrowAmount) = stratax.calculateOpenParams(details);
// borrowAmount is computed using the inflated collateral price
// This is >> what the oracle would compute, creating a massively oversized borrow
console.log("Flash loan amount:", flashLoanAmount);
console.log("Borrow amount (inflated):", borrowAmount);
// If caller now passes these to createLeveragedPosition with matching 1inch data,
// the position opens with a borrow amount far beyond what is safe
assertTrue(borrowAmount > 0, "Oversized borrow calculated from inflated price");
}
}

Recommended Mitigation

Remove the caller-supplied price parameters from calculateOpenParams. Always source prices from the oracle:

// src/Stratax.sol — TradeDetails struct
struct TradeDetails {
address collateralToken;
uint256 collateralAmount;
- uint256 collateralTokenPrice; // remove
uint8 collateralTokenDec;
address borrowToken;
- uint256 borrowTokenPrice; // remove
uint8 borrowTokenDec;
uint256 desiredLeverage;
}
// src/Stratax.sol — calculateOpenParams
- if (details.collateralTokenPrice == 0) {
- require(strataxOracle != address(0), "Oracle not set");
- details.collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(details.collateralToken);
- }
- require(details.collateralTokenPrice > 0, "Collateral token price must be > 0");
+ require(strataxOracle != address(0), "Oracle not set");
+ uint256 collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(details.collateralToken);
- if (details.borrowTokenPrice == 0) {
- require(strataxOracle != address(0), "Oracle not set");
- details.borrowTokenPrice = IStrataxOracle(strataxOracle).getPrice(details.borrowToken);
- }
- require(details.borrowTokenPrice > 0, "Borrow token price must be > 0");
+ uint256 borrowTokenPrice = IStrataxOracle(strataxOracle).getPrice(details.borrowToken);

For the Aave/Stratax oracle divergence risk, add a configurable maximum acceptable divergence check comparing strataxOracle.getPrice() against IAaveOracle(aavePool.ADDRESSES_PROVIDER().getPriceOracle()).getAssetPrice().

Support

FAQs

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

Give us feedback!