Readme setup + interest rate calc fixes (#57)

* basic setup instructions for installation and testing

* details on the localnet testing setup

* fixed collat ratio missing in readme

* relayer mention

* Update README.md

* Update README.md

* rphr

* Update README.md

* document piecewise linear model correctly

* add precision to piecewise linear calc;

* audit disclaimer

Co-authored-by: derpy-duck <115193320+derpy-duck@users.noreply.github.com>
This commit is contained in:
wrinkledLatherer 2023-01-26 14:30:16 -05:00 committed by GitHub
parent 0c2d3fe62e
commit 9a396c2351
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 48 additions and 20 deletions

View File

@ -1,6 +1,31 @@
# Cross Chain Borrow Lend Hub and Spoke
**WARNING**: This repository has not been audited, so it may contain bugs and should be used for example purposes only. If you intend to use or build on this example to perform actual lending, it's highly recommended that you have the final code commit audited by a smart contract auditing firm.
We propose a design and reference implementation (EVM only) for a Cross-chain borrow lend protocol, using a Hub and Spoke model.
# Setup
## Running tests in localnet
To run tests on this reference example suite, you will need to [install yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable).
After installing yarn, you should:
1. run `make build`--This will install Forge (a standard EVM development toolkit that allows tests to be written in Solidity) and the necessary node modules. It will also compile the smart contract code.
2. run `make test`--This will compile and then run all the tests, which are defined in `example-2/evm/test/Hub.t.sol`. This will take a bit the first time and should print the results of the tests as well as some logs output in the terminal.
## Cross-chain design and testing in localnet
The `make test` command references the `Makefile` and runs the [`forge test`](https://book.getfoundry.sh/forge/tests) command as defined in the `Makefile`. For the purposes of simulating and testing hubs and spokes on localnet, the current codebase does not spin up different networks and interface across them. Instead, it uses the same network for the hub and all the spokes and registers each with a specific chain ID. Messages to particular spokes are then routed to those spokes via a forked Wormhole contract that simulates the Wormhole network but with one guardian that signs using the key stored in the `TESTING_DEVNET_GUARDIAN` environment variable. This simulates the Wormhole cross-chain connectivity by replicating the normal VAA construction, without requiring spinning up multiple local networks. This setup was adapted from the [Wormhole scaffolding repo](https://github.com/wormhole-foundation/wormhole-scaffolding/tree/main/evm).
All environment variables are defined in `testing.env`. In addition to the guardian key, the tests fork the [Wormhole core contract](https://book.wormhole.com/reference/contracts.html#core-bridge) and [Token Bridge contract from mainnet](https://book.wormhole.com/reference/contracts.html#token-bridge) (in the current codebase, from Avalanche), set the guardian set index to represent the one guardian Wormhole testing framework leveraged on localnet, set the Wormhole chain ID, and fork the [Pyth contract](https://docs.pyth.network/pythnet-price-feeds/evm#mainnet) from mainnet. All this is done in the `testSetUp` function found in `example-2/evm/test/helpers/TestHelpers.sol`. forge enables forking the existing EVM mainnet environment by setting a `fork-url` flag equal to an RPC endpoint from which to fork state.
## Tips for deploying and testing on different chains on devnet/testnet
Deploying and testing on a devnet/testnet environment would involve different steps than the localnet setup outlined above. One would not need to deploy the Wormhole core, Token Bridge, or Pyth contracts, since they could leverage the existing official contracts on devnet/testnet. One would need to deploy the hub on a single network and the spoke on as many chains as they wanted. [The Foundry Book](https://book.getfoundry.sh/forge/deploying) has some helpful instructions on deploying contracts.
Note that there is currently no cross-chain relayer infrastructure in place around these contracts. To actually run the protocol in practice, one may want to build in relayer capabilities, either by building out and operating a bespoke relayer for this application or by connecting to generic relayer infrastructure. More details on relayers can be found in the [Wormhole book](https://book.wormhole.com/wormhole/6_relayers.html).
# Cross Chain Borrow Lend Hub and Spoke Docs
We propose a design and reference example (EVM only) for a Cross-chain borrow lend protocol, using a Hub and Spoke model.
Users can deposit assets into the protocol. They can also borrow assets from the protocol, using the assets that they have deposited as collateral. The protocol keeps track of the amount of each asset a user has deposited, and the amount of each asset a user has borrowed. We will often refer to a users deposited and borrowed assets in the protocol as their vault.
@ -12,7 +37,7 @@ We store all state on the Hub chain, and have a Spoke contract deployed on each
![Untitled](imgs/initial_diagram.png)
# Table of Contents
## Table of Contents
1. Preface
2. Setting up the Hub and Spoke
@ -24,7 +49,7 @@ We store all state on the Hub chain, and have a Spoke contract deployed on each
8. Decimal Considerations
9. Protocol Design Choices
# 1. Preface
## 1. Preface
- Why Cross Chain?
@ -73,7 +98,7 @@ We store all state on the Hub chain, and have a Spoke contract deployed on each
This is better than the previous approach of deposits and withdrawals done directly on different chains, because now there is no state asynchronicity problem. The user does need to wrap assets and then deposit those wrapped assets on chain X to make deposits, and a withdrawer needs to take an action on chain X and then unwrap their assets, but all of this is configurable on the frontend. Because all accounting is done on a single blockchain, state is atomically updated, and there is no potential for asynchronicity. In essence, the ability to atomically execute and update makes it far easier to maintain this design of the protocol than a version where borrowing and lending happen on different chains. That type of design in any case does not provide too much for a user base that shouldnt really care all that much about where the borrowing and lending happen, as long as they can initiate the deposit and borrow of assets easily, on their preferred chains, and with good UX.
# 2. Setting up the Hub and Spoke
## 2. Setting up the Hub and Spoke
As the deployer/owner of the protocol, one has to:
@ -188,13 +213,10 @@ As the deployer/owner of the protocol, one has to:
bytes32 pythId
) public onlyOwner;
```
************************************Note: Click on any of the above arrows to see the relevant functions and inputs************************************
Now the system is set up so that users can perform deposits, borrows, repays, and withdraws of any of the registered assets, from any of the registered spokes!
# 3. User Functions
## 3. User Functions
- depositCollateral
@ -342,7 +364,7 @@ Now the system is set up so that users can perform deposits, borrows, repays, an
Then, the Hub contract, if the liquidation is valid, will spend these tokens, use them to repay the collateral of the user at address `vault` , and give, for each i, `assetReceiptAmount[i]` of the asset with address `assetReceiptAddresses[i]` back to User A.
# 4. Illustration of User Functions
## 4. Illustration of User Functions
*********************************************************Note: Some of the equations in this section arent exactly what is checked in the protocol, because we consider 1) interest, 2) collateralization ratios, and 3) price confidence intervals. Check sections 4, 5, and 6 for more information. For this section you can assume there is no interest, collateralization ratios are all 1, and all used Pyth price updates have confidence value (i.e. standard deviation) 0.*********************************************************
@ -370,7 +392,9 @@ Now, suppose User 2 has no assets in the protocol. User 2 wishes to borrow 35 CC
1) Does the user have enough value in their vault for this borrow to be valid; i.e.,
$40 \cdot (\text{price of wAAA}) + 50 \cdot (\text{price of wBBB}) ≥ 35 \cdot (\text{price of wCCC})$
$40 \cdot (\text{price of wAAA}) \cdot (\text{deposit collat. ratio of wAAA}) + 50 \cdot (\text{price of wBBB}) \cdot (\text{deposit collat. ratio of wBBB}) ≥ 35 \cdot (\text{price of wCCC}) \cdot (\text{borrow collat. ratio of wCCC})$
The deposit and borrow collateralization ratios are important but are discussed in more detail in a later section; for now, you can ignore them and understand the equation as providing a check that the value of the borrow does not exceed the value of the deposited assets.
2) Does the protocol have enough total supply of the CCC token to lend out these 35 CCC? The answer to this is yes because User 1 has 100 wCCC in their vault, so the protocol currently has 100 wCCC available to lend out.
@ -391,7 +415,7 @@ Suppose next that User 2 wishes to withdraw some of its collateral, specifically
1) does the user have enough value in their vault for this withdraw to be valid (we use Pyth prices of the assets in the users vault to check this. In fact we use slightly different prices for borrowed vs deposited assets; see details in the Pyth Oracle Integration section).
$(40-10) \cdot (\text{price of wAAA}) + 50 \cdot (\text{price of wBBB}) \ge 35 \cdot (\text{price of wCCC})$
$(40-10) \cdot (\text{price of wAAA}) \cdot (\text{deposit collat. ratio of wAAA}) + 50 \cdot (\text{price of wBBB}) \cdot (\text{deposit collat. ratio of wBBB}) \ge 35 \cdot (\text{price of wCCC}) \cdot (\text{borrow collat. ratio of wCCC})$
2) does the user have enough of the token in their vault for the withdraw to be valid. Specifically, does the user have ≥ 10 wAAA in their vault. The answer to this is yes since User 2 has 40 wAAA in their vault.
@ -431,7 +455,7 @@ Then suppose that User 3 on Chain X (the Hub) attempts to liquidate the vault by
1) The vault is underwater. Specifically,
$30 \cdot (\text{price of wAAA}) + 50 \cdot (\text{price of wBBB}) < 20 \cdot (\text{price of wCCC})$
$30 \cdot (\text{price of wAAA}) \cdot (\text{deposit collat. ratio of wAAA}) + 50 \cdot (\text{price of wBBB}) \cdot (\text{deposit collat. ratio of wBBB}) < 20 \cdot (\text{price of wCCC}) \cdot (\text{borrow collat. ratio of wCCC})$
2) The liquidator is receiving nonnegative value, i.e. (total price of received assets) ≥ the (total price of repaid assets) (this isnt strictly necessary for the health of the protocol but serves as a safety check to safeguard liquidators from making bad liquidations).
@ -452,7 +476,7 @@ Then suppose that User 3 on Chain X (the Hub) attempts to liquidate the vault by
![Untitled](imgs/hub_diagram_liquidation.png)
# 5. Keeping Track of Interest
## 5. Keeping Track of Interest
We store two indices for each asset; we call them the deposit interest accrual index and borrow interest accrual index for each asset. These values generally increase over time, starting at 1 at the initialization of the protocol.
@ -488,7 +512,7 @@ All that remains to decide is how these interest accrual indices change over tim
Intuitively, if deposited amounts are high relative to borrowed amounts, then a deposit should not earn much interest, and a borrow should not be charged much interest. On the converse, if borrowed amounts are high (a.k.a. utilization rate is high), then we want to hike up the deposit interest rate to encourage more deposits relative to borrows. This philosophy is at the heart of the variable interest rate model.
Our interest rate model is a simpler version of the [model used by Aave](https://docs.aave.com/risk/liquidity-risk/borrow-interest-rate). Our model is simpler because we dont use a piecewise function and instead use a single linear function. This model can be replaced with a piecewise linear function, or something more complicated (e.g. Euler uses a [control-theory-set rate](https://docs.euler.finance/getting-started/white-paper#reactive-interest-rates)). The interest owed by a borrower (since the last update of the accrual index) can be written out as
Our interest rate model is a version of the [model used by Aave](https://docs.aave.com/risk/liquidity-risk/borrow-interest-rate). This model can be replaced with something more complicated (e.g. Euler uses a [control-theory-set rate](https://docs.euler.finance/getting-started/white-paper#reactive-interest-rates)). Below, we describe the form of the basic linear model. The interest owed by a borrower (since the last update of the accrual index) can be written out as
$t \cdot (a \cdot \frac{N_{borrowed}}{N_{deposited}} + b)$
@ -501,9 +525,13 @@ All that remains to decide is how these interest accrual indices change over tim
is distributed amongst depositors of that asset.
For illustration, consider the following example. At time $0$ the interest accrual index is $1$ and a depositor $D_1$ deposits $X$ tokens; at time $t_1$ another depositor $D_2$ deposits $Y$ tokens. Suppose for simplicity that at time $0$ a borrower $B_0$ withdraws all of the $X$ tokens deposited by $D_1$. Then, from time $0$ through $t_1$, $D_1$ collects all but $r$ of the paid out interest by $B_1$, and from time $t_1$ through $2 \cdot t_1$, $D_1$ and $D_2$ split (all but $r$ of) the interest paid out by $B_1$ pro rata according to $X$ and $Y$. Do note that the interest paid out by $B_1$ between $t_1$ and $2 \cdot t_1$ will be lower than that paid out between $0$ and $t_1$, since the utilization rate is lower.
The piecewise linear version of this involves performing a cumulative sum of linear functions through inflection points that define the endpoints of each of the pieces. Suppose we define a set of inflection points $\{i_0 = 0, i_1, i_2, \ldots\}$ (such that $i_j \leq 1 \, \forall \, j, j_1 < j_2 \Rightarrow i_{j_1} < i_{j_2}$, that is all inflection points are less than or equal to $1$ and are monotonically increasing) that correspond to interest rates $\{r_0, r_1, r_2, \ldots\}$ (such that $r_j > 0 \, \forall \, j, j_1 < j_2 \Rightarrow r_{j_1} < r_{j_2}$). Then we want to find the indices $j, j+1$ such that our observed $i = \frac{N_{borrowed}}{N_{deposited}}$ satisfies $i_j \leq i < i_{j+1}$), and we compute the interest rate as:
$r_j + \frac{r_{j+1}-r_j}{k_{j+1}-k_j} \cdot (\frac{N_{borrowed}}{N_{deposited}} - k_j)$
# 6. Collateralization Ratios
## 6. Collateralization Ratios
For health of the protocol, it doesnt make sense to allow borrowing x AAA against a deposit of y BBB, if x AAA and y BBB have the same value. For example, if there is even a slight decrease in price of BBB, then suddenly the users vault is unhealthy and there is no incentive for a liquidator to liquidate.
@ -528,7 +556,7 @@ In each of these cases, we modify the corresponding equation to multiply all amo
$40 \cdot (\text{price of wAAA}) \cdot (\text{deposit collat. ratio of wAAA}) + 50 \cdot (\text{price of wBBB}) \cdot (\text{deposit collat. ratio of wBBB}) ≥ 35 \cdot (\text{price of wCCC}) \cdot (\text{borrow collat. ratio of wCCC})$
# 7. Prices - Pyth Oracle Integration
## 7. Prices - Pyth Oracle Integration
Our protocol leverages the Pyth price oracle model for providing high-fidelity, precise prices for different price feeds. More details on how to integrate Pyth can be found [here](https://docs.pyth.network/consume-data/best-practices). A list of available price feeds on different chains is accessible [here](https://pyth.network/developers/price-feed-ids).
@ -571,7 +599,7 @@ Using these adjusted prices, we undervalue collateral and overvalue debt, so as
To install the `pythnetwork` requirements for testing and deployment, run `npm install @pythnetwork/pyth-sdk-solidity`.
# 8. Decimal Considerations
## 8. Decimal Considerations
We want to be able to store non-integer values (i.e. non-integer amounts of assets, non-integer collateralization ratios, non-integer interest accrual indices, etc).
@ -610,7 +638,7 @@ Also, due to decimal constraints, when a user deposits $x$ tokens (where $X = x
This is upper bounded by $\left\lfloor f \cdot \lfloor X/f \rfloor \right\rfloor > f(X/f - 1) - 1 = X - f - 1$. So, in each action, we lose at most $10^{-1 \cdot \text{decimals}} (f + 1)$ of the token, where f is the interest accrual index. This ideally is a negligible value compared to the gas.
# 9. Design Choices
## 9. Design Choices
- Why Liquidations on the Hub only? Why not allow liquidations from spokes?

View File

@ -72,7 +72,7 @@ contract HubInterestUtilities is HubSpokeStructs, HubGetters, HubSetters {
interestRate = rates[0];
}
else {
interestRate += (rates[i] - rates[i-1]) * ((borrowed - kinks[i-1] * deposited) / deposited) / (kinks[i] - kinks[i-1]);
interestRate += (rates[i] - rates[i-1]) * ((borrowed * interestRateModel.ratePrecision - kinks[i-1] * deposited) / deposited) / (kinks[i] - kinks[i-1]);
}