User's being able to join multiple times on single deposit invlates the reward.
Description
When user has deposited once, they can easily join the event for every country in the list rising their chance of winning the event. But, dilutes the reward and locks the fund inside of the protocol.
-
If more then one player has joined the event and after the winning country is decided user must get entire reward based his shares.
-
However when joining the event, contract does not check whether the user has already the join the evenr or not resulting in same user on single deposit joining multiple times which eventually inflates the rewards resulting in imporoper distribution of shares.
function joinEvent(uint256 countryId) public {
userToCountry[msg.sender] = teams[countryId];
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares;
usersAddress.push(msg.sender);
}
Risk
Likelihood: HIGH
Impact:
-
Improper rewards is received by the winner.
-
Even if the malicious actor does not get anything, funds get locked inside the protocol with no way to exit.
Proof of Concept
Add this test in briVault.t.sol . This POC shows how shares are diluted when a single user on a single deposit is allowed to join the event multiple time
function test_multipleJoinsManipulatesReward() public {
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
for (uint256 i = 0; i < countries.length; ++i) {
briVault.joinEvent(i);
}
vm.stopPrank();
vm.startPrank(user2);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user2);
briVault.joinEvent(2);
vm.stopPrank();
vm.startPrank(user3);
mockToken.approve(address(briVault), 5 ether);
uint256 user3Shares = briVault.deposit(5 ether, user3);
briVault.joinEvent(3);
vm.stopPrank();
vm.warp(eventEndDate + 2 seconds);
vm.prank(owner);
briVault.setWinner(3);
uint256 expectedReward = mockToken.balanceOf(address(briVault));
uint256 withdrawableAmount = assetsToWithdraw(user3Shares);
uint256 user3Before = mockToken.balanceOf(user3);
vm.prank(user3);
briVault.withdraw();
vm.prank(user3);
briVault.withdraw();
uint256 withdrawnAmt = mockToken.balanceOf(user3) - user3Before;
assertEq(withdrawableAmount, withdrawnAmt);
assert(withdrawnAmt != expectedReward);
assertApproxEqAbs(expectedReward, withdrawnAmt, 15e18);
}
Recommended Mitigation
As recommended mitigation do not allow any user to rejoin the event and also if the user address is already in the usersAddress array then don't push it to the array.
+ mapping(address => bool) public hasUserJoined;
+ modifier hasNotJoined(address user) {
+ if (hasUserJoined[user]) {
+ revert AlreadyJoined();
+ }
+ _;
+ hasUserJoined[user] = true; // Set AFTER successful join (avoids partial state)
+ }
- function joinEvent(uint256 countryId))public {
+ function joinEvent(uint256 countryId) public hasNotJoined(msg.sender) {
// ...
userToCountry[msg.sender] = teams[countryId];
...
}
// on cancel allow user to join again
+ function cancelParticipation() public {
// ...
// Reset hasJoin for user to rejoin the event
+ hasUserJoined[msg.sender] = false;
...
}