Skip to content

Instantly share code, notes, and snippets.

@buendiadas
Last active March 30, 2021 09:45
Show Gist options
  • Save buendiadas/91fe7f86e7a330aa9d74f63970981c40 to your computer and use it in GitHub Desktop.
Save buendiadas/91fe7f86e7a330aa9d74f63970981c40 to your computer and use it in GitHub Desktop.
Notes on Enzyme Borrowing

Notes on enzyme borrowing

Overview

This gist has some thoughts on a general solution to enable borrowing.

The exercise is divided in the following sections.

  • Problem: where the core problem to be solved is formally defined
  • Solution: where some solutions will be studied to the previously mentioned problem
  • Integrations: Where different borrowing solutions are studied, and the previous model is tested, and modified if necessary
  • Conclusions: Final notes to be considered

Problem

The problem is divided into two sub problems: value calculation and interactions.

a) Value calculation

We want to calculate the value of positions that are stored in an external contract. Those positions are not ERC20 tokens, but a callable balance.

We can differentiate then between two types of those positions:

Lending (collateral) position:

Position that accrues value over time

v(t) = price * amount = v(t=0) * (1 + k(t))  
Debt position:

Negative position which value increases with an interest over time.

v(t) = - ( price * amount) = - v(t=0) * (1 + k(t)) 

The idea is to update our GAV calculation so it can actually calculate those positions:

GAV = Σ price[i] * token_amount[i]  + Σ lending_positions - Σ debt_positions

b) Interactions

All borrowing protocols share a simmilar borrowing lifecycle [1] that includes the following actions:

  1. Deposit: User lends an amount of tokens, creating a lending position (e.g: chai, cTokens, aTokens).
  2. Add Collateral: Uses part of the lended tokens as collateral to receive debt.
  3. Borrow: Using the previously added collateral, the user creates a loan from the pool
  4. Payback: Action where the borrower repays part of his debt, reducing his debt position.
  5. Liquidation: Action that happens when the credit amount is >= minimum collateral.

We need to be able to interact with the borrowing protocols through along their full lifecycle.

c) Debt Position Lifecycle

The new debt position module will hold vaultProxy funds. It is needed to create a lifecycle of the debt position that can upgrade logic without having to change the debtPosition address. This is still something to do and it hasn't been taken into consideration in the current PoC designs to simplify the problem.

Solution

The proposal includes the creation of external contracts (owned by the Vault) that will lock collateral and interact with the borrowing protocols in their full lifecycle. The Comptroller must be able to recognize those contracts, associate them to the Vault and calculate the total borrowed and collateral balance.

The protocol will also need to interact with that debt position contract. It does it through using an special adapter, driving it through the different states of the borrowing lifecycle.

Let's divide the solution solving the two problems of the previous section: value calculation and borrowing lifecycle.

a) Value calculation

The first problem that needs to be solved is how to calculate the total GAV of the fund after a new debtPosition has been created. The following diagram proposes a solution to do that:

image

VaultProxy

In order to be able to calculate its GAV, Vaults must include a record of all the CDP's owned, that is accessible from an external contract. One solution to do would be to define a new VaultLibBase

abstract contract VaultLibBase2 is VaultLibBaseCore {
    address[] internal activeDebtPositions;
    mapping(address => bool) internal activePositionToActive;
}

And an accessor from the VaultLib:

function getActiveDebtPositions() external view returns (address[] memory debtPositions_){
    return activeDebtPositions;
}

In a simmilar that it is done with trackedAssets.

DebtPosition

A debtPosition contract must track its current collateral and borrowed balances, providing an accesor to do so. The following functions are proposed:

function getCollateralBalances() returns (address[] memory collateralAssets_, uint256[] memory amounts_);

function getBorrowedBalances() returns (address[] memory borrowedAssets_, uint256[] memory amounts_);
ComptrollerLib -> calcGav()

To calculate the totalGav, it is possible now to

  1. Get all debtPositions using getActiveDebtPositions
  2. Call ValueInterpreter's calcCanonicalAssetsTotalValue from collateral and assets balances (see figure)

The new GAV is equal to:

gav_ += Σi (collateralValue[i] - borrowedValue[i])
IntegrationManager

It is also needed to create a new function on the Integration Manager in order to addActiveDebtPosition. This function would mean creating another actionId.

b) Interactions

