The BriVault::_getWinnerShares() function loops through the entire usersAddress array to compute the total shares of users who selected the winning country:
This loop is unbounded, meaning its cost grows linearly with the number of participants.
The problem is that _getWinnerShares() is called inside setWinner(), which must succeed before withdrawals are allowed. If the number of participants becomes large (thousands+), this loop will exceed the block gas limit, causing setWinner() to always revert.
Since the vault relies on _setWinner == true before users can withdraw, this design allows a single attacker to force a permanent DoS simply by injecting a large number of entries into the vault.
Likelihood:
Reason 1: This issue occurs once the participants list grows large enough that iterating over usersAddress exceeds the block gas limit during setWinner(), making the function revert consistently.
Reason 2: This situation arises whenever an attacker (or organic user growth) submits a high number of deposits and joins the event repeatedly, since there is no cap or restriction on how many entries can be added.
Impact: Admin cannot call setWinner() once the participants list grows large → function becomes uncallable due to gas limit.
_setWinner never becomes true.
withdraw() requires winnerSet modifier:
This directly leads to a High-severity Denial-of-Service where the contract cannot progress to the withdrawal phase, completely breaking core protocol functionality.
| Number of participants | Gas used by setWinner() |
|---|---|
| ~1000 users | ≈ 0.9 – 1.1 million gas |
| ~2000 users | ≈ 1.8 – 2.0 million gas |
| ~3000 users | ≈ 2.7 – 3.0 million gas |
| ~4000 users | ≈ 3.6 – 4.0 million gas |
There are a few recomendations.
Impose participation limits / block Sybil spam, But this alone won’t fully solve scalability issues.
Use a mapping countryId → totalShares
Use pull-based withdrawal math
Every user computes their own proportional reward without global aggregation.
BriVault::_getWinnerShares() causes permanent Denial-of-Service (DoS), preventing winner finalization and blocking withdrawalsLikelihood:
Impact:
balanceOf) allows asset inflation before finalization — winners can be overpaid (Inflation / payout manipulation)BriVault._setFinallizedVaultBalance() uses the raw token balance of the contract to record the final vault assets:
Because finalizedVaultAsset is taken from ERC20.balanceOf(address(this)), anyone can inflate this value by transferring tokens directly to the vault before setWinner() finalises the event. withdraw() uses finalizedVaultAsset to compute user payouts:
This makes payouts manipulable by external transfers (inflation attack): attackers or benign third parties can send tokens to the vault immediately before finalization so winners receive a larger share than entitled.
Likelihood:
Reason 1 — The vault accepts ERC20 tokens and there is no restriction preventing arbitrary transfers to the vault address. External token transfers to the contract can occur at any time prior to finalization.
Reason 2 — Finalization is performed once by the owner; this single point in time is a realistic window for an attacker to cheaply deposit tokens to inflate balanceOf
immediately before setWinner().
Impact:
Impact 1 — Winners receive inflated payouts: external token transfers directly increase finalizedVaultAsset, so winners can be overpaid and honest participants or protocol funds are diverted.
Impact 2 — Protocol economics are broken (incorrect distribution), reputation and funds at risk; if the inflation is manipulative, owner or participants may lose funds relative to intended distribution.
High-level steps (manual / script PoC):
Deploy BriVault and have several participants deposit and join the event. Example: A, B, C deposit normal amounts and join. totalWinnerShares is computed on finalization based on shares assigned earlier.
Before the owner calls setWinner(...) (but after eventEndDate), attacker sends X tokens to the vault directly:
This increases IERC20(asset).balanceOf(address(briVault)) by X.
Owner calls setWinner(countryIndex). _setFinallizedVaultBalance() reads the inflated balance and records finalizedVaultAsset = original_pool + X.
Winners call withdraw(); each winner receives shares / totalWinnerShares * finalizedVaultAsset, i.e. an inflated payout that includes X.
This confirms that winners were paid from an inflated vault asset pool.
Use tracked vault accounting instead of relying on raw ERC20 balance:
Use ERC4626 totalAssets() if properly implemented and not based on balanceOf.
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.