Stratax Contracts

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

L04. Borrow Token Same as Collateral Token Edge Case Not Prevented

Author Revealed upon completion

Root + Impact

Description

createLeveragedPosition applies no check that _borrowToken differs from _flashLoanToken (the collateral token).

If both are the same asset, the protocol flash-loans token A, supplies it as collateral, borrows token A again (from the same Aave supply), then swaps token A → token A via 1inch (a no-op swap at best, a slippage-generating swap at worst), and repays the flash loan with the borrowed amount.

The net result is an identical collateral and debt position denominated in the same asset — Aave's effective leverage in this case is zero, since any price move affects both collateral value and debt value equally.

// src/Stratax.sol:314-344 — createLeveragedPosition
function createLeveragedPosition(
address _flashLoanToken, // collateral token (e.g., WETH)
uint256 _flashLoanAmount,
uint256 _collateralAmount,
address _borrowToken, // @> no check that _borrowToken != _flashLoanToken
uint256 _borrowAmount,
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
) public onlyOwner {
require(_collateralAmount > 0, "Collateral Cannot be Zero");
// @> Missing: require(_borrowToken != _flashLoanToken, "...")

When _borrowToken == _flashLoanToken:

  1. Flash loan borrows N WETH

  2. aavePool.supply(WETH, N + userCollateral) — supplies WETH collateral

  3. aavePool.borrow(WETH, borrowAmount) — borrows WETH against WETH collateral

  4. 1inch swap of WETH → WETH: at best a zero-fee identity swap returning exactly the input; at worst a slippage-consuming swap

  5. Flash loan repaid with borrowed WETH — net state: same WETH collateral, WETH debt, no actual leverage

Risk

Likelihood:

  • Off-chain tooling that allows any ERC-20 for both collateral and borrow token can produce this misconfiguration by user error

  • Automated position managers that do not validate token pair distinctness before calling can trigger this path

  • The missing guard is a single-line oversight

Impact:

  • The position costs the user the flash loan fee, gas, and potential 1inch slippage with no economic benefit — all capital is locked in Aave in a hedged position where leverage = 1x (no leverage)

  • The event LeveragePositionCreated is emitted with what appears to be a valid position, misleading off-chain accounting

  • If the 1inch swap for an identical pair incurs significant slippage (e.g., WETH → WETH routed through a pool at non-1:1 rate), the returnAmount >= totalDebt check may fail, wasting all the gas of the flash loan callback

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";
import {IERC20} from "forge-std/interfaces/IERC20.sol";
import {ConstantsEtMainnet} from "../Constants.t.sol";
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
contract BorrowTokenEqualsCollateralTest is Test, ConstantsEtMainnet {
Stratax stratax;
address owner = address(0x123);
function setUp() public {
vm.createSelectFork(vm.envString("ETH_RPC_URL"));
Stratax impl = new Stratax();
UpgradeableBeacon beacon = new UpgradeableBeacon(address(impl), address(this));
bytes memory initData = abi.encodeWithSelector(
Stratax.initialize.selector,
AAVE_POOL, AAVE_PROTOCOL_DATA_PROVIDER,
address(0xdead), address(0), address(0)
);
BeaconProxy proxy = new BeaconProxy(address(beacon), initData);
stratax = Stratax(address(proxy));
stratax.transferOwnership(owner);
}
function test_same_token_for_flash_loan_and_borrow_accepted() public {
deal(WETH, owner, 1 ether);
vm.startPrank(owner);
IERC20(WETH).approve(address(stratax), 1 ether);
// @> _flashLoanToken == _borrowToken == WETH — no protocol guard
// Expected: should revert with a clear error; actual: accepted by createLeveragedPosition
// (will eventually fail inside Aave or the swap, but with no clear error)
vm.expectRevert(); // fails later, not at input validation
stratax.createLeveragedPosition(
WETH, // @> collateral token = WETH
1 ether,
1 ether,
WETH, // @> borrow token = WETH (same as collateral — no guard)
0.8 ether,
bytes(""),
0
);
vm.stopPrank();
}
}

Recommended Mitigation

Add an explicit distinctness check at the entry point:

// src/Stratax.sol — createLeveragedPosition
require(_collateralAmount > 0, "Collateral Cannot be Zero");
require(_borrowAmount > 0, "Borrow amount Cannot be Zero");
require(_flashLoanAmount > 0, "Flash loan amount Cannot be Zero");
+ require(_borrowToken != _flashLoanToken, "Borrow token must differ from collateral token");

Support

FAQs

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

Give us feedback!