BriVault

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

Unbounded Loop Causes Permanent DoS and Locked Funds

Root + Impact

Description

  • The contract maintains a dynamic list of all participants in usersAddress. When the winner is set, the contract computes the total winning stake:

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

  • This loop runs on-chain inside setWinner():

    function setWinner(uint256 countryIndex) public onlyOwner {
    ...
    @> _getWinnerShares(); // unbounded loop over usersAddress
    }

  • Since usersAddress grows without limit, a sufficiently large number of participants will push setWinner() over the block gas limit, causing it to revert permanently.

  • When this happens:

    • winner is never set,

    • withdraw() cannot be used by anyone,

    • all assets become permanently locked in the vault.

  • This is a Permanent Denial of Service affecting all users.

Risks

Likelihood: Medium/Low

  • Chances of a protocol getting users in such a massive amount ain't that easy, unless it becomes way more popular.

  • Even one attacker can mass-join using cheap addresses to force setWinner to revert.

Impact: High

  • Winner cannot be finalized.

  • No one can withdraw.

  • All funds become permanently stuck.

  • Protocol becomes unusable.

Proof of Concept

  • Add test_gasLimit to briVault.t.sol:

    function test_gasLimit() public {
    // setup
    vm.prank(owner);
    briVault.setCountry(countries);
    // around 5618 users depositing and joining the event
    for (uint256 i = 1; i < 5619; i++) {
    address participant = address(uint160(i));
    mockToken.mint(participant, 6 ether);
    vm.startPrank(participant);
    mockToken.approve(address(briVault), type(uint256).max);
    briVault.deposit(5 ether, participant);
    briVault.joinEvent(10);
    vm.stopPrank();
    }
    // Event starts
    vm.warp(eventStartDate + 1);
    uint256 gasStart = gasleft();
    // Event ends, and owner sets the winner
    vm.warp(eventEndDate + 1);
    vm.prank(owner);
    briVault.setWinner(10); // Reverts here!!!
    uint256 gasEnds = gasleft();
    console.log("Gas consumed:", gasStart - gasEnds);
    }

  • Run the test using the following command:

    forge test --mt test_gasLimit -vv

  • Logs:

    Ran 1 test for test/briVault.t.sol:BriVaultTest
    [FAIL: EvmError: Revert] test_gasLimit() (gas: 1073607798)
    Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 581.31ms (578.50ms CPU time)
    Ran 1 test suite in 583.81ms (581.31ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • Implement a limit on how many users can participate such that the gas limit doesn't reach to the full. And use the existing numberOfParticipants variable.

    + uint256 constant MAX_PARTICIPANTS = 4000;
    // ...
    function joinEvent(uint256 countryId) public {
    // ...
    + require(numberOfParticipants < MAX_PARTICIPANTS, "too many participants");
    // ...
    }

  • Store totalParticipantShares by country at join time:

    + mapping(uint256 => uint256) totalSharesForCountry;
    function joinEvent(uint256 countryId) public {
    uint256 participantShares = balanceOf(msg.sender);
    + totalSharesForCountry[countryId] += participantShares;
    ...
    }
    function _getWinnerShares() internal returns (uint256) {
    - expensive loop
    + return totalSharesForCountry[winnerCountryId];
    }
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!