Stratax Contracts

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

`_call1InchSwap` Trusts Return Data Without Balance Verification

Author Revealed upon completion

_call1InchSwap Trusts Return Data Without Balance Verification

Description

  • The _call1InchSwap function executes a low-level call to the 1inch router using arbitrary calldata provided by the owner. It determines the swap output amount by decoding the return data from the external call, rather than measuring the actual token balance change in the contract.

function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
internal returns (uint256 returnAmount)
{
// @audit Arbitrary calldata — can call ANY function on the 1inch router
(bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);
require(success, "1inch swap failed");
if (result.length > 0) {
// @audit returnAmount decoded from return data, not verified against actual balance
(returnAmount,) = abi.decode(result, (uint256, uint256));
} else {
// Fallback: check balance (only used when no return data)
returnAmount = IERC20(_asset).balanceOf(address(this));
}
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
}
  • Two issues compound here:

    1. returnAmount is trusted from external return data without independent verification

    2. _swapParams is raw calldata that can invoke any function on the 1inch router, not just swap() — there is no function selector validation

Risk

Likelihood:

  • The 1inch router is trusted infrastructure and should return accurate values under normal operation

  • However, calling arbitrary functions on the router via raw calldata introduces an unpredictable surface area

  • If the router is upgraded or a non-swap function is called, the return data format may not match the expected (uint256, uint256) decoding

Impact:

  • The returnAmount value flows into surplus calculations at lines 529-531 (_executeOpenOperation) and 592-594 (_executeUnwindOperation)

  • If returnAmount is higher than actual tokens received: aavePool.supply() will be called with more tokens than the contract holds, causing a revert

  • If returnAmount is lower than actual tokens received: surplus tokens are not supplied to Aave, potentially leaving dust in the contract

  • Mitigating factor: Flash loan atomicity provides a strong backstop — if the contract can't repay the flash loan, the entire transaction reverts, preventing fund loss

Proof of Concept

How the issue manifests:

  1. Owner crafts _oneInchSwapData that calls a non-standard function on the 1inch router

  2. The function succeeds but returns data in a different format than (uint256, uint256)

  3. abi.decode(result, (uint256, uint256)) produces an incorrect returnAmount (could be garbage or zero)

  4. If the decoded value is very high, the surplus calculation returnAmount - totalDebt overestimates, and aavePool.supply() reverts due to insufficient balance

  5. The entire flash loan transaction reverts — no fund loss but the operation fails unexpectedly

Expected outcome: The operation reverts with an opaque Aave error rather than the expected "Insufficient return amount from swap" message, making debugging difficult.

Recommended Mitigation

The root cause is that the function relies on external return data for a critical accounting value instead of independently measuring the actual token balance change. The fix should use a before/after balance measurement pattern.

Primary fix — Measure actual balance change:

function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
internal returns (uint256 returnAmount)
{
uint256 balanceBefore = IERC20(_asset).balanceOf(address(this));
(bool success,) = address(oneInchRouter).call(_swapParams);
require(success, "1inch swap failed");
returnAmount = IERC20(_asset).balanceOf(address(this)) - balanceBefore;
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
}

Why this works:

  • The balance change is measured independently from the external call's return data — it reflects actual tokens received regardless of what the router returns

  • Eliminates the dependency on the return data format, making the function resilient to router upgrades or non-standard function calls

  • The balanceOf calls add ~2,600 gas each (warm SLOAD) — negligible compared to the swap gas cost

Support

FAQs

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

Give us feedback!