Stratax Contracts

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

User Collateral/Profit Can Become Permanently Locked During Unwind

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: _executeUnwindOperation is supposed to close a leveraged position by withdrawing enough collateral to repay the debt and flash loan, then return any remaining profit to the user.

  • Issue: The contract only withdraws collateral sufficient to cover the debt, and does not return the surplus collateral or profit to the user. Any remaining funds are permanently locked in the contract because there is no public withdraw function for users.

// Root cause in the codebase with @> marks to highlight the relevant section
// Step 2: Calculate and withdraw only the collateral that backed the repaid debt
@> uint256 collateralToWithdraw = (
@> _amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
@> ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);
@> withdrawnAmount = aavePool.withdraw(unwindParams.collateralToken, collateralToWithdraw, address(this));
// Step 4: Repay flash loan
@> uint256 totalDebt = _amount + _premium;
@> require(returnAmount >= totalDebt, "Insufficient funds to repay flash loan");
// Any leftover tokens are only optionally supplied back to Aave, never sent to the user
@> if (returnAmount - totalDebt > 0) {
@> IERC20(_asset).approve(address(aavePool), returnAmount - totalDebt);
@> aavePool.supply(_asset, returnAmount - totalDebt, address(this), 0);
@> }

Risk

Likelihood:

  • Occurs whenever the calculated collateral to withdraw is less than the actual collateral held by the user.

  • Also happens when the flash loan repayment or 1inch swap only partially succeeds, leaving surplus collateral inside the contract.

Impact:

  • User funds (capital + profits) are permanently locked and cannot be recovered.

  • Causes a loss of trust in the Stratax platform, as unwinding positions may leave funds stranded or fail silently.

Proof of Concept

// Example: User deposited 1000 USDC as collateral
// Flash loan + swap only allows 900 USDC to repay the loan
// Remaining 100 USDC stays permanently in the contract
address collateralToken = USDC;
uint256 userCollateral = 1000 ether;
uint256 debtAmount = 500 ether;
bytes memory dummySwap = hex"00";
stratax.unwindPosition(collateralToken, userCollateral, USDC, debtAmount, dummySwap, 0);
// After execution, 100 USDC remains in contract
// User cannot withdraw this 100 USDC
uint256 strandedBalance = IERC20(collateralToken).balanceOf(address(stratax));
assert(strandedBalance == 100 ether);

The PoC shows that after a standard unwind operation, any leftover collateral that exceeds the amount needed to repay the flash loan is not returned to the user. Since the contract does not provide a public withdrawal mechanism, these funds remain trapped forever inside the contract. This applies to both profit and excess collateral.

Recommended Mitigation

- if (returnAmount - totalDebt > 0) {
- IERC20(_asset).approve(address(aavePool), returnAmount - totalDebt);
- aavePool.supply(_asset, returnAmount - totalDebt, address(this), 0);
- }
+ if (returnAmount - totalDebt > 0) {
+ IERC20(_asset).transfer(user, returnAmount - totalDebt);
+ }

The mitigation ensures that any leftover collateral or profit after repaying the flash loan is immediately returned to the user who initiated the unwind. This prevents funds from being permanently locked in the contract. It also improves trust and reduces centralization risk, as users can retrieve all of their assets without relying on the contract owner.

Support

FAQs

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

Give us feedback!