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: 98/127
Findings: 2
Award: $33.46
🌟 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
Causes users to slashed when they are not causing them to loose their stake and not able to earn rewards.Precisily when a gauge is onboarded again after a gaugeloss then it will always return slashed for every user even if they are not.
Below is a get rewards function
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; } // if the user is not staking, do nothing userStake = _stakes[user][term]; 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); if (_profitIndex == 0) _profitIndex = 1e18; if (_userProfitIndex == 0) _userProfitIndex = 1e18; uint256 deltaIndex = _profitIndex - _userProfitIndex; if (deltaIndex != 0) { uint256 creditReward = (uint256(userStake.guild) * deltaIndex) / 1e18; uint256 guildReward = (creditReward * rewardRatio) / 1e18; if (slashed) { guildReward = 0; } // forward rewards to user if (guildReward != 0) { RateLimitedMinter(rlgm).mint(user, guildReward); emit GuildReward(block.timestamp, user, guildReward); } if (creditReward != 0) { CreditToken(credit).transfer(user, creditReward); } // save the updated profitIndex userStake.profitIndex = SafeCastLib.safeCastTo160(_profitIndex); updateState = true; } // if a loss occurred while the user was staking, the GuildToken.applyGaugeLoss(address(this)) // can be called by anyone to slash address(this) and decrement gauge weight etc. // The contribution to the surplus buffer is also forfeited. if (slashed) { emit Unstake(block.timestamp, term, uint256(userStake.credit)); userStake = UserStake({ stakeTime: uint48(0), lastGaugeLoss: uint48(0), profitIndex: uint160(0), credit: uint128(0), guild: uint128(0) }); updateState = true; } // store the updated stake, if needed if (updateState) { _stakes[user][term] = userStake; } }
As can be seen from the following lines lastGaugeLoss > uint256(userStake.lastGaugeLoss) here userStake.lastGaugeLoss will always return 0 because the original userStake = _stakes[user][term] which is initialised after being checked
bool updateState; lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term); if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) { slashed = true; } // if the user is not staking, do nothing userStake = _stakes[user][term];
Now if a gauge is onboarded again then lastGaugeLoss will be non zero and userStake.lastGaugeLoss will be zero therefore every user will be slashed.
Manual Review
Following changes should be made
bool updateState; lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term); userStake = _stakes[user][term]; if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) { slashed = true; }
Error
#0 - c4-pre-sort
2023-12-29T14:36:49Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-29T14:37:13Z
0xSorryNotSorry marked the issue as duplicate of #1164
#2 - c4-judge
2024-01-28T20:17:14Z
Trumpero marked the issue as satisfactory
🌟 Selected for report: neocrao
Also found by: 0xStalin, Aymen0909, Byteblockers, Chinmay, The-Seraphs, TheSchnilch, Timenov, Varun_05, ether_sky, kaden, mojito_auditor, mussucal, nonseodion, rbserver, santipu_, thank_you, twcctop
30.4141 USDC - $30.41
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/LendingTerm.sol#L324 https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/LendingTerm.sol#L328
In some cases the debtCeiling function might not return the correct value which can cause more issuance of loans of errors in other function which rely on the debtCeiling function like the applyGaugeLoss function in GuilToken Contract.
Following is debtCeiling function
function debtCeiling( int256 gaugeWeightDelta ) public view returns (uint256) { address _guildToken = refs.guildToken; // cached SLOAD uint256 gaugeWeight = GuildToken(_guildToken).getGaugeWeight( address(this) ); gaugeWeight = uint256(int256(gaugeWeight) + gaugeWeightDelta); uint256 gaugeType = GuildToken(_guildToken).gaugeType(address(this)); uint256 totalWeight = GuildToken(_guildToken).totalTypeWeight( gaugeType ); uint256 creditMinterBuffer = RateLimitedMinter(refs.creditMinter) .buffer(); uint256 _hardCap = params.hardCap; // cached SLOAD if (gaugeWeight == 0) { return 0; // no gauge vote, 0 debt ceiling } else if (gaugeWeight == totalWeight) { // one gauge, unlimited debt ceiling // returns min(hardCap, creditMinterBuffer) return _hardCap < creditMinterBuffer ? _hardCap : creditMinterBuffer; } uint256 _issuance = issuance; // cached SLOAD uint256 totalBorrowedCredit = ProfitManager(refs.profitManager) .totalBorrowedCredit(); uint256 gaugeWeightTolerance = ProfitManager(refs.profitManager) .gaugeWeightTolerance(); if (totalBorrowedCredit == 0 && gaugeWeight != 0) { // first-ever CREDIT mint on a non-zero gauge weight term // does not check the relative debt ceilings // returns min(hardCap, creditMinterBuffer) return _hardCap < creditMinterBuffer ? _hardCap : creditMinterBuffer; } uint256 toleratedGaugeWeight = (gaugeWeight * gaugeWeightTolerance) / 1e18; uint256 debtCeilingBefore = (totalBorrowedCredit * toleratedGaugeWeight) / totalWeight; if (_issuance >= debtCeilingBefore) { return debtCeilingBefore; // no more borrows allowed } uint256 remainingDebtCeiling = debtCeilingBefore - _issuance; // always >0 if (toleratedGaugeWeight >= totalWeight) { // if the gauge weight is above 100% when we include tolerance, // the gauge relative debt ceilings are not constraining. return _hardCap < creditMinterBuffer ? _hardCap : creditMinterBuffer; } uint256 otherGaugesWeight = totalWeight - toleratedGaugeWeight; // always >0 uint256 maxBorrow = (remainingDebtCeiling * totalWeight) / otherGaugesWeight; uint256 _debtCeiling = _issuance + maxBorrow; // return min(creditMinterBuffer, hardCap, debtCeiling) if (creditMinterBuffer < _debtCeiling) { return creditMinterBuffer; } if (_hardCap < _debtCeiling) { return _hardCap; } return _debtCeiling; }
As can be seen from the comments it should return min(creditMinterBuffer, hardCap, debtCeiling) but currently it misses the scenario when _hardCap <creditMinterBuffer < _debtCeiling then it should return _hardCap but it returns creditMinterBuffer
Similarly when creditMinterBuffer < _hardCap < _debtCeiling
Manual review
Make the following changes
if (creditMinterBuffer < _debtCeiling) { return creditMinterBuffer <_hardCap ?creditMinterBuffer: _hardCap; } if (_hardCap < _debtCeiling) { return _hardCap < creditMinterBuffer ?_hardCap: creditMinterBuffer; }
Error
#0 - c4-pre-sort
2024-01-01T13:45:32Z
0xSorryNotSorry marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-01-01T13:45:39Z
0xSorryNotSorry marked the issue as duplicate of #878
#2 - c4-pre-sort
2024-01-04T12:45:04Z
0xSorryNotSorry marked the issue as not a duplicate
#3 - c4-pre-sort
2024-01-04T12:45:31Z
0xSorryNotSorry marked the issue as duplicate of #708
#4 - c4-judge
2024-01-30T13:11:23Z
Trumpero marked the issue as satisfactory
#5 - c4-judge
2024-01-31T13:41:25Z
Trumpero changed the severity to 2 (Med Risk)