# Walk-Through of Liquity codebase
###### tags: `DeFi` `Liquity`
## Redemption

Liquity allows to [redeem](https://www.liquity.org/blog/understanding-liquitys-redemption-mechanism) ETH collateral from others' Trove by burning LUSD from redeemer. A [doubly-linked sorted Trove list](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/SortedTroves.sol) (descending from higher `NICR` to lower `NICR`, i.e., **N**ominal **I**ndividual **C**ollateral **R**atio) is used to facilitate the redemption operation.
#### Doubly-Linked Sorted Trove List
Each `node` in the list is structured as following:
```
struct Node {
bool exists;
address nextId; // Id of next node (smaller NICR) in the list
address prevId; // Id of previous node (larger NICR) in the list
}
```
For `remove` operation, it is quite straight-forward: simply update the `pointer` of nodes `prevId` and `nextId` accordingly:
```
// Set next pointer of previous node to the next node
data.nodes[data.nodes[_id].prevId].nextId = data.nodes[_id].nextId;
// Set prev pointer of next node to the previous node
data.nodes[data.nodes[_id].nextId].prevId = data.nodes[_id].prevId;
```
For `insert` operation, to maintain the ordering of the sorted list after insertion, we need both `prevId` and `nextId` to pinpoint the correct position to insert:
```
function _insert(ITroveManager _troveManager, address _id, uint256 _NICR, address _prevId, address _nextId) internal {
...
// Insert at insert position between `prevId` and `nextId`
data.nodes[_id].nextId = nextId;
data.nodes[_id].prevId = prevId;
data.nodes[prevId].nextId = _id;
data.nodes[nextId].prevId = _id;
...
}
```
#### Redeem Collateral
The logic for redemption is located in [TroveManager.sol](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/TroveManager.sol#L925) where it start to search the first Trove whose `ICR` is bigger than or equal to [`MCR` (`110%`)](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/Dependencies/LiquityBase.sol#L22). In other words, the first Trove that will be redeemed against would be the most risky one.
```
if (_isValidFirstRedemptionHint(contractsCache.sortedTroves, _firstRedemptionHint, totals.price)) {
currentBorrower = _firstRedemptionHint;
} else {
currentBorrower = contractsCache.sortedTroves.getLast();
// Find the first trove with ICR >= MCR
while (currentBorrower != address(0) && getCurrentICR(currentBorrower, totals.price) < MCR) {
currentBorrower = contractsCache.sortedTroves.getPrev(currentBorrower);
}
}
```
The redemption process continue the **search loop** until some predefined conditions for [early termination](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/TroveManager.sol#L991) are hit.
```
// Loop through the Troves starting from the one with lowest collateral ratio until _amount of LUSD is exchanged for collateral
if (_maxIterations == 0) { _maxIterations = uint(-1); }
while (currentBorrower != address(0) && totals.remainingLUSD > 0 && _maxIterations > 0) {
_maxIterations--;
// Save the address of the Trove preceding the current one, before potentially modifying the list
address nextUserToCheck = contractsCache.sortedTroves.getPrev(currentBorrower);
_applyPendingRewards(contractsCache.activePool, contractsCache.defaultPool, currentBorrower);
SingleRedemptionValues memory singleRedemption = _redeemCollateralFromTrove(
contractsCache,
currentBorrower,
totals.remainingLUSD,
totals.price,
_upperPartialRedemptionHint,
_lowerPartialRedemptionHint,
_partialRedemptionHintNICR
);
if (singleRedemption.cancelledPartial) break; // Partial redemption was cancelled (out-of-date hint, or new net debt < minimum), therefore we could not redeem from the last Trove
totals.totalLUSDToRedeem = totals.totalLUSDToRedeem.add(singleRedemption.LUSDLot);
totals.totalETHDrawn = totals.totalETHDrawn.add(singleRedemption.ETHLot);
totals.remainingLUSD = totals.remainingLUSD.sub(singleRedemption.LUSDLot);
currentBorrower = nextUserToCheck;
}
```
Finally, the redemption will [update `baseRate`](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/TroveManager.sol#L1358) and calculate `redemption fee`(sent to `LQTY staking contract`) before sending redeemed collateral to redeemer and burning the `LUSD`.
```
function _calcDecayedBaseRate() internal view returns (uint) {
uint minutesPassed = _minutesPassedSinceLastFeeOp();
uint decayFactor = LiquityMath._decPow(MINUTE_DECAY_FACTOR, minutesPassed);
return baseRate.mul(decayFactor).div(DECIMAL_PRECISION);
}
/*
* This function has two impacts on the baseRate state variable:
* 1) decays the baseRate based on time passed since last redemption or LUSD borrowing operation.
* then,
* 2) increases the baseRate based on the amount redeemed, as a proportion of total supply
*/
function _updateBaseRateFromRedemption(uint _ETHDrawn, uint _price, uint _totalLUSDSupply) internal returns (uint) {
uint decayedBaseRate = _calcDecayedBaseRate();
/* Convert the drawn ETH back to LUSD at face value rate (1 LUSD:1 USD), in order to get
* the fraction of total supply that was redeemed at face value. */
uint redeemedLUSDFraction = _ETHDrawn.mul(_price).div(_totalLUSDSupply);
uint newBaseRate = decayedBaseRate.add(redeemedLUSDFraction.div(BETA));
newBaseRate = LiquityMath._min(newBaseRate, DECIMAL_PRECISION); // cap baseRate at a maximum of 100%
...
}
```
#### Hint for Trove positions
There are some interesting [`hint` parameters](https://github.com/liquity/beta/blob/main/README.md#hints-for-redeemcollateral) required to call `redeemCollateral` on `TroveManager` for gas efficiency due to the sorted list operation as mentioned above. A possible hint could be made available by [querying `HintHelper` smart contract](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/HintHelpers.sol) from off-chain frontend or [script](https://github.com/liquity/dev/blob/main/packages/contracts/utils/hintExamples.js#L86).
- firstly call `HintHelper.getRedemptionHints()` which simulates the redemption and return the `NICR` for the last (possibly) Trove as `partialRedemptionHintNICR`
- secondly call `HintHelper.getApproxHint()` which use a trial-based method to loop-search for a position close to the correct index. Note this function could require a trial number as `15 * sqrt(troveSize)` as probabilistic.
```
while (i < _numTrials) {
latestRandomSeed = uint(keccak256(abi.encodePacked(latestRandomSeed)));
uint arrayIndex = latestRandomSeed % arrayLength;
address currentAddress = troveManager.getTroveFromTroveOwnersArray(arrayIndex);
uint currentNICR = troveManager.getNominalICR(currentAddress);
// check if abs(current - CR) > abs(closest - CR), and update closest if current is closer
uint currentDiff = LiquityMath._getAbsoluteDifference(currentNICR, _CR);
if (currentDiff < diff) {
diff = currentDiff;
hintAddress = currentAddress;
}
i++;
}
```
- Lastly use Sorted List [`findInsertPosition()`](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/SortedTroves.sol#L377) to find the exact position for re-insertion of the last partially-redeemed Trove.
## Stability Pool

[Stability pool](https://www.liquity.org/blog/understanding-liquitys-stability-pool) allows LUSD depositor to earn collateral Ether at the expense of lossing deposited LUSD (pro rata) as part of offsetting debt from liquidated Troves.
In order to calculate the Ether gain and apply LUSD loss fairly among all depositors and across liquidations, Liquity utilizes [two accumulative global variables](https://github.com/liquity/liquity/blob/master/papers/Scalable_Reward_Distribution_with_Compounding_Stakes.pdf) to index all the changes made to the system by liquidation:
- [`P` (stored as `uint`)](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/StabilityPool.sol#L202) which is a function of `(LUSD loss)`
- [`S` (stored as nested `mapping` epoch->scale->S)](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/StabilityPool.sol#L220) which is a function of `(P, Ether gain, total Deposits)`
Global `P` and `S` are [updated upon each liquidation](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/StabilityPool.sol#L574) as following (omitting additional implementation details for edge cases like if one liquidation empties the Stability Pool or updated `P` is too small to be rounded to zero)
```
// Update the Stability Pool reward sum S and product P
function _updateRewardSumAndProduct(uint _ETHGainPerUnitStaked, uint _LUSDLossPerUnitStaked) internal {
...
/*
* The newProductFactor is the factor by which to change all deposits, due to the depletion of Stability Pool LUSD in the liquidation.
* We make the product factor 0 if there was a pool-emptying. Otherwise, it is (1 - LUSDLossPerUnitStaked)
*/
uint newProductFactor = uint(DECIMAL_PRECISION).sub(_LUSDLossPerUnitStaked);
...
uint marginalETHGain = _ETHGainPerUnitStaked.mul(currentP);
uint newS = currentS.add(marginalETHGain);
epochToScaleToSum[currentEpochCached][currentScaleCached] = newS;
...
newP = currentP.mul(newProductFactor).div(DECIMAL_PRECISION);
...
P = newP;
...
}
```
Then upon liquidation, `TroveManager` will call `offset()` on `Stablility Pool` when [`LQTY` rewards](https://www.liquity.org/blog/liquity-launch-details) for both LUSD depositors and frontends are also calculated and issued (only sent to receipient upon [deposit into](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/StabilityPool.sol#L332) or [withdraw from](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/StabilityPool.sol#L379) Stability pool, so-called `Pull-Based`)
```
/*
* Cancels out the specified debt against the LUSD contained in the Stability Pool (as far as possible)
* and transfers the Trove's ETH collateral from ActivePool to StabilityPool.
* Only called by liquidation functions in the TroveManager.
*/
function offset(uint _debtToOffset, uint _collToAdd) external override {
_requireCallerIsTroveManager();
uint totalLUSD = totalLUSDDeposits; // cached to save an SLOAD
if (totalLUSD == 0 || _debtToOffset == 0) { return; }
_triggerLQTYIssuance(communityIssuance);
(uint ETHGainPerUnitStaked, uint LUSDLossPerUnitStaked) = _computeRewardsPerUnitStaked(_collToAdd, _debtToOffset, totalLUSD);
_updateRewardSumAndProduct(ETHGainPerUnitStaked, LUSDLossPerUnitStaked); // updates S and P
_moveOffsetCollAndDebt(_collToAdd, _debtToOffset);
}
```
When user deposit or withdraw, the [reward `ETHGain`](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/StabilityPool.sol#L661) and [LUSD loss(`compoundedStake`)](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/StabilityPool.sol#L778) are calculated by snapshot of `P` & `S` at the time when the user deposit along with their current global values.
```
...
* We calculate the depositor's accumulated ETH gain for the scale at which they made the deposit, using the ETH gain formula:
* e_1 = d_t * (S - S_t) / P_t
*
* and also for scale after, taking care to divide the latter by a factor of 1e9:
* e_2 = d_t * S / (P_t * 1e9)
*
* The gain in the second scale will be full, as the starting point was in the previous scale, thus no need to subtract anything.
* The deposit therefore was present for reward events from the beginning of that second scale.
...
uint firstPortion = epochToScaleToSum[epochSnapshot][scaleSnapshot].sub(S_Snapshot);
uint secondPortion = epochToScaleToSum[epochSnapshot][scaleSnapshot.add(1)].div(SCALE_FACTOR);
uint ETHGain = initialDeposit.mul(firstPortion.add(secondPortion)).div(P_Snapshot).div(DECIMAL_PRECISION);
...
compoundedStake = initialStake.mul(P).div(snapshot_P);
...
```
## Liquidation

Liquity employs [two-steps mechanism](https://www.liquity.org/blog/how-faster-liquidations-improve-capital-efficiency) for liquidation: firstly offset the liquidated debt with Stablity Pool; then if not enough, redistribute remaining debt over all active Troves ordering by collateral amounts. Again, another pair of [global tracking indexes (`L_ETH` & `L_LUSDDebt`)](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/TroveManager.sol#L91-L106) are used to calculate the accumulated collateral and debt distributions (they even call it `reward` in the codeš¤£) for active Troves.
```
/*
* L_ETH and L_LUSDDebt track the sums of accumulated liquidation rewards per unit staked. During its lifetime, each stake earns:
*
* An ETH gain of ( stake * [L_ETH - L_ETH(0)] )
* A LUSDDebt increase of ( stake * [L_LUSDDebt - L_LUSDDebt(0)] )
*
* Where L_ETH(0) and L_LUSDDebt(0) are snapshots of L_ETH and L_LUSDDebt for the active Trove taken at the instant the stake was made
*/
uint public L_ETH;
uint public L_LUSDDebt;
```
There are also two different liquidation modes in Liquity: [normal mode](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/TroveManager.sol#L314) and [recovery mode]() which is triggered if Total `CR` of Liquity system(`TCR`) is below [**C**ritical system **C**ollateral **R**atio (`CCR, 150%`)](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/Dependencies/LiquityBase.sol#L25).
For normal mode, each Trove is liquidated by [following](https://github.com/liquity/dev#liquidations-in-normal-mode-tcr--150) above two-steps to apply changes to accounting:
```
// Liquidate one trove, in Normal Mode.
function _liquidateNormalMode(IActivePool _activePool, IDefaultPool _defaultPool, address _borrower,uint _LUSDInStabPool) internal returns (LiquidationValues memory singleLiquidation)
{
...
(singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, vars.pendingDebtReward, vars.pendingCollReward) = getEntireDebtAndColl(_borrower);
_movePendingTroveRewardsToActivePool(_activePool, _defaultPool, vars.pendingDebtReward, vars.pendingCollReward);
_removeStake(_borrower);
...
(singleLiquidation.debtToOffset, singleLiquidation.collToSendToSP, singleLiquidation.debtToRedistribute, singleLiquidation.collToRedistribute) = _getOffsetAndRedistributionVals(singleLiquidation.entireTroveDebt, collToLiquidate, _LUSDInStabPool);
_closeTrove(_borrower, Status.closedByLiquidation);
...
}
```
For recovery mode, there are [various situations](https://github.com/liquity/dev#liquidations-in-recovery-mode-tcr--150) to handle, depending on current Trove `ICR`:
```
// Liquidate one trove, in Recovery Mode.
function _liquidateRecoveryMode(IActivePool _activePool, IDefaultPool _defaultPool, address _borrower, uint _ICR, uint _LUSDInStabPool, uint _TCR, uint _price) internal returns (LiquidationValues memory singleLiquidation)
{
...
// If ICR <= 100%, purely redistribute the Trove across all active Troves
if (_ICR <= _100pct) {
...
singleLiquidation.debtToRedistribute = singleLiquidation.entireTroveDebt;
singleLiquidation.collToRedistribute = vars.collToLiquidate;
...
// If 100% < ICR < MCR, offset as much as possible, and redistribute the remainder
} else if ((_ICR > _100pct) && (_ICR < MCR)) {
...
(singleLiquidation.debtToOffset, singleLiquidation.collToSendToSP, singleLiquidation.debtToRedistribute, singleLiquidation.collToRedistribute) = _getOffsetAndRedistributionVals(singleLiquidation.entireTroveDebt, vars.collToLiquidate, _LUSDInStabPool);
...
/*
* If 110% <= ICR < current TCR (accounting for the preceding liquidations in the current sequence)
* and there is LUSD in the Stability Pool, only offset, with no redistribution,
* but at a capped rate of 1.1 and only if the whole debt can be liquidated.
* The remainder due to the capped rate will be claimable as collateral surplus.
*/
} else if ((_ICR >= MCR) && (_ICR < _TCR) && (singleLiquidation.entireTroveDebt <= _LUSDInStabPool)) {
...
singleLiquidation = _getCappedOffsetVals(singleLiquidation.entireTroveDebt, singleLiquidation.entireTroveColl, _price);
...
if (singleLiquidation.collSurplus > 0) {
collSurplusPool.accountSurplus(_borrower, singleLiquidation.collSurplus);
}
}
...
}
```
## Other Pools
#### Active Pool
This [pool](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/ActivePool.sol) holds the ETH collateral and LUSD debt (but not LUSD tokens) for all active troves. When a trove is liquidated, it's ETH and LUSD debt are transferred from the Active Pool, to either the Stability Pool, the Default Pool, or both, depending on the liquidation conditions.
```
// --- Pool functionality ---
function sendETH(address _account, uint _amount) external override {
_requireCallerIsBOorTroveMorSP();
ETH = ETH.sub(_amount);
emit ActivePoolETHBalanceUpdated(ETH);
emit EtherSent(_account, _amount);
(bool success, ) = _account.call{ value: _amount }("");
require(success, "ActivePool: sending ETH failed");
}
function increaseLUSDDebt(uint _amount) external override {
_requireCallerIsBOorTroveM();
LUSDDebt = LUSDDebt.add(_amount);
ActivePoolLUSDDebtUpdated(LUSDDebt);
}
function decreaseLUSDDebt(uint _amount) external override {
_requireCallerIsBOorTroveMorSP();
LUSDDebt = LUSDDebt.sub(_amount);
ActivePoolLUSDDebtUpdated(LUSDDebt);
}
```
Upon [`openTrove()`](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L156), `BorrowerOperations` smart contract will send Ether from user to Active Pool
```
// Send ETH to Active Pool and increase its recorded ETH balance
function _activePoolAddColl(IActivePool _activePool, uint _amount) internal {
(bool success, ) = address(_activePool).call{value: _amount}("");
require(success, "BorrowerOps: Sending ETH to ActivePool failed");
}
```
#### Default Pool
This [pool](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/DefaultPool.sol) holds the ETH and LUSD debt (but not LUSD tokens) from liquidations that have been redistributed to active troves but not yet "applied", i.e. not yet recorded on a recipient active trove's struct. When a trove makes an operation that applies its pending ETH and LUSD debt (like being liquidated or redeemed against), its pending (accumulated) ETH and LUSD debt from distribution are moved from the Default Pool to the Active Pool, i.e., made available to the Trove.
```
// --- Pool functionality ---
function sendETHToActivePool(uint _amount) external override {
_requireCallerIsTroveManager();
address activePool = activePoolAddress; // cache to save an SLOAD
ETH = ETH.sub(_amount);
emit DefaultPoolETHBalanceUpdated(ETH);
emit EtherSent(activePool, _amount);
(bool success, ) = activePool.call{ value: _amount }("");
require(success, "DefaultPool: sending ETH failed");
}
function increaseLUSDDebt(uint _amount) external override {
_requireCallerIsTroveManager();
LUSDDebt = LUSDDebt.add(_amount);
emit DefaultPoolLUSDDebtUpdated(LUSDDebt);
}
function decreaseLUSDDebt(uint _amount) external override {
_requireCallerIsTroveManager();
LUSDDebt = LUSDDebt.sub(_amount);
emit DefaultPoolLUSDDebtUpdated(LUSDDebt);
}
```
Upon liquidation, `TroveManager` will transfer Ether collateral to Default Pool from Active Pool out of liquiated Troves
```
function _redistributeDebtAndColl(IActivePool _activePool, IDefaultPool _defaultPool, uint _debt, uint _coll) internal {
...
// Transfer coll and debt from ActivePool to DefaultPool
_activePool.decreaseLUSDDebt(_debt);
_defaultPool.increaseLUSDDebt(_debt);
_activePool.sendETH(address(_defaultPool), _coll);
}
```
#### Collateral Surplus Pool
This [pool](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/CollSurplusPool.sol) holds Ether collateral for closed Trove upon [recovery liquidation](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/TroveManager.sol#L414) or [full redemption](https://github.com/liquity/dev/blob/main/packages/contracts/contracts/TroveManager.sol#L888).
```
// --- Pool functionality ---
function accountSurplus(address _account, uint _amount) external override {
_requireCallerIsTroveManager();
uint newAmount = balances[_account].add(_amount);
balances[_account] = newAmount;
emit CollBalanceUpdated(_account, newAmount);
}
function claimColl(address _account) external override {
_requireCallerIsBorrowerOperations();
uint claimableColl = balances[_account];
require(claimableColl > 0, "CollSurplusPool: No collateral available to claim");
balances[_account] = 0;
emit CollBalanceUpdated(_account, 0);
ETH = ETH.sub(claimableColl);
emit EtherSent(_account, claimableColl);
(bool success, ) = _account.call{ value: claimableColl }("");
require(success, "CollSurplusPool: sending ETH failed");
}
```