Stratax Contracts

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

Multiple Positions Overlap - No Position Tracking

Author Revealed upon completion

Description

  • The Stratax contract allows users to create leveraged positions using flash loans and Aave V3. Users should be able to create multiple positions and unwind them individually.

  • The contract has no mechanism to track individual positions. When createLeveragedPosition() is called multiple times, all positions overlap on the same Aave account. The calculateUnwindParams() function returns total collateral and debt across all positions, making it impossible to close individual positions. Funds become permanently locked.

// src/Stratax.sol
contract Stratax is Initializable {
// @> No position tracking: no Position[] array, no mapping, no positionId counter
function createLeveragedPosition(...) public onlyOwner {
// @> Can be called multiple times
// @> No position ID returned
// @> All positions overlap on same Aave account
IERC20(_flashLoanToken).transferFrom(msg.sender, address(this), _collateralAmount);
// ... creates position without tracking
}
function calculateUnwindParams(address _collateralToken, address _debtToken)
returns (uint256 collateralAmount, uint256 debtAmount)
{
// @> No position ID parameter - returns TOTAL of ALL positions
(uint256 totalCollateralBase, uint256 totalDebtBase,,,,) =
aavePool.getUserAccountData(address(this));
// @> Cannot specify which position to unwind
}
}

Risk

Likelihood: High

  • Owner creates Position #1 with 1000 USDC at 3x leverage (0.66 WETH debt on Aave)

  • Owner creates Position #2 with 800 USDC at 2.5x leverage (0.40 WETH debt added to same Aave account)

  • Owner attempts to close Position #1 but calculateUnwindParams() returns total debt (1.06 WETH) not individual position debt (0.66 WETH)

  • Unwinding Position #1 reverts because Aave validates health factor against total debt, and withdrawing Position #1's collateral alone drops health factor below minimum

  • This occurs through normal protocol usage without requiring any attack

Impact: Critical

  • Permanent Fund Locking: Cannot close individual positions. Attempting to unwind one position reverts. Funds locked on Aave.

  • Loss of Risk Management: High-leverage position cannot be closed without closing all positions together.

  • Forced Liquidation: When one position becomes risky, entire account faces liquidation.

  • Contract Bricking: Creating opposite positions (USDC→WETH, then WETH→USDC) makes all unwind operations impossible.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {Stratax} from "../../src/Stratax.sol";
