Ethereum Credit Guild - Akali'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: 52/127

Findings: 2

Award: $252.27

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

3.0466 USDC - $3.05

Labels

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

External Links

Lines of code

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

Vulnerability details

Impact

After a lending term (gauge) incurs a loss, the SurplusGuildMinter.getRewards() function always returns slashed = true, even when this term is deprecated and then onboarded again. This results in users losing their assets if they stake in this term afterward because they will always be slashed in this term.

Proof of Concept

In the SurplusGuildMinter.getRewards function, userStake is a new memory struct with userStake.lastGaugeLoss set to 0. Therefore, if this term incurred a loss in the past, GuildToken(guild).lastGaugeLoss(term) always returns a non-zero value. Consequently, lastGaugeLoss will be greater than userStake.lastGaugeLoss, causing slashed to always be true.

function getRewards(
    address user,
    address term
)
    public
    returns (
        uint256 lastGaugeLoss, // GuildToken.lastGaugeLoss(term)
        UserStake memory userStake, // stake state after execution of getRewards()
        bool slashed // true if the user has been slashed
    )
{
    bool updateState;
    lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term);
    if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) {
        slashed = true;
    }
    ...

Additionally, even when a term is deprecated, the lastGaugeLoss of this term is still saved in the storage of the GuildToken contract. Therefore, it will always return a non-zero value, and users who stake in a re-added lending term will always be slashed.

Tools Used

Manual review

getRewards function should load from _stakes[user][term] before validation of slashed variable.

lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term);
userStake = _stakes[user][term];
if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) {
    slashed = true;
}

Assessed type

Context

#0 - c4-pre-sort

2023-12-29T14:46:09Z

0xSorryNotSorry marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-29T14:46:41Z

0xSorryNotSorry marked the issue as duplicate of #1164

#2 - c4-judge

2024-01-28T20:10:31Z

Trumpero marked the issue as satisfactory

Findings Information

🌟 Selected for report: critical-or-high

Also found by: 0xadrii, 0xpiken, Akali, Chinmay, Tendency, serial-coder

Labels

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

Awards

249.2197 USDC - $249.22

External Links

Lines of code

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

Vulnerability details

Impact

The SurplusGuildMinter.claimRewards function loops and claims all rewards from active lending terms delegated by this contract. Consequently, this function can run out of gas and revert, potentially causing a DoS state on other functions of SurplusGuildMinter. This situation may occur when the market has numerous lending terms, and users stake in many of them.

Proof of Concept

In the SurplusGuildMinter contract, getRewards function is used to claim and transfer rewards from a lending term (gauge) to the user. However, it calls the claimRewards function from the ProfitManager contract, which loops and claims rewards from all delegated lending terms of this contract. That action is unnecessary because it only needs to claim the reward of a specific term, resulting in the risk of reverting due to running out of gas for this function.

function getRewards(
    address user,
    address term
)...
{
    ...
    ProfitManager(profitManager).claimRewards(address(this));
    ...
}

function claimRewards(
    address user
) external returns (uint256 creditEarned) {
    address[] memory gauges = GuildToken(guild).userGauges(user);
    for (uint256 i = 0; i < gauges.length; ) {
        creditEarned += claimGaugeRewards(user, gauges[i]);
        unchecked {
            ++i;
        }
    }
}

Tools Used

Manual review

getRewards function should call claimGaugeRewards function instead of claimRewards:

ProfitManager(profitManager).claimGaugeRewards(address(this), term);

Assessed type

DoS

#0 - c4-pre-sort

2024-01-04T20:03:25Z

0xSorryNotSorry marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-04T20:03:38Z

0xSorryNotSorry marked the issue as duplicate of #1110

#2 - c4-judge

2024-01-28T20:23:04Z

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