Stratax Contracts

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

Arbitrary External Call via Unvalidated 1inch Swap Data

Author Revealed upon completion

Root + Impact

Location: src/Stratax.sol:617

Description

  • _call1InchSwap executes a raw .call() to the 1inch router using entirely user-supplied calldata. While the destination contract is the trusted 1inch router, the function selector and all parameters are unconstrained.

  • The 1inch swap() function accepts a dstReceiver field in its descriptor struct. If this is set to an address other than address(this), swap proceeds are redirected off-contract.

// src/Stratax.sol:617
(bool success, bytes memory result) = address(oneInchRouter).call(_swapParams); // @> _swapParams entirely user-controlled
require(success, "1inch swap failed");
// @> no validation that dstToken, dstReceiver, or srcToken match expected values

Risk

Likelihood:

  • The owner constructs 1inch API calldata off-chain with no on-chain validation of its contents before execution

  • If ownership is transferred to a multisig or governance contract, signers may not inspect raw calldata byte-by-byte

Impact:

  • Swap proceeds redirected to an attacker-controlled address — contract cannot repay flash loan

  • Malicious calldata can invoke any function on the 1inch router, including ones that do not perform a swap

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {Stratax} from "../../src/Stratax.sol";
contract ArbitrarySwapDataPoCTest is Test {
Stratax stratax;
address attacker = address(0xBAD);
function test_swapProceedsRedirectedToAttacker() public {
// Craft 1inch swap() calldata with dstReceiver = attacker instead of address(stratax)
// 1inch swap() descriptor: (srcToken, dstToken, srcReceiver, dstReceiver, amount, minReturn, flags)
bytes memory maliciousSwapData = abi.encodeWithSignature(
"swap(address,(address,address,address,address,uint256,uint256,uint256),bytes)",
address(0), // executor
// SwapDescription struct with dstReceiver = attacker
abi.encode(
address(borrowToken), // srcToken
address(collateral), // dstToken
address(stratax), // srcReceiver
attacker, // @> dstReceiver set to attacker
borrowAmount,
minReturn,
0
),
""
);
vm.prank(stratax.owner());
// Transaction reverts (flash loan unpaid) but demonstrates no validation exists
vm.expectRevert("Insufficient funds to repay flash loan");
stratax.createLeveragedPosition(
address(collateral), flashAmount, collateralAmount,
address(borrowToken), borrowAmount, maliciousSwapData, 0
);
// In a scenario where attacker pre-funds the contract, this succeeds
}
}

Recommended Mitigation

+ // Decode 1inch swap descriptor and validate critical fields
+ (address srcToken, address dstToken,, address dstReceiver,,,,) =
+ abi.decode(_swapParams[4:], (address, address, address, address, uint256, uint256, uint256, bytes));
+ require(dstToken == _asset, "Wrong destination token");
+ require(dstReceiver == address(this), "Wrong swap receiver");
(bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);

Support

FAQs

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

Give us feedback!