Summary
Whenever a user decides to add to their stake amount, the call will update their earnings without sending the previous earnings to these users if they have values to claim, thus causing these rewards to be lost and inaccessible forever.
Vulnerability Details
Stake shares doesn't not check if a user has previous claims to be collected from the vault hence this funds are lost.
function stake(uint128 vaultId, uint128 shares) external {
if (shares < Constants.MIN_OF_SHARES_TO_STAKE) {
revert Errors.QuantityOfSharesLessThanTheMinimumAllowed(Constants.MIN_OF_SHARES_TO_STAKE, uint256(shares));
}
Vault.Data storage vault = Vault.loadLive(vaultId);
uint256[] memory vaultsIds = new uint256[]();
vaultsIds[0] = uint256(vaultId);
Vault.recalculateVaultsCreditCapacity(vaultsIds);
@audit>> Distribution.Data storage wethRewardDistribution = vault.wethRewardDistribution;
@audit>> bytes32 actorId = bytes32(uint256(uint160(msg.sender)));
@audit>>
@audit>> wethRewardDistribution.accumulateActor(actorId);
Distribution.Actor storage actor = wethRewardDistribution.actor[actorId];
UD60x18 updatedActorShares = ud60x18(actor.shares).add(ud60x18(shares));
wethRewardDistribution.setActorShares(actorId, updatedActorShares);
IERC20(vault.indexToken).safeTransferFrom(msg.sender, address(this), shares);
emit LogStake(vaultId, msg.sender, shares);
}
Accumulation without first claiming fees will cause this actor to lose this funds.
These was correctly enforced in the unsatking action
function unstake(uint128 vaultId, uint256 shares) external {
Vault.Data storage vault = Vault.loadLive(vaultId);
uint256[] memory vaultsIds = new uint256[]();
vaultsIds[0] = uint256(vaultId);
Vault.recalculateVaultsCreditCapacity(vaultsIds);
@audit>> Distribution.Data storage wethRewardDistribution = vault.wethRewardDistribution;
@audit>> bytes32 actorId = bytes32(uint256(uint160(msg.sender)));
@audit>>
@audit>> UD60x18 amountToClaimX18 = vault.wethRewardDistribution.getActorValueChange(actorId).intoUD60x18();
@audit>>
@audit>> if (!amountToClaimX18.isZero()) revert Errors.UserHasPendingRewards(actorId, amountToClaimX18.intoUint256());
@audit>> wethRewardDistribution.accumulateActor(actorId);
Users are not allowed to lose funds in the unstake function but this fees will be lost in the stake function because it lacks the necessary check
function claimFees(uint128 vaultId) external {
Vault.Data storage vault = Vault.load(vaultId);
bytes32 actorId = bytes32(uint256(uint160(msg.sender)));
if (vault.wethRewardDistribution.actor[actorId].shares == 0) revert Errors.NoSharesAvailable();
UD60x18 amountToClaimX18 = vault.wethRewardDistribution.getActorValueChange(actorId).intoUD60x18();
if (amountToClaimX18.isZero()) revert Errors.NoFeesToClaim();
vault.wethRewardDistribution.accumulateActor(actorId);
address weth = MarketMakingEngineConfiguration.load().weth;
Collateral.Data storage wethCollateral = Collateral.load(weth);
uint256 amountToClaim = wethCollateral.convertUd60x18ToTokenAmount(amountToClaimX18);
IERC20(weth).safeTransfer(msg.sender, amountToClaim);
emit LogClaimFees(msg.sender, vaultId, amountToClaim);
}
Impact
Lose of fee distributed to this actor/user.
Tools Used
Manual Review
Recommendations
implement the same check as done in the unstake function
implement another claim function that can allow the contract to call fee-distribution contract directly and forward this rewards to the user.