The BriVault contract’s setCountry function accepts a list of 48 team names (countries) to populate the teams array. The function lacks input validation for both empty strings and duplicate entries, resulting in two key vulnerabilities:
pragma solidity ^0.8.24;
import {Test, console} 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";
* @title M-01: Missing Country Validation in setCountry
* @notice Demonstrates two scenarios:
* Scenario A: Empty string countries cause confusion
* Scenario B: Duplicate countries cause incorrect payouts
*/
contract M01_MissingCountryValidation is Test {
MockERC20 public mockToken;
BriVault public vault;
address owner = makeAddr("owner");
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address carol = makeAddr("carol");
address feeAddress = makeAddr("feeAddress");
uint256 participationFeeBsp = 300;
uint256 eventStartDate;
uint256 eventEndDate;
uint256 minimumAmount = 1 ether;
function setUp() public {
eventStartDate = block.timestamp + 2 days;
eventEndDate = eventStartDate + 31 days;
mockToken = new MockERC20("Mock Token", "MTK");
mockToken.mint(alice, 100 ether);
mockToken.mint(bob, 100 ether);
mockToken.mint(carol, 100 ether);
}
* @notice Scenario A: Empty string countries
* @dev Owner sets countries with empty strings, causing confusion
*/
function test_ScenarioA_EmptyStringCountries() public {
console.log("=== Scenario A: Empty String Countries ===");
string[48] memory countriesWithEmpty;
countriesWithEmpty[0] = "Brazil";
countriesWithEmpty[1] = "Argentina";
countriesWithEmpty[2] = "";
countriesWithEmpty[3] = "";
countriesWithEmpty[4] = "France";
for (uint i = 5; i < 48; i++) {
countriesWithEmpty[i] = "";
}
console.log("\n--- Step 1: Deploy with Empty Strings ---");
vm.startPrank(owner);
vault = new BriVault(
IERC20(address(mockToken)),
participationFeeBsp,
eventStartDate,
feeAddress,
minimumAmount,
eventEndDate
);
vault.setCountry(countriesWithEmpty);
vm.stopPrank();
console.log("Countries set:");
console.log(" teams[0]:", vault.getCountry(0));
console.log(" teams[1]:", vault.getCountry(1));
console.log(" teams[2]:", vault.getCountry(2), "(empty)");
console.log(" teams[3]:", vault.getCountry(3), "(empty)");
console.log(" teams[4]:", vault.getCountry(4));
console.log("\n--- Step 2: User Joins Empty Team ---");
vm.startPrank(alice);
mockToken.approve(address(vault), 10 ether);
vault.deposit(10 ether, alice);
vault.joinEvent(2);
vm.stopPrank();
string memory aliceTeam = vault.userToCountry(alice);
console.log("Alice joined team at index 2");
console.log("Alice's userToCountry:", aliceTeam);
console.log("Team name length:", bytes(aliceTeam).length);
console.log("Is empty?", bytes(aliceTeam).length == 0);
console.log("\n--- Step 3: Winner is Empty String ---");
vm.warp(eventEndDate + 1);
vm.prank(owner);
string memory winner = vault.setWinner(2);
console.log("Winner set to index 2");
console.log("Winner name:", winner);
console.log("Winner is empty?", bytes(winner).length == 0);
console.log("\n--- Step 4: Withdrawal Confusion ---");
vm.prank(alice);
vault.withdraw();
console.log("Alice withdrew successfully");
console.log("But winner team name is empty - confusing UX");
console.log("\n=== Scenario A Impact ===");
console.log("Empty strings accepted in setCountry");
console.log("Users can join teams with no name");
console.log("Winner can be declared with empty name");
console.log("Extremely poor UX and confusing");
console.log("Makes off-chain verification impossible");
console.log("Users cannot verify which team they picked");
}
* @notice Scenario B: Duplicate countries
* @dev Owner sets duplicate country names, breaking winner shares calculation
*/
function test_ScenarioB_DuplicateCountries() public {
console.log("=== Scenario B: Duplicate Countries ===");
string[48] memory countriesWithDuplicates;
countriesWithDuplicates[0] = "Brazil";
countriesWithDuplicates[1] = "Argentina";
countriesWithDuplicates[2] = "Brazil";
countriesWithDuplicates[3] = "France";
countriesWithDuplicates[4] = "Brazil";
for (uint i = 5; i < 48; i++) {
countriesWithDuplicates[i] = string(
abi.encodePacked("Country", vm.toString(i))
);
}
console.log("\n--- Step 1: Deploy with Duplicates ---");
vm.startPrank(owner);
vault = new BriVault(
IERC20(address(mockToken)),
participationFeeBsp,
eventStartDate,
feeAddress,
minimumAmount,
eventEndDate
);
vault.setCountry(countriesWithDuplicates);
vm.stopPrank();
console.log("Countries set:");
console.log(" teams[0]:", vault.getCountry(0));
console.log(" teams[1]:", vault.getCountry(1));
console.log(" teams[2]:", vault.getCountry(2), "(duplicate of 0)");
console.log(" teams[3]:", vault.getCountry(3));
console.log(" teams[4]:", vault.getCountry(4), "(duplicate of 0)");
console.log(
"\n--- Step 2: Users Join Same Country (Different Indices) ---"
);
vm.startPrank(alice);
mockToken.approve(address(vault), 10 ether);
vault.deposit(10 ether, alice);
vault.joinEvent(0);
vm.stopPrank();
console.log("\nAlice:");
console.log(" Joined index: 0");
console.log(" Team:", vault.userToCountry(alice));
vm.startPrank(bob);
mockToken.approve(address(vault), 20 ether);
vault.deposit(20 ether, bob);
vault.joinEvent(2);
vm.stopPrank();
console.log("\nBob:");
console.log(" Joined index: 2");
console.log(" Team:", vault.userToCountry(bob));
vm.startPrank(carol);
mockToken.approve(address(vault), 15 ether);
vault.deposit(15 ether, carol);
vault.joinEvent(1);
vm.stopPrank();
console.log("\nCarol:");
console.log(" Joined index: 1");
console.log(" Team:", vault.userToCountry(carol));
console.log("\n--- Step 3: Brazil Wins (Index 0) ---");
vm.warp(eventEndDate + 1);
vm.prank(owner);
string memory winner = vault.setWinner(0);
console.log("Winner:", winner);
console.log("Winner index: 0");
console.log("\n--- Step 4: Winner Share Calculation ---");
console.log("\nAlice (picked Brazil at index 0):");
console.log(" userToCountry:", vault.userToCountry(alice));
console.log(
" Matches winner 'Brazil'?",
keccak256(abi.encodePacked(vault.userToCountry(alice))) ==
keccak256(abi.encodePacked(winner))
);
console.log("\nBob (picked Brazil at index 2):");
console.log(" userToCountry:", vault.userToCountry(bob));
console.log(
" Matches winner 'Brazil'?",
keccak256(abi.encodePacked(vault.userToCountry(bob))) ==
keccak256(abi.encodePacked(winner))
);
uint256 totalWinnerShares = vault.totalWinnerShares();
console.log("\nTotal winner shares:", totalWinnerShares);
console.log("This only includes shares from index 0, not index 2!");
console.log("\n--- Step 5: Withdrawal Attempts ---");
uint256 aliceBalanceBefore = mockToken.balanceOf(alice);
vm.prank(alice);
vault.withdraw();
uint256 aliceBalanceAfter = mockToken.balanceOf(alice);
uint256 aliceReceived = aliceBalanceAfter - aliceBalanceBefore;
console.log("\nAlice:");
console.log(" Withdrawal: SUCCESS");
console.log(" Received:", aliceReceived / 1 ether, "ETH");
uint256 bobBalanceBefore = mockToken.balanceOf(bob);
vm.prank(bob);
vault.withdraw();
uint256 bobBalanceAfter = mockToken.balanceOf(bob);
uint256 bobReceived = bobBalanceAfter - bobBalanceBefore;
console.log("\nBob:");
console.log(" Withdrawal: SUCCESS");
console.log(" Received:", bobReceived / 1 ether, "ETH");
console.log("\n--- Step 6: The Problem ---");
console.log("Bob's shares were NOT counted in totalWinnerShares");
console.log("But Bob can still withdraw because userToCountry matches");
console.log("This breaks the proportional distribution math!");
console.log("");
console.log("Expected behavior:");
console.log(" Total winner shares should include ALL Brazil indices");
console.log(" Alice + Bob shares should be used for calculations");
console.log("");
console.log("Actual behavior:");
console.log(" Only index 0 shares counted in totalWinnerShares");
console.log(" Bob withdraws using incorrect denominator");
console.log(" Distribution is unfair/broken");
console.log("\n=== Scenario B Impact ===");
console.log("Duplicate countries accepted");
console.log("Users pick same team at different indices");
console.log("Winner share calculation only uses one index");
console.log("Proportional distribution breaks");
console.log("Some winners get unfair share");
console.log("Math invariants violated");
}
}