BriVault

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

Deposit Overwrite Causes Inaccurate State and Refund Loss

Description

The function deposit() in BriVault overwrites the previous stake amount stored in stakedAsset[user] instead of accumulating it.

This causes a mismatch between the vault’s total token balance and the internal accounting for each user.
As a result:

When a user deposits multiple times, only the last deposit amount (after fees) is stored in stakedAsset[user].

Functions like cancelParticipation() or joinEvent() rely on this wrong value.

This leads to partial refunds and incorrect event participation state.

function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
if (block.timestamp >= eventStartDate) {
revert eventStarted();
}
uint256 fee = _getParticipationFee(assets);
// charge on a percentage basis points
if (minimumAmount + fee > assets) {
revert lowFeeAndAmount();
}
uint256 stakeAsset = assets - fee;
stakedAsset[receiver] = stakeAsset; //affected state or function

Affected Functions

  • deposit()

  • cancelParticipation()

  • joinEvent()

Impact

  • Impact Users lose part of their funds during refund or incorrect participation recording.

Proof of Concept (POC)

Add this test case to your suite (test/briVault.t.sol):

function testDepositOverwrite() public {
vm.startPrank(user1);
mockToken.approve(address(briVault), 100 ether);
// Deposit twice
briVault.deposit(1 ether, user1);
briVault.deposit(2 ether, user1);
vm.stopPrank();
uint256 feeRate = briVault.participationFeeBsp(); // 1.5%
uint256 BASE = 10000;
uint256 stakeAfterFee1 = 1 ether - ((1 ether * feeRate) / BASE);
uint256 stakeAfterFee2 = 2 ether - ((2 ether * feeRate) / BASE);
uint256 expectedVaultTotal = stakeAfterFee1 + stakeAfterFee2;
uint256 recordedStaked = briVault.stakedAsset(user1);
uint256 vaultBalance = mockToken.balanceOf(address(briVault));
console.log("Vault balance:", vaultBalance);
console.log("Recorded stakedAsset[user1]:", recordedStaked);
assertEq(vaultBalance, expectedVaultTotal, "Vault holds full amount");
assertEq(recordedStaked, stakeAfterFee2, "Only last stake stored");
assertTrue(recordedStaked != expectedVaultTotal, "Mismatch proves overwrite bug");
}

Log test:

an 1 test for test/briVaultTest.t.sol:DepositPoCTest
[PASS] testDepositOverwrite() (gas: 232612)
Logs:
feeRate (bps): 150
stakeAfterFee1: 985000000000000000
stakeAfterFee2: 1970000000000000000
expectedVaultTotal (sum of stakes): 2955000000000000000
recorded stakedAsset[user1]: 1970000000000000000
vault token balance: 2955000000000000000
fee receiver balance: 45000000000000000
minted shares for user1: 2955000000000000000
totalSupply (BTT): 2955000000000000000

Expected stake must be 2955000000000000000
but contract only record last deposit is 1970000000000000000

Impact on

cancelParticipation() Refund Mismatch

Add this test case to your suite:

function test_cancelParticipationRefundMismatch() public {
vm.startPrank(user1);
mockToken.approve(address(briVault), 100 ether);
briVault.deposit(1 ether, user1);
briVault.deposit(2 ether, user1);
uint256 balanceBefore = mockToken.balanceOf(user1);
briVault.cancelParticipation();
uint256 balanceAfter = mockToken.balanceOf(user1);
vm.stopPrank();
uint256 refunded = balanceAfter - balanceBefore;
console.log("Refunded:", refunded);
assertTrue(refunded < 2.955 ether, "Refund mismatch proves overwrite bug");
}

Log Test CancelParticipation :

Ran 1 test for test/briVaultTest.t.sol:DepositPoCTest
[PASS] test_cancelParticipationRefundMismatch() (gas: 175749)
Logs:
Refunded: 1970000000000000000
Vault remaining balance: 985000000000000000

Refunded only equals the last deposit minus fee, not the total of both deposits.
Impact on function

joinEvent() Records Only Last Stake

Add this test case to your suite:

function test_joinEventRecordsOnlyLastStake() public {
vm.startPrank(user1);
mockToken.approve(address(briVault), 100 ether);
briVault.deposit(1 ether, user1);
briVault.deposit(2 ether, user1);
briVault.joinEvent(10);
vm.stopPrank();
assertLt(
briVault.stakedAsset(user1),
mockToken.balanceOf(address(briVault)),
"joinEvent uses wrong stake accounting"
);
}

joinevent test log :

Ran 1 test for test/briVaultTest.t.sol:DepositPoCTest
[PASS] test_joinEventRecordsOnlyLastStake() (gas: 338295)
Logs:
User recorded stake: 1970000000000000000
Vault token balance: 2955000000000000000

Event participation logic reads only the latest stake, undercounting the total user contribution.

Root Cause

Inside deposit(), the code likely looks like this:

stakedAsset[receiver] = stakeAsset; //overwrites previous value

Risk

Integrity Risk

The contract’s internal accounting (stakedAsset) does not reflect the actual vault balance, causing logical inconsistencies.

Recommended Mitigation

The issue occurs because the deposit() function overwrites the user’s previous stake instead of accumulating it.
This leads to inconsistent accounting between the vault’s actual balance and the recorded value of each participant.

To fix this, the contract should accumulate deposits rather than overwrite the previous value.
Replace the line that currently assigns the stake value:
This change ensures that every time a user makes a new deposit, the value is added to their total stake rather than replacing the previous one.

- stakedAsset[receiver] = stakeAsset;
+ stakedAsset[receiver] += stakeAsset;
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

`stakedAsset` Overwritten on Multiple Deposits

Vault tracks only a single deposit slot per user and overwrites it on every call instead of accumulating the total.

Support

FAQs

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

Give us feedback!