BriVault

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

Zero Address Fee Recipient Causes Complete DoS

Description

  • The BriVault contract charges a participation fee (up to 3%) on all user deposits, which is transferred to the participationFeeAddress upon each deposit. This fee mechanism is intended to generate protocol revenue while users stake their tokens for event participation.

  • The constructor accepts participationFeeAddress as a parameter but fails to validate that it is not the zero address (address(0)). When deployed with participationFeeAddress = address(0), all deposit attempts will revert because the underlying ERC20 token (OpenZeppelin implementation) prevents transfers to the zero address. This results in a complete denial of service where no users can deposit, rendering the entire contract unusable.

constructor(
IERC20 _asset,
uint256 _participationFeeBsp,
uint256 _eventStartDate,
@> address _participationFeeAddress, // No validation for address(0)
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; // ❌ No address(0) check
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);
// This transfer will REVERT if participationFeeAddress is address(0)
@> 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 participationFeeAddress = address(0) can occur through copy-paste errors, uninitialized variables in deployment scripts, or incorrect parameter ordering during deployment

  • No constructor validation exists to prevent this misconfiguration, allowing silent deployment with invalid configuration

  • Unlike timestamp misconfigurations, this error manifests immediately when the first user attempts to deposit, making it detectable but only after deployment costs are incurred

Impact: High

  • Complete denial of service affecting 100% of users - no deposits can succeed regardless of deposit amount or user

  • Contract becomes entirely unusable for its intended purpose, requiring full redeployment with new addresses and user communication

  • All users attempting to deposit will experience transaction failures, resulting in poor user experience and loss of trust

  • Protocol loses all potential participants and revenue for the event, with gas costs wasted on failed transactions

  • Time-sensitive events may pass their start date before the issue is discovered and corrected, making the contract permanently useless

Proof of Concept

Add the following test to your test suite:

// 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 C-02: Zero Address Fee Recipient Causes Complete DoS
* @notice Demonstrates that address(0) fee recipient blocks ALL deposits
*/
contract C02_ZeroAddressFeeRecipient is Test {
MockERC20 public mockToken;
BriVault public vault;
address owner = makeAddr("owner");
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
address user4 = makeAddr("user4");
address user5 = makeAddr("user5");
uint256 participationFeeBsp = 300; // 3%
uint256 eventStartDate;
uint256 eventEndDate;
uint256 minimumAmount = 0.0002 ether;
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(user1, 100 ether);
mockToken.mint(user2, 200 ether);
mockToken.mint(user3, 50 ether);
mockToken.mint(user4, 150 ether);
mockToken.mint(user5, 75 ether);
}
function test_ZeroAddressFeeRecipient_CompleteDoS() public {
console.log("=== POC: Zero Address Fee Recipient - Complete DoS ===");
// Step 1: Deploy with address(0) as fee recipient
console.log("\n--- Step 1: Deployment ---");
vm.startPrank(owner);
vault = new BriVault(
IERC20(address(mockToken)),
participationFeeBsp,
eventStartDate,
address(0), // ❌ Fee recipient is address(0)
minimumAmount,
eventEndDate
);
vm.stopPrank();
console.log("Vault deployed successfully");
console.log("Fee recipient: address(0)");
console.log("Participation fee:", (participationFeeBsp * 100) / 10000, "%");
// Step 2: Multiple users attempt deposits
console.log("\n--- Step 2: Multiple Users Attempt Deposits ---");
uint256 totalAttemptedDeposits = 0;
// User 1
console.log("\nUser1 attempting to deposit 10 ETH...");
uint256 deposit1 = 10 ether;
totalAttemptedDeposits += deposit1;
vm.startPrank(user1);
mockToken.approve(address(vault), deposit1);
vm.expectRevert();
vault.deposit(deposit1, user1);
vm.stopPrank();
console.log("Result: REVERTED");
// User 2
console.log("\nUser2 attempting to deposit 50 ETH...");
uint256 deposit2 = 50 ether;
totalAttemptedDeposits += deposit2;
vm.startPrank(user2);
mockToken.approve(address(vault), deposit2);
vm.expectRevert();
vault.deposit(deposit2, user2);
vm.stopPrank();
console.log("Result: REVERTED");
// User 3
console.log("\nUser3 attempting to deposit 25 ETH...");
uint256 deposit3 = 25 ether;
totalAttemptedDeposits += deposit3;
vm.startPrank(user3);
mockToken.approve(address(vault), deposit3);
vm.expectRevert();
vault.deposit(deposit3, user3);
vm.stopPrank();
console.log("Result: REVERTED");
// User 4
console.log("\nUser4 attempting to deposit 100 ETH...");
uint256 deposit4 = 100 ether;
totalAttemptedDeposits += deposit4;
vm.startPrank(user4);
mockToken.approve(address(vault), deposit4);
vm.expectRevert();
vault.deposit(deposit4, user4);
vm.stopPrank();
console.log("Result: REVERTED");
// User 5
console.log("\nUser5 attempting to deposit 30 ETH...");
uint256 deposit5 = 30 ether;
totalAttemptedDeposits += deposit5;
vm.startPrank(user5);
mockToken.approve(address(vault), deposit5);
vm.expectRevert();
vault.deposit(deposit5, user5);
vm.stopPrank();
console.log("Result: REVERTED");
// Step 3: Verify complete DoS
console.log("\n--- Step 3: Impact Verification ---");
uint256 vaultBalance = mockToken.balanceOf(address(vault));
uint256 numberOfParticipants = vault.numberOfParticipants();
console.log("Total users attempted: 5");
console.log("Successful deposits: 0");
console.log("Attempted deposit amount:", totalAttemptedDeposits / 1 ether, "ETH");
console.log("Actual vault balance:", vaultBalance);
console.log("Participants registered:", numberOfParticipants);
assertEq(vaultBalance, 0, "Vault should be empty");
assertEq(numberOfParticipants, 0, "No participants");
console.log("\n=== IMPACT SUMMARY ===");
console.log("Users affected: 5/5 (100%)");
console.log("Deposits blocked:", totalAttemptedDeposits / 1 ether, "ETH");
console.log("Contract status: UNUSABLE - REQUIRES REDEPLOYMENT");
}
}

