Stratax Contracts

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

[H-04] Incorrect Asset Passed to _call1InchSwap Breaks Swap Accounting and Flash Loan Repayment Logic

Author Revealed upon completion

Root + Impact

Description

  • In _executeOpenOperation, the protocol calls _call1InchSwap with:

    _call1InchSwap(
    flashParams.oneInchSwapData,
    flashParams.borrowToken,
    flashParams.minReturnAmount
    );

    However, the swap being executed converts:

    borrowToken → collateralToken (_asset)

    The _asset parameter (flash loan asset) represents the collateral token, not the borrow token.

    Passing flashParams.borrowToken instead of the collateral token into _call1InchSwap causes swap output accounting to be performed against the wrong token.s

function _executeOpenOperation(address _asset, uint256 _amount, uint256 _premium, bytes calldata _params)
internal
returns (bool)
{
//......
// Step 3: Swap borrowed tokens via 1inch to get back the collateral token
IERC20(flashParams.borrowToken).approve(address(oneInchRouter), flashParams.borrowAmount);
// Execute swap via 1inch
uint256 returnAmount =
@> _call1InchSwap(flashParams.oneInchSwapData, flashParams.borrowToken, flashParams.minReturnAmount);
// Ensure all borrowed tokens were used in the swap
uint256 afterSwapBorrowTokenbalance = IERC20(flashParams.borrowToken).balanceOf(address(this));
require(afterSwapBorrowTokenbalance == prevBorrowTokenBalance, "Borrow token left in contract");
//....rest of the code
return true;
}

Risk

Likelihood:

  • This vulnerability will always occur when the low level call returns no data

  • Incorrect returnAmount

    • returnAmount reflects the wrong token balance as borrowToken is passed instead of collateralToken.

    • May equal zero even when swap succeeded.

    • May include unrelated borrow token balances.

Impact:

  • Return amount would read wrong balance

  • and break flash loan repayment logic

Proof of Concept

Add the following test case to test/fork/Stratax.t.soland run: forge test --match-test test_Exploit_Confirmed_WrongAssetAccounting -vvv

function test_Exploit_Confirmed_WrongAssetAccounting() public {
// 1. Setup: Contract has 200* 10**18 USDC "Dust"
uint256 massiveUSDC = 200 * 10**18; // This is a "fake" high USDC balance
deal(USDC, address(stratax), massiveUSDC);
// 2. Mocks: Prevent Aave logic from reverting our execution flow
vm.mockCall(AAVE_POOL, abi.encodeWithSelector(IPool.supply.selector), abi.encode(true));
vm.mockCall(AAVE_POOL, abi.encodeWithSelector(IPool.borrow.selector), abi.encode(true));
vm.mockCall(AAVE_POOL, abi.encodeWithSelector(IPool.getUserAccountData.selector),
abi.encode(0, 0, 0, 0, 0, 2e18)); // Health Factor = 2.0 (Healthy)
// 3. Mock 1inch: Return SUCCESS but NO DATA
// This forces: returnAmount = IERC20(_asset).balanceOf(address(this))
bytes memory swapCalldata = abi.encodeWithSignature("swap()");
vm.mockCall(INCH_ROUTER, swapCalldata, "");
// 4. Parameters: Expect 100 WETH back from the swap.
// BUG: Contract will check USDC balance instead of WETH.
Stratax.FlashLoanParams memory params = Stratax.FlashLoanParams({
collateralToken: WETH,
collateralAmount: 0,
borrowToken: USDC,
borrowAmount: 0, // Keep at 0 to pass the "leftover" check easily
oneInchSwapData: swapCalldata,
minReturnAmount: 100 ether
});
// 5. Execute as Aave Pool
vm.startPrank(AAVE_POOL);
bool success = stratax.executeOperation(
WETH,
0.1 ether, // Small flash loan
0, // No premium for simplicity
address(stratax),
abi.encode(Stratax.OperationType.OPEN, address(this), params)
);
vm.stopPrank();
// 6. PROOF
assertTrue(success, "Vulnerability Confirmed: Accepted USDC balance as WETH return amount!");
console.log("The contract reported success because it looked at USDC balance instead of WETH.");
}

Recommended Mitigation

Change flashParams.borrowToken to flashParams.collateralToken as shown below

function _executeOpenOperation(address _asset, uint256 _amount, uint256 _premium, bytes calldata _params)
internal
returns (bool)
{
(, address user, FlashLoanParams memory flashParams) =
abi.decode(_params, (OperationType, address, FlashLoanParams));
// Step 1: Supply flash loan amount + user's extra amount to Aave as collateral
uint256 totalCollateral = _amount + flashParams.collateralAmount;
IERC20(_asset).approve(address(aavePool), totalCollateral);
aavePool.supply(_asset, totalCollateral, address(this), 0);
//......
// Step 3: Swap borrowed tokens via 1inch to get back the collateral token
IERC20(flashParams.borrowToken).approve(address(oneInchRouter), flashParams.borrowAmount);
// Execute swap via 1inch
uint256 returnAmount =
- _call1InchSwap(flashParams.oneInchSwapData, flashParams.borrowToken, flashParams.minReturnAmount);
+ _call1InchSwap(flashParams.oneInchSwapData, flashParams.collateralToken, flashParams.minReturnAmount);
// Ensure all borrowed tokens were used in the swap
uint256 afterSwapBorrowTokenbalance = IERC20(flashParams.borrowToken).balanceOf(address(this));
require(afterSwapBorrowTokenbalance == prevBorrowTokenBalance, "Borrow token left in contract");
//.....rest of the code
}

Support

FAQs

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

Give us feedback!