Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optimize _calcSurplusAndCap() in LiquidationLibrary #687

Merged
merged 5 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 66 additions & 39 deletions packages/contracts/contracts/LiquidationLibrary.sol
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,15 @@ contract LiquidationLibrary is CdpManagerStorage {
);

{
(_cappedColPortion, _collSurplus, _debtToRedistribute) = _calculateSurplusAndCap(
(
_cappedColPortion,
_collSurplus,
_debtToRedistribute
) = _calculateFullLiquidationSurplusAndCap(
_liqState.ICR,
_liqState.price,
_totalDebtToBurn,
_totalColToSend,
true
_totalColToSend
);
if (_collSurplus > 0) {
// due to division precision loss, should be zero surplus in normal mode
Expand Down Expand Up @@ -308,12 +311,15 @@ contract LiquidationLibrary is CdpManagerStorage {

// avoid stack too deep
{
(_cappedColPortion, _collSurplus, _debtToRedistribute) = _calculateSurplusAndCap(
(
_cappedColPortion,
_collSurplus,
_debtToRedistribute
) = _calculateFullLiquidationSurplusAndCap(
_recoveryState.ICR,
_recoveryState.price,
_totalDebtToBurn,
_totalColToSend,
true
_totalColToSend
);
if (_collSurplus > 0) {
collSurplusPool.increaseSurplusCollShares(_borrower, _collSurplus);
Expand Down Expand Up @@ -389,12 +395,11 @@ contract LiquidationLibrary is CdpManagerStorage {
uint256 newDebt = _debtAndColl.entireDebt - _partialDebt;

// credit to https://arxiv.org/pdf/2212.07306.pdf for details
(uint256 _partialColl, uint256 newColl, ) = _calculateSurplusAndCap(
(uint256 _partialColl, uint256 newColl, ) = _calculatePartialLiquidationSurplusAndCap(
_partialState.ICR,
_partialState.price,
_partialDebt,
_debtAndColl.entireColl,
false
_debtAndColl.entireColl
);

// early return: if new collateral is zero, we have a full liqudiation
Expand Down Expand Up @@ -525,45 +530,67 @@ contract LiquidationLibrary is CdpManagerStorage {
);
}

// Function that calculates the amount of collateral to send to liquidator (plus incentive) and the amount of collateral surplus
function _calculateSurplusAndCap(
// Partial Liquidation Cap Logic
function _calculatePartialLiquidationSurplusAndCap(
uint256 _ICR,
uint256 _price,
uint256 _totalDebtToBurn,
uint256 _totalColToSend,
bool _fullLiquidation
)
private
view
returns (uint256 cappedColPortion, uint256 collSurplus, uint256 debtToRedistribute)
{
// Calculate liquidation incentive for liquidator:
// If ICR is less than 103%: give away 103% worth of collateral to liquidator, i.e., repaidDebt * 103% / price
// If ICR is more than 103%: give away min(ICR, 110%) worth of collateral to liquidator, i.e., repaidDebt * min(ICR, 110%) / price
uint256 _totalColToSend
) private view returns (uint256 toLiquidator, uint256 collSurplus, uint256 debtToRedistribute) {
uint256 _incentiveColl;

// CLAMP
if (_ICR > LICR) {
// Cap at 10%
_incentiveColl = (_totalDebtToBurn * (_ICR > MCR ? MCR : _ICR)) / _price;
} else {
if (_fullLiquidation) {
// for full liquidation, there would be some bad debt to redistribute
_incentiveColl = collateral.getPooledEthByShares(_totalColToSend);
uint256 _debtToRepay = (_incentiveColl * _price) / LICR;
debtToRedistribute = _debtToRepay < _totalDebtToBurn
? _totalDebtToBurn - _debtToRepay
: 0;
// now CDP owner should have zero surplus to claim
cappedColPortion = _totalColToSend;
} else {
// for partial liquidation, new ICR would deteriorate
// since we give more incentive (103%) than current _ICR allowed
_incentiveColl = (_totalDebtToBurn * LICR) / _price;
}
// Min 103%
_incentiveColl = (_totalDebtToBurn * LICR) / _price;
}
if (cappedColPortion == 0) {
cappedColPortion = collateral.getSharesByPooledEth(_incentiveColl);

toLiquidator = collateral.getSharesByPooledEth(_incentiveColl);

/// @audit MUST be like so, else we have debt redistribution, which we assume cannot happen in partial
assert(toLiquidator < _totalColToSend); // Assert is correct here for Echidna

/// Because of above we can subtract
collSurplus = _totalColToSend - toLiquidator; // Can use unchecked but w/e
}

function _calculateFullLiquidationSurplusAndCap(
uint256 _ICR,
uint256 _price,
uint256 _totalDebtToBurn,
uint256 _totalColToSend
) private view returns (uint256 toLiquidator, uint256 collSurplus, uint256 debtToRedistribute) {
uint256 _incentiveColl;

if (_ICR > LICR) {
_incentiveColl = (_totalDebtToBurn * (_ICR > MCR ? MCR : _ICR)) / _price;

// Convert back to shares
toLiquidator = collateral.getSharesByPooledEth(_incentiveColl);
} else {
// for full liquidation, there would be some bad debt to redistribute
_incentiveColl = collateral.getPooledEthByShares(_totalColToSend);

// Since it's full and there's bad debt we use spot conversion to
// Determine the amount of debt that willl be repaid after adding the LICR discount
// Basically this is buying underwater Coll
// By repaying debt at 3% discount
// Can there be a rounding error where the _debtToRepay > debtToBurn?
uint256 _debtToRepay = (_incentiveColl * _price) / LICR;

debtToRedistribute = _debtToRepay < _totalDebtToBurn
? _totalDebtToBurn - _debtToRepay // Bad Debt (to be redistributed) is (CdpDebt - Repaid)
: 0; // Else 0 (note we may underpay per the comment above, althought that may be imaginary)

// now CDP owner should have zero surplus to claim
toLiquidator = _totalColToSend;
}
cappedColPortion = cappedColPortion < _totalColToSend ? cappedColPortion : _totalColToSend;
collSurplus = (cappedColPortion == _totalColToSend) ? 0 : _totalColToSend - cappedColPortion;

toLiquidator = toLiquidator < _totalColToSend ? toLiquidator : _totalColToSend;
collSurplus = (toLiquidator == _totalColToSend) ? 0 : _totalColToSend - toLiquidator;
}

// --- Batch liquidation functions ---
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/specs/PROPERTIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ List of properties of the eBTC protocol, following the categorization by [Certor
| L-14 | If the RM grace period is set and we're in recovery mode, new actions that keep the system in recovery mode should not change the cooldown timestamp | High Level | ✅ |
| L-15 | The RM grace period should set if a BO/liquidation/redistribution makes the TCR above CCR | High Level | ✅ |
| L-16 | The RM grace period should reset if a BO/liquidation/redistribution makes the TCR below CCR | High Level | ✅ |
| L-17 |Partial Liquidations Cannot Close CDPs | High Level | ✅ |

## Fees

Expand Down
3 changes: 1 addition & 2 deletions packages/contracts/test/CdpManager_SimpleLiquidation_Test.js
Original file line number Diff line number Diff line change
Expand Up @@ -680,9 +680,8 @@ contract('CdpManager - Simple Liquidation with external liquidators', async acco
const tx = await cdpManager.partiallyLiquidate(_aliceCdpId, _partialAmounts[i], _aliceCdpId, _aliceCdpId, {from: bob})
_partialLiquidationTxs.push(tx);
}else{
// pass 0 or a number bigger than (leftColl*price/LICR) for partialLiquidate equals to full liquidation
let _leftColl = (await cdpManager.getDebtAndCollShares(_aliceCdpId))[1]
const finalTx = await cdpManager.partiallyLiquidate(_aliceCdpId, (_leftColl.add(liqStipend).mul(toBN(_newPrice)).div(LICR)), _aliceCdpId, _aliceCdpId, {from: bob})
const finalTx = await cdpManager.liquidate(_aliceCdpId, {from: bob})
_partialLiquidationTxs.push(finalTx);
}
}
Expand Down
Loading