Stratax Contracts

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

Arbitrary External Call via User-Supplied 1inch Swap Calldata

Author Revealed upon completion

Root + Impact

Description

  • The Stratax contract integrates with the 1inch aggregation router to perform token swaps during leveraged position creation and unwinding. The _call1InchSwap function is used to execute swaps by forwarding arbitrary calldata to the router via a low-level .call(). Before this call, the contract approves the full borrow/collateral amount to the oneInchRouter address.

  • The _oneInchSwapData parameter passed to createLeveragedPosition and unwindPosition is directly forwarded to the 1inch router without any validation of its contents. Since the router contract exposes multiple functions beyond just swapping (including functions that can transfer tokens to arbitrary destinations), a malicious or compromised owner could craft calldata that redirects the approved tokens to an attacker address instead of performing a legitimate swap.

function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
internal
returns (uint256 returnAmount)
{
// Execute the 1inch swap using low-level call with the calldata from the API
@> (bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);
@> require(success, "1inch swap failed");
// Decode the return amount from the swap
if (result.length > 0) {
(returnAmount,) = abi.decode(result, (uint256, uint256));
} else {
// If no return data, check balance
@> returnAmount = IERC20(_asset).balanceOf(address(this));
}
// Sanity check
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
return returnAmount;
}

Risk

Likelihood:

  • The owner is the sole caller of createLeveragedPosition and unwindPosition, and they supply the _oneInchSwapData parameter directly. No on-chain validation occurs on the calldata structure, function selector, or destination address encoded within it.

  • The 1inch aggregation router inherently supports multiple function signatures beyond swap(), including functions that can route tokens to arbitrary receivers. The contract approves the full token amount before the .call() is executed.

Impact:

  • All tokens approved to the 1inch router (up to the full borrow or collateral amount) can be redirected to any address, resulting in complete loss of the position's funds.

  • When the result is empty (the else branch), the return amount is determined by the contract's balance of _asset, which can be manipulated if the calldata performs an unexpected operation that does not return tokens but the contract already holds a balance of that asset.

Proof of Concept

The following shows how _executeOpenOperation approves the full borrowAmount to the 1inch router and then passes user-controlled oneInchSwapData directly into a low-level .call(). Because no function selector or destination whitelist is enforced, the caller can encode any router function — including those that transfer the approved tokens to an arbitrary receiver rather than swapping them back to the contract.

// In _executeOpenOperation, the full borrowAmount is approved to oneInchRouter
// then _call1InchSwap is called with user-supplied bytes:
function _executeOpenOperation(...) internal returns (bool) {
// ...
// Step 3: Swap borrowed tokens via 1inch to get back the collateral token
IERC20(flashParams.borrowToken).approve(address(oneInchRouter), flashParams.borrowAmount);
// The oneInchSwapData is user-supplied, forwarded directly to router.call()
uint256 returnAmount = _call1InchSwap(flashParams.oneInchSwapData, ...);
// ...
}
// An attacker-crafted _oneInchSwapData could encode:
// - A call to a router function that transfers approved tokens to an attacker address
// - A swap with attacker-controlled destination (receiver != address(this))
// - Any arbitrary function on the 1inch router, since no selector validation occurs

Recommended Mitigation

Restrict the allowed function selectors to known safe 1inch swap functions (swap, unoswap) and replace the return-value decoding with a balance-delta check. This eliminates both the arbitrary-call vector and the unreliable else branch that falls back to balanceOf.

function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
internal
returns (uint256 returnAmount)
{
+ // Validate the function selector matches an allowed 1inch swap function
+ bytes4 selector;
+ assembly {
+ selector := mload(add(_swapParams, 32))
+ }
+ require(
+ selector == IAggregationRouter.swap.selector ||
+ selector == IAggregationRouter.unoswap.selector,
+ "Invalid swap selector"
+ );
+
+ 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;
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
return returnAmount;
}

Support

FAQs

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

Give us feedback!