Health

The information provided by Aloe Labs, Inc. (“we,” “us” or “our”) on docs.aloe.capital (the “Site”) is for general informational purposes only. All information on the Site is provided in good faith, however we make no representation or warranty of any kind, express or implied, regarding the accuracy, adequacy, validity, reliability, availability or completeness of any information on the Site.

Under no circumstance shall we have any liability to you for any loss or damage of any kind incurred as a result of the use of the site or reliance on any information provided on the site. Your use of the site and your reliance on any information on the site is solely at your own risk.

Borrower health is a key parameter in Aloe II. Borrowers must be healthy after any actions taken in Borrower.modify's callback, and if they become unhealthy over time (due to interest accrual, price movement, or IV increase), they can be liquidated. This page explains the variables that go into health and how it's calculated so that you can understand BalanceSheet.isHealthy and BalanceSheet.computeLiquidationIncentive.

Types of Assets

Borrowers understand three types of assets:

  • Raw TOKEN0, i.e. any amount of TOKEN0 owned by the Borrower

  • Raw TOKEN1, i.e. any amount of TOKEN1 owned by the Borrower

  • Uniswap V3 Positions in Borrower.UNISWAP_POOL, which are made up of TOKEN0 and/or TOKEN1. The amount of each token varies with price.

    • To save gas, uncollected earned fees are not included as assets

    • The Borrower can own and manage as many Uniswap positions as you want, but only the ones stored in slot0 will count as assets. slot0 can hold up to 3 positions.

It'd be straightforward to count Lender shares as assets too (simply calling lender.underlyingBalance), but that increases risks for lenders and makes liquidations more complicated.

Types of Liabilities

Borrowers can borrow from LENDER0 and/or LENDER1. Those borrows are augmented by the need for a liquidation incentive (at most ×1.05) and a small leverage constraint (always ×1.005).

Solvency

To be solvent, total assets must be greater than total liabilities:

assets>liabilities\text{assets} \gt \text{liabilities}

Expanding for more specificity and evaluating in terms of TOKEN1 using price PP, we get:

raw0⋅P+raw1+∑uniswapPositionValue>1.005(borrows0⋅P+borrows1)+liquidationIncentive\text{raw}_0 \cdot P + \text{raw}_1 + \sum \text{uniswapPositionValue} \gt 1.005(\text{borrows}_0 \cdot P + \text{borrows}_1) + \text{liquidationIncentive}

The uniswapPositionValue is given in the Uniswap V3 Whitepaper, Equations 6.29 and 6.30 (where PP is the current price, and Pl,PuP_l,P_u are the lower and upper bound of the position, respectively):

uni0={liquidity⋅(1Pl−1Pu)P<Plliquidity⋅(1P−1Pu)Pl≤P<Pu0Pu≤P\text{uni}_{0}= \begin{cases} \text{liquidity} \cdot (\frac{1}{\sqrt{P_l}} - \frac{1}{\sqrt{P_u}}) & P \lt P_l \\ \text{liquidity} \cdot (\frac{1}{\sqrt{P}} - \frac{1}{\sqrt{P_u}}) & P_l \leq P \lt P_u \\ 0 & P_u \leq P \\ \end{cases}

uni1={0P<Plliquidity⋅(P−Pl)Pl≤P<Puliquidity⋅(Pu−Pl)Pu≤P\text{uni}_{1}= \begin{cases} 0 & P \lt P_l \\ \text{liquidity} \cdot (\sqrt{P} - \sqrt{P_l}) & P_l \leq P \lt P_u \\ \text{liquidity} \cdot (\sqrt{P_u} - \sqrt{P_l}) & P_u \leq P \\ \end{cases}

In some cases we only care about the total value of the position, in terms of TOKEN1. In those cases, we can simplify like so:

value={liquidity⋅(PPl−PPu)P<Plliquidity⋅(2P−Pl−PPu)Pl≤P<Puliquidity⋅(Pu−Pl)Pu≤P\text{value}= \begin{cases} \text{liquidity} \cdot (\frac{P}{\sqrt{P_l}} - \frac{P}{\sqrt{P_u}}) & P \lt P_l \\ \text{liquidity} \cdot (2\sqrt{P} - \sqrt{P_l} - \frac{P}{\sqrt{P_u}}) & P_l \leq P \lt P_u \\ \text{liquidity} \cdot (\sqrt{P_u} - \sqrt{P_l}) & P_u \leq P \\ \end{cases}

All of these equations are implemented in the LiquidityAmounts library.

The liquidationIncentive is based on the assets that a liquidator would have to swap in order to repay all debt:

if (liabilities0 > assets0) {
    // shortfall is the amount that cannot be directly repaid using Borrower assets at this price
    uint256 shortfall = liabilities0 - assets0;
    // to cover it, a liquidator may have to use their own assets, taking on inventory risk.
    // to compensate them for this risk, they're allowed to seize some of the surplus asset.
    incentive1 += mulDiv128(shortfall, meanPriceX128) / LIQUIDATION_INCENTIVE;
}

if (liabilities1 > assets1) {
    // shortfall is the amount that cannot be directly repaid using Borrower assets at this price
    uint256 shortfall = liabilities1 - assets1;
    // to cover it, a liquidator may have to use their own assets, taking on inventory risk.
    // to compensate them for this risk, they're allowed to seize some of the surplus asset.
    incentive1 += shortfall / LIQUIDATION_INCENTIVE;
}

For example, if an account has 100 TOKEN0 as collateral, and (for some reason) borrows 50 TOKEN0, the incentive would be 0. The debt can be repaid in-kind; no swap is necessary, so the liquidator doesn't need to be compensated for inventory risk.

If, on the other hand, the account had borrowed TOKEN1, the incentive would be non-zero. Before repaying the debt, the liquidator would have to swap the collateral (TOKEN0) for TOKEN1. They must be compensated for slippage and/or inventory risk.

Regardless of the incentive, the Borrower's ante is used to pay for liquidators' gas costs.

Probe Prices

Checking solvency at the current price is insufficient-by the time it detects danger, it's too late to do anything about it. Instead, we want to consider future prices and make sure the Borrower is solvent in all "likely" scenarios. Implied volatility (IV) allows us to do just that.

With Black-Scholes assumptions, the daily IV represents one standard deviation of price movement in a 24 hour period. Aloe has nSigma = 5 by default, meaning it considers 5 standard deviations of price movement. This number defines a "likely" scenario and is governable on a per-market basis.

Given IV and nSigma, it's easy to calculate Probe Prices:

Probe Prices=TWAP⋅e±nSigma⋅IV\text{Probe Prices}=\text{TWAP} \cdot e^{\pm \text{nSigma} \cdot IV}

Health

To be healthy, a Borrower must be solvent at both Probe Prices. If it isn't, it can be liquidated.

Implications

  • Uncollected Uniswap fees do not count as assets

  • As in other lending protocols, health depends on the current price

  • When IV is high, you'll need more collateral because the effective LTV is lower

    • IV can change by about 4%/day. This could be the difference between 90% and 75% LTV, so always keep an eye on your health! Liquidation warnings can help with this.

  • Governance should exercise extreme caution if changing nSigma because it changes the risk profile of the market. Users should be made aware well before the change takes effect.


A note on asset calculation

Borrows and raw asset amounts do not change w.r.t price, but the amounts underlying a Uniswap position do. This is important both in two parts of the solvency check:

  • assessing the Uniswap positions themselves

  • assessing shortfall when computing the liquidation incentive

The Asset struct is populated with the necessary data in Borrower._getAssets:

function _getAssets(uint256 slot0_, Prices memory prices, bool withdraw) private returns (Assets memory assets) {
    assets.fixed0 = TOKEN0.balanceOf(address(this));
    assets.fixed1 = TOKEN1.balanceOf(address(this));

    int24[] memory positions = extract(slot0_);
    uint256 count = positions.length;
    unchecked {
        for (uint256 i; i < count; i += 2) {
            // Load lower and upper ticks from the `positions` array
            int24 l = positions[i];
            int24 u = positions[i + 1];
            // Fetch amount of `liquidity` in the position
            (uint128 liquidity, , , , ) = UNISWAP_POOL.positions(keccak256(abi.encodePacked(address(this), l, u)));

            if (liquidity == 0) continue;

            // Compute lower and upper sqrt ratios
            uint160 L = TickMath.getSqrtRatioAtTick(l);
            uint160 U = TickMath.getSqrtRatioAtTick(u);

            // Compute the value of `liquidity` (in terms of token1) at both probe prices
            assets.fluid1A += LiquidityAmounts.getValueOfLiquidity(prices.a, L, U, liquidity);
            assets.fluid1B += LiquidityAmounts.getValueOfLiquidity(prices.b, L, U, liquidity);

            // Compute what amounts underlie `liquidity` at the current TWAP
            (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(prices.c, L, U, liquidity);
            assets.fluid0C += amount0;
            assets.fluid1C += amount1;

            if (!withdraw) continue;

            // Withdraw all `liquidity` from the position
            _uniswapWithdraw(l, u, liquidity, address(this));
        }
    }
}

Last updated