Badger eBTC Audit + Certora Formal Verification Competition - Nyx'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: 9/52

Findings: 1

Award: $3,243.95

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: rvierdiiev

Also found by: Nyx, Stormy

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
duplicate-36

Awards

3243.9544 USDC - $3,243.95

External Links

Lines of code

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

Vulnerability details

Impact

Borrowers may end up with more bad debt than they should get.

Proof of Concept

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

Tools Used

Manual Review

Bad debt needs to be distributed after every liquidation inside the batchLiquidateCdps() function.

Assessed type

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

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