Stratax Contracts

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

Permanent Collateral Lock in `unwindPosition` Calculation

Author Revealed upon completion

Root + Impact

Description

  • Users engaging in leveraged positions expect to retrieve all their remaining collateral after fully repaying their debt.

  • The unwinding logic calculates the withdrawal amount based on the liquidation threshold risk parameter instead of the user's actual equity, leaving the surplus safety margin permanently locked in the protocol.

// src/Stratax.sol:575
// @> Root cause: Formula uses liqThreshold to calculate min required collateral, ignoring actual surplus
uint256 collateralToWithdraw = (
_amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);

Risk

Likelihood:

  • The calculation is hardcoded into the only available unwind function path.

  • Every user attempting a full unwind will experience this fund lock.

Impact:

  • Users lose access to significant portions of their principal (often >20%).

  • The protocol fails to fulfill its primary solvency guarantee of returning user funds.

Proof of Concept

This test case demonstrates the issue by simulating a full unwind operation. It sets up a scenario with 100 ETH collateral and 50k USDC debt, which is a healthy position. When the user attempts to fully repay the debt, the protocol calculates the withdrawal amount using the flawed formula. The assertion confirms that the actual withdrawn amount (~31.25 ETH) is significantly less than the user's total collateral (100 ETH), effectively locking the remaining ~68.75 ETH.

function test_ExecutionLocksCollateral() public {
uint256 debtToRepay = 50_000 * 1e6; // $50k Debt
// User has 100 ETH Collateral ($200k value)
// Position is healthy.
vm.prank(stratax.owner());
stratax.unwindPosition(
address(weth),
1,
address(usdc),
debtToRepay,
hex"",
0
);
// Expected Logic in _executeUnwindOperation:
// Returns minimal collateral to cover healthy factor=1.0 at 80% LT
// Formula: Returns only ~31.25 ETH.
// Funds Locked: 100 ETH - 31.25 ETH = 68.75 ETH is left in Aave.
uint256 actualWithdrawn = pool.lastWithdrawAmount();
uint256 restrictedAmount = 31250000000000000000;
assertEq(actualWithdrawn, restrictedAmount, "Should withdraw collateral based on Risk (LT)");
// The remaining collateral is now inaccessible because debt is 0.
}

Recommended Mitigation

Remove the restrictive calculation that bases withdrawal on the Liquidation Threshold. Instead, allow the user to specify the amount of collateral they wish to withdraw in unwindParams, or default to withdrawing the full collateral balance when performing a full debt repayment.

- uint256 collateralToWithdraw = (
- _amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
- ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);
+ // Allow user to specify amount, or default to full withdraw if full repay
+ uint256 collateralToWithdraw = unwindParams.collateralToWithdraw;

Support

FAQs

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

Give us feedback!