Description:
The deposit() function has a critical design flaw where it accepts a receiver parameter but inconsistently uses both receiver and msg.sender throughout the function:
function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
stakedAsset[receiver] = stakeAsset;
IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
_mint(msg.sender, participantShares);
}
The problems:
stakedAsset[receiver] is set, but shares are minted to msg.sender
If receiver != msg.sender, the receiver gets credit for the deposit but msg.sender gets the shares
The receiver cannot use joinEvent() because they have no shares, even though stakedAsset[receiver] > 0
The msg.sender has shares but stakedAsset[msg.sender] == 0, so they also cannot join
Impact:
If a user calls deposit(1000, alice) intending alice to be the receiver:
stakedAsset[alice] = 990 (after fees)
Shares are minted to msg.sender, not alice
Alice cannot call joinEvent() because balanceOf(alice) == 0
msg.sender cannot call joinEvent() because stakedAsset[msg.sender] == 0
Complete broken functionality when receiver != msg.sender
Users lose the ability to participate in betting
Shares and staked amounts become desynchronized
Proof of Concept:
function testReceiverMsgSenderMismatch() public {
address alice = makeAddr("alice");
vm.startPrank(attacker);
asset.approve(address(vault), 10000 * 10**18);
uint256 shares = vault.deposit(10000 * 10**18, alice);
vm.stopPrank();
assertEq(vault.stakedAsset(alice), 9900 * 10**18, "Alice has staked amount");
assertEq(vault.balanceOf(alice), 0, "Alice has no shares");
assertEq(vault.balanceOf(attacker), shares, "Attacker has shares");
vm.prank(alice);
vault.joinEvent(0);
assertEq(vault.userSharesToCountry(alice, 0), 0, "Alice joined with 0 shares");
vm.prank(attacker);
vm.expectRevert(BriVault.noDeposit.selector);
vault.joinEvent(1);
}
Mitigation:
The function should only use msg.sender for consistency, or properly handle the receiver pattern:
Option 1 (Recommended): Remove receiver parameter and use msg.sender:
function deposit(uint256 assets) public returns (uint256) {
address receiver = msg.sender;
if (block.timestamp >= eventStartDate) {
revert eventStarted();
}
uint256 fee = _getParticipationFee(assets);
if (minimumAmount + fee > assets) {
revert lowFeeAndAmount();
}
uint256 stakeAsset = assets - fee;
stakedAsset[msg.sender] = 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(msg.sender, stakeAsset);
return participantShares;
}
Option 2: Consistently use receiver throughout:
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(receiver, participantShares);
emit deposited(receiver, stakeAsset);
return participantShares;
}