An attacker can exploit the missing access control in joinEvent() (see H-01) to bloat the usersAddress array by calling the function thousands of times. This creates a Denial of Service (DoS) vector where the setWinner() function becomes so gas-intensive that it exceeds the block gas limit, making it impossible to set a winner and permanently locking all funds in the contract.
The root cause is the unbounded loop in _getWinnerShares(), which is called by setWinner(). This loop iterates through the entire usersAddress array to calculate the total shares of winning users:
By maliciously adding thousands of duplicate entries to the usersAddress array via repeated joinEvent() calls, an attacker can make the gas cost of this loop exceed the block gas limit (typically 30 million gas on Ethereum mainnet).
Note: There is no maximum participant limit or check to prevent array bloating.
Likelihood: High
The vulnerability is easily exploitable by any user who has deposited funds. The attack requires calling joinEvent() approximately 10,000+ times, which can be automated in a script. The attack cost is relatively low (~0.5-1 ETH in gas fees), but the damage is catastrophic and permanent.
Impact: High
The attacker can:
Permanently lock all funds: Once the usersAddress array is bloated, setWinner() will always exceed the block gas limit
Cause total protocol failure: No winner can ever be set, so no user can withdraw their funds
No recovery mechanism: There is no function to bypass setWinner() or recover funds
This results in a permanent loss of access to all deposited funds for all users, including the attacker. While this is a griefing attack (the attacker also loses their deposit), the impact is severe enough to warrant High severity.
The exploit was confirmed using a Foundry test that demonstrates the gas cost of setWinner() growing linearly with the number of joinEvent() calls.
Setup:
The BriVault contract is deployed with a tournament event
An attacker deposits 10 ETH
Attack:
The attacker calls joinEvent() 50 times (cycling through valid team IDs)
This bloats the usersAddress array to 50 entries (all the same address)
Result:
numberOfParticipants: 50
Gas cost for setWinner() with 50 entries: 166,180 gas
Projected gas cost with 10,000 entries: ~33.2 million gas (exceeds 30M block limit)
Attack cost: ~0.5-1 ETH in gas fees
Impact: All funds permanently locked
Gas Analysis:
| Number of Calls | Gas Cost for setWinner() | Status |
|---|---|---|
| 50 | 166,180 | Callable |
| 1,000 | ~3,320,000 | Callable |
| 10,000 | ~33,200,000 | Exceeds 30M block limit |
Supporting Code:
Test Results:
The primary fix is to prevent multiple joinEvent() calls (see H-01 mitigation). Additionally, implement a maximum participant limit as a failsafe:
This ensures that:
Each user can only join once (prevents array bloating)
The total number of participants is capped (failsafe against DoS)
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.