The briVault::joinEvent function lets anyone join the event after depositing assets, by passing their favourite countryId. Moreover, it doesn't restrict anyone from calling this function again, in case someone feels like changing their choice, or wants to deposit some more. And maybe, that's a good feature (hopefully!).
But that's where a critical vulnerability lies. Well, joinEvent does update the user's new country choice on line 257, as well as update the participationShares accordingly on line 260-261. However, it also pushes the user into the briVault::usersAddress array (on line 263), again.
It will create issues later when the owner sets the winner. In case the country selected by the user (who called the joinEvent() again, or multiple times) wins, then their participationShares will be added multiple times to totalWinnerShares in the _getWinnerShares() function.
This will lead to totalWinnerShares value inflated more than it should, and as it acts like a denominator during the time of withdrawal, the winners will be receiving much less than they actually should.
Likelihood: High/Medium
It's easy to make such a mistake by any user, but not every user will make such a decision to deposit again
No restriction on calling joinEvent multiple times tho
Impact: High
Trims down every winner's payout, almost bringing the system to its knees.
It's like, everyone pays the price for one user's silly mistake.
Vault keeps excess funds locked
Here's how this vulnerability unfolds:
Total 4 users: user1, user2, user3, and user4
First 3 users deposit 5e18 tokens, and join the event with countryId - 10, 11, 47, respectively.
Then the 4th user, i.e. user4 deposits 5e18 tokens and calls joinEvent(47).
After a while, something came to his mind, and he thought of depositing 5e18 more tokens. And then he called joinEvent(47) again, in order to get the newly added tokens recorded under userSharesToCountry.
However, this proved harmful as user4 as well as his shares are counted two times, thus increasing the value of totalWinnerShares that acts as a denominator to distribute assets at the time of withdrawal.
Due to this, both user3 and user4 receive much less in assets than they should. Overall, they are at a loss despite being winners.
And the leftover assets are clearly stuck in the contract...
Here's the test_UserCanJoinMultipleTimes test, demonstrating the above thing. However, to make the best use of this test, follow these steps:
First, run this test as it is and observe the logs.
Next, comment out the lines where the 2nd deposit and joinEvent call are made by user4. Looks like this:
Compare both logs.
Add it in briVault.t.sol:
Run it using the following command:
Logs
On the default form of test:
When the 2nd deposit of user4 is commented out:
The outputs clearly show:
| Scenario | User3 Change | User4 Change | totalWinnerShares |
|---|---|---|---|
| Single Join | +4.85e18 | +4.85e18 | 9.85e18 (real) |
| Multiple Joins | –0.075e18 | –0.15e18 | 24.625e18 (inflated) |
Totally depends on how the protocol wants to fix it; there are two options:
Literally deny any user calling the joinEvent function again. Can be done in such a way:
Or, if the protocol wants the users to call joinEvent as many times as they want, such that users can change the choice if needed, then add a user to usersAddress for just one time. Can be done in such a way:
The _getWinnerShares() function is intended to iterate through all users and sum their shares for the winning country, returning the total.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.