Stratax Contracts

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

_call1InchSwap Balance Fallback Overcounts Pre-Existing Contract Balance

Author Revealed upon completion

Root + Impact

Location: src/Stratax.sol:621-626

Description

_call1InchSwap uses two paths to determine the swap return amount. When the 1inch call returns data it decodes returnAmount correctly. When no data is returned, it reads the contract's full balance of _asset — which includes any tokens already held before the swap, not just proceeds from this swap.

// src/Stratax.sol:621-626
if (result.length > 0) {
(returnAmount,) = abi.decode(result, (uint256, uint256));
} else {
returnAmount = IERC20(_asset).balanceOf(address(this)); // @> full balance, not swap proceeds
// @> pre-existing balance inflates returnAmount, bypassing slippage check
}
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");

Risk

Likelihood:

  • Contract accumulates _asset balance from the surplus re-supply pattern (M-2) and prior incomplete unwind operations

  • Some 1inch router function selectors (e.g. uniswapV3Swap) return no data, triggering the else branch

Impact:

  • A swap returning zero proceeds passes the minReturnAmount slippage check using pre-existing balance

  • Flash loan repayment silently consumes pre-existing funds rather than swap proceeds, draining the contract's reserve

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
// Numeric trace showing slippage check bypassed by pre-existing balance:
//
// State before call:
// stratax.balanceOf(USDC) = 500e6 ← surplus from prior unwind (M-2)
//
// Owner calls createLeveragedPosition with calldata for uniswapV3Swap (returns no data)
// Swap executes but liquidity is insufficient — 0 USDC received by contract
//
// In _call1InchSwap():
// (bool success, bytes memory result) = oneInchRouter.call(swapParams)
// success = true (uniswapV3Swap call succeeded)
// result = "" (no return data from this selector)
//
// // else branch executes:
// returnAmount = IERC20(USDC).balanceOf(address(this))
// = 500e6 ← pre-existing balance, NOT swap proceeds
//
// require(500e6 >= minReturnAmount) ← minReturnAmount was set to 400e6
// → PASSES — slippage check bypassed
//
// Flash loan repaid from the 500e6 pre-existing USDC
// Swap produced 0 tokens — position is opened without the swap actually working

Recommended Mitigation

Snapshot the _asset balance immediately before the 1inch call and subtract it from the post-call balance. This isolates only the tokens received from this specific swap, preventing any pre-existing balance from inflating the measured return amount.

+ uint256 balanceBefore = IERC20(_asset).balanceOf(address(this));
(bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);
require(success, "1inch swap failed");
if (result.length > 0) {
(returnAmount,) = abi.decode(result, (uint256, uint256));
} else {
- returnAmount = IERC20(_asset).balanceOf(address(this));
+ returnAmount = IERC20(_asset).balanceOf(address(this)) - balanceBefore;
}

Support

FAQs

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

Give us feedback!