BriVault

First Flight #52
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: low
Likelihood: medium
Invalid

Missing Validation on Minimum Deposit Amount Enables DoS and User Exclusion

Description

  • The BriVault contract requires users to deposit a minimum amount of tokens to participate in the event. The minimumAmount parameter is set during contract deployment and is intended to prevent spam deposits while ensuring meaningful participation. The contract checks this minimum during the deposit process to ensure users contribute a reasonable stake.

  • The constructor accepts minimumAmount as a parameter but performs no validation on its value. This allows deployment with either zero (enabling dust attacks that bloat the usersAddress array and amplify DoS vulnerabilities) or astronomically high values (effectively excluding all legitimate users from participation). Both extremes render the contract either vulnerable to attacks or unusable for its intended purpose.

constructor(
IERC20 _asset,
uint256 _participationFeeBsp,
uint256 _eventStartDate,
address _participationFeeAddress,
@> uint256 _minimumAmount, // No validation on minimum or maximum bounds
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; // ❌ No bounds checking
_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) { // Only check that fails
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;
}

Risk

Likelihood: Medium

  • Deployment with inappropriate minimumAmount values can occur through configuration errors, copy-paste mistakes, or lack of understanding of reasonable token denominations

  • No deployment validation exists to warn about extreme values (zero or excessively high)

  • The impact manifests differently depending on the value but both extremes create significant issues

Impact: Low to Medium

Scenario A - Zero Minimum (DoS Amplification):

  • Attackers can perform dust attacks with negligible cost (1 wei deposits)

  • Each dust deposit adds an entry to usersAddress array with minimal capital requirement

  • Amplifies the existing DoS vulnerability in _getWinnerShares() which loops through all participants

  • With zero minimum, an attacker can bloat the array with 10,000+ entries for trivial cost

  • Makes the existing critical DoS issue easier and cheaper to exploit

Scenario B - Excessive Minimum (User Exclusion):

  • Setting minimumAmount too high (e.g., 1 million tokens) effectively locks out legitimate users

  • 99%+ of potential participants cannot afford the minimum deposit requirement

  • Event fails due to insufficient participation, rendering the contract purposeless

  • Poor user experience and potential reputational damage for the protocol

Proof of Concept

Add the following test file:

// SPDX-License-Identifier: MIT
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; // 3%
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) ===");
// Step 1: Deploy with minimumAmount = 0
console.log("\n--- Step 1: Deployment ---");
vm.startPrank(owner);
vault = new BriVault(
IERC20(address(mockToken)),
participationFeeBsp,
eventStartDate,
feeAddress,
0, // ❌ minimumAmount = 0
eventEndDate
);
vault.setCountry(countries);
vm.stopPrank();
console.log("Deployed with minimumAmount: 0");
console.log("Participation fee:", (participationFeeBsp * 100) / 10000, "%");
// Step 2: Attacker performs dust attack
console.log("\n--- Step 2: Dust Attack ---");
uint256 dustAmount = 1; // 1 wei per deposit
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());
// Step 3: Calculate attack cost
console.log("\n--- Step 3: Attack Cost Analysis ---");
uint256 totalCost = spamCount * (dustAmount + 1); // Approximate
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) ===");
// Step 1: Deploy with unreasonable minimum
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, // ❌ minimumAmount = 1M tokens
eventEndDate
);
vault.setCountry(countries);
vm.stopPrank();
console.log("Deployed with minimumAmount:", unreasonableMin / 1 ether, "tokens");
// Step 2: Normal user excluded
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();
// Step 3: Wealthy user also excluded
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();
// Step 4: Only whale succeeds
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");
}
}

Run tests:

forge test --match-contract L01_UnreasonableMinimumAmount -vv

Expected Output - Scenario A:

