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: 124/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
Users are not able to unstake their tokens or receive rewards for them when they should be able to.
Due to not caching the userStake
info inside the function SurplusGuildMinter::getRewards
before making a critical comparison, whenever a gauge takes a single loss, all user rewards 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)) { // @audit comparison here slashed = true; } // if the user is not staking, do nothing userStake = _stakes[user][term]; // @audit caching here
userStake.lastGaugeLoss
will always be the default value of uint256
which is 0
. Whenever a gauge takes a single loss, this comparison will always be true
, thus slashed
will be true
.
Furthermore, since the getRewards
function is called every time when a user tries to unstake
, as long as a gauge takes a single loss, all users that have staked tokens will not be able to unstake them even if they should be able to. Clear loss of funds/tokens for the users.
function unstake(address term, uint256 amount) external { // apply pending rewards (, UserStake memory userStake, bool slashed) = getRewards( msg.sender, term ); // if the user has been slashed, there is nothing to do if (slashed) return;
Manual Review
Cache the userStake
before the comparison.
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; + userStake = _stakes[user][term]; lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term); if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) { // @audit comparison here slashed = true; } // if the user is not staking, do nothing - userStake = _stakes[user][term]; // @audit caching here
Invalid Validation
#0 - c4-pre-sort
2023-12-29T15:18:14Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-29T15:18:38Z
0xSorryNotSorry marked the issue as duplicate of #1164
#2 - c4-judge
2024-01-28T20:12:36Z
Trumpero marked the issue as satisfactory