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: 86/127
Findings: 2
Award: $62.65
🌟 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
Due to the uninitialized userStake.lastGaugeLoss
in the SurplusGuildMinter.getRewards()
function, a new user's stake is consistently and erroneously slashed. This critical flaw not only imposes unjustified financial losses on users but also undermines the integrity of the staking mechanism.
lastGaugeLoss
in the past and simulate the passage of time.getRewards()
.Add the function below into test/unit/loan/SurplusGuildMinter.t.sol
.
function testAuditUserAlwaysSlashed() public { // // 1. Setting up the Term (with non-zero `lastGaugeLoss` somewhere in the past) // // simulate a past loss to test its impact on new stakers testUnstakeWithLoss(); // next block vm.warp(block.timestamp + 13); vm.roll(block.number + 1); // there was a loss somewhere in the past uint256 lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term); assertGt(lastGaugeLoss, 0); assertGt(block.timestamp, lastGaugeLoss); // // 2. Initiating New User Stake // // mint credit for a user address newStaker = vm.addr(1); credit.mint(newStaker, 100e18); // user don't have an active stake yet assertEq(sgm.getUserStake(newStaker, term).stakeTime, 0); // set up a new staker and confirm initial conditions vm.startPrank(newStaker); credit.approve(address(sgm), 100e18); sgm.stake(term, 100e18); assertEq(credit.balanceOf(newStaker), 0); assertEq(sgm.getUserStake(newStaker, term).credit, 100e18); vm.stopPrank(); // new stake `lastGaugeLoss` was initialized with term's `lastGaugeLoss` correctly lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term); assertEq(sgm.getUserStake(newStaker, term).lastGaugeLoss, lastGaugeLoss); // next block vm.warp(block.timestamp + 13); vm.roll(block.number + 1); // // 3. Observing the Erroneous Slashing // (, , bool slashed) = sgm.getRewards(newStaker, term); assertTrue(slashed); }
Run test with forge test -vv --match-test testAuditUserAlwaysSlashed
. Output example:
[â †] Compiling... No files changed, compilation skipped Running 1 test for test/unit/loan/SurplusGuildMinter.t.sol:SurplusGuildMinterUnitTest [PASS] testAuditUserAlwaysSlashed() (gas: 1011790) Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.07ms Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
VSCode, Foundry
It is recommended to initialize the userStake
variable before its use in the getRewards()
function.
/// @notice get rewards from a staking position without unstaking. /// This can be used to slash users that have an outstanding unapplied loss. 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 ) { userStake = _stakes[user][term]; bool updateState; lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term); if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) { slashed = true; } // if the user is not staking, do nothing if (userStake.stakeTime == 0) return (lastGaugeLoss, userStake, slashed); // compute CREDIT rewards ProfitManager(profitManager).claimRewards(address(this)); // this will update profit indexes uint256 _profitIndex = ProfitManager(profitManager) .userGaugeProfitIndex(address(this), term); uint256 _userProfitIndex = uint256(userStake.profitIndex);
Other
#0 - c4-pre-sort
2023-12-29T14:50:06Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-pre-sort
2023-12-29T14:50:26Z
0xSorryNotSorry marked the issue as duplicate of #1164
#2 - c4-judge
2024-01-28T20:20:07Z
Trumpero marked the issue as satisfactory
59.6005 USDC - $59.60
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/tokens/GuildToken.sol#L197-L200 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/main/src/loan/SurplusGuildMinter.sol#L130-L136
The setProfitManager
function in GuildToken
allows for the changing of the ProfitManager
address. However, this change is not reflected in other components like LendingTerm
, SimplePSM
, and SurplusGuildMinter
, which still refer to the old address. This inconsistency can lead to a critical system failure. This discrepancy can result in failed transactions or incorrect reward distributions, leading to system instability and potential financial risks.
GuildToken.setProfitManager()
is executed with newProfitManager
.SurplusGuildMinter.stake()
makes a call to oldProfitManager.donateToTermSurplusBuffer()
.GuildToken.incrementGauge()
, which incorrectly calls newProfitManager.claimGaugeRewards()
.ProfitManager
address in SurplusGuildMinter
as it is in GuildToken
.VSCode
Ensure that the method of updating the ProfitManager
address is consistent across all components (GuildToken
, LendingTerm
, SimplePSM
, SurplusGuildMinter
). This can be achieved by implementing a similar update mechanism in all components or by restricting the ability to change the ProfitManager
in GuildToken
to align with other components.
Other
#0 - c4-pre-sort
2024-01-03T21:13:47Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-03T21:14:06Z
0xSorryNotSorry marked the issue as duplicate of #1001
#2 - c4-judge
2024-01-29T21:37:49Z
Trumpero marked the issue as satisfactory