Stratax Contracts

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

Unwind math uses the wrong Aave parameter (liquidationThreshold instead of LTV)

Author Revealed upon completion

Description

  • When unwinding, the contract should withdraw just enough collateral to cover the repaid debt (plus fees/slippage), keeping the position’s risk consistent. The correct proportionality uses LTV (loan‑to‑value), because LTV defines the borrowable value against collateral during regular operation.

  • _executeUnwindOperation uses liquidationThreshold (LT) instead of ltv in the collateral‑to‑withdraw calculation. Since liquidationThreshold is typically greater than ltv on Aave, dividing by a larger denominator yields a smaller withdrawal than required. That can under‑withdraw collateral, starving the subsequent swap and causing returnAmount < totalDebt → unwind reverts; or it leaves unnecessary collateral locked, deviating from the caller’s expectation.

// Stratax.sol::_executeUnwindOperation
// @> Correct comment but wrong action
// Get LTV from Aave for the collateral token
(,, uint256 liqThreshold,,,,,,,) =
aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);
// prices & decimals fetched above...
// @> BUG: uses liquidationThreshold, not LTV, to compute how much collateral backed the repaid debt
uint256 collateralToWithdraw = (
_amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
) / (
collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold
);
withdrawnAmount = aavePool.withdraw(unwindParams.collateralToken, collateralToWithdraw, address(this));

Risk

Likelihood: High

  • Aave markets frequently set liquidationThreshold > ltv. Under those common configurations, the current formula will compute a smaller withdraw amount than the LTV‑based proportion.

  • Any unwind that relies on this amount to fully repay the flash loan after the swap will hit the shortfall under realistic prices/slippage and may revert with Insufficient funds to repay flash loan.

Impact: High

  • Operational failure: Unwind transactions can revert, trapping positions and increasing operational risk during volatile markets.

  • Capital inefficiency: Even when the swap still covers the flash loan, the contract withdraws less collateral than intended, leaving excess collateral locked in Aave and breaking user expectations for “proportional” unwind.

Proof of Concept

  • Copy the code below to test/fork/Stratax.t.sol.

  • Run command forge test --mt test_UnwindMathUsesLiquidationThresholdInsteadOfLTV -vvvv.

// The test demonstrates, using Aave config and oracle prices on the fork,
// that the contract’s formula (with LT) yields a strictly smaller withdrawal
// than the correct LTV‑based formula—showing the root cause.
function test_UnwindMathUsesLiquidationThresholdInsteadOfLTV() public view {
// Fetch Aave reserve config via low-level call.
address dataProvider = address(stratax.aaveDataProvider());
bytes memory payload = abi.encodeWithSignature(
"getReserveConfigurationData(address)",
USDC // collateral token in typical USDC/WETH position
);
(bool ok, bytes memory out) = dataProvider.staticcall(payload);
require(ok, "aave data provider call failed");
// Decode as (decimals, ltv, liquidationThreshold, reserveFactor, bonus, flags...)
( ,
uint256 ltv,
uint256 liqThreshold,
,
,
,
,
,
,
) = abi.decode(out, (uint256, uint256, uint256, uint256, uint256, bool, bool, bool, bool, bool));
// Sanity: in Aave, liquidationThreshold is typically > ltv
assertTrue(liqThreshold >= ltv, "Unexpected config: LT should be >= LTV");
// Pull live prices and decimals
uint256 debtPrice = strataxOracle.getPrice(WETH); // 8 decimals
uint256 collPrice = strataxOracle.getPrice(USDC); // 8 decimals
uint8 collDec = IERC20(USDC).decimals();
uint8 debtDec = IERC20(WETH).decimals();
// Use any positive debt amount unit for comparison (1 debt token unit here)
uint256 amount = 10 ** uint256(debtDec);
// Current implementation math (BUG): uses liquidationThreshold in denominator
uint256 withLT = (amount * debtPrice * (10 ** uint256(collDec)) * stratax.LTV_PRECISION())
/ (collPrice * (10 ** uint256(debtDec)) * liqThreshold);
// Correct math: use LTV, not liquidationThreshold
uint256 withLTV = (amount * debtPrice * (10 ** uint256(collDec)) * stratax.LTV_PRECISION())
/ (collPrice * (10 ** uint256(debtDec)) * ltv);
// Since LT >= LTV and denominators are larger with LT, the computed withdrawal is smaller.
assertLt(withLT, withLTV, "Using LT under-withdraws collateral vs correct LTV math");
}

Output:

