Notional Double Counting Free Collateral Bugfix Review

Summary
From time to time, we receive reports that are resolved and paid so quickly that we are barely even able to think about writing a bugfix review. That was the case with Notional and the bug that was submitted to them by whitehat 0x60511e57 via Immunefi on January 7th.
The bug was given a severity of critical, since it could have allowed a user to drain almost all liquidity from markets on all currencies. In the processing of account contexts with bitmap portfolios enabled, Notional V2 contracts had a logic issue. As a result, the free collateral was miscalculated and overpriced by a factor of two.
As an example, whitehat 0x60511e57 showcased that he was able to drain all 26.2 million of DAI from the DAI market. The whitehat prepared a runnable PoC (Proof of Concept) which demonstrated the vulnerability in a local blockchain fork. First, he showed in the brownie script how the double accounting can happen. Next, he provided three attacker contracts that could be used in an attack. These three contracts were later used in a brownie script and executed on a mainnet-fork (local copy of a blockchain).
The Notional team confirmed the bug in less than an hour (a very impressive response time) and upgraded the affected contracts to remove the vulnerable feature. Once they determined that no funds were at risk, they proceeded with a proper fix to reenable the feature.
For the discovery, 0x60511e57 is being paid the max amount of the reward for the finding which was a whooping one million dollars plus additional 100,000 NOTE tokens for the report! Someone had a great start of the year, that’s for sure :)
Notional went through three different audits and did a formal verification of their protocol. This bug shows that the quest for security never ends and bug bounty programs can help you find bugs that might be missed using other security methods and tools.
Intro to Notional
Notional is a fixed-term lending and borrowing platform on Ethereum. All operations done on the Notional V2 platform are done using their fCash token. fCash tokens are transferable tokens that indicate a claim on a positive or negative cash flow at a future date. The interest rate for lending and borrowing on Notional V2 is set by fCash Markets. Users deposit or receive cash in return for fCash when they lend or borrow at set rates.
As we can read in the Notional V2 documentation:
“fCash is denominated in an underlying token like DAI or USDC, but it is settled in a cToken like cDAI or cUSDC. For example, upon maturity, 100 fDAI is not directly redeemable for 100 DAI, rather it is redeemable for 100 DAI’s worth of cDAI which is then freely convertible into DAI at the user’s will.”
If you want to go deeper into how the Notional system works, we can recommend reading the official documentation or their technical deep dive.
What we are interested in are the so-called Bitmap Portfolios.
Bitmap Portfolio
The majority of Notional V2 users will have a portfolio that is fairly similar to that of Notional V1. This portfolio is simply an array of assets; the number of assets is limited by governance-tunable parameters. Notional V2 does, however, include a second “asset bitmap” portfolio, which can be enabled on any account and is particularly handy for market makers.
Bitmap Portfolios, or Asset Bitmaps, must first be activated as an account activity. Bitmap currencies can only be changed if there are no debts or credits in the bitmap asset.
Before and after activating a bitmap portfolio, users do not need to make any changes to the way they interact with the platform. Only fCash assets in that bitmap currency can be kept once a bitmap portfolio is activated. MAX_BITMAP_ASSETS
is the maximum number of fCash assets a bitmap portfolio can store.
Now that we know all about bitmap portfolios, let’s dive into the vulnerability!
Vulnerability Analysis
The vulnerability itself lies inside the function AccountContextHandler.enableBitmapForAccount()
which is called from the account action contract.
enableBitmapForAccount()
only calls the function setActiveCurrency() as long as no bitmap currency is set for an account. Once a bitmap currency has been set, enableBitmapForAccount
fails to clear the copy of the active bitmap currency in accountContext.activeCurrencies
when the bitmap currency is changed. This is critical, as we can trigger double accounting of free collateral due to this. Ok, but how?
We can accomplish this by depositing a second currency (the one we want double accounted) into the account by calling depositUnderlyingToken()
. This will make a call to setActiveCurrency with constant.ACTIVE_IN_BALANCES
as flags argument and isActive
being true, which will enable the currency in accountContext.activeCurrencies
only if this currency is not the active bitmap currency. We can force this condition to be true by calling enableBitmapForAccount
with some dummy currency that we don’t care about.
To finish exploiting the issue, we need to make a second call to enableBitmapForAccount
after the deposit to change the bitmap currency to the one we deposited in the previous step. Due to the logic error described in the beginning, the free collateral calculations will be run twice on the asset we deposited: once for the bitmap and once for the accountContext.activeCurrencies
.
This will in fact trigger double accounting of free collateral of the activated currency! Awesome….but what does it mean exactly?
To evaluate an account’s collateral position and validate that its debts are sufficiently overcollateralized, Notional uses the concept of free collateral.
Free collateral represents the amount of collateral denominated in ETH that an account holds beyond what it needs to meet its minimum collateral requirements. If an account’s free collateral figure is positive, the account is adequately collateralized. If the account’s free collateral figure is negative, it is under-collateralized and eligible for liquidation.
If calculations for free collateral are doubled (collateralization is really high), this means the attacker could borrow more without actually holding enough collateral currency. In other words, they could drain the funds from the protocol.
Here’s step-by-step guide on how to reach double accounting of free collateral:
- An attacker enables the bitmap portfolio for any currency. It can be ETH or a dummy currency. It can be accomplished by calling
enableBitmapCurrency
function in the Account Action contract - Next, they need to call
depositUnderlyingToken()
, depositing the currency the attacker wants double accounting for: for example, DAI. - This will activate this currency only if this currency is not the active bitmap currency. That’s why we made the
enableBitmapCurrency
call to activate a different currency than we deposit in this step. - We make a second call to
enableBitmapCurrency
function in the Account Action contract, this time setting the DAI as a bitmap currency. - This will trigger the bug, which will result in the system believing that it has to check DAI twice in free collateral, effectively doubling the DAI collateral believed to be present in the account.
- An attacker can now transfer out double the amount of fCash without Notional registering that the account went into debt.
- An attacker could then send the fCash to another account and have that account convert received fCash into the underlying currency using a Notational market.
Vulnerability Fix
As mentioned previously, Notional team disabled the enableBitmapCurrency
function and created a PR with a proposed fix.
As we can read from the PR:
“This fix will re-enable the enableBitmapCurrency method to be called by accounts. The fix here is to simply remove the functionality of changing a bitmap currency on an account once it is set. This should eliminate the potential for double counting altogether. There is no valid use case for accounts changing bitmap currencies. Furthermore, once enabled a bitmap currency also cannot be disabled.”
Acknowledgment
We would like to thank 0x60511e57 for doing an amazing job and reporting this really clever finding. Props also to the Notional team who did an amazing job responding quickly to the report.
This issue was reported responsibly and securely via the Immunefi platform, leading to a happy outcome for everyone, especially the users.
If you’d like to start bug hunting, we got you. Check out the Web3 Security Library, and start earning rewards on Immunefi — the leading bug bounty platform for web3 with the world’s biggest payouts.