Notional V2 Technical Deep Dive Part 4: Cash Groups and nTokens
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!). In the previous post, we will discussed Notional V2's valuation framework and how collateralization works for borrowers. In this post, we will discuss Cash Groups and nTokens which Notional governance uses to define how fCash is traded.
Currencies are ERC20 tokens listed by governance that Notional will accept. Each currency is assigned a unique
uint16 identifier. It is also defined by a
Currently Notional V2 supports cTokens but it can be upgraded to support aTokens, yTokens or any other interest bearing token. Interest bearing tokens have an
UnderlyingToken which they can be redeemed for.
NonMintable tokens allow for acceptance of ERC20 tokens that do not have interest bearing wrapper tokens.
Notional has two broad categories of currencies. Tradable currencies and non-tradable currencies. Non-tradable currencies can only be held as collateral and cannot be lent or borrowed. These currencies will not have a corresponding cash group.
Tradable currencies will have a cash group enabled and updated by governance. Cash group settings are stored in a single storage slot to increase gas efficiency. Each setting is stored as a
uint8 value at a specified granularity. We will briefly discuss all 22(!) potential settings for each cash group. How governance will determine the values for each setting is beyond the scope of this post.
maxMarketIndexis the longest tenor that this cash group will allow trading for. If set, this cannot be less than 2 due to dynamics with initializing markets. The max value is 7 which corresponds to a 20 year fCash tenor (very cool, indeed).
rateOracleTimeWindowis stored as minutes but expanded to seconds in memory. This is the time window that oracle rates are averaged over.
totalFeeBPSis the total fee in annualized basis points for trading fCash.
reserveFeeShareis the percentage share of the fee that will go to the protocol reserve fund.
debtBufferare set in 5 basis point increments and used for risk adjusted fCash valuations.
liquidationDebtBufferare set in 5 basis point increments and define the discount that liquidators get when they purchase fCash.
settlementPenaltyRateis defined in 5 basis point increments and defines the penalty relative to market interest rates for borrowers who do not repay their debts at maturity.
liquidityTokenHaircutsare set per tenor and define the percentage of haircut applied to liquidity token claims during risk adjusted valuation.
rateScalarsare set per tenor in increments of 10 and define the rate of slippage.
If you thought the job of governors was done after all those cash group settings you'd be wrong. Each tradable currency must also have an nToken deployed with its own set of governance parameters. The nToken is a bundle of liquidity tokens managed by the Notional V2 protocol to ensure that there is always a minimum amount of liquidity in fCash markets. It also can act as a lender if interest rates cross a certain threshold. We will see this dynamic later in this post.
An fCash market's maturity date should not diverge too far from the actual tenor. If we allowed a 2 year fCash market to be active for the entire length of it's tenor, users would actually be getting shorter and shorter dated fCash as the market rolls down to maturity. After 1 year, users would actually be trading 1 year fCash, not 2 year!
This is why Notional V2 resets fCash markets every 90 days. It ensures that users are able to trade in fCash markets that are close to their actual tenors. This dynamic will be discussed more in the next post about settlement and market initialization. This was all to say that nTokens play an important role in initializing the market state on these "quarterly rolls" and we'll take a look at those settings here.
Each nToken has a number of settings determined by governance and set in the nTokenHandler.sol file. The description for each of these parameters can be found in the code comments in the GovernanceAction.sol file.
nToken ERC20 Proxy
When a cash group is enabled by governance, an nToken ERC20 proxy contract is deployed.
This proxy exposes an ERC20 compatible interface so that nTokens can be transferred, traded, and held as collateral much like any other ERC20 token. We worked very hard to create the nToken as a non-maturing asset so that it could be natively used in the rest of DeFi. Since an nToken receives yield from multiple sources (cToken supply rate, fCash market trading fees, fCash fixed interest, and NOTE incentives) and is also freely redeemable to it's underlying token it makes for extremely high quality collateral in other lending protocols. The lack of a maturity means that other protocols don't need to do any special handling for nTokens.
Actual implementation of the transfers is done in the nTokenAction.sol contract. Since all nToken balances are actually held in storage on a single contract, they are automatically used as collateral during borrowing (no transfers required!)
This pattern also allows for setting a blanket allowance for all nToken balances to a single spender. This means that a user could issue a single approval to a DEX (say CurveFi) to transfer any of their nTokens into a pool (perhaps an nDAI/nUSDC/nUSDT pool).
Minting nTokens is done inside the
_executeNTokenAction method via the
batchBalanceAndTradeAction methods. The actual implementation is in the nTokenMintAction.sol library. The reasoning for this was simply running up against the Ethereum contract byte code size limit.
When a user mints nTokens, the following occurs:
- A given amount of "asset cash" is deducted from the user's cash balance and used to calculate the amount of nTokens to be credited back to the user.
- The user's asset cash will be distributed to fCash markets in proportion to the
depositShareof each fCash market set by governance. If
depositSharesare 50%, 25%, and 25% that means the 3 month fCash market will receive 50% of the liquidity, 6 month will get 25% and so forth. The looping here starts from the longest tenor and the reasoning is described in this comment.
- The nToken portfolio is updated with the corresponding amount of liquidity token, fCash, and asset cash balances.
- The user's balance is credited with the number of nTokens minted.
- If the user already has an nToken balance, then incentives are minted accordingly. See the final section for more details on this.
- The last step is to update the nToken total supply based on the number of nTokens minted. This ensures that users cannot manipulate the total supply to change the incentives they are minted.
Deleveraging the Market
Providing liquidity puts a negative fCash balance in a liquidity provider's portfolio. When the
totalfCash in a market is high relative to
totalCashUnderlying, this negative fCash balance will be large relative to the amount of cash deposited. Providing liquidity when the fCash market is at a high "proportion" like this is risky – if
totalfCash starts to decrease as other users lend to take advantage of high interest rates, the liquidity provider may be left a significant net borrower (and potentially undercollateralized).
Under collateralization is not an option for the nToken – Notional governance must protect the assets of nToken holders. Therefore, governance will set a
leverageThreshold which the nToken will not provide liquidity above. Governance can empirically measure a safe threshold where under collateralization will not occur for the nToken.
If a user mints nTokens when an fCash market is above the
leverageThreshold then the nToken will lend to the market instead, adding cash and removing fCash. This will decrease the fCash market's proportion and lower interest rates. In this case, the nToken acts as a lender instead of a liquidity provider to fCash markets.
Notice that the nToken needs to make a runtime decision on whether to lend or provide liquidity. Since calculating the cash amount to lend is quite expensive in gas terms, the code takes a shortcut by calculating an exchange rate less than the implied rate, assuming that slippage won't be more than this
In practice, this should be sufficient to ensure that a reasonable amount of fCash is lent to the market. If lending is unsuccessful the cash that should have been deposited to the market will be rolled over to the next fCash market. A large total asset cash deposit may result in slippage beyond the
DELEVERAGE_BUFFER and cause lending to fail. In this case a smaller total asset cash deposit will likely succeed.
Redeeming nTokens can be done via the
batchBalanceAndTradeAction methods as well as calling
nTokenRedeem. The first two methods do not allow for the account to retain nToken residuals,
nTokenRedeem allows this as an option.
When a user redeems nTokens, the following occurs:
- The user's nToken balance is reduced.
- The user's share of each nToken fCash asset is withdrawn from the nToken fCash portfolio. This share is
nTokenBalance / nTokenTotalSupply.
- The user's share of the nToken's cash balance is withdrawn.
- The user's share of the nToken's liquidity token cash claims and fCash claims are withdrawn from fCash markets and offset against the share of fCash assets from the nToken portfolio.
- At this point, the user has an asset cash balance and a set of residual fCash assets that will be added to their portfolio.
- If the user has elected to
sellTokenAssetsthen the contract will trade the opposite fCash position to convert the fCash to asset cash. Note that if the fCash position is negative, some of the
netAssetCashwill be deposited into the market (i.e. the user will lend) to repay the fCash debt. The advantage of this setting is that the user will receive a single net asset cash balance which can be easily withdrawn and does not need to worry about a basket of fCash assets.
- Finally, a similar set of steps occurs as in
MintNToken, the account's nToken balance is reduced, incentives are minted and then the nToken supply is updated.
Liquidity providers become net lenders or borrowers as a result of providing liquidity and are therefore left with fCash residuals when they withdraw their liquidity. When nToken holders redeem their nTokens, they may be left with these residuals. In most scenarios, nToken holders will be able to sell these residuals back to the fCash markets in exchange for cash. When fCash residuals are larger than the fCash market can trade or when fCash residuals are idiosyncratic, they must be absorbed into the user's portfolio.
Providing liquidity via nTokens is incentivized via the Notional V2 liquidity mining program. The calculation for the NOTE incentives minted to an nToken holder can be seen in Incentives.sol. Whenever nToken balances are minted, redeemed, or transferred the incentives will be minted and the
lastClaimSupply values will be updated in the user's balance storage.
Notice that it's not really possible to predict exactly how many incentives will be minted for a given nToken and
incentiveEmissionRate since the nToken total supply is a factor. NOTE tokens are transferred from the core Notional contract.
We've covered a lot of topics in this post but only scratched the surface. Notional governors will have their hands full with setting parameters to ensure a healthy protocol. Notional V2 has a lot of governance settings which reflects the sophistication of the protocol. That sophistication is what we believe will ultimately deliver a capital efficient, decentralized fixed rate platform that can rival centralized offerings.
In addition, we're really excited about the introduction of the nToken into the DeFi ecosystem. Building the nToken was not easy, but we believe that it has excellent technical (i.e. native ERC20 compliance) and economic (i.e. multiple sources of yield and redeemability) properties that will enable it to be high quality collateral in other lending protocols.
Notional Finance Newsletter
Join the newsletter to receive the latest updates in your inbox.