Platform: Code4rena
Start Date: 24/10/2023
Pot Size: $149,725 USDC
Total HM: 7
Participants: 52
Period: 21 days
Judge: ronnyx2017
Total Solo HM: 2
Id: 300
League: ETH
Rank: 8/52
Findings: 1
Award: $4,217.14
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: rvierdiiev
4217.1407 USDC - $4,217.14
LiquidationLibrary.batchLiquidateCdps is called from CDPManager
in order to liquidate list of cdps.
Depending if system is currently in recovery mode liquidations will be done inside _getTotalFromBatchLiquidate_RecoveryMode
or _getTotalsFromBatchLiquidate_NormalMode
function.
After that _finalizeLiquidation
function will be called, which will then do several important system updates, token transfer and what is important for this report bad debt redistribution. In case if new bad debt occurs in the system, then this debt amount is redistributed to all stakers that are still in the system.
Now, lets check how liquidations occur by _getTotalsFromBatchLiquidate_NormalMode
function.
This function loops through all cpds one by one. It fetches ICR
for them using getSyncedICR
function. This function will calculate ICR
not on stored debt
and coll
of cdp, but it will update this values, according to new fee rate(which means that coll
will be reduced) and redistribute rate(which means that debt will increase, if rate is bigger than stored for cdp).
This is how it will be done for redistribution. https://github.com/code-423n4/2023-10-badger/blob/main/packages/contracts/contracts/CdpManagerStorage.sol#L864-L868
function getSyncedICR(bytes32 _cdpId, uint256 _price) public view returns (uint256) { uint256 _debt = getSyncedCdpDebt(_cdpId); uint256 _collShare = getSyncedCdpCollShares(_cdpId); return _calculateCR(_collShare, _debt, _price); }
Then getSyncedCdpDebt
is called, which calls _getSyncedCdpDebtAndRedistribution
function.
https://github.com/code-423n4/2023-10-badger/blob/main/packages/contracts/contracts/CdpManagerStorage.sol#L822-L833
function _getSyncedCdpDebtAndRedistribution( bytes32 _cdpId ) internal view returns (uint256, uint256, uint256) { (uint256 pendingDebtRedistributed, uint256 _debtIndexDelta) = _getPendingRedistributedDebt( _cdpId ); uint256 _newDebt = Cdps[_cdpId].debt; if (pendingDebtRedistributed > 0) { _newDebt = _newDebt + pendingDebtRedistributed; } return (_newDebt, pendingDebtRedistributed, _debtIndexDelta); }
So debt of cdp will be increased pendingDebtRedistributed
, which depends on redistribution rate of position and current redistribution rate.
Later, for each cdp _getLiquidationValuesNormalMode
function will be called, which will actually do the liquidation. It will call _liquidateIndividualCdpSetupCDPInNormalMode
function. Inside of it _calculateFullLiquidationSurplusAndCap
function is called and it's purpose is to calculate shares that liquidator and cdp owner should receive and bad debt to redistribute.
So after liquidation has finished, then _addLiquidationValuesToTotals
function is called, which will add values from previous liquidation(including redistribution debt) to the total accumulator.
So finally, i can explain the problem. In case if in batch there is cdp, that will create bad debt after liquidation, then this debt will not be redistributed to all next liquidations, as redistribution index is not updated after each liquidation. And as result, next liquidations will have incorrect debt
value, which means that smaller amount of debt will be liquidated and bigger amount of coll
will be received by cdp owner. And also all else users in the system will have to cover that delta debt that was not redistributed to next liquidations.
Bad debt is distributed incorrectly.
VsCode
You need to redistribute bad debt after each liquidation in the batch(in case if bad debt occured).
Error
#0 - c4-pre-sort
2023-11-15T15:56:02Z
bytes032 marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-15T16:33:53Z
bytes032 marked the issue as primary issue
#2 - GalloDaSballo
2023-11-20T09:21:01Z
This is a known finding from Cantina that unfortunately was not added to the report
The finding specifically means that 3% of bad debt generated in a batch liquidation will be skipped, effectively making liquidations more favourable
That said, it's worth noting that since partial liquidations don't cause a redistribution, the 3% bad debt could have been skipped under other circumnstances
https://github.com/GalloDaSballo/Cdp-Demo/blob/main/scripts/insolvency_cascade.py
MAX_BPS = 10_000 def cr(debt, coll): return coll / debt * 100 def getDebtByCr(coll, cr_bps): return coll / cr_bps * MAX_BPS LICR = 103_00 def max(a, b): if(a > b): return a return b def min(a, b): if(a > b): return b return a def full_liq(COLL): ## burn all return [COLL, COLL / LICR * MAX_BPS] def partial_liq(COLL, percent): return [COLL * percent / 100, COLL * percent / 100 / LICR * MAX_BPS] def loop_full(cr_bps): COLL = 100 DEBT = getDebtByCr(COLL, cr_bps) print("DEBT", DEBT) print("cr", cr(DEBT, COLL)) ## Liquidate Partially based on premium while(COLL > 2): [sub_coll, sub_debt] = partial_liq(COLL, 10) # [sub_coll, sub_debt] = full_liq(COLL) print("DEBT", DEBT) print("COLL", COLL) DEBT -= sub_debt COLL -= sub_coll print("DEBT", DEBT) CRS = [ 100_00, 90_00, 80_00, 70_00 ] def main(): print("Simulate Total Liquidation with Bad debt") print("Coll is always 100") print("No price means 1 coll = 1 debt for price") for cr in CRS: print("") print("") print("") loop_full(cr) main()
#3 - c4-sponsor
2023-11-20T09:21:06Z
GalloDaSballo (sponsor) acknowledged
#4 - jhsagd76
2023-11-27T09:00:37Z
I didn't find a public record about the issue. So I mark it as a satisfy med
#5 - c4-judge
2023-11-27T09:00:52Z
jhsagd76 marked the issue as satisfactory
#6 - c4-judge
2023-11-28T03:24:48Z
jhsagd76 marked the issue as selected for report