Platform: Code4rena
Start Date: 11/12/2023
Pot Size: $90,500 USDC
Total HM: 29
Participants: 127
Period: 17 days
Judge: TrungOre
Total Solo HM: 4
Id: 310
League: ETH
Rank: 54/127
Findings: 1
Award: $249.22
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: carrotsmuggler
Also found by: 0xPhantom, CaeraDenoir, SECURITISE, Soltho, pavankv, y0ng0p3
249.2197 USDC - $249.22
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/governance/ProfitManager.sol#L332-L333 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/LendingTerm.sol#L357-L358
Users will no be able to borrow.
In the event that bad debts zero out ProfitManager::creditMultiplier
, new loans can no longer be taken thereby putting the burden of repaying all the current outstanding bad debt solely on those who have an open debt position.
This goes in contrast to when ProfitManager::creditMultiplier > 1 wei
as in this case, the bad debt is shared among those who have an open debt position as well as those opening a new debt position (which is not possible when ProfitManager::creditMultiplier = 0
)
If there is bad debt but ProfitManager::creditMultiplier > 1 wei
, then we can still take out new loans (open a loan position) thereby sharing the burden of repaying the bad debt among all those who already have an open loan as well as those opening new loans.
But when ProfitManager::creditMultiplier < 2 wei
, we no longer can open new loan positions and as a consequence, the burden of repaying the bad debt is solely on those who already have an open loan position.
In ProfitManager.sol
, creditMultiplier
is updated as follows :
uint256 newCreditMultiplier = (creditMultiplier * (creditTotalSupply - loss)) / creditTotalSupply;
creditMultiplier
decrease each time a loss occurs. The more this loss is high, the smaller creditMultiplier
will be.
If huge losses occur a sufficient number of times creditMultiplier
is rounded down to 0.
function test_PrecisionLoss_CreditMultiplier() public { // grant roles to test contract vm.startPrank(governor); core.grantRole(CoreRoles.GAUGE_PNL_NOTIFIER, address(this)); core.grantRole(CoreRoles.CREDIT_MINTER, address(this)); vm.stopPrank(); // initial state // 100 CREDIT circulating (assuming backed by >= 100 USD) assertEq(profitManager.creditMultiplier(), 1e18); assertEq(credit.totalSupply(), 100e18); // apply losses for(uint256 i; i < 10; ) { // 99 CREDIT of loans completely default (~99 USD loss) profitManager.notifyPnL(address(this), -99e18); unchecked{ ++i; } } assertEq(profitManager.creditMultiplier(), 0); }
Place the code for the above test function in test/unit/governance/ProfitManager.t.sol
and run the following command in the terminal :
forge test --match-contract ProfitManagerUnit --mt test_PrecisionLoss_CreditMultiplier -vvv
With creditMultiplier
zeroed LendingTerm::borrow()
will always revert, making users unable to borrow tokens. And so LendingTerm::repay()
too, making repayments blocked.
Place the code for the following test function in test/unit/governance/ProfitManager.t.sol
.
function test_BorrowFailed_CreditMultiplierZeroed() public { // prepare // LendingTerm params uint256 _CREDIT_PER_COLLATERAL_TOKEN = 2000e18; // 2000, same decimals uint256 _INTEREST_RATE = 0.10e18; // 10% APR uint256 _MAX_DELAY_BETWEEN_PARTIAL_REPAY = 63115200; // 2 years uint256 _MIN_PARTIAL_REPAY_PERCENT = 0.2e18; // 20% uint256 _HARDCAP = 20_000_000e18; RateLimitedMinter rlcm = new RateLimitedMinter( address(core) /*_core*/, address(credit) /*_token*/, CoreRoles.RATE_LIMITED_CREDIT_MINTER /*_role*/, type(uint256).max /*_maxRateLimitPerSecond*/, type(uint128).max /*_rateLimitPerSecond*/, type(uint128).max /*_bufferCap*/ ); AuctionHouse auctionHouse = new AuctionHouse(address(core), 650, 1800); LendingTerm term = LendingTerm(Clones.clone(address(new LendingTerm()))); term.initialize( address(core), LendingTerm.LendingTermReferences({ profitManager: address(profitManager), guildToken: address(guild), auctionHouse: address(auctionHouse), creditMinter: address(rlcm), creditToken: address(credit) }), LendingTerm.LendingTermParams({ collateralToken: address(pegToken), maxDebtPerCollateralToken: _CREDIT_PER_COLLATERAL_TOKEN, interestRate: _INTEREST_RATE, maxDelayBetweenPartialRepay: _MAX_DELAY_BETWEEN_PARTIAL_REPAY, minPartialRepayPercent: _MIN_PARTIAL_REPAY_PERCENT, openingFee: 0, hardCap: _HARDCAP }) ); uint256 borrowAmount = 20_000e18; uint256 collateralAmount = 12e18; pegToken.mint(address(this), collateralAmount); pegToken.approve(address(term), collateralAmount); // grant roles to test contract vm.startPrank(governor); core.grantRole(CoreRoles.GAUGE_PNL_NOTIFIER, address(this)); core.grantRole(CoreRoles.CREDIT_MINTER, address(this)); vm.stopPrank(); // initial state // 100 CREDIT circulating (assuming backed by >= 100 USD) assertEq(profitManager.creditMultiplier(), 1e18); assertEq(credit.totalSupply(), 100e18); // apply losses for(uint256 i; i < 10; ) { // 99 CREDIT of loans completely default (~99 USD loss) profitManager.notifyPnL(address(this), -99e18); unchecked{ ++i; } } assertEq(profitManager.creditMultiplier(), 0); vm.expectRevert(); // borrow bytes32 loanId = term.borrow(borrowAmount, collateralAmount); }
In terminal, run the following command:
forge test --match-contract ProfitManagerUnit --mt test_BorrowFailed_CreditMultiplierZeroed -vvv
Manual review
A naive recommendation will be to never let ProfitManager::creditMultiplier < 2
Math
#0 - c4-pre-sort
2024-01-05T12:54:47Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-05T12:55:07Z
0xSorryNotSorry marked the issue as duplicate of #1166
#2 - c4-judge
2024-01-28T23:18:20Z
Trumpero marked the issue as satisfactory