28791 - [SC - Low] The system protects from any rounding issues wh...
Submitted on Feb 27th 2024 at 14:19:55 UTC by @Stormy for Boost | eBTC
Report ID: #28791
Report type: Smart Contract
Report severity: Low
Target: https://github.com/ebtc-protocol/ebtc/blob/release-0.7/packages/contracts/contracts/LiquidationLibrary.sol
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The system may issue surplus in recovery mode when liquidating cdps with ICR < MCR duo to rounding error.
Vulnerability Details
On short explanation the liquidating system in eBTC works on the following bases.
The system is in normal mode as a result any cdps with ICR < MCR can be liquidated.
The system is in recovery mode duo to that cdps with ICR < CCR can be liquidated.
Normal mode liquidation
If we look at the function _liquidateIndividualCdpSetupCDPInNormalMode which liquidates a cdp position with collateral ratio below the minimum one, we can see that the system doesn't allow any spare collateral to be send to the cdp owner when accounting the surplus of the liquidation.
This is made in case any rounding errors occur when calculating the incentive collateral, as based on the system rules when liquidation happens with a cdp's ICR < MCR the whole cdp collateral should be send to the liquidator.
if (_collSurplus > 0) {
// due to division precision loss, should be zero surplus in normal mode
_cappedColPortion = _cappedColPortion + _collSurplus;
_collSurplus = 0;
}
Recovery mode liquidation
The system enters recovery mode as a defensive mode to increase the total collateral ratio of the system, as a result liquidators can further liquidate positions with ICR above the MCR and below the CCR one.
Lets say we liquidate cdp position with ICR == 120%, the liquidator gets a maximum incentive of 110% and the rest is returned to the cdp owner via surplus.
However in recovery mode we are still free to liquidate cdps below the minimum collateral ratio which can still lead to the rounding error when calculating the surplus. In this case the liquidator will get less incentive collateral while the cdp owner will earn extra surplus which shouldn't be possible when liquidating cdps with ICR < MCR.
if (_collSurplus > 0) {
collSurplusPool.increaseSurplusCollShares(_borrower, _collSurplus);
_recoveryState.totalSurplusCollShares =
_recoveryState.totalSurplusCollShares +
_collSurplus;
}
Impact Details
l would say the loss here is not significant but rather broken invariant, the system enforces a rule that there should not be any surplus in normal mode which is true as when liquidating cdps with ICR < MCR the whole incentive collateral is supposed to be send to the liquidator. However this invariant doesn't hold in recovery mode, but theoretically it should be the same as the system is allowed to liquidate cdps below the minimum collateral ratio which shouldn't issue any surplus.
References
https://github.com/ebtc-protocol/ebtc/blob/release-0.7/packages/contracts/contracts/LiquidationLibrary.sol#L336
Proof of concept
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;
import "forge-std/Test.sol";
import {eBTCBaseInvariants} from "./BaseInvariants.sol";
contract stormy is eBTCBaseInvariants {
address wallet = address(0xbad455);
uint256 shareEth = 1158379174506084879;
bytes32 underwater;
function setUp() public override {
super.setUp();
connectCoreContracts();
connectLQTYContractsToCore();
}
function testSurplusInRMWhenICRBelowMCR() public {
// set eth per stETH share
collateral.setEthPerShare(shareEth);
// fetch price before open
uint256 oldprice = priceFeedMock.fetchPrice();
// open five cdps
_openTestCDP(wallet, 2e18 + 2e17, ((2e18 * oldprice) / 240e16));
_openTestCDP(wallet, 2e18 + 2e17, ((2e18 * oldprice) / 240e16));
_openTestCDP(wallet, 2e18 + 2e17, ((2e18 * oldprice) / 240e16));
_openTestCDP(wallet, 2e18 + 2e17, ((2e18 * oldprice) / 240e16));
underwater = _openTestCDP(wallet, 2e18 + 2e17, ((2e18 * oldprice) / 210e16));
// reduce the price by half to make underwater cdp
priceFeedMock.setPrice(oldprice / 2);
// fetch new price after reduce
uint256 newPrice = priceFeedMock.fetchPrice();
// ensure the system is in recovery mode
assert(cdpManager.getSyncedTCR(newPrice) < CCR);
// liquidate underwater cdp with ICR < MCR
vm.startPrank(wallet);
cdpManager.liquidate(underwater);
vm.stopPrank();
// make sure the cdp is no longer in the sorted list
assert(!sortedCdps.contains(underwater));
// fetch the surplus after the liquidation
uint256 surplus = collSurplusPool.getSurplusCollShares(wallet);
// ensure that the surplus is non-zero
assert(surplus != 0);
// console log the surplus coll
console.log("Surplus:", surplus);
}
}
Last updated
Was this helpful?