Notional V2 Technical Deep Dive Part 5: Settlement and Initialization
In this series of posts, I will walk you through the Notional V2 smart contracts. The goal of these posts is to help technically minded users and developers better understand a large and complex codebase (~12,000+ lines of Solidity!). We've covered a lot so far but in the next few posts we're going to cover some of the more technical parts of Notional (you didn't think it was going to get easier, right?). This post will cover asset settlement and market initialization. These two concepts are not "user facing" but are important administrative tasks that ensure Notional V2 works.
Settlement
Notional differs from variable rate lending platforms because fCash and liquidity token assets mature. Maturity is a necessary component of creating a fixed rate asset – a fixed rate of return in perpetuity is mathematically impossible (who's going to pay you 4% interest literally forever?).
fCash assets and liquidity token assets settle differently and we will discuss them separately here. Settlement for array type and bitmap type portfolios are implemented in two libraries here. Settlement is triggered during withdraw
, batchBalanceAction
and batchBalanceTradeAction
when the nextSettleTime in the account context has passed. It can also be manually triggered via settleAccount
.
Internal settlement libraries are called via the SettleAssetsExternal.sol library. The different external methods expose different types of storage updates for gas optimization purposes.
Settling fCash
fCash settles at maturity. While fCash is denominated in a fixed amount of underlying (i.e. DAI), Notional V2 cash balances are denominated in asset cash (i.e. cTokens). At settlement, the fCash is converted to asset cash denomination at the settlement rate. For example, at settlement the cDAI to DAI exchange rate is 50 cDAI to 1 DAI (i.e. the settlement rate). A 100 fDAI asset will then be converted to 5000 cDAI in the user's cash balance. If this user is the first to settle fDAI at this particular maturity, the settlement rate will be recorded in storage and used for all other fDAI at this maturity.
Since Notional guarantees that for every positive fCash balance there is an equal and offsetting negative fCash balance, we know that there will be equal positive and negative "asset cash" balances (i.e. cToken balances) when all fCash assets are settled. The settlement rate might not be set exactly on the maturity date but it's sufficient to ensure that the same settlement rate is used for all fCash assets.
Settling Cash Debts
An account can only have a negative cash balance as a result of settling a negative fCash asset. This means that the account has a debt due that has not been repaid. Although Notional V2 guarantees there is sufficient collateral to cover this debt, that collateral may not be denominated in the same currency of the negative cash balance (i.e. a negative DAI cash balance collateralized by ETH).
If this happens en masse, lenders will be unable to withdraw the DAI they are owed since the contract does not have a sufficient DAI balance. Notional V2 solves this problem by allowing third parties to force an account with a negative cash balance to borrow at a penalty rate. This is a special type of trade called _settleCashDebt
.
Note that the Notional fCash market is not involved in this trade. An offsetting pair of positive and negative fCash assets are placed directly in the portfolios of the settler and settlee. The maturity of these fCash assets will match the current 3 month fCash market. The settler deposits cash into the settlee's portfolio to repay the debt. The 3 month fCash market's oracle rate is used to determine the base for the penalty rate.
The reason for putting this action inside of TradingAction.sol
is to enable a settler to borrow from fCash markets and settle cash debts. The settler will earn a profit on the spread between the penalty interest rate and the interest rate they borrow at. Using the 3 month fCash market will provide a pure arbitrage opportunity.
Settling Liquidity Tokens
While fCash settles at maturity, liquidity tokens settle every 90 days along with their corresponding fCash market. Settling liquidity tokens is a bit trickier than settling fCash, these are the steps that must occur:
- A liquidity token settles by withdrawing its asset cash and fCash claim from its corresponding fCash market. It's important that the correct fCash market is loaded from storage, note that two different fCash markets with different settlement dates may trade the same fCash asset at different times.
- The settled fCash market must be updated to reflect the portion of liquidity tokens that have settled.
- If the fCash residual has matured then it can be settled to asset cash.
- If the fCash residual has not matured then the net fCash residual will be stored in the portfolio. This fCash residual will remain in the portfolio until it is traded away or settles to cash. This method relies on the fact that the portfolio is sorted properly.
Initialize Markets
Since fCash markets settle every 90 days, they must be re-initialized on the same cadence. nTokens are used to ensure that there is sufficient liquidity in the fCash markets immediately upon initialization. The InitializeMarketsAction.sol library is certainly daunting (it was daunting to write!) but we will step through it in this section.
The entrypoint of this method is initializeMarkets
. Every 90 days, initializeMarkets
must be called in order to enable trading for the quarter. initializeMarkets
has two major steps. The first is _calculateNetAssetCashAvailable
which will determine how much asset cash is available to deposit into fCash markets as liquidity. The second is the loop which determines the ratio of asset cash to fCash to initialize each fCash market with.
Net Asset Cash Available
Unless this is the first initialization for the currency, we must settle all the assets in the nToken portfolio. Note that _settleNTokenPortfolio
is the only place where nToken assets are settled. nTokens are not normal accounts in the system and settleAccount
will have no effect if called against an nToken address. Also notice that an nToken portfolio is special because it has both an array portfolio for holding liquidity tokens and a bitmap portfolio for holding fCash assets.
_getPreviousMarkets
loads the fCash market data from the previous quarter. These will be used in the rest of the method.
_withholdAndSetfCashAssets
takes the residual fCash amounts left behind by settling liquidity tokens and sets them in the nToken's bitmap portfolio. By the end of this method there should be no liquidity tokens or fCash assets in the portfolioState.storedAssets
array and the nToken's bitmap portfolio should have all the fCash residuals.
_getNTokenNegativefcashWithholding
checks for negative fCash residuals in the nToken and sets aside asset cash with which to purchase those fCash residuals. We will discuss this in more detail in a bit.
The last steps in _calculateNetAssetCashAvailable
calculates how much asset cash is available to deposit as liquidity and sets the nToken's new asset cash balance.
Set fCash Market Liquidity
Starting on this line there is a very long for loop that has as much commentary as code. Rather than retread all the comments, I'll just summarize what happens here.
At the beginning of the loop, all the fCash markets have no liquidity and no interest rates set. _setLiquidityAmount
adds the amount of asset cash based on depositShares
to the market and sets the initial totalAssetCash
and totalLiquidity
fields.
Starting on this line there is an if / else statement that sets the initial totalfCash
, oracleRate
and previousTradeTime
values for the market. The goal of all the calculations is to ensure that the interpolated interest rates for each market do not suddenly change before and after initializing markets. If they did, accounts' fCash valuations would shift suddenly and potentially put some accounts under water. A visual diagram can be seen here. This code needs to deal with some potential complexities:
- Governance may have extended the
maxMarketIndex
from the previous quarter, so the newly launched fCash tenor does not have any previous fCash market to reference. (dealt with in the first branch of the if statement). - The three and six month tenors are special cases. The code comments describe the different scenarios for interpolating their rates.
- Market proportions may be above leverage thresholds. If that is the case then the nToken will reduce the totalfCash provided to the market to the leverageThreshold. In this case, oracleRates will change from before and after the initializeMarkets call. However, this is a reasonable compromise because the nToken would lend down to the leverage threshold in this case anyway.
Purchase fCash Residuals
After initializeMarkets
is called, the nToken portfolio will be left with idiosyncratic fCash residuals. If left alone, these residuals will make it impossible for nToken holders to redeem and sell their fCash positions. The idiosyncratic fCash residuals have no fCash market to sell to (i.e. a 9 month fCash residual has no matching fCash market).
Additionally, these residuals represent potential liquidity that is locked up in fCash assets rather than being put to work as asset cash in active fCash markets. For these reasons, Notional V2 incentivizes users to purchase the nToken residuals at a discount as a special trading method, _purchaseNTokenResidual
. In fact, the nTokens will fail if nToken idiosyncratic fCash residuals are not all purchased by the next market initialization.
Sweep Cash Into Markets
There is also a method to sweepCashIntoMarkets
which can be called at any point after markets are initialized. This method ensures that any cash balances in the nToken account which are not used as withholding are added to markets as liquidity.
Wrapping Up
You made it! This was a big section and if you were able to follow along with the code you have probably come away with some appreciation for how cool nTokens are (at least I do). Initializing markets this way is what enables us to make nTokens a truly non-maturing asset and therefore natively ERC20. It may all seem a bit esoteric at first but we believe that over time our approach will prove to be the right one.
Reading this code is difficult, even for the person who wrote it. Take your time and if you find a bug report it to security@notional.finance!