Stratax Contracts

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

Missing slippage/safety validation in createLeveragedPosition()

Author Revealed upon completion

Description

  • When opening a leveraged position, the contract should ensure the requested borrow amount is safely below the Aave LTV limit with an internal safety margin (e.g., BORROW_SAFETY_MARGIN = 95% of the theoretical maximum). This prevents opening positions that are immediately liquidatable or that ride the knife‑edge of liquidation.

  • createLeveragedPosition accepts _flashLoanAmount and _borrowAmount directly from the caller and never validates that _borrowAmount fits within the safe bound derived from (collateral, LTV, prices) using the contract’s own margin (BORROW_SAFETY_MARGIN). While calculateOpenParams computes a safe _borrowAmount, its use is optional; the function that actually mutates state (createLeveragedPosition) does not enforce any bound. A mistaken bot/operator, or a malicious owner, can pass a _borrowAmount right at (or infinitesimally below) the LTV limit, yielding a position with HF ≈ (LT / LTV), i.e., dangerously close to 1, and immediately exposed to liquidation on tiny price moves.

// Stratax.sol (excerpt)
function createLeveragedPosition(
address _flashLoanToken,
uint256 _flashLoanAmount,
uint256 _collateralAmount,
address _borrowToken,
uint256 _borrowAmount, // @> Taken at face value
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
) public onlyOwner {
require(_collateralAmount > 0, "Collateral Cannot be Zero");
// @> No check that _borrowAmount respects Aave LTV with BORROW_SAFETY_MARGIN
IERC20(_flashLoanToken).transferFrom(msg.sender, address(this), _collateralAmount);
// Encodes params and triggers flash loan...
aavePool.flashLoanSimple(address(this), _flashLoanToken, _flashLoanAmount, encodedParams, 0);
}

Risk

Likelihood: High

  • In practice, integrators/bots will compute the numbers off‑chain. Operational slips or rounding errors will occur over time, especially across chains/feeds.

  • The owner is the “position owner” in this design; mistakes in automation or parameterization are not rare for power users during volatile markets.

Impact: High

  • Immediate liquidation risk / unhealthy positions: Borrow set at or near the LTV edge yields HF ≈ liquidationThreshold / ltv (often ≈ 1.05–1.10), i.e., one small tick from liquidation.

  • Bad user experience / unexpected reverts: If _borrowAmount slightly overshoots Aave limits, the transaction reverts deep in the flow (after the flash‑loan began), wasting gas and increasing operational fragility.

Proof of Concept

  • Copy test test_CreateLeveragedPositionTooHighBorrowRevertsNoEarlyCheck() to test/fork/Stratax.t.sol: inside the StrataxForkTest contract.

  • Copy mock contract MockOneInchRouterSafety to test/fork/Stratax.t.sol: after the StrataxForkTest contract.

  • Run command forge test --mt test_CreateLeveragedPositionTooHighBorrowRevertsNoEarlyCheck --via-ir -vv.

function test_CreateLeveragedPositionTooHighBorrowRevertsNoEarlyCheck() public {
// Same setup as above
MockOneInchRouterSafety mock = new MockOneInchRouterSafety();
Stratax impl = new Stratax();
UpgradeableBeacon b = new UpgradeableBeacon(address(impl), address(this));
bytes memory initData = abi.encodeWithSelector(
Stratax.initialize.selector, AAVE_POOL, AAVE_PROTOCOL_DATA_PROVIDER, address(mock), USDC, address(strataxOracle)
);
BeaconProxy p = new BeaconProxy(address(b), initData);
Stratax s = Stratax(address(p));
s.transferOwnership(ownerTrader);
uint256 collateralAmount = 1_000 * 1e6;
(uint256 flSafe, ) = s.calculateOpenParams(
Stratax.TradeDetails({
collateralToken: USDC,
borrowToken: WETH,
desiredLeverage: 30_000,
collateralAmount: collateralAmount,
collateralTokenPrice: 0, borrowTokenPrice: 0, collateralTokenDec: 6, borrowTokenDec: 18
})
);
// Fetch ltv for max formula
(bool ok, bytes memory out) = address(AAVE_PROTOCOL_DATA_PROVIDER).staticcall(
abi.encodeWithSignature("getReserveConfigurationData(address)", USDC)
);
require(ok, "aave cfg fail");
( , uint256 ltv, , , , , , , , ) = abi.decode(out, (uint256,uint256,uint256,uint256,uint256,bool,bool,bool,bool,bool));
uint256 pUSDC = strataxOracle.getPrice(USDC);
uint256 pWETH = strataxOracle.getPrice(WETH);
uint8 decUSDC = IERC20(USDC).decimals();
uint8 decWETH = IERC20(WETH).decimals();
uint256 totalCollateral = collateralAmount + flSafe;
uint256 totalCollUSD = (totalCollateral * pUSDC) / (10 ** decUSDC);
uint256 borrowUSDMax = (totalCollUSD * ltv) / 10_000;
uint256 borrowMax = (borrowUSDMax * (10 ** decWETH)) / pWETH;
// Exceed by 1 wei to force an Aave revert (no early guard in Stratax)
uint256 borrowTooHigh = borrowMax + 1;
// Fund router to repay flash loan
uint256 premium = (flSafe * s.flashLoanFeeBps()) / s.FLASHLOAN_FEE_PREC();
uint256 minReturn = flSafe + premium;
deal(USDC, address(mock), minReturn);
bytes memory openSwapData = abi.encode(WETH, USDC, minReturn);
deal(USDC, ownerTrader, collateralAmount);
vm.startPrank(ownerTrader);
IERC20(USDC).approve(address(s), collateralAmount);
// Reverts deep in Aave.borrow (gas-wasteful) because Stratax does not pre-validate
vm.expectRevert();
s.createLeveragedPosition(USDC, flSafe, collateralAmount, WETH, borrowTooHigh, openSwapData, minReturn);
vm.stopPrank();
}
/// @dev Mock router that pulls all approved `inputToken` from msg.sender
/// and pays back `outputAmount` of `outputToken`.
/// Calldata: abi.encode(inputToken, outputToken, outputAmount)
contract MockOneInchRouterSafety {
function _pullAllApproved(address token, address from) internal returns (uint256 spent) {
uint256 allowance = IERC20(token).allowance(from, address(this));
uint256 bal = IERC20(token).balanceOf(from);
spent = allowance < bal ? allowance : bal;
if (spent > 0) require(IERC20(token).transferFrom(from, address(this), spent), "pull fail");
}
fallback() external payable {
(address inputToken, address outputToken, uint256 outputAmount) =
abi.decode(msg.data, (address, address, uint256));
uint256 spent = _pullAllApproved(inputToken, msg.sender);
require(IERC20(outputToken).transfer(msg.sender, outputAmount), "pay fail");
bytes memory out = abi.encode(outputAmount, spent);
assembly { return(add(out, 0x20), mload(out)) }
}
}

