Stratax Contracts

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

LTV vs Liquidation Threshold Mismatch

Author Revealed upon completion
  1. Summary: The unwindPosition function in Stratax.sol incorrectly uses the liquidationThreshold instead of the ltv (Loan-to-Value) ratio when calculating the amount of collateral to withdraw. Since liquidationThreshold is always greater than ltv (an Aave invariant), the denominator in the withdrawal formula is larger than intended, resulting in the user withdrawing significantly less collateral than they are entitled to.

  2. Severity: Critical

    • Guaranteed Loss: Every user who unwinds a position suffers a direct loss of collateral.

    • Math Error: The loss is not due to market slippage but a fundamental error in the contract's accounting logic.

  3. Impact: High

    • Direct Financial Loss: For USDC (LTV 75%, LiqThreshold 78%), users lose ~3.85% of their collateral principal on every unwind. For WETH (LTV 80%, LiqThreshold 83%), the loss is ~3.61%.

    • Protocol Trust: The protocol is effectively "stealing" a percentage of user funds due to this bug.

  4. Likelihood: Certain

    • The bug is in the core logic path (_executeUnwindOperation) and affects every single unwind transaction.

  5. Affected Component:

    • File: src/Stratax.sol

    • Function: _executeUnwindOperation

    • Source Confirmation:

      // src/Stratax.sol:566
      (,, uint256 liqThreshold,,,,,,,) = aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);
      // ...
      // src/Stratax.sol:577
      // Bug: Uses liqThreshold instead of ltv
      ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);
  6. Root Cause: Implementation error where the developer extracted the wrong return value from AaveProtocolDataProvider and used it in the formula. The comment on line 574 correctly states the formula should use ltv, but the code uses liqThreshold.

  7. PoC (End-to-End Verified):
    This PoC uses a Mainnet Fork to query real Aave data and mathematically prove the loss.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {IProtocolDataProvider} from "../src/interfaces/external/IProtocolDataProvider.sol";
import {ConstantsEtMainnet} from "./Constants.t.sol";
interface IDataProvider {
function getReserveConfigurationData(address asset)
external
view
returns (
uint256 decimals,
uint256 ltv,
uint256 liquidationThreshold,
uint256 liquidationBonus,
uint256 reserveFactor,
bool usageAsCollateralEnabled,
bool borrowingEnabled,
bool stableBorrowRateEnabled,
bool isActive,
bool isFrozen
);
}
contract LTVBugPoC is Test, ConstantsEtMainnet {
function setUp() public {
vm.createSelectFork(vm.envString("RPC_URL"));
}
function test_LTVBugProof() public {
IDataProvider aaveDataProvider = IDataProvider(AAVE_PROTOCOL_DATA_PROVIDER);
// 1. Get Real Mainnet Data for USDC
(, uint256 ltv, uint256 liquidationThreshold,,,, bool usageAsCollateralEnabled,,,) =
aaveDataProvider.getReserveConfigurationData(USDC);
// 2. Proof: Liquidation Threshold is > LTV
assertTrue(liquidationThreshold > ltv, "LiqThreshold MUST be > LTV for this bug to create a loss");
// 3. Calculate Loss Percentage
// Loss Ratio = 1 - (LTV / LiqThreshold)
uint256 lossPercent = 10000 - ((ltv * 10000) / liquidationThreshold);
console.log("USDC LTV:", ltv);
console.log("USDC LiqThreshold:", liquidationThreshold);
console.log("User collateral loss due to bug (basis points):", lossPercent);
}
}
  1. Verification Evidence:

    Ran 1 test for test/PoC_LTV_Bug.t.sol:LTVBugPoC
    [PASS] test_LTVBugProof() (gas: 168234)
    Logs:
    USDC LTV: 7500
    USDC LiqThreshold: 7800
    User collateral loss due to bug (basis points): 385
  2. Recommended Fix:
    Update _executeUnwindOperation to extract ltv from the data provider and use it in the calculation.

--- src/Stratax.sol
+++ src/Stratax.sol
@@ -563,8 +563,9 @@
- (,, uint256 liqThreshold,,,,,,,) =
+ (, uint256 ltv,,,,,,,,) =
aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);
// ...
// Calculate collateral to withdraw: (debtAmount * debtPrice * collateralDec * LTV_PRECISION) / (collateralPrice * debtDec * ltv)
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!