The user guild amount
is not updated if the mintRatio
is updated, causing users to get more rewards in the SurplusGuildMinter
contract
#1160
Labels
bug
Something isn't working
downgraded by judge
Judge downgraded the risk level of this issue
duplicate-937
grade-a
high quality report
This report is of especially high quality
QA (Quality Assurance)
Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax
Lines of code
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/SurplusGuildMinter.sol#L319
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/SurplusGuildMinter.sol#L293
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/SurplusGuildMinter.sol#L216
Vulnerability details
Impact
The user can stake credit tokens using the SurplusGuildMinter contract. The guildAmount to mint is calculated using the mintRatio and the credit tokens amount:
Then based on the calculated guildAmount, the corresponding rewards can be obtained for the user code line 250:
The issue arises when the
mintRatio
is updated, causing the user's rewards to not be calculated correctly because theguildAmount
is not updated based on the newmintRatio
. The SurplusGuildMinter::updateMintRatio() function helps to update the new user's guildAmount based on the newmintRatio
however this function is not required to be called by the user and there are not comments/doc which mention thatupdateMintRatio()
will be called to all users after amintRatio
change.In the other hand there is a comment that mentions the following:
That is, when
rewardRatio
is changed with the function SurplusGuildMinter::setRewardRatio(), it is necessary to call the rewards of all users via the SurplusGuildMinter::getRewards() function, however nothing is mentioned whenmintRatio
is updated.Proof of Concept
Please consider the next scenario where the
mintRatio
increases from2e18
to3e18
:userA
stakes10e18 creditTokens
and20e18 guild tokens
are mintedguildAmount = (_mintRatio * amount) / 1e18; = (2e18 * 10e18) / 1e18 = 20e18
mintRatio
is updated via setMintRatio() tomintRatio=3e18
.userA
stakes another10e18 creditTokens
and30e18 guild tokens
are minted, based on the formulaguildAmount = (_mintRatio * amount) / 1e18; = (3e18 * 10e18) / 1e18 = 30e18
. Now he has20e18 + 30e18 = 50e18 guildTokens
.That is incorrect because
userA
should have60e18 guildTokens
insted of50e18 guildTokens
. Since the newmintRatio is 3e18
and the user has staked20e18 creditTokens
, the guild amount should beguildAmount = (mintRatio * amount) / 1e18; = (3e18 * 20e18) / 1e18 = 60e18
. I created the following test where the correct guild amount is updated once the SurplusGuildMinter::updateMintRatio() is called the problem is that theupdateMintRatio()
function is not forced to be called and the user can get more rewards than it should be.Consider the next scenario where the
mintRatio
decreases from3e18
to2e18
:userA
stakes10e18 creditTokens
and30e18 guild tokens
are minted.guildAmount = (_mintRatio * amount) / 1e18; = (3e18 * 10e18) / 1e18 = 30e18
mintRatio
is updated tomintRatio=2e18
.userA
claims rewards via the SurplusGuildMinter::getRewards() function using a not updatedguildAmount=30e18
In the above scenario,
userA
will get more rewards because rewards will be calculated usingguildAmount=30e18
, that's is incorrect because the newmintRatio is 2e18
soguildAmount = (_mintRatio * amount) / 1e18; = (2e18 * 10e18) / 1e18 = 20e18
Tools used
Manual review
Recommended Mitigation Steps
The SurplusGuildMinter::updateMintRatio() function is not enforced to be called once the
mintRatio
is changed, so the recommendation is to update theguildAmount
if the mintRatio has changed at the end of the SurplusGuildMinter::getRewards() function, so now the user is forced to update theguildAmount
andmintRatio
and get fair rewards:File: SurplusGuildMinter.sol 216: function getRewards( 217: address user, 218: address term 219: ) 220: public 221: returns ( 222: uint256 lastGaugeLoss, // GuildToken.lastGaugeLoss(term) 223: UserStake memory userStake, // stake state after execution of getRewards() 224: bool slashed // true if the user has been slashed 225: ) 226: { 227: bool updateState; 228: lastGaugeLoss = GuildToken(guild).lastGaugeLoss(term); 229: if (lastGaugeLoss > uint256(userStake.lastGaugeLoss)) { 230: slashed = true; 231: } 232: 233: // if the user is not staking, do nothing 234: userStake = _stakes[user][term]; 235: if (userStake.stakeTime == 0) 236: return (lastGaugeLoss, userStake, slashed); 237: 238: // compute CREDIT rewards 239: ProfitManager(profitManager).claimRewards(address(this)); // this will update profit indexes 240: uint256 _profitIndex = ProfitManager(profitManager) 241: .userGaugeProfitIndex(address(this), term); 242: uint256 _userProfitIndex = uint256(userStake.profitIndex); 243: 244: if (_profitIndex == 0) _profitIndex = 1e18; 245: if (_userProfitIndex == 0) _userProfitIndex = 1e18; 246: 247: uint256 deltaIndex = _profitIndex - _userProfitIndex; 248: 249: if (deltaIndex != 0) { 250: uint256 creditReward = (uint256(userStake.guild) * deltaIndex) / 251: 1e18; 252: uint256 guildReward = (creditReward * rewardRatio) / 1e18; 253: if (slashed) { 254: guildReward = 0; 255: } 256: 257: // forward rewards to user 258: if (guildReward != 0) { 259: RateLimitedMinter(rlgm).mint(user, guildReward); 260: emit GuildReward(block.timestamp, user, guildReward); 261: } 262: if (creditReward != 0) { 263: CreditToken(credit).transfer(user, creditReward); 264: } 265: 266: // save the updated profitIndex 267: userStake.profitIndex = SafeCastLib.safeCastTo160(_profitIndex); 268: updateState = true; 269: } 270: 271: // if a loss occurred while the user was staking, the GuildToken.applyGaugeLoss(address(this)) 272: // can be called by anyone to slash address(this) and decrement gauge weight etc. 273: // The contribution to the surplus buffer is also forfeited. 274: if (slashed) { 275: emit Unstake(block.timestamp, term, uint256(userStake.credit)); 276: userStake = UserStake({ 277: stakeTime: uint48(0), 278: lastGaugeLoss: uint48(0), 279: profitIndex: uint160(0), 280: credit: uint128(0), 281: guild: uint128(0) 282: }); 283: updateState = true; 284: } 285: 286: // store the updated stake, if needed 287: if (updateState) { 288: _stakes[user][term] = userStake; 289: } 290: } ... ++ if (user.mintRatio != mintRatio) updateMintRatio(user, term); }
Assessed type
Context
The text was updated successfully, but these errors were encountered: