When bug-fixes go wrong: RAI debt auctions bug

“The way to be safe is never to be secure.”

-Benjamin Franklin

Its common to see projects hacked after an audit. After all, a security review doesn't guarantee a project is entirely free of bugs; it's more about enhancing security standards and uncovering as many potential issues as possible within a limited timeframe.

But, things turn around when in the process of addressing a seemingly low-severity bug (some might even call it informational), the proposed bug-fixes introduce a higher severity one. This scenario happened after an audit of RAI (the so-called "truly stable" non-pegged stable-coin) conducted by OpenZeppelin. Fortunately, I stumbled upon this issue during one of my late-night bug-hunting sessions and thanks to Immunefi and the Reflexer team it was quickly addressed and fixed. Get ready to take notes anon, because things are about to get messy.

Chances are you’ve heard about DAI and MakerDAO, the biggest decentralised stable-coin of the ecosystem. Well RAI is a similar concept (also backed by ETH), but its price is not pegged to the US Dollar. What's the significance of this, you may wonder?

The Reflexer team asserts that:

RAI = RAI (In simpler terms, one RAI equals one RAI).

Interesting huh? In essence, RAI can be thought of as a low-volatility version of ETH, grounded in the notion that "the purpose of a stable-coin isn't to mimic the USD but to provide an asset with minimal price fluctuations." Essentially, if we aspire to create a truly crypto-native stablecoin, it should have no ties to real-world assets.

Write-up

Enough with theory, let’s get hands-on.

The protocol uses the GEB framework which is a modified fork of MakerDAO’s Multi-collateral DAI. And unlike DAI, it is pegged to itself, achieving this through the use of a PID Controller to dampen its volatility.

RAI Stability Mechanism
RAI Stability Mechanism

While locking collateral into the protocol, the depositor creates a position called “SAFE” that can mint new RAI. Furthermore, the system requires a minimum collateral ratio of 145%; below this threshold, the borrower’s positions are liquidated through an auction model that relies on third parties, much like MakerDAO.

When there's a situation of bad debt, meaning that collateral auctions can't fully cover the outstanding debt, the system resorts to debt auctions to replenish its capital. During these auctions, protocol tokens are offered for sale in exchange for a fixed amount of system coins. In this process, bidders compete by offering to accept decreasing amounts of protocol tokens for the coins they will end up paying.

The AccountingEngine serves as the protocol's central component responsible for initialising both debt and surplus auctions. Debt auctions are consistently initiated with a predefined minimum bid referred to as debtAuctionBidSize. This is done to ensure that the protocol can only auction debt that is not currently undergoing an auction and is not locked within the debt queue.

This necessity prompts the check on line 317, which verifies that there is an adequate amount of bad debt available to auction:

require(debtAuctionBidSize <= unqueuedUnauctionedDebt(),"...");

The issue arises due to a proposed modification by OpenZeppelin, which involves invoking settleDebt(safeEngine.coinBalance(address(this))) before initiating the auction. The purpose of this change is to ensure that only bad debt is considered for the auction creation, thus preventing front-running problems.

However, if, after settling the debt, the remaining bad debt amount falls below the threshold defined by debtAuctionBidSize <= unqueuedUnauctionedDebt(), the auction would still be initiated with an incorrect amount of bad debt to cover

In other words, this invariant does not hold: debtAuctionBidSize <= remainingDebt.

Since the insufficient debt check is performed prior to the settlement call, the auction proceeds even when there is non-existent debt to be auctioned. This situation leads to the unnecessary dilution of the protocol token (the bidders will buy the debt in exchange of protocol tokens minted to his address).

In practice this means the system will print FLX, and auction it for RAI to repay bad debt that does not exist.

Conclusion

At the time of reporting the bug, the protocol was in a surplus state, making it unlikely for debt auctions to occur. However, in the event of a "Black Thursday" scenario, this issue could have posed a recursive threat to the protocol. Thanks to the fast response of the RAI team and the assistance of the Immunefi platform, we reached an agreement on a bounty and successfully addressed this hidden problem.

Hope you enjoyed this article, and it piqued your curiosity about bug-hunting and the mechanisms behind decentralised stable-coins.

// See you on the next write-up, anon

Subscribe to VNMRTZ
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.