Stratax Contracts

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

_executeUnwindOperation uses liqThreshold instead of ltv, causing user to receive 6.25% less collateral on every unwind

Author Revealed upon completion

Description

  • Stratax::_executeUnwindOperation calculates how much collateral to withdraw from Aave after repaying a portion of debt. The formula is intended to withdraw exactly the collateral that backed the repaid debt, scaled by the asset's LTV ratio. The code comment on line 574 explicitly names the denominator variable as ltv.

  • The destructuring of getReserveConfigurationData captures the third return value (index 2 = liquidationThreshold) and names it liqThreshold, while the second return value (index 1 = ltv) is skipped. For WETH on Aave (ltv=7500, liqThreshold=8000), using liqThreshold in the denominator inflates it by 8000/7500, which reduces collateralToWithdraw by exactly 6.25% on every call. The shortfall remains locked in Aave on the contract's Aave balance and is not returned to the user.

// Comment says "ltv", but code reads liqThreshold (index 2, not index 1):
// @> (,, uint256 liqThreshold,,,,,,,) =
aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);
// Calculate collateral to withdraw: (...) / (collateralPrice * debtDec * ltv) ← comment
uint256 collateralToWithdraw = (
_amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
// @> ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);

Risk

Likelihood:

  • Every call to unwindPosition or executeOperation on any collateral token where ltv != liqThreshold triggers the loss — which is true for all standard Aave v3 assets (e.g. WETH: ltv=7500, liqThreshold=8000).

  • The loss is deterministic and accumulates with every partial unwind; a user performing 10 partial unwinds on a $10,000 position loses approximately $83 in collateral.

Impact:

  • The user receives 6.25% less collateral than they are entitled to on each unwind. The shortfall (e.g. ~0.028 WETH per 1000 USDC of debt) stays on the contract's Aave deposit and cannot be individually recovered.

  • Cumulative losses scale linearly with the number of unwind iterations and the size of the position, representing a direct and silent financial loss for every user who closes or partially unwinds a position.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
contract LiqThresholdPoCTest is Test {
uint256 constant DEBT_AMOUNT = 1000e6;
uint256 constant USDC_PRICE = 1e8;
uint256 constant WETH_PRICE = 3000e8;
uint256 constant AAVE_LTV = 7500;
uint256 constant AAVE_LIQ_THRESHOLD = 8000;
uint256 constant LTV_PRECISION = 1e4;
/// @notice Proves liqThreshold underestimates withdrawal by exactly 6.25%.
function test_liqThresholdUnderWithdraws() public pure {
// What the user should receive (correct: uses ltv = 7500)
uint256 expected = (DEBT_AMOUNT * USDC_PRICE * 1e18 * LTV_PRECISION)
/ (WETH_PRICE * 1e6 * AAVE_LTV);
// What the contract actually withdraws (bug: uses liqThreshold = 8000)
uint256 actual = (DEBT_AMOUNT * USDC_PRICE * 1e18 * LTV_PRECISION)
/ (WETH_PRICE * 1e6 * AAVE_LIQ_THRESHOLD);
uint256 lossBps = ((expected - actual) * 10000) / expected;
assertLt(actual, expected, "liqThreshold underestimates collateral withdrawal");
assertEq(lossBps, 625, "loss is exactly 625 bps = 6.25%");
}
}

Recommended Mitigation

- (,, uint256 liqThreshold,,,,,,,) =
+ (, uint256 liqThreshold,,,,,,,,) =
aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);

This changes the destructuring to capture index 1 (ltv) instead of index 2 (liquidationThreshold), matching the intent described in the comment on line 574.

Support

FAQs

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

Give us feedback!