# The liquidation floor is sized from the wrong maximum leverage

Bug Deep Dive #35 · 13 August 2026

Bug Deep Dives · [The Contest Academy](https://0xsimao.com/the-contest-academy) · written by 0xSimao

---

| Title | Base calculation in `Leverager::isLiquidateable()` is incorrect as the max leverage may be smaller |
| Reward | $1009, 3 finders |
| Contest | [Yieldoor - 24 February 2025 on Sherlock](https://audits.sherlock.xyz/contests/791) |
| Author | [0x73696d616f (0xSimao)](https://0xsimao.com) |
| Context | Liquidation threshold |

[Yieldoor](https://audits.sherlock.xyz/contests/791)'s `Leverager` lets you borrow from a lending pool and put the borrowed funds into a concentrated liquidity vault. Whether that position can be liquidated comes down to one comparison, and the right hand side of it is derived from the maximum leverage the protocol allows.

There are two maximum leverages. The check reads one of them.

**Both ceilings apply when the position opens**

[`_checkWithinlimits`](https://github.com/sherlock-audit/2025-02-yieldoor/blob/b5a0f779dce4236b02665606adb610099451a51a/yieldoor/src/Leverager.sol#L459-L469) runs on every open, and reads a per vault limit alongside a per asset limit from the lending pool:

```solidity:459
function _checkWithinlimits(Position memory up) internal {
    VaultParams memory vp = vaultParams[up.vault];
@>    (uint256 maxIndividualBorrow, uint256 maxLevTimes) =
        ILendingPool(lendingPool).getLeverageParams(up.denomination);

    uint256 positionLeverage = (up.initCollateralValue + up.borrowedAmount) * 1e18 / up.initCollateralValue;

    require(up.initBorrowedUsd >= minBorrow, "position must be at least minBorrow amount");
@>    require(positionLeverage <= vp.maxTimesLeverage && positionLeverage <= maxLevTimes, "too high x leverage");
    require(up.initBorrowedUsd <= vp.maxUsdLeverage, "too high borrow usd amount");
    require(up.borrowedAmount <= maxIndividualBorrow, "too high borrow for the vault");
```

`vp.maxTimesLeverage` is the vault's own limit. `maxLevTimes` comes from the lending pool and is set per borrowed asset, so the same vault has a different ceiling depending on what you borrow against it. Both are enforced with a single `&&`, which means the real ceiling is whichever of the two is smaller.

**Only one of them applies when it is checked**

The tail of [`isLiquidateable`](https://github.com/sherlock-audit/2025-02-yieldoor/blob/b5a0f779dce4236b02665606adb610099451a51a/yieldoor/src/Leverager.sol#L403-L413):

```solidity:403
    uint256 bIndex = ILendingPool(lendingPool).getCurrentBorrowingIndex(pos.denomination);
    uint256 owedAmount = pos.borrowedAmount * bIndex / pos.borrowedIndex;

    /// here we make a calculation what would be the necessary collateral
    /// if we had the same borrowed amount, but at max leverage. Check docs for better explanation why.
@>    uint256 base = owedAmount * 1e18 / (vp.maxTimesLeverage - 1e18);
    base = base < pos.initCollateralValue ? base : pos.initCollateralValue;

    if (owedAmount > totalDenom || totalDenom - owedAmount < vp.minCollateralPct * base / 1e18) return true;
    else return false;
}
```

The comment on [line 406](https://github.com/sherlock-audit/2025-02-yieldoor/blob/b5a0f779dce4236b02665606adb610099451a51a/yieldoor/src/Leverager.sol#L406) says what `base` is meant to be: the collateral a position would have needed to borrow this much at maximum leverage. Leverage here is `(collateral + borrowed) / collateral`, so inverting it for the collateral gives `owed / (maxLeverage - 1)`, which is [line 408](https://github.com/sherlock-audit/2025-02-yieldoor/blob/b5a0f779dce4236b02665606adb610099451a51a/yieldoor/src/Leverager.sol#L408). The liquidation floor is then `minCollateralPct` of that.

[Line 408](https://github.com/sherlock-audit/2025-02-yieldoor/blob/b5a0f779dce4236b02665606adb610099451a51a/yieldoor/src/Leverager.sol#L408) only knows about `vp.maxTimesLeverage`. `maxLevTimes` is never read in this function.

**The floor lands too low**

`base` divides by `maxLeverage - 1e18`, so a larger divisor gives a smaller `base` and a smaller floor. Using the vault ceiling when the pool ceiling is tighter is exactly the case where the divisor is too large.

| vp.maxTimesLeverage | maxLevTimes | Real ceiling | Divisor used | Divisor that applies | base is |
| --- | --- | --- | --- | --- | --- |
| 5e18 | 5e18 | 5x | 4e18 | 4e18 | correct |
| 5e18 | 3e18 | 3x | 4e18 | 2e18 | half of it |
| 5e18 | 2e18 | 2x | 4e18 | 1e18 | a quarter of it |
| 10e18 | 2e18 | 2x | 9e18 | 1e18 | a ninth of it |

The gap opens precisely when the lending pool tightens its limit below the vault's, which is what a pool does when the borrowed asset gets riskier.

**Impact**

`isLiquidateable` returns false while the position sits below the collateral floor the configuration implies, so no liquidator calls it and the collateral keeps falling. The liquidation that eventually happens starts from a smaller buffer, and if the collateral falls past the debt first the shortfall is bad debt on the lending pool.

**Alpha:** when a value is bounded in two places, find every consumer and check each one applies the same bound. Opening took the minimum of two ceilings through an `&&`. Liquidating took one of the two terms. A limit written as a compound require at the entry point and as a single variable everywhere else is the shape to look for. `getLeverageParams` returns two values here and only one of them is read a second time.

**Conclusion**

The fix is to take the minimum of `vp.maxTimesLeverage` and `maxLevTimes` before subtracting `1e18`. It paid **\$1009**, split three ways. Nothing about it needs deep protocol knowledge: the require that gates the open has two conditions and the check that gates the liquidation has one.

[**See more of the Yieldoor audit contest**](/reports/yieldoor)

Written by 0xSimao (https://0xsimao.com/) · Sherlock's 2025 Watson of the Year · 21 first places in 57 audit contests.

---

Older: [Rebalancing on slot0.tick centres the band one tick off the price](https://0xsimao.com/the-contest-academy/yieldoor-slot0-tick-stale-at-the-boundary)
