BriVault

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

Wrong Mint Receiver in deposit() Breaks ERC-4626 Semantics and Misallocates Shares

Root + Impact

Description

  • Normal behavior:
    Per the ERC-4626 standard, shares minted during a deposit must be credited to the receiver argument — the address that the depositor specifies.
    This allows third-party integrations and front-ends to perform deposits on behalf of other users safely.

  • Specific issue:

    In Vault.sol, the contract calls _mint(msg.sender, participantShares) instead of _mint(receiver, participantShares) inside deposit():
    This violates ERC-4626 and misdirects shares when a protocol, wrapper, or relayer deposits tokens for another user.
    It also prevents delegated deposits and can misattribute ownership in automated strategies.

// Root cause in the codebase with @> marks to highlight the relevant section
@> function deposit(uint256 assets, address receiver) public override returns (uint256) {
...
@> _mint(msg.sender, participantShares);

Risk

Likelihood:

  • Reason 1: Occurs whenever a DeFi protocol or relayer deposits on behalf of another address, which is common with vault integrations.


Reason 2: The incorrect mint target is hard-coded (msg.sender), so the bug happens deterministically in every such deposit.

Impact:

  • Assets are deposited for receiver, but the minted shares go to msg.sender, causing loss of user ownership.


Breaks ERC-4626 compatibility → integrations relying on standard behavior will malfunction.

Proof of Concept

PoC Explanation

PoC for Finding (Vault.deposit mints shares to msg.sender instead of receiver).
Steps to reproduce (Hardhat / Remix):

  1. Deploy the ERC20 asset token (or use an existing test token). Mint tokens to A (relayer).

  2. Deploy BriVault (vault) with that token as asset, set participationFeeAddress to a test address,
    set eventStartDate > now, eventEndDate > eventStartDate, minimumAmount set appropriately.

  3. From account A (relayer), approve the vault to spend X tokens: token.approve(vault, X).

  4. Choose account B (user, different from A).

  5. From A, call PoC.exploitDeposit(vaultAddress, tokenAddress, amount, B).

  6. Inspect vault.balanceOf(A) and vault.balanceOf(B). Expected: A got shares, B got 0 — demonstrating misallocation.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IERC20Minimal {
function approve(address spender, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address who) external view returns (uint256);
}
interface IBriVault {
// matching the vault signature in your code
function deposit(uint256 assets, address receiver) external returns (uint256);
function balanceOf(address owner) external view returns (uint256);
function stakedAsset(address who) external view returns (uint256);
}
contract VaultDepositPoC {
event PoCResult(address relayer, address receiver, uint256 relayerShares, uint256 receiverShares, uint256 stakedForReceiver);
/*
* @notice Perform deposit on behalf of `receiver` using `relayer` (msg.sender).
* Precondition: msg.sender (relayer) must hold `amount` tokens and have approved the vault.
*/
function exploitDeposit(address vaultAddr, address tokenAddr, uint256 amount, address receiver) external {
IBriVault vault = IBriVault(vaultAddr);
IERC20Minimal token = IERC20Minimal(tokenAddr);
// call deposit on the vault as relayer (msg.sender) specifying `receiver`
// according to ERC4626 semantics, shares should be minted to `receiver`
vault.deposit(amount, receiver);
// read balances to demonstrate misallocation
uint256 relayerShares = vault.balanceOf(msg.sender);
uint256 receiverShares = vault.balanceOf(receiver);
uint256 receiverStaked = vault.stakedAsset(receiver);
emit PoCResult(msg.sender, receiver, relayerShares, receiverShares, receiverStaked);
}
// helper reading functions (optional)
function readRelayerShares(address vaultAddr, address relayer) external view returns (uint256) {
return IBriVault(vaultAddr).balanceOf(relayer);
}
function readReceiverShares(address vaultAddr, address receiver) external view returns (uint256) {
return IBriVault(vaultAddr).balanceOf(receiver);
}
function readReceiverStaked(address vaultAddr, address receiver) external view returns (uint256) {
return IBriVault(vaultAddr).stakedAsset(receiver);
}
}

Recommended Mitigation

Explanation

Mint shares to the receiver parameter as the ERC-4626 standard requires.
This ensures deposits made on behalf of others properly credit the intended recipient.

- remove this code
+ add this code
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;
uint256 participantShares = _convertToShares(stakeAsset);
IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
- _mint(msg.sender, participantShares);
+ _mint(receiver, participantShares); // mint shares to receiver as per ERC4626
emit deposited(receiver, stakeAsset);
return participantShares;
}
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Shares Minted to msg.sender Instead of Specified Receiver

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!