Badger eBTC Audit + Certora Formal Verification Competition - rvierdiiev's results

Use stETH to borrow Bitcoin with 0% fees | The only smart contract based #BTC.

General Information

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

eBTC Protocol

Findings Distribution

Researcher Performance

Rank: 8/52

Findings: 1

Award: $4,217.14

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: rvierdiiev

Also found by: Nyx, Stormy

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor acknowledged
sufficient quality report
M-06

Awards

4217.1407 USDC - $4,217.14

External Links

Lines of code

https://github.com/code-423n4/2023-10-badger/blob/main/packages/contracts/contracts/LiquidationLibrary.sol#L679-L727

Vulnerability details

Proof of Concept

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.

Impact

Bad debt is distributed incorrectly.

Tools Used

VsCode

You need to redistribute bad debt after each liquidation in the batch(in case if bad debt occured).

Assessed type

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter