nToken Redemption Bug Post Mortem

On Sept 2, 2022 8:09 PM, we received a critical bug report via Immunefi regarding a potential flash loan attack vector against Notional. Within two hours of the report we had identified the issue and disabled the affected feature. We believe that approximately $5000 total of DAI and USDC have been withdrawn uncollateralized as a result of this bug, likely unbeknownst to the withdrawing account. The Notional protocol reserve will cover this shortfall.

A fix to the issue has been deployed and upgraded into the system.

Affected Code

The reported bug affects the nTokenRedeem method on the AccountAction contract. Most accounts (namely those who use the Notional web UI) do not call this method. This method exists primarily to allow accounts to redeem their nTokens in non standard ways during abnormal economic conditions.

The bug manifests itself in the following if statement.

if (hasResidual) {
   // This method will store assets and update the account context in memory
   context = TransferAssets.placeAssetsInAccount(redeemer, context, assets);
}

If the account passes in true for both sellTokenAssets and acceptResiduals, the hasResiduals flag will be set to false and the idiosyncratic fCash residual will not be properly stored in the account. In the nTokens current state, this means that a negative fCash debt would not be placed in the account, allowing it to withdraw cash that it had received.

Post Mortem

The changes that caused this bug were introduced in the Notional v2.1 upgrade and audited by Consensys Diligence. The goal of these changes was to give accounts more flexibility when redeeming nTokens and reduce the risk of illiquid or unredeemable nTokens.

Although this feature was thoroughly tested, this path was missed. Although we make significant use of both invariant testing and property testing, this bug serves as a reminder that rigorous testing remains of paramount importance. As a part of this fix, test coverage for this feature has been significantly improved.

Understanding the nToken

nTokens are a very sophisticated instrument as they represent a perpetual liquidity position across multiple fCash AMM pools which are destroyed and recreated every quarter as part of the quarterly roll process. The quarterly roll process is required to ensure that Notional fCash lenders and borrowers are always presented with a consistent set of maturities to lend and borrow at fixed rates.

In Notional V1, there was no nToken and a single LP had to initialize each fCash AMM pool as it was created, a cumbersome and not-so-decentralized process that yielded mixed results. Allowing a single LP to set initial interest rates was not only bad UX but a risk to the solvency of the system.

Thus, a lot of effort was put into designing the nToken as a “managed liquidity provider” for fCash markets in Notional V2. Doing so not only improves UX (LPs in Notional V2 are fully passive and no longer have to roll their positions forward or initialize markets) but also greatly decentralizes the system. fCash markets will be initialized on a predictable cadence at deterministic interest rates relative to previous market interest rates. nToken liquidity is also distributed predictably across the newly created markets. All of these calculations are done internally to the Notional V2 system – no external inputs are required.

A byproduct of the nToken system are “fCash residuals”, namely fCash lending or borrowing positions that remain after a quarterly roll has completed. Longer term markets (1 year or longer) have “idiosyncratic” fCash (ifCash) residuals that have no matching fCash AMM market. These residuals are temporarily illiquid and a necessary consequence of offering longer term markets – it would be capital inefficient to offer liquidity for fCash at all potential maturities.

The nToken has a “residual purchase incentive” that allows users to purchase these ifCash residuals at a discount, which can be quite profitable if done correctly. In the latest quarter, however, the ifCash residuals were not completely purchased and some residuals remain on the nToken account as a result. This illiquid asset has not prevented nToken holders from freely minting and redeeming nTokens as a result of the changes put in place in Notional v2.1. In Notional v2.1 we changed the nToken redemption algorithm to discount the present value of nToken holdings to account for ifCash assets and assess a slight redemption penalty along with it.

Because of this, we provided the nTokenRedeem method on AccountAction (where this bug presents itself), to allow accounts to redeem the illiquid idiosyncratic fCash asset along with the rest of the liquid assets to avoid this redemption fee. However, as a consequence these accounts will be left with an illiquid asset that they cannot immediately sell.

Because the current ifCash residuals are negative (i.e. the nToken has an illiquid debt position), accounts that redeem DAI or USDC using this route will receive both cash and some amount of negative fCash. The negative fCash was lost due to this bug, accounts were able to simply withdraw the cash without first passing a free collateral check.

As previously stated, this feature has already been disabled and the upgrade will go live shortly. We want to thank the whitehat who reported the bug, our partners at ImmuneFi for alerting us to this report in a timely fashion, and we’re proud of our response time. We continue to assess our auditing processes, and are happy to announce that we are adding a new partner, working with the innovative team at Sherlock to run audit contests with a competitive pool of top security researchers for our upcoming leveraged vaults.