Open sherlock-admin4 opened 5 months ago
The prize pool must be passed the fee during each individual claim call so it can give the claimer their reward. The fees are calculated before in bulk and then split evenly for each claim in the batch. As the issue demonstrates, it is possible for some of these claims to revert, resulting in less fees collected than if the claimer excluded the reverting claims.
It's important for the claimers to simulate the results before claiming to ensure their claims will result in the expected rewards. Claimers compete to perform claims at the lowest costs, so the bots that simulate results will have higher success rates and be able to claim at lower margins compared to those who don't.
The impact of this issue is very low for competitive bots since claim simulations are commonly available and encouraged.
Hey @nevillehuang, just noticed #57 is incorrectly duplicated with this finding. The submission do not show the fee calculation error, it simply tell that a user could claim the canary tier before prizes are claimed, which will increase the PrizePool.claimedCount counter, which is expected behavior. There is no reference about already claimed prizes which are still counted as claimed by the VRGDA. Sorry for not having noticed it before.
@WangSecurity can you check this please, it has been overlooked, thanks (I've shared 6 days ago that #57 is not a duplicate of this finding)
infect3d
medium
Claimers
can receive lessfeePerClaim
than they should if some prizes are already claimed or if reverts because of a reverting hookSummary
If a claimer propose an array of prizes to claim, but some of these prizes have already been claimed, or some claims revert, then the actual
feePerClaim
received will be less compared to what it should really be, as expected by the VRGDA algorithmThis happens because
_computeFeePerClaim
can undervaluate the value offeePerClaim
, as it compute failed claims as successful ones.This poses an issue, as
feePerClaim
is then used as an input by_vault.claimPrize(_winners[w], _tier, _prizeIndices[w][p], _feePerClaim, _feeRecipient)
, which will transfer the undervaluated fee to the claimer.Vulnerability Detail
Auctions for claiming prizes are based on the VRGDA algorithm In simple terms, this algorithm update price depending on either the numbers of claim is behind or ahead of time schedule. In order to have a schedule, a target of claim per time unit is defined. Just to give an idea, let's simplify that to the extreme (we will see complete formula afterward) and say :
price(t) = claim/expected * targetPrice(t)
E.g: if 10 claims per 2 hours are exepected, then at t=1h, 5 claims should be concluded. If only 4 were claimed, then we can calculate that (4 claims)/(5 expected) < 1, price will be lower that target. if 6 were claimed, then we will have (6 claims)/(5 expected) > 1, price will be greater than target.The formula that has been implemented into
LinearVRGDALib
is the following:With:
n
the number of claim already completedr
the expected rate per hourk
the decay constant (speed at which price will change)p0
the target price (or fee in our case)The more
k *(t - n+1/r) > 0
, the moreprice > p0
Whent = n+1/r
<=>(k * (t - n+1/r)) = 0
, thenprice = p0
The morek * (t - n+1/r) < 0
, the moreprice < p0
We understand that the more whe are behind schedule in term of expected claim, the higher the fees earned by claimer will be. And the more we are ahead of schedule, the lower the fee for claimers will be (as their is no urgency)
Scenario
Now let's see what happens in this scenario:
n = 0
n = 2
Now let's see what happen from a code perspective. The code above is the entry point for claiming prizes. As we can see L113, the
feePerClaim
is computed based on the number of claims to count (_countClaims
) and the number of already claimed prizes (prizePool::claimCount()
) Then, the computedfeePerClaim
value is given to_claim
which actually claim the prizes into the Prize Pool.https://github.com/sherlock-audit/2024-05-pooltogether//blob/main/pt-v5-claimer/src/Claimer.sol#L113
Now, let's see how
_computeFeePerClaim
actually computefeePerClaim
. We see above L230-241 that a fee is calculated for each of the claims of the array, starting at_claimedCount
(The number of prizes already claimed) based on the VRGDA formula L309. The returned value (which is stored intofeePerClaim
) is the averaged fee as shown L241. And as we explained earlier, the higher the number of claim we make, the lower the earned fee are. So, a higher value of_claimedCount + i
will give lower fees.https://github.com/sherlock-audit/2024-05-pooltogether//blob/main/pt-v5-claimer/src/Claimer.sol#L236
What we can see from this, is that the computation will be executed for
_claimCount = 2
and up toi = 5
, so as if there has been 7 claimed prizes, while in reality only 5 prizes are claimed, leading in an undervaluation of the fees to award. As you probably have infered, the computation should have been made fori = 3
to be correct.Impact
The
feePerClaim
computation is incorrect as the VRGDA is calculated for more claims that will really happen, leading to less fee earned by claimers at the time of the call.Code Snippet
https://github.com/sherlock-audit/2024-05-pooltogether//blob/main/pt-v5-claimer/src/Claimer.sol#L113 https://github.com/sherlock-audit/2024-05-pooltogether//blob/main/pt-v5-claimer/src/Claimer.sol#L236
Tool used
Manual Review
Recommendation
The
PrizePool
contract expose a function to check if a prize has already been claimed:wasClaimed
This can be used to countClaims based on the actual true number of claimable prizes from the array.This isn't a "perfect" solution though, as there are still issues when not already claimed prizes revert because of reverting prize hooks. In that case, VRGDA will still count the claim as happening, but we can consider this less likely to happen.