[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/fork/Stratax.t.sol:StrataxForkTest
[PASS] test_UnwindMathUsesLiquidationThresholdInsteadOfLTV() (gas: 117070)
Logs:
Available swap data files: 3
Randomly selected block: 24329390
Current fork block number is: 24329390
Traces:
[117070] StrataxForkTest::test_UnwindMathUsesLiquidationThresholdInsteadOfLTV()
├─ [11067] BeaconProxy::fallback() [staticcall]
│ ├─ [2515] UpgradeableBeacon::implementation() [staticcall]
│ │ └─ ← [Return] Stratax: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]
│ ├─ [2727] Stratax::aaveDataProvider() [delegatecall]
│ │ └─ ← [Return] 0x0a16f2FCC0D44FaE41cc54e079281D84A363bECD
│ └─ ← [Return] 0x0a16f2FCC0D44FaE41cc54e079281D84A363bECD
├─ [11703] 0x0a16f2FCC0D44FaE41cc54e079281D84A363bECD::getReserveConfigurationData(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
│ ├─ [7732] 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2::getConfiguration(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
│ │ ├─ [2680] 0x8147b99DF7672A21809c9093E6F6CE1a60F119Bd::getConfiguration(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [delegatecall]
│ │ │ └─ ← [Return] 0x100000000000000000000007d01bf08eb001a13b860003e8a50628d21e781d4c
│ │ └─ ← [Return] 0x100000000000000000000007d01bf08eb001a13b860003e8a50628d21e781d4c
│ └─ ← [Return] 6, 7500, 7800, 10450 [1.045e4], 1000, true, true, false, true, false
├─ [0] VM::assertTrue(true, "Unexpected config: LT should be >= LTV") [staticcall]
│ └─ ← [Return]
├─ [22293] StrataxOracle::getPrice(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) [staticcall]
│ ├─ [15642] 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419::latestRoundData() [staticcall]
│ │ ├─ [7409] 0x7d4E742018fb52E48b08BE73d041C18B21de6Fb5::latestRoundData() [staticcall]
│ │ │ └─ ← [Return] 23929 [2.392e4], 300469062333 [3.004e11], 1769552573 [1.769e9], 1769552591 [1.769e9], 23929 [2.392e4]
│ │ └─ ← [Return] 129127208515966885241 [1.291e20], 300469062333 [3.004e11], 1769552573 [1.769e9], 1769552591 [1.769e9], 129127208515966885241 [1.291e20]
│ └─ ← [Return] 300469062333 [3.004e11]
├─ [22293] StrataxOracle::getPrice(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) [staticcall]
│ ├─ [15642] 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6::latestRoundData() [staticcall]
│ │ ├─ [7409] 0xc9E1a09622afdB659913fefE800fEaE5DBbFe9d7::latestRoundData() [staticcall]
│ │ │ └─ ← [Return] 732, 99968715 [9.996e7], 1769500821 [1.769e9], 1769500835 [1.769e9], 732
│ │ └─ ← [Return] 55340232221128655580 [5.534e19], 99968715 [9.996e7], 1769500821 [1.769e9], 1769500835 [1.769e9], 55340232221128655580 [5.534e19]
│ └─ ← [Return] 99968715 [9.996e7]
├─ [9664] 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48::decimals() [staticcall]
│ ├─ [2381] 0x43506849D7C04F9138D1A2050bbF3A0c054402dd::decimals() [delegatecall]
│ │ └─ ← [Return] 6
│ └─ ← [Return] 6
├─ [2444] 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2::decimals() [staticcall]
│ └─ ← [Return] 18
├─ [1754] BeaconProxy::fallback() [staticcall]
│ ├─ [515] UpgradeableBeacon::implementation() [staticcall]
│ │ └─ ← [Return] Stratax: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]
│ ├─ [414] Stratax::LTV_PRECISION() [delegatecall]
│ │ └─ ← [Return] 10000 [1e4]
│ └─ ← [Return] 10000 [1e4]
├─ [1754] BeaconProxy::fallback() [staticcall]
│ ├─ [515] UpgradeableBeacon::implementation() [staticcall]
│ │ └─ ← [Return] Stratax: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]
│ ├─ [414] Stratax::LTV_PRECISION() [delegatecall]
│ │ └─ ← [Return] 10000 [1e4]
│ └─ ← [Return] 10000 [1e4]
├─ [0] VM::assertLt(3853372993 [3.853e9], 4007507913 [4.007e9], "Using LT under-withdraws collateral vs correct LTV math") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 749.54ms (4.92ms CPU time)
Ran 1 test suite in 757.40ms (749.54ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • Replace liquidationThreshold with ltv in _executeUnwindOperation so the withdrawn collateral tracks the repaid debt at the asset’s LTV.

  • Update comments to reflect this.

function _executeUnwindOperation(address _asset, uint256 _amount, uint256 _premium, bytes calldata _params)
internal
returns (bool)
{
(, address user, UnwindParams memory unwindParams) = abi.decode(_params, (OperationType, address, UnwindParams));
// Repay Aave variable debt with flash-loaned tokens
IERC20(_asset).approve(address(aavePool), _amount);
aavePool.repay(_asset, _amount, 2, address(this));
uint256 withdrawnAmount;
{
- // Get LTV config (but mistakenly used liquidationThreshold before)
- (,, uint256 liqThreshold,,,,,,,) =
- aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);
+ // Get LTV for the collateral asset (use borrow-time constraint, not liquidation)
+ (, uint256 ltv,,,,,,,,) =
+ aaveDataProvider.getReserveConfigurationData(unwindParams.collateralToken);
// Prices and decimals
uint256 debtTokenPrice = IStrataxOracle(strataxOracle).getPrice(_asset);
uint256 collateralTokenPrice = IStrataxOracle(strataxOracle).getPrice(unwindParams.collateralToken);
require(debtTokenPrice > 0 && collateralTokenPrice > 0, "Invalid prices");
- // OLD COMMENT: "Calculate collateral to withdraw ... / ... * liqThreshold"
- // FIX: Use LTV so we withdraw the collateral proportion that backed the repaid debt.
- uint256 collateralToWithdraw = (
- _amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
- ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);
+ // Calculate collateral to withdraw proportional to repaid debt at LTV (4-decimals precision)
+ uint256 collateralToWithdraw = (
+ _amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
+ ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * ltv);
withdrawnAmount = aavePool.withdraw(unwindParams.collateralToken, collateralToWithdraw, address(this));
}
// ... rest of function unchanged ...
}

Support

FAQs

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

Give us feedback!