Stratax Contracts

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

`_collateralToWithdraw` Parameter in `unwindPosition` Is Completely Unused

Author Revealed upon completion

_collateralToWithdraw Parameter in unwindPosition Is Completely Unused

Description

  • unwindPosition accepts _collateralToWithdraw as its second parameter (line 238) and stores it in UnwindParams.collateralToWithdraw (line 245).

  • However, _executeUnwindOperation never reads unwindParams.collateralToWithdraw. Instead, it independently calculates the withdrawal amount from oracle prices and Aave's liquidation threshold:

// Stratax.sol:236 — Parameter accepted from user
function unwindPosition(
address _collateralToken,
uint256 _collateralToWithdraw, // @audit This parameter is stored but NEVER used
address _debtToken,
uint256 _debtAmount,
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
) external onlyOwner {
UnwindParams memory params = UnwindParams({
collateralToken: _collateralToken,
collateralToWithdraw: _collateralToWithdraw, // @audit Stored here...
debtToken: _debtToken,
debtAmount: _debtAmount,
oneInchSwapData: _oneInchSwapData,
minReturnAmount: _minReturnAmount
});
// ...
}
// Stratax.sol:575-577 — This CALCULATED value is used instead
uint256 collateralToWithdraw = (
_amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);
// @audit unwindParams.collateralToWithdraw is NEVER referenced

Risk

Likelihood:

  • Every call to unwindPosition is affected — the dead parameter exists on every invocation

  • Users who call the companion calculateUnwindParams() to get the "correct" collateralToWithdraw value will pass it in, believing it controls the withdrawal amount

Impact:

  • Users have a false sense of control over position unwinding — the parameter they provide has zero effect

  • The actual withdrawal amount is determined entirely by oracle prices and Aave's liquidation threshold at execution time, which may differ significantly from when the user computed their parameters

  • This creates a trust assumption gap: the user believes they are specifying how much collateral to withdraw, but the protocol ignores their input entirely

Proof of Concept

How the issue manifests:

  1. Owner calls calculateUnwindParams(collateralToken, borrowToken) which returns (collateralToWithdraw, debtAmount)

  2. Owner calls unwindPosition(collateralToken, collateralToWithdraw, debtToken, debtAmount, swapData, minReturn) with the calculated value

  3. Inside _executeUnwindOperation, the collateralToWithdraw from the user is completely ignored

  4. A fresh calculation using current oracle prices determines the actual withdrawal amount

  5. If prices have moved between the calculateUnwindParams call and the unwindPosition execution, the actual withdrawal amount differs from what the user expected — with no warning or revert

PoC code:

function testExploit_UnusedCollateralToWithdraw() public {
// Both calls below produce identical behavior despite different _collateralToWithdraw values
stratax.unwindPosition(weth, 1e18, usdc, debtAmount, swapData, minReturn);
stratax.unwindPosition(weth, 999999e18, usdc, debtAmount, swapData, minReturn);
// The _collateralToWithdraw parameter (1e18 vs 999999e18) has ZERO effect on execution
}

Expected outcome: Both calls execute identically — the _collateralToWithdraw parameter has no effect on the withdrawal amount or any other behavior.

Recommended Mitigation

The root cause is that the parameter was added to the function signature but the internal logic was never updated to use it (or vice versa — the internal logic was changed to calculate dynamically but the parameter was never removed).

Option A (Recommended) — Remove the dead parameter:

function unwindPosition(
address _collateralToken,
address _debtToken,
uint256 _debtAmount,
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
) external onlyOwner {

Why this works: Removing the unused parameter eliminates confusion and aligns the function signature with its actual behavior. This also fixes the interface mismatch (H-003) since IStratax.unwindPosition already has 5 parameters.

Option B — Use the user-provided value:

// In _executeUnwindOperation, use the user's value instead of calculating
withdrawnAmount = aavePool.withdraw(
unwindParams.collateralToken,
unwindParams.collateralToWithdraw, // Use user-provided value
address(this)
);

Tradeoff: Option B gives the user explicit control but requires them to calculate correctly. Option A is simpler and lets the protocol always compute the optimal amount. Option A is preferred for safety and simplicity.

Support

FAQs

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

Give us feedback!