Fixed rate, fixed term lending is by far the most common type of lending in traditional financial markets. In 2018, there was $15.3 trillion dollars of debt outstanding in U.S. corporate debt and mortgage debt markets. 88% of that debt was in terms of fixed rates; fixed rates are simply more desireable for consumers of the financial system (i.e. corporations and households) who do not want exposure to interest rate volatility. In this paper we describe Notional, an on-chain Ethereum protocol, that enables users to lend and borrow at fixed rates at predefined maturities. It is inspired by other successful Ethereum protocols such as Uniswap, Compound, and MakerDAO.
DeFi (Decentralized Finance) is an exciting and rapidly growing ecosystem of new financial products that live on the Ethereum blockchain. In 2019, the amount of funds locked up in DeFi products grew from $274M USD to $674M USD and was continuing to grow into 2020.
The benefits of DeFi are clear: users are able to seamlessly lend, borrow and exchange tokens within a financial system that is secure, private, transparent, and globally accessible. Those without access to the traditional financial system are able to participate in the DeFi ecosystem with nothing but an internet connection and tokens; no financial intermediaries exist to create barriers to entry. Since DeFi products are nothing more than autonomous computer programs, they enable an ecosystem of programmable money where different protocols integrate in order to create entirely new categories of products and businesses.
A core, missing component of this ecosystem is fixed rate, fixed term financing. Notional proposes to fill this gap. Notional is heavily inspired by the constant-product market maker used by Uniswap as well as Compound’s collaterlization mechanism. Our contributions include: the concept of fCash, periodic maturities, the portfolio, and cash settlement which we describe in the following sections.
Finally, Notional is a subset of a larger, more ambitious protocol called Notional V2. Notional V2 will enable users to hedge their exposure to any rate in DeFi, including hash rates, gas prices, token exchange rates as well as interest rates. It does all of this in a capital efficient manner in order to maximize liquidity which generates network effects. We discuss Notional V2 briefly here but will leave a full description for a separate paper.
The Notional Protocol
fCash is a tokenized representation of a fCash flow. It represents the amount of tokens (in this case Dai) that an account is either entitled to receive (
CASH_RECEIVER) or obligated to pay (
CASH_PAYER) at its designated maturity. For example, if an account holds +100 fCash tokens for a maturity at block 100, it is entitled to 100 Dai at any block greater than or equal to block 100. Similarly, -100 fCash tokens for the same maturity means that the account is obligated to pay 100 Dai at block 100. A detailed description of lending and borrowing mechanics will follow.
fCash in one maturity (i.e. due to mature at block 100) is fungible with other fCash tokens with the same maturity. However it is not directly fungible with fCash with different maturities. Also note that the entitlement to receive fCash (
CASH_RECEIVER) is freely transferrable but the obligation to pay (
CASH_PAYER) is not.
Finally, fCash tokens are not strictly ERC20 tokens because fCash tokens at different maturities are not fungible with each other. fCash tokens can be represented under the ERC1155 token standard which allows for interoperability.
Notional uses an internal accounting concept called current cash to represent matured cash flows. Since cash flows may be positive (
CASH_RECEIVER) or negative (
CASH_PAYER), current cash is represented as a signed integer (i.e. positive or negative). Current cash is different from collateral balances in this way (collateral balances can only be positive).
When a fCash token matures, its positive or negative value is added to the account’s total current cash balance. The current cash balance is used when we settle balances to collateral. This is discussed in the settlement section.
Notional introduces the notion of periodic, rolling maturities to simplify trading and pool liquidity. These maturities are defined by two governance parameters:
G_PERIOD_SIZE defines how long each periodic maturity will last in blocks.
G_NUM_PERIODS defines how many of these maturities will be traded in the future. For example, if
G_PERIOD_SIZE = 1000 and
G_NUM_PERIODS = 4 and we are starting at block 0, there will be maturities at blocks 1000, 2000, 3000, 4000. Each one of these maturities will have a pool of fCash tokens that can be bought and sold.
We have chosen to parameterize maturities with blocks instead of timestamps because this enables blockchain-native behaviors that timestamps do not. The reasoning is not immediately apparent here but this is an important choice for the full Notional V2 system. Block times for the Ethereum blockchain are non-deterministic but are designed to have a consistent average of around 13 seconds. This allows us to translate a number of blocks to an approximate time.
Each maturity defined above creates a market that functions very similarly to a Uniswap exchange. Each market is defined by the following variables and functions:
- maturity: The block height where all the fCash tokens in this market will mature.
- totalfCash: The amount of fCash tokens available for purchase.
- totalCollateral: The amount of collateral (i.e. Dai) available for purchase.
- totalLiquidity: The amount of liquidity tokens minted by liquidity providers in this market.
Trading fCash tokens for collateral or vice versa is done via the constant-product market maker formula used in Uniswap. Each trade is subject to a fee that goes to reward liquidity providers.
Lending (Collateral for fCash)
takefCash function allows users to deposit collateral (i.e. Dai) to the market in exchange for the right to receive a cash flow at maturity. This is the equivalent of lending; the user is depositing collateral for the right to receive a (hopefully larger) cash flow at maturity. From an economic standpoint, the user would simply not deposit collateral unless they were entitled to a greater amount of fCash at maturity; no one would trade 100 Dai today for 95 Dai in the future. If the exchange rate were to fall to that level, arbitrageurs could easily obtain risk-free profits by moving it back into line with the market’s expectation for interest rates over the given period.
def takefCash(maturity, daiToDeposit): # 0 < G_LIQUIDITY_FEE < 1 fee = daiToDeposit * G_LIQUIDITY_FEE fCashToReceive = maturity.totalfCash - ( (maturity.totalfCash * maturity.totalCollateral) / (maturity.totalCollateral - fee + daiToDeposit) ) # Market variables are updated. maturity.totalfCash = maturity.totalfCash - fCashToReceive maturity.totalCollateral = maturity.totalCollateral + daiToDeposit
Borrowing (fCash for Collateral)
takeDai function allows users to deposit a fCash obligation in order to receive collateral. This is the equivalent of borrowing; the user is committing to a future obligation in exchange for an amount of Dai that they can withdraw from the contract and spend as they wish. This obligation will be collateralized by ETH,
LIQUIDITY_TOKEN. See the Portfolio section for more details on this.
def takeDai(maturity, fCashObligation): # 0 < G_LIQUIDITY_FEE < 1 fee = fCashObligation * G_LIQUIDITY_FEE daiToReceive = totalCollateral - ( (totalfCash * totalCollateral) / (totalfCash - fee + fCashObligation) ) # Market variables are updated. totalCollateral = totalCollateral - daiToReceive totalfCash = totalfCash + fCashObligation
Note that the amount of Dai and fCash exchanged in the two trades described above (
takefCash) are calculated when the trade is made. This means, in effect, the interest rate for the user is fixed from that point on. A lender will not have to deposit any more Dai for the cash flow that they are promised; the borrower will not have to pay more fCash for the Dai they received.
For example, a lender deposits 100 Dai in exchange for 105 fCash tokens that mature in 1 year. Once this trade is made, the lender has entered into a fixed rate loan at a 5% annualized rate for a term of 1 year. From this point on, none of these terms will change.
This does not mean that the next lender to trade will also receive the same 5% annualized rate. Since the amount of
totalfCash have changed after the previous trade, the next lender will receive a different fixed rate.
This is how Notional provides fixed rates at fixed maturities.
Borrowers and lenders change the exchange rate by taking from one side of the market and depositing on the other. Without some amount of liquidity on both sides of the market these interactions would not be possible. This is where the concept of liquidity tokens plays a role. Again, this is heavily inspired by the Uniswap liquidity token model.
Liquidity providers can provide liquidity to a market at a specfied maturity by calling the
removeLiquidity functions. When a liquidity provider adds liquidity, they deposit Dai and fCash at the prevailing exchange rate to both sides of the market. The provider submits the amount of fCash to add and the corresponding amount of Dai is calculated using the current exchange rate. Liquidity tokens are minted to account for the provider’s contribution. Since the liquidity provider is depositing fCash, we create a
CASH_PAYER token to represent the obligation a liquidity provider has to provide that fCash when the market matures. The net present value of the
LIQUIDITY_TOKEN will partially offset this obligation.
# Implied Market Interest Rate = 5% maturity.totalfCash = 1050 maturity.totalCollateral = 1000 maturity.totalLiquidity = 1050 def addLiquidity(maturity, fCashToAdd, maxDaiToAdd): # Both of these are in proportion to the fCash market. tokensToMint = maturity.totalLiquidity * fCashToAdd / maturity.totalfCash daiRequired = maturity.totalCollateral * fCashToAdd / maturity.totalfCash # Ensures that the provider can bail out if the rate has moved against them assert(daiRequired <= maxDaiToAdd) # Update the markets maturity.totalfCash += fCashToAdd maturity.totalCollateral += daiRequired maturity.totalLiquidity += tokensToMint # Remove the Dai balance as well as account for a fCash obligation daiBalances[msg.sender] -= daiRequired accountPortfolio[msg.sender].push(Asset(CASH_PAYER, maturity, fCashToAdd)) accountPortfolio[msg.sender].push(Asset(LIQUIDITY_TOKEN, maturity, tokensToMint))
When liquidity providers want to stop providing liquidity, they are able to remove their tokens and receive Dai and fCash in proportion to the market. Note that the
CASH_RECEIVER here will offset the
CASH_PAYER that was added to the liquidity provider’s portfolio when they deposited the tokens. The pseudocode is below:
def removeLiquidity(maturity, tokensToRemove): # Both of these are in proportion to the ownership stake in the liquidity pool daiOwed = maturity.totalCollateral * tokensToRemove / maturity.totalLiquidity fCashOwed = maturity.totalfCash * tokensToRemove / maturity.totalLiquidity # Update the markets maturity.totalfCash -= fCashOwed maturity.totalCollateral -= daiOwed maturity.totalLiquidity -= tokensToRemove # Credit the balances back to the liquidity provider daiBalances[msg.sender] += daiOwed accountPortfolio[msg.sender].push(Asset(CASH_RECEIVER, maturity, fCashOwed)) accountPortfolio[msg.sender].push(Asset(LIQUIDITY_TOKEN, maturity, -tokensToRemove))
Liquidity providers are incentivized by a transaction fee (parameterized in
G_LIQUIDITY_FEE) on every transaction. The advantage of this design is that liquidity providers can passively provide liquidity and earn transaction fees in the market.
A key consideration for liquidity providers is the understanding that they are providing liquidity in a single maturity. Once that maturity passes, their liquidity tokens will be converted to a Dai balance and a current cash balance. In order to provide liquidity in a new market, they will have to make a smart contract call to add liquidity to the new maturity.
Each user in Notional has an account represented by their Ethereum address.
The Notional protocol allows for two types of collateral to be deposited: Dai and ETH. Notional V2 will be designed to support multiple collateral types but for simplicity Notional is designed only to support the most common lending and borrowing collateral. There is nothing in the design of Notional that prevents additional collateral types from being supported; it was simply a decision to omit this feature from the initial release.
ETH is used to collateralize Dai loans and is subject to a collateralization ratio. We use the DAI/ETH Uniswap exchange to get the current exchange rate for ETH. The reasoning for this is that we will need to exchange ETH for Dai on Uniswap during liquidation and therefore the Uniswap ETH/DAI rate is an accurate reflection of this price.
Haircuts (Collateralization Ratio)
When calculating the value of an account’s holdings for the purposes of collateralization checks, Notional will apply a discount to every collateral asset (i.e. ETH and fCash) that reflects the riskiness of that asset. An asset’s riskiness is defined as the magnitude by which its value relative to Dai is likely to change. This discount is known as a haircut. Haircuts ensure that there is ample time for an account to be liquidated before a market move pushes it into insolvency.
In addition to collateral balances, each account has a portfolio which is an array of its
LIQUIDITY_TOKEN balances. The reason for storing this in an array as opposed to mappings is so that the contract can iterate over a portfolio and calculate the net present value of all the assets that the account holds. We do this in order to calculate free collateral. It is important to note that each asset in the portfolio contains some amount of risk and therefore we apply a haircut to the value of each asset.
An additional benefit of the portfolio construction is that it allows for Notional to net out opposing
CASH_RECEIVER positions. If an account has an obligation of 100 fCash in 1 year (
CASH_PAYER), but then buys the right to receive 50 fCash (
CASH_RECEIVER) at the same maturity, its net position is now a
CASH_PAYER of 50 fCash in 1 year. The portfolio construction ensures that there is always only one entry per maturity that represents the net position of the account no matter how many trades they have made in that maturity.
It may be odd to see in the following sections that we are valuing fixed rate fCash tokens against a market rate and then applying a haircut. It’s important to realize that when you lend and borrow in Notional and you do not trade again your rate will stay fixed. However, if you need to exit that position before maturity you will have to trade with the market at the prevailing rate. Therefore we value portfolios as if they had to be liquidated immediately; this accounts for the worst case value required to maintain the solvency of the protocol.
The formulas that define the present value for each one of these assets are as follows:
The value of a fCash obligation is the cost to purchase an equal number of
CASH_RECEIVER tokens at the same maturity such that the position is net zero. The formula is as follows:
G_HAIRCUT = 0.01 def getPayerValue(maturity, fCashAmount): daiCost = ( (maturity.totalCollateral * market.totalfCash) / (maturity.totalfCash - fCashAmount)) - maturity.totalCollateral ) / (1 - G_LIQUIDITY_FEE) # We add a buffer here to account for changes in the market rate daiCost = daiCost * (1 + G_HAIRCUT) if daiCost > fCashAmount: # However we cap this value at the total obligation return fCashAmount else: return daiCost
In the case of negative interest rates (when there is more
totalfCash in the market), the cost to purchase
CASH_RECEIVER tokens will be greater than the actual obligation itself and therefore we cap the value of
CASH_PAYER tokens at their face value (i.e. the value of 100
CASH_PAYER tokens will never be more than 100).
The value of a fCash entitlement is the discounted present value of the token amount at the current market rate. Note that this market rate is not the same as the fixed rate that the account is entitled to receive. The formula for this is:
G_HAIRCUT = 0.01 def getReceiverValue(maturity, fCashAmount): fee = fCashAmount * G_LIQUIDITY_FEE return market.totalCollateral - ( (maturity.totalCollateral * market.totalfCash) / (maturity.totalfCash + fCashAmount - fee) ) * (1 - G_HAIRCUT)
Liquidity tokens are a claim to some amount of Dai as well as a positive fCash flow (
CASH_RECEIVER). The value of a liquidity token is the present value of those claims.
def getTokenValue(maturity, tokenAmount): daiOwed = maturity.totalCollateral * tokenAmount / market.totalLiquidity fCashOwed = maturity.totalfCash * tokenAmount / market.totalLiquidity return daiOwed + getReceiverValue(maturity, fCashOwed)
Free collateral represents the amount of excess collateral an account holds beyond what it needs to collateralize its obligations. It represents the buffer that the account has to withstand market volatility, the amount of collateral an account is allowed to withdraw from Notional, as well as the approximate maximum amount that the account is allowed to borrow. There are multiple sources of positive collateral: Dai, ETH,
CASH_RECEIVER tokens and
CASH_PAYER tokens represent obligations and are the only source of negative collateral. Cash balances are discussed in settlement and they may be positive or negative.
The formula to calculate free collateral is as follows:
G_ETH_HAIRCUT = 0.30 def freeCollateral(): portfolioValue = sum(getReceiverValue(...)) + sum(getTokenValue(...)) - sum(getPayerValue(...)) return daiBalance + cashBalance + (ethBalance * daiEthExchangeRate * (1 - G_ETH_HAIRCUT)) + portfolioValue
CASH_RECEIVER tokens have positive value, they can be used as collateral for borrowing in other maturities. The following portfolio below is completely valid and holds no current collateral balances. This is called cross margin.
Computing resources are heavily constrained in the EVM so settling matured tokens requires a two stage process. First we settle matured assets to cash balances, then we give accounts the option of converting their cash balances to collateral if they want to withdraw Dai from Notional. Note that
cashBalance is treated as one to one equivalent to Dai balances (as seen in free collateral) so it can be used to collateralize positions as if it were Dai. We can do this because the design of Notional assures that for every positive cash balance there is an offsetting negative cash balance somewhere in the system.
The reasoning for this added complexity is because O(n) operations must happen when a maturity block is reached (i.e. all accounts that participated in the maturity need to be updated). In other protocols like Uniswap or Compound, each transaction has O(1) operations - only the user’s balances and the liquidity pools need to be updated. Because O(n) operations within a single block are not possible, we delay the evaluation of these operations until the account wants to trade again or withdraw collateral.
The actions described in this section are unauthenticated, meaning any account can settle the portfolio of any other account. This is required so that unavailable accounts cannot prevent the rest of the system from settling their balances. All the actions described simply reconfigure the way a portfolio is represented in Notional, they do not change the net position of the portfolio.
Settling Matured Assets
First we settle all matured fCash and liquidity tokens to an account’s current cash balance. The formula for this is simple:
def settlePortfolio(portfolio): for asset in portfolio if maturity < blockHeight: if asset is CASH_PAYER: cashBalances -= asset.amount else if asset is CASH_RECEIVER: cashBalances += asset.amount else if asset is LIQUIDITY_TOKEN: cashBalances += asset.fCashOwed # Note that liquidity tokens will take all the remaining Dai # left in the market daiBalances += asset.daiOwed
Withdrawing Cash Balances
In a simplified scenario, there are only two accounts, a lender and a borrower. Their cash balances are below and the free collateral condition requires Barbara to have sufficient collateral to cover their obligations. Let’s assume the DAI/ETH exchange rate is 100 Dai/ETH.
|Account||Maturity||Asset||Value||ETH Balance||Dai Balance|
At maturity, both portfolios are settled to cash balances. At this point, Barbara may elect to deposit 1000 Dai into her Dai balances in order to cover her obligation. However, in this scenario she does not.
|Account||Cash Balance||ETH Balance||Dai Balance|
Leonard would like to withdraw his 1000 Dai. However, there is no Dai in the system - only a negative cash balance. Leonard can observe all of this off-chain. He sees that Barbara has a negative cash balance and enough ETH to cover it. He submits a transaction to Notional and specifies that he would like to settle his 1000 cash balance against Barbara. Notional sells 10 ETH on Uniswap to raise 1000 Dai and transfers it to Leonard’s account. It also erases the negative cash balance that Barabara has accrued. The system is now:
|Account||Cash Balance||ETH Balance||Dai Balance|
Leonard can now withdraw his 1000 Dai from Notional.
Raising Cash From Portfolio
The first step in liquidation is to sell off assets in the portfolio in order to generate Dai to de-risk the portfolio. Notional looks at sources of positive collateral and converts them to Dai starting with ETH and then
LIQUIDITY_TOKEN. It sells them on Uniswap or Notional markets until the free collateral shortfall is covered. Take the following portfolio:
|Maturity||Asset||Market Value||Post Haircut|
|ETH Balance||ETH = 5; ETH/DAI = 220||+1100||+880|
Let’s say this portfolio was collateralized when ETH/DAI was priced at 275, but it has rapidly dropped to 220. The portfolio is at risk of becoming insolvent, we must liquidate it. Notional proceeds as follows:
|Liquidation Procedure||Dai Raised||Free Collateral|
|Sell 5 ETH||1100||-30|
|Trade 36 units of
Now the portfolio looks as follows:
|Maturity||Asset||Market Value||Post Haircut|
|ETH Balance||ETH = 0; ETH/DAI = 100||0||0|
De-Risking the Portfolio
This, however, does not completely solve the problem. If the market for
CASH_PAYER tokens at block 300 moves against the portfolio, it will have to be liquidated again. In the second step of liquidation, Notional takes the Dai Balance raised and purchases offsetting
CASH_RECEIVER tokens at Block 300 in order to offset the obligation the account has at that maturity. Let’s say that 1130 Dai purchases 700
CASH_RECEIVER tokens at Block 300. Now the portfolio looks as follows:
|Maturity||Asset||Market Value||Post Haircut|
|ETH Balance||ETH = 0; ETH/DAI = 100||0||0|
We have described Notional, a decentralized, automated market making protocol for fixed rate, fixed term lending and borrowing on Ethereum. While we are very excited about this protocol and the benefits it will provide to the greater DeFi ecosystem, this is only the beginning for the Notional V2 protocol. Notional represents a subset of the greater functionality that we plan to deliver with the full Notional V2 protocol.
The full Notional V2 protocol will include many of the concepts we’ve introduced in this paper and extend them to enable users to receive fixed rates over fixed maturities against any data that is available on the Ethereum blockchain, including token exchange rates, hash rates, gas prices, as well as variable interest rates including Compound, dydx, and Dai Savings Rate.
The full vision of Notional V2 is a system that allows lenders and borrows to get fixed rate exposure, miners to fix their future mining revenues, crypto startups to hedge their future gas costs, and many new use cases that we have yet to discover.