BriVault

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

Share Inflation Attack via Deposit-Cancel-Deposit Cycle

Root + Impact

The deposit() function mints shares to msg.sender but updates stakedAsset[receiver]. This mismatch allows an attacker to inflate their shares by repeatedly depositing for different receivers and canceling. Since cancelParticipation() only checks stakedAsset[msg.sender] and burns balanceOf(msg.sender), an attacker can deposit for address(1), receive shares, then cancel (which fails the refund but burns 0 shares since stakedAsset[msg.sender] = 0), keeping the minted shares. This breaks the share accounting system.

Impact

Attackers can mint unlimited shares without corresponding asset deposits, diluting legitimate participants' shares and potentially claiming the entire vault balance as a “winner” with inflated shares.

Scenario

// Step 1: Attacker deposits for a different address
vault.deposit(1000 ether, address(0xdead));
// stakedAsset[0xdead] = 1000 ether - fee
// attacker receives shares
// Step 2: Attacker tries to cancel
vault.cancelParticipation();
// refundAmount = stakedAsset[attacker] = 0
// shares = balanceOf(attacker) = shares from deposit
// Burns the shares and refunds 0
// However, the attacker can't directly exploit this due to share burning
// But they can use this pattern:
// Step 1: Deposit for self first
vault.deposit(100 ether, attacker);
// stakedAsset[attacker] = 100 ether - fee
// Step 2: Deposit for another address
vault.deposit(1000 ether, address(0xdead));
// stakedAsset[0xdead] = 1000 ether - fee
// attacker gets more shares
// Step 3: Join event with all accumulated shares
vault.joinEvent(winningCountry);
// All shares are now mapped to attacker's country choice

Affected code

function deposit(uint256 assets, address receiver) public override returns (uint256) {
// ... checks ...
uint256 stakeAsset = assets - fee;
stakedAsset[receiver] = stakeAsset; // Updates receiver
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;
}

Proposed fix

function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
require(receiver == msg.sender, "Can only deposit for self");
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); // Mint to receiver, not msg.sender
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!