Ethereum Credit Guild - Inference'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: 47/127

Findings: 2

Award: $289.20

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: JCN

Also found by: 0xStalin, 3docSec, AlexCzm, Chinmay, Cosine, Inference, JCN, Soul22, almurhasan, c47ch3m4ll, grearlake, xeros

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
upgraded by judge
duplicate-476

Awards

286.1479 USDC - $286.15

External Links

Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/LendingTerm.sol#L766

Vulnerability details

Impact

The protocol may incur bad debt than necessary, since it may not always be feasible to achieve the optimal arrangement for ECG, which involves repaying as much of the entire called loan as possible, due to a failing contract in certain conditions.

Proof of Concept

Called loans in the LendingTerm contract undergo auctioning in the AuctionHouse contract. The outstanding debt is calculated prior to submission to the AuctionHouse.

Upon completion of a bid in an auction, the auction concludes. If the bid occurs within the auction period where the requested "callDept" remains constant but the offered collateral increases, the bid may not be settled if there have been additional badDept events since the start of the auction. This is due to the recalculation of the principal in the onBid function, leading to the contract failure in line 766, as the collateralToBorrower would not be zero in this scenario.

The loanDebt calculated in the getLoanDebt function when starting an auction is determined by:

uint256 borrowAmount = loan.borrowAmount; uint256 interest = (borrowAmount * params.interestRate * (block.timestamp - borrowTime)) / YEAR / 1e18; uint256 loanDebt = borrowAmount + interest; uint256 _openingFee = params.openingFee; if (_openingFee != 0) { loanDebt += (borrowAmount * _openingFee) / 1e18; } uint256 creditMultiplier = ProfitManager(refs.profitManager) .creditMultiplier(); loanDebt = (loanDebt * loan.borrowCreditMultiplier) / creditMultiplier;

The principal in onBid is calculated as:

uint256 creditMultiplier = ProfitManager(refs.profitManager) .creditMultiplier(); uint256 borrowAmount = loans[loanId].borrowAmount; uint256 principal = (borrowAmount * loans[loanId].borrowCreditMultiplier) / creditMultiplier;

Therefore, the case 'creditFromBidder < principal' in line 761 occurs when the "creditMultiplier," responsible for accounting for bad debt, sets off the interests and openingFee. This happens when the creditMultiplier decreases by the formula (InterestFee * creditMultiplier_at_auction_start) / (borrowAmount + InterestFee), where InterestFee is calculated as (borrowAmount * _openFee + interest).

Tools Used

None. Manual review

Consider reflecting such a case where 'creditFromBidder < principal' by setting collateralToBorrower value to zero instead failing, if the value is not zero.

Assessed type

Other

#0 - c4-pre-sort

2024-01-05T16:45:06Z

0xSorryNotSorry marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-05T16:45:21Z

0xSorryNotSorry marked the issue as duplicate of #1069

#2 - c4-judge

2024-01-29T19:47:46Z

Trumpero marked the issue as satisfactory

#3 - c4-judge

2024-01-31T13:44:24Z

Trumpero changed the severity to 3 (High Risk)

Awards

3.0466 USDC - $3.05

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
duplicate-473

External Links

Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/SurplusGuildMinter.sol#L229

Vulnerability details

Impact

The user loses their staked CREDITs when they start staking in a LendingTerm that has previously incurred a loss, as the tokens are written off within the SurPlusGuildMinter contract.

Proof of Concept

The getRewards function in the SurplusGuildMinter contract initializes an empty UserStake object within the definition of the function's return values.

Thus, in L229 within the getRewards function, the code literally checks the following:

lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term); if (lastGaugeLoss > 0) { slashed = true; }

Thus, 'slashed' is set to 'true' as soon as the corresponding LendingTerm has already incurred a loss, regardless of when the user started staking their CREDITs.

Therefore, when a user unstakes, all of their CREDITS and GUILD tokens are written off within the getRewards function, even if there was no loss since the user started staking.

Tools Used

Manual review.

Load the user's userStake before check the "lastGaugeLoss" value.

--- SurplusGuildMinter.sol 2023-12-20 11:11:08.380882238 +0100 +++ SurplusGuildMinter_fixed.sol 2023-12-20 11:16:41.511329264 +0100 @@ -226,12 +226,12 @@ { bool updateState; lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term); + userStake = _stakes[user][term]; if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) { slashed = true; } // if the user is not staking, do nothing - userStake = _stakes[user][term]; if (userStake.stakeTime == 0) return (lastGaugeLoss, userStake, slashed);

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-12-29T14:29:28Z

0xSorryNotSorry marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-29T14:29:49Z

0xSorryNotSorry marked the issue as duplicate of #1164

#2 - c4-judge

2024-01-28T20:18:10Z

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