Stratax Contracts

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

Wrong Token Passed to `_call1InchSwap` in `_executeOpenOperation` Causes DoS When Router Returns No Data

Author Revealed upon completion

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences


At Stratax.sol:L514:

uint256 returnAmount =
_call1InchSwap(flashParams.oneInchSwapData, flashParams.borrowToken, flashParams.minReturnAmount);

The swap performed here is borrowToken -> collateralToken (i.e., the borrowed tokens from Aave are swapped back to the collateral token to repay the flash loan). The _asset parameter in _call1InchSwap is used in the fallback balance-check path when the 1inch router does not return data (L620-626):

function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
internal
returns (uint256 returnAmount)
{
(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 {
// If no return data, check balance
@> returnAmount = IERC20(_asset).balanceOf(address(this)); // L625
}
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
return returnAmount;
}

After the swap completes:

  • borrowToken balance = 0 (all input tokens were consumed by the swap)

  • collateralToken balance = swap output amount (the tokens received)

Because L514 passes borrowToken as _asset, the fallback path at L625 reads borrowToken.balanceOf(address(this)) = 0, causing returnAmount to be 0. The subsequent require(returnAmount >= _minReturnAmount) at L628 fails, reverting the entire transaction.

Contrast with _executeUnwindOperation (L584), which correctly passes _asset (the flash loan token = the swap output token):

uint256 returnAmount = _call1InchSwap(unwindParams.oneInchSwapData, _asset, unwindParams.minReturnAmount);

In the unwind case, the swap is collateralToken -> _asset, so _asset is indeed the output token, and the balance check works correctly.

Risk

Likelihood:

  • The vulnerability triggers specifically when the 1inch router implementation does not return data from its swap function

  • The 1inch aggregation router has multiple versions and aggregator contracts across chains; some implementations return (uint256 returnAmount, uint256 spentAmount) while others are void functions

  • The protocol explicitly handles this case (the else branch at L623-625 exists precisely for routers that return no data), confirming this is an expected scenario

Impact:

  • createLeveragedPosition() becomes permanently non-functional when paired with a no-return-data router, as every call to _executeOpenOperation will revert

  • Users cannot open new leveraged positions, which is the primary purpose of the protocol

  • Existing positions can still be unwound (L584 is correct), so funds are not permanently locked

  • The issue is a complete DoS of the position-creation functionality, not a fund loss

Proof of Concept

The PoC replicates the _call1InchSwap logic in an external SwapHandler contract and uses a MockNoReturnRouter (void swap function that returns no data). Two tests demonstrate:

  1. test_buggy_passesInputToken_reverts: Passes the input token (borrowToken) as _asset -- the balance check reads 0, causing revert

  2. test_fixed_passesOutputToken_succeeds: Passes the output token (collateralToken) as _asset -- the balance check reads the correct swap output, succeeding

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
/// @dev Minimal ERC20 for testing
contract MockToken {
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
function mint(address to, uint256 amount) external { balanceOf[to] += amount; }
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
}
/// @dev Mock 1inch router: performs a real swap but returns NO data (void function)
contract MockNoReturnRouter {
function swap(address src, address dst, uint256 srcAmt, uint256 dstAmt) external {
MockToken(src).transferFrom(msg.sender, address(this), srcAmt);
MockToken(dst).transfer(msg.sender, dstAmt);
// Crucially: void function = no return data
}
}
/// @dev Replicates _call1InchSwap logic from Stratax.sol L612-630 as an external contract
/// so vm.expectRevert works correctly
contract SwapHandler {
address public router;
constructor(address _router) { router = _router; }
function doSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
external
returns (uint256 returnAmount)
{
(bool success, bytes memory result) = router.call(_swapParams);
require(success, "1inch swap failed");
if (result.length > 0) {
(returnAmount,) = abi.decode(result, (uint256, uint256));
} else {
// _asset balance is checked -- must be the OUTPUT token
returnAmount = MockToken(_asset).balanceOf(address(this));
}
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
}
}
contract WrongSwapTokenTest is Test {
MockToken public borrowToken;
MockToken public collateralToken;
MockNoReturnRouter public router;
SwapHandler public handler;
uint256 constant SWAP_INPUT = 1000e18;
uint256 constant SWAP_OUTPUT = 0.5e18;
uint256 constant MIN_RETURN = 0.48e18;
function setUp() public {
borrowToken = new MockToken();
collateralToken = new MockToken();
router = new MockNoReturnRouter();
// Fund router with output tokens (simulates DEX liquidity)
collateralToken.mint(address(router), 100e18);
// Deploy handler that replicates Stratax's _call1InchSwap logic
handler = new SwapHandler(address(router));
}
function _buildSwapData() internal view returns (bytes memory) {
return abi.encodeWithSelector(
MockNoReturnRouter.swap.selector,
address(borrowToken), address(collateralToken), SWAP_INPUT, SWAP_OUTPUT
);
}
/// @notice BUG: Passing borrowToken (swap INPUT) as _asset.
/// After swap, all borrowToken consumed -> balance = 0 -> returnAmount = 0 -> REVERT
function test_buggy_passesInputToken_reverts() public {
// Give handler input tokens and approve router
borrowToken.mint(address(handler), SWAP_INPUT);
vm.prank(address(handler));
borrowToken.approve(address(router), SWAP_INPUT);
// Passes borrowToken as _asset (THE BUG at Stratax L514)
vm.expectRevert("Insufficient return amount from swap");
handler.doSwap(_buildSwapData(), address(borrowToken), MIN_RETURN);
}
/// @notice FIX: Passing collateralToken (swap OUTPUT) as _asset.
/// After swap, collateralToken balance = 0.5e18 -> returnAmount = 0.5e18 -> SUCCESS
function test_fixed_passesOutputToken_succeeds() public {
// Give handler input tokens and approve router
borrowToken.mint(address(handler), SWAP_INPUT);
vm.prank(address(handler));
borrowToken.approve(address(router), SWAP_INPUT);
// Passes collateralToken as _asset (THE FIX)
uint256 returnAmount = handler.doSwap(_buildSwapData(), address(collateralToken), MIN_RETURN);
assertEq(returnAmount, SWAP_OUTPUT, "Returns correct output amount");
assertEq(borrowToken.balanceOf(address(handler)), 0, "All input tokens consumed");
assertEq(collateralToken.balanceOf(address(handler)), SWAP_OUTPUT, "Output tokens received");
}
}

Recommended Mitigation

Pass _asset (the flash loan token = collateral token = swap output) instead of flashParams.borrowToken at L514:

// Execute swap via 1inch
uint256 returnAmount =
- _call1InchSwap(flashParams.oneInchSwapData, flashParams.borrowToken, flashParams.minReturnAmount);
+ _call1InchSwap(flashParams.oneInchSwapData, _asset, flashParams.minReturnAmount);

Support

FAQs

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

Give us feedback!