Output:

[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/fork/Stratax.t.sol:StrataxForkTest
[PASS] test_CreateLeveragedPositionTooHighBorrowRevertsNoEarlyCheck() (gas: 6469986)
Logs:
Available swap data files: 3
Randomly selected block: 24329390
Current fork block number is: 24329390
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.11s (33.41ms CPU time)
Ran 1 test suite in 1.12s (1.11s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

Enforce the safety bound inside createLeveragedPosition before initiating the flash loan:

  • Recompute the maximum safe borrow on‑chain using the oracle and Aave config:

function createLeveragedPosition(
address _flashLoanToken,
uint256 _flashLoanAmount,
uint256 _collateralAmount,
address _borrowToken,
uint256 _borrowAmount,
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
) public onlyOwner {
require(_collateralAmount > 0, "Collateral Cannot be Zero");
+
+ // --- Enforce safe borrow bound ---
+ // 1) Fetch Aave LTV for collateral asset
+ (, uint256 ltv,,,,,,,,) = aaveDataProvider.getReserveConfigurationData(_flashLoanToken);
+ require(ltv > 0, "Asset not usable as collateral");
+
+ // 2) Oracle prices (8 decimals)
+ require(strataxOracle != address(0), "Oracle not set");
+ uint256 collPrice = IStrataxOracle(strataxOracle).getPrice(_flashLoanToken);
+ uint256 debtPrice = IStrataxOracle(strataxOracle).getPrice(_borrowToken);
+ require(collPrice > 0 && debtPrice > 0, "Invalid oracle prices");
+
+ // 3) Decimals
+ uint8 collDec = IERC20(_flashLoanToken).decimals();
+ uint8 debtDec = IERC20(_borrowToken).decimals();
+
+ // 4) Compute max SAFE borrow with BORROW_SAFETY_MARGIN applied
+ uint256 totalCollateral = _collateralAmount + _flashLoanAmount;
+ uint256 totalCollUSD = (totalCollateral * collPrice) / (10 ** collDec);
+ uint256 borrowUSD = (totalCollUSD * ltv * BORROW_SAFETY_MARGIN) / (LTV_PRECISION * 10_000);
+ uint256 maxSafeBorrow = (borrowUSD * (10 ** debtDec)) / debtPrice;
+ require(_borrowAmount <= maxSafeBorrow, "Borrow exceeds safety margin");
+
+ // 5) Optional: ensure swap minReturn can repay flash-loan + fee
+ uint256 premium = (_flashLoanAmount * flashLoanFeeBps) / FLASHLOAN_FEE_PREC;
+ require(_minReturnAmount >= _flashLoanAmount + premium, "minReturn too low");
IERC20(_flashLoanToken).transferFrom(msg.sender, address(this), _collateralAmount);
// continue with flash loan...
aavePool.flashLoanSimple(address(this), _flashLoanToken, _flashLoanAmount, encodedParams, 0);
}

Support

FAQs

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

Give us feedback!