MEDIUM severity: CRV claimed as an extraRewardToken will be stuck in Strategy. When rewards are harvested via the strategy (either `OnlyBoost.sol` or `Strategy.sol`, depending on whether the optimizer is set or not), the goal is to claim all rewards and pass them along to the appropriate reward distributor.

The logic attempts to take into account that CRV will be the...

Vulnerable code:
function _claimExtraRewards(address gauge, address rewardDistributor)
    internal
    virtual
    returns (uint256 _rewardTokenClaimed)
{
    /// If the gauge doesn't support extra rewards, skip.
    if (lGaugeType[gauge] > 0) return 0;

    // Cache the reward tokens and their balance before locker
    address[8] memory extraRewardTokens;
    uint256[8] memory snapshotLockerRewardBalances;

    uint8 i;
    address extraRewardToken;
    /// There can be up to 8 extra reward tokens.
    for (i; i < 8;) {
        /// Get extra reward token address.
        extraRewardToken = ILiquidityGauge(gauge).reward_tokens(i);
        if (extraRewardToken == address(0)) break;

        // Add the reward token address on the array
        extraRewardTokens[i] = extraRewardToken;
        // Add the reward token balance ot the locker on the array
        snapshotLockerRewardBalances[i] = ERC20(extraRewardToken).balanceOf(address(locker));

        unchecked {
            ++i;
        }
    }

    /// There's two ways to claim extra rewards:
    /// 1. Call claim_rewards on the gauge with the strategy as receiver.
    /// 2. Call claim_rewards on the gauge with the locker as receiver, then transfer the rewards from the locker to the strategy.
    /// 1 is not supported by all gauges, so we try to call it first, and if it fails, we fallback to 2.
    (bool isRewardReceived,) = locker.execute(
        gauge, 0, abi.encodeWithSignature("claim_rewards(address,address)", address(locker), address(this))
    );

    if (!isRewardReceived) {
        ILiquidityGauge(gauge).claim_rewards(address(locker));
    }

    for (i = 0; i < 8;) {
        extraRewardToken = extraRewardTokens[i];
        if (extraRewardToken == address(0)) break;
        uint256 claimed;
        if (!isRewardReceived) {
            claimed = ERC20(extraRewardToken).balanceOf(address(locker)) - snapshotLockerRewardBalances[i];
            if (claimed != 0) {
                // Transfer the freshly rewards from the locker to this contract.
                _transferFromLocker(extraRewardToken, address(this), claimed);
            }
        }

        if (extraRewardToken == rewardToken) {
            _rewardTokenClaimed += claimed;
        } else {
            claimed = ERC20(extraRewardToken).balanceOf(address(this));
            if (claimed != 0) {
                // Distribute the extra reward token.
                ILiquidityGauge(rewardDistributor).deposit_reward_token(extraRewardToken, claimed);
            }
        }

        unchecked {
            ++i;
        }
    }
}

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;

import "forge-std/Test.sol";

import "src/CRVStrategy.sol";
import "solady/utils/LibClone.sol";
import {Vault} from "src/staking/Vault.sol";
import {IBooster} from "src/interfaces/IBooster.sol";
import {ISDLiquidityGauge, IGaugeController, PoolFactory, CRVPoolFactory} from "src/factory/curve/CRVPoolFactory.sol";

contract CRVExtraRewardsTest is Test {
    Vault vaultImplementation;
    CRVPoolFactory poolFactory;

    CRVStrategy strategy;
    CRVStrategy implementation;

    uint256 public pid = 252;
    ERC20 public token = ERC20(0xfEF79304C80A694dFd9e603D624567D470e1a0e7);
    address public gauge = 0x62B8DA8f1546a092500c457452fC2d45fa1777c4;

    address[] public extraRewardTokens;

    address public constant BOOSTER = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31);

    IGaugeController public constant GAUGE_CONTROLLER = IGaugeController(0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB);

    address public constant VE_CRV = 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2;
    address public constant MINTER = 0xd061D61a4d941c39E5453435B6345Dc261C2fcE0;
    address public constant SD_VOTER_PROXY = 0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6;
    ILocker public locker = ILocker(SD_VOTER_PROXY);
    address public constant REWARD_TOKEN = address(0xD533a949740bb3306d119CC777fa900bA034cd52);
    address public constant gaugeImplementation = address(0x3Dc56D46F0Bd13655EfB29594a2e44534c453BF9);

    function setUp() public {
        vm.createSelectFork("https://eth.llamarpc.com");

        /// Deploy Strategy
        implementation = new CRVStrategy(address(this), SD_VOTER_PROXY, VE_CRV, REWARD_TOKEN, MINTER);
        address _proxy = LibClone.deployERC1967(address(implementation));
        strategy = CRVStrategy(payable(_proxy));
        strategy.initialize(address(this));

        // Give strategy roles from depositor to new strategy
        vm.prank(locker.governance());
        locker.setStrategy(payable(address(strategy)));

        vaultImplementation = new Vault();
        poolFactory = new CRVPoolFactory(
            address(strategy),
            REWARD_TOKEN,
            address(vaultImplementation),
            gaugeImplementation
        );

        strategy.setFactory(address(poolFactory));
    }

    function testZach__CRVExtra() public {
        (address vault, address rewardDist) = poolFactory.create(0x62B8DA8f1546a092500c457452fC2d45fa1777c4);

        // confirm that CRV is a reward token based on the info grabbed from the gauge
        assert(REWARD_TOKEN == ILiquidityGauge(rewardDist).reward_tokens(1));

        uint amount = 100e18;
        deal(address(token), address(this), amount);
        ERC20(address(token)).approve(vault, amount);
        Vault(vault).deposit(address(token), amount, true);

        /// Then skip weeks to harvest SD.
        skip(2 weeks);

        // Claim all the CRV from the gauge (not via extra reward tokens)
        strategy.harvest(address(token), false, false, false);
        console2.log("harvesting native CRV (not extra reward token)...");

        // All the CRV so far is in the reward distributor
        uint rewardDistBalance = ERC20(REWARD_TOKEN).balanceOf(rewardDist);
        uint strategyBalance = ERC20(REWARD_TOKEN).balanceOf(address(strategy));
        console2.log("- reward dist CRV balance: ", rewardDistBalance);
        console2.log("- strategy CRV balance: ", strategyBalance);

        // Claim the CRV from the extra
        strategy.harvest(address(token), false, true, false);
        console2.log("\n  harvesting again to get only the extra rewards CRV...");

        // None of the new CRV claimed flowed to reward distributor, it's stuck in Strategy
        console2.log("- increase in reward dist CRV balance: ", ERC20(REWARD_TOKEN).balanceOf(rewardDist) - rewardDistBalance);
        console2.log("- increase in strategy CRV balance: ", ERC20(REWARD_TOKEN).balanceOf(address(strategy)) - strategyBalance);
    }
}

Logs:
  harvesting native CRV (not extra reward token)...
  - reward dist CRV balance:  196691120093108400
  - strategy CRV balance:  0

  harvesting again to get only the extra rewards CRV...
  - increase in reward dist CRV balance:  0
  - increase in strategy CRV balance:  15879878192329000