Lucene search

K
code423n4Code4renaCODE423N4:2023-07-TAPIOCA-FINDINGS-ISSUES-1688
HistoryAug 04, 2023 - 12:00 a.m.

Direct claim of convex rewards causes rewards to get stuck

2023-08-0400:00:00
Code4rena
github.com
5
rewards stuck
convex
claimzap
vulnerability
strategy

AI Score

6.7

Confidence

High

Lines of code

Vulnerability details

Impact

ConvexTriCryptoStrategy does not take into account that rewards from Convex can be claimed directly on behalf of any address. All rewards that get into the strategy contract this way will get stuck and compounding of yield will be denied.

Proof of Concept

The ConvexTriCryptoStrategy allows the claim and conversion of Convex rewards in its compound function:

    function compound(bytes memory data) public {
        if (data.length == 0) return;
        (uint256[] memory rewards, address[] memory tokens) = _executeClaim(
            data
        );

        //iterate through rewards and swap to wETH ...

The _executeClaim function performs the claim and then checks the balance difference of the contract before and after to calculate the rewards:

//parse input and check balances before
...

zap.claimRewards(
    tempData.rewardContracts,
    tempData.extraRewardContracts,
    tempData.tokenRewardContracts,
    tempData.tokenRewardTokens,
    extrasTempData.depositCrvMaxAmount,
    extrasTempData.minAmountOut,
    extrasTempData.depositCvxMaxAmount,
    extrasTempData.spendCvxAmount,
    extrasTempData.options
);

uint256[] memory balancesAfter = new uint256[](tempData.tokens.length);
for (uint256 i = 0; i < tempData.tokens.length; i++) {
    balancesAfter[i] = IERC20(tempData.tokens[i]).balanceOf(
        address(this)
    );
}

uint256[] memory finalBalances = new uint256[](tempData.tokens.length);
for (uint256 i = 0; i < tempData.tokens.length; i++) {
    finalBalances[i] = balancesAfter[i] - balancesBefore[i];
}

return (finalBalances, tempData.tokens);

The ClaimZap contract (Link) called here is from Convex and allows a batch claim of rewards from rewardContracts, extraRewardContracts and tokenRewardContracts:

for(uint256 i = 0; i < rewardContracts.length; i++){
    IBasicRewards(rewardContracts[i]).getReward(msg.sender,true);
}
//claim from extra rewards
for(uint256 i = 0; i < extraRewardContracts.length; i++){
    IBasicRewards(extraRewardContracts[i]).getReward(msg.sender);
}

Looking at the getReward functions (here and here), it can be seen that anyone is allowed to claim for any address:

//BaseRewardPool (rewardContract)
function getReward(address _account, bool _claimExtras) public updateReward(_account) returns(bool){
    uint256 reward = earned(_account);
    if (reward > 0) {
        rewards[_account] = 0;
        rewardToken.safeTransfer(_account, reward);
        IDeposit(operator).rewardClaimed(pid, _account, reward);
        emit RewardPaid(_account, reward);
    }

    //also get rewards from linked rewards
    if(_claimExtras){
        for(uint i=0; i < extraRewards.length; i++){
            IRewards(extraRewards[i]).getReward(_account);
        }
    }
    return true;
}

//VirtualRewardPool (extraRewards)
function getReward(address _account) public updateReward(_account){
    uint256 reward = earned(_account);
    if (reward > 0) {
        rewards[_account] = 0;
        rewardToken.safeTransfer(_account, reward);
        emit RewardPaid(_account, reward);
    }
}

So, if anyone claims directly on behalf of the strategy, the funds will get locked in the strategy contract, since the swap after the claim only accounts for balance differences within the compound call and no other function exists to handle unswapped rewards.

Tools Used

Manual Review

Recommended Mitigation Steps

Dont check for balance difference and take the whole balance in the contract for reward tokens after the claim. Add any input validation if necessary.

Assessed type

Other


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

All reactions

AI Score

6.7

Confidence

High