Stratax Contracts

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

Health factor check insufficient

Author Revealed upon completion

Description

  • A leveraged position protocol should enforce a safety buffer above liquidation, not merely “HF > 1”. Healthy systems set a configurable minimum health factor (e.g., 1.05–1.15) and/or derive it from conservative borrow sizing so small adverse price moves or oracle updates cannot instantly liquidate positions.

  • Stratax only checks healthFactor > 1e18 once in the open path and uses no buffer nor configurability:

    • It permits barely‑healthy positions (e.g., HF = 1.0001) that are one oracle tick from liquidation.

    • There is no min‑HF guard in unwind (partial unwinds could leave a knife‑edge HF).

    • Combined with user‑supplied _borrowAmount and lenient _minReturnAmount, operators can open positions right at LTV (see a related bug), and this check won’t stop it as long as HF is just above 1.

// Stratax.sol :: _executeOpenOperation (after swap & repayment)
(,,,,, uint256 healthFactor) = aavePool.getUserAccountData(address(this));
require(healthFactor > 1e18, "Position health factor too low"); // @> minimal check only

Risk

Likelihood: Medium

  • Routine market volatility and small price oracle updates occur every block. A position opened at HF ≈ 1.00–1.03 is very likely to be liquidatable in the next update.

  • Off‑chain bots/integrations often optimize for capital efficiency; without a firm on‑chain minimum, they’ll tend to push HF near 1.0.

Impact: Medium

  • Instant liquidation risk / user losses: Positions can be liquidated immediately after opening due to tiny adverse moves or oracle updates.

  • Operational fragility: Strategies that appear “successful” on one block may revert/lose funds on the next due to minimal safety margin.

Proof of Concept

  • Conceptual pseudocode:

// Precondition:
// - Owner (position owner) supplies params that set borrow ~ at LTV (no safety),
// and sets minReturnAmount just enough to repay flash-loan + premium (no extra buffer).
// Execution:
// 1) supply(collateral + flash)
// 2) borrow(debt) // sized close to LTV
// 3) swap(borrowToken -> collateralToken) returns barely enough for flash loan repayment
// 4) repay flash loan
// 5) getUserAccountData() => healthFactor ≈ liquidationThreshold / LTV ~ 1.01..1.08
// 6) require(healthFactor > 1e18) passes (e.g., HF = 1.02e18)
// Outcome:
// - Transaction succeeds with HF only slightly above 1.
// - Next block’s price update or small adverse swap slippage would push HF < 1 => liquidation.

Recommended Mitigation

  • Enforce a configurable minimum HF (default ≥ 1.05)
    Introduce a state variable (e.g., minHealthFactorWad) and require it at the end of open and (optionally) after partial unwinds:

contract Stratax is Initializable {
+ /// @notice Minimum required health factor (WAD, e.g., 1.05e18)
+ uint256 public minHealthFactorWad;
function initialize(...) external initializer {
...
flashLoanFeeBps = 9;
+ minHealthFactorWad = 1_05e18; // 1.05x default buffer
}
+ function setMinHealthFactor(uint256 _minHF) external onlyOwner {
+ require(_minHF >= 1e18, "min HF must be >= 1.0");
+ require(_minHF <= 2e18, "min HF unreasonably high");
+ minHealthFactorWad = _minHF;
+ }
}
// _executeOpenOperation(...)
(,,,,, uint256 healthFactor) = aavePool.getUserAccountData(address(this));
- require(healthFactor > 1e18, "Position health factor too low");
+ require(healthFactor >= minHealthFactorWad, "HF below protocol minimum");
// _executeUnwindOperation(...)
// If not fully closing, ensure remaining position is still above minimum.
(,,,,, uint256 hfAfter) = aavePool.getUserAccountData(address(this));
+ require(hfAfter == 0 || hfAfter >= minHealthFactorWad, "HF below protocol minimum after unwind");

Support

FAQs

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

Give us feedback!