Run with:

forge test --match-contract C02_ZeroAddressFeeRecipient -vv

Test Output:

[PASS] test_ZeroAddressFeeRecipient_CompleteDoS() (gas: 4001217)
Logs:
=== POC: Zero Address Fee Recipient - Complete DoS ===
--- Step 1: Deployment ---
Vault deployed successfully
Fee recipient: address(0)
Participation fee: 3 %
Minimum deposit: 200000000000000
--- Step 2: Multiple Users Attempt Deposits ---
User1 attempting to deposit 10 ETH...
Result: REVERTED
User1 balance unchanged: true
User2 attempting to deposit 50 ETH...
Result: REVERTED
User2 balance unchanged: true
User3 attempting to deposit 25 ETH...
Result: REVERTED
User3 balance unchanged: true
User4 attempting to deposit 100 ETH...
Result: REVERTED
User4 balance unchanged: true
User5 attempting to deposit 30 ETH...
Result: REVERTED
User5 balance unchanged: true
--- Step 3: DoS Impact Analysis ---
Total users attempted to deposit: 5
Successful deposits: 0
Total attempted deposit amount: 215 ETH
Actual vault balance: 0
Number of participants: 0
--- Step 4: Financial Impact ---
Expected total deposits: 215 ETH
Expected protocol fees: 6 ETH
Expected vault balance: 208 ETH
Actual vault balance: 0 ETH
Actual participants: 0
=== IMPACT SUMMARY ===
Vulnerability: Fee recipient set to address(0)
Consequence: ERC20 transfer to address(0) reverts
DoS Statistics:
- Users affected: 5/5 (100%)
- Deposits blocked: 215 ETH
- Successful deposits: 0 ETH
- Contract participants: 0
Business Impact:
- Lost user deposits: 215 ETH
- Lost protocol fees: 6 ETH
- Contract status: UNUSABLE
- Required action: REDEPLOY CONTRACT
Severity: HIGH
Impact: Complete Denial of Service
User Experience: All deposit attempts fail

Analysis: The test demonstrates complete denial of service with 100% of deposit attempts failing due to ERC20's protection against transfers to address(0).

Recommended Mitigation

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(_participationFeeAddress != address(0), "Invalid fee address");
participationFeeBsp = _participationFeeBsp;
eventStartDate = _eventStartDate;
eventEndDate = _eventEndDate;
participationFeeAddress = _participationFeeAddress;
minimumAmount = _minimumAmount;
_setWinner = false;
}

Alternative - Custom Error (Gas Efficient):

+ error InvalidFeeAddress();
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 (_participationFeeAddress == address(0)) {
+ revert InvalidFeeAddress();
+ }
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!