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
Rank: 81/127
Findings: 3
Award: $77.78
π Selected for report: 0
π Solo Findings: 0
π Selected for report: JCN
Also found by: 0xadrii, 0xaltego, 0xdice91, 0xivas, 0xpiken, Akali, AlexCzm, Chinmay, DanielArmstrong, HighDuty, Infect3d, Inference, KupiaSec, PENGUN, SECURITISE, Stormreckson, SweetDream, TheSchnilch, Timeless, Varun_05, XDZIBECX, alexzoid, asui, beber89, btk, carrotsmuggler, cats, cccz, developerjordy, ether_sky, grearlake, imare, jasonxiale, kaden, klau5, santipu_, serial-coder, sl1, smiling_heretic, stackachu, wangxx2026, whitehat-boys
3.0466 USDC - $3.05
In SurplusGuildMinter, the user stakes credit tokens to mint guild tokens and increase gauge, and when bad debt arises, the user's stake is slashed. In getRewards(), when lastGaugeLoss > userStake.lastGaugeLoss, slashed is true, indicating that the user will be slashed.
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; }
The problem here is that the userStake is uninitialized when getting userStake.lastGaugeLoss, which results in slashed being true when getRewards() is called again even though the user has already been slashed, and if the user stakes credit tokens after it has been slashed, the user will be slashed again. The POC is as follows.
function testpoc() public { // setup credit.mint(address(this), 150e18); credit.approve(address(sgm), 150e18); sgm.stake(term, 99e18); assertEq(sgm.getUserStake(address(this), term).credit, 99e18); // the guild token earn interests vm.prank(governor); profitManager.setProfitSharingConfig( 0.5e18, // surplusBufferSplit 0, // creditSplit 0.5e18, // guildSplit 0, // otherSplit address(0) // otherRecipient ); // next block vm.warp(block.timestamp + 13); vm.roll(block.number + 1); profitManager.notifyPnL(term, -27.5e18); guild.applyGaugeLoss(term, address(sgm)); // next block vm.warp(block.timestamp + 13); vm.roll(block.number + 1); sgm.stake(term, 9e18); assertEq(sgm.getUserStake(address(this), term).credit, 9e18); // next block vm.warp(block.timestamp + 13); vm.roll(block.number + 1); sgm.stake(term, 10e18); assertEq(sgm.getUserStake(address(this), term).credit, 10e18); //@audit: should be 19e18 }
none
Change to
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); + userStake = _stakes[user][term]; if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) { slashed = true; } // if the user is not staking, do nothing - userStake = _stakes[user][term];
Error
#0 - c4-pre-sort
2023-12-29T08:16:35Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-29T15:17:53Z
0xSorryNotSorry marked the issue as duplicate of #1164
#2 - c4-judge
2024-01-28T20:11:23Z
Trumpero marked the issue as satisfactory
π Selected for report: SBSecurity
Also found by: 0x_6a70, 0xanmol, 0xbepresent, 0xfave, Arz, Byteblockers, CaeraDenoir, EV_om, EllipticPoint, Infect3d, JCN, Mike_Bello90, SECURITISE, Soul22, almurhasan, c47ch3m4ll, carrotsmuggler, cccz, critical-or-high, ether_sky, evmboi32, grearlake, kaden, rbserver, smiling_heretic, whitehat-boys, zhaojie
3.4087 USDC - $3.41
ProfitManager.notifyPnL is used to distribute rewards or notify gauge loss. When amount is less than 0, it will notify gauge losse and users will lose the guild tokens incremented to the gauge. And in SurplusGuildMinter, users also lose pledged credit tokens.
function notifyPnL( address gauge, int256 amount ) external onlyCoreRole(CoreRoles.GAUGE_PNL_NOTIFIER) { uint256 _surplusBuffer = surplusBuffer; uint256 _termSurplusBuffer = termSurplusBuffer[gauge]; address _credit = credit; // handling loss if (amount < 0) { uint256 loss = uint256(-amount); // save gauge loss GuildToken(guild).notifyGaugeLoss(gauge);
When amount is greater than 0, the reward is distributed.
else if (amount > 0) { ... if (amountForCredit != 0) { CreditToken(_credit).distribute(amountForCredit); } // distribute to the guild if (amountForGuild != 0) { // update the gauge profit index // if the gauge has 0 weight, does not update the profit index, this is unnecessary // because the profit index is used to reattribute profit to users voting for the gauge, // and if the weigth is 0, there are no users voting for the gauge. uint256 _gaugeWeight = uint256( GuildToken(guild).getGaugeWeight(gauge) ); if (_gaugeWeight != 0) { uint256 _gaugeProfitIndex = gaugeProfitIndex[gauge]; if (_gaugeProfitIndex == 0) { _gaugeProfitIndex = 1e18; } gaugeProfitIndex[gauge] = _gaugeProfitIndex + (amountForGuild * 1e18) / _gaugeWeight; } }
And since the protocol does not implement a locking mechanism, users can add or remove tokens at any time. This allows users to frontrun ProfitManager.notifyPnL to avoid losing or claiming rewards.
For example, since the auction is linear and bad debts are only generated after the midpoint which makes pnl negative, users can avoid losses by unstaking tokens from SurplusGuildMinter in advance of the midpoint.
In addition, when interest is incurred due to user repays or bids, the user can frontrun the transaction to increment to the gauge and decrement as soon as getting the reward.
None
It is recommended that instead of allowing the user to make immediate exit, the user would be required to make a exit request and would be allowed to exit after a period, and then the exit would only be allowed within a limited time frame.
Context
#0 - c4-pre-sort
2023-12-29T08:37:47Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-29T17:55:32Z
0xSorryNotSorry marked the issue as primary issue
#2 - c4-pre-sort
2023-12-29T19:07:06Z
0xSorryNotSorry marked the issue as duplicate of #877
#3 - Trumpero
2024-01-25T09:06:32Z
Partial duplicate of #994
#4 - c4-judge
2024-01-25T09:06:39Z
Trumpero marked the issue as not a duplicate
#5 - c4-judge
2024-01-25T09:06:50Z
Trumpero marked the issue as duplicate of #994
#6 - c4-judge
2024-01-25T09:48:02Z
Trumpero marked the issue as unsatisfactory: Invalid
#7 - c4-judge
2024-01-25T18:15:34Z
Trumpero marked the issue as satisfactory
#8 - Slavchew
2024-02-01T23:05:57Z
Hey, @Trumpero,
This issue doesnβt show the real impact mentioned in the #994
The only impact which is common for both reports is this line:
βIn addition, when interest is incurred due to user repays or bids, the user can frontrun the transaction to increment to the gauge and decrement as soon as getting the reward.β
The whole report clearly emphasizes the notifyPnL frontrun part which means it is a duplicate of #877
Thanks for your time.
#9 - Trumpero
2024-02-05T20:03:25Z
Agree that this issue is similar to both #994 and #877, but #994 is a higher severity issue. Due to the lack of quality regarding it, I will decrease the credit of this issue.
#10 - c4-judge
2024-02-05T20:03:30Z
Trumpero marked the issue as partial-50
π Selected for report: SpicyMeatball
Also found by: Byteblockers, JCN, TheSchnilch, cccz, ether_sky, kaden, klau5, mojito_auditor, niroh, nocoder, rbserver, santipu_
71.3169 USDC - $71.32
In _decrementGaugeWeight, debtCeiling is called to get the debt ceiling after decrementation and requires the issuance to be less than it.
function _decrementGaugeWeight( address user, address gauge, uint256 weight ) internal override { uint256 _lastGaugeLoss = lastGaugeLoss[gauge]; uint256 _lastGaugeLossApplied = lastGaugeLossApplied[gauge][user]; require( _lastGaugeLossApplied >= _lastGaugeLoss, "GuildToken: pending loss" ); // update the user profit index and claim rewards ProfitManager(profitManager).claimGaugeRewards(user, gauge); // check if gauge is currently using its allocated debt ceiling. // To decrement gauge weight, guild holders might have to call loans if the debt ceiling is used. uint256 issuance = LendingTerm(gauge).issuance(); if (issuance != 0) { uint256 debtCeilingAfterDecrement = LendingTerm(gauge).debtCeiling(-int256(weight)); require( issuance <= debtCeilingAfterDecrement, "GuildToken: debt ceiling used" ); }
In debtCeiling, gaugeWeight is added to gaugeWeightDelta as the latest gaugeWeight. The problem here is that totalWeight does not have gaugeWeightDelta added to it, and since the totalWeight in the real post-decrease debtCeiling has gaugeWeightDelta added to it, this results in a larger totalWeight than it really is (gaugeWeightDelta is negative), the returned debtCeiling may be smaller than the actual, the user's decrease is smaller than actual.
function debtCeiling( int256 gaugeWeightDelta ) public view returns (uint256) { address _guildToken = refs.guildToken; // cached SLOAD uint256 gaugeWeight = GuildToken(_guildToken).getGaugeWeight( address(this) ); gaugeWeight = uint256(int256(gaugeWeight) + gaugeWeightDelta); uint256 gaugeType = GuildToken(_guildToken).gaugeType(address(this)); uint256 totalWeight = GuildToken(_guildToken).totalTypeWeight( gaugeType );
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/LendingTerm.sol#L270-L281 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/tokens/GuildToken.sol#L207-L231
None
Change to
function debtCeiling( int256 gaugeWeightDelta ) public view returns (uint256) { address _guildToken = refs.guildToken; // cached SLOAD uint256 gaugeWeight = GuildToken(_guildToken).getGaugeWeight( address(this) ); gaugeWeight = uint256(int256(gaugeWeight) + gaugeWeightDelta); uint256 gaugeType = GuildToken(_guildToken).gaugeType(address(this)); uint256 totalWeight = GuildToken(_guildToken).totalTypeWeight( gaugeType ); + if (!GuildToken(refs.guildToken).isDeprecatedGauge(address(this))) + totalWeight += uint256(int256(totalWeight) + gaugeWeightDelta)
Math
#0 - c4-pre-sort
2023-12-29T08:33:13Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-30T15:00:13Z
0xSorryNotSorry marked the issue as primary issue
#2 - c4-sponsor
2024-01-16T07:48:56Z
eswak (sponsor) confirmed
#3 - c4-judge
2024-01-28T19:14:30Z
Trumpero marked the issue as satisfactory
#4 - c4-judge
2024-01-28T19:26:09Z
Trumpero marked issue #586 as primary and marked this issue as a duplicate of 586