At Three Sigma, we recognize the intricate opportunities within the Web3 space. Based in Lisbon, our team of experts offers premier services in development, security, and economic modeling to drive your project's success. Whether you need precise code audits, advanced economic modeling, or comprehensive blockchain engineering, Three Sigma is your trusted partner.
Explore our website to learn more.
We are not just experts in code, economic auditing, and blockchain engineering—we are passionate about breaking down Web3 concepts to drive clarity and adoption across the industry.
Introduction
Liquity V1 was built to be immutable, governed solely by code. Since its launch in April 2021, it has functioned seamlessly, offering 0% interest rates while ensuring the security of users' funds. With the release of Liquity V2, the platform takes a bold step forward, introducing a new feature: user-set interest rates, creating a more efficient and attractive market for both borrowers and stablecoin holders.
Liquity V2 is a decentralized collateralized debt platform that allows users to lock up assets like WETH or Liquid Staking Tokens (LSTs) to issue its stablecoin token, known as BOLD. BOLD is designed to maintain a value of $1 USD by ensuring that the system is always over-collateralized and that BOLD can always be redeemed for a corresponding amount of protocol collateral.
The system enables users to open collateralized debt positions, called Troves, by depositing ERC20 tokens as collateral. BOLD tokens can be borrowed against the collateral as long as the collateralization ratio stays above the minimum required. BOLD is freely transferable across Ethereum addresses and can be redeemed by anyone for a dollar's worth of collateral, minus fees.
Architecture
- The core contracts are structured as follows:
- There is one
CollateralRegistry
, a singleBoldToken
, and a collection of core system contracts that are deployed for each collateral "branch." - The
CollateralRegistry
is responsible for mapping external ERC20 collateral tokens to a correspondingTroveManager
address. It also handles the routing of redemptions across the various collateral branches. - Example: Each WETH, rETH and wstETH have its own dedicated collateral branch, which contains all the necessary logic for managing Troves, liquidating them, handling Stability Pool deposits, and facilitating redemptions specific to that branch.
- There is one
- Top-level Contracts
CollateralRegistry
– Keeps track of all LST collaterals and maps branch-specific TroveManagers to these collaterals. It calculates redemption fees and directs BOLD redemptions to the appropriate TroveManagers across different branches, proportional to their outstanding debt.BOLDToken
– This is the stablecoin token contract that implements the ERC20 standard and includes EIP-2612 permit functionality. The contract is responsible for minting, burning, and transferring BOLD tokens.
- Branch-level Contracts
BorrowerOperations
– Contains the core functions for borrowers and managers to interact with their Troves. This includes creating Troves, adding or withdrawing collateral, issuing and repaying BOLD, and adjusting interest rates.BorrowerOperations
functions call theTroveManager
to update the Trove’s state and interact with the various Pools, moving collateral and BOLD between Pools or between the Pool and user. It also instructs theActivePool
to mint interest.TroveManager
– Responsible for liquidations, redemptions, and calculating interest for individual Troves. It keeps the state of each Trove, including the collateral, debt, and interest rate. However, theTroveManager
itself does not hold any value (collateral or BOLD). It calls various Pools to move collateral or BOLD when needed.TroveNFT
– Implements basic minting and burning functionality for Trove NFTs, under the control ofTroveManager
. It also implements thetokenURI
functionality, which provides metadata for each Trove, including a unique image.LiquityBase
– Contains shared functions used byCollateralRegistry
,TroveManager
,BorrowerOperations
, andStabilityPool
.StabilityPool
– Manages Stability Pool operations like deposits, and withdrawal of compounded deposits, collateral, and BOLD gains from liquidations. It holds the Stability Pool BOLD deposits, yield gains, and liquidation collateral for all depositors on a branch.SortedTroves
– A doubly linked list that stores the addresses of Trove owners, sorted by their annual interest rate. It automatically inserts and re-inserts Troves in the correct position based on interest rates. The contract also handles inserting or re-inserting batches of Troves, modeled as slices of the doubly linked list.ActivePool
– Holds the collateral balance for a branch and tracks the total BOLD debt of the active Troves. It mints aggregate interest, split between theStabilityPool
and a yield router (currently,MockInterestRouter
for DEX LP incentives).DefaultPool
– Holds the collateral balance and BOLD debt of liquidated Troves pending redistribution to active Troves. If an active Trove has collateral and debt "rewards" pending in theDefaultPool
, they are applied to the Trove during its next borrower operation, redemption, or liquidation.CollSurplusPool
– Tracks and holds collateral surpluses from liquidated Troves. It distributes a borrower's accumulated surplus when claimed.GasPool
– Manages WETH gas compensation. When a Trove is opened, WETH is transferred from the borrower to theGasPool
and is then paid out when a Trove is liquidated or closed.MockInterestRouter
– A placeholder contract that currently receives the LP yield split of minted interest. It will later be replaced by a real yield router that directs yield to DEX LP incentives.
- Peripheral Helper Contracts
HintHelpers
– A helper contract that provides read-only functionality to calculate accurate hints, which can be supplied to borrower operations.MultiTroveGetter
– A helper contract that offers read-only functionality to fetch arrays of Trove data structs, containing the complete recorded state of each Trove.
- Oracle Price Feed Contracts Different
PriceFeed
contracts are used to price collaterals on different branches due to varying price calculation methods across LSTs. However, core functionality is shared among parent contracts. More info can be read here.MainnetPriceFeedBase
: Fetches prices from Chainlink (or Redstone) oracles and handles oracle failure.CompositePriceFeed
: Combines prices from LST-ETH and ETH-USD oracles to calculate a composite LST-USD price.WETHPriceFeed
: Fetches ETH-USD price for WETH collateral.WSTETHPriceFeed
: Computes WSTETH-USD price using STETH-USD and WSTETH-STETH exchange rates.RETHPriceFeed
: UsesCompositePriceFeed
to fetch the RETH-ETH rate for RETH collateral.
What's new in V2
- Multi-collateral system: The system now operates as a multi-collateral framework, consisting of a
CollateralRegistry
and multiple collateral branches. Each branch is independently configured with its own Minimum Collateral Ratio (MCR), Critical Collateral Ratio (CCR), and Shutdown Collateral Ratio (SCR). Each branch also has its ownTroveManager
andStabilityPool
, with Troves in a branch only accepting a single type of collateral. Liquidations in a branch are offset solely against its corresponding Stability Pool, and any liquidation gains for depositors are paid in that specific collateral. Redistribution of collateral and debt from liquidations also applies only to active Troves within the same branch. TheCollateralRegistry
constructor can accept up to 10TroveManagers
along with their respective collateral types. However, currently, only WETH and two LSTs—rETH and wstETH—are available as collateral, while native ETH is not accepted.
- User-set interest rates: Borrowers can select their own annual interest rate when opening a Trove, with the option to adjust it at any time. Simple, non-compounding interest accrues continuously on their debt and is compounded only when the Trove is interacted with. The total accrued debt is periodically minted as BOLD. The lowest interest rate isn't chosen by default because redemptions are prioritized for Troves with the lowest interest rates first, as explained later in the article.
- Interest Yield Distribution: Yield from interest is distributed to both the Stability Pool (SP) and liquidity providers (LPs). BOLD generated from Trove interest is periodically split, with one portion going to the SP and the other routed to DEX LP incentives via a yield router. Interest yield for a specific branch is always paid to the SP on that same branch. The distribution occurs inside
_mintAggInterest
in theActivePool
This function is called inside mintAggInterestAndAccountForTroveChange which is triggered by all state-changing user operations, including borrower operations, liquidations, redemptions, and Stability Pool deposits/withdrawals. If a user's operation alters the Trove’s debt, the aggregate recorded debt is updated by the total pending interest and the net Trove debt change.
- Redemption routing: BOLD redemptions are managed by the CollateralRegistry and are distributed across branches based on their level of "unbackedness." The redemption volume directed to each branch is proportional to how unbacked it is. The primary goal of redemptions is to restore the BOLD peg, while the secondary goal is to reduce the unbackedness of the most undercollateralized branches more than the better-backed ones. Unbackedness refers to the difference between a branch's total BOLD debt and the BOLD held in its Stability Pool.
- Redemption ordering: Branches where redemptions are executed are selected based on their level of unbackedness, while within each branch, redemptions target Troves in order of their annual interest rate, starting with the lowest and moving to the highest. Troves with higher interest rates are more protected from redemptions, as they have more "debt-in-front" of them compared to those with lower interest rates. The collateral ratio of a Trove is not considered in the redemption order. The actual logic for looping over the Troves is executed inside
redeemCollateral
inTroveManager
- Unredeemable Troves: Redemptions no longer close Troves but leave them open. If a redemption reduces a Trove's BOLD debt to zero or below the MIN_DEBT threshold, it is marked as unredeemable to prevent a redemption griefing attack. These Troves can become redeemable again once the borrower increases the debt back above the MIN_DEBT threshold. The function
_redeemCollateralFromTrove
inTroveManager
which is called insideredeemCollateral
is tasked to mark a Trove as unredeemable.
- Troves represented by NFTs: Troves are freely transferable and can be owned by multiple Ethereum addresses, with each Trove represented by a corresponding NFT. An address can hold multiple Troves through ownership of these NFTs. NFTs are minted when opening a Trove in
TroveManager
and burned when closing a Trove. - Individual delegation: A Trove owner can delegate an individual manager to manage their Trove, allowing the manager to set the interest rate and make adjustments to debt and collateral. The delegation is done via the
setInterestIndividualDelegate
function inBorrowerOperations
. The following function is used to check if a particular delegate is eligible to change the interest rate:
- Batch delegation: A Trove owner can delegate a batch manager to manage their interest rate. The batch manager has the ability to adjust the interest rates for all Troves in the batch within a predefined range, which is set by the manager during registration. A batch interest rate adjustment efficiently updates the interest rates for all Troves in the batch, minimizing gas costs.
- Batch Structure: Batches are modeled as slices in the SortedTroves list, utilizing the new
Batch
data structure with head and tail properties. When a batch manager updates the interest rate, the entire batch is reinserted into its appropriate position in the list based on the new interest rate. To streamline gas costs, batches are treated as "shared Troves," where the system tracks the total debt and interest rate for the batch. Interest and management fees accrue over time, with each Trove’s redistribution gains tracked individually. - Batch Management Fees: Batches accrue management fees annually, calculated similarly to interest. The recorded debt of a batch is updated when: Interest and management fees are added to the batch’s recorded debt, along with any individual changes resulting from Trove interactions.
- A borrower adjusts their Trove's debt.
- The batch manager changes the interest rate.
- Pending debt on a Trove within the batch is applied.
- Premature Adjustment Fees: Batch managers are subject to fees for premature adjustments if they alter interest rates before the cooldown period ends, similar to individual Troves. Borrowers in a batch rely on the manager’s competence to avoid excessive fees. Competent batch managers are expected to build reputations and attract more borrowers, while poor managers may see their batches empty.
- Batch Invariant: Batch Troves function equivalently to individual Troves. If two Troves—one in a batch and one independent—have identical states, they will remain identical after performing the same operations, such as adjusting collateral, debt, or receiving redistribution gains.
- Batch Structure: Batches are modeled as slices in the SortedTroves list, utilizing the new
When a batch's interest rate is updated, it automatically affects all troves within that batch:
The _updateBatchShares
function plays a key role in managing the relationship between individual troves and the batches they belong to. This function ensures that the debt and collateral associated with a trove are accurately reflected in the corresponding batch.
onOpenTroveAndJoinBatch()
, onAdjustTroveInsideBatch()
, onRegisterBatchManager()
, onLowerBatchManagerAnnualFee()
, onSetBatchManagerAnnualInterestRate()
, onSetInterestBatchManager()
, onRemoveFromBatch()
are responsible for updating troves when they belong to a batch and updating the batch manager related variables.
When performing a redemption, specific handling is in place for Troves that are part of batches:
The liquidation mechanism also takes into account whether a trove is part of a batch:
Of course, these aren't the only instances where batches are taken into account, but they serve as a good example.
- Collateral branch shutdown: In extreme situations, such as a significant collapse in the collateral market price or an oracle failure, a collateral branch will be shut down. This results in freezing all borrower operations (except for Trove closures), halting interest accrual, and enabling urgent redemptions with no redemption fee and a small collateral bonus for the redeemer. The goal is to rapidly reduce the debt from the affected branch. A branch can be shut down when the Total Collateral Ratio (TCR) falls below or is equal to the Shutdown Collateral Ratio (SCR). In this case, anyone can call the
shutdown
function in theBorrowerOperations
contract, which then shuts down all other branch contracts, such as theTroveManager
andActivePool
. The other scenario for shutdown occurs when an oracle fails (reverts, or returns 0). In this case, contracts inheriting fromMainnetPriceFeedBase
will trigger the shutdown by calling theshutdownFromOracleFailure
function inBorrowerOperations
through their_disableFeedAndShutDown
function.- Upon Shutdown
- All pending aggregate interest and batch management fees are applied and minted.
- Afterward, no further aggregate interest or batch management fees are minted or accrued.
- Individual Troves stop accruing interest, with the accrued interest calculated only up to the shutdown timestamp.
- Batches stop accruing interest and management fees, with all calculations only reflecting values up to the shutdown timestamp.
- Shutdown Logic During shutdown, the following operations are disallowed:
- Opening a new Trove.
- Adjusting a Trove’s debt, collateral, or interest rate.
- Applying a Trove’s interest.
- Adjusting a batch’s interest rate.
- Applying a batch’s interest and management fees.
- Normal redemptions.
- Allowed Operations During Shutdown
- Closing a Trove.
- Liquidating Troves.
- Depositing to and withdrawing from the Stability Pool (SP).
- Urgent redemptions (as detailed below).
- Urgent Redemptions During a shutdown, redemption logic is modified to incentivize a swift reduction of the branch's debt, even if BOLD is trading at $1 USD. Urgent redemptions:
- Are executed directly through the shut-down branch’s
TroveManager
and affect only that branch, without being routed across other branches. - Charge no redemption fee.
- Provide a 1% collateral bonus to redeemers, meaning for every 1 BOLD redeemed, the redeemer receives $1.01 worth of collateral.
- Do not redeem Troves in order of interest rate. Instead, the redeemer passes a list of Troves to redeem from.
- Do not create unredeemable Troves, even if a Trove is left with a very small or zero debt, ensuring future urgent redemptions are not hindered by tiny Troves.
- Are executed directly through the shut-down branch’s
- Upon Shutdown
BorrowerOperations
WETHPriceFeed
TroveManager
ActivePool
- Removal of Recovery Mode: The previous Recovery Mode logic has been removed. Troves are now only liquidated if their individual collateral ratio (ICR) drops below the minimum collateral ratio (MCR). However, borrowing restrictions still apply when the total collateral ratio (TCR) falls below the critical collateral ratio (CCR) for a specific branch. This is because certain operations can lower the ICR and, in turn, the TCR below the CCR. When the TCR is below the CCR, borrowing is restricted, and actions like setting interest rates or withdrawing collateral are limited, except when accompanied by a debt repayment of equal or greater value. The
_requireNewTCRisAboveCCR
function inBorrowerOperations
ensures that the TCR remains healthy during borrowing operations:
BorrowerOperations
TroveManager
- Liquidation penalties: Borrowers who are liquidated no longer always forfeit their entire collateral. Depending on the specific collateral branch and the type of liquidation, they may be able to recover a small portion of their remaining collateral.
_liquidate
inTroveManager
transfers the surplus collateral to theCollSurplusPool
:
Where the borrower can call claimColl at CollSurplusPool to claim his remaining collateral:
- Gas compensation: Liquidators are now compensated for gas costs with a mix of collateral and WETH. The liquidation reserve is always denominated in WETH, regardless of the collateral type, and includes additional compensation in the form of collateral. However, this collateral compensation is capped to prevent excessive payouts.
_getCollGasCompensation
is responsible for calculating the amount of compensation:
The _sendGasCompensation
function is used to pull the gas from the ActivePool
or GasPool
contract, whose sole purpose is to hold gas reserves for paying liquidators.
- More flexibility for SP reward claiming: Stability Pool (SP) depositors now have the option to either claim or stash their LST gains from liquidations. Additionally, they can choose to claim their BOLD yield gains or automatically add them to their existing deposit.
Security Perspective
Oracle Frontrunning
Push oracles are used for collateral pricing, which can be vulnerable to frontrunning attacks. In such attacks, an attacker can observe an upcoming price increase from the oracle in the mempool, execute a redemption transaction before the update is processed, and then sell the redeemed collateral at a higher price after the price increase is validated. This allows the attacker to extract profits beyond typical arbitrage gains.
In Liquity v1, this issue was mitigated by a 0.5% minimum redemption fee, matching Chainlink's ETH-USD oracle update threshold of 0.5%. In v2, some LST-ETH oracles have larger update thresholds (e.g., 2% for Chainlink’s RETH-ETH), but frontrunning is expected to be less of an issue due to the stability of these feeds, which typically update based on a heartbeat rather than price deviations.
Redemption Routing Manipulation
The redemption routing logic reduces the "outside" debt of each branch by the same percentage, where outside debt for a branch is calculated as:
outside_debt_i = bold_debt_i - bold_in_SP_i
This allows a redeemer to temporarily manipulate the outside debt of certain branches by depositing into the Stability Pool (SP) of branches they don’t wish to redeem from. This tactic directs redemptions toward branches where the redeemer desires, allowing them to target specific Liquid Staking Tokens (LSTs) with potentially lower slippage in external markets.
An attacker could achieve this by depositing BOLD into unwanted branches’ SPs and redeeming from chosen branches in the same transaction, possibly funded through a flash loan.
Solution: Currently, no fix is in place because:
- Redemption arbitrage is highly competitive, and flash loan fees reduce the attacker's profits.
- The manipulation does not extract direct value from the system.
- Redemption routing is a soft measure intended to nudge the system toward better health but is not critical to system stability, which relies primarily on the health of collateral markets.
Path-Dependent Redemption Fee
The redemption fee in Liquity v2 is path-dependent. This means that redeeming a large amount of BOLD in one transaction incurs a higher fee compared to redeeming the same amount split across multiple smaller transactions (assuming no changes to the system state between transactions). As a result, redeemers may be incentivized to split their redemptions into smaller chunks to minimize the total fee.
Example:
An example illustrating this fee structure can be found in this spreadsheet.
Solution:
No fix is considered necessary for the following reasons:
- Competitive Arbitrage: Redemption arbitrage is competitive, and profit margins are slim. Splitting redemptions into smaller transactions increases the total gas cost, which reduces overall arbitrage profits.
- Proven Stability: The same fee formula was used in Liquity v1, where it functioned effectively in maintaining the BOLD peg.
- Non-Critical Optimization: The redemption fee formula’s parameters are "best-guess" estimates, and there’s no reason to assume that even the intended fee structure is perfectly optimal.
Oracle Failure and Branch Shutdown Consequences
When an oracle failure triggers a branch shutdown, the respective PriceFeed’s fetchPrice
function defaults to the recorded lastGoodPrice for that branch's LST. After shutdown, this price is used for urgent redemptions, even though the real market price may differ, causing potential distortions:
Solution Status: No fix is currently in place for the following reasons:
- Over-Redemption (lastGoodPrice < market price): While urgent redemptions return too much collateral, they still help clear debt from the branch.
- Under-Redemption (lastGoodPrice > market price): In this case, some BOLD debt remains uncleared on the shut-down branch, resulting in unbacked debt. This is a known risk in a multi-collateral system and is dependent on the economic health of integrated LST assets. A solution for clearing bad debt remains to be implemented (see Branch shutdown and bad debt section).
- Oracle Failure Likelihood: Oracle failures are more likely to result from a disabled Chainlink feed rather than a technical issue or hack. A disabled LST oracle likely points to low liquidity or volume for that asset, which suggests it constitutes a small portion of the total Liquity v2 collateral.
Stale oracle price before shutdown triggered
Liquity v2 monitors the market oracle answers for staleness, and if they exceed a pre-set threshold, it triggers a shutdown for the associated branch. However, during the period between the last oracle update and the branch shutdown, the system continues to use the most recent (possibly outdated) oracle price. This can lead to pricing discrepancies that cause distortions, such as:
- Profitable or unprofitable redemptions due to outdated collateral prices.
- Under-collateralized borrowing, where users borrow BOLD with insufficient collateral.
- Liquidation of healthy Troves if the collateral price is inaccurately low.
Solution: To minimize these risks, carefully selected staleness threshold parameters are essential. More volatile feeds like ETH-USD and STETH-USD should have lower staleness thresholds compared to LST-ETH feeds, as the USD feeds are more prone to significant price deviations. All staleness thresholds should be longer than the push oracle’s update heartbeat to avoid premature shutdowns.
Conclusion
Overall, Liquity V2 brings fresh innovation to the DeFi space, making its already successful predecessor even more competitive among lending protocols. Building on the strong foundation of the original, Liquity v2 is poised for success with its new features and improvements, offering benefits that will undoubtedly resonate with users. We look forward to a smooth and successful launch for the Liquity team!
Reach out to Three Sigma, and let our team of seasoned professionals guide you confidently through the Web3 landscape. Our expertise in smart contract security, economic modeling, and blockchain engineering, we will help you secure your project's future.
Contact us today and turn your Web3 vision into reality!
References
https://github.com/liquity/dev/blob/main/README.md
https://www.liquity.org/blog/liquity-v2-enhancing-the-borrowing-experience
https://www.liquity.org/blog/liquity-v2-why-user-set-interest-rates