This write-up concerns a particularly interesting finding that came out of the audits we received for Notional V2. First of all, big shout-out to cmichel, Code Arena’s top-earning warden in our audit contest! This finding is a testament to the value of the Code Arena model and the importance of a thorough and varied approach to auditing.
Cmichel discovered that Notional was vulnerable to a flash loan attack because of the way we calculate the collateral value of Notional’s liquidity tokens. As background, when users provide liquidity on a Notional fCash market they receive liquidity tokens in return (much like Uniswap LP tokens). Notional automatically considers these liquidity tokens as collateral, which means users can borrow against them. Their collateral value is based on the underlying claims to cash and fCash within the fCash market.
An attacker can’t use a flash loan to manipulate the value of cash or fCash itself because the pricing oracle for fCash is specifically designed to guard against flash loans. But an attacker CAN use a flash loan to change the ratio of cash to fCash in the liquidity pool by making a large trade. This would change the cash and fCash claims that a given liquidity token represents, and would thus marginally change the amount of collateral value that those liquidity tokens confer to their owner.
An attacker could cause the free collateral of an account to marginally decrease by executing a large trade on a Notional liquidity pool and altering the size of the cash and fCash claims for liquidity tokens associated with that liquidity pool. This would allow the attacker to liquidate accounts who had borrowed to their limit against those liquidity tokens.
An attacker should never be able to manipulate the value of an account’s free collateral using a flash loan, and so this issue presents a real risk to the system. It’s worth noting here that this issue is present for both Notional V1 and Notional V2. But given technical differences in the way liquidation works in Notional V1 vs Notional V2, this issue is innocuous in Notional V1 and would only have been problematic in Notional V2. Here is an example that demonstrates why this issue does not pose real risk to Notional V1:
- The December 31st USDC liquidity pool holds 1,000,000 USDC and 1,000,000 fUSDC.
- An account holds 10% of the outstanding liquidity tokens for that pool.
- Those liquidity tokens have a claim on 100,000 USDC and 100,000 fUSDC.
- After applying the liquidity token haircut (10%), those claims come out to 90,000 USDC and 90,000 fUSDC.
- Now we apply the fCash haircut (~15%) to the fCash claim to get the free collateral value of 90,000 + 0.85 * 90,000 = 166,500
Now let’s say the attacker borrows all the cash in the pool.
- The December 1st USDC liquidity pool now holds 0 USDC and 2,050,000 fUSDC.
- Liquidity token claims: 0 USDC and 205,000 fUSDC.
- Post-liquidity token haircut: 0 USDC and 184,500 fUSDC
- Post-fCash haircut: 0 + 156,825 = 156,825
This attack has caused the account’s free collateral to decrease by 9,675 USDC or roughly 6%, which would allow the attacker to liquidate accounts who had borrowed to their limit against those liquidity tokens.
The attack doesn’t present risk to Notional V1 users in practice because the cost of executing the attack vastly outweighs the potential profits.
- Attack cost: In order to change the cash claims substantially, the attacker needs to make large, costly trades on the liquidity pool. In this scenario, the attacker had to borrow 1,000,000 USDC at the highest interest rate the pool could support (~20%). If they were to exit their borrow at the prevailing interest rate before they initiated the attack (~4%), they would lock in a 16% loss on a three-month, 1,000,000 USDC loan. That’s a 40,000 USDC loss.
- Attack profit: In return for this loss, their payoff is slim. All they can do is liquidate the account, force it to recoup it’s shortfall of 9,675 USDC (liquidations on Notional V1 return an account to minimal collateralization and no more), and earn a liquidation discount of at most 6% for doing so. That’s a profit of 580 USDC, or less than 1.5% of their cost. Even if all the liquidity providers to this pool were this close to undercollateralized (extremely unlikely) and he liquidated all of them, they would still only earn a grand total of 5,800 USDC against a 40,000 USDC cost. Not to mention the fact that by liquidating all the LPs to the pool, they would no longer be able to exit their loan!
In summary, even if an account borrows against their liquidity tokens to such an extent that they become vulnerable to this scenario, a profitable attack vector does not exist.
Short term remediation:
Even though this effect can’t be profitably exploited, we still don’t want it to happen. Thankfully, it is very difficult for an account to get into a position where they could be vulnerable to this. As of now, no accounts are vulnerable. To ensure that no accounts become vulnerable, we are going to disable borrowing in the UI for accounts that hold liquidity tokens until the launch of Notional V2. We feel that this is a sufficient measure given the low-severity of the risk and the short amount of time until the transition to Notional V2 where we handle this issue at the smart contract level.
Long term remediation:
The root cause of this issue is that we derive the value of liquidity tokens directly from a liquidity pool without using a TWAP. To fix this issue, that’s what we’ll do. We will store a lagged average of the cashClaim and fCashClaim per liquidity token. The lagged claims will gradually converge to the actual claims, similar to how our lagged average interest rate gradually converges to the last traded interest rate. In the freeCollateral calculation we will use these lagged claims to value liquidity tokens, not the actual claims. This will protect us from flash loan attacks in Notional V2.
Thanks again to Code Arena, cmichel, and all our auditors who help us make Notional a safe place for our users!
Notional Finance Newsletter
Join the newsletter to receive the latest updates in your inbox.