Brainstorming: Exposure ceiling

The Aave protocol has some specific risk management characteristics. Compared to other competing technologies, it features:

  1. Custom liquidation penalty per asset
  2. Custom Loan To Value (aka borrowing power) per asset
  3. Custom Liquidation threshold (aka Maintenance Margin) per asset

These risk parameters greatly help in mitigating liquidity risk. A higher liquidation penalty for less liquid assets means higher chance for liquidations to be profitable, which greatly increases protocol safety. A lower LTV than the maintenance margin ensures that no borrowers can be liquidated right off the bat, and provides a soft protection against market volatility (soft because there isn’t any constraint for borrowers to withdraw some of the assets and have a riskier position if they wish to do so).

One thing that the Aave protocol is missing is an adequate strategy for debt ceiling.The Debt ceiling concept was initially introduced by Maker and is a powerful way of handling protocol exposure to a certain asset.

The debt ceiling specifically helps protecting against the following:

  1. Economic attacks: Pump and dumps, even potentially in one transaction using flashloan/flashminting, might temporarily increase the price of an asset even by orders of magnitude, allowing attackers to borrow more than they are allowed to. The immediate drop of the price after the attack is completed leaves the protocol insolvent.

  2. Supply attacks (aka infinite minting): Some permissioned/upgradeable tokens might be attacked in a way where a malicious actor might be able to mint an infinite amount of tokens. Bugs might cause that too (see the recent $COVER infinite minting attack).

  3. Concentration risk: The protocol might become overexposed to a certain asset that might fail for reasons not strictly related to attacks.

In Maker each asset can mint up to a certain amount of DAI. Once the ceiling is reached borrowers will need to wait until the governance increases it. This introduces some UX friction:

  1. Users that deposited the asset and didn’t immediately borrow might not be able to do so at a later time depending on the state of the debt ceiling

  2. Users can’t borrow more once the ceiling is reached

Users can always, anyway, introduce more of the asset in the system as a way to protect from liquidations. These frictions are relatively minimal compared to the safety that the debt ceiling provides to the whole system.

Achieving the same result in liquidity markets like Aave is extremely complex, and impossible to do completely onchain. Since all the deposits of a user are aggregated to calculate the total borrowing power and a user can borrow both stable and volatile assets, there is no way of having onchain at any point in time the exact amount each user has borrowed with a specific asset. Even calculating that offchain can be rather complex due to the nature of the cryptomarket and the way the protocol works (at the very least there would be multiple ways of doing that, each with certain drawbacks).

Some projects already attempted/are in the process of implementing an equivalent or similar approach to the debt ceiling:

  • BentoBox is moving away from the liquidity market/aggregated borrowing power idea to introduce lending pairs. Although this would technically allow for an exact debt ceiling like Maker does, the individual lending pair approach comes with a whole host of different UX problems (eg liquidity segregation, need for multiple transactions to deposit/borrow the same asset, need two pairs for the same asset for the ability to long/short, and so on) that makes the solution not particularly attractive.

  • Cream.Finance introduced a supply cap to their forked implementation of the compound protocol, by adding a check on the mintAllowed() function once a certain amount of tokens have been deposited. This solution also comes with some drawbacks: Legit holders of a certain asset won’t be allowed to add more of it to protect against liquidation (something that Maker always allows) and they would be forced to deposit a different asset to increase their maintenance margin. This is a big problem especially for certain entities/institutions for which keeping exposure to a specific asset (eg WBTC) is mandatory. Also due to the nature of how cTokens work, the supply cap does not completely cover infinite minting risk (an attacker might still airdrop minted underlying to the cToken contract artificially inflating the cToken:underlying exchange rate). Moreover, this doesn’t work well with stablecoins (you never want a supply cap with stablecoins as their most relevant utility is not to be used as collateral but to be borrowed).

Although the Aave protocol, compared to the Compound protocol, is technically capable of a true supply cap (since the accrual of the interest in the aTokens is separated from the actual amount of underlying available in the token) i personally believe it’s not the best solution, as i think the possibility of adding more collateral to prevent liquidation is something that should always be allowed.

What I am proposing is a different way of approaching the problem that i call “Exposure Ceiling”. The exposure ceiling uses the LTV (borrowing power) risk parameter to drop the borrowing power of a certain asset when it’s time to reduce exposure.

The implementation would introduce a depositCeiling configuration parameter. Given a token X, The deposit cap would specify a certain amount of the total supply of X deposited after which the LTV of X would automatically drop to 0. By doing so, a user would still be allowed to:

  1. Borrow using X as collateral when the total aToken supply is below the deposit ceiling (being 1:1, the aToken supply closely tracks the amount of underlying in the system)

  2. Always refill the system when needed to prevent liquidation using X, since X maintains its liquidation threshold

  3. Not allowed to borrow more when the aX total supply is past the deposit ceiling.

