BriVault

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

Stale userSharesToCountry Mapping Causes Incorrect Winner Share Calculation

Description

  • Normal behavior: when a user deposits and then joins a country, the contract should record the user's current vault shares for that country and keep that country-level share record synchronized with any later deposits or burns so final winner distribution is correct.

  • Specific issue: the contract snapshots userSharesToCountry[msg.sender][countryId] at the time of joinEvent() but never updates that mapping on subsequent deposit() or _burn() calls. As a result the country-level snapshot becomes stale and does not reflect the user’s real holdings at payout time, causing incorrect reward distribution.

function joinEvent(uint256 countryId) public {
...
uint256 participantShares = balanceOf(msg.sender);
@> userSharesToCountry[msg.sender][countryId] = participantShares; // snapshot only once
usersAddress.push(msg.sender);
...
}

Risk

Likelihood:

  • A user deposits once, calls joinEvent(), and later deposits additional assets before the event ends — the stale snapshot will be present at settlement.

  • Users frequently top up or cancel participation before the event start (common UX), so the stale condition will occur in normal usage patterns.

Impact:

  • Final payout uses outdated per-country shares, causing winners to receive less than their fair share (denominator undervalued for some, overvalued for others), breaking fairness.

  • Accounting corruption persists on-chain (state variable userSharesToCountry remains wrong), undermining trust and potentially requiring manual off-chain reconciliation or contract migration.

Proof of Concept

Foundry test (PoC) that demonstrates the stale mapping: user deposits, joins, deposits again — balance increases but userSharesToCountry remains at old snapshot.

function test_stalemappingissue() public {
// user1 approves vault to pull tokens
vm.startPrank(user1);
mockToken.approve(address(briVault), type(uint256).max);
// First deposit: user1 deposits 5 tokens (units), receiver = user1
uint256 deposit1 = 5 ether;
uint256 shares1 = briVault.deposit(deposit1, user1);
console.log("shares1 (after first deposit):", shares1);
// user joins country 0 (snapshot stored in userSharesToCountry)
briVault.joinEvent(0);
uint256 snapshotAfterJoin = briVault.userSharesToCountry(user1, 0);
uint256 balanceAfterJoin = briVault.balanceOf(user1);
console.log("snapshotAfterJoin:", snapshotAfterJoin);
console.log("balanceAfterJoin:", balanceAfterJoin);
// Sanity: snapshot should equal balance at time of join
assertEq(snapshotAfterJoin, balanceAfterJoin);
// Second deposit: user1 deposits another 5 tokens
uint256 deposit2 = 5 ether;
uint256 shares2 = briVault.deposit(deposit2, user1);
console.log("shares2 (after second deposit):", shares2);
// Now balanceOf should have increased (shares1 + shares2)
uint256 balanceAfterSecondDeposit = briVault.balanceOf(user1);
uint256 snapshotStillStored = briVault.userSharesToCountry(user1, 0);
console.log("balanceAfterSecondDeposit:", balanceAfterSecondDeposit);
console.log("snapshotStillStored:", snapshotStillStored);
// EXPECTED BUG: snapshotStillStored remains equal to the first snapshot
assertGt(balanceAfterSecondDeposit, snapshotAfterJoin, "balance did not increase after second deposit");
assertEq(snapshotStillStored, snapshotAfterJoin, "userSharesToCountry should be stale (not updated)"); // demonstrates stale mapping
vm.stopPrank();
}
Logs:
shares1 (after first deposit): 4925000000000000000
snapshotAfterJoin: 4925000000000000000
balanceAfterJoin: 4925000000000000000
shares2 (after second deposit): 4925000000000000000
balanceAfterSecondDeposit: 9850000000000000000
snapshotStillStored: 4925000000000000000
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
bube Lead Judge 14 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Deposits after joining the event are not correctly accounted

Support

FAQs

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

Give us feedback!