Platform: Code4rena
Start Date: 03/05/2023
Pot Size: $60,500 USDC
Total HM: 25
Participants: 114
Period: 8 days
Judge: Picodes
Total Solo HM: 6
Id: 234
League: ETH
Rank: 108/114
Findings: 1
Award: $15.58
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: aviggiano
Also found by: 0xSmartContract, 0xTheC0der, 0xcm, ABAIKUNANBAEV, Audinarey, Audit_Avengers, BGSecurity, Bauchibred, Dug, Evo, Haipls, Jerry0x, TS, bytes032, devscrooge, kenta, ladboy233, mrvincere, patitonar, sakshamguruji, tsvetanovv
15.5756 USDC - $15.58
https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-core/src/RewardsManager.sol#L106-L125 https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-core/src/RewardsManager.sol#L553-L598 https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-core/src/RewardsManager.sol#L806-L821 https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-core/src/RewardsManager.sol#L377-L414
In RewardsManager.sol
we have claimRewards() function:
function claimRewards( Â Â Â Â uint256 tokenId_, Â Â Â Â uint256 epochToClaim_ Â Â ) external override { Â Â Â Â StakeInfo storage stakeInfo = stakes[tokenId_]; Â Â Â Â if (msg.sender != stakeInfo.owner) revert NotOwnerOfDeposit(); Â Â Â Â if (isEpochClaimed[tokenId_][epochToClaim_]) revert AlreadyClaimed(); Â Â Â Â _claimRewards(stakeInfo, tokenId_, epochToClaim_, true, stakeInfo.ajnaPool); Â Â }
This function allows the owner of a stake, to claim the rewards for a specific epoch.
First check if the msg.sender
is the owner of the stake, after this checks if the rewards for the specified epoch have already been claimed by the stake owner and call _claimRewards() function:
function _claimRewards(     StakeInfo storage stakeInfo_,     uint256 tokenId_,     uint256 epochToClaim_,     bool validateEpoch_,     address ajnaPool_   ) internal {     // revert if higher epoch to claim than current burn epoch     if (validateEpoch_ && epochToClaim_ > IPool(ajnaPool_).currentBurnEpoch()) revert EpochNotAvailable();     // update bucket exchange rates and claim associated rewards     uint256 rewardsEarned = _updateBucketExchangeRates(       ajnaPool_,       positionManager.getPositionIndexes(tokenId_)     );     rewardsEarned += _calculateAndClaimRewards(tokenId_, epochToClaim_);     uint256[] memory burnEpochsClaimed = _getBurnEpochsClaimed(       stakeInfo_.lastClaimedEpoch,       epochToClaim_     );     emit ClaimRewards(       msg.sender,       ajnaPool_,       tokenId_,       burnEpochsClaimed,       rewardsEarned     );     // update last interaction burn event     stakeInfo_.lastClaimedEpoch = uint96(epochToClaim_);     // transfer rewards to sender     _transferAjnaRewards(rewardsEarned);   }
And _claimRewards()
call _transferAjnaRewards():
function _transferAjnaRewards(uint256 rewardsEarned_) internal {     // check that rewards earned isn't greater than remaining balance     // if remaining balance is greater, set to remaining balance     uint256 ajnaBalance = IERC20(ajnaToken).balanceOf(address(this));     if (rewardsEarned_ > ajnaBalance) rewardsEarned_ = ajnaBalance;     if (rewardsEarned_ != 0) {       // transfer rewards to sender       IERC20(ajnaToken).safeTransfer(msg.sender, rewardsEarned_);     }   }
_transferAjnaRewards()
finally transfers rewards tokens.
The problem here is that it is possible for a user to claim their tokens and receive only a portion of the rewards. Imagine the following situation:
claimRewards()
and take her rewards. For example 2000 tokens._claimRewards
._claimRewards
calculates the rewards in the variable rewardsEarned
_calculateAndClaimRewards()
updated the map that the prizes are taken: isEpochClaimed[tokenId_][epoch] = true;
_claimRewards
call _transferAjnaRewards()
._transferAjnaRewards()
check that rewards earned isn't greater than remaining balance and if remaining balance is greater, set to remaining balance:if (rewardsEarned_ > ajnaBalance) rewardsEarned_ = ajnaBalance;
ajnaBalance
has only 500 tokens and sets rewardsEarned_
to 500 and Alice lost 1500 tokens.claimRewards()
but is stopped at the second check :if (isEpochClaimed[tokenId_][epochToClaim_]) revert AlreadyClaimed();
Manual Review
This doesn't seem very fair to me, and I think rewards should be used for each user in a mapping and tracked if there are rewards left to take and allow a user to take them instead of losing them.
Other
#0 - c4-judge
2023-05-18T14:14:24Z
Picodes marked the issue as duplicate of #361
#1 - c4-judge
2023-05-29T20:55:44Z
Picodes changed the severity to 3 (High Risk)
#2 - c4-judge
2023-05-29T21:00:20Z
Picodes marked the issue as satisfactory