This solution would cover most of the needs, although it is bypassable: Supposing X is past the deposit ceiling and is being attacked with infinite minting, an attacker might use a legit currency Y (where Y has still borrowing power), borrow from the system, deposit X and then withdraw Y - the attacker factually used Y to initiate the loan, then replaced Y with X to keep the maintenance margin.

This requires that one limitation is introduced. Specifically, users who deposit multiple assets will be required to first withdraw assets that are past the deposit ceiling, if they are borrowing. If they aren’t borrowing, there won’t be limitations. See how this prevents the scenario above:

  1. User deposits Y, borrows Z

  2. User deposits X, his maintenance margin increases but not his borrowing power.

  3. User tries to withdraw Y, he’s not allowed to. The system will require that he withdraws X first.

  4. Debt is still covered by Y

This potentially leaves open a window for abuses if users that are close to liquidation increase their maintenance margin using an infinite minted asset. I believe this is a remote possibility:

  1. Assets that get infinite minted quickly drop to 0. Therefore the most important protection that is needed is immediately after the infinite minting takes place. After that is passed, the governance and the emergency admins have the capability to act accordingly (for example by freezing the reserve so that X can’t be deposited anymore).

  2. It does not increase, anyway, the total debt of the system.

So what do we get compared to the most precise implementation of debt ceiling:

  1. Protection against infinite minting attacks

  2. Limited protection against economic attacks (although the concept can be extended to also consider the exposure in USD, see final paragraph).

  3. Limited protection against concentration risk: Since there isn’t a direct correlation between the total supply and the price of an asset, even by reducing the exposure cap, the protocol might still become overexposed if the asset’s price increases significantly (in this case too, the exposure in USD can improve this).

In terms of additional UX friction:

  1. Users that deposited when the deposit cap was not hit will not be allowed to borrow if the deposit cap is passed
  2. Users will not be allowed to borrow with an asset that is past the deposit cap
  3. Users that are borrowing using multiple assets and want to withdraw (lowering their maintenance margin) will be required to withdraw first the assets that are past the deposit cap.
  4. Users are always allowed to refill their position with the asset
  5. Users are always allowed to deposit for lending

1 and 2 are in common with the most exact debt ceiling implementation. The 3 I believe is due to the nature of the system and I don’t feel like it is a dealbreaker, when we factor in the additional safety this feature provides to the whole system.

4, in my opinion the most important one, is preserved. 5 is a great side effect, that is especially important with permissioned stablecoins.

Potential extensions of the idea:

  1. Implement also an exposure cap in USD: when the supply*price is past a certain threshold, the LTV drops to 0

  2. Hard supply cap: We can have a soft exposure cap and a hard one: users that deposit when the soft cap is reached are still allowed to deposit for lending and/or increase the maintenance margin; once the hard cap is hit, no more deposits are allowed.

This (long) thread is meant not to be a guideline for implementation. The goal is to bootstrap a discussion on this much needed feature and a brainstorming session with the community and especially the users.

10 Likes

This is an excellent proposal, and it’s definitely in the best interest of anyone contributing to the AAVE safety module to read, understand, and support this!

I’m curious to get the AAVE team’s thoughts on the implementation details of an exposure cap (price*supply) as opposed to a supply cap.

It seems to me like implementing an exposure cap would be strictly better from a risk management perspective, but would be much harder to implement. Only implementing a supply cap would not address an economic attack where an asset price gets artificially manipulated higher. If this were to happen, the attacker could borrow with no limit until the asset price decreases or people deposit the asset up to the supply cap.

The implementation challenge is determining when each cap would be hit. A supply cap would be much easier to calculate as it would only need to be checked at the time of deposit, whereas an exposure cap would need to be checked on deposits, borrows, or withdrawal of any asset used as collateral. How feasible and efficient would it be to implement all of these checks, and are there any other benefits/drawbacks to an exposure cap that I’m missing?

3 Likes

Great question. If this idea is proven to be sound, the implementation would be straightforward and not really gas intensive;
currently the protocol calculates the total borrowing power (LTV) maintenance margin (liq threshold), total collateral and borrowed amount in a function called calculateUserAccountData() (protocol-v2/GenericLogic.sol at 3782bfda1a9d0c9fa4c1bd26e69f1f3b3eed88ce · aave/protocol-v2 · GitHub)

The idea would be, for each asset the user has deposited as collateral, to fetch the totalSupply() of the corresponding aToken; if the total supply is past the exposure cap, the ltv of the asset is considered to be 0 in the calculation of the average, specifically here

This automatically drops the borrowing power of the user. So whenever the user borrows, his total collateral and the ltv is calculated and validated on the fly.

Example: An user has 50 ETH worth of collateral, 25ETH worth of Asset 1(LTV 75%) and 25ETH worth of asset 2(LTV 50%). The average ltv of the user is 62.5%

If we introduce these changes, supposing the asset 2 goes past the exposure ceiling, the user still has 50ETH worth of collateral, but his ltv is now 37.5% ((0.75+0)/2). Which is the equivalent of having 25 ETH worth of collateral with LTV 75%.

Another change is required on withdraw: to verify if a user has an asset past the exposure ceiling, the calculateUserAccountData() should track this with a boolean that is then returned as an additional parameter. In validateWithdraw() protocol-v2/ValidationLogic.sol at 3782bfda1a9d0c9fa4c1bd26e69f1f3b3eed88ce · aave/protocol-v2 · GitHub, it’s possible to add a require: if the calculateUserAccountData() returns that the user deposited assets that hit the exposure ceiling and the asset the user is trying to withdraw hasn’t reached it yet, the tx reverts.

I expect this implementation to add around 2-3k gas per user deposited currency on calculateUserAccountData() (so if the user has 10 different collaterals any borrow/withdrawal would cost 20-30k gas more), and likely another 5K gas flat on withdraw.

4 Likes

This seems like a very well thought out implementation of the exposure ceiling feature. I cannot see any drawbacks to the implementation other than the additional gas costs required which seem like a worthwhile tradeoff given the added safety and UX improvements.

That said, the main drawback I see with supply caps more generally is that they don’t take into account the value of the assets deposited. For instance, a supply cap doesn’t seem to fully protect against P+D attacks since the value of the asset would increase (potentially by orders of magnitude) and permit additional borrowing even without any new supply being added to the market.

This is solved by implementing an exposure cap denominated in USD. I would be curious to hear how this would be done technically and what the potential tradeoffs are.

1 Like

While this mechanism is a good way of avoiding recursive lending and other adversarial borrower behavior, we think there are a few nuances that appear to make this tricky to implement. Firstly, borrowers who are near liquidation accounts (e.g. need re-up deposits to count towards “debt ceiling” even if collateral ceiling is hit) will need to be able to save their positions. While this is mentioned in the article, we have observed it to be a key safety mechanic during times of duress. Proper implementation of this mechanic is therefore systemically important.

The current implementation of liquidationCall checks healthFactor through calculateUserAccountDataVars. This iterates through each reserve and calculates totalCollateralBalanceEth by multiplying the token quantity by the reserve collateral’s base LTV (which would be the same for all accounts). With the exposure ceiling, this would need to be changed, and the total increase in computational load seems non-trivial as it would require extra state per account (e.g. there will be a large gas increase).

“Example: An user has 50 ETH worth of collateral, 25ETH worth of Asset 1(LTV 75%) and 25ETH worth of asset 2(LTV 50%). The average ltv of the user is 62.5%
If we introduce these changes, supposing the asset 2 goes past the exposure ceiling, the user still has 50ETH worth of collateral, but his ltv is now 37.5% ((0.75+0)/2). Which is the equivalent of having 25 ETH worth of collateral with LTV 75%.”

When total deposits of asset X hit their debt ceiling value DebtCeiling[X], LTV of future deposits of X will be 0. You also want to make sure that it applies on FUTURE deposits, not current ones. In the example Emilio gives above, it doesn’t matter if the avgLiquidationThreshold is unchanged as the new drop in totalCollateralInETH would lead to accounts immediately being liquidatable.

Perhaps having a rolling maxNetSupplyChange value that checks on deposit whether or not the total deposit amount reaches this value (this value can be kept very high for stablecoins such that users are always allowed to deposit). The idea is that the collateral assets for which this debt ceiling is useful will probably have very low utilization. Not allowing users to deposit more would force them to deposit a different collateral which helps mitigate the exposure to the asset as well as decrease calculation overhead on user withdraws, repays, etc.

Hey @wfu thanks for digging into it.
Actually the calculateHealthFactor uses the liquidationThreshold and not the LTV to calculate the health of the user position (protocol-v2/GenericLogic.sol at 09d084eeb09adf58db66fed5c3972fb0dda9b395 · aave/protocol-v2 · GitHub) and the function calculateHealthFactorFromBalances protocol-v2/GenericLogic.sol at 09d084eeb09adf58db66fed5c3972fb0dda9b395 · aave/protocol-v2 · GitHub.
This means that dropping the LTV does not affect the health factor of existing users.
The borrow function, on the other hand, calculates the borrowing power using the LTV (protocol-v2/ValidationLogic.sol at 09d084eeb09adf58db66fed5c3972fb0dda9b395 · aave/protocol-v2 · GitHub).
Which means that any individual asset with LTV = 0 will not participate in the borrowing power of the user. But if the asset had maintenance margin (liq threshold) and was used to borrow before, its collateralization capabilities are unchanged. This is actually the exact reason why we decided to split ltv and liquidation threshold in Aave V1