Stratax Contracts

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

Arbitrary calldata execution via 1inch router

Author Revealed upon completion

Description

  • When integrating a DEX aggregator, the contract should constrain what can be executed—e.g., call a specific, typed function (swap) and verify that the swap parameters (tokenIn, tokenOut, receiver, amounts) match the protocol’s expectations (e.g., borrowToken → collateralToken, receiver = address(this)). This prevents misuse of approvals and avoids calling unintended code paths.

  • Stratax forwards user‑supplied, opaque calldata directly to the 1inch router via a low‑level call without decoding or validating it. Any router function selector can be invoked, and Stratax will trust the router’s returned bytes as (returnAmount, spentAmount) (or fall back to a balance probe). This creates a broad class of risks: the router can be instructed to move approved tokens in unexpected ways, route to unexpected receivers, or execute a function that doesn’t conform to the assumed ABI.
    Where: _call1InchSwap uses address(oneInchRouter).call(_swapParams); callers pass oneInchSwapData straight from createLeveragedPosition/unwindPosition into the flash‑loan path without any semantic checks.

// Stratax.sol
function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
internal returns (uint256 returnAmount)
{
// @> Unrestricted low-level call with opaque calldata
(bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);
require(success, "1inch swap failed");
// @> Trusts returned encoding (uint256, uint256); otherwise probes a single token balance
if (result.length > 0) {
(returnAmount,) = abi.decode(result, (uint256, uint256));
} else {
returnAmount = IERC20(_asset).balanceOf(address(this));
}
require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
return returnAmount;
}

Risk

Likelihood: Medium

  • Operator error / malformed calldata is common with off‑chain quoting. Any mis‑encoded call or selector drift on router upgrades will hit this path.

  • Defense‑in‑depth gap: Even if 1inch router is trustworthy, initialization mistakes (wrong router address on some chain) or future router changes can turn this into a serious foot‑gun. Over time, such misconfigurations will occur.

Impact: Medium

  • Loss of approved funds / mis‑routing: Calldata may instruct the router to send funds to a third party or skip returning the intended destination token.

  • Hard‑to‑debug reverts: The code trusts the router’s return tuple; if it returns crafted bytes or none, the subsequent logic can miscompute returnAmount and fail late (e.g., flash‑loan repayment revert), causing gas loss and operational instability.

Proof of Concept

  • Conceptual pseudocode:

// Preconditions:
// - Stratax approved `borrowToken` (OPEN) / `collateralToken` (UNWIND) to oneInchRouter.
// - Caller supplies arbitrary `oneInchSwapData` (no on-chain validation).
// Example misuse through opaque calldata:
bytes memory data = abi.encodeWithSelector(
IAggregationRouter.unoswapTo.selector,
attacker, // dstReceiver NOT address(this)
borrowToken, // srcToken approved by Stratax
amountIn, // spends the full approved amount
minOut, // arbitrary
poolsPath // arbitrary pools
);
// In flash-loan callback:
(bool ok, bytes ret) = address(oneInchRouter).call(data); // succeeds
// Router transfers `borrowToken` from Stratax to `attacker` per its own logic.
// Stratax trusts `ret` (or probes wrong token), then attempts to repay the flash loan.
// Repayment fails or funds are misrouted -> revert (DoS) or loss depending on router path.

Recommended Mitigation

  • Replace raw .call with a typed interface call like IAggregationRouterV5.swap(Executor, Desc, data).

  • Decode and verify invariants before calling:

    • desc.srcToken == expectedInput (e.g., borrowToken in OPEN).

    • desc.dstToken == expectedOutput (e.g., flash‑loan asset _asset in OPEN).

    • desc.dstReceiver == address(this).

    • desc.amount <= approvedAmount and flags within an allowlist.

  • Reject any other selector/shape (keep a selector allowlist if you must support multiple router entrypoints).

- (bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);
+ // Parse selector and only allow known entrypoints (e.g., swap)
+ bytes4 sel = bytes4(_swapParams);
+ require(sel == IAggregationRouter.swap.selector, "Unsupported 1inch selector");
+ // Decode Desc fields and assert invariants (src/dst/receiver/amount)
+ (IAggregationExecutor ex, IAggregationRouter.SwapDescription memory desc, bytes memory data) =
+ abi.decode(_swapParams[4:], (IAggregationExecutor, IAggregationRouter.SwapDescription, bytes));
+ require(desc.srcToken == expectedSrc, "Bad srcToken");
+ require(desc.dstToken == expectedDst, "Bad dstToken");
+ require(desc.dstReceiver == address(this), "Bad receiver");
+ (uint256 returnAmount, ) = IAggregationRouter(oneInchRouter).swap(ex, desc, data);

Support

FAQs

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

Give us feedback!