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: 6/52
Findings: 2
Award: $6,406.81
🌟 Selected for report: 1
🚀 Solo Findings: 0
4217.1407 USDC - $4,217.14
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.
Follow the next steps to run the coded proof of concept:
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.
Manual Review
Redemptions should have the same rules for other cdp changes. For example:
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:
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:
#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
#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
🌟 Selected for report: nobody2018
2189.6692 USDC - $2,189.67
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" );
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); })
Manual review
openCdp
returns the cdpId
. This value should be used for the post-check.
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