BriVault

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

HIGH-02: setCountry Allows Duplicate Team Names

Root + Impact

The setCountry() function has no validation to prevent duplicate team names in the teams array, which breaks the winner determination logic and causes incorrect payouts.

Description

Normal behavior expects each team in a tournament to have a unique identifier. Users select teams by index, and the winner is determined by matching team names.

The current implementation allows the owner to set duplicate team names in different indices. This causes multiple indices to match the winner string, breaking the assumption that each team is unique.

function setCountry(string[48] memory countries) public onlyOwner {
// @> No validation for duplicate names
for (uint256 i = 0; i < countries.length; ++i) {
teams[i] = countries[i];
}
emit CountriesSet(countries);
}
function joinEvent(uint256 countryId) public {
// ... validation ...
// @> User selects by index
userToCountry[msg.sender] = teams[countryId];
userSharesToCountry[msg.sender][countryId] = participantShares;
// ... rest ...
}
function setWinner(
uint256 countryIndex
) public onlyOwner returns (string memory) {
// ... validation ...
// @> Winner set by index
winner = teams[countryIndex];
// ... rest ...
}
function withdraw() external winnerSet {
// ... validation ...
// @> Matches by STRING comparison
// @> If multiple indices have same string, logic breaks
if (
keccak256(abi.encodePacked(userToCountry[msg.sender])) !=
keccak256(abi.encodePacked(winner))
) {
revert didNotWin();
}
// ... rest ...
}

Risk

Likelihood:

  • Owner can accidentally set duplicates due to typos or copy-paste errors

  • No on-chain validation prevents this during setup

  • Easy to make mistake when setting 48 team names manually

Impact:

  • If teams[0] = "TeamA" and teams[5] = "TeamA" (duplicate)

  • Users joining index 0 set userSharesToCountry[user][0] = shares

  • Users joining index 5 set userSharesToCountry[user][5] = shares

  • Owner sets winner to index 0: winner = "TeamA"

  • _getWinnerShares() counts shares for BOTH index 0 and 5

  • totalWinnerShares is artificially inflated

  • Both groups can withdraw but payouts are diluted incorrectly

  • Creates confusion about which "TeamA" actually won

  • Breaks tournament fairness and integrity

Proof of Concept

// Owner sets up teams with duplicate
string[48] memory teams;
teams[0] = "TeamA";
teams[1] = "TeamB";
teams[2] = "TeamA"; // Duplicate!
vault.setCountry(teams);
// Alice joins index 0 (first "TeamA")
alice.deposit(1000e18, alice);
alice.joinEvent(0);
// userSharesToCountry[alice][0] = 990
// Bob joins index 2 (second "TeamA")
bob.deposit(1000e18, bob);
bob.joinEvent(2);
// userSharesToCountry[bob][2] = 990
// Owner sets winner to index 0
vault.setWinner(0);
// winner = "TeamA"
// _getWinnerShares() executes:
// Loop through usersAddress
// Alice: totalWinnerShares += userSharesToCountry[alice][0] = 990
// Bob: totalWinnerShares += userSharesToCountry[bob][0] = 0 (wrong index!)
// BUG: Bob's shares at index 2 are not counted!
// Only shares at winnerCountryId (0) are counted
// Bob bet on "TeamA" but stored at different index
// withdraw() checks pass for both (same string)
// But payout calculation is wrong

Recommended Mitigation

function setCountry(string[48] memory countries) public onlyOwner {
+ // Validate no duplicates
+ for (uint256 i = 0; i < countries.length; ++i) {
+ // Skip empty strings
+ if (bytes(countries[i]).length == 0) continue;
+
+ for (uint256 j = i + 1; j < countries.length; ++j) {
+ require(
+ keccak256(abi.encodePacked(countries[i])) !=
+ keccak256(abi.encodePacked(countries[j])),
+ "Duplicate team names not allowed"
+ );
+ }
+ }
+
for (uint256 i = 0; i < countries.length; ++i) {
teams[i] = countries[i];
}
emit CountriesSet(countries);
}

Better approach: Use calldata and prevent calling after deposits

-function setCountry(string[48] memory countries) public onlyOwner {
+function setCountry(string[48] calldata countries) external onlyOwner {
+ // Prevent changing teams after deposits started
+ require(totalSupply() == 0, "Cannot change teams after deposits");
+
+ // Validate no duplicates
+ for (uint256 i = 0; i < countries.length; ++i) {
+ if (bytes(countries[i]).length == 0) continue;
+
+ for (uint256 j = i + 1; j < countries.length; ++j) {
+ require(
+ keccak256(abi.encodePacked(countries[i])) !=
+ keccak256(abi.encodePacked(countries[j])),
+ "Duplicate team names not allowed"
+ );
+ }
+ }
+
for (uint256 i = 0; i < countries.length; ++i) {
teams[i] = countries[i];
}
emit CountriesSet(countries);
}
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
mostafapahlevani93 Submitter
19 days ago
bube Lead Judge
15 days ago
bube Lead Judge 15 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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

Give us feedback!