BriVault

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

H01. Broken Accounting Between stakedAsset and ERC20 Shares

Root + Impact

Description

  • Under normal conditions, the vault uses ERC20-compatible shares to represent ownership of deposited assets. These shares are freely transferable, following the ERC4626 design. Separately, the vault also maintains a mapping stakedAsset[address] to track each depositor’s contributed assets for refund or cancellation logic.

  • However, when shares are transferred between users, the stakedAsset mapping remains unchanged. This creates an inconsistency between the on-chain share ownership (ERC20 logic) and the internal accounting (stakedAsset). As a result, a depositor who transfers away their shares can still call cancelParticipation() to reclaim their deposit, while the transferee retains shares that can later be redeemed via withdraw(). This enables double refunds or theft of vault funds.

function cancelParticipation() public {
...
@> uint256 refundAmount = stakedAsset[msg.sender]; // Refunds based on mapping
@> stakedAsset[msg.sender] = 0; // No check for current share balance
IERC20(asset()).safeTransfer(msg.sender, refundAmount);
emit cancelParticipationEvent(msg.sender, refundAmount);
}

Risk

Likelihood:

  • This occurs whenever a depositor transfers their ERC4626 shares to another address before event start or vault finalization.

  • The vault does not restrict share transfers and does not validate ownership consistency between shares and the stakedAsset mapping.

Impact:

  • A user can transfer their shares to another address and still receive a full refund using cancelParticipation().

  • The new share owner can later call withdraw() to claim the same underlying assets, causing vault under-collateralization and potential total asset drain.


Proof of Concept

function test_doubleRefund_exploit() public {
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
uint256 user1Shares = briVault.deposit(5 ether, user1);
vm.stopPrank();
// user1 transfers shares to user2
vm.startPrank(user1);
briVault.transfer(user2, user1Shares);
vm.stopPrank();
// user1 cancels participation despite no longer owning shares
vm.startPrank(user1);
briVault.cancelParticipation();
vm.stopPrank();
// user2 still holds valid shares and can later withdraw
vm.warp(eventEndDate + 1);
vm.startPrank(owner);
briVault.setCountry(countries);
briVault.setWinner(10);
vm.stopPrank();
vm.startPrank(user2);
briVault.withdraw();
vm.stopPrank();
// Assert: vault paid out twice for same 5 ETH deposit
assertGt(mockToken.balanceOf(user1), 0); // Refunded
assertGt(mockToken.balanceOf(user2), 0); // Withdrew funds
}

Explanation:

  1. user1 deposits 5 ETH and receives shares.

  2. user1 transfers all shares to user2.

  3. The vault still records stakedAsset[user1] = 5 ether.

  4. user1 calls cancelParticipation() and receives a refund, but user2’s shares remain valid.

  5. When the vault is finalized, user2 can withdraw again, resulting in a double payout from the same original deposit.


Recommended Mitigation

The vault must ensure that the deposit accounting (stakedAsset) remains consistent with ERC20 share ownership. This can be achieved by either disabling share transfers or by updating the accounting logic whenever transfers occur.

- mapping(address => uint256) public stakedAsset;
+ mapping(address => uint256) public stakedAsset;
+ function _update(address from, address to, uint256 value) internal override {
+ if (from != address(0)) {
+ stakedAsset[from] -= value;
+ }
+ if (to != address(0)) {
+ stakedAsset[to] += value;
+ }
+ super._update(from, to, value);
+ }

This ensures that deposits and refunds always correspond to the actual token holders, preventing refund or withdrawal double-spending.

Updates

Appeal created

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

Unrestricted ERC4626 functions

Support

FAQs

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

Give us feedback!