BriVault

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

Gas Limit Vulnerability in Winner Shares Calculation Leading to Contract Lockup

[M-1] Gas Limit Vulnerability in Winner Shares Calculation Leading to Contract Lockup

Description

The _getWinnerShares function performs an unbounded iteration through all user addresses to calculate total winner shares. For large tournaments with many participants, this operation will exceed block gas limits, causing the setWinner function to consistently revert, permanently preventing winner selection.

function _getWinnerShares () internal returns (uint256) {
for (uint256 i = 0; i < usersAddress.length; ++i){
address user = usersAddress[i];
totalWinnerShares += userSharesToCountry[user][winnerCountryId];
}
return totalWinnerShares;
}

Risk

Likelihood:

  • This vulnerability will occur once the number of participants in the tournament exceeds approximately 2,000-5,000 users, triggering an inevitable gas limit error when the owner attempts to set the winner.

  • Tournament betting systems typically aim to attract large numbers of participants, making it almost certain this threshold will be reached in a successful deployment.

Impact:

Once the usersAddress array exceeds ~2,000-5,000 entries (depending on gas limits), the setWinner function becomes permanently unusable

  • This creates a systemic contract lockup where no winner can ever be selected

  • All user funds become permanently trapped in the contract, as withdrawals require winner selection

  • This vulnerability is guaranteed to trigger in popular tournaments with moderate participation

Proof of Concept

The PoC demonstrates how the contract fails when too many users join:

This occurs because _getWinnerShares() must iterate through all participants (5,000) to calculate total shares for the winning team, exceeding Ethereum's block gas limit. Once this happens, the contract is permanently locked - no winner can be set, and no user can withdraw funds.

// Simulate a popular tournament with many participants
for (uint256 i = 0; i < 5000; i++) {
address user = address(uint160(i + 1));
// Fund user address with tokens and ETH
token.transfer(user, 1000 ether);
// Have each user join the tournament
vm.startPrank(user);
token.approve(address(vault), 1000 ether);
vault.deposit(1000 ether, user);
vault.joinEvent(i % 48); // Select teams evenly
vm.stopPrank();
}
// Tournament ends, owner attempts to set winner
vm.startPrank(owner);
vault.setWinner(5); // REVERTS: "out of gas"
vm.stopPrank();
// All user funds are now permanently locked

Recommended Mitigation

The fix eliminates the unbounded loop in _getWinnerShares() by tracking shares incrementally:

This approach prevents gas limit issues by:

  1. Calculating totals incrementally during join/cancel operations

  2. Removing the O(n) iteration through all users

  3. Making share calculations constant time regardless of participant count

// Add a mapping to track total shares by country
mapping(uint256 => uint256) public totalSharesByCountry;
function joinEvent(uint256 countryId) public {
// [existing validation code]
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares;
// Incrementally update shares by country
totalSharesByCountry[countryId] += participantShares;
// [rest of function]
}
function _getWinnerShares() internal view returns (uint256) {
// Simply return the pre-calculated total, no iteration needed
return totalSharesByCountry[winnerCountryId];
}
// When users cancel, also update the shares tracking
function cancelParticipation() public {
// [existing checks]
uint256 countryId = userToCountryId[msg.sender];
uint256 userShares = balanceOf(msg.sender);
// Decrement the country's total shares
totalSharesByCountry[countryId] -= userShares;
// [rest of function]
}
Updates

Appeal created

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

Unbounded Loop in _getWinnerShares Causes Denial of Service

The _getWinnerShares() function is intended to iterate through all users and sum their shares for the winning country, returning the total.

Support

FAQs

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

Give us feedback!