Notional Whitepaper

Notional Whitepaper

Abstract

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[1]. 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.

Introduction

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[4] 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. We also introduce a novel new liquidity curve that we have designed specifically for trading our fCash tokens.

The Notional Protocol

fCash

fCash is a tokenized representation of a fCash flow. It represents the amount of tokens (i.e. 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 timestamp 100, it is entitled to 100 Dai at any time greater than or equal to timestamp 100. Similarly, -100 fCash tokens for the same maturity means that the account is obligated to pay 100 Dai at timestamp 100. A detailed description of lending and borrowing mechanics will follow.

fCash in one maturity (i.e. due to mature at timestamp 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.

Current Cash

Notional uses an internal accounting concept called current cash to represent deposits and 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).

When a fCash token matures, its positive or negative value is added to the account's total current cash balance. This is discussed in the settlement section.

Maturities

Notional introduces the notion of periodic, rolling maturities to simplify trading and pool liquidity. These maturities are defined by two governance parameters: G_NUM_MATURITIES and G_MATURITY_LENGTH. G_MATURITY_LENGTH defines how long each periodic maturity will last in seconds. G_NUM_MATURITIES defines how many of these maturities will be traded in the future. For example, if G_MATURITY_LENGTH = 1000 and G_NUM_MATURITIES = 4 and we are starting at time 0, there will be maturities at timestamps 1000, 2000, 3000, 4000. Each one of these maturities will have a pool of fCash tokens that can be bought and sold.

Markets

Each maturity defined above creates a market that functions similarly to a Uniswap exchange with an important distinction in how the rate curve functions. Each market is defined by the following variables:

Variables

  • maturity: The timestamp where all the fCash tokens in this market will mature.

  • totalfCash: The amount of fCash tokens available for purchase.

  • totalCurrentCash: The amount of cash (i.e. Dai) available for purchase.

  • totalLiquidity: The amount of liquidity tokens minted by liquidity providers in this market.

  • rateAnchor: Offsets the interest rate curve from zero, can be thought of this as a bias term.

  • rateScalar: A scalar that determines the slope of the interest rate curve.

  • lastImpliedRate: The implied period rate of the most recent trade.

Trading

Trading fCash tokens is done via a special liquidity curve that has been designed to minimize slippage when trading fCash tokens. Consider Uniswap's constant product liquidity curve; it is bounded at zero and positive infinity. The advantage of this curve is that a trade can always find a market price regardless of how much liquidity is in the pool. The disadvantage is that exchange rates must move large amounts in order to accomodate this; the amount of slippage incurred would be intolerable for trading fixed term cash flows. For example, if you were to trade 1% of the liquidity pool using the constant product curve for 1-month fCash, it could result in a 10% change in the interest rate. On an annualized basis, this means a 120% change in the interest rate. This is simply too volatile for a useful trading experience. Also note that this problem explodes exponentially as the token gets closer to maturity -- that 10% change in the interest rate becomes almost a 50% change when the fCash is one week from maturity (a 600% change on an annualized basis). This is why Notional has a special liquidity curve designed for trading fCash that we describe next.

Interest Rate Curve

The liquidity curve in Notional is based on the logit function which allows us to greatly reduce slippage and create upper and lower bounds for interest rates. The detailed algorithm for calculating the liquidity curve along with proofs of its required properties are described in Appendix A. Here, we will just outline some of the important properties of the curve.

This curve has the properties we need. The center part of the curve is relatively flat and therefore there will be less slippage when trading under normal conditions. The edges of the curve change exponentially and will incentivize the market to trade the interest rate back to the flatter, middle area. There are three ways that we can control the shape of this curve: rateAnchor and rateScalar are governance parameters that determine the offset from zero and the slope of the curve, respectively. The proportion is determined by liquidity providers and trading in the market in general. Like Uniswap, the first liquidity provider will be able to set this proportion and therefore impact the initial rate. Importantly, however, the ratio of current cash and fCash the liquidity provider adds does not determine the interest rate in isolation (like it would in Uniswap), it is one factor in combination with the two governance parameters.

The general formula for calculating interest rates is:

exchangeRate = ln(proportion / (1 - proportion)) / rateScalar + rateAnchor

where:
daiAmount = fCashAmount / exchangeRate
proportion = totalfCash / (totalfCash + totalCurrentCash)

A trade of 1% of the pool under this liquidity curve would would move the proportion from 0.5 to 0.495. With a rateScalar value of 100, this trade would move the exchange rate 0.02%. That equates to a 0.24% move in the annualized interest rate -- a much more reasonable slippage.

Terminology

When discussing interest rates, we distinguish between three different types of rates for clarity:

  • Exchange Rate: the spot rate at which fCash is exchanged for current cash.

  • Implied Period Rate: the interest rate over the period that is implied by the exchange rate (i.e. impliedPeriodRate = (exchangeRate - 1) * MATURITY_LENGTH / timeToMaturity)

  • Implied Annual Rate: the implied period rate, annualized

Lending (Cash for fCash)

The takefCash function allows users to deposit cash (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 cash for the right to receive a (hopefully larger) cash flow at maturity. From an economic standpoint, the user would simply not deposit cash 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. The getExchangeRate function is described in detail in Appendix A.

def takefCash(maturity, fCashToReceive):
    # 0 < G_LIQUIDITY_FEE < 1
    fee = (G_LIQUIDITY_FEE * timeToMaturity) / MATURITY_LENGTH
    tradeExchangeRate = getExchangeRate(maturity, fCashToReceive) - fee
    daiToDeposit = fCashToReceive / tradeExchangeRate

    # Market variables are updated.
    maturity.totalfCash = maturity.totalfCash - fCashToReceive
    maturity.totalCurrentCash = maturity.totalCurrentCash + daiToDeposit

Borrowing (fCash for Cash)

The takeCurrentCash function allows users to deposit a fCash obligation in order to receive cash. 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, CASH_RECEIVER or LIQUIDITY_TOKEN. See the Portfolio section for more details on this. The getExchangeRate function is described in detail in Appendix A.

def takeCurrentCash(maturity, fCashObligation):
    # 0 < G_LIQUIDITY_FEE < 1
    fee = (G_LIQUIDITY_FEE * timeToMaturity) / MATURITY_LENGTH
    tradeExchangeRate = getExchangeRate(maturity, fCashObligation) + fee
    daiToDeposit = fCashObligation / tradeExchangeRate

    # Market variables are updated.
    totalfCash = totalfCash + fCashObligation
    totalCurrentCash = totalCurrentCash - daiToReceive

Fixed Rates

Note that the amount of Dai and fCash exchanged in the two trades described above (takeCurrentCash and 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 totalCurrentCash and 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.

Providing Liquidity

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. 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 addLiquidity and 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.totalCurrentCash = 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.totalCurrentCash * 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.totalCurrentCash += 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.totalCurrentCash * tokensToRemove / maturity.totalLiquidity
    fCashOwed = maturity.totalfCash * tokensToRemove / maturity.totalLiquidity

    # Update the markets
    maturity.totalfCash -= fCashOwed
    maturity.totalCurrentCash -= 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.

User Accounts

Each user in Notional has an account represented by their Ethereum address.

Settling Negative Cash Balances

Cash balances can either be positive or negative. Positive cash balances can be withdrawn from Notional after passing a free collateral check. These positive cash balances can only be withdrawn if Notional actually holds the corresponding tokens, this will not be the case without settling negative 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. For this example, let's also assume that the Notional contract holds no Dai balance.

Notional Dai Balance: 0

At maturity, both portfolios are settled to cash balances.

Notional Dai Balance: 0

At this point, Barbara may elect to deposit 1000 Dai into her Dai balances in order to cover her obligation. This will net out her -1000 cash balance and add 1000 to Notional's Dai balance. Leonard will now be able to withdraw 1000 Dai from Notional.

Notional Dai Balance: 1000

However, imagine a scenario where Barbara does not deposit 1000 Dai to repay her debt. Leonard would like to withdraw his 1000 Dai. However, there is no Dai in the system - only a negative cash balance. Enter Sally the settler. Sally sees that Notional will give her a discount on ETH if she deposits 1000 Dai into Barbara's account in return for purchasing her ETH. After Sally settles out Barbara's debt (purchasing 11 ETH for 1000 Dai at an exchange rate of 90 Dai to 1 ETH).

Notional Dai Balance: 1000

Leonard can now withdraw his 1000 Dai from Notional.

Since LIQUIDITY_TOKEN positions have a claim on Dai, if there is insufficient Dai and ETH balances to settle a negative cash position, liquidity tokens will be withdrawn from the markets for their dai portion in order to settle the cash position. See portfolio assets for a full explanation.

Portfolio

In addition to cash balances, each account has a portfolio which is an array of its CASH_PAYER, CASH_RECEIVER and 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 [currency buffer](#currency buffer-(collateralization-ratio)) to the value of each asset.

An additional benefit of the portfolio construction is that it allows for Notional to net out opposing CASH_PAYER and 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.

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 settleMaturedAssets(portfolio):
    for asset in portfolio if maturity < blockTime:
        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

Liquidity Token

Liquidity tokens have a claim on current cash and CASH_RECEIVER tokens in the liquidity pool of a specified maturity. As trades occur, the claim the liquidity tokens have will shift between current cash and the cash receivers. Since CASH_RECEIVER tokens hold no value in the free collateral calculation, the amount of current cash an account has available to collateralize other obligations will shift as trades occur.

In order to give liquidators time to react to this while the account still has enough cash on had to cover their shortfall we will haircut both the current cash and cash receiver claims on the liquidity token, using the G_LIQUIDITY_HAIRCUT parameter. The calculation is as follows:

G_LIQUIDITY_HAIRCUT = 0.95

def getLiquidityTokenValue(maturity, tokenAmount):
    daiClaim = maturity.totalCurrentCash * tokenAmount / market.totalLiquidity
    fCashClaim = maturity.totalfCash * tokenAmount / market.totalLiquidity

    if maturity > blockTime:
        # If trades can still occur at this maturity, the daiClaim and fCashClaim
        # are not finalized. We discount them both to account for this risk.
        daiClaim = daiClaim * G_LIQUIDITY_HAIRCUT
        fCashClaim = fCashClaim * G_LIQUIDITY_HAIRCUT

    return (daiClaim, fCashClaim)

Cash Receiver Value

CASH_RECEIVER represents cash an account is scheduled to receive at maturity. Any time before maturity, CASH_RECEIVER tokens may be sold for current cash if there is sufficient liquidity in the relevant cash market. Since there is no guarantee that this liquidity will be available if a CASH_RECEIVER token must be liquidated to pay off obligations, we apply a haircut to the value of these tokens. We haircut it by 50% on an annualied basis. Therefore 100 Dai CASH_RECEIVER tokens maturing in 1 year will be valued at 50 Dai. 100 Dai CASH_RECEIVER tokens maturing in 6 months will be valued at 75 Dai. We apply a max haircut value to ensure that strange behaviors do not occur as these assets get close to maturity. The haircut applied follows the formula:

G_FCASH_HAIRCUT = 0.5
G_FCASH_MAX_HAIRCUT = 0.95

def getCashReceiverValue(maturity, blockTime, amount):
    annualizedHaircut = G_FCASH_HAIRCUT * (maturity - blockTime) / SECONDS_IN_YEAR
    postHaircutValue = amount * annualizedHaircut

    maxPostHaircutValue = amount * G_FCASH_MAX_HAIRCUT

    return min(postHaircutValue, maxPostHaircutValue)

In the future, this haircut may be allowed to reflect the rate at which these tokens trade on the market.

Cash Ladder

Each portfolio has a cash ladder which represents the net amount that it is scheduled to pay or receive at each future maturity. The cash ladder is used to calculate the collateral requirement for the account. Take the example portfolio:

The cash ladder would be computed as follows, with a 5% haircut given to liquidity token claims (assume a claim of 500 Dai and 500 fCash).

Free Collateral

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 three sources of positive collateral: cash balances, LIQUIDITY_TOKEN and CASH_RECEIVER. CASH_PAYER tokens represent obligations and are the only source of negative collateral. CASH_PAYER tokens convert to negative cash balances as maturity which will continue to be a source of negative collateral.

The free collateral figure is derived from the net per currency requirements converted to ETH. When net per currency requirements are negative, we apply a currency buffer to account for the risk of collateralizing the debt with a foreign currency.

G_ETH_BUFFER = 0.30

def freeCollateral():
    netDaiBalance = daiCashBalance
        + sum(getLiquidityTokenValue(...)['daiClaim'])
        + sum([
            getCashReceiverValue(...)
            for asset in portfolio
            if asset.assetType == "CASH_RECEIVER"
        ])
        - sum([
            asset.notional
            for asset in portfolio
            if asset.assetType == "CASH_PAYER"
        ])

    netEthBalance = netDaiBalance * daiEthExchangeRate

    if netEthBalance < 0:
        netEthBalance = netEthBalance * (1 - G_ETH_BUFFER)

    return netEthBalance

Currency Buffer (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 liquidity tokens) that reflects the riskiness of that asset. An asset's riskiness is defined as the magnitude by which its value is likely to change. This discount is known as a currency buffer. Currency buffers ensure that there is ample time for an account to be liquidated before a market move pushes it into insolvency. Currency buffers also incorporate the cost paid to liquidators for settling cash or liquidating accounts as described below.

Liquidation

When an account's free collateral drops below zero it can be liquidated in order to ensure that it remains solvent. This can occur if the price of ETH/DAI drops. Liquidation is discussed in further depth in Appendix B

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. There are potentially two sources of positive collateral: portfolio assets and cash balances. Notional first extracts collateral from portfolio assets before proceeding on to cash balances.

Portfolio Assets

CASH_RECEIVER and LIQUIDITY_TOKEN assets are potential sources of positive collateral within an account's portfolio. Currently, Notional only uses the liquidity tokens' claim on Dai as an asset. In the event of liquidation, these tokens would be removed from the market and the account would be credited with its claim on both Dai and fDai.

Note that the free collateral calculation already accounts for the daiClaim in the liquidity token. Therefore, only the haircut portion of this daiClaim is actually available to recollateralize the account. For example, in the cash ladder section we describe an account that has a 475 post haircut Dai claim. The difference between this and the actual 500 Dai claim (25 Dai) is what is available to recollateralize the account.

Currency Balances

ETH can be held as collateral for a Dai loan. Take the following portfolio that has a 30% currency buffer on the ETH/DAI price:

Let's say this portfolio was collateralized when ETH/DAI was priced at 110, but it has since dropped to 100. The portfolio is at risk of becoming insolvent, we must liquidate it. Notional will allow a liquidator to purchase the ETH for Dai at a discount to the prevailing price and deposit Dai to offset the debt.

Now the portfolio looks as follows (note that the -1000 Dai debt is net out with the 1000 Dai balance)

Liquidating fCash Assets

A portfolio may be undercollateralized and only have CASH_RECEIVER assets as a source of positive collateral. We do not allow liquidation of until this condition is met -- meaning there are no liquidity tokens or positive cash balances in the portfolio. We do this because liquidating fCash is risky on multiple fronts. First we must sell the entire balance of the fCash asset into the market, we cannot calculate a portion of the CASH_RECEIVER to sell (see Appendix A: Calculating Current Cash Requirement). Secondly, it is unclear if the market will have sufficient liquidity to purchase an arbitrary CASH_RECEIVER asset. When market proportions reach 90% fCash the calculations will start to overflow and prevent trading. Finally, if this is the case then we allow the liquidator to purchase the fCash asset at a severe discount (see: Cash Receiver Value)

In the event that CASH_RECEIVER assets will be liquidated, we first attempt to sell them into their respective market. If that fails then we allow the liquidator to purchase the assets.

Appendix A: Liquidity Curve

The algorithm for calculating the corresponding amount of current cash for a given fCash amount is as follows:

  1. Find the new rate anchor for the current blocktime

  2. Calculate the trade exchange rate for the given fCash amount

  3. Add the trading fee to the trade exchange rate

  4. Calculate the current cash amount from the exchange rate

  5. Update the current cash and fCash pools accordingly

  6. Calculate the final implied period rate for the market and save it

Note the difference between exchange rates and implied rates. Exchange rates are the spot rate that fCash will be exchanged for current cash. Implied period rates are interest rates normalized to the length of the maturity. We are able to compare implied period rates across time periods; exchange rates can only be compared at a single point in time.

This is the Python psuedocode for calculating interest rates in Notional:

def getDaiAmount(maturity, fCashAmount):
    # if fCashAmount > 0, we are borrowing
    # if fCashAmount < 0, we are lending
    timeToMaturity = maturity.blockTime - currentBlockNum
    maturity.rateAnchor = getNewRateAnchor(maturity, timeToMaturity)

    # Assert that the implied rate we have now is exactly equal to the last implied rate we
    # traded at. See: Interest Rate Continuity
    assert(getImpliedRate(getExchangeRate(maturity, timeToMaturity, 0)) == maturity.lastImpliedRate)
    tradeExchangeRate = getExchangeRate(maturity, timeToMaturity, fCashAmount)

    # The fee decreases as we get closer to maturity
    fee = G_LIQUIDITY_FEE * timeToMaturity / MATURITY_LENGTH
    tradeExchangeRate = tradeExchangeRate + fee

    # We do not allow negative interest rates
    assert(tradeExchangeRate >= 1)

    daiAmount = fCashAmount / tradeExchangeRate

    # We update the liquidity pools to reflect the trade
    maturity.totalfCash += fCashAmount
    maturity.totalCurrentCash -= daiAmount

    # We update the lastImpliedRate, see: Interest Rate Continuity
    finalExchangeRate = getExchangeRate(maturity, timeToMaturity, 0)
    maturity.lastImpliedRate = getImpliedRate(finalExchangeRate, timeToMaturity)

    return daiAmount

def getNewRateAnchor(maturity, timeToMaturity):
    exchangeRate = getExchangeRate(maturity, timeToMaturity, 0)
    # Converts the spot exchange rate to an implied period rate
    impliedPeriodRate = getImpliedRate(exchangeRate, timeToMaturity)
    # Converts the difference in implied period rates back to an exchange rate
    rateDifference = (impliedPeriodRate - market.lastImpliedRate) * timeToMaturity / MATURITY_LENGTH

    # Adjusts the rate anchor to ensure Interest Rate Continuity
    return maturity.rateAnchor - rateDifference

def getExchangeRate(maturity, timeToMaturity, fCashAmount):
    # See exchange rate formula above
    proportion = (maturity.totalfCash + fCashAmount)
        / (maturity.totalfCash + maturity.totalCurrentCash)

    # The rate scalar becomes less sensitive as we get closer to maturity. See: Rolldown to Maturity
    rateScalar = maturity.rateScalar * MATURITY_LENGTH / timeToMaturity
    rate = Math.ln(proportion / (1 - proportion)) / rateScalar + maturity.rateAnchor

    return rate

def getImpliedRate(exchangeRate, timeToMaturity):
    # Scale the exchange rate to something that is comparable across blocks
    return (exchangeRate - 1) * MATURITY_LENGTH / timeToMaturity

Rolldown to Maturity

As the curve approaches maturity, a given change in the proportion will produce an exponential change in the implied period rate of fCash. Let's assume that rateScalar is constant:

impliedPeriodRate = (ln(p / (1 - p)) / rateScalar + rateAnchor - 1) * MATURITY_LENGTH / timeToMaturity
d impliedPeriodRate / d proportion = -((p - 1) * p)^-1 * (MATURITY_LENGTH / timeToMaturity)
(d impliedPeriodRate / d proportion) / d timeToMaturity = MATURITY_LENGTH / timeToMaturity^2

In order to counteract this, we change rateScalar as the curve approaches maturity:

rateScalar = rateScalar_0 * MATURITY_LENGTH / timeToMaturity
impliedPeriodRate = (ln(p / (1 - p)) * (timeToMaturity / (rateScalar * MATURITY_LENGTH)) + rateAnchor - 1) * MATURITY_LENGTH / timeToMaturity
                    = ln(p / (1 - P)) / rateScalar + [(rateAnchor - 1) * MATURITY_LENGTH / timeToMaturity]
d impliedPeriodRate / d proportion = -((p - 1) * p)^-1
(d impliedPeriodRate / d proportion) / d timeToMaturity = 0

Now we see that the implied period rate for a given change in proportion does not change as the curve approaches maturity.

Interest Rate Continuity

Since we vary the scalar as we approach maturity, it means that implied interest rates will also change without any trading. This creates the possibility that traders could game the system by simply waiting for the rate to change and gradually siphon off value from liquidity providers. To counter act this, we save the implied period rate at every trade. We update the rate anchor before calculating tradeExchangeRate such that the new implied rate matches the last implied rate the market traded at. The proof is as follows:

Prove: ir_0 = (exchangeRate(btm_1) - 1) * (PS / btm_1)
            = [ln(p / (1 - p)) * (btm_1 / (s * PS)) + a_1 - 1] * (PS / btm_1)
where:
p := proportion of liquidity pools (does not change between the two rates)
PS := period size
s := rate scalar
btm_1 := time to maturity at t = 1
btm_0 := time to maturity at t = 0
ir_1 := implied rate at t = 1
ir_0 := implied rate at t = 0
a_1 := rate anchor at t = 1
a_0 := rate anchor at t = 0
a := initial rate anchor

Note that:
ir_t = [ln(p / (1 - p)) * (btm_t / (s * PS)) + a_(t-1) - 1] * (PS / btm_t)
     = ln(p / (1 - p)) / s + [a_(t-1) - 1] * (PS / btm_t)

Since p does not change when we calculate the new anchor, the natural log terms cancel in:
a_1 = a_0 - [ir_1 - ir_0] * (btm_1 / PS)

Therefore:
a_1 = a_0 - [(a_0 - 1) * (PS / btm_1) - (a - 1) * (PS / btm_0)] * (btm_1 / PS)
    = 1 + (a - 1) * (btm_1 / btm_0)

Substituting back into our initial equality:
ir_0 = ln(p / (1 - p)) / s + [a - 1] * (PS / btm_0)

Which is our original equation for the implied period rate.

Traded Exchange Rate Proof

We need to ensure that the users trade at an exchange rate which is worse or equal to the pool's exchange rate after the trade. If this is not true, the curve is vulnerable to manipulation. Since the logit curve is monotonic (i.e. always increasing or decreasing), it is enough to prove this using the change in the proportion before and after the trade.

Here is the proof that this is the case. When X is positive, we are selling fCash (i.e. borrowing) and we are buying fCash (i.e. lending) when X is negative.

X = fCashTraded
Y = currentCashTraded
tradeProportion = (totalfCash + X) / (totalfCash + totalCurrentCash)
endProportion = (totalfCash + X) / (totalfCash + totalCurrentCash + (X - Y))
X = Y * exchangeRate
exchangeRate >= 1

if X > 0, then X - Y > 0, therefore tradeProportion > endProportion
if X < 0, then X - Y < 0, therefore tradeProportion < endProportion

Calculating Current Cash Requirement

This curve only allows fCash to be specified by the user. It is not possible for us to solve analytically for the inverse of the formula (i.e. given current cash, how much fCash is required). We can do this via numerical approximation in an off-chain SDK.

Calculating fCash requirements given current cash is not required in any on-chain interactions. In liquidation and settlement, the contract will be calculating the net present value of fCash flows. Therefore it will have an input of a fCash amount and expect an output of the corresponding current cash amount. It will not be the case that we would need to price the fCash amount of some current cash amount since current cash is liquid and does not carry risk.

Appendix B: Liquidation

Notional allows any currency deposited in the system to be used as collateral against debts in another currency. Any currency (if fCash markets are listed by governors of the system) can also be traded as fCash. This also means that LIQUIDITY_TOKENs may exist in any currency. Because these are a source of positive free collateral we may have to liquidate them in order to recapitalize an account. Doing so is non trivial and so the algorithm will be described here.

Liquidation

The goal of liquidation is to trade either assets in an account's portfolio or positive cash balances in order to repay debts that the account owes. Since each debt requires additional collateral beyond its face value (i.e. a 100 Dai CASH_PAYER may require 120 Dai worth of other assets), by repaying the 100 Dai debt the account will have a net 20 Dai benefit in its free collateral position.

A liquidation procedure requires specifying two currencies, the local currency and the collateral currency. The local currency is the currency that debts are denominated in (in our above example, Dai). The collateral currency is the currency that holds a positive cash balance that can be sold in exchange for local currency.

Definitions

  • Net Currency Available: If negative, the post haircut requirement for debts. If positive, the non haircut amount of currency available to collateralize other debts. Defined as: netCurrencyAvailable = cashBalance + postHaircutCashClaim - requirement

  • Cash Claim: The sum total of cashClaims (not fCashClaim) held by liquidity tokens in this currency. See: Liquidity Token for more details.

  • Post Haircut Cash Claim: The value of Cash Claim after the LIQUIDITY_TOKEN_HAIRCUT is applied.

  • Local Currency Required: The value of local currency required to recollateralize the account

Local Currency Liquidity Tokens

The LIQUIDITY_TOKEN_HAIRCUT implies that accounts may become undercollateralized but have a source of positive cash in their liquidity token cash claims. Note that this situation can arise when providing liquidity because it is done on leverage; when the liquidity provider deposits cash into a market they incur a debt equal to their pre haircut fCash claim.

  • LIQUIDITY_TOKEN_HAIRCUT = 0.9

  • LIQUIDITY_TOKEN_REPO_INCENTIVE = 0.01

  • Assume Dai/ETH exchange rate = 0.1

The account is undercollateralized based on its post haircut values but it has sufficient Dai in its cash claims to recollateralize the account. It is possible for a liquidator to liquidate this account without any capital input. We incentivize the liquidation of these accounts with the LIQUIDITY_TOKEN_REPO_INCENTIVE. Note that only the difference between the cash claim and the post haircut cash claim is available to recollateralize the account. The post haircut cash claim has already been accounted for in the free collateral calculation. The formula is as follows:

cashClaim - cashClaim * liquidityTokenHaircut = localCurrencyRequired * (1 + incentive)

Rearranging to calculate the total amount of cashClaim to remove from the account:

cashClaim = localCurrencyRequired * (1 + incentive) / (1 - liquidityTokenHaircut)

Since we pay localCurrencyRequired * incentive to the liquidator, this can be calculated as follows:

incentivePayment = cashClaim * (1 - liquidityTokenHaircut) - localCurrencyRequired

After this is completed, the account's net currency available should be:

netCurrencyAvailable' = netCurrencyAvailable + cashClaim * (1 - liquidityTokenHaircut) - incentivePayment

In our example above these values are:

  • cashClaim = 10 Dai * (1.01) / 0.1 = 101 Dai

  • incentivePayment = 101 Dai * (0.1) - 10 Dai = 0.1 Dai

  • netCurrencyAvailable' = -10 + 101 * (0.1) - 0.1 Dai = 0 Dai

NOTE: a careful reader will observe that the LIQUIDITY_TOKEN in this scenario also holds a claim to fDai which may potentially recollateralize the account if the CASH_PAYER and LIQUIDITY_TOKEN are in the same maturity. This is because the fDai will directly offset the negative balance of the CASH_PAYER. For simplicity, we do not account for this during liquidation at this time.

Collateral Currency Liquidity Tokens

The same scenario above can also arise when trading a collateral currency for local currency during liquidation. It's possible that the value in the collateral currency is providing liquidity and we must remove it in order to settle balances with the liquidator. In order to access the pre haircut cash claim we do the following calculations:

This is the value of the collateral currency's liquidity token haircut:

haircutValue = postHaircutCashClaim / liquidityTokenHaircut - postHaircutCashClaim
             = postHaircutCashClaim * (1 / liquidityTokenHaircut - 1)

We calculate the amount of collateral currency we need to sell to the liquidator, accounting for the discount we provide as an incentive: collateralToSell = localCurrencyRequired * exchangeRate[local][collateral] * liquidationDiscount

It is preferable that we do not remove liquidity tokens if we do not have to. We prefer to use cash balances if possible, but we determine this using the following inequalities:

  • amountToRaise: the amount of cash claims that must be removed from liquidity tokens and given to the liquidator

  • balanceToTransfer: the portion of the liquidated account's cash balance that will be given to the liquidator

  • creditToAccount: the amount to credit back to the liquidated account from the amountToRaise value

if netCollateralAvailable >= collateralToSell:
    # We have sufficient currency available in either balances or postHaircutCashClaims
    # to recollateralize the account
    if balance >= collateralToSell:
        amountToRaise = 0
        creditToAccount = 0
        balanceToTransfer = collateralToSell
    else:
        # We must remove the remaining collateralToSell from liquidity tokens.
        amountToRaise = collateralToSell - balance
        creditToAccount = 0
        # We cannot transfer a negative balance
        balanceToTransfer = balance > 0 ? balance : 0

else if netCollateralAvailable + haircutValue >= collateralToSell:
    # We must access the haircut value in order to recollateralize the account.
    amountToRaise = (collateralToSell - netCollateralAvailable) / (1 - liquidityTokenHaircut)

    if balance >= collateralToSell:
        # collateralToSell - netCurrencyAvailable <= haircutValue (this is the amount of haircut we need to access)
        # We need to scale this by (1 - liquidityTokenHaircut) to determine the actual amount to raise. In this case
        # the amountToRaise is reconstituting the account's balances via liquidity tokens.
        balanceToTransfer = collateralToSell
        creditToAccount = amountToRaise
    else:
        if amountToRaise < collateralToSell - balance:
            # This will occur when an account has debts that are being collateralized in part by the liquidity token
            # post haircut cash claim. See:
            # netCollateralAvailable = balance + postHaircutCashClaim - requirement
            # netCollateralAvailable + haircutValue = balance + postHaircutCashClaim - requirement + haircutValue > collateralToSell
            # balance + postHaircutCashClaim - requirement + haircutValue > collateralToSell
            # postHaircutCashClaim - requirement + haircutValue > collateralToSell - balance
            # preHaircutCashClaim - requirement > collateralToSell - balance
            # Here we've proven that in this case there is sufficient cash in the liquidity tokens to recollateralize the account.
            amountToRaise = collateralToSell - balance
            creditToAccount = 0
            balanceToTransfer = balance > 0 ? balance : 0
        else:
            creditToAccount = amountToRaise - (collateralToSell - balance)
            balanceToTransfer = balance > 0 ? balance : 0
else if netCollateralAvailable + haircutValue < collateralToSell:
    # There is not enough collateral currency to fully recollateralize the account, we calculate the
    # the maximum amount that we can trade
    collateralToSell = netCollateralAvailable + haircutValue
    amountToRaise = haircutValue / (1 - liquidityTokenHaircut)

    if balance >= collateralToSell:
        # We have sufficient balance to transfer but the portfolio will be short haircutValue
        # if we do not remove this value from its cash claims
        balanceToTransfer = collateralToSell
        creditToAccount = amountToRaise
    else:
        # Logic here applies similarly to what is above
        if amountToRaise < collateralToSell - balance:
            amountToRaise = collateralToSell - balance
            creditToAccount = 0
            balanceToTransfer = balance > 0 ? balance : 0
        else:
            creditToAccount = amountToRaise - (collateralToSell - balance)
            balanceToTransfer = balance > 0 ? balance : 0

Adjusting netCollateralAvailable for CASH_RECEIVER

It's possible that a portfolio holds CASH_RECEIVER assets in a collateral currency during liquidation. Since we will not trade CASH_RECEIVER during liquidation (only during liquidate fCash) we must adjust the netCollateralAvailable figure accordingly. In this case we want to ensure that positive value from fCash is used to net off against negative current cash balances.

def calculatePostfCashValue(fCashValue, netCollateralAvailable, collateralBalance):

    if fCashValue < 0:
        # There is net negative fCash value so there is nothing to net off
        return {
            netCollateralAvailable: netCollateralAvailable
            netCollateralBalance: collateralBalance
        }

    if collateralBalance >= 0:
        # Collateral balance is positive so there is nothing to net off
        return {
            netCollateralAvailable: netCollateralAvailable - fCashValue
            netCollateralBalance: collateralBalance
        }

    if collateralBalance + fCash > 0:
        # Partially net off collateral balance up until zero
        return {
            netCollateralAvailable: netCollateralAvailable - (collateralBalance + fCash)
            netCollateralBalance: 0
        }
    else:
        # Use all fCashValue to net off collateral balance, will remain negative
        return {
            netCollateralAvailable: netCollateralAvailable
            netCollateralBalance: collateralBalance + fCashValue
        }

Appendix C: Block Trades and Idiosyncratic Assets

Since a fCash pair CASH_PAYER and CASH_RECEIVER at the same maturity offset each other, creation and trading of these assets should not be limited to on chain markets. We allow accounts to agree to create offsetting fCash pairs off chain and submit them to Notional for settlement. Accounts holding CASH_PAYER assets must hold sufficient collateral for their debts. CASH_RECEIVER assets that have corresponding on chain cash markets will contribute positive collateral to a portfolio. CASH_RECEIVER assets that are idiosyncratic (i.e do not have a maturity that corresponds to a market on chain), have no positive collateral value. This restriction may be lifted in the future if there are liquid markets for purchasing these types of assets during liquidation.

These trades will allow for higher value trades to occur off chain while enabling the security of on chain custody and settlement.

Last updated