function setWinner(uint256 countryIndex) public onlyOwner returns (string memory) {
if (block.timestamp <= eventEndDate) {
revert eventNotEnded();
}
require(countryIndex < teams.length, "Invalid country index");
if (_setWinner) {
revert WinnerAlreadySet();
}
winnerCountryId = countryIndex;
winner = teams[countryIndex];
_setWinner = true;
@> _getWinnerShares();
_setFinallizedVaultBalance();
emit WinnerSet (winner);
return winner;
}
function _getWinnerShares () internal returns (uint256) {
for (uint256 i = 0; i < usersAddress.length; ++i){
address user = usersAddress[i];
@> totalWinnerShares += userSharesToCountry[user][winnerCountryId];
}
return totalWinnerShares;
}
function withdraw() external winnerSet {
if (block.timestamp < eventEndDate) {
revert eventNotEnded();
}
if (
keccak256(abi.encodePacked(userToCountry[msg.sender])) !=
keccak256(abi.encodePacked(winner))
) {
revert didNotWin();
}
uint256 shares = balanceOf(msg.sender);
uint256 vaultAsset = finalizedVaultAsset;
@> uint256 assetToWithdraw = Math.mulDiv(shares, vaultAsset, totalWinnerShares);
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, assetToWithdraw);
emit Withdraw(msg.sender, assetToWithdraw);
}
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {BriVault} from "../src/briVault.sol";
import {BriTechToken} from "../src/briTechToken.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract WithdrawalLockZeroDivisionPoC is Test {
BriVault public vault;
BriTechToken public token;
address owner = makeAddr("owner");
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
address feeAddress = makeAddr("feeAddress");
uint256 constant DEPOSIT_AMOUNT = 1000e18;
function setUp() public {
vm.startPrank(owner);
token = new BriTechToken();
token.mint();
uint256 currentTime = block.timestamp;
vault = new BriVault(
IERC20(address(token)),
150,
currentTime + 1 days,
feeAddress,
1e18,
currentTime + 7 days
);
string[48] memory countries = [
"Team0", "Team1", "Team2", "Team3", "Team4", "Team5",
"", "", "", "", "", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", ""
];
vault.setCountry(countries);
token.transfer(user1, 10000e18);
token.transfer(user2, 10000e18);
token.transfer(user3, 10000e18);
vm.stopPrank();
}
function test_WithdrawalLockIfNoWinningParticipants() public {
console.log("=== Withdrawal Lock via Division by Zero PoC ===");
vm.startPrank(user1);
uint256 fee = (DEPOSIT_AMOUNT * 150) / 10000;
token.approve(address(vault), DEPOSIT_AMOUNT + fee);
vault.deposit(DEPOSIT_AMOUNT, user1);
vault.joinEvent(0);
console.log("User1 joined Team0");
vm.stopPrank();
vm.startPrank(user2);
token.approve(address(vault), DEPOSIT_AMOUNT + fee);
vault.deposit(DEPOSIT_AMOUNT, user2);
vault.joinEvent(0);
console.log("User2 joined Team0");
vm.stopPrank();
vm.startPrank(user3);
token.approve(address(vault), DEPOSIT_AMOUNT + fee);
vault.deposit(DEPOSIT_AMOUNT, user3);
vault.joinEvent(0);
console.log("User3 joined Team0");
vm.stopPrank();
console.log("\n--- ALL USERS BET ON TEAM 0 ---");
console.log("Total participants:", vault.numberOfParticipants());
console.log("Winner Country ID will be set to: 1 (Team1 with 0 participants)");
vm.warp(block.timestamp + 8 days);
console.log("\n--- OWNER SETS TEAM 1 AS WINNER ---");
vm.prank(owner);
vault.setWinner(1);
console.log("Winner set to Team1");
console.log("totalWinnerShares:", vault.totalWinnerShares());
console.log("\n--- USER1 ATTEMPTS WITHDRAWAL ---");
uint256 user1BalanceBefore = token.balanceOf(user1);
vm.startPrank(user1);
vm.expectRevert();
vault.withdraw();
vm.stopPrank();
uint256 user1BalanceAfter = token.balanceOf(user1);
console.log("User1 balance before withdrawal:", user1BalanceBefore / 1e18, "tokens");
console.log("User1 balance after withdrawal attempt:", user1BalanceAfter / 1e18, "tokens");
console.log("Withdrawal FAILED - Division by zero in Math.mulDiv");
console.log("\n--- FUND LOCK VERIFICATION ---");
console.log("User1 shares:", vault.balanceOf(user1));
console.log("Vault asset balance:", token.balanceOf(address(vault)) / 1e18, "tokens");
console.log("Winner shares (zero):", vault.totalWinnerShares());
console.log("\nResult: ALL FUNDS PERMANENTLY LOCKED");
console.log("Fund Loss: ~2850 tokens (3 users * 1000 - fees)");
assertEq(vault.totalWinnerShares(), 0, "totalWinnerShares should be 0");
}
}
forge test --match-contract WithdrawalLockZeroDivisionPoC -vv
[⠑] Compiling...
No files changed, compilation skipped
Ran 1 test for test/WithdrawalLockZeroDivisionPoC.t.sol:WithdrawalLockZeroDivisionPoC
[PASS] test_WithdrawalLockIfNoWinningParticipants() (gas: 824604)
Logs:
=== Withdrawal Lock via Division by Zero PoC ===
User1 joined Team0
User2 joined Team0
User3 joined Team0
--- ALL USERS BET ON TEAM 0 ---
Total participants: 3
Winner Country ID will be set to: 1 (Team1 with 0 participants)
--- OWNER SETS TEAM 1 AS WINNER ---
Winner set to Team1
totalWinnerShares: 0
--- USER1 ATTEMPTS WITHDRAWAL ---
User1 balance before withdrawal: 9000 tokens
User1 balance after withdrawal attempt: 9000 tokens
Withdrawal FAILED - Division by zero in Math.mulDiv
--- FUND LOCK VERIFICATION ---
User1 shares: 985000000000000000000
Vault asset balance: 2955 tokens
Winner shares (zero): 0
Result: ALL FUNDS PERMANENTLY LOCKED
Fund Loss: ~2850 tokens (3 users * 1000 - fees)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.15ms (294.76µs CPU time)
Ran 1 test suite in 3.83ms (1.15ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)