Immunefi Audit Competitions
Alchemix V3

58239 sc medium missing aave incentives rewards claiming mechanism leads to permanent loss of protocol royalties

Submitted on Oct 31st 2025 at 16:24:11 UTC by @dobrevaleri for Audit Comp | Alchemix V3

Description

Brief/Intro

The AaveV3ARBUSDCStrategy contract supplies USDC to Aave V3 but fails to implement any mechanism to claim available incentive rewards from Aave's RewardsController, resulting in the permanent loss of these rewards that would otherwise benefit the protocol and its users.

Vulnerability Details

The AaveV3ARBUSDCStrategy contract inherits from MYTStrategy which provides a standardized interface for reward claiming through the claimRewards() function and its internal _claimRewards() implementation. However, the Aave strategy completely omits any implementation of reward claiming functionality.

The base MYTStrategy class provides the framework for reward claiming:

function claimRewards() public virtual returns (uint256) {
    require(!killSwitch, "emergency");
    _claimRewards();
}

function _claimRewards() internal virtual returns (uint256) {}

Aave V3 implements an incentives program through its RewardsController contract that distributes additional tokens to users who supply liquidity to the protocol. These rewards are separate from the yield earned through interest and must be explicitly claimed.

The contract interfaces only include the basic Aave Pool operations but completely omit the RewardsController.

Impact Details

All Aave incentive tokens accumulate in the RewardsController but can never be claimed by the strategy contract, representing a permanent loss of unclaimed royalties.

References

Proof of Concept

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import "forge-std/Test.sol";
import "forge-std/console.sol";
import {AaveV3ARBUSDCStrategy} from "../../strategies/arbitrum/AaveV3ARBUSDCStrategy.sol";
import {IMYTStrategy} from "../../interfaces/IMYTStrategy.sol";
import {IVaultV2} from "../../../lib/vault-v2/src/interfaces/IVaultV2.sol";
import {MockAlchemistAllocator} from "../mocks/MockAlchemistAllocator.sol";
import {MYTTestHelper} from "../libraries/MYTTestHelper.sol";
import {TokenUtils} from "../../libraries/TokenUtils.sol";

// Interface for Aave Rewards Controller
interface IAaveRewardsController {
    function getRewardsBalance(address[] calldata assets, address user) external view returns (uint256);
    function getAllUserRewards(address[] calldata assets, address user)
        external
        view
        returns (address[] memory rewardsList, uint256[] memory unclaimedAmounts);
    function claimAllRewards(address[] calldata assets, address to) external returns (address[] memory rewardsList, uint256[] memory claimedAmounts);
}

// Interface for ERC20
interface IERC20 {
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external returns (bool);
}

