diff --git a/src/core/RewardsManager.sol b/src/core/RewardsManager.sol index eba3bf94..00a34352 100644 --- a/src/core/RewardsManager.sol +++ b/src/core/RewardsManager.sol @@ -5,6 +5,7 @@ import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Ini import {AccessControlUpgradeable} from "@openzeppelin-upgradeable/contracts/access/AccessControlUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; +import {EnumerableSetUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/structs/EnumerableSetUpgradeable.sol"; import {IRewardsCoordinator} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; import {IRewardsCoordinatorTypes} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -16,6 +17,8 @@ import {ILiquidToken} from "../interfaces/ILiquidToken.sol"; import {ILiquidTokenManager} from "../interfaces/ILiquidTokenManager.sol"; /// @title RewardsManager +/// @notice Manages reward claims from EigenLayer and distributes them to the Liquid Token +/// @dev Handles both supported and unsupported tokens, transferring all value to LAT holders contract RewardsManager is IRewardsManager, Initializable, @@ -25,6 +28,7 @@ contract RewardsManager is { using SafeERC20 for IERC20; using Math for uint256; + using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; // ------------------------------------------------------------------------------ // State @@ -42,11 +46,11 @@ contract RewardsManager is /// @notice Unsupported rewards which remain in the contract until swapped out mapping(address => uint256) public unsupportedAssetBalances; - address[] public unsupportedAssets; + EnumerableSetUpgradeable.AddressSet private _unsupportedAssets; /// @notice Claimer for earners on EL mapping(address => bool) public isClaimerFor; - address[] public claimerFor; + EnumerableSetUpgradeable.AddressSet private _claimerForSet; // ------------------------------------------------------------------------------ // Init functions @@ -61,6 +65,7 @@ contract RewardsManager is function initialize(Init memory init) public initializer { __AccessControl_init(); __ReentrancyGuard_init(); + __Pausable_init(); if ( address(init.initialOwner) == address(0) || @@ -100,6 +105,8 @@ contract RewardsManager is function processClaims( IRewardsCoordinatorTypes.RewardsMerkleClaim[] calldata claims ) external override nonReentrant whenNotPaused { + require(claims.length <= 50, "Too many claims"); // Prevent gas exhaustion + for (uint256 i = 0; i < claims.length; i++) { _processClaim(claims[i]); } @@ -109,6 +116,16 @@ contract RewardsManager is /// @dev OUT OF SCOPE FOR V2 /// function swapAndTransferRewards() external override nonReentrant whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) {} + /// @inheritdoc IRewardsManager + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /// @inheritdoc IRewardsManager + function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { + _unpause(); + } + // ------------------------------------------------------------------------------ // Getter functions // ------------------------------------------------------------------------------ @@ -122,6 +139,25 @@ contract RewardsManager is return balances; } + /// @inheritdoc IRewardsManager + function claimerFor() external view returns (address[] memory) { + return _claimerForSet.values(); + } + + /// @inheritdoc IRewardsManager + function claimerForLength() external view returns (uint256) { + return _claimerForSet.length(); + } + + /// @inheritdoc IRewardsManager + function unsupportedAssets() external view returns (address[] memory) { + return _unsupportedAssets.values(); + } + + function unsupportedAssetsLength() external view returns (uint256) { + return _unsupportedAssets.length(); + } + // ------------------------------------------------------------------------------ // Internal functions // ------------------------------------------------------------------------------ @@ -131,87 +167,56 @@ contract RewardsManager is address earner = claim.earnerLeaf.earner; if (!_verifyAndUpdateClaimerFor(earner)) revert NotClaimerFor(earner); - // Call for claim on EL - rewardsCoordinator.processClaim(claim, address(this)); - - // Record current balances of all expected assets from this claim - IERC20[] memory expectedSupportedAssets = new IERC20[](claim.tokenLeaves.length); - IERC20[] memory expectedUnsupportedAssets = new IERC20[](claim.tokenLeaves.length); - uint256[] memory supportedBalances = new uint256[](claim.tokenLeaves.length); - uint256[] memory unsupportedBalances = new uint256[](claim.tokenLeaves.length); + // Get unique tokens from the claim + IERC20[] memory uniqueTokens = _getUniqueTokensFromClaim(claim); + // Categorize tokens into supported and unsupported IERC20[] memory allSupportedAssets = liquidTokenManager.getSupportedTokens(); + + IERC20[] memory supportedTokens = new IERC20[](uniqueTokens.length); + IERC20[] memory unsupportedTokens = new IERC20[](uniqueTokens.length); uint256 supportedCount = 0; uint256 unsupportedCount = 0; - for (uint256 i = 0; i < claim.tokenLeaves.length; i++) { - IERC20 currentToken = claim.tokenLeaves[i].token; - bool tokenAlreadyProcessed = false; - - for (uint256 k = 0; k < supportedCount; k++) { - if (expectedSupportedAssets[k] == currentToken) { - tokenAlreadyProcessed = true; - break; - } - } - - if (!tokenAlreadyProcessed) { - for (uint256 k = 0; k < unsupportedCount; k++) { - if (expectedUnsupportedAssets[k] == currentToken) { - tokenAlreadyProcessed = true; - break; - } - } - } - - if (!tokenAlreadyProcessed) { - bool isSupported = false; - - for (uint256 j = 0; j < allSupportedAssets.length; j++) { - if (allSupportedAssets[j] == currentToken) { - isSupported = true; - break; - } - } + for (uint256 i = 0; i < uniqueTokens.length; i++) { + IERC20 currentToken = uniqueTokens[i]; + bool isSupported = _isTokenSupported(currentToken, allSupportedAssets); - if (isSupported) { - expectedSupportedAssets[supportedCount] = currentToken; - supportedBalances[supportedCount] = currentToken.balanceOf(address(this)); - supportedCount++; - } else { - expectedUnsupportedAssets[unsupportedCount] = currentToken; - unsupportedBalances[unsupportedCount] = currentToken.balanceOf(address(this)); - unsupportedCount++; - } + if (isSupported) { + supportedTokens[supportedCount] = currentToken; + supportedCount++; + } else { + unsupportedTokens[unsupportedCount] = currentToken; + unsupportedCount++; } } - // Trim arrays to actual sizes - assembly { - mstore(expectedSupportedAssets, supportedCount) - mstore(expectedUnsupportedAssets, unsupportedCount) - mstore(supportedBalances, supportedCount) - mstore(unsupportedBalances, unsupportedCount) - } + // Resize arrays to actual counts + supportedTokens = _resizeTokenArray(supportedTokens, supportedCount); + unsupportedTokens = _resizeTokenArray(unsupportedTokens, unsupportedCount); + + // Process the claim on EigenLayer + rewardsCoordinator.processClaim(claim, address(this)); // Update balances for unsupported tokens // These tokens will stay in the contract until `swapAndTransferRewards` is called - _setAssetBalances(expectedUnsupportedAssets, unsupportedBalances); + uint256[] memory unsupportedAmounts = _setAssetBalances(unsupportedTokens); // Transfer all supported assets to `LiquidToken` - uint256[] memory netTransferredAmounts = _transferRewards(expectedSupportedAssets, supportedBalances); + uint256[] memory netTransferredAmounts = _transferRewards(supportedTokens); emit RewardsClaimed( claim.rootIndex, earner, - expectedSupportedAssets, + supportedTokens, netTransferredAmounts, - expectedUnsupportedAssets, - unsupportedBalances + unsupportedTokens, + unsupportedAmounts ); } - /// @dev Called by `setClaimerFor` and `_processClaim` + /// @dev Verify and update claimer status for an earner + /// @dev Called by `_processClaim` function _verifyAndUpdateClaimerFor(address earner) internal returns (bool) { if (address(earner) == address(0)) revert ZeroAddress(); @@ -222,56 +227,103 @@ contract RewardsManager is if (isClaimerOnEl) { if (!isClaimerFor[earner]) { isClaimerFor[earner] = true; - claimerFor.push(earner); - + _claimerForSet.add(earner); emit ClaimerForAdded(earner); } } else { if (isClaimerFor[earner]) { isClaimerFor[earner] = false; - _removeClaimerFor(earner); + _claimerForSet.remove(earner); + emit ClaimerForRemoved(earner); } } return isClaimerOnEl; } - /// @dev Called by `_verifyAndUpdateClaimerFor` - function _removeClaimerFor(address earner) private { - for (uint i = 0; i < claimerFor.length; i++) { - if (claimerFor[i] == earner) { - claimerFor[i] = claimerFor[claimerFor.length - 1]; - claimerFor.pop(); - emit ClaimerForRemoved(earner); - break; + /// @dev Get unique tokens from claim to avoid processing duplicates + /// @dev Called by `_processClaim` + function _getUniqueTokensFromClaim( + IRewardsCoordinatorTypes.RewardsMerkleClaim calldata claim + ) internal pure returns (IERC20[] memory) { + IERC20[] memory tempTokens = new IERC20[](claim.tokenLeaves.length); + uint256 uniqueCount = 0; + + for (uint256 i = 0; i < claim.tokenLeaves.length; i++) { + IERC20 currentToken = claim.tokenLeaves[i].token; + bool isDuplicate = false; + + // Check if token already exists in our temp array + for (uint256 j = 0; j < uniqueCount; j++) { + if (tempTokens[j] == currentToken) { + isDuplicate = true; + break; + } + } + + if (!isDuplicate) { + tempTokens[uniqueCount] = currentToken; + uniqueCount++; + } + } + + return _resizeTokenArray(tempTokens, uniqueCount); + } + + /// @dev Helper to check if token is supported + /// @dev Called by `_processClaim` + function _isTokenSupported(IERC20 token, IERC20[] memory supportedTokens) internal pure returns (bool) { + for (uint256 i = 0; i < supportedTokens.length; i++) { + if (supportedTokens[i] == token) { + return true; } } + return false; } + /// @dev Resize token array to actual size /// @dev Called by `_processClaim` - function _setAssetBalances(IERC20[] memory assets, uint256[] memory amounts) internal { - if (assets.length != amounts.length) revert ArrayLengthMismatch(); + function _resizeTokenArray(IERC20[] memory arr, uint256 newSize) internal pure returns (IERC20[] memory) { + IERC20[] memory resized = new IERC20[](newSize); + for (uint256 i = 0; i < newSize; i++) { + resized[i] = arr[i]; + } + return resized; + } + /// @dev Called by `_processClaim` + function _setAssetBalances(IERC20[] memory assets) internal returns (uint256[] memory) { + uint256[] memory amounts = new uint256[](assets.length); for (uint256 i = 0; i < assets.length; i++) { - if (unsupportedAssetBalances[address(assets[i])] == 0 && amounts[i] > 0) { - unsupportedAssets.push(address(assets[i])); + IERC20 asset = assets[i]; + uint256 oldBalance = unsupportedAssetBalances[address(asset)]; + uint256 newBalance = asset.balanceOf(address(this)); + + if (!_unsupportedAssets.contains(address(asset)) && newBalance > 0) { + // Asset doesn't exist in set yet + _unsupportedAssets.add(address(asset)); + } + if (oldBalance != newBalance) { + unsupportedAssetBalances[address(asset)] = newBalance; + amounts[i] = newBalance; + emit UnsupportedAssetBalanceUpdated(address(asset), oldBalance, newBalance); } - unsupportedAssetBalances[address(assets[i])] = amounts[i]; } + + return amounts; } /// @dev Called by `_processClaim` - function _transferRewards(IERC20[] memory assets, uint256[] memory amounts) internal returns (uint256[] memory) { - if (assets.length != amounts.length) revert ArrayLengthMismatch(); - + function _transferRewards(IERC20[] memory assets) internal returns (uint256[] memory) { // Transfer to `LiquidToken` and calculate actual net amounts received uint256[] memory netTransferredAmounts = new uint256[](assets.length); for (uint256 i = 0; i < assets.length; i++) { - uint256 liquidTokenBalanceBefore = assets[i].balanceOf(address(liquidToken)); - uint256 rewardsManagerBalanceBefore = amounts[i]; + IERC20 asset = assets[i]; + uint256 rewardsManagerBalanceBefore = asset.balanceOf(address(this)); if (rewardsManagerBalanceBefore > 0) { + uint256 liquidTokenBalanceBefore = asset.balanceOf(address(liquidToken)); assets[i].safeTransfer(address(liquidToken), rewardsManagerBalanceBefore); uint256 liquidTokenBalanceAfter = assets[i].balanceOf(address(liquidToken)); netTransferredAmounts[i] = liquidTokenBalanceAfter - liquidTokenBalanceBefore; @@ -286,6 +338,7 @@ contract RewardsManager is return netTransferredAmounts; } + /// @dev Get balance of unsupported asset /// @dev Called by `balanceAssets` function _balanceAsset(IERC20 asset) internal view returns (uint256) { return unsupportedAssetBalances[address(asset)]; diff --git a/src/interfaces/IRewardsManager.sol b/src/interfaces/IRewardsManager.sol index 82ebac59..aef1bc4d 100644 --- a/src/interfaces/IRewardsManager.sol +++ b/src/interfaces/IRewardsManager.sol @@ -28,10 +28,13 @@ interface IRewardsManager { // EVENTS // ============================================================================ + /// @notice Emitted when unsupported asset balance is updated + event UnsupportedAssetBalanceUpdated(address indexed asset, uint256 oldBalance, uint256 newBalance); + /// @notice Emitted when rewards are claimed /// @dev These values tell us the actual tokens realized by the LAT after a process claim procedure - /// @dev The values may differ from the corresponding EL event due to rounding/transfer loss or unexepected token transfers to this contract - /// @dev We are only concerned with actual value accured to LAT, exact EL data can be found via corresponding EL events + /// @dev The values may differ from the corresponding EL event due to rounding/transfer loss or unexpected token transfers to this contract + /// @dev We are only concerned with actual value accrued to LAT, exact EL data can be found via corresponding EL events event RewardsClaimed( uint32 indexed rootIndex, address indexed earner, @@ -84,4 +87,26 @@ interface IRewardsManager { /// @param assetList The list of assets to get balances for /// @return An array of asset balances function balanceAssets(IERC20[] calldata assetList) external view returns (uint256[] memory); + + /// @notice Get all claimers as an array + /// @return Array of all claimer addresses + function claimerFor() external view returns (address[] memory); + + /// @notice Get number of claimers + /// @return The total number of claimers + function claimerForLength() external view returns (uint256); + + /// @notice Get all unsupported assets as an array + /// @return Array of all unsupported assets addresses + function unsupportedAssets() external view returns (address[] memory); + + /// @notice Get number of unsupported assets + /// @return The total number of unsupported assets + function unsupportedAssetsLength() external view returns (uint256); + + /// @notice Pauses the contract + function pause() external; + + /// @notice Unpauses the contract + function unpause() external; } diff --git a/test/RewardsManagerTest.sol b/test/RewardsManagerTest.sol new file mode 100644 index 00000000..2c433ddd --- /dev/null +++ b/test/RewardsManagerTest.sol @@ -0,0 +1,815 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "forge-std/console.sol"; +import {Test} from "forge-std/Test.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IRewardsCoordinator} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; +import {IRewardsCoordinatorTypes} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; + +import {MockRewardsCoordinator} from "./mocks/MockRewardsCoordinator.sol"; +import {BaseTest} from "./common/BaseTest.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; +import {RewardsManager} from "../src/core/RewardsManager.sol"; +import {IRewardsManager} from "../src/interfaces/IRewardsManager.sol"; +import {ILiquidToken} from "../src/interfaces/ILiquidToken.sol"; + +contract RewardsManagerTest is BaseTest { + MockERC20 public unsupportedToken1; + MockERC20 public unsupportedToken2; + + address public earner1 = address(0x1001); + address public earner2 = address(0x1002); + + // Track the real rewards coordinator + address public realCoordinator; + + function setUp() public override { + super.setUp(); + + // Get the real coordinator address + realCoordinator = address(rewardsManager.rewardsCoordinator()); + + // Create unsupported tokens + unsupportedToken1 = new MockERC20("Unsupported Token 1", "UNSUP1"); + unsupportedToken2 = new MockERC20("Unsupported Token 2", "UNSUP2"); + } + + // Helper function to set up mock coordinator storage + function setupMockCoordinatorStorage(address earner, IERC20 token, uint256 amount) internal { + // Set claimerFor[earner] storage slot + bytes32 claimerSlot = keccak256(abi.encode(earner, uint256(0))); + bytes32 claimerValue = bytes32(uint256(uint160(address(rewardsManager)))); + vm.store(realCoordinator, claimerSlot, claimerValue); + + // Set transferAmounts[token][recipient] storage slot + bytes32 innerSlot = keccak256(abi.encode(address(token), uint256(1))); + bytes32 transferSlot = keccak256(abi.encode(address(rewardsManager), innerSlot)); + vm.store(realCoordinator, transferSlot, bytes32(amount)); + } + + function test_ProcessClaim_AccurateBalanceTransfer() public { + // Pre-fund RewardsManager with existing balance + testToken.mint(address(rewardsManager), 50 ether); + uint256 existingBalance = testToken.balanceOf(address(rewardsManager)); + + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 100 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + // Create mock coordinator + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + setupMockCoordinatorStorage(earner1, testToken, 75 ether); + testToken.mint(realCoordinator, 75 ether); + + uint256 liquidTokenBalanceBefore = testToken.balanceOf(address(liquidToken)); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + uint256 liquidTokenBalanceAfter = testToken.balanceOf(address(liquidToken)); + uint256 transferred = liquidTokenBalanceAfter - liquidTokenBalanceBefore; + + // CHANGED: Should transfer FULL balance (existing + new) + assertEq(transferred, 125 ether, "Should transfer full balance"); + assertEq(testToken.balanceOf(address(rewardsManager)), 0, "RewardsManager should have 0 balance"); + } + + function test_ProcessClaim_LiquidTokenIntegration() public { + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](2); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 100 ether + }); + tokenLeaves[1] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken2)), + cumulativeEarnings: 50 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + // Create mock coordinator + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + + // Set up storage for both tokens + setupMockCoordinatorStorage(earner1, testToken, 100 ether); + setupMockCoordinatorStorage(earner1, testToken2, 50 ether); + + // Fund the coordinator + testToken.mint(realCoordinator, 100 ether); + testToken2.mint(realCoordinator, 50 ether); + + uint256 liquidTokenAssetBalance1Before = liquidToken.assetBalances(address(testToken)); + uint256 liquidTokenAssetBalance2Before = liquidToken.assetBalances(address(testToken2)); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + assertEq(liquidToken.assetBalances(address(testToken)) - liquidTokenAssetBalance1Before, 100 ether); + assertEq(liquidToken.assetBalances(address(testToken2)) - liquidTokenAssetBalance2Before, 50 ether); + assertEq(testToken.balanceOf(address(liquidToken)), 100 ether); + assertEq(testToken2.balanceOf(address(liquidToken)), 50 ether); + } + + function test_ProcessClaim_ReentrancyProtection() public { + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 100 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + setupMockCoordinatorStorage(earner1, testToken, 100 ether); + + testToken.mint(realCoordinator, 100 ether); + + uint256 balanceBefore = testToken.balanceOf(address(rewardsManager)); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + uint256 balanceAfter = testToken.balanceOf(address(rewardsManager)); + assertEq(balanceAfter, balanceBefore); // All transferred to LiquidToken + assertEq(testToken.balanceOf(address(liquidToken)), 100 ether); + } + + function test_ProcessClaim_SafeArrayOperations() public { + // Create claim with duplicate tokens + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](4); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 50 ether + }); + tokenLeaves[1] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), // Duplicate + cumulativeEarnings: 30 ether + }); + tokenLeaves[2] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(unsupportedToken1)), + cumulativeEarnings: 25 ether + }); + tokenLeaves[3] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken2)), + cumulativeEarnings: 40 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + + // Set up storage - since MockRewardsCoordinator transfers per leaf, + // we need to set the transfer amount to what we want per leaf + setupMockCoordinatorStorage(earner1, testToken, 40 ether); // Amount per leaf + setupMockCoordinatorStorage(earner1, testToken2, 40 ether); + setupMockCoordinatorStorage(earner1, unsupportedToken1, 25 ether); + + // Fund coordinator for multiple transfers of the same token + testToken.mint(realCoordinator, 80 ether); // 40 * 2 leaves + testToken2.mint(realCoordinator, 40 ether); + unsupportedToken1.mint(realCoordinator, 25 ether); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + // Verify results - MockRewardsCoordinator will transfer for each leaf + assertEq(rewardsManager.unsupportedAssetBalances(address(unsupportedToken1)), 25 ether); + + // The RewardsManager should receive tokens for each leaf, but dedupe when processing + // Check the actual balances to see what happened + uint256 testTokenReceived = testToken.balanceOf(address(liquidToken)); + uint256 testToken2Received = testToken2.balanceOf(address(liquidToken)); + + // The exact amounts depend on the RewardsManager's deduplication logic + assertGt(testTokenReceived, 0, "Should receive some testToken"); + assertEq(testToken2Received, 40 ether, "Should receive testToken2"); + } + + function test_ProcessClaim_SecurityBalanceCheck() public { + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(unsupportedToken1)), + cumulativeEarnings: 100 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + setupMockCoordinatorStorage(earner1, unsupportedToken1, 100 ether); + + unsupportedToken1.mint(realCoordinator, 100 ether); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + assertEq(rewardsManager.unsupportedAssetBalances(address(unsupportedToken1)), 100 ether); + } + + function test_ClaimerManagement_EfficientOperations() public { + address[] memory earners = new address[](10); + for (uint i = 0; i < 10; i++) { + earners[i] = address(uint160(0x2000 + i)); + + vm.mockCall( + realCoordinator, + abi.encodeWithSelector(IRewardsCoordinator.claimerFor.selector, earners[i]), + abi.encode(address(rewardsManager)) + ); + } + + uint256 gasBefore = gasleft(); + + for (uint i = 0; i < 10; i++) { + vm.prank(earners[i]); + rewardsManager.updateClaimerFor(earners[i]); + } + + uint256 gasUsed = gasBefore - gasleft(); + console.log("Gas used for 10 claimer updates:", gasUsed); + + assertEq(rewardsManager.claimerForLength(), 10); + + // Test removal + for (uint i = 0; i < 5; i++) { + vm.mockCall( + realCoordinator, + abi.encodeWithSelector(IRewardsCoordinator.claimerFor.selector, earners[i]), + abi.encode(address(0)) + ); + + vm.prank(earners[i]); + rewardsManager.updateClaimerFor(earners[i]); + } + + assertEq(rewardsManager.claimerForLength(), 5); + + address[] memory remainingClaimers = rewardsManager.claimerFor(); + assertEq(remainingClaimers.length, 5); + } + + function test_ProcessClaim_UnauthorizedClaimer() public { + // Mock to return address(0) - no claimer set + vm.mockCall( + realCoordinator, + abi.encodeWithSelector(IRewardsCoordinator.claimerFor.selector, earner1), + abi.encode(address(0)) + ); + + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 100 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + vm.prank(earner1); + vm.expectRevert(abi.encodeWithSelector(IRewardsManager.NotClaimerFor.selector, earner1)); + rewardsManager.processClaim(claim); + } + + function test_ProcessClaims_GasLimitProtection() public { + // Create valid claims with non-zero earner + IRewardsCoordinatorTypes.RewardsMerkleClaim[] memory claims = new IRewardsCoordinatorTypes.RewardsMerkleClaim[]( + 51 + ); + for (uint i = 0; i < 51; i++) { + // Create empty token leaves array + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](0); + claims[i] = _createBasicClaim(earner1, tokenLeaves); + } + + // Try to process more than 50 claims + vm.expectRevert("Too many claims"); + rewardsManager.processClaims(claims); + + // Create 50 valid claims + claims = new IRewardsCoordinatorTypes.RewardsMerkleClaim[](50); + for (uint i = 0; i < 50; i++) { + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](0); + claims[i] = _createBasicClaim(earner1, tokenLeaves); + } + + // Should work with 50 or fewer - create valid claims + // Expect NotClaimerFor since we didn't set up coordinator + vm.expectRevert(abi.encodeWithSelector(IRewardsManager.NotClaimerFor.selector, earner1)); + rewardsManager.processClaims(claims); + } + /// + // more tests for RewardsManagerTest contract + + function test_ProcessClaim_ZeroAmountClaim() public { + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 0 // Zero amount + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + setupMockCoordinatorStorage(earner1, testToken, 0); // No transfer + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + // Should handle zero amounts gracefully + assertEq(testToken.balanceOf(address(liquidToken)), 0); + } + + function test_ProcessClaim_EmptyTokenLeaves() public { + // Claim with no token leaves + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](0); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + setupMockCoordinatorStorage(earner1, testToken, 0); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + // Should handle empty claims gracefully + assertEq(testToken.balanceOf(address(liquidToken)), 0); + } + + function test_ProcessClaim_MaxTokenLeaves() public { + // Test with maximum reasonable number of token leaves + uint256 maxLeaves = 20; + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](maxLeaves); + + // Create many different tokens + MockERC20[] memory manyTokens = new MockERC20[](maxLeaves); + for (uint256 i = 0; i < maxLeaves; i++) { + manyTokens[i] = new MockERC20(string(abi.encodePacked("Token", i)), string(abi.encodePacked("TK", i))); + tokenLeaves[i] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(manyTokens[i])), + cumulativeEarnings: 1 ether + }); + } + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + + // Setup all tokens as unsupported with transfers + for (uint256 i = 0; i < maxLeaves; i++) { + setupMockCoordinatorStorage(earner1, IERC20(address(manyTokens[i])), 1 ether); + manyTokens[i].mint(realCoordinator, 1 ether); + } + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + // All should be unsupported assets + for (uint256 i = 0; i < maxLeaves; i++) { + assertEq(rewardsManager.unsupportedAssetBalances(address(manyTokens[i])), 1 ether); + } + } + + function test_ProcessClaim_MixedSupportedUnsupportedTokens() public { + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](3); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), // Supported + cumulativeEarnings: 100 ether + }); + tokenLeaves[1] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(unsupportedToken1)), // Unsupported + cumulativeEarnings: 50 ether + }); + tokenLeaves[2] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken2)), // Supported + cumulativeEarnings: 75 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + + setupMockCoordinatorStorage(earner1, testToken, 100 ether); + setupMockCoordinatorStorage(earner1, unsupportedToken1, 50 ether); + setupMockCoordinatorStorage(earner1, testToken2, 75 ether); + + testToken.mint(realCoordinator, 100 ether); + unsupportedToken1.mint(realCoordinator, 50 ether); + testToken2.mint(realCoordinator, 75 ether); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + // Supported tokens should go to LiquidToken + assertEq(testToken.balanceOf(address(liquidToken)), 100 ether); + assertEq(testToken2.balanceOf(address(liquidToken)), 75 ether); + + // Unsupported should stay in RewardsManager + assertEq(rewardsManager.unsupportedAssetBalances(address(unsupportedToken1)), 50 ether); + } + + function test_ProcessClaim_ClaimerStatusChangeDuringExecution() public { + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 100 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + // Mock the processClaim to do nothing for simplicity + vm.mockCall(realCoordinator, abi.encodeWithSelector(IRewardsCoordinator.processClaim.selector), abi.encode()); + + // Initially set as claimer + vm.mockCall( + realCoordinator, + abi.encodeWithSelector(IRewardsCoordinator.claimerFor.selector, earner1), + abi.encode(address(rewardsManager)) + ); + + // First call should work + vm.prank(earner1); + rewardsManager.processClaim(claim); + + // Verify the claimer was added to local storage + assertTrue(rewardsManager.isClaimerFor(earner1)); + assertEq(rewardsManager.claimerForLength(), 1); + + // Test the failed case - change claimer status to address(0) + vm.mockCall( + realCoordinator, + abi.encodeWithSelector(IRewardsCoordinator.claimerFor.selector, earner1), + abi.encode(address(0)) // No longer a claimer + ); + + // This call should fail with NotClaimerFor + vm.prank(earner1); + vm.expectRevert(abi.encodeWithSelector(IRewardsManager.NotClaimerFor.selector, earner1)); + rewardsManager.processClaim(claim); + + // The claimer should still be in local storage because the transaction reverted + assertTrue(rewardsManager.isClaimerFor(earner1)); + + // Now test successful claimer removal by calling updateClaimerFor directly + vm.prank(earner1); + rewardsManager.updateClaimerFor(earner1); + + // Now the claimer should be removed + assertFalse(rewardsManager.isClaimerFor(earner1)); + assertEq(rewardsManager.claimerForLength(), 0); + } + + function test_ProcessClaim_BalanceCalculationWithExistingBalance() public { + // Pre-fund RewardsManager with existing balance + testToken.mint(address(rewardsManager), 200 ether); + + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 100 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + setupMockCoordinatorStorage(earner1, testToken, 100 ether); + testToken.mint(realCoordinator, 100 ether); + + uint256 liquidTokenBalanceBefore = testToken.balanceOf(address(liquidToken)); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + uint256 liquidTokenBalanceAfter = testToken.balanceOf(address(liquidToken)); + uint256 transferred = liquidTokenBalanceAfter - liquidTokenBalanceBefore; + + // CHANGED: Should transfer FULL balance + assertEq(transferred, 300 ether, "Should transfer full balance"); + assertEq(testToken.balanceOf(address(rewardsManager)), 0, "RewardsManager should have 0 balance"); + } + + function test_ProcessClaim_UnsupportedTokenAccumulation() public { + // First claim + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves1 = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves1[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(unsupportedToken1)), + cumulativeEarnings: 50 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim1 = _createBasicClaim(earner1, tokenLeaves1); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + setupMockCoordinatorStorage(earner1, unsupportedToken1, 50 ether); + unsupportedToken1.mint(realCoordinator, 50 ether); + + vm.prank(earner1); + rewardsManager.processClaim(claim1); + + assertEq(rewardsManager.unsupportedAssetBalances(address(unsupportedToken1)), 50 ether); + + // Second claim - balance is set, not accumulated + setupMockCoordinatorStorage(earner1, unsupportedToken1, 30 ether); + unsupportedToken1.mint(realCoordinator, 30 ether); + + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves2 = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves2[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(unsupportedToken1)), + cumulativeEarnings: 30 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim2 = _createBasicClaim(earner1, tokenLeaves2); + + vm.prank(earner1); + rewardsManager.processClaim(claim2); + + // CHANGED: Should set to full balance (80 ether), not accumulate separately + assertEq(rewardsManager.unsupportedAssetBalances(address(unsupportedToken1)), 80 ether); + } + + function test_ProcessClaim_CoordinatorReturnsLessThanClaimed() public { + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 1000 ether // Claim 1000 + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + setupMockCoordinatorStorage(earner1, testToken, 100 ether); // Only transfer 100 + testToken.mint(realCoordinator, 100 ether); + + uint256 liquidTokenBalanceBefore = testToken.balanceOf(address(liquidToken)); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + uint256 liquidTokenBalanceAfter = testToken.balanceOf(address(liquidToken)); + uint256 transferred = liquidTokenBalanceAfter - liquidTokenBalanceBefore; + + // Should only transfer what was actually received + assertEq(transferred, 100 ether); + } + + function test_ProcessClaim_CoordinatorTransfersNothing() public { + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 100 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + setupMockCoordinatorStorage(earner1, testToken, 0); // No transfer + // Don't mint any tokens to coordinator + + uint256 liquidTokenBalanceBefore = testToken.balanceOf(address(liquidToken)); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + uint256 liquidTokenBalanceAfter = testToken.balanceOf(address(liquidToken)); + + // Should handle zero transfers gracefully + assertEq(liquidTokenBalanceAfter, liquidTokenBalanceBefore); + } + + function test_ProcessClaim_MultipleEarnersSequentially() public { + // Setup claims for multiple earners + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 100 ether + }); + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim1 = _createBasicClaim(earner1, tokenLeaves); + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim2 = _createBasicClaim(earner2, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + + // Setup both earners + setupMockCoordinatorStorage(earner1, testToken, 100 ether); + setupMockCoordinatorStorage(earner2, testToken, 100 ether); + testToken.mint(realCoordinator, 200 ether); + + uint256 liquidTokenBalanceBefore = testToken.balanceOf(address(liquidToken)); + + // Process first claim + vm.prank(earner1); + rewardsManager.processClaim(claim1); + + // Process second claim + vm.prank(earner2); + rewardsManager.processClaim(claim2); + + uint256 liquidTokenBalanceAfter = testToken.balanceOf(address(liquidToken)); + uint256 totalTransferred = liquidTokenBalanceAfter - liquidTokenBalanceBefore; + + // Should transfer total from both claims + assertEq(totalTransferred, 200 ether); + } + + function test_ProcessClaim_ExtremelyLargeDuplicates() public { + // Create claim with many duplicates of the same token + uint256 duplicateCount = 50; + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](duplicateCount); + + for (uint256 i = 0; i < duplicateCount; i++) { + tokenLeaves[i] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 1 ether + }); + } + + IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = _createBasicClaim(earner1, tokenLeaves); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + setupMockCoordinatorStorage(earner1, testToken, 1 ether); + testToken.mint(realCoordinator, duplicateCount * 1 ether); + + uint256 gasBefore = gasleft(); + + vm.prank(earner1); + rewardsManager.processClaim(claim); + + uint256 gasUsed = gasBefore - gasleft(); + console.log("Gas used for", duplicateCount, "duplicates:", gasUsed); + + // Should handle duplicates efficiently + assertGt(testToken.balanceOf(address(liquidToken)), 0); + } + + function test_ProcessClaims_BatchProcessing() public { + // Create multiple claims + uint256 claimCount = 10; + IRewardsCoordinatorTypes.RewardsMerkleClaim[] memory claims = new IRewardsCoordinatorTypes.RewardsMerkleClaim[]( + claimCount + ); + + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + + for (uint256 i = 0; i < claimCount; i++) { + address earner = address(uint160(0x3000 + i)); + + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(testToken)), + cumulativeEarnings: 10 ether + }); + + claims[i] = _createBasicClaim(earner, tokenLeaves); + setupMockCoordinatorStorage(earner, testToken, 10 ether); + } + + testToken.mint(realCoordinator, claimCount * 10 ether); + + uint256 liquidTokenBalanceBefore = testToken.balanceOf(address(liquidToken)); + + vm.prank(earner1); // Any caller can process batch + rewardsManager.processClaims(claims); + + uint256 liquidTokenBalanceAfter = testToken.balanceOf(address(liquidToken)); + uint256 totalTransferred = liquidTokenBalanceAfter - liquidTokenBalanceBefore; + + assertEq(totalTransferred, claimCount * 10 ether); + } + + function test_UpdateClaimerFor_EdgeCases() public { + // Test with zero address should revert + vm.expectRevert(abi.encodeWithSelector(IRewardsManager.ZeroAddress.selector)); + rewardsManager.updateClaimerFor(address(0)); + + // Test claimer addition and removal cycle + vm.mockCall( + realCoordinator, + abi.encodeWithSelector(IRewardsCoordinator.claimerFor.selector, earner1), + abi.encode(address(rewardsManager)) + ); + + vm.prank(earner1); + rewardsManager.updateClaimerFor(earner1); + assertTrue(rewardsManager.isClaimerFor(earner1)); + assertEq(rewardsManager.claimerForLength(), 1); + + // Remove claimer + vm.mockCall( + realCoordinator, + abi.encodeWithSelector(IRewardsCoordinator.claimerFor.selector, earner1), + abi.encode(address(0)) + ); + + vm.prank(earner1); + rewardsManager.updateClaimerFor(earner1); + assertFalse(rewardsManager.isClaimerFor(earner1)); + assertEq(rewardsManager.claimerForLength(), 0); + } + + function test_BalanceAssets_MultipleAssets() public { + // Setup some unsupported asset balances + MockRewardsCoordinator mockCoordinator = new MockRewardsCoordinator(); + vm.etch(realCoordinator, address(mockCoordinator).code); + + setupMockCoordinatorStorage(earner1, unsupportedToken1, 100 ether); + setupMockCoordinatorStorage(earner1, unsupportedToken2, 200 ether); + unsupportedToken1.mint(realCoordinator, 100 ether); + unsupportedToken2.mint(realCoordinator, 200 ether); + + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves1 = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves1[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(unsupportedToken1)), + cumulativeEarnings: 100 ether + }); + + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] + memory tokenLeaves2 = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves2[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({ + token: IERC20(address(unsupportedToken2)), + cumulativeEarnings: 200 ether + }); + + vm.prank(earner1); + rewardsManager.processClaim(_createBasicClaim(earner1, tokenLeaves1)); + + vm.prank(earner1); + rewardsManager.processClaim(_createBasicClaim(earner1, tokenLeaves2)); + + // Test balanceAssets function + IERC20[] memory assets = new IERC20[](3); + assets[0] = unsupportedToken1; + assets[1] = unsupportedToken2; + assets[2] = testToken; // Should be 0 + + uint256[] memory balances = rewardsManager.balanceAssets(assets); + + assertEq(balances[0], 100 ether); + assertEq(balances[1], 200 ether); + assertEq(balances[2], 0); + } + + //// + function _createBasicClaim( + address earner, + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] memory tokenLeaves + ) internal pure returns (IRewardsCoordinatorTypes.RewardsMerkleClaim memory) { + IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf memory earnerLeaf = IRewardsCoordinatorTypes + .EarnerTreeMerkleLeaf({earner: earner, earnerTokenRoot: bytes32(0)}); + + bytes memory emptyProof = new bytes(0); + bytes[] memory tokenTreeProofs = new bytes[](tokenLeaves.length); + for (uint i = 0; i < tokenLeaves.length; i++) { + tokenTreeProofs[i] = new bytes(0); + } + + return + IRewardsCoordinatorTypes.RewardsMerkleClaim({ + rootIndex: 1, + earnerIndex: 0, + earnerTreeProof: emptyProof, + earnerLeaf: earnerLeaf, + tokenIndices: new uint32[](tokenLeaves.length), + tokenTreeProofs: tokenTreeProofs, + tokenLeaves: tokenLeaves + }); + } +} diff --git a/test/mocks/MockAVSRegistrar.sol b/test/mocks/MockAVSRegistrar.sol new file mode 100644 index 00000000..05063c32 --- /dev/null +++ b/test/mocks/MockAVSRegistrar.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; + +contract MockAVSRegistrar is IAVSRegistrar { + function registerOperator( + address operator, + address avs, + uint32[] calldata operatorSetIds, + bytes calldata data + ) external override { + // Do nothing - stub implementation + } + + function deregisterOperator(address operator, address avs, uint32[] calldata operatorSetIds) external override { + // Do nothing - stub implementation + } + + function supportsAVS(address avs) external pure override returns (bool) { + return true; + } +} diff --git a/test/mocks/MockRewardsCoordinator.sol b/test/mocks/MockRewardsCoordinator.sol new file mode 100644 index 00000000..d43ac2e5 --- /dev/null +++ b/test/mocks/MockRewardsCoordinator.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IRewardsCoordinatorTypes} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; + +contract MockRewardsCoordinator { + mapping(address => address) public claimerFor; + mapping(address => mapping(address => uint256)) public transferAmounts; + + function setClaimerFor(address earner, address claimer) external { + claimerFor[earner] = claimer; + } + + function setupClaimTransfer(address token, address to, uint256 amount) external { + transferAmounts[token][to] = amount; + } + + function processClaim(IRewardsCoordinatorTypes.RewardsMerkleClaim calldata claim, address recipient) external { + // Simulate transferring tokens for each token leaf + for (uint256 i = 0; i < claim.tokenLeaves.length; i++) { + address tokenAddr = address(claim.tokenLeaves[i].token); + uint256 transferAmount = transferAmounts[tokenAddr][recipient]; + + if (transferAmount > 0) { + IERC20(tokenAddr).transfer(recipient, transferAmount); + } + } + } +}