Lucene search

K
code423n4Code4renaCODE423N4:2023-01-POPCORN-FINDINGS-ISSUES-757
HistoryFeb 07, 2023 - 12:00 a.m.

RewardTokens can be locked in MultiRewardStaking contract when the rewardsEndTimestamp of the rewardsTokens are different.

2023-02-0700:00:00
Code4rena
github.com
5
multirewardstaking
rewardtokens
rewardsendtimestamp
claimrewards
fund locking
claimable tokens
rewardspersecond
amount

Lines of code

Vulnerability details

Impact

To claim reward tokens from the MultiRewardStaking contract deployed, a user must call claimRewards(address user, IERC20[] memory _rewardsTokens).
The _rewardsTokens array is populated with getAllRewardsTokens() which returns all the reward Tokens the MultiStakingContract has.
claimRewards calls will likely revert in many cases, locking funds (of rewardTokens) in the staking contract.

Proof of Concept

The VaultController contract is in charge of deploying Staking contracts.
A MultiRewardStaking contract is deployed with deployStaking(IERC20 asset), based on the latest staking template activeTemplateId[STAKING].

Considering a MultiStakingContract with N reward tokens (such as N > 1) IERC20[] public rewardTokens = [iRewardToken1, iRewardToken2, …, iRewardTokenN],

The user must give an IERC20[] memory rewardTokens to call claimRewards(user, rewardTokens). It can be done with getAllRewardsTokens()
claimRewards function in MultiRewardStaking.sol GitHub Link

However, if the balance of one of the N reward tokens is empty (i.e it exists iRewardToken in rewardTokens such as ERC20(iRewardToken).balanceOf(address(staking)) == 0 is true ) and the other tokens are claimable, claimRewards(user, rewardTokens) will revert without the claimable tokens being claimed.

One could say that the user must construct a sub-array of rewardTokens, IERC20[] memory claimableRewardTokens which is only made of the current claimable reward tokens interfaces:
Every iRewardToken in [iRewardToken1 ; iRewardTokenN] where accruedRewards[user][iRewardToken] > 0 after the call of accrueRewards(msg.sender, user)
But there is no logic implemented to get those specific tokens and it would need to compute modifier accrueRewards(msg.sender, user) to know which interfaces to add before calling claimRewards(user, claimableRewardTokens):

The variables that can set the MultiRewardStaking contract in this locked funds state are rewardsPerSecond &amount, (rewardTokenAmount).

Taking In J such as I != J and I,J in [1;N]

rewardsEndTimestampI = block.timestamp + (amount1 / uint256(rewardsPerSecond1))
rewardsEndTimestampJ = block.timestamp + (amount2 / uint256(rewardsPerSecond2))

_calcRewardsEnd function GitHub Link

If rewardsEndTimestampI != rewardsEndTimestampJ, one of the reward token claim attempts will revert as seen above.

There are multiple ways to set up the reward tokens accordingly:

As a basic PoC, I’ve set a staking contract with two reward tokens:

  • [rewardToken1, rewardToken2]
  • Same reward speed
  • Different reward amounts.
  1. The first claimRewards call claim 10% of the rewardToken1 and fully claims the rewardToken2.
  2. The second claimRewards call tries to claim another 10% of the rewardToken1 funds but reverts due to rewardToken2 empty balance.

test__claim_multiple_tokens_different_funds source code

test__claim_multiple_tokens_different_funds logs 1
test__claim_multiple_tokens_different_funds logs 1

Tools Used

Manual Review

Recommended Mitigation Steps

The claimRewards function from MultiRewardStaking.sol could emit an event instead of reverting, so the claimable rewards can be claimed, regardless of the user input.

/// This token has not rewards claimable
event ZeroRewards(IERC20 rewardToken);

function claimRewards(
	address user,
	ERC20[] memory _rewardTokens
) external accrueRewards(msg.sender, user) {
	for (uint8 i; i < _rewardTokens.length; i++) {
		uint256 rewardAmount = accruedRewards[user][_rewardTokens[i]];
		if (rewardAmount == 0) {
			emit ZeroRewards(_rewardTokens[i]);
		} else {
			EscrowInfo memory escrowInfo = escrowInfos[_rewardTokens[i]];
			if (escrowInfo.escrowPercentage > 0) {
				_lockToken(user, _rewardTokens[i], rewardAmount, escrowInfo);
				emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, true);
			} else {
				_rewardTokens[i].transfer(user, rewardAmount);
					emit RewardsClaimed(
						user,
						_rewardTokens[i],
						rewardAmount,
						false
					);
			}
			
			accruedRewards[user][_rewardTokens[i]] = 0;
		}
		
	}
}  

The text was updated successfully, but these errors were encountered:

All reactions