Back to Blog
Liquity Protocol — Stability Pool, Liquidations & Redemptions (Part 2)
DeFiSolidityWeb3 Security

Liquity Protocol — Stability Pool, Liquidations & Redemptions (Part 2)

7 min
In a previous article, I started explaining Liquity protocol from its smart contracts, where I went through its native tokens and its BorrowerOperations.sol among other things.
In this article, we will continue going through three of the most important and interesting parts of the protocol:

Stability Pool

The Stability Pool, as the name suggests, ensures that the protocol remains stable and can operate at peak efficiency.
When any Trove is liquidated, an amount of LUSD corresponding to the remaining debt of the Trove is burned from the Stability Pool's balance to repay its debt. In exchange, the entire collateral from the Trove is transferred to the Stability Pool.
The necessary amount of LUSD is removed from the pool to cancel out the debt, and then the locked Ethereum is distributed proportionally to the pool's stakers. These are known as "liquidation gains."

StabilityPool.sol

Contains functionality for Stability Pool operations: making deposits, and withdrawing compounded deposits and accumulated ETH and LQTY gains.
Holds the LUSD Stability Pool deposits and the ETH gains for depositors, from liquidations.
Some of StabilityPool's main functions are:
  • provideToSP(uint _amount, address _frontEndTag) — allows stablecoin holders to deposit _amount of LUSD to the Stability Pool.
  • withdrawFromSP(uint _amount) — allows a stablecoin holder to withdraw _amount of LUSD from the Stability Pool, up to the value of their remaining Stability deposit.
  • withdrawETHGainToTrove(address _hint) — sends the user's entire accumulated ETH gain to the user's active Trove, and updates their Stability deposit with its accumulated loss from debt absorptions.
  • registerFrontEnd(uint _kickbackRate) — registers an address as a front end and sets their chosen kickback rate in the range [0,1].
Along with StabilityPool.sol, these contracts hold Ether and/or tokens for their respective parts of the system, and contain minimal logic:

ActivePool.sol

Holds the total Ether balance and records the total stablecoin debt of the active Troves.
1function sendETH(address _account, uint _amount) external override {
2 _requireCallerIsBOorTroveMorSP();
3 ETH = ETH.sub(_amount);
4 emit ActivePoolETHBalanceUpdated(ETH);
5 emit EtherSent(_account, _amount);
6
7 (bool success, ) = _account.call{ value: _amount }("");
8 require(success, "ActivePool: sending ETH failed");
9}
10
11function increaseLUSDDebt(uint _amount) external override {
12 _requireCallerIsBOorTroveM();
13 LUSDDebt = LUSDDebt.add(_amount);
14 ActivePoolLUSDDebtUpdated(LUSDDebt);
15}
16
17function decreaseLUSDDebt(uint _amount) external override {
18 _requireCallerIsBOorTroveMorSP();
19 LUSDDebt = LUSDDebt.sub(_amount);
20 ActivePoolLUSDDebtUpdated(LUSDDebt);
21}

DefaultPool.sol

Holds the total Ether balance and records the total stablecoin debt of the liquidated Troves that are pending redistribution to active Troves.
If a Trove has pending ether/debt "rewards" in the DefaultPool, then they will be applied to the Trove when it next undergoes a borrower operation, a redemption, or a liquidation.
1function sendETHToActivePool(uint _amount) external override {
2 _requireCallerIsTroveManager();
3 address activePool = activePoolAddress; // cache to save an SLOAD
4 ETH = ETH.sub(_amount);
5 emit DefaultPoolETHBalanceUpdated(ETH);
6 emit EtherSent(activePool, _amount);
7
8 (bool success, ) = activePool.call{ value: _amount }("");
9 require(success, "DefaultPool: sending ETH failed");
10}

CollSurplusPool.sol

Holds the ETH surplus from Troves that have been fully redeemed and from Troves with an ICR > MCR that were liquidated in Recovery Mode. Sends the surplus back to the owning borrower, when told to do so by BorrowerOperations.sol.
1function accountSurplus(address _account, uint _amount) external override {
2 _requireCallerIsTroveManager();
3
4 uint newAmount = balances[_account].add(_amount);
5 balances[_account] = newAmount;
6
7 emit CollBalanceUpdated(_account, newAmount);
8}
9
10function claimColl(address _account) external override {
11 _requireCallerIsBorrowerOperations();
12 uint claimableColl = balances[_account];
13 require(claimableColl > 0, "CollSurplusPool: No collateral available to claim");
14
15 balances[_account] = 0;
16 emit CollBalanceUpdated(_account, 0);
17
18 ETH = ETH.sub(claimableColl);
19 emit EtherSent(_account, claimableColl);
20
21 (bool success, ) = _account.call{ value: claimableColl }("");
22 require(success, "CollSurplusPool: sending ETH failed");
23}

GasPool.sol

Holds the total LUSD liquidation reserves. LUSD is moved into the GasPool when a Trove is opened and moved out when a Trove is liquidated or closed.

Liquidations

