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: 120/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 will lost all staked tokens after pnl loss.
If a bidder bids on a loan between mid points and duration periods, the system incurs some losses and is called out notifyPnl()
.
In that case, the loss amount will be negative value.
function notifyPnL( address gauge, int256 amount ) external onlyCoreRole(CoreRoles.GAUGE_PNL_NOTIFIER) { ... if (amount < 0) { uint256 loss = uint256(-amount); // save gauge loss GuildToken(guild).notifyGaugeLoss(gauge); ... } else if (amount > 0) { ... } ... }
To save guage loss, GuildToken(guild).notifyGaugeLoss(gauge)
is called in notifyPnl
function.
function notifyGaugeLoss(address gauge) external { require(msg.sender == profitManager, "UNAUTHORIZED"); // save gauge loss lastGaugeLoss[gauge] = block.timestamp; emit GaugeLoss(gauge, block.timestamp); }
As you can see above, lastGaugeLoss[gauge] will be updated to block.timestamp.
In getRewards
function, there is no initialization for the userStake.lastGaugeLoss
before if (lastGaugeLoss > uint256(userStake.lastGaugeLoss))
part.
So userStake.lastGaugeLoss is always set to 0, and slashed flag is set to true.
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; } ... }
In unStake function, if slashed flag is true, the function returns after calling getRewards. So users cannot get their staked tokens.
function unstake(address term, uint256 amount) external { // apply pending rewards (, UserStake memory userStake, bool slashed) = getRewards( msg.sender, term ); console2.log("slashed", slashed); // if the user has been slashed, there is nothing to do if (slashed) return; ... }
function testSlashFlag() public { address term1 = address(new MockLendingTerm(address(core))); guild.addGauge(1, term1); guild.mint(address(this), 100e18); guild.incrementGauge(term1, 100e18); bool slashed = false; address user1 = address(19028109281092); credit.mint(user1, 100e18); (, , slashed ) = sgm.getRewards(user1, term1); assertEq(slashed, false); vm.startPrank(user1); credit.approve(address(sgm), 100e18); sgm.stake(term1, 100e18); vm.stopPrank(); (, , slashed ) = sgm.getRewards(user1, term1); assertEq(slashed, false); assertEq(profitManager.surplusBuffer(), 0); assertEq(profitManager.termSurplusBuffer(term1), 100e18); vm.prank(governor); profitManager.setProfitSharingConfig( 0.5e18, // surplusBufferSplit 0, // creditSplit 0.5e18, // guildSplit 0, // otherSplit address(0) // otherRecipient ); credit.mint(address(profitManager), 60e18); profitManager.notifyPnL(term1, 60e18); vm.startPrank(user1); sgm.unstake(term1, 50e18); vm.stopPrank(); (, , slashed ) = sgm.getRewards(user1, term1); assertEq(slashed, false); assertEq(credit.balanceOf(user1), 50e18 + 20e18);//include rewards vm.warp(block.timestamp + 13); vm.roll(block.number + 1); profitManager.notifyPnL(term1, -20e18); guild.applyGaugeLoss(term1, address(sgm)); (, , slashed ) = sgm.getRewards(user1, term1); assertEq(slashed, true); vm.warp(block.timestamp + 13); vm.roll(block.number + 1); vm.startPrank(user1); sgm.unstake(term1, 50e18); vm.stopPrank(); (, , slashed ) = sgm.getRewards(user1, term1); assertEq(slashed, true); assertFalse(credit.balanceOf(user1) == 70e18 + 50e18, "balance = 70e + 50e18"); assertEq(credit.balanceOf(user1), 70e18); vm.startPrank(user1); credit.approve(address(sgm), 10e18); sgm.stake(term1, 10e18); vm.stopPrank(); (, , slashed ) = sgm.getRewards(user1, term1); assertEq(slashed, true); }
VScode
The state of userStake should be updated before if (lastGaugeLoss > uint256(userStake.lastGaugeLoss))
part.
userStake = _stakes[user][term];
Other
#0 - 0xSorryNotSorry
2023-12-29T14:52:13Z
The issue is well demonstrated, properly formatted, contains a coded POC. Marking as HQ.
#1 - c4-pre-sort
2023-12-29T14:52:17Z
0xSorryNotSorry marked the issue as high quality report
#2 - c4-pre-sort
2023-12-29T14:52:30Z
0xSorryNotSorry marked the issue as duplicate of #1164
#3 - c4-judge
2024-01-28T20:19:55Z
Trumpero marked the issue as satisfactory