BriVault

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

Loss of deposited funds when receiver != msg.sender

Root + Impact

Description

Inconsistent crediting of stakedAsset vs minting of shares when receiver != msg.sender

The bug during the deposit:

  • stakedAsset[receiver] credits the receiver

  • _mint(msg.sender, ...) mints shares to msg.sender

  • Result: Receiver has stakedAsset credit but no shares

  • Result: Msg.sender has shares but no stakedAsset credit


// Root cause in the codebase with @> marks to highlight the relevant section
function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
// ...
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); // Mints to msg.sender!
emit deposited(receiver, stakeAsset);
return participantShares;
}

Risk

Likelihood: High

  • Every time when someone wants to bet for someone else (receiver != msg.sender)

Impact: High

  • Loss of funds

Proof of Concept

Scenario 1: Alice deposits for Bob, then Alice cancels

  • Bob gets stakedAsset credit but 0 shares

  • Alice gets shares but no stakedAsset

  • Alice cancels → receives 0 ETH refund

  • Result: Alice loses 100% (5 ETH)

Scenario 2: Alice deposits for Bob, Bob joins event and wins

  • Bob passes stakedAsset check (has credit)

  • Bob registers with 0 shares (balanceOf(bob) = 0)

  • Bob withdraws 0 ETH despite 5 ETH deposited

  • Result: Bob's funds redistributed to other winners

// Scenario 1
function test_ReceiverSenderMismatch() public {
address alice = user1;
address bob = user2;
// Alice deposits for Bob
vm.startPrank(alice);
mockToken.approve(address(briVault), 10 ether);
briVault.deposit(5 ether, bob);
vm.stopPrank();
// Verify the mismatch
uint256 expected = 4925000000000000000;
assertEq(briVault.stakedAsset(bob), expected, "Bob has stakedAsset");
assertEq(briVault.stakedAsset(alice), 0, "Alice has no stakedAsset");
assertEq(briVault.balanceOf(alice), expected, "Alice has shares");
assertEq(briVault.balanceOf(bob), 0, "Bob has no shares");
// Test: Alice tries to cancel - gets 0 refund!
vm.startPrank(alice);
uint256 aliceBalBefore = mockToken.balanceOf(alice);
briVault.cancelParticipation();
uint256 aliceBalAfter = mockToken.balanceOf(alice);
uint256 refund = aliceBalAfter - aliceBalBefore;
vm.stopPrank();
assertEq(refund, 0, "Alice gets 0 refund");
}
// Scenario 2
function test_ReceiverJoinsEvent_ZeroShares() public {
address alice = user1;
address bob = user2;
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
// Alice deposits for Bob
vm.startPrank(alice);
mockToken.approve(address(briVault), 10 ether);
briVault.deposit(5 ether, bob);
vm.stopPrank();
// Bob tries to join event
vm.startPrank(bob);
briVault.joinEvent(10);
vm.stopPrank();
// Another user joins for comparison
vm.startPrank(user3);
mockToken.approve(address(briVault), 10 ether);
briVault.deposit(5 ether, user3);
briVault.joinEvent(10);
vm.stopPrank();
// Set winner and check withdrawals
vm.warp(eventEndDate + 1);
vm.startPrank(owner);
briVault.setWinner(10);
vm.stopPrank();
// Bob tries to withdraw
vm.startPrank(bob);
uint256 bobBalBefore = mockToken.balanceOf(bob);
briVault.withdraw();
uint256 bobBalAfter = mockToken.balanceOf(bob);
uint256 bobPayout = bobBalAfter - bobBalBefore;
vm.stopPrank();
// User3 withdraws for comparison
vm.startPrank(user3);
uint256 user3BalBefore = mockToken.balanceOf(user3);
briVault.withdraw();
uint256 user3BalAfter = mockToken.balanceOf(user3);
uint256 user3Payout = user3BalAfter - user3BalBefore;
uint256 expectedUser3Shares = 4925000000000000000;
vm.stopPrank();
// Assertions
assertEq(briVault.userSharesToCountry(bob, 10), 0, "Bob has 0 shares registered");
assertEq(bobPayout, 0, "Bob receives 0 payout");
assertGt(user3Payout, 0, "User3 receives payout");
}

Recommended Mitigation

Mint shares to the receiver address, so both shares and staked assets are attached to the receiver.

function deposit(uint256 assets, address receiver) public override returns (uint256) {
// ...
stakedAsset[receiver] = stakeAsset;
// ...
- _mint(msg.sender, participantShares);
+ _mint(receiver, 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!