Liquity's mechanism for liquidation consists of offsetting the liquidated debt with the Stability Pool, and then if there is not enough LUSD in the pool, redistributing the remaining debt over all active Troves ordered by collateral amounts.
Liquidations only take place under two circumstances:
  • When a Trove falls below the minimum collateral ratio of 110%
  • If the system is in Recovery Mode

Get the DeFi Protocol Security Checklist

15 vulnerabilities every DeFi team should check before mainnet. Used by 30+ protocols.

No spam. Unsubscribe anytime.

The owner of a liquidated Trove is allowed to keep the LUSD they were loaned, but will still lose money since they won't receive their collateral back (which will always be higher than the price of the loan).

Who can liquidate Troves?

Anybody can liquidate a Trove as soon as it drops below the Minimum Collateral Ratio of 110%. The initiator receives a gas compensation (200 LUSD + 0.5% of the Trove's collateral) as reward for this service.

Normal Mode

There are two different liquidation modes in Liquity. In Normal Mode:
1function _liquidateNormalMode(
2 IActivePool _activePool,
3 IDefaultPool _defaultPool,
4 address _borrower,
5 uint _LUSDInStabPool
6) internal returns (LiquidationValues memory singleLiquidation)
7{
8 [...]
9 (singleLiquidation.entireTroveDebt,
10 singleLiquidation.entireTroveColl,
11 vars.pendingDebtReward,
12 vars.pendingCollReward) = getEntireDebtAndColl(_borrower);
13
14 _movePendingTroveRewardsToActivePool(
15 _activePool, _defaultPool,
16 vars.pendingDebtReward, vars.pendingCollReward
17 );
18 _removeStake(_borrower);
19
20 [...]
21 (singleLiquidation.debtToOffset,
22 singleLiquidation.collToSendToSP,
23 singleLiquidation.debtToRedistribute,
24 singleLiquidation.collToRedistribute) =
25 _getOffsetAndRedistributionVals(
26 singleLiquidation.entireTroveDebt,
27 collToLiquidate,
28 _LUSDInStabPool
29 );
30
31 _closeTrove(_borrower, Status.closedByLiquidation);
32
33 [...]
34}

Recovery Mode

Recovery Mode is triggered if the total CR of the Liquity system (TCR) is below the Critical system Collateral Ratio (CCR, 150%). During Recovery Mode, Troves with an ICR below the TCR can also be liquidated, providing additional protection for the system.

Redemptions

Redemption is the process of exchanging LUSD for ETH at face value, as if 1 LUSD is exactly worth $1. That is, for x LUSD you get x Dollars worth of ETH in return.
Users can redeem their LUSD for ETH at any time without limitations. However, a redemption fee might be charged on the redeemed amount.

What are LUSD redemptions?

LUSD redemptions are one of Liquity's most unique features. Simply put: the redemption mechanism gives LUSD holders the ability to redeem LUSD at face value for the underlying ETH collateral at any time.

TroveManager.sol — Redemption Logic

The logic for redemption is located in TroveManager.sol where it starts to search the first Trove whose ICR is bigger than or equal to MCR (110%).
1function redeemCollateral(
2 uint _LUSDamount,
3 address _firstRedemptionHint,
4 address _upperPartialRedemptionHint,
5 address _lowerPartialRedemptionHint,
6 uint _partialRedemptionHintNICR,
7 uint _maxIterations,
8 uint _maxFeePercentage
9)
10 external
11 override
12{
13 [...]
14
15 if (_isValidFirstRedemptionHint(
16 contractsCache.sortedTroves,
17 _firstRedemptionHint,
18 totals.price))
19 {
20 currentBorrower = _firstRedemptionHint;
21 } else {
22 currentBorrower = contractsCache.sortedTroves.getLast();
23 // Find the first trove with ICR >= MCR
24 while (currentBorrower != address(0)
25 && getCurrentICR(currentBorrower, totals.price) < MCR)
26 {
27 currentBorrower = contractsCache.sortedTroves.getPrev(
28 currentBorrower
29 );
30 }
31 }
32
33 [...]
34}
The redemption mechanism traverses the sorted Troves list from lowest ICR upward, redeeming from each Trove until the full LUSD amount is covered. This design naturally targets the riskiest positions first, reinforcing system health. Liquity forks that modify this traversal logic — for example by skipping certain Troves or capping iterations differently — frequently introduce edge cases around partial redemptions. For related vulnerability patterns in DeFi lending and borrowing protocols, see our guide on oracle manipulation and reentrancy attacks.

Get in touch

At Zealynx, we specialize in DeFi protocol security audits — from lending and borrowing systems like Liquity to complex stablecoin mechanisms and liquidation logic. Whether you're building a CDP-based protocol or forking an existing one, our team is ready to help you ship secure code. Before engaging an auditor, review our pre-audit checklist to make sure your codebase is ready. Reach out to start the conversation.
Want to stay ahead with more in-depth analyses like this? Subscribe to our newsletter and ensure you don't miss out on future insights.

Get the DeFi Protocol Security Checklist

15 vulnerabilities every DeFi team should check before mainnet. Used by 30+ protocols.

No spam. Unsubscribe anytime.

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx