BriVault

First Flight #52
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

setCountry() callable anytime enables post-join team manipulation

Root + Impact

setCountry() can be called after event creation and user participation causes critical mapping inconsistencies and potential result manipulation

Description

  • Normally, the setCountry() function should only be callable once, before users start joining the event. This ensures that the teams array remains constant and that the mapping between team indices (countryId) and names stays consistent throughout the tournament lifecycle.

  • However, the current implementation allows the owner to call setCountry() multiple times and at any moment, even after participants have joined and selected their teams.

  • Since users’ selections are stored as strings (userToCountry[msg.sender] = teams[countryId]), modifying the teams array after players have joined can invalidate or alter existing mappings — causing users’ stored “countries” to no longer match their intended bets or the actual winning country.

function setCountry(string[48] memory countries) public onlyOwner {
@> // ❌ No restriction on when this function can be called
@> // Owner can call it anytime, even after participants joined
@> // This allows overwriting 'teams' array and corrupting mappings
for (uint256 i = 0; i < countries.length; ++i) {
@> teams[i] = countries[i]; // overwrites existing team names and order
}
emit CountriesSet(countries);
}

Risk

Likelihood:

  • The issue occurs whenever the owner mistakenly or maliciously calls setCountry() after participants have joined or after the event has started.

  • Because there is no timestamp or state restriction (eventStartDate / countriesSet flag), this can happen in any real deployment

Impact:

  • If the owner modifies teams after users have joined, the countryIdteamName mapping changes, breaking stored userToCountry references.

  • When the winner is later set, comparisons are performed using string equality (keccak256(userToCountry) == keccak256(winner)), leading to false negatives or false positives: legitimate winners may lose rewards or losers may withdraw as winners.

  • This destroys the fairness and integrity of the entire vault system.

Proof of Concept

function setCountry(string[48] memory countries) public onlyOwner {
for (uint256 i = 0; i < countries.length; ++i) {
teams[i] = countries[i];
}
emit CountriesSet(countries);
}
// Example exploit flow:
//
// 1. Owner calls setCountry() → teams[0] = "USA", teams[1] = "Germany".
// 2. User A joins with countryId = 0 → userToCountry[userA] = "USA".
// 3. Owner calls setCountry() again → teams[0] = "France", teams[1] = "Brazil".
// 4. User A still holds "USA" internally, but countryId 0 now means "France".
// 5. When the owner sets winnerCountryId = 0 (now "France"),
// the system compares strings and fails: User A (who bet on “USA”) loses unfairly.

Recommended Mitigation

Prevent the setCountry() function from being called after the event starts or once it has been called once.

  • Replace userToCountry (string-based) with userToCountryId (uint256-based) for efficient and reliable matching.

  • Compare country IDs directly during withdrawal (userToCountryId[msg.sender] == winnerCountryId) instead of comparing strings.

+ bool public countriesSet;
function setCountry(string[48] memory countries) public onlyOwner {
+ require(!countriesSet, "Countries already set");
+ if (block.timestamp >= eventStartDate) {
+ revert eventStarted();
+ }
for (uint256 i = 0; i < countries.length; ++i) {
teams[i] = countries[i];
}
+ countriesSet = true;
emit CountriesSet(countries);
}
- mapping (address => string) public userToCountry;
+ mapping (address => uint256) public userToCountryId;
function joinEvent(uint256 countryId) public {
...
- userToCountry[msg.sender] = teams[countryId];
+ userToCountryId[msg.sender] = countryId;
}
function withdraw() external winnerSet {
...
- if (keccak256(abi.encodePacked(userToCountry[msg.sender])) != keccak256(abi.encodePacked(winner))) {
- revert didNotWin();
- }
+ if (userToCountryId[msg.sender] != winnerCountryId) {
+ revert didNotWin();
+ }
}
Updates

Appeal created

bube Lead Judge 20 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

setCountry() Can Be Called After Users Join

This is owner action.

Support

FAQs

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

Give us feedback!