Now that we have a debtPosition contract, it will be necessary to interact with it in order to add collateral, borrow, and keep track of those balances.

The current solution proposes an scheme where interactions to the

Deposit

The deposit phase is already supported at the protocol, and it doesn't require to make any interaction with the debtPosition, since those assets are tokens stored at the Vault directly.

Add collateral

addCollateral can be seen as a CoI where spendAssets are the collateral assets, and doesn't expect any asset in exchange.

The following figure shows the case for adding collateral:

img

This requires to add another function to the debtPositionContract:

 function addCollateral(address[] memory, uint256[] memory) external;
Borrow

borrow can be seen as a CoI where spendAssets is empty, and incomingAssets and minIncomingAssetAmounts are the borrowed assets

The following figure shows the case for adding collateral: borrow-diagram

This requires to add another function to the debtPositionContract:

 function borrow(address[] memory, uint256[] memory) external;
Payback [WIP]

c) Debt Position Lifecycle

TODO

Integrations

This section compares how the different available lending / borrowing solutions would work on the previous solution. Let's try to link the previously mentioned lifectycle to some of the biggest lending/borrowing protocols (Compound, Aave, Maker):

Compound

Compound implements a multi asset collateral approach, where the cTokens are optionally included as collateral. In this approach, the debt position that registered at the Vault, would be accessed through borrowBalance() function

Implemented lifecycle
  1. Deposit: cToken.mint: The same function which is used for lending, creates an amount of cTokens which is sent to the vault.
  2. Add Collateral: comptroller.enterMarkets: This is a function that needs to be called from the recipient, indicating the array of tokens which are supposed to be approved to be added as collateral. To call this function, from the Vault, it will be needed to register a new VaultCall
  3. Borrow:cToken.Borrow: cTokens include a borrow function, which also needs to be called from the owner of collateral. There can be a workaround for this, which would consist of sending cTokens to the adapter, by including it on the outgoingToken and incomingToken
  4. Payback: Repay borrow or RepayBorrow Behalf

Aave

Aave implements a multi asset collateral approach. In this approach, the debt position is registered as a debt token. Apart from that, it is worth mentioning two different things that differentiate Aave from the rest of the protocols:

  • Aave allows to take two types of loans: fixed and stable. The implementation of fixed loans should not add extra work given that this calculation is done through the tokenized debt balance.
  • Borrowing on Aave includes a referral fee, that could also provide profit for Enzyme.
  • Another interesting point is the fact that it allows to swap collateral assets
Implemented lifecycle
  1. Deposit: lendingPool.deposit. This function is already used in Aave Lending. Returns an aToken in exchange for a Token, that changes the balance according to the interest rate.
  2. Add Collateral:lendingPool.setUserUseReserveAsCollateral
  3. Borrow:: lendingProvider.borrow: This function allows an account to borrow an specific asset. It differs from compound in the sense that it allows to borrow on Behalf of an account. In order to do that, it is needed to approve this action from the vault, so it will be needed to register a new VaultCall
  4. Payback:Repay borrow or RepayBorrow Behalf. The last action would just exchange an amount of tokens for the debt.

Maker

Maker also manages debt, wich what they call Collateral Debt Positions(CDP). This debt can be managed on behalf of a third party user by using the cdpManager

The way MKR work is however slightly different from Aave and Compound in the following points:

  • CDP's only have one asset as collateral
  • Lending (adding collateral) doesn't accrue any interest
Implemented lifecycle
  1. Deposit: cdpManager.open.
  2. Add Collateral: cdpManager.join:
  3. Borrow:: lcdpManager.frob: This function allows an account to borrow an specific asset. It differs from compound in the sense that it allows to borrow on Behalf of an account. In order to do that, it is needed to approve this action from the vault, so it will be needed to register a new VaultCall**
  4. Payback:Repay borrow or RepayBorrow Behalf. The last action would just exchange an amount of tokens for the debt.

Other protocols

Other protocols that were not taken into study but could be interesting include:

  • Synthetix borrowing
  • Iron Bank

Examples

Example1: MKR CDP

Let's see how the previous solutions would work on a real example, using as an example a MKR CDP:

  • Portfolio_0: { ETH: $1500, DAI: 0, CDPs: {} }
  • Action: Borrow $1000 DAI using $1500 ETH as collateral.

