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: 121/127
Findings: 1
Award: $3.05
🌟 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
SurplusGuildMinter.getRewards
is used to update a user's position info(including credit, guild), but there is an error in current implementation that if GuildToken(guild).lastGaugeLoss(term)
is larger than zero, all users' UserStake
can be cleared, which will cause all stakers lose tokens/rewards.
Since SurplusGuildMinter.getRewards
can be called by anyone, a malicious user can abuse this function to clear all stakeers' UserStake
In SurplusGuildMinter.getRewards, slashed is set depends on term's lastGaugeLoss and userStake.lastGaugeLoss, the issue is userStake is a memory variable and hasn't been initialized yet, in such case userStake.lastGaugeLoss
will be 0.
216 function getRewards( 217 address user, 218 address term 219 ) 220 public 221 returns ( 222 uint256 lastGaugeLoss, // GuildToken.lastGaugeLoss(term) 223 UserStake memory userStake, // stake state after execution of getRewards() 224 bool slashed // true if the user has been slashed 225 ) 226 { 227 bool updateState; 228 lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term); 229 if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) { <<<--- Here userStake hasn't been initialized yet 230 slashed = true; 231 } 232 ... 270 271 // if a loss occurred while the user was staking, the GuildToken.applyGaugeLoss(address(this)) 272 // can be called by anyone to slash address(this) and decrement gauge weight etc. 273 // The contribution to the surplus buffer is also forfeited. 274 if (slashed) { 275 emit Unstake(block.timestamp, term, uint256(userStake.credit)); 276 userStake = UserStake({ <<<---- Here stake info will be cleared 277 stakeTime: uint48(0), 278 lastGaugeLoss: uint48(0), 279 profitIndex: uint160(0), 280 credit: uint128(0), 281 guild: uint128(0) 282 }); 283 updateState = true; 284 } 285 286 // store the updated stake, if needed 287 if (updateState) { 288 _stakes[user][term] = userStake; 289 } 290 }
So if lastGaugeLoss is larger than zero, slashed
will be true
And if slashed
variable is true, userStake will be cleared
VIM
diff --git a/src/loan/SurplusGuildMinter.sol b/src/loan/SurplusGuildMinter.sol index f68e9ff..95c0f5c 100644 --- a/src/loan/SurplusGuildMinter.sol +++ b/src/loan/SurplusGuildMinter.sol @@ -226,12 +226,12 @@ contract SurplusGuildMinter is CoreRef { { 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]; if (userStake.stakeTime == 0) return (lastGaugeLoss, userStake, slashed);
Error
#0 - c4-pre-sort
2023-12-29T14:26:53Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-29T14:27:14Z
0xSorryNotSorry marked the issue as duplicate of #1164
#2 - c4-judge
2024-01-28T20:13:22Z
Trumpero marked the issue as satisfactory