Description
-
A tournament vault should be able to run multiple rounds (seasons/tournaments) over time: open deposits before each event, finalize winners, let participants withdraw, then start a new event with fresh state (dates, participants, shares accounting, winner data).
-
The current contract hard‑codes a single lifecycle with no way to reset or roll over the vault to a new round. After the first event:
deposit() is permanently blocked by eventStarted() because eventStartDate remains in the past.
setWinner() can be called only once due to WinnerAlreadySet.
State like winner, _setWinner, totalWinnerShares, usersAddress, numberOfParticipants, snapshots, etc., persists with no reset path.
The system therefore cannot host a second betting round without redeploying a new contract, and any attempt to reuse the same instance conflicts with stale state.
uint256 public eventStartDate;
uint256 public eventEndDate;
bool public _setWinner;
string public winner;
uint256 public totalWinnerShares;
address[] public usersAddress;
uint256 public numberOfParticipants;
function deposit(uint256 assets, address receiver) public override returns (uint256) {
...
if (block.timestamp >= eventStartDate) {
revert eventStarted();
}
...
}
function setWinner(uint256 countryIndex) public onlyOwner returns (string memory) {
...
if (_setWinner) {
revert WinnerAlreadySet();
}
...
}
Risk
Likelihood: High
After the first tournament ends and participants withdraw, operators will attempt to run a second event with the same contract address for continuity. With no reset, this occurs naturally in production operations.
Impact: High
-
Operational dead‑end: Further deposits are impossible; setWinner cannot be called again; the vault becomes a one‑shot contract, forcing redeployment for every event (loss of integrations, liquidity fragmentation).
-
State contamination: Even if deposits could be reopened by changing timestamps (not possible today), stale usersAddress, totalWinnerShares, and snapshots would corrupt the next round’s accounting.
Proof of Concept
pragma solidity ^0.8.24;
import {Test} 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 SingleRoundOnlyTest is Test {
BriVault vault;
MockERC20 token;
address owner = makeAddr("owner");
address user = makeAddr("user");
address fee = makeAddr("fee");
string[48] countries;
uint256 start;
uint256 end;
function setUp() public {
start = block.timestamp + 2 days;
end = start + 30 days;
token = new MockERC20("Mock", "M");
token.mint(user, 10 ether);
vm.startPrank(owner);
vault = new BriVault(IERC20(address(token)), 150, start, fee, 0.0002 ether, end);
countries[10] = "Japan";
vault.setCountry(countries);
vm.stopPrank();
vm.startPrank(user);
token.approve(address(vault), type(uint256).max);
vault.deposit(5 ether, user);
vm.stopPrank();
vm.warp(end + 1);
vm.prank(owner);
vault.setWinner(10);
}
function test_CannotStartSecondRound() public {
vm.prank(user);
vm.expectRevert(abi.encodeWithSignature("eventStarted()"));
vault.deposit(5 ether, user);
vm.prank(owner);
vm.expectRevert(abi.encodeWithSignature("WinnerAlreadySet()"));
vault.setWinner(10);
}
}
Recommended Mitigation
+ error eventNotFinalized();
+ error invalidDates();
+ event GameReset(uint256 newStart, uint256 newEnd);
// After withdrawals are done for the previous round, owner resets state.
+ function resetGame(
+ uint256 newEventStartDate,
+ uint256 newEventEndDate,
+ string[48] calldata newCountries
+ ) external onlyOwner {
+ // Require last game is finalized
+ if (!_setWinner) revert eventNotFinalized();
+ if (newEventStartDate == 0 || newEventEndDate <= newEventStartDate) revert invalidDates();
+
+ // --- Clear per-round state ---
+ winner = "";
+ winnerCountryId = 0;
+ _setWinner = false;
+ finalizedVaultAsset = 0;
+
+ // Reset aggregates
+ totalWinnerShares = 0;
+ totalParticipantShares = 0;
+ numberOfParticipants = 0;
+
+ // Clear participants list (O(n)); acceptable for moderate sizes,
+ // otherwise prefer the roundId approach below.
+ while (usersAddress.length > 0) {
+ usersAddress.pop();
+ }
+
+ // Clear user snapshots (best replaced by roundId scoping; see Option B).
+ // For a simple reset, you could iterate and clear userSharesToCountry for known participants.
+ // (O(n * countries)); consider the roundId approach to avoid this loop.
+ // Set new dates
+ eventStartDate = newEventStartDate;
+ eventEndDate = newEventEndDate;
+ // Set / replace countries
+ for (uint256 i = 0; i < newCountries.length; ++i) {
+ teams[i] = newCountries[i];
+ }
+ emit CountriesSet(newCountries);
+ emit GameReset(newEventStartDate, newEventEndDate);
+ }