BriVault

First Flight #52
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Transferable shares exploit — participants can steal rewards by transferring BTT after joining

Root + Impact

Description

Normal behaviour : Participants deposit assets, receive vault shares (BTT), choose a team and—if their team wins—they withdraw a proportional share of the final pooled assets.

problem: The contract records a participant’s chosen country and the user’s shares at join time, but final payout logic uses the user’s current balance of BTT (and totalWinnerShares is compiled from stored per-user values that can be manipulated). This allows a participant to transfer their BTT shares to any other address after joinEvent() and before setWinner(); the recipient can then call withdraw() and claim rewards that should belong to the transferor.

// joinEvent stores current balance
userSharesToCountry[msg.sender][countryId] = balanceOf(msg.sender);
// withdraw uses balanceOf(msg.sender) (or uses totalWinnerShares built from manipulable values)
uint256 shares = balanceOf(msg.sender); // vulnerable: current balance, not a finalized snapshot
uint256 assetToWithdraw = Math.mulDiv(shares, finalizedVaultAsset, totalWinnerShares);
// Root cause:No immutable snapshot of each participant’s winning shares is taken at finalization. Shares are transferable during the event window, and the withdraw calculation relies on mutable balances.

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoidThis attack can be executed by any participant after calling joinEvent() and before setWinner().

No special permissions are required; the attacker only needs to hold and transfer BTT using "if" statements)


Impact:

  • A single attacker can consolidate many participants’ shares by acquiring/transferring BTT and then drain a disproportionately large portion (potentially the entire) prize pool.

  • Financial loss for honest participants and complete break of payout fairness.Impact 1


Proof of Concept

// Pseudocode PoC
alice.deposit(10 ether, alice);
alice.joinEvent(0);
bob.deposit(10 ether, bob);
bob.joinEvent(0);
// Alice transfers her BTT to Bob after joinEvent
vault.transfer(bob, vault.balanceOf(alice));
// Owner calls setWinner(0)
vault.setWinner(0);
// Bob withdraws — claims > fair share (steals the pool)
vm.prank(bob);
vault.withdraw();

Recommended Mitigation

- // joinEvent: record current balance (optional)
- userSharesToCountry[msg.sender][countryId] = balanceOf(msg.sender);
+ // keep local record if desired (not used for payout)
+ userSharesToCountry[msg.sender][countryId] = balanceOf(msg.sender);
+ // at setWinner(): take final snapshots for the winning country
+ function _computeWinnerSnapshots() internal {
+ totalWinnerShares = 0;
+ for (i over usersAddress) {
+ if (keccak256(bytes(userToCountry[user])) == keccak256(bytes(winner))) {
+ uint256 snap = balanceOf(user); // snapshot at finalize time
+ userSharesSnapshot[user][winnerCountryId] = snap;
+ totalWinnerShares += snap;
+ }
+ }
+ }
- // withdraw uses current balance
- uint256 shares = balanceOf(msg.sender);
+ // withdraw must use the snapshot taken at finalize
+ uint256 shares = userSharesSnapshot[msg.sender][winnerCountryId];
+ require(shares > 0, "no shares");
+ userSharesSnapshot[msg.sender][winnerCountryId] = 0; // prevent double withdraw
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!