Stratax Contracts

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

Missing slippage/safety validation in createLeveragedPosition()

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);
}
Updates

Lead Judging Commences

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

Need borrow safety margin and slippage protection

Parameters should not be hardcoded and user defined. Similar to the finding the calculateUnwindParams, however the impact is lower in this situation.

Support

FAQs

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

Give us feedback!