Closed c4-bot-8 closed 10 months ago
0xSorryNotSorry marked the issue as sufficient quality report
0xSorryNotSorry marked the issue as duplicate of #152
Trumpero changed the severity to QA (Quality Assurance)
Trumpero marked the issue as grade-a
Trumpero marked the issue as grade-c
Lines of code
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/tokens/ERC20Gauges.sol#L500-#L539 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/tokens/GuildToken.sol#L279-#L315 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/tokens/ERC20Gauges.sol#L230-#L247
Vulnerability details
Impact
ERC20Gauges.sol::_incrementGaugeWeight()
there is no check for a minimum weight. A weight of 0 can be passed in. Importantly, even though the weight is 0, the gauge will still be added to_userGauges
mapping.ERC20Gauges::_decrementWeightUntilFree()
function, which is mandatorily called before anytransfer()
,burn()
, ortransferFrom()
function for the GuildToken. (Even though the GuildToken will not be transferrable on launch I think, there are plans to have it be transferrable in the future).ERC20Gauges::_decrementWeightUntilFree()
is in the incorrect spot, so if there is a gauge weight of 0, the loop counter does NOT increment because the logic never gets inside of theif (userGaugeWeight != 0) {...
block. So the transaction will always run out of gas and revert if there is a gauge with a weight of 0 ingetUserGaugeWeight
mapping and the user needed to decrement some weight before doing any kind of balance changing function for their GUILD tokens, causing the user to pay all the gas up until the point of the function reverting.Imagine a scenario where there are 3 gauges and a user has delegated some weight to all 3, importantly with at least one of them being 0 weight:
_userGauges
:[Gauge1, Gauge2, Gauge3]
getUserGaugeWeight
:[Gauge1: 69, Gauge2: 0, Gauge3: 420]
The user has 1000 tokens and wants to
transfer
their whole balance of 1000 tokens. However, they have already dished out 489 in weight to the gauges. This means they have 511 free tokens available for transfer, so they need to free up 489 tokens so they can transfer out their whole 1000.The function
_decrementWeightUntilFree()
will start looping through the users gauges perfectly, but when it encounters Gauge2 with a weight 0, the counter cannot increment because it is in the wrong spot in the for loop. This causes the tx to run until it is out of gas and then revert.The gauge with 0 weight must be at any position in the addresses array where the loop still needs to free some weight before completing. This breaks
transfer()
,_burn()
andtransferFrom()
functions fromERC20Gauges.sol
and thusGuildToken.sol
because they are all prefaced with a call to_decrementWeightUntilFree()
.Last thought that may be useful to double check/investigate further: I thought functions like
applyGaugeLoss()
would always revert when a user has a gauge weight of 0 and needed to free weight because_burn()
is called, but it appears this checkif (userFreeWeight >= weight) return;
in_decrementWeightUntilFree()
is always true when called from the context ofapplyGaugeLoss()
, because it always calls_decrementGaugeWeight()
and decrements the user's gauge weight before calling_burn()
, so there is always more user free weight than the weight passed in.Proof of Concept
forge test --match-path ./test/unit/tokens/ERC20Gauges.t.sol --match-test testBreakTransferWithZeroWeightIncrement --gas-limit 30000000 -vv
Tools Used
Manual Review
Recommended Mitigation Steps
Move the counter to the end of the loop, and also make sure gauges cannot be incremented/decremented by 0 with a
!= 0
check. If this bug happened in production, the user could callERC20Gauges::_decrementGaugeWieght()
and pass in 0 for the weight of that gauge address, and it should properly remove the gauge from the users gauges and fix the issue.Assessed type
Loop