Introduction
In this post we’ll look at a critical bug we identified through fuzzing that was missed in a security contest.
The bug was found in the Beraborrow codebase (a fork of Liquity) after undergoing multiple rounds of audits.
This bug was discovered because the Beraborrow team was relentlessly committed to applying a multi-layered security approach that included:
Internal peer reviews
An invite only security contest including some of the most experienced solo auditors
A fuzzing engagement with Recon led by 0xsi + solo review by Alex The Entreprenerd
This highlights the importance of a multi-layered approach as each has its own advantages and applying more layers ensures lower likelihood that a bug will slip through one layer and make it into production.

As you’ll see below, this bug demonstrates how fuzzing is particularly well-suited for discovering rounding related issues. Rounding was the root cause of break of a simple property: no operation should ever decrease the price per share of the vault, uncovering a precondition that could be used in a larger exploit chain, leading to a critical severity vulnerability.
How Liquity V1 Works
Since Beraborrow is a fork of Liquity V1 we first need to understand how Liquity works to understand the bug that was found.
Liquity is a borrowing protocol that allows users to take out interest free loans using ETH in exchange for LUSD (an ETH backed stablecoin) and pay them back on any schedule.
Liquity allows users to open a borrowing position (a trove) by depositing ETH into a contract. Troves must always be overcollateralized, meaning that the value of the collateral is greater than the value of the borrowed stablecoins.
Once a user has an open trove, they can borrow LUSD stablecoins against their collateral. Instead of having to pay a recurring interest fee, Liquity only charges users a one-time fee which a user pays when they borrow against their trove.
To ensure solvency, a user’s trove can be liquidated if their collateralization ratio falls below the minimum. Liquidations distribute the liquidated ETH collateral to users that deposit LUSD into the StabilityPool
contract to provide liquidity that absorbs debt from liquidated troves.
Systemic risk in Liquity is measured using the TCR (Total Collateral Ratio) metric. This is defined as the ratio of the total collateral value to the total debt in the system. This allows quantifying the overall health of the system where a high TCR means the system is well-collateralized and a low TCR means the system is at risk of insolvency.
Recovery Mode
Now that we understand how Liquity works, we need to understand the mechanism that could be exploited once the fuzzer gave us a precondition via a broken property: recovery mode.
Liquity V1 introduced recovery mode as a mechanism to mitigate high levels of risk in the system. When the risk (measured by the TCR) in the system is too high, recovery mode is enabled and allows the system to require that all borrowers take less risk and forces accounts that would otherwise be non-liquidatable to be liquidatable. The CCR (Critical Collateral Ratio) threshold that the TCR must fall below to trigger recovery mode is 150%.
For example, positions would normally be liquidated if their CR (Collateralization Ratio - the value of the position’s collateral / the value of the position’s debt) fell below 110%. In recovery mode however, since the system is trying to reduce risk, positions with a CR as high as 150% can instantaneously be liquidated.
This is done to de-risk the system and ensure that liquidations can be executed profitably for all liquidators. Additionally, the activation into recovery mode isn’t done using a delay or interest rates, it takes effect instantaneously.
Because recovery mode gets triggered instantaneously, Liquity V1 has put in place mechanisms to disincentivize users from attempting to trigger it maliciously in order to profit from forcibly liquidating other users. These safety mechanisms include: preventing users from profitably self-liquidating, preventing any user operation from triggering recovery mode (with an explicit check for this after all operations) and preventing the user from introducing more risk when the system is already in recovery mode.
With these protections in place, this type of attack has been prevented from being attempted on mainnet.
The Property that Found the Bug
After having reviewed many systems implementing ERC4626-like vaults, we’ve started implementing the common practice of defining properties that check for price arbitrage opportunities in them. Essentially, these types of properties check for any operation that changes the price per full share (PPFS) after an operation in an unexpected manner.
While there are many ways to test for this, these tests can be categorized into two general types using global (standalone properties) or in-lined checks (in a target function handler):
Checking the PPFS before and after a deposit or withdrawal operation for any change using the BeforeAfter contract in the Chimera Framework.
Deposit/withdraw doomsday-type checks which check for states that should never be reached after a call to a handler.
In this case the property was implemented as a global check in the Properties contract:

With this property defined and many runs of the fuzzer later, we kept seeing it breaking, indicating something was causing a decrease in the PPFS.
Understanding the Fuzzer Output
In the case of PPFS arbitrage in Liquity, we knew that if this property was broken the call sequence that the fuzzer generated wouldn’t directly be an exploit, but could act as a precondition that led to an exploit. So first we had to understand why the PPFS was decreasing unexpectedly:

From the logs in the above test we could see that the totalAssets
were decreasing by 2 but the totalSupply
of shares was only decreasing by 1 after a redemption:
This indicated that because the system was taking payment for fees via share tokens instead of asset tokens and rounding up when it did so, when a user redeemed shares the ratio between shares and assets changed unexpectedly.
More specifically, when a user was supposed to burn X - fee shares, the rounding up meant 1 wei of shares wasn’t burned or taken as a fee, causing the asset/share ratio to become unbalanced as there were now more shares remaining than there should have been for the amount of assets removed. Effectively this deflated the price of the share, socializing the loss of value among all existing depositors.
Tying it All Together
Once the root cause of incorrectly decreasing the totalSupply
of the share token on a redemption and therefore deflating the PPFS was understood, this was an entrypoint that could be used for attacking the Den Manager (responsible for managing user-specific vaults where users deposit collateral) by triggering recovery mode and liquidating other borrowers.
With the knowledge of the inherent danger in being able to trigger recovery mode on purpose, Alex was able to escalate this low severity price issue into a high severity attack vector that could be used against other borrowers.
An attacker could use the ability to drive the share price down with the following set of operations to directly trigger recovery mode:
Open a trove and borrow enough collateral and debt to move the TCR very close to the CCR (it can’t be reduced past the CCR because there’s an explicit check that prevents this).
Cause the 1 wei loss to decrease the value of all open positions.
The change in price causes the CCR threshold to be passed and the system enters recovery mode.
Liquidate all other troves in reverse order, starting with the healthiest (closest to the 150% MCR; lowest value) and ending with the unhealthiest (closest to the 150% MCR; highest value) which causes significantly more liquidations than necessary to reach the CCR threshold.
Close the attacker position.
These steps would be performed via a multicall
function from an attacking contract, ensuring that the attack can’t be frontrun.
Looking at what the attacker gains versus what they had to pay to execute the attack we can see that the cost of the attack is equivalent to:
Gas cost
Fees for opening a position
Whereas their gain from the attack is equivalent to:
Liquidation caller incentive (50 basis points of all collateral liquidated)
Gas stipend
A percent gain on the liquidated collateral based on the amount of debt the attacker can provide to the
StabilityPool
. If they use a flashloan, this debt amount can be significantly increased, allowing for greater profit in the attack execution.

Conclusion
We hope the above demonstrates how looking for specific preconditions that can be found with fuzzing can open up possibilities for known attack paths that otherwise wouldn’t have been available.
To our surprise this bug slipped through the audit contest that was held for the Beraborrow codebase as well as multiple manual reviews, demonstrating the unique ability of fuzzing to find these types of rounding related edge cases that are often difficult to reason about mentally for manual reviewers.
When combined with the prowess of a skilled security researcher, fuzzing can act as a tool that allows you to have insights about a codebase that no one else does, leading to finding unique high severity vulnerabilities.
If you want help defining your own invariant testing suite and a skilled security researcher to take full advantage of it to uncover vulnerabilities that others may have missed in your codebase, reach out to us!