import {StrataxOracle} from "../../src/StrataxOracle.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";
/**
* @title Practical Minimal POC
* @notice Bug Bounty: Multiple Positions Overlap - No Position Tracking
*/
contract PracticalMinimalPOC is Test {
Stratax public stratax;
StrataxOracle public oracle;
address aavePool = address(0x100);
address aaveDataProvider = address(0x101);
address oneInchRouter = address(0x102);
address usdc = address(0x200);
address weth = address(0x201);
address debtToken = address(0x402);
uint256 position1DebtAmount = 0.66e18;
uint256 position2DebtAmount = 0.4e18;
function setUp() public {
oracle = new StrataxOracle();
stratax = new Stratax();
stratax.initialize(aavePool, aaveDataProvider, oneInchRouter, usdc, address(oracle));
vm.mockCall(address(0x300), abi.encodeWithSignature("latestRoundData()"),
abi.encode(uint80(1), int256(100000000), uint256(block.timestamp), uint256(block.timestamp), uint80(1)));
vm.mockCall(address(0x300), abi.encodeWithSignature("decimals()"), abi.encode(uint8(8)));
oracle.setPriceFeed(usdc, address(0x300));
oracle.setPriceFeed(weth, address(0x300));
vm.mockCall(usdc, abi.encodeWithSelector(IERC20.transferFrom.selector), abi.encode(true));
vm.mockCall(usdc, abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true));
vm.mockCall(usdc, abi.encodeWithSelector(IERC20.decimals.selector), abi.encode(uint8(6)));
vm.mockCall(weth, abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true));
vm.mockCall(weth, abi.encodeWithSelector(IERC20.balanceOf.selector), abi.encode(0));
vm.mockCall(weth, abi.encodeWithSelector(IERC20.decimals.selector), abi.encode(uint8(18)));
vm.mockCall(aavePool, abi.encodeWithSignature("supply(address,uint256,address,uint16)"), abi.encode());
vm.mockCall(aavePool, abi.encodeWithSignature("borrow(address,uint256,uint256,uint16,address)"), abi.encode());
vm.mockCall(aavePool, abi.encodeWithSignature("getUserAccountData(address)"),
abi.encode(uint256(5000e8), uint256(2000e8), uint256(0), uint256(0), uint256(0), uint256(150e16)));
vm.mockCall(aaveDataProvider, abi.encodeWithSignature("getReserveConfigurationData(address)"),
abi.encode(uint256(0), uint256(8000), uint256(8500), uint256(10500), uint256(0), false, false, false, false, false));
vm.mockCall(aaveDataProvider, abi.encodeWithSignature("getReserveTokensAddresses(address)"),
abi.encode(address(0x400), address(0x401), debtToken));
vm.mockCall(oneInchRouter, abi.encodeWithSignature("swap(address,(address,address,address,address,uint256,uint256,uint256),bytes,bytes)"),
abi.encode(uint256(2100e6), uint256(0)));
}
function test_MultiplePositions_NoTracking() public {
// Create Position #1
stratax.createLeveragedPosition(usdc, 2000e6, 1000e6, weth, 0.66e18, "", 0);
// Mock debt token balance after position 1
vm.mockCall(debtToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(stratax)),
abi.encode(position1DebtAmount));
// Get unwind params for position 1
(uint256 collateral1, uint256 debt1) = stratax.calculateUnwindParams(usdc, weth);
// Create Position #2 - overlaps on same Aave account
stratax.createLeveragedPosition(usdc, 1200e6, 800e6, weth, 0.4e18, "", 0);
// Mock TOTAL debt after both positions
uint256 totalDebt = position1DebtAmount + position2DebtAmount;
vm.mockCall(debtToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(stratax)),
abi.encode(totalDebt));
// Try to get unwind params for "position 1" again
(uint256 collateral2, uint256 debt2) = stratax.calculateUnwindParams(usdc, weth);
// BUG: debt2 returns TOTAL debt, not just position 1's debt
assertGt(debt2, debt1, "Debt should increase after 2nd position");
assertEq(debt2, totalDebt, "Returns TOTAL debt of both positions");
// IMPACT: Cannot unwind position 1 individually - parameters are for ALL positions
}
function test_NoPositionIdParameter() public {
// Mock debt token balance
vm.mockCall(debtToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(stratax)),
abi.encode(1e18));
// PROOF: calculateUnwindParams has no position ID parameter
// Function signature: calculateUnwindParams(address, address)
// Missing: position ID, position index, or any identifier
(uint256 collateral, uint256 debt) = stratax.calculateUnwindParams(usdc, weth);
// Returns totals - no way to get values for a specific position
assertGt(debt, 0, "Returns total debt without position ID");
}
}

Recommended Mitigation

// src/Stratax.sol
contract Stratax is Initializable {
+ struct Position {
+ address collateralToken;
+ uint256 collateralAmount;
+ address debtToken;
+ uint256 debtAmount;
+ bool isActive;
+ }
+
+ Position[] public positions;
+ uint256 public nextPositionId;
function createLeveragedPosition(
address _flashLoanToken,
uint256 _flashLoanAmount,
uint256 _collateralAmount,
address _borrowToken,
uint256 _borrowAmount,
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
- ) public onlyOwner {
+ ) public onlyOwner returns (uint256 positionId) {
+ positionId = nextPositionId++;
+ positions.push(Position({
+ collateralToken: _flashLoanToken,
+ collateralAmount: _collateralAmount,
+ debtToken: _borrowToken,
+ debtAmount: _borrowAmount,
+ isActive: true
+ }));
// ... rest of function
}
- function calculateUnwindParams(address _collateralToken, address _debtToken)
+ function calculateUnwindParams(uint256 _positionId)
public view
returns (uint256 collateralAmount, uint256 debtAmount)
{
+ require(_positionId < positions.length, "Invalid position");
+ Position memory position = positions[_positionId];
+ require(position.isActive, "Position not active");
+ return (position.collateralAmount, position.debtAmount);
- (uint256 totalCollateralBase, uint256 totalDebtBase,,,,) =
- aavePool.getUserAccountData(address(this));
}
function unwindPosition(
+ uint256 _positionId,
// ... other parameters
) public onlyOwner {
+ require(_positionId < positions.length, "Invalid position");
+ Position storage position = positions[_positionId];
+ require(position.isActive, "Position not active");
// ... unwind logic
+ position.isActive = false;
}
}

Support

FAQs

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

Give us feedback!