contract PoC_AaveV3NoRewardsClaimed is Test {
    // Arbitrum Mainnet addresses
    address public constant USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831;
    address public constant AAVE_V3_USDC_ATOKEN = 0x724dc807b04555b71ed48a6896b6F41593b8C637;
    address public constant AAVE_V3_USDC_POOL = 0x794a61358D6845594F94dc1DB02A252b5b4814aD;
    address public constant PERMIT2 = 0x000000000022d473030f1dF7Fa9381e04776c7c5;
    // ARB_TOKEN is used as an example reward token - Aave V3 may distribute various reward tokens
    address public constant ARB_TOKEN = 0x912CE59144191C1204E64559FE8253a0e49E6548;
    // Aave V3 Rewards Controller on Arbitrum
    address public constant AAVE_REWARDS_CONTROLLER = 0x929EC64c34a17401F460460D4B9390518E5B473e;

    AaveV3ARBUSDCStrategy public strategy;
    address public vault;
    address public allocator;
    address public admin = address(1);
    address public curator = address(2);
    address public operator = address(3);
    address public user1 = address(4);

    function setUp() public {
        // Fork Arbitrum at a recent block
        uint256 forkId = vm.createFork(vm.envString("ARBITRUM_RPC_URL"), 387_030_683);
        vm.selectFork(forkId);

        // Setup vault and strategy
        vm.startPrank(admin);
        vault = address(MYTTestHelper._setupVault(USDC, admin, curator));

        IMYTStrategy.StrategyParams memory params = IMYTStrategy.StrategyParams({
            owner: admin,
            name: "AaveV3ARBUSDC",
            protocol: "AaveV3ARBUSDC",
            riskClass: IMYTStrategy.RiskClass.LOW,
            cap: 10_000e6,
            globalCap: 1e18,
            estimatedYield: 100e6,
            additionalIncentives: true, // Note: strategy indicates it has additional incentives
            slippageBPS: 1
        });

        strategy = new AaveV3ARBUSDCStrategy(vault, params, USDC, AAVE_V3_USDC_ATOKEN, AAVE_V3_USDC_POOL, PERMIT2);

        allocator = address(new MockAlchemistAllocator(vault, admin, operator));
        vm.stopPrank();

        // Configure vault with strategy
        vm.startPrank(curator);
        _vaultSubmitAndFastForward(abi.encodeCall(IVaultV2.setIsAllocator, (allocator, true)));
        IVaultV2(vault).setIsAllocator(allocator, true);
        _vaultSubmitAndFastForward(abi.encodeCall(IVaultV2.addAdapter, address(strategy)));
        IVaultV2(vault).addAdapter(address(strategy));

        bytes memory idData = strategy.getIdData();
        _vaultSubmitAndFastForward(abi.encodeCall(IVaultV2.increaseAbsoluteCap, (idData, 10_000e6)));
        IVaultV2(vault).increaseAbsoluteCap(idData, 10_000e6);
        _vaultSubmitAndFastForward(abi.encodeCall(IVaultV2.increaseRelativeCap, (idData, 1e18)));
        IVaultV2(vault).increaseRelativeCap(idData, 1e18);
        vm.stopPrank();

        // Deposit to vault
        _magicDepositToVault(vault, user1, 1_000_000e6); // 1M USDC
    }

    function _vaultSubmitAndFastForward(bytes memory data) internal {
        IVaultV2(vault).submit(data);
        bytes4 selector = bytes4(data);
        vm.warp(block.timestamp + IVaultV2(vault).timelock(selector));
    }

    function _magicDepositToVault(address _vault, address depositor, uint256 amount) internal {
        deal(USDC, depositor, amount);
        vm.startPrank(depositor);
        TokenUtils.safeApprove(USDC, _vault, amount);
        IVaultV2(_vault).deposit(amount, depositor);
        vm.stopPrank();
    }

    function test_PoC_claimRewards_Returns_Zero_No_Implementation() public {
        // Allocate funds to the strategy
        uint256 depositAmount = 10_000e6; // 10K USDC
        deal(USDC, address(strategy), depositAmount);

        vm.startPrank(vault);
        bytes memory prevAllocationAmount = abi.encode(0);
        strategy.allocate(prevAllocationAmount, depositAmount, "", vault);
        vm.stopPrank();

        uint256 strategyBalance = strategy.realAssets();
        assertGt(strategyBalance, 0, "Strategy should have deposited funds");

        // Advance time to simulate reward accrual
        vm.warp(block.timestamp + 30 days);
        vm.roll(block.number + 216_000);

        // Check balances before claiming
        uint256 strategyARBBalanceBefore = IERC20(ARB_TOKEN).balanceOf(address(strategy));
        uint256 vaultARBBalanceBefore = IERC20(ARB_TOKEN).balanceOf(vault);

        // Call claimRewards() - should return 0 and claim nothing
        uint256 rewardsClaimed = strategy.claimRewards();

        // Check balances after claiming
        uint256 strategyARBBalanceAfter = IERC20(ARB_TOKEN).balanceOf(address(strategy));
        uint256 vaultARBBalanceAfter = IERC20(ARB_TOKEN).balanceOf(vault);

        // Verify the issue: claimRewards() returns 0 and no rewards are transferred
        assertEq(rewardsClaimed, 0, "claimRewards() should return 0 (not implemented)");
        assertEq(strategyARBBalanceAfter, strategyARBBalanceBefore, "Strategy ARB balance should not change");
        assertEq(vaultARBBalanceAfter, vaultARBBalanceBefore, "Vault ARB balance should not change");
    }
}