A logical error in briVault::deposit() can cause incorrect accounting of a user’s total deposited assets when multiple deposits are made. This will result in users receiving less than their entitled balance - specifically, only the amount from the most recent deposit minus fees—when calling briVault::cancelParticipation, potentially leading to permanent loss of previously deposited funds.
Description
-
Normal behavior - If a user makes multiple deposits, the total amount of deposited assets must be accurately tracked in the stakedAsset mapping. If the user later on decided to cancel participation, they should recover the full sum of their deposits minus the applicable fees.
-
Issue - Depositing multiple times is not being handled correctly which leads to loss of funds if a user wants to quit and cancels their participation.
function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
if (block.timestamp >= eventStartDate) {
revert eventStarted();
}
uint256 fee = _getParticipationFee(assets);
if (minimumAmount + fee > assets) {
revert lowFeeAndAmount();
}
uint256 stakeAsset = assets - fee;
@> stakedAsset[receiver] = stakeAsset;
uint256 participantShares = _convertToShares(stakeAsset);
IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
_mint(msg.sender, participantShares);
emit deposited (receiver, stakeAsset);
return participantShares;
}
Risk
Likelihood:
Impact:
Proof of Concept
User1 deposits some amount of tokens - 5e18
User1 deposits one more time - 4e18
User1 decided he wants to cancel participation
His shares are burned, and he receives back 4e18 - depositFee, resulting in a loss relative to his original deposit amount.
function test_lossOfFundsWhenQuitting() public {
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
uint256 shares1 = briVault.balanceOf(user1);
mockToken.approve(address(briVault), 4 ether);
briVault.deposit(4 ether, user1);
shares1 = briVault.balanceOf(user1);
console.log("user's assets => ",briVault.stakedAsset(user1));
briVault.cancelParticipation();
assertEq(0, briVault.balanceOf(user1));
assertNotEq(0, mockToken.balanceOf(address(briVault)));
}
Recommended Mitigation
In the briVault.deposit() consider updating the stakedAsset mapping correctly
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;
+ stakedAsset[receiver] += stakeAsset;
uint256 participantShares = _convertToShares(stakeAsset);
IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
_mint(msg.sender, participantShares);
emit deposited (receiver, stakeAsset);
return participantShares;
}