🔍
Contract Details
A concrete Pool is a monolithic contract composed of three mix-ins: the abstract base
Pool
, a collateral filter, and an interest rate model. The mixins provide internal virtual overrides that allow the Pool to validate collateral and calculate interest for a loan, respectively. The abstract base class for collateral filters is CollateralFilter
. The abstract base class for interest rate models is InterestRateModel
.For example, the concrete
WeightedRateCollectionPool
is comprised of Pool
, CollectionCollateralFilter
, and WeightedInterestRateModel
.Collateral wrappers are external token contracts that implement the
ICollateralWrapper
interface. They are deployed independently of concrete Pools and are associated with a Pool at construction time. A concrete Pool can support up to three collateral wrappers.Collateral liquidators are external contracts that implement the
ICollateralLiquidator
interface. They are deployed independently of concrete Pools and are associated with a Pool at initialization time.The
PoolFactory
is responsible for creating pools, and in doing so, becomes the administrator of any created Pool to support future collection of admin fees.Pools have two sets of parameters, those bound at a construction and those bound at initialization. Since the Pool contract is large, it is deployed as an implementation contract and the
PoolFactory
creates a proxy for a new Pool.Construction parameters include up to three collateral wrappers and an optional delegate.cash registry address (see Borrow section below for more information on this feature). These parameters are stored as immutable addresses, so they are bound to the implementation contract at deploy time.
Initialization parameters for a proxied pool include the collateral token, the currency token, the collateral liquidator, a discrete set of durations, a discrete set of rates, and interest rate model specific parameters. The role of durations, rates, and interest rate model parameters are discussed in the sections below.
While the Pool initialization parameters are essentially permissionless, the frontend is still required to validate a Pool is proxied with a vetted implementation contract and is initialized with a vetted collateral liquidator, as these can implement malicious behavior.
Ticks are unsigned, 128-bit values that encode conditions on liquidity, including a loan limit, duration index, and rate index. The
Tick
utility library is responsible for encoding and decoding ticks. Deposits are made into specific ticks by depositors, and liquidity is sourced from specific ticks to assemble the funds of a loan for borrowers. The ticks used in a loan become the tranches of the loan. Tick Bit Layout
+-----------------------------------------------------------------------+
| 128 |
+--------------------------------------|----------|----------|----------+
| 120 | 3 | 3 | 2 |
| Limit | Dur. Idx | Rate Idx | Reserved |
+-----------------------------------------------------------------------+
Limit is a 120-bit value that imposes the maximum limit funds sourced from the tick can be used in. Duration index is the maximum duration funds sourced from the tick can be used for, and rate index is the interest rate tier associated with the funds. Duration index and rate index are indices into predetermined, discrete tiers that are assigned at Pool initialization.
Example of a possible configuration of durations, rates, and ticks:
Durations = [ 7 days, 14 days, 30 days ]
Rates = [ 10%, 30%, 50% ]
# Tick Liquidity
6 (50 ETH, 7 days, 50%) 20 ETH
5 (40 ETH, 7 days, 50%) 30 ETH
4 (30 ETH, 14 days, 30%) 30 ETH
3 (15 ETH, 30 days, 30%) 50 ETH
2 (5 ETH, 30 days, 10%) 100 ETH
1 (2.5 ETH, 30 days, 10%) 150 ETH
To assemble a 30 day loan, ticks 1, 2, 3 can be used to create a 15 ETH loan that is organized as follows:
[2.5 ETH from #1, 2.5 ETH from #2, 10 ETH from #3]
. The interest for the loan would be determined by the 10%
, 10%
, and 30%
interest rate tiers applied to amount used from each tick and loan duration. Note that ticks 4, 5, 6 are ineligible for this loan, because the loan duration exceeds their maximum duration.Similarly, a 14 day loan can be assembled from ticks 1-4, and a 7 day loan from ticks 1-6. Longer duration ticks can be used for shorter duration loans.
Note that loan limit is an upper bound on the amount of funds that can be used from a tick, but the actual amount pulled from each tick depends on the cumulative amount built up from previous ticks.
The
LiquidityManager
, particularly the source()
function, is responsible for sourcing liquidity from ticks and creating a record of their usage for bookkeeping. It also responsible for enforcing the conditions on tick usage, like the loan limit and maximum duration.Ticks are selected offchain and provided to the borrow API when originating a loan. This avoids the gas costs associated with many storage lookups, and also allows for complex, offchain optimization of the ticks used. It also means that it is possible for borrowers to originate suboptimal loans, using too few ticks or more expensive (e.g. higher interest rate tier) ticks than necessary. However, this is not a violation of the protocol, as the protocol's guarantee is that funds from a tick are not used beyond its loan limit or maximum duration, and that the loan is priced according to the associated interest rate tier.
In order to reduce storage costs, loan metadata is stored offchain and a commitment to it stored onchain. (Technically, the loan metadata is onchain, as it's emitted in the
LoanOriginated
event, but it's not accessible from a contract.)The loan metadata, called Loan Receipt, contains all the relevant details of the loan required for its repayment or liquidation, including the principal, repayment, borrower, maturity, collateral, ticks used, etc. The
LoanReceipt
utility library is responsible for encoding and decoding loan receipts, which are tightly packed. The layout of a loan receipt is summarized below:Header (155 bytes)
1 uint8 version 0:1
32 uint256 principal 1:33
32 uint256 repayment 33:65
20 address borrower 65:85
8 uint64 maturity 85:93
8 uint64 duration 93:101
20 address collateralToken 101:121
32 uint256 collateralTokenId 121:153
2 uint16 collateralWrapperContextLen 153:155
Collateral Wrapper Context Data (M bytes) 155:---
Node Receipts (48 * N bytes)
N NodeReceipts[] nodeReceipts
16 uint128 tick
16 uint128 used
16 uint128 pending
The node receipts contain the amount used from each tick (
used
), and the amount due on repayment (pending
). Upon repayment, the pending
amount is restored to each tick referenced in the loan.Interest rate models are responsible for two roles: determining the overall interest rate for a loan given the ticks used, and distributing that interest to the ticks used.
The primary interest rate model is the
WeightedInterestRateModel
. It determines the interest rate of a loan in _rate()
by computing the average of all tick rates, weighted by the amount used for each tick. For example, if a 20 ETH
loan used 5 ETH at 10%
, 10 ETH at 10%
, and 10 ETH at 30%
, the weighted average interest rate would be (5 * 10% + 10 * 10% + 10 * 30%)/20
or 22.5%
.The
WeightedInterestRateModel
distributes interest in _distribute()
along a negative exponential curve, to allocate greater interest to higher ticks in compensation for their greater exposure to default risk. The negative exponential base is configured at Pool initialization time.For example, for an exponential base of 2, the distribution of interest to five ticks that source equal liquidity would follow the allocation:
# Allocation Normalized
5 1/2^1 = 0.50 0.5161...
4 1/2^2 = 0.25 0.2581...
3 1/2^3 = 0.125 0.1290...
2 1/2^4 = 0.0625 0.0645...
1 1/2^5 = 0.03125 0.0323...
The interest rate model performs a normalization pass to ensure the allocation sums to one.
Since liquidity might not be sourced equally from the ticks used, e.g. some ticks may contribute more liquidity to a loan than others, the interest rate model prorates the weight of the ideal negative exponential curve by the liquidity used, and then normalizes the allocation across all the ticks. While higher ticks receive higher weights, their final interest allocation is still scaled by their overall contribution to the loan.
Ticks that contribute insignificant liquidity to a loan below a tick interest threshold configured at Pool initialization time, also called "dust ticks", receive no interest in a loan. This is to prevent a class of attacks where borrowers could otherwise receive a free or significantly reduced interest loans by borrowing from their own high position ticks with little deposited liquidity and paying interest to themselves.
Admin fees are collected from loan repayments, as a fixed percentage of the total interest of the loan. Only successfully repaid loans contribute admin fees, while liquidations do not.
The Pool administrator — the
PoolFactory
contract — can set the admin fee rate on a Pool and withdraw admin fees from a Pool.Admin fees are set to zero for the time being. They may be enabled in the future to accrue fees to the protocol.
Collateral filters are responsible for validating collateral is acceptable when originating a loan.
The primary collateral filter is the
CollectionCollateralFilter
, which simply checks that the collateral token address matches the one configured with the Pool at initialization time. This allows the Pool to originate loans for any token ID that belongs to the specified collection as collateral.The deposit interface is responsible for depositing, redeeming, and withdrawing capital into a Pool with user-defined risk parameters.
function deposit(uint128 tick, uint256 amount) external;
The
deposit()
function accepts an amount of cash to deposit under a tick in exchange for tick shares.Tick shares represent an ownership stake in the tick value, which will experience appreciation with repayments and profitable liquidations, and depreciation with liquidation losses.
The deposit price is computed with the current tick value plus 50% of pending interest to the tick. This elevated deposit price is designed to prevent capturing the interest of repaid loans prematurely, and to encourage longer term deposits.
The
LiquidityManager
imposes a tick limit spacing requirement on deposits, to facilitate liquidity aggregation that ultimately minimizes the amount of ticks needed in a loan. Currently, this spacing requirement is set to 10%, so no deposit can instantiate a new tick with a loan limit within 10% of an existing tick loan limit.function redeem(uint128 tick, uint256 shares) external;
If sufficient cash is available in the tick, the shares are immediately redeemed at a redemption price computed from the current tick value. The remaining, unredeemed shares are scheduled for redemption within the tick, and converted to cash in the future as loans are repaid or liquidated. Scheduled redemptions may be executed at various redemption share prices, as repayment and liquidation activity affect the tick value. Redemptions are serviced in the order they are scheduled.
Only one redemption can be outstanding in a depositor's tick position at a time.
The current cash available for a redemption can be determined with the
redemptionAvailable()
getter.function withdraw(uint128 tick) external returns (uint256 amount);
function rebalance(uint128 srcTick, uint128 dstTick) external returns (uint256 amount);
The
rebalance()
function deposits cash from a redemption that is available into another tick, instead of withdrawing it.The lending interface is responsible for quoting, borrowing, repaying, refinancing, and liquidating loans with the Pool.
function quote(uint256 principal, uint64 duration, address collateralToken,
uint256[] calldata collateralTokenIds, uint128[] calldata ticks,
bytes calldata options) external view returns (uint256);
function borrow(uint256 principal, uint64 duration, address collateralToken,
uint256 collateralTokenId, uint256 maxRepayment,
uint128[] calldata ticks, bytes calldata options
) external returns (uint256);
The
borrow()
function originates a loan with the specified loan terms and liquidity ticks. The collateral may either be the Pool's native collateral token or a collateral wrapper token.A variety of additional options are supported by
borrow()
in the encoded options
parameter. These include:- Collateral wrapper context, needed by some collateral wrappers
- Collateral filter context, reserved for future use
Option data is encoded with a type-length-value (TLV) system, with a 2 byte type or tag, 2 byte length, and variable length data. See
_getOptionsData()
for more details.On successful loan origination, the
borrow()
function emits a LoanOriginated
event with an encoded loan receipt. This loan receipt is used in future repay, refinance, and liquidate operations for the loan.function repay(bytes calldata encodedLoanReceipt) external returns (uint256);
The
repay()
function repays a loan, prorating the repayment with the elapsed loan duration, and transfers the collateral back to the borrower.function refinance(bytes calldata encodedLoanReceipt, uint256 principal,
uint64 duration, uint256 maxRepayment,
uint128[] calldata ticks) external returns (uint256);
The
refinance()
function refinances a loan with the specified loan terms and liquidity ticks. Internally, it combines repay and borrow operations, and emits a LoanOriginated
event with a new loan receipt.function liquidate(bytes calldata loanReceipt) external;
The
liquidate()
function liquidates an overdue loan, transferring the collateral to the collateral liquidator for liquidation.Proceeds from the liquidation are transferred from the collateral liquidator to the Pool, and are processed in the
onCollateralLiquidated()
callback. Any surplus from the liquidation is remitted to the borrower.Collateral liquidators are responsible for liquidating loan collateral and returning the proceeds to the Pool. Collateral liquidators implement the
ICollateralLiquidator
interface to accept liquidations, while the Pool implements the ICollateralLiquidationReceiver
interface to receive the proceeds of liquidations.The primary collateral liquidator is the
EnglishAuctionCollateralLiquidator
. When a loan is liquidated with a Pool, it transfers the collateral to the liquidator. The EnglishAuctionCollateralLiquidator
starts an auction for the collateral with the first bid()
on the collateral. The auction runs for the auction duration configured at initialization. If a higher bid appears within a time extension window before the end of the auction, the contract extends the auction by a time extension, both of which are also configured at initialization. Finally, when the auction ends, the winning bidder can claim()
the collateral, the proceeds are transferred to the Pool, and then processed by the Pool in the onCollateralLiquidated()
callback.Collateral wrappers allow a Pool to recognize and accept collateral that exists in a wrapped form for a loan. This facility is useful for implementing a number of extensions to the Pool, such as bundles, airdrop receivers, and collateral in the form of promissory notes from third-party lending platforms.
Collateral wrappers are implemented as an ERC721 token that the Pool takes custody of instead of the native collateral token for a loan. Additionally, collateral wrappers implement the
ICollateralWrapper
interface, which allows a Pool to enumerate the underlying collateral for validation and to unwrap the underlying collateral for liquidation.To reduce storage requirements and for gas efficiency, collateral wrappers may use an offchain context that is provided in calldata when borrowing. This context is forwarded to the collateral wrapper when enumerating or liquidating the underlying collateral. The context is stored in the loan receipt to make it available for liquidations.
The
BundleCollateralWrapper
is the collateral wrapper deployed with all Pools. It allows a borrower to wrap multiple collateral tokens into a bundle and borrow a greater principal, multiplied by the count of collateral.A user can mint a bundle with the
mint()
function, which will transfer the specified token IDs to the bundle contract, and mint a bundle token to the user. The minted bundle token can then be used in a loan with a Pool that supports the underlying collateral. The bundle token is held by the Pool during a loan, and transferred back to the borrower on repayment. A borrower can withdraw their bundled NFTs with unwrap()
, which also burns the bundle. Bundles do not support partial withdrawals.The
NoteCollateralWrapper
(maintained in an separate repository), is a permissioned collateral wrapper that wraps the promissory notes of third-party lending platforms. This collateral wrapper allows a Pool to lend against the underlying collateral of a third-party promissory note. Its unwrap()
implementation allows for liquidating an overdue note for the underlying collateral.Last modified 2mo ago