Users relying on calculateUnwindParams() to determine unwind parameters will receive incorrect collateral withdrawal estimates, causing incorrectly constructed 1inch swap data that leads to transaction reverts or stranded tokens in the contract.
The Stratax contract provides calculateUnwindParams() as a public view function intended for users to preview the values needed when calling unwindPosition(). Separately, _executeUnwindOperation() is the internal function that performs the actual unwind logic during the Aave flash loan callback.
Two distinct problems exist in this code path. First, the _collateralToWithdraw parameter accepted by unwindPosition() and stored in the UnwindParams struct is dead code. The _executeUnwindOperation() function never reads unwindParams.collateralToWithdraw; instead, it recalculates the collateral withdrawal amount independently using its own formula.
Second, and more critically, the formula used by calculateUnwindParams() differs materially from the formula used by _executeUnwindOperation(). The view function performs a simple price conversion with a 5% slippage buffer, while the execution function performs a price conversion scaled by LTV_PRECISION / liqThreshold. For typical Aave V3 assets where liqThreshold ranges from 8000 to 8500, the execution formula produces values approximately 17-25% larger than the simple price conversion, resulting in a systematic ~15% discrepancy after accounting for the 5% buffer.
The calculateUnwindParams() function computes collateral to withdraw as follows:
The _executeUnwindOperation() function computes collateral to withdraw using a different formula:
A concrete numerical example illustrates the discrepancy. Assume a position with 10,000 USDC debt (6 decimals, price $1) collateralised by WETH (18 decimals, price $2,000) with a liqThreshold of 8250 (82.5%).
The calculateUnwindParams() formula produces: (1e8 * 10000e6 * 1e18) / (2000e8 * 1e6) = 5e18 (5 WETH), then multiplied by 1050/1000 yielding 5.25e18 (5.25 WETH).
The _executeUnwindOperation() formula produces: (10000e6 * 1e8 * 1e18 * 1e4) / (2000e8 * 1e6 * 8250) = 6.06e18 (6.06 WETH).
The result is a 15.4% discrepancy. A user who calls calculateUnwindParams() and constructs 1inch swap data to swap 5.25 WETH will find that _executeUnwindOperation() actually withdraws 6.06 WETH. The swap calldata was prepared for 5.25 WETH input but the contract holds 6.06 WETH, leading to either the swap only converting 5.25 WETH and leaving 0.81 WETH stranded in the contract, or the transaction reverting if the swap router enforces exact input amounts.
This issue has a medium impact as the primary consequences are transaction reverts or stranded tokens. Stranded tokens can be recovered by the owner via recoverTokens(), limiting direct fund loss. However, failed unwind transactions during volatile market conditions could prevent timely position closure, exposing positions to liquidation before they can be unwound.
This issue has a high likelihood as any user following the intended workflow of calling calculateUnwindParams() before unwindPosition() will always encounter this discrepancy. The issue is deterministic and not dependent on edge cases or special conditions.
Align the formula in calculateUnwindParams() with the formula used in _executeUnwindOperation(). The execution function formula, which accounts for the liquidation threshold, should be the canonical formula. Update calculateUnwindParams() to fetch liqThreshold from Aave and apply the same LTV_PRECISION / liqThreshold scaling factor instead of the arbitrary 5% buffer.
Additionally, either consume the unwindParams.collateralToWithdraw value within _executeUnwindOperation() instead of recalculating it, or remove the unused collateralToWithdraw field from the UnwindParams struct and the corresponding _collateralToWithdraw parameter from unwindPosition() to eliminate the dead code and avoid confusion.
To ensure long-term consistency between the two code paths, extract the shared collateral calculation into a single internal helper function and call it from both calculateUnwindParams() and _executeUnwindOperation().
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.