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
The problem is divided into two sub problems: value calculation and interactions.
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:
Position that accrues value over time
v(t) = price * amount = v(t=0) * (1 + k(t))
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
All borrowing protocols share a simmilar borrowing lifecycle [1] that includes the following actions:
- Deposit: User lends an amount of tokens, creating a lending position (e.g: chai, cTokens, aTokens).
- Add Collateral: Uses part of the lended tokens as collateral to receive debt.
- Borrow: Using the previously added collateral, the user creates a loan from the pool
- Payback: Action where the borrower repays part of his debt, reducing his debt position.
- 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.
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.
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.
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:
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
.
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_);
To calculate the totalGav
, it is possible now to
- Get all
debtPositions
usinggetActiveDebtPositions
- Call ValueInterpreter's
calcCanonicalAssetsTotalValue
from collateral and assets balances (see figure)
The new GAV is equal to:
gav_ += Σi (collateralValue[i] - borrowedValue[i])
It is also needed to create a new function on the Integration Manager in order to addActiveDebtPosition
. This function would mean creating another actionId
.
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
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.
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:
This requires to add another function to the debtPositionContract
:
function addCollateral(address[] memory, uint256[] memory) external;
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:
This requires to add another function to the debtPositionContract
:
function borrow(address[] memory, uint256[] memory) external;
TODO
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 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
- Deposit: cToken.mint: The same function which is used for lending, creates an amount of cTokens which is sent to the vault.
- 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
- 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
andincomingToken
- Payback: Repay borrow or RepayBorrow Behalf
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
- 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.
- Add Collateral:lendingPool.setUserUseReserveAsCollateral
- 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
- Payback:Repay borrow or RepayBorrow Behalf. The last action would just exchange an amount of tokens for the debt.
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
- Deposit: cdpManager.open.
- Add Collateral: cdpManager.join:
- 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**
- Payback:Repay borrow or RepayBorrow Behalf. The last action would just exchange an amount of tokens for the debt.
Other protocols that were not taken into study but could be interesting include:
- Synthetix borrowing
- Iron Bank
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
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.
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 genericcallOnDebtPosition()
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 extravaultCallOnContract
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.