Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infinite loop in ERC20Gauges::_decrementWeightUntilFree when one of the user's gauges has a zero weight #152

Closed
c4-bot-6 opened this issue Dec 18, 2023 · 9 comments
Labels
bug Something isn't working disagree with severity Sponsor confirms validity, but disagrees with warden’s risk assessment (sponsor explain in comments) downgraded by judge Judge downgraded the risk level of this issue grade-c primary issue Highest quality submission among a set of duplicates QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") sufficient quality report This report is of sufficient quality unsatisfactory does not satisfy C4 submission criteria; not eligible for awards

Comments

@c4-bot-6
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/tokens/ERC20Gauges.sol#L532-L534

Vulnerability details

Impact

The position of ++i in the loop of the ERC20Gauges::_decrementWeightUntilFree function is incorrect, and can result in an infinite loop.

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

    function _decrementWeightUntilFree(address user, uint256 weight) internal {
...
        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;
@>              }
            }
        }

When userGaugeWeight == 0, i is not incremented and will not break out of the for loop. This would only happen if a user increments their gauge to zero. If they then transfer or burn their tokens, the transaction would fail.

Proof of Concept

Test case in ERC20Gauges.t.sol

    function testAudit_IncrementGauges() public {
        token.mint(address(this), 100e18);

        token.setMaxGauges(2);
        token.addGauge(1, gauge1);
        token.addGauge(1, gauge2);

        // User calls incrementGauge with a weight of 0.
        token.incrementGauge(gauge1, 0);
        token.incrementGauge(gauge2, 100e18);

        require(token.getUserGaugeWeight(address(this), gauge1) == 0);
        // Transfers tokens. This results in the infinite loop and the transaction will fail.
        token.transfer(address(1), 100e18);
    }

Tools Used

Foundry

Recommended Mitigation Steps

Move ++i outside of the if (userGaugeWeight != 0) block

        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;
-               }
            }
+           unchecked {
+               ++i;
+           }
        }

Assessed type

Loop

@c4-bot-6 c4-bot-6 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Dec 18, 2023
c4-bot-4 added a commit that referenced this issue Dec 18, 2023
@c4-pre-sort c4-pre-sort added the sufficient quality report This report is of sufficient quality label Dec 30, 2023
@c4-pre-sort
Copy link

0xSorryNotSorry marked the issue as sufficient quality report

@c4-pre-sort c4-pre-sort added the primary issue Highest quality submission among a set of duplicates label Dec 30, 2023
@c4-pre-sort
Copy link

0xSorryNotSorry marked the issue as primary issue

This was referenced Dec 30, 2023
This was referenced Jan 5, 2024
@c4-sponsor
Copy link

eswak marked the issue as disagree with severity

@c4-sponsor c4-sponsor added the disagree with severity Sponsor confirms validity, but disagrees with warden’s risk assessment (sponsor explain in comments) label Jan 8, 2024
@eswak
Copy link

eswak commented Jan 8, 2024

While this is a valid issue, I think it should be QA because I don't think there is a way to exploit this.

The user would need to purposefully increment 0 weight on a gauge (through etherscan directly and not through our UI), and then it would only prevent them from transferring their tokens until they decrement 0 weight.

I initially thought this could be used to prevent slashing (which would have higher severity), but this is not the case because applyGaugeLoss calls _decrementGaugeWeight before calling _burn (that itself calls _decrementWeightUntilFree, where the bug is).

@c4-sponsor
Copy link

eswak (sponsor) confirmed

@c4-sponsor c4-sponsor added the sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") label Jan 8, 2024
@Trumpero
Copy link

Agree with the sponsor that this issue should be a QA/low since it can't prevent slashing. applyGaugeLoss calls _decrementGaugeWeight before calling _burn, so _decrementWeightUntilFree will return from the start (code snippet).
This issue may only break the transfer/burn of a user when they increment 0 weight to a gauge. In such cases, it's the user's mistake, and they can recover by adding/allocating weight greater than 0 again

@c4-judge c4-judge added downgraded by judge Judge downgraded the risk level of this issue QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax and removed 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value labels Jan 28, 2024
@c4-judge
Copy link
Contributor

Trumpero changed the severity to QA (Quality Assurance)

@c4-judge
Copy link
Contributor

Trumpero marked the issue as grade-a

@c4-judge
Copy link
Contributor

Trumpero marked the issue as grade-c

@c4-judge c4-judge added grade-c unsatisfactory does not satisfy C4 submission criteria; not eligible for awards and removed grade-a labels Jan 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working disagree with severity Sponsor confirms validity, but disagrees with warden’s risk assessment (sponsor explain in comments) downgraded by judge Judge downgraded the risk level of this issue grade-c primary issue Highest quality submission among a set of duplicates QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") sufficient quality report This report is of sufficient quality unsatisfactory does not satisfy C4 submission criteria; not eligible for awards
Projects
None yet
Development

No branches or pull requests

6 participants