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

Findings: 2

Award: $6,406.81

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0xRobocop

Also found by: Weed0607, ether_sky

Labels

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

Awards

4217.1407 USDC - $4,217.14

External Links

Lines of code

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/CdpManager.sol#L320

Vulnerability details

Impact

Most actions that modify the (ICR) of a Collateralized Debt Position (CDP) must adhere to specific regulations to maintain the system's stability. For instance, closing a CDP or decreasing its collateral is not permitted if it would trigger the system's recovery mode, or if the system is already in that mode.

However, the rules for redemptions are less stringent. The only conditions are that both the system and the CDPs undergoing redemption must have an TCR and ICR above the MCR. So, it is possible to redeem a cdp that is actually helping the system to stay healthy.

Proof of Concept

Follow the next steps to run the coded proof of concept:

  • Copy and paste the following test under test/CdpManagerTest.js:
it.only("redeemCollateral(): Is inconsistent with other cdp behaviors", async () => { await contracts.collateral.deposit({from: A, value: dec(1000, 'ether')}); await contracts.collateral.approve(borrowerOperations.address, mv._1Be18BN, {from: A}); await contracts.collateral.deposit({from: B, value: dec(1000, 'ether')}); await contracts.collateral.approve(borrowerOperations.address, mv._1Be18BN, {from: B}); await contracts.collateral.deposit({from: C, value: dec(1000, 'ether')}); await contracts.collateral.approve(borrowerOperations.address, mv._1Be18BN, {from: C}); // @audit-info Current BTC / ETH price is 1. const newPrice = dec(1, 18); await priceFeed.setPrice(newPrice) // @audit-info Open some CDPs. await borrowerOperations.openCdp(dec(400, 18), A, A, dec(1000, 'ether'), { from: A }) await borrowerOperations.openCdp(dec(75, 18), B, B, dec(100, 'ether'), { from: B }) await borrowerOperations.openCdp(dec(75, 18), C, C, dec(100, 'ether'), { from: C }) let _aCdpId = await sortedCdps.cdpOfOwnerByIndex(A, 0); let _bCdpId = await sortedCdps.cdpOfOwnerByIndex(B, 0); let _cCdpId = await sortedCdps.cdpOfOwnerByIndex(C, 0); const price = await priceFeed.getPrice() const currentTCR = await cdpManager.getSyncedTCR(price); console.log("TCR of the system before price reduction: " + currentTCR.toString()) // @audit-info Some price reduction. const newPrice2 = dec(6, 17); await priceFeed.setPrice(newPrice2) const price2 = await priceFeed.getPrice() const currentTCR2 = await cdpManager.getSyncedTCR(price2); console.log("TCR of the system after price reduction" + currentTCR2.toString()) // @audit-info All cdps are in the linked list. assert.isTrue(await sortedCdps.contains(_aCdpId)) assert.isTrue(await sortedCdps.contains(_bCdpId)) assert.isTrue(await sortedCdps.contains(_cCdpId)) await cdpManager.setBaseRate(0) // @audit-info Redemption of 400 eBTC. const EBTCRedemption = dec(400, 18) await th.redeemCollateralAndGetTxObject(A, contracts, EBTCRedemption, GAS_PRICE, th._100pct) // @audit-info Redemption will happen to A's position because B and C ICRs are below MCR. assert.isFalse(await sortedCdps.contains(_aCdpId)) assert.isTrue(await sortedCdps.contains(_bCdpId)) assert.isTrue(await sortedCdps.contains(_cCdpId)) // @audit-issue UNDERCOLLATERALIZED const currentTCR3 = await cdpManager.getSyncedTCR(price2); console.log("TCR after redemption: " + currentTCR3.toString()) })

This proof of concept highlights a significant risk: a situation where the entire system becomes undercollateralized. Although the likelihood of this occurring is low, there are other, more probable scenarios to consider. One such scenario involves executing one or multiple redemptions that push the system into recovery mode.

Currently, there's a redemption fee in place that escalates the cost of withdrawing large amounts of collateral, serving as a financial deterrent to potential manipulation. However, this is primarily an economic obstacle. Given that eBTC aims to be a fundamental component for Bitcoin in the Ethereum DeFi ecosystem, it's crucial to ensure that redemptions do not jeopardize the system's stability.

It should also be noted that the RiskDao's analysis regarding redemptions does not extend to situations where redemptions are strategically used to affect the system's collateralization.

Tools Used

Manual Review

Redemptions should have the same rules for other cdp changes. For example:

  • Redeeming should not let the system in RM
  • Redeeming should not be possible if the system is in RM unless it makes the system to get out of RM

Assessed type

Other

#0 - c4-pre-sort

2023-11-16T07:57:02Z

bytes032 marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-11-17T06:35:06Z

bytes032 marked the issue as primary issue

#2 - GalloDaSballo

2023-11-20T09:29:30Z

This finding doesn't account for many scenarios in which you could have RM and a downward depeg

While liquidations would be preferred in such cases, redemptions are completely fine and they typically do raise the TCR as well

#3 - c4-sponsor

2023-11-20T09:29:34Z

GalloDaSballo (sponsor) disputed

#4 - GalloDaSballo

2023-11-20T09:29:42Z

Interested if you want to send more math

#5 - jhsagd76

2023-11-26T01:06:15Z

It's like a whale sniping when cold start. As I explained in the #180 , if attacker loss, the system will benefit.

I think make sure the redeem should NOT toggle the system from NM to RM is a good idea. So I mark these issues as QA now.

Expecting someone to provide a poc test that would allow the attacker to gain more from liquidation than the loss from redemption, I would elevate these issues to med.

#6 - c4-judge

2023-11-26T01:06:29Z

jhsagd76 changed the severity to QA (Quality Assurance)

#7 - c4-judge

2023-11-27T10:23:55Z

jhsagd76 marked the issue as grade-b

#8 - c4-judge

2023-11-27T11:16:05Z

jhsagd76 removed the grade

#9 - jhsagd76

2023-11-27T11:18:29Z

I think that prohibiting the artificial switching from NM to RM could be an important invariant. I plan to elevate this report to a medium and ask for the sponsor's opinion. @GalloDaSballo

#10 - GalloDaSballo

2023-11-27T12:34:34Z

Are you saying that you can trigger RM via redemptions?

#11 - rayeaster

2023-11-28T02:30:55Z

The POC test provided in the original report https://github.com/code-423n4/2023-10-badger-findings/issues/199#issue-1991846604 is a very special case where only one CDP could be redeemed (ICR > MCR) while other two CDPs are all under-water (should be susceptible to liquidation already).

First of all, if system is already in RM before redemption, then no redemption is allowed as @jhsagd76 pointed out https://github.com/code-423n4/2023-10-badger-findings/issues/199#issuecomment-1826457826

Secondly, redemption only apply to those CDPs with ICR > MCR by design. This means if all CDPs eligible to redemption are redeemed (closed), then rest are below MCR (below CCR as well). But in practice, I doubt this could really happen due to the economical consideration for redemption.

#12 - rayeaster

2023-11-28T02:38:09Z

I think that prohibiting the artificial switching from NM to RM could be an important invariant. I plan to elevate this report to a medium and ask for the sponsor's opinion. @GalloDaSballo

Maybe a final check that if system changes into RM, then revert the entire redemption tx totally

#13 - jhsagd76

2023-11-28T06:29:25Z

The order of CDP redemption has implicitly ensured the invariants. Close this series of issues as invalid

#14 - c4-judge

2023-11-28T06:29:54Z

jhsagd76 marked the issue as grade-c

#15 - GalloDaSballo

2023-11-28T17:43:57Z

We are still looking into this issue

#16 - GalloDaSballo

2023-12-06T10:29:59Z

I believe the finding to be valid, under the specific circumnstance of:

  • CDPs with Bad Debt
  • No CDPs with ICR > MCR and ICR < CCR

A redemption can cause TCR to decrease and trigger recovery mode

This is extremely unlikely as it's irrational for people to redeem instead of liquidate, however it could be used to trigger Recovery Mode in unintended way

Additionally, the issue is not fixable as bad debt redistributions could be so big as to trigger RM separately and that's not avoidable

I appreciate the time taken by the Warden and believe this will have to be sorted by reserving some amount of eBTC for Bad Debt Liquidations as means to "clean up" those risky CDPs, I believe this can be done profitable for the DAO in place of redemptions and as such believe we wil:

  • Add the check to prevent directly triggering RM
  • Assume it will still be possible to trigger RM directly
  • Monitor this situation
  • Have some eBTC to perform said liquidations to avoid this scenario under most reasonable conditions

#17 - c4-sponsor

2023-12-06T10:30:08Z

GalloDaSballo (sponsor) confirmed

#18 - CodingNameKiki

2023-12-06T12:40:10Z

Agree with Alex and we were able to prove this issue based on the below statements, so it is possible to trigger RM if we have the correct cdp and system values when redemption happens. POC

  • CDPs with Bad Debt
  • No CDPs with ICR > MCR and ICR < CCR

#19 - c4-judge

2023-12-07T02:34:18Z

This previously downgraded issue has been upgraded by jhsagd76

#20 - c4-judge

2023-12-07T02:35:00Z

jhsagd76 marked issue #268 as primary and marked this issue as a duplicate of 268

#21 - c4-judge

2023-12-07T02:35:55Z

jhsagd76 marked the issue as satisfactory

#22 - c4-judge

2023-12-07T02:43:11Z

jhsagd76 marked the issue as selected for report

Findings Information

🌟 Selected for report: nobody2018

Also found by: 0xRobocop, BARW, Stormy

Labels

bug
2 (Med Risk)
high quality report
satisfactory
duplicate-152

Awards

2189.6692 USDC - $2,189.67

External Links

Lines of code

https://github.com/code-423n4/2023-10-badger/blob/f2f2e2cf9965a1020661d179af46cb49e993cb7e/packages/contracts/contracts/LeverageMacroBase.sol#L173

Vulnerability details

Impact

The doOperation function in the LeverageMacroBase allows to make post conditions checks depending on the operations that was carried out. One of these checks can be done after opening a cdp, lets check how this is carried out.


// ...

uint256 initialCdpIndex;
if (postCheckType == PostOperationCheck.openCdp) {
   initialCdpIndex = sortedCdps.cdpCountOf(address(this));
}

// ...

if (postCheckType == PostOperationCheck.openCdp) {
   bytes32 cdpId = sortedCdps.cdpOfOwnerByIndex(address(this), initialCdpIndex);

   // Check for param details
   ICdpManagerData.Cdp memory cdpInfo = cdpManager.Cdps(cdpId);
   _doCheckValueType(checkParams.expectedDebt, cdpInfo.debt);
   _doCheckValueType(checkParams.expectedCollateral, cdpInfo.coll);
   require(
       cdpInfo.status == checkParams.expectedStatus,
       "!LeverageMacroReference: openCDP status check"
   );
}

First of all is using the functions sortedCdps.cdpCountOf and sortedCdps.cdpOfOwnerByIndex that are functions meant to be used off-chain, since they are O(n).

The main issue is not that,but a logic flaw. First, we see initialCdpIndex = sortedCdps.cdpCountOf(address(this));. Hence, initialCdpIndex is the amount of cdps the owner has. Later on the post check we see:

bytes32 cdpId = sortedCdps.cdpOfOwnerByIndex(address(this), initialCdpIndex);

Where initialCdpIndex is used to get the cdpId. Hence, the code assumes that the returned cdpId is the one that was opened previously. This assumption is incorrect, cdpOfOwnerByIndex starts at the tail of the linked list and checks if the current cdp is of the owner, if it is and the index match initialCdpIndex then it returns it, if not, increments index and keep searching.

Given the above, we know that bytes32 cdpId = sortedCdps.cdpOfOwnerByIndex(address(this), initialCdpIndex); will return the last cdpId of the owner in the linked list, which is not necessarily the cdp that was recently opened. Hence the following checks will be meaning less:

// Check for param details
ICdpManagerData.Cdp memory cdpInfo = cdpManager.Cdps(cdpId);
_doCheckValueType(checkParams.expectedDebt, cdpInfo.debt);
_doCheckValueType(checkParams.expectedCollateral, cdpInfo.coll);
require(
   cdpInfo.status == checkParams.expectedStatus,
   "!LeverageMacroReference: openCDP status check"
);

Proof of Concept

The following test can be added to test/CdpManagerTest.js:

it.only("sortedCdps.cdpOfOwnerByIndex(): Post Check is Incorrect", async () => {
    await contracts.collateral.deposit({from: A, value: dec(1000, 'ether')});
    await contracts.collateral.approve(borrowerOperations.address, mv._1Be18BN, {from: A});

    // @audit-info Current BTC / ETH price is 1.
    const newPrice = dec(1, 18);
    await priceFeed.setPrice(newPrice)

    // @audit-info Open a cdp
    await borrowerOperations.openCdp(dec(50, 18), A, A, dec(100, 'ether'), { from: A })

    let initialCdpIndex = await sortedCdps.cdpCountOf(A);

    // @audit-info Open a cdp with a smaller ICR than the first one.
    let secondCdpId = await borrowerOperations.openCdp(dec(70, 18), A, A, dec(100, 'ether'), { from: A })

    let assumedToBe_secondCdpId = await sortedCdps.cdpOfOwnerByIndex(A, toBN(initialCdpIndex.toString()));

    // @audit-info They are not equal
    expect(secondCdpId).to.not.be.equal(assumedToBe_secondCdpId);
  })

Tools Used

Manual review

openCdp returns the cdpId. This value should be used for the post-check.

Assessed type

Other

#0 - c4-pre-sort

2023-11-16T06:43:28Z

bytes032 marked the issue as high quality report

#1 - c4-pre-sort

2023-11-16T06:44:14Z

bytes032 marked the issue as duplicate of #152

#2 - c4-judge

2023-11-27T09:29:13Z

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