Stratax Contracts

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

`_executeUnwindOperation()` uses `liquidationThreshold` instead of `ltv` causing ~3-7% less collateral withdrawn on every unwind

Author Revealed upon completion

Root + Impact

In Stratax.sol, the _executeUnwindOperation() function is intended to use the Loan-to-Value (LTV) ratio from Aave to calculate how much collateral to withdraw after repaying debt. However, the code incorrectly destructures the return value of getReserveConfigurationData(), taking liquidationThreshold (index 2) instead of ltv (index 1). This causes ~3-7% less collateral to be withdrawn on every unwindPosition() call, leaving excess collateral in Aave.

Description

  • The IProtocolDataProvider.getReserveConfigurationData() returns values in this order: decimals (index 0), ltv (index 1), liquidationThreshold (index 2). At line 566, _executeUnwindOperation() skips two positions (,, and takes liquidationThreshold at index 2, while the comment says "Get LTV" and the formula comment also says "ltv".

  • The calculateOpenParams() function at line 385 correctly uses (, uint256 ltv,,,,,,,,) taking index 1. This inconsistency within the same contract proves the developer intended to use ltv in both places.

  • For ETH collateral (LTV=8000, LiqThreshold=8250), the function withdraws 9.697 ETH instead of 10.0 ETH for a 24,000 USDC debt — a shortfall of 0.303 ETH ($909), approximately 3% of collateral value per unwind.

// Stratax.sol L565-577 — BUGGY:
// @> Comment says "Get LTV" but code takes liquidationThreshold (index 2)
// Get LTV from Aave for the collateral token
(,, uint256 liqThreshold,,,,,,,) =
aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);
// @> Formula comment says "ltv" but uses liqThreshold variable
uint256 collateralToWithdraw = (
_amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold); // @> BUG: should be ltv
// Compare with calculateOpenParams() L385-386 — CORRECT:
(, uint256 ltv,,,,,,,,) = aaveDataProvider.getReserveConfigurationData(details.collateralToken);

Risk

Likelihood:

  • This occurs on every single call to unwindPosition(). No special conditions are required — the incorrect destructuring always takes liquidationThreshold instead of ltv.

  • Three independent pieces of evidence confirm this is unintentional: (1) comment says "Get LTV", (2) formula comment says "ltv", (3) calculateOpenParams() correctly uses ltv.

Impact:

  • For ETH collateral (LTV=8000, LiqThreshold=8250), the function withdraws 9.697 ETH instead of 10.0 ETH for a 24,000 USDC debt — a loss of 0.303 ETH ($909) per unwind, approximately 3% of collateral value.

  • The remaining collateral stays in Aave as aTokens. Recovery is possible via recoverTokens(aTokenAddress, amount) but requires manual intervention, knowledge of aToken addresses, and only works when no other debt positions exist (since aToken transfers check Aave health factor).

Proof of Concept

The following demonstrates that _executeUnwindOperation() takes the wrong index from Aave's getReserveConfigurationData() return values. The IProtocolDataProvider interface defines ltv at index 1 and liquidationThreshold at index 2, but the destructuring pattern (,, uint256 liqThreshold,,,,,,,) skips two positions, landing on index 2 instead of index 1. A mathematical proof with real Aave parameters (ETH LTV=80%, LiqThreshold=82.5%) shows the resulting collateral shortfall.

// IProtocolDataProvider interface defines return order:
// function getReserveConfigurationData(address asset) returns (
// uint256 decimals, // index 0
// uint256 ltv, // index 1 ← CORRECT VALUE TO USE
// uint256 liquidationThreshold, // index 2 ← WHAT CODE ACTUALLY TAKES
// ...
// );
// _executeUnwindOperation (L565-567) — BUGGY:
// (,, uint256 liqThreshold,,,,,,,) = ...getReserveConfigurationData(...)
// ^ ^ takes index 2 (liquidationThreshold)
// | skip index 1 (ltv)
// skip index 0 (decimals)
// calculateOpenParams (L385-386) — CORRECT:
// (, uint256 ltv,,,,,,,,) = ...getReserveConfigurationData(...)
// ^ takes index 1 (ltv)
// skip index 0 (decimals)
// Mathematical proof with ETH (LTV=80%, LiqThreshold=82.5%):
// Debt: 24,000 USDC, ETH price: $3,000
//
// With liqThreshold (8250) [BUG]:
// collateral = (24000e6 * 1e8 * 1e18 * 1e4) / (3000e8 * 1e6 * 8250)
// = 9.697 ETH
//
// With ltv (8000) [CORRECT]:
// collateral = (24000e6 * 1e8 * 1e18 * 1e4) / (3000e8 * 1e6 * 8000)
// = 10.0 ETH
//
// Difference: 0.303 ETH ($909) = ~3% NOT withdrawn, left in Aave

Recommended Mitigation

Change the destructuring pattern in _executeUnwindOperation() to take ltv at index 1 instead of liquidationThreshold at index 2. This aligns with how calculateOpenParams() already correctly retrieves ltv, and matches the developer's intent as documented in the code comments. The fix requires changing the comma pattern from (,, to (, and updating the variable name from liqThreshold to ltv in the formula denominator.

// Stratax.sol _executeUnwindOperation(), line 566
- (,, uint256 liqThreshold,,,,,,,) =
+ (, uint256 ltv,,,,,,,,) =
aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);
// Line 577 - update the denominator variable name
uint256 collateralToWithdraw = (
_amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
- ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);
+ ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * ltv);

Support

FAQs

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

Give us feedback!