Ethereum Credit Guild - deliriusz'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: 84/127

Findings: 2

Award: $67.67

QA:
grade-b

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Awards

46.8502 USDC - $46.85

Labels

bug
3 (High Risk)
disagree with severity
satisfactory
sponsor confirmed
sufficient quality report
duplicate-1194

External Links

Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/governance/ProfitManager.sol#L409-L436

Vulnerability details

Gauge rewards in ECG are similar to Compound - there is a global ever-increasing profit index, and one profit index per user, synchronized by applying the delta between gaugeProfitIndex and userGaugeProfitIndex.

When user profit index is not initialized, it's set to 1e18. This is an issue when the ProfitManager operates for some time and the gaugeProfitIndex grew to a new value. Then we have following situation:

gaugeProfitIndex = 1.2e18 User just joined, userGaugeProfitIndex is set to 1e18. Hence He's elligible to all rewards since gauge started operating.

The problem is even worse if the Guild token transfer are enabled. Then a malicious actor can use sybil accounts and claim all the rewards of other users to themselves.

Impact

All rewards are stolen by sybil accounts, able to claim all rewards since the protocol launch.

Proof of Concept

  1. Wait for gaugeProfitIndex to grow.
  2. Call GuildToken.incrementGauge(). From a new account. This will internally call ProfitManager.claimGaugeRewards().
  3. Because the user didn't claimed their profit before, their profit will be initialzed to 1e18, allowing them to claim all rewards since beginning.
  4. If Guild token transfers are enabled, the user can get the tokens back, transfer the amount to their account and repeat the attack.

Tools Used

Manual analysis

In case that the user doesn't have index assigned, assign it to current gauge profit index and return from the function early:

    function claimGaugeRewards(
        address user,
        address gauge
    ) public returns (uint256 creditEarned) {
        uint256 _userGaugeWeight = uint256(
            GuildToken(guild).getUserGaugeWeight(user, gauge)
        );
        if (_userGaugeWeight == 0) {
            return 0;
        }
        uint256 _gaugeProfitIndex = gaugeProfitIndex[gauge];
        uint256 _userGaugeProfitIndex = userGaugeProfitIndex[user][gauge];
        if (_gaugeProfitIndex == 0) {
            _gaugeProfitIndex = 1e18;
        }
        if (_userGaugeProfitIndex == 0) {
-            _userGaugeProfitIndex = 1e18;
+            userGaugeProfitIndex[user][gauge] = _gaugeProfitIndex;
+            return 0;

Assessed type

Math

#0 - c4-pre-sort

2024-01-01T12:19:52Z

0xSorryNotSorry marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-01T12:20:00Z

0xSorryNotSorry marked the issue as primary issue

#2 - eswak

2024-01-16T14:21:13Z

#1194 (marked as duplicate to this issue) contains a test that can be run to confirm

Disputing severity (think it's more fit for medium because there is no loss of user funds, just rewards)

#3 - c4-sponsor

2024-01-16T14:21:19Z

eswak (sponsor) confirmed

#4 - c4-sponsor

2024-01-16T14:21:23Z

eswak marked the issue as disagree with severity

#5 - c4-judge

2024-01-19T10:42:04Z

Trumpero marked the issue as satisfactory

#6 - c4-judge

2024-01-19T20:10:31Z

Trumpero marked issue #1194 as primary and marked this issue as a duplicate of 1194

#7 - Trumpero

2024-01-19T20:18:20Z

I still consider this issue a high severity because the rewards in ProfitManager are matured yield, which should be distributed to the intended users, and the loss of matured yield is considered a high-severity issue based on C4's criteria. @eswak

Awards

20.8157 USDC - $20.82

Labels

bug
downgraded by judge
grade-b
QA (Quality Assurance)
sufficient quality report
duplicate-152
Q-03

External Links

Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/tokens/ERC20Gauges.sol#L500-L539

Vulnerability details

When a loss is recorded in the system, it's shared across all gauge voters, and in order to perform any action on them, GuildToken.applyGaugeLoss() has to be called. However, this function will run indefinitely when it meets getUserGaugeWeight[user][gauge] that is 0. This is easy to achieve by increasing voting weight by 0 by user. Then, the loop will run indefinitely, because of mismatched ++i position. This makes the position loss unappliable:

        for (
            uint256 i = 0;
            i < size && (userFreeWeight + userFreed) < weight;

        ) {
            address gauge = gaugeList[i];
            uint256 userGaugeWeight = getUserGaugeWeight[user][gauge];
            if (userGaugeWeight != 0) {
                userFreed += userGaugeWeight;
                _decrementGaugeWeight(user, gauge, userGaugeWeight);

                // If the gauge is live (not deprecated), include its weight in the total to remove
                if (!_deprecatedGauges.contains(gauge)) {
                    totalTypeWeight[gaugeType[gauge]] -= userGaugeWeight;
                    totalFreed += userGaugeWeight;
                }

                unchecked {
                    ++i;
                }
            }
        }

Impact

User GUILD tokens which are menat to be burned after the protocol, leaving "poisonous" GUILD tokens in a gauge forever.

Tools Used

Manual analysis

The fix is very straight forward:

        for (
            uint256 i = 0;
            i < size && (userFreeWeight + userFreed) < weight;

        ) {
            address gauge = gaugeList[i];
            uint256 userGaugeWeight = getUserGaugeWeight[user][gauge];
            if (userGaugeWeight != 0) {
                userFreed += userGaugeWeight;
                _decrementGaugeWeight(user, gauge, userGaugeWeight);

                // If the gauge is live (not deprecated), include its weight in the total to remove
                if (!_deprecatedGauges.contains(gauge)) {
                    totalTypeWeight[gaugeType[gauge]] -= userGaugeWeight;
                    totalFreed += userGaugeWeight;
                }
+            }
                unchecked {
                    ++i;
                }
-            }
        }

Generally, micro optimizations are an enemy to the security and should be verified very carefully.

Assessed type

Error

#0 - c4-pre-sort

2024-01-03T19:17:39Z

0xSorryNotSorry marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-01-03T19:18:03Z

0xSorryNotSorry marked the issue as duplicate of #152

#2 - c4-judge

2024-01-28T19:06:03Z

Trumpero changed the severity to QA (Quality Assurance)

#3 - c4-judge

2024-01-28T19:09:30Z

Trumpero marked the issue as grade-b

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