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.
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.