Deposit From the protocol point of view we would first "trade" $1500 usd for a $1500 lending position, having the following Portfolio

  • Portfolio_1: { ETH: 0, DAI: 0, CETH: $1500, DEBT: { DAI: 0 } }

Add Collateral This action would not modify the current portfolio, and would only require to have a Vault permissioned action to add collateral

Borrow

This action would trade a "-$1000" debt position for $1000 DAI.

  • Portfolio_2: { ETH: 0, DAI: $1000, CETH: $1500, DEBT: { DAI: -$1000 } }
GAV_BEFORE = $1500
GAV_AFTER = $1000 DAI + $1500 CETH + (-$1000) = $1500

Conclusions

How much complexity does each solution add?

In this order of complexity I would argue: 1) Aave, 2) Compound, 3) MKR. The reason for Maker to be the last is that it doesn't create a clear separation between collateral and debt, integrating it into a single product. However the last can definitely be modeled separating debt and collateral.

Which solutions work out-of-box, or what would we need to integrate them (e.g., the Vault must own the CDP, but some protocols don't allow transferring a CDP, so we may need a wrapper... Maker provides wrapped CDP vaults)

Most of the question gets answered above

How debt can be represented in our system Using adapters we can create a single interface to get balances in our system. Debt would be only a type of it, always referenced to an underlying primitive.

How investors can force a partial payback of CDP debt to remove collateral

I don't think this should be a problem: external balance would change once collateral is removed.

Sources

https://defirate.com/loans/

@SeanJCasey
Copy link

Sounds like you prefer to start with either Aave or Compound; I think this makes sense for a number of reasons, especially because we don't need another integration to open up more than just DAI borrowing, plus we already have the interest bearing collateral assets (aTokens and cTokens) in our system.

If we go with Compound (or Aave), I think that we might want for each debt position to be a separate contract, which is owned by the Vault. Here's why...

We need to ensure that a debt collateralization ratio is not easy to disturb. Redeeming shares is the most obviously disruptive, e.g., if a user redeems shares and a fund holds cUSDC (which is also being used as collateral), then the redemption will pay out some cUSDC to the user and lower the collateralization ratio, possibly triggering a liquidation. If a debt position is its own contract, then we can simply send the cUSDC (and whatever other assets) to be used as collateral to that contract, and we know that the collateralization ratio can only be changed by intentionally adding/withdrawing collateral from that contract.

Also, having the debt position as its own contract would mean that we could permission the VaultProxy to act on it, and could either use a standard interface for things like addCollateral() or could create a generic callOnDebtPosition() function. We could do these things either in the VaultProxy or in the ComptrollerProxy, I'm thinking the latter (the debt position contract could allow access from both the VaultProxy and the VaultProxy.accessor, just to be safe). We could expose this function to the IntegrationManager, so that we don't need to have any/many extra vaultCallOnContract actions.

At that point, we might be able to get away with a simple address[] activeDebtPositions in the VaultProxy, and only simple functions to add/remove them that can be called by the ComptrollerProxy. The debt position contract itself could have a standard interface for everything that absolutely need.

If you feel like it would be easier or better to leave the collateral in the VaultProxy, please let me know and we can figure out an alternative.

@SeanJCasey
Copy link

If we do have these debt positions as separate contracts, then we need to keep the architecture really simple so that we can upgrade and never need to change the logic... or maybe have them use a proxy pattern and a particular lib... let's see what is necessary, but would be great to avoid that complexity of course.

@buendiadas
Copy link
Author

buendiadas commented Mar 9, 2021

I think it makes sense to separate collateral in a different contract to avoid those issues, if we transfer the collateral to a new contract (let's call it CdpContract) that have the collateral.

I'm thinking on how we could simplify GAV calculation after we make that change. Given that the collateral assets came from the Vault, we already know how to calculate its value, because we have created a price feed. Now we need to interpret the value of the active loans. To do that, we need a balance, and a price.

  • The borrow balance can be easily taken from functions like Compound's borrowBalance
  • The price comes from referencing an underlying asset. Assuming we are only borrowing primitives, it which will be a primitive

Then, inside CdpContract we can calculate the debt value by having a function like calcUnderlyingValues, let's call it

calcUnderlyingDebtValues() returns (address[] memory underlyings_, uint256[] memory underlyingAmounts_) for the debt amount, and the comptroller could calculate the total debt value out of that.

What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment