Stratax Contracts

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

Surplus Tokens From Unwind Auto-Supplied to Aave With No Direct Withdrawal Path

Author Revealed upon completion

Surplus Tokens From Unwind Auto-Supplied to Aave With No Direct Withdrawal Path

Description

  • In _executeUnwindOperation, after the flash loan is repaid, any surplus debt tokens are automatically supplied to Aave as a new supply position:

// Stratax.sol:590-595
// Note: There might be other positions open, so unwinding one position will increase the health factor
if (returnAmount - totalDebt > 0) {
IERC20(_asset).approve(address(aavePool), returnAmount - totalDebt);
aavePool.supply(_asset, returnAmount - totalDebt, address(this), 0);
}
  • The contract has no direct withdraw() function to retrieve tokens supplied to Aave. The only available function is recoverTokens(), which can transfer aTokens out of the contract — but aTokens cannot be redeemed for the underlying asset without calling aavePool.withdraw(), which the contract doesn't expose.

// These functions exist:
function recoverTokens(address _token, uint256 _amount) external onlyOwner { ... } // Can transfer aTokens
function createLeveragedPosition(...) public onlyOwner { ... } // Only creates positions
function unwindPosition(...) external onlyOwner { ... } // Only unwinds positions
// This function does NOT exist:
// function withdrawFromAave(address _asset, uint256 _amount) external onlyOwner { ... }

Risk

Likelihood:

  • Surplus tokens are generated on every unwind operation where the swap returns more debt tokens than needed for flash loan repayment

  • The collateral-to-withdraw calculation uses the liquidation threshold (liqThreshold), which provides a buffer above 100% of the debt value. After swapping this buffer, surplus tokens are expected

  • Example: With liqThreshold = 85%, the collateral withdrawn is ~117.6% of the debt value. After swap slippage, ~115% remains. The ~15% surplus is auto-supplied to Aave

Impact:

  • Over multiple unwind operations, significant value accumulates in Aave supply positions that the contract cannot directly access

  • The owner cannot withdraw these surplus funds through any existing function

  • The recoverTokens() function can transfer aTokens to the owner, but the owner then needs to interact with Aave directly to redeem them — a suboptimal and error-prone workaround

  • Funds are not permanently lost, but they are operationally inaccessible through the Stratax contract

Proof of Concept

How the issue manifests:

  1. Owner has a leveraged position: 10 ETH collateral, 8000 USDC debt

  2. Owner calls unwindPosition to close the position

  3. Flash loan: 8000 USDC → repay Aave debt → withdraw ~9400 USDC worth of ETH (117.6% of debt at liqThreshold 85%)

  4. Swap ETH → USDC: receives ~9200 USDC (accounting for slippage)

  5. Flash loan repayment: 8000 USDC + ~7.2 USDC premium = ~8007.2 USDC

  6. Surplus: 9200 - 8007.2 = ~1192.8 USDC is auto-supplied to Aave

  7. Owner now has ~1192.8 USDC worth of aUSDC in the contract's Aave position

  8. There is no function to withdraw this USDC from Aave

Expected outcome: ~1192.8 USDC is locked in an Aave supply position with no direct withdrawal mechanism available through the Stratax contract.

Recommended Mitigation

The root cause is that the contract creates Aave supply positions (both intentionally during leveraged position creation and incidentally during unwind surplus handling) but lacks a general-purpose withdrawal function.

Primary fix — Add a direct Aave withdrawal function:

function withdrawFromAave(address _asset, uint256 _amount, address _to) external onlyOwner {
require(_to != address(0), "Invalid address");
aavePool.withdraw(_asset, _amount, _to);
}

Why this works:

  • Provides the owner with a direct mechanism to withdraw any tokens supplied to Aave, including surplus from unwind operations

  • onlyOwner ensures only the position owner can withdraw

  • The _to parameter allows withdrawing directly to the owner's wallet without an intermediate step

  • This complements the existing recoverTokens() for tokens held directly by the contract (not in Aave)

Alternative fix — Send surplus to owner instead of supplying to Aave:

if (returnAmount - totalDebt > 0) {
IERC20(_asset).transfer(owner, returnAmount - totalDebt);
}

Tradeoff: The alternative is simpler but loses the "supply to Aave to improve health factor" behavior for multi-position scenarios. The primary fix (adding withdrawFromAave) is more flexible and preserves the existing behavior while giving the owner control.

Support

FAQs

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

Give us feedback!