Ethereum Credit Guild - y0ng0p3's results

A trust minimized pooled lending protocol.

General Information

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

Ethereum Credit Guild

Findings Distribution

Researcher Performance

Rank: 54/127

Findings: 1

Award: $249.22

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: carrotsmuggler

Also found by: 0xPhantom, CaeraDenoir, SECURITISE, Soltho, pavankv, y0ng0p3

Labels

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

Awards

249.2197 USDC - $249.22

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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

Tools Used

Manual review

A naive recommendation will be to never let ProfitManager::creditMultiplier < 2

Assessed type

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

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