The deposit function is the only intended way to add assets to the vault, and it correctly charges a participationFeeBsp on all deposited funds.
But this security assumption is broken by the _convertToShares function, which prices shares using the contract's entire token balance. An attacker can exploit this when totalSupply is 0 by first sending a large amount of tokens via a direct transfer (which pays no fee) and then calling deposit with a tiny amount. This tiny deposit triggers the if (totalShares == 0) logic, minting shares 1:1.
Likelihood:
This attack is only possible when the totalSupply of shares is 0. This is a pre-condition and occurs when the vault is first deployed or all previous depositors have redeemed or burned their shares.
Impact:
The attacker avoids paying the participationFee, denying the participationFeeAddress its intended revenue on the attacker's large stake.
This PoC (test_FirstStakerBypassFee) demonstrates the fee bypass at the vault's creation (when totalSupply is 0).
user1 (attacker) transfers ~20 ether directly to the briVault contract. No fee is paid on this amount. balanceOf(vault) is now ~20 ether.
user1 then deposits a tiny amount (0.00024 ether). The 1.5% fee is only paid on this tiny amount. Because totalSupply is 0, user1 gets the first shares 1:1 for their stakeAsset, which are now backed by all ~20 ether in the contract.
user2 deposits 20 ether normally. They pay the full 1.5% fee (0.3 ether).
The logs show user1 withdraws their full ~20 ether (having paid almost no fee), while user2 only gets back 19.7 ether (their principal minus the fee).
The mitigation is to ensure the contract's share price calculation is based on its own tracked assets, not the public balanceOf. This prevents poisoning the vault with fee-free transfers.
Add a state variable uint256 internalTotalAssets to track assets only from deposits.
Increase this variable in deposit (by stakeAsset) and decrease it in cancelParticipation and withdraw.
Use this internalTotalAssets variable in _convertToShares instead of IERC20(asset()).balanceOf(address(this)).
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.