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: 9/52
Findings: 1
Award: $3,243.95
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rvierdiiev
3243.9544 USDC - $3,243.95
Borrowers may end up with more bad debt than they should get.
When the liquidator uses the batchLiquidateCdps() function, the systemDebtRedistributionIndex is updated at the end of the function. As a result, bad debt redistribution does not occur within the function, which can lead to more bad debt for other borrowers.
Liquidation values were added to totals and then used in the _finalizeLiquidation() function.
for (vars.i = _start; ; ) { vars.cdpId = _cdpArray[vars.i]; // only for active cdps if (vars.cdpId != bytes32(0) && Cdps[vars.cdpId].status == Status.active) { vars.ICR = getSyncedICR(vars.cdpId, _price); if (_checkICRAgainstMCR(vars.ICR)) { _syncAccounting(vars.cdpId); _getLiquidationValuesNormalMode(_price, _TCR, vars, singleLiquidation); // Add liquidation values to their respective running totals totals = _addLiquidationValuesToTotals(totals, singleLiquidation); } }
The _finalizeLiquidation() redistributes bad debt, if any.
if (totalDebtToRedistribute > 0) { _redistributeDebt(totalDebtToRedistribute); }
Due to bad debt being redistributed only at the end of the function instead of after every liquidation, if the first liquidation results in bad debt, subsequent liquidations will occur without it.
POC: GracePeriod.t.sol
function testNyx() public { // Open Safe and RM Risky bytes32[] memory rmLiquidatableCdps = _openCdps(5); // Open Degen bytes32 degen = _openDegen(); // Trigger RM _triggerRMViaPrice(); // Do extra checks for Degen getting liquidated Etc.. uint256 degenSnapshot = _checkLiquidationsForDegen(degen); vm.revertTo(degenSnapshot); bytes32[] memory cdpArray = new bytes32[](2); cdpArray[0] = degen; cdpArray[1] = rmLiquidatableCdps[1]; vm.startPrank(liquidator); cdpManager.batchLiquidateCdps(cdpArray); // Liquidate them "for real" console.log(cdpManager.getCdpDebt(rmLiquidatableCdps[1])); console.log(cdpManager.systemDebtRedistributionIndex()); vm.stopPrank(); }
emit CdpLiquidated(_cdpId: 0x2f66c75a001ba71ccb135934f48d844b46454543000000010000000000000001, _borrower: 0x2f66c75A001Ba71ccb135934F48d844b46454543, _debt: 2000000000000000000 [2e18], _collShares: 30963920301561658559 [3.096e19], _operation: 5, _liquidator: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, _premiumToLiquidator: 1247089092323341082 [1.247e18])
_debt: 2000000000000000000
function testBadDebt1() public { // Open Safe and RM Risky bytes32[] memory rmLiquidatableCdps = _openCdps(5); // Open Degen bytes32 degen = _openDegen(); // Trigger RM _triggerRMViaPrice(); // Do extra checks for Degen getting liquidated Etc.. uint256 degenSnapshot = _checkLiquidationsForDegen(degen); vm.revertTo(degenSnapshot); bytes32[] memory cdpArray = new bytes32[](2); cdpArray[0] = degen; cdpArray[1] = rmLiquidatableCdps[1]; vm.startPrank(liquidator); // Liquidate Degen cdpManager.liquidate(degen); // Liquidate them "for real" console.log(cdpManager.systemDebtRedistributionIndex()); console.log(cdpManager.getCdpDebt(rmLiquidatableCdps[1])); cdpManager.liquidate(rmLiquidatableCdps[1]); console.log(cdpManager.systemDebtRedistributionIndex()); vm.stopPrank(); }
emit Liquidation(_liquidatedDebt: 2000120887296469974 [2e18], _liquidatedColl: 30963920301561658565 [3.096e19], _liqReward: 200000000000000000 [2e17])
_liquidatedDebt: 2000120887296469974
Manual Review
Bad debt needs to be distributed after every liquidation inside the batchLiquidateCdps() function.
Other
#0 - c4-pre-sort
2023-11-15T15:56:13Z
bytes032 marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-15T16:34:09Z
bytes032 marked the issue as duplicate of #36
#2 - c4-judge
2023-11-27T09:28:49Z
jhsagd76 marked the issue as satisfactory