[PASS] test_MinimumAmountZero_DustAttack() (gas: 10280831)
Logs:
=== POC: Minimum Amount = 0 (Dust Attack) ===
--- Step 1: Deploy with minimumAmount = 0 ---
Vault deployed with minimumAmount: 0
Fee percentage: 3 %
--- Step 2: Attacker Dust Attack ---
Dust amount per deposit: 1 wei
Number of dust deposits: 100
Participants after dust attack: 100
usersAddress array length: 100
--- Step 3: Normal User Deposits ---
Normal user deposited: 10 ETH
Total participants: 101
--- Step 4: Impact Analysis ---
Attacker Stats:
- Deposits made: 100
- Total spent: 100 wei
- Array pollution: 100 entries
Normal User Stats:
- Deposits made: 1
- Total spent: 10 ETH
- Array entries: 1
=== IMPACT SUMMARY ===
Vulnerability: minimumAmount = 0
Attack cost: 100 wei (negligible)
usersAddress bloated by: 100 entries
Gas cost for setWinner increased significantly
Amplifies DoS attack in _getWinnerShares()
With enough spam, setWinner() will run out of gas
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 27.35ms (24.66ms CPU time)
Ran 1 test suite in 31.13ms (27.35ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Expected Output - Scenario B:

[FAIL: lowFeeAndAmount()] test_MinimumAmountTooHigh_ExcludesUsers() (gas: 5066842)
Logs:
=== POC: Minimum Amount Too High (User Exclusion) ===
--- Step 1: Deploy with minimumAmount = 1M tokens ---
Vault deployed with minimumAmount: 1000000 tokens
Fee percentage: 3 %
--- Step 2: Normal User Attempts Deposit ---
Normal user balance: 100 tokens
Required minimum: 1000000 tokens
User can afford? false
Required with fee: 1030000 tokens
Attempting deposit with 100 tokens...
Result: REVERTED (lowFeeAndAmount)
--- Step 3: Wealthy User Attempts ---
Wealthy user balance: 500000 tokens
Required minimum: 1000000 tokens
User can afford? false
Attempting deposit with 500000 tokens...
Result: REVERTED (still not enough)
--- Step 4: Extremely Wealthy User ---
Whale user balance: 2000000 tokens
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 5.23ms (2.36ms CPU time)
Ran 1 test suite in 11.74ms (5.23ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/poc/L01_UnreasonableMinimumAmount.t.sol:L01_UnreasonableMinimumAmount
[FAIL: lowFeeAndAmount()] test_MinimumAmountTooHigh_ExcludesUsers() (gas: 5066842)
Encountered a total of 1 failing tests, 0 tests succeeded

Recommended Mitigation

Add bounds validation to the constructor to ensure minimumAmount falls within a reasonable range:

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();
}
+ require(_minimumAmount >= 1e18, "Minimum too low"); // At least 1 token
+ require(_minimumAmount <= 1e24, "Minimum too high"); // Max 1M tokens
participationFeeBsp = _participationFeeBsp;
eventStartDate = _eventStartDate;
eventEndDate = _eventEndDate;
participationFeeAddress = _participationFeeAddress;
minimumAmount = _minimumAmount;
_setWinner = false;
}

Explanation:

The mitigation establishes reasonable bounds for the minimum deposit amount:

Lower Bound (1e18 = 1 token):

  • Prevents dust attacks by requiring meaningful deposits

  • Makes it economically infeasible to spam the usersAddress array

  • If an attacker wants to add 10,000 entries, they need 10,000 tokens instead of 10,000 wei

  • This significantly increases the cost of DoS attacks that bloat the participant array

Upper Bound (1e24 = 1 million tokens):

  • Ensures the minimum is not set so high that it excludes legitimate users

  • Provides reasonable participation threshold even for high-value tokens

  • Can be adjusted based on the specific token's economics and expected user base

  • Prevents accidental misconfigurations that would render the contract unusable

Alternative - Custom Errors (Gas Efficient):

+ error MinimumAmountTooLow();
+ error MinimumAmountTooHigh();
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();
}
+ if (_minimumAmount < 1e18) revert MinimumAmountTooLow();
+ if (_minimumAmount > 1e24) revert MinimumAmountTooHigh();
participationFeeBsp = _participationFeeBsp;
eventStartDate = _eventStartDate;
eventEndDate = _eventEndDate;
participationFeeAddress = _participationFeeAddress;
minimumAmount = _minimumAmount;
_setWinner = false;
}
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Missing Constructor Validation

This is owner action and the owner is assumed to be trusted and to provide correct input arguments.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!