BriVault

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

Shares minted to wrong address causes loss of funds

Description

The deposit() function overrides the standard ERC4626 deposit() function. According to the ERC4626 standard, when deposit(assets, receiver) is called, the shares should be minted to the receiver address. However, the implementation mints shares to msg.sender instead of receiver, while still recording stakedAsset for the receiver address.

The specific issue is that `_mint(msg.sender, participantShares)` mints shares to the caller instead of the receiver. This breaks the ERC4626 standard interface contract and creates state inconsistency. If user1 deposits on behalf of user2, user1 receives the shares but user2's stake is recorded. This means user2 cannot use the shares to participate in events, and user1 cannot receive the funds if they cancel because the stake is recorded for user2. Also, other users will get the tokens user1 deposited on behalf of user2 when they win.

function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
// ... validation and fee calculation ...
uint256 stakeAsset = assets - fee;
stakedAsset[receiver] = stakeAsset; // @> Recorded for receiver
uint256 participantShares = _convertToShares(stakeAsset);
// ... transfers ...
_mint(msg.sender, participantShares); // @> VULN: Minted to msg.sender instead of receiver
// @> Should be: _mint(receiver, participantShares);
emit deposited (receiver, stakeAsset);
return participantShares;
}

Risk

**Likelihood**:
* Users may deposit on behalf of other addresses
* The function accepts a `receiver` parameter but doesn't use it for minting
* Breaks expected ERC4626 standard behavior
**Impact**:
* Depositor loses their tokens (cannot join, or withdraw)
* Receiver loses their bet (participates with 0 shares, gets no prize even if country wins)
* Prize pool unfairly distributed to other winners (receiver's portion goes to others)
* Breaks ERC4626 standard interface contract
* May cause integration issues with ERC4626-compatible protocols

Proof of Concept

The following PoC shows how if user1 deposits on behalf of user2 then:

  • user1 can't withdraw the deposited token

  • user1 also can't join an event (noDeposit() error is thrown)

  • user2 can join an event, but they will do it 0 shares, because they were minted to user1

  • user3 gets the prize that would have corresponded to user2, who bet on the winner country with user1's money (but really didn't)

function testSharesMintedToWrongAddress() public {
vm.prank(owner);
briVault.setCountry(countries);
// User1 deposits on behalf of user2 (receiver)
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
uint256 sharesReceived = briVault.deposit(5 ether, user2);
vm.stopPrank();
// VULN: Shares minted to msg.sender (user1) instead of receiver (user2)
// But stakedAsset recorded for receiver (user2) - breaks ERC4626 standard
assertEq(briVault.balanceOf(user1), sharesReceived);
assertEq(briVault.balanceOf(user2), 0);
assertGt(briVault.stakedAsset(user2), 0);
// User1 can't join an event and gets nothing back when they cancel (stakedAsset is for user2)
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("noDeposit()"));
briVault.joinEvent(10);
assertEq(briVault.userSharesToCountry(user1, 10), 0);
console2.log("User1 shares to country 10 after joining event:", briVault.userSharesToCountry(user1, 10));
uint256 balanceBefore = mockToken.balanceOf(user1);
briVault.cancelParticipation();
vm.stopPrank();
assertEq(mockToken.balanceOf(user1), balanceBefore);
// User2 can join event but with 0 shares recorded
vm.startPrank(user2);
briVault.joinEvent(10);
assertEq(briVault.userSharesToCountry(user2, 10), 0);
console2.log("User2 shares to country 10 after joining event:", briVault.userSharesToCountry(user2, 10));
vm.stopPrank();
// Add another user who properly deposits and joins to show contrast
vm.startPrank(user3);
mockToken.approve(address(briVault), 5 ether);
uint256 user3Shares = briVault.deposit(5 ether, user3);
briVault.joinEvent(10); // Same country as user2
vm.stopPrank();
// If country 10 wins, user2 participates with 0 shares, user3 with actual shares
vm.warp(eventEndDate + 1);
vm.startPrank(owner);
briVault.setWinner(10);
uint256 totalWinnerShares = briVault.totalWinnerShares();
vm.stopPrank();
assertEq(totalWinnerShares, user3Shares);
console2.log("User3 shares:", user3Shares);
console2.log("Total winner shares:", totalWinnerShares);
vm.stopPrank();
// User2 cannot withdraw anything (0 shares, division would fail or return 0)
vm.startPrank(user2);
uint256 balanceBeforeWithdraw = mockToken.balanceOf(user2);
// Withdraw will revert or return 0 because user2 has 0 shares
// The calculation would be: (0 * vaultAsset) / totalWinnerShares = 0
uint256 user2Shares = briVault.balanceOf(user2);
assertEq(user2Shares, 0);
// Even if withdraw succeeds, they get 0 assets
briVault.withdraw();
uint256 balanceAfterWithdraw = mockToken.balanceOf(user2);
assertEq(balanceAfterWithdraw, balanceBeforeWithdraw);
console2.log("User2 bet on the winning couuntry, yet they get :", balanceAfterWithdraw, "assets");
vm.stopPrank();
// User3 can withdraw their shares and they get also those that would correspond to user 2
vm.startPrank(user3);
uint256 balanceBeforeWithdraw3 = mockToken.balanceOf(user3);
briVault.withdraw();
uint256 balanceAfterWithdraw3 = mockToken.balanceOf(user3);
assertGt(balanceAfterWithdraw3, balanceBeforeWithdraw3);
console2.log("User3 received:", balanceAfterWithdraw3 - balanceBeforeWithdraw3, "assets");
vm.stopPrank();
}

Recommended Mitigation

Mint the shares to the `receiver` instead of msg.sender.

function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
// ... existing validation and fee calculation ...
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);
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!