Repository: moondance-labs/tanssi-symbiotic
Pull request #67: [MD-973] Staker rewards distribution based on operator power
Author: login: steven2308
Created: 2025-03-31T15:31:27Z | Merged: 2025-04-08T02:37:45Z



Diff:
diff --git a/src/contracts/rewarder/ODefaultOperatorRewards.sol b/src/contracts/rewarder/ODefaultOperatorRewards.sol
index 7fcccbc9..d46c0175 100644
--- a/src/contracts/rewarder/ODefaultOperatorRewards.sol
+++ b/src/contracts/rewarder/ODefaultOperatorRewards.sol
@@ -20,6 +20,7 @@ pragma solidity 0.8.25;
 
 import {INetworkMiddlewareService} from "@symbiotic/interfaces/service/INetworkMiddlewareService.sol";
 import {EpochCapture} from "@symbiotic-middleware/extensions/managers/capture-timestamps/EpochCapture.sol";
+import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol";
 
 //**************************************************************************************************
 //                                      OPENZEPPELIN
@@ -35,7 +36,9 @@ import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/ut
 //**************************************************************************************************
 import {ScaleCodec} from "@tanssi-bridge-relayer/snowbridge/contracts/src/utils/ScaleCodec.sol";
 
-import {Middleware} from "src/contracts/middleware/Middleware.sol";
+//**************************************************************************************************
+//                                      TANSSI
+//**************************************************************************************************
 import {IOBaseMiddlewareReader} from "src/interfaces/middleware/IOBaseMiddlewareReader.sol";
 import {IODefaultOperatorRewards} from "src/interfaces/rewarder/IODefaultOperatorRewards.sol";
 import {IODefaultStakerRewards} from "src/interfaces/rewarder/IODefaultStakerRewards.sol";
@@ -48,6 +51,8 @@ contract ODefaultOperatorRewards is
 {
     using SafeERC20 for IERC20;
     using Math for uint256;
+    using Subnetwork for address;
+    using Subnetwork for bytes32;
 
     /// @custom:storage-location erc7201:tanssi.rewards.ODefaultOperatorRewards.v1
     struct OperatorRewardsStorage {
@@ -163,7 +168,7 @@ contract ODefaultOperatorRewards is
 
         address middlewareAddress = INetworkMiddlewareService(i_networkMiddlewareService).middleware(i_network);
         // Starlight sends back only the operator key, thus we need to get back the operator address
-        address recipient = Middleware(middlewareAddress).operatorByKey(abi.encode(input.operatorKey));
+        address recipient = IOBaseMiddlewareReader(middlewareAddress).operatorByKey(abi.encode(input.operatorKey));
 
         // Calculate the total amount of tokens that can be claimed which is:
         // total amount of tokens = total points claimable * tokens per point
@@ -197,24 +202,88 @@ contract ODefaultOperatorRewards is
         uint48 epoch,
         uint48 eraIndex,
         uint256 stakerAmount,
-        address recipient,
+        address operator,
         address middlewareAddress,
         address tokenAddress,
         bytes calldata data
     ) private {
         uint48 epochStartTs = EpochCapture(middlewareAddress).getEpochStart(epoch);
 
-        //TODO: For now this is expected to be a single vault. Change it to be able to handle multiple vaults.
         (, address[] memory operatorVaults) =
-            IOBaseMiddlewareReader(middlewareAddress).getOperatorVaults(recipient, epochStartTs);
+            IOBaseMiddlewareReader(middlewareAddress).getOperatorVaults(operator, epochStartTs);
+
+        uint256 totalVaults = operatorVaults.length;
+        if (totalVaults == 0) {
+            revert ODefaultOperatorRewards__NoVaults();
+        }
+        uint256[] memory amountPerVault = _getRewardsAmountPerVault(
+            operatorVaults, totalVaults, epochStartTs, operator, middlewareAddress, stakerAmount
+        );
+
+        _distributeRewardsPerVault(epoch, eraIndex, tokenAddress, totalVaults, operatorVaults, amountPerVault, data);
+    }
+
+    function _getRewardsAmountPerVault(
+        address[] memory operatorVaults,
+        uint256 totalVaults,
+        uint48 epochStartTs,
+        address operator,
+        address middlewareAddress,
+        uint256 stakerAmount
+    ) private view returns (uint256[] memory amountPerVault) {
+        // First we get the operator power per vault
+        uint96 subnetwork = i_network.subnetwork(0).identifier();
+        IOBaseMiddlewareReader reader = IOBaseMiddlewareReader(middlewareAddress);
+
+        uint256[] memory vaultPowers = new uint256[](totalVaults);
+        uint256 totalPower;
+        for (uint256 i; i < totalVaults;) {
+            vaultPowers[i] = reader.getOperatorPowerAt(epochStartTs, operator, operatorVaults[i], subnetwork);
+            totalPower += vaultPowers[i];
+            unchecked {
+                ++i;
+            }
+        }
 
-        // TODO: Currently it's only for a specific vault. We don't care now about making it able to send rewards for multiple vaults. It's hardcoded to the first vault of the operator.
+        // Then we calculate the rewards according to the operator power on each vault
+        uint256 distributedAmount;
+        amountPerVault = new uint256[](totalVaults);
+        for (uint256 i; i < totalVaults;) {
+            uint256 amountForVault;
+            // Last vault gets the remaining amount
+            if (i == totalVaults - 1) {
+                amountForVault = stakerAmount - distributedAmount;
+            } else {
+                amountForVault = vaultPowers[i].mulDiv(stakerAmount, totalPower);
+            }
+            amountPerVault[i] = amountForVault;
+            unchecked {
+                distributedAmount += amountForVault;
+                ++i;
+            }
+        }
+    }
+
+    function _distributeRewardsPerVault(
+        uint48 epoch,
+        uint48 eraIndex,
+        address tokenAddress,
+        uint256 totalVaults,
+        address[] memory operatorVaults,
+        uint256[] memory amountPerVault,
+        bytes calldata data
+    ) private {
         OperatorRewardsStorage storage $ = _getOperatorRewardsStorage();
-        if (operatorVaults.length > 0) {
-            IERC20(tokenAddress).approve($.vaultToStakerRewardsContract[operatorVaults[0]], stakerAmount);
-            IODefaultStakerRewards($.vaultToStakerRewardsContract[operatorVaults[0]]).distributeRewards(
-                epoch, eraIndex, stakerAmount, tokenAddress, data
+        for (uint256 i; i < totalVaults;) {
+            address stakerRewardsForVault = $.vaultToStakerRewardsContract[operatorVaults[i]];
+            IERC20(tokenAddress).approve(stakerRewardsForVault, amountPerVault[i]);
+            IODefaultStakerRewards(stakerRewardsForVault).distributeRewards(
+                epoch, eraIndex, amountPerVault[i], tokenAddress, data
             );
+
+            unchecked {
+                ++i;
+            }
         }
     }
 
diff --git a/src/interfaces/rewarder/IODefaultOperatorRewards.sol b/src/interfaces/rewarder/IODefaultOperatorRewards.sol
index ade9cc8f..a14b9049 100644
--- a/src/interfaces/rewarder/IODefaultOperatorRewards.sol
+++ b/src/interfaces/rewarder/IODefaultOperatorRewards.sol
@@ -24,6 +24,7 @@ interface IODefaultOperatorRewards {
     error ODefaultOperatorRewards__InvalidOperatorShare();
     error ODefaultOperatorRewards__InvalidAddress();
     error ODefaultOperatorRewards__AlreadySet();
+    error ODefaultOperatorRewards__NoVaults();
 
     /**
      * @notice Emitted when rewards are distributed by providing a Merkle root.
diff --git a/test/integration/Rewards.t.sol b/test/integration/Rewards.t.sol
index b8605ad3..6c6b15c8 100644
--- a/test/integration/Rewards.t.sol
+++ b/test/integration/Rewards.t.sol
@@ -15,7 +15,7 @@
 pragma solidity 0.8.25;
 
 import {Test} from "forge-std/Test.sol";
-
+import {console2} from "forge-std/console2.sol";
 //**************************************************************************************************
 //                                      SYMBIOTIC
 //**************************************************************************************************
@@ -39,6 +39,11 @@ import {PauseableEnumerableSet} from "@symbiotic-middleware/libraries/PauseableE
 import {VaultManager} from "@symbiotic-middleware/managers/VaultManager.sol";
 import {OperatorManager} from "@symbiotic-middleware/managers/OperatorManager.sol";
 
+//**************************************************************************************************
+//                                      CHAINLINK
+//**************************************************************************************************
+import {MockV3Aggregator} from "@chainlink/tests/MockV3Aggregator.sol";
+
 //**************************************************************************************************
 //                                      OPENZEPPELIN
 //**************************************************************************************************
@@ -75,6 +80,7 @@ import {DeployRewards} from "script/DeployRewards.s.sol";
 import {IODefaultOperatorRewards} from "src/interfaces/rewarder/IODefaultOperatorRewards.sol";
 import {ODefaultOperatorRewards} from "src/contracts/rewarder/ODefaultOperatorRewards.sol";
 import {IODefaultStakerRewardsFactory} from "src/interfaces/rewarder/IODefaultStakerRewardsFactory.sol";
+import {IOBaseMiddlewareReader} from "src/interfaces/middleware/IOBaseMiddlewareReader.sol";
 
 contract RewardsTest is Test {
     using Subnetwork for address;
@@ -84,20 +90,33 @@ contract RewardsTest is Test {
     uint48 public constant VAULT_EPOCH_DURATION = 8 days;
     uint48 public constant NETWORK_EPOCH_DURATION = 6 days;
     uint48 public constant SLASHING_WINDOW = 7 days;
-    uint48 public constant VETO_DURATION = 1 days;
-    uint256 public constant SLASH_AMOUNT = 30 ether;
-    uint256 public constant OPERATOR_STAKE = 100 ether;
-    uint256 public constant DEFAULT_WITHDRAW_AMOUNT = 30 ether;
     uint256 public constant OPERATOR_INITIAL_BALANCE = 1000 ether;
-    uint256 public constant MIN_SLASHING_WINDOW = 1 days;
-    bytes32 public constant OPERATOR_KEY = bytes32(uint256(1));
-    bytes32 public constant OPERATOR2_KEY = bytes32(uint256(2));
-    bytes32 public constant OPERATOR3_KEY = bytes32(uint256(3));
-    uint48 public constant OPERATOR_SHARE = 2000;
-    uint256 public constant TOTAL_NETWORK_SHARES = 3;
-    uint256 public constant PARTS_PER_BILLION = 1_000_000_000;
+    uint48 public constant OPERATOR_SHARE = 2000; // 20%
     uint256 public constant ONE_DAY = 86_400;
 
+    // We use pregenerated keys and proofs for the operators, alice becomes operator 3
+    bytes32 public constant OPERATOR_KEY = bytes32(uint256(1));
+    bytes32 public OPERATOR2_KEY;
+    bytes32 public OPERATOR3_KEY;
+    bytes32 public OPERATOR3_PROOF;
+    bytes32 public REWARDS_ROOT;
+    bytes public REWARDS_ADDITIONAL_DATA;
+    bytes public CLAIM_REWARDS_ADDITIONAL_DATA;
+
+    uint256 public constant AMOUNT_TO_DISTRIBUTE = 100 ether;
+    uint32 public constant AMOUNT_TO_CLAIM = 20;
+    uint256 public constant TOKENS_PER_POINT = 1;
+    uint256 public constant EXPECTED_CLAIMABLE = uint256(AMOUNT_TO_CLAIM) * TOKENS_PER_POINT;
+
+    uint256 public constant OPERATOR_STAKE_ST_ETH = 90 ether;
+    uint256 public constant OPERATOR_STAKE_R_ETH = 90 ether;
+    uint256 public constant OPERATOR_STAKE_BTC = 2 ether;
+
+    uint8 public constant ORACLE_DECIMALS = 16;
+    int256 public constant ORACLE_CONVERSION_ST_ETH = 3000 ether;
+    int256 public constant ORACLE_CONVERSION_R_ETH = 3000 ether;
+    int256 public constant ORACLE_CONVERSION_W_BTC = 90_000 ether;
+
     struct VaultAddresses {
         address vault;
         address delegator;
@@ -147,9 +166,7 @@ contract RewardsTest is Test {
     address public owner = vm.addr(ownerPrivateKey);
 
     address public operator = makeAddr("operator");
-
     address public operator2 = makeAddr("operator2");
-
     address public operator3 = makeAddr("operator3");
 
     address public resolver1 = makeAddr("resolver1");
@@ -205,6 +222,18 @@ contract RewardsTest is Test {
     DeployVault deployVault;
 
     function setUp() public {
+        string memory root = vm.projectRoot();
+        string memory path = string.concat(root, "/test/unit/rewards_data.json");
+        string memory json = vm.readFile(path);
+
+        // Get payload
+        OPERATOR3_KEY = vm.parseJsonBytes32(json, "$.alice_key");
+        OPERATOR2_KEY = vm.parseJsonBytes32(json, "$.bob_key");
+        REWARDS_ROOT = vm.parseJsonBytes32(json, "$.root");
+        OPERATOR3_PROOF = vm.parseJsonBytes32(json, "$.alice_rewards_proof");
+        REWARDS_ADDITIONAL_DATA = vm.parseJsonBytes(json, "$.rewards_additional_data");
+        CLAIM_REWARDS_ADDITIONAL_DATA = vm.parseJsonBytes(json, "$.claim_rewards_additional_data");
+
         DeployCollateral deployCollateral = new DeployCollateral();
 
         vm.startPrank(owner);
@@ -219,6 +248,10 @@ contract RewardsTest is Test {
         wBTC.mint(owner, 1_000_000 ether);
         vm.stopPrank();
 
+        address stEthOracle = _deployOracle(ORACLE_DECIMALS, ORACLE_CONVERSION_ST_ETH);
+        address rEthOracle = _deployOracle(ORACLE_DECIMALS, ORACLE_CONVERSION_R_ETH);
+        address wBtcOracle = _deployOracle(ORACLE_DECIMALS, ORACLE_CONVERSION_W_BTC);
+
         deployVault = new DeployVault();
         DeploySymbiotic deploySymbiotic = new DeploySymbiotic();
 
@@ -260,6 +293,10 @@ contract RewardsTest is Test {
         );
         middleware = _deployMiddlewareWithProxy(tanssi, owner, address(operatorRewards), stakerRewardsFactoryAddress);
 
+        middleware.setCollateralToOracle(address(stETH), stEthOracle);
+        middleware.setCollateralToOracle(address(rETH), rEthOracle);
+        middleware.setCollateralToOracle(address(wBTC), wBtcOracle);
+
         vetoSlasher = VetoSlasher(vaultAddresses.slasherVetoed);
 
         vetoSlasher.setResolver(0, resolver1, hex"");
@@ -283,20 +320,20 @@ contract RewardsTest is Test {
         _setLimitForNetworkAndOperators(tanssi);
 
         vm.startPrank(operator);
-        _depositToVault(vault, operator, 100 ether, stETH);
+        _depositToVault(vault, operator, OPERATOR_STAKE_ST_ETH, stETH);
 
         vm.startPrank(operator2);
+        _depositToVault(vaultSlashable, operator2, OPERATOR_STAKE_R_ETH, rETH);
+        _depositToVault(vaultVetoed, operator2, OPERATOR_STAKE_BTC, wBTC);
         operatorVaultOptInService.optIn(address(vaultSlashable));
-        _depositToVault(vaultSlashable, operator2, 100 ether, rETH);
-        _depositToVault(vaultVetoed, operator2, 100 ether, wBTC);
         vm.stopPrank();
 
         vm.startPrank(operator3);
+        _depositToVault(vault, operator3, OPERATOR_STAKE_ST_ETH, stETH);
+        _depositToVault(vaultSlashable, operator3, OPERATOR_STAKE_R_ETH, rETH);
+        _depositToVault(vaultVetoed, operator3, OPERATOR_STAKE_BTC, wBTC);
         operatorVaultOptInService.optIn(address(vault));
         operatorVaultOptInService.optIn(address(vaultVetoed));
-        _depositToVault(vault, operator3, 100 ether, stETH);
-        _depositToVault(vaultSlashable, operator3, 100 ether, rETH);
-        _depositToVault(vaultVetoed, operator3, 100 ether, wBTC);
 
         vm.stopPrank();
 
@@ -359,7 +396,7 @@ contract RewardsTest is Test {
     }
 
     function _depositToVault(Vault _vault, address _operator, uint256 _amount, Token collateral) public {
-        collateral.approve(address(_vault), _amount * 10);
+        collateral.approve(address(_vault), _amount);
         _vault.deposit(_operator, _amount);
     }
 
@@ -424,6 +461,7 @@ contract RewardsTest is Test {
         INetworkRestakeDelegator(vaultAddresses.delegator).setMaxNetworkLimit(0, 1000 ether);
         INetworkRestakeDelegator(vaultAddresses.delegatorSlashable).setMaxNetworkLimit(0, 1000 ether);
         INetworkRestakeDelegator(vaultAddresses.delegatorVetoed).setMaxNetworkLimit(0, 1000 ether);
+
         INetworkRestakeDelegator(vaultAddresses.delegator).setNetworkLimit(tanssi.subnetwork(0), 1000 ether);
         INetworkRestakeDelegator(vaultAddresses.delegatorSlashable).setNetworkLimit(tanssi.subnetwork(0), 1000 ether);
         INetworkRestakeDelegator(vaultAddresses.delegatorVetoed).setNetworkLimit(tanssi.subnetwork(0), 1000 ether);
@@ -589,4 +627,106 @@ contract RewardsTest is Test {
             makeMockProof()
         );
     }
+
+    function testClaimRewardsWithMultipleVaults() public {
+        uint48 epoch = 1;
+        uint48 eraIndex = 0;
+        uint48 epochStartTs = middleware.getEpochStart(epoch);
+
+        vm.warp(NETWORK_EPOCH_DURATION * 2 + 1);
+
+        Token rewardsToken = new Token("Rewards", 18);
+        rewardsToken.mint(address(middleware), AMOUNT_TO_DISTRIBUTE);
+
+        vm.startPrank(address(gateway));
+        middleware.distributeRewards(
+            epoch, eraIndex, AMOUNT_TO_DISTRIBUTE, AMOUNT_TO_DISTRIBUTE, REWARDS_ROOT, address(rewardsToken)
+        );
+        vm.stopPrank();
+
+        bytes32[] memory operatator3Proof = new bytes32[](1);
+        // Create a valid proof that matches the root we set
+        operatator3Proof[0] = OPERATOR3_PROOF;
+
+        IODefaultOperatorRewards.ClaimRewardsInput memory claimRewardsData = IODefaultOperatorRewards.ClaimRewardsInput({
+            operatorKey: OPERATOR3_KEY,
+            eraIndex: eraIndex,
+            totalPointsClaimable: AMOUNT_TO_CLAIM,
+            proof: operatator3Proof,
+            data: REWARDS_ADDITIONAL_DATA
+        });
+
+        // 40% of the staker rewards are distributed to the first vault. Order is important due to rounding.
+        uint256 expectedAmountStakers = (EXPECTED_CLAIMABLE * 80) / 100;
+        uint256[] memory expectedRewardsPerVault =
+            _getExpectedRewardsPerVault(operator3, epochStartTs, expectedAmountStakers);
+
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultStakerRewards.DistributeRewards(
+            tanssi, address(rewardsToken), eraIndex, epoch, expectedRewardsPerVault[0], REWARDS_ADDITIONAL_DATA
+        );
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultStakerRewards.DistributeRewards(
+            tanssi, address(rewardsToken), eraIndex, epoch, expectedRewardsPerVault[1], REWARDS_ADDITIONAL_DATA
+        );
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultStakerRewards.DistributeRewards(
+            tanssi, address(rewardsToken), eraIndex, epoch, expectedRewardsPerVault[2], REWARDS_ADDITIONAL_DATA
+        );
+
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultOperatorRewards.ClaimRewards(
+            operator3, address(rewardsToken), eraIndex, epoch, address(this), EXPECTED_CLAIMABLE
+        );
+
+        {
+            uint256 gasBefore = gasleft();
+            operatorRewards.claimRewards(claimRewardsData);
+            uint256 gasAfter = gasleft();
+
+            uint256 gasClaiming = gasBefore - gasAfter;
+            console2.log("Total gas used: ", gasClaiming);
+        }
+
+        uint256 amountClaimed_ = operatorRewards.claimed(eraIndex, operator3);
+        assertEq(amountClaimed_, EXPECTED_CLAIMABLE);
+
+        _checkStakerRewardsBalanceForVault(rewardsToken, address(vault), expectedRewardsPerVault[0]);
+        _checkStakerRewardsBalanceForVault(rewardsToken, address(vaultSlashable), expectedRewardsPerVault[1]);
+        _checkStakerRewardsBalanceForVault(rewardsToken, address(vaultVetoed), expectedRewardsPerVault[2]);
+    }
+
+    function _getExpectedRewardsPerVault(
+        address operator_,
+        uint48 epochStartTs,
+        uint256 expectedAmountStakers
+    ) private view returns (uint256[] memory rewards) {
+        uint256 powerVault1 = IOBaseMiddlewareReader(address(middleware)).getOperatorPowerAt(
+            epochStartTs, operator_, address(vault), uint96(0)
+        );
+        uint256 powerVault2 = IOBaseMiddlewareReader(address(middleware)).getOperatorPowerAt(
+            epochStartTs, operator_, address(vaultSlashable), uint96(0)
+        );
+        uint256 powerVault3 = IOBaseMiddlewareReader(address(middleware)).getOperatorPowerAt(
+            epochStartTs, operator_, address(vaultVetoed), uint96(0)
+        );
+
+        uint256 totalPower = powerVault1 + powerVault2 + powerVault3;
+        rewards = new uint256[](3);
+
+        rewards[0] = expectedAmountStakers.mulDiv(powerVault1, totalPower);
+        rewards[1] = expectedAmountStakers.mulDiv(powerVault2, totalPower);
+        rewards[2] = expectedAmountStakers - rewards[0] - rewards[1];
+    }
+
+    function _checkStakerRewardsBalanceForVault(Token token, address vault_, uint256 expectedAmount) private view {
+        address stakerRewards = operatorRewards.vaultToStakerRewardsContract(vault_);
+        uint256 stakerRewardsVault1Balance = token.balanceOf(stakerRewards);
+        assertEq(stakerRewardsVault1Balance, expectedAmount);
+    }
+
+    function _deployOracle(uint8 decimals, int256 answer) public returns (address) {
+        MockV3Aggregator oracle = new MockV3Aggregator(decimals, answer);
+        return address(oracle);
+    }
 }
diff --git a/test/unit/Rewards.t.sol b/test/unit/Rewards.t.sol
index 1ef3c026..28f9a6f9 100644
--- a/test/unit/Rewards.t.sol
+++ b/test/unit/Rewards.t.sol
@@ -15,6 +15,7 @@
 pragma solidity 0.8.25;
 
 import {Test} from "forge-std/Test.sol";
+import {console2} from "forge-std/console2.sol";
 
 //**************************************************************************************************
 //                                      SYMBIOTIC
@@ -58,6 +59,8 @@ import {IODefaultStakerRewardsFactory} from "src/interfaces/rewarder/IODefaultSt
 import {OBaseMiddlewareReader} from "src/contracts/middleware/OBaseMiddlewareReader.sol";
 import {Middleware} from "src/contracts/middleware/Middleware.sol";
 import {MiddlewareProxy} from "src/contracts/middleware/MiddlewareProxy.sol";
+import {OBaseMiddlewareReader} from "src/contracts/middleware/OBaseMiddlewareReader.sol";
+import {IOBaseMiddlewareReader} from "src/interfaces/middleware/IOBaseMiddlewareReader.sol";
 import {IMiddleware} from "src/interfaces/middleware/IMiddleware.sol";
 
 import {DelegatorMock} from "../mocks/symbiotic/DelegatorMock.sol";
@@ -86,6 +89,8 @@ contract RewardsTest is Test {
     bytes32 public constant REWARDS_ROOT = 0x4b0ddd8b9b8ec6aec84bcd2003c973254c41d976f6f29a163054eec4e7947810;
     bytes32 public constant STAKER_REWARDS_STORAGE_LOCATION =
         0xe07cde22a6017f26eee680b6867ce6727151fb6097c75742cbe379265c377400;
+    bytes32 public constant MIDDLEWARE_STORAGE_LOCATION =
+        0xca64b196a0d05040904d062f739ed1d1e1d3cc5de78f7001fb9039595fce9100;
 
     // Operator keys with which the operator is registered
     bytes32 public ALICE_KEY;
@@ -201,6 +206,8 @@ contract RewardsTest is Test {
         networkRegistry.registerNetwork();
         networkMiddlewareService.setMiddleware(address(middleware));
 
+        _setVaultToCollateral(address(vault), address(token));
+
         vm.startPrank(alice);
         operatorRegistry.registerOperator();
         operatorNetworkOptIn.optIn(tanssi);
@@ -251,21 +258,21 @@ contract RewardsTest is Test {
         return proof;
     }
 
-    function _mockVaultActiveSharesStakeAt(uint48 epoch, bool mockShares, bool mockStake) private {
+    function _mockVaultActiveSharesStakeAt(address vault_, uint48 epoch, bool mockShares, bool mockStake) private {
         uint48 epochTs = middleware.getEpochStart(epoch);
         (, bytes memory activeSharesHint, bytes memory activeStakeHint) =
             abi.decode(REWARDS_ADDITIONAL_DATA, (uint256, bytes, bytes));
 
         if (mockShares) {
             vm.mockCall(
-                address(vault),
+                vault_,
                 abi.encodeWithSelector(IVaultStorage.activeSharesAt.selector, epochTs, activeSharesHint),
                 abi.encode(AMOUNT_TO_DISTRIBUTE)
             );
         }
         if (mockStake) {
             vm.mockCall(
-                address(vault),
+                vault_,
                 abi.encodeWithSelector(IVaultStorage.activeStakeAt.selector, epochTs, activeStakeHint),
                 abi.encode(AMOUNT_TO_DISTRIBUTE)
             );
@@ -315,6 +322,15 @@ contract RewardsTest is Test {
         }
     }
 
+    function _setVaultToCollateral(address vault_, address collateral_) internal {
+        bytes32 slot = bytes32(uint256(MIDDLEWARE_STORAGE_LOCATION) + uint256(5)); // 5 is mapping slot number for the vault to collateral
+        // Get slot for mapping with vault_
+        slot = keccak256(abi.encode(vault_, slot));
+
+        // Store array length
+        vm.store(address(middleware), slot, bytes32(uint256(uint160(collateral_))));
+    }
+
     function _setActiveSharesCache(uint48 epoch, address _stakerRewards) private {
         // For StakerRewardsStorage.activeSharesCache[epoch] = AMOUNT_TO_DISTRIBUTE / 10
 
@@ -427,7 +443,7 @@ contract RewardsTest is Test {
         uint48 eraIndex = 0;
 
         vm.warp(NETWORK_EPOCH_DURATION);
-        _mockVaultActiveSharesStakeAt(epoch, true, true);
+        _mockVaultActiveSharesStakeAt(address(vault), epoch, true, true);
         _mockGetOperatorVaults(epoch);
         _distributeRewards(epoch, eraIndex, AMOUNT_TO_DISTRIBUTE, address(token));
 
@@ -456,6 +472,123 @@ contract RewardsTest is Test {
         assertEq(amountClaimed_, EXPECTED_CLAIMABLE);
     }
 
+    function testClaimRewardsWithMultipleVaults() public {
+        uint48 epoch = 0;
+        uint48 eraIndex = 0;
+        uint48 epochStartTs = middleware.getEpochStart(epoch);
+        address[] memory vaults = new address[](2);
+        vaults[0] = address(vault);
+        vaults[1] = makeAddr("vault2");
+
+        vm.warp(NETWORK_EPOCH_DURATION);
+        _mockVaultActiveSharesStakeAt(vaults[0], epoch, true, true);
+        _mockVaultActiveSharesStakeAt(vaults[1], epoch, true, true);
+
+        vm.mockCall(
+            address(middleware),
+            abi.encodeWithSelector(IOBaseMiddlewareReader.getOperatorVaults.selector, alice, epochStartTs),
+            abi.encode(2, vaults)
+        );
+
+        // The method has 3 implementations so we need to get the selector manually
+        bytes4 selector = bytes4(keccak256("getOperatorPowerAt(uint48,address,address,uint96)"));
+        vm.mockCall(
+            address(middleware), abi.encodeWithSelector(selector, epochStartTs, alice, vaults[0], 0), abi.encode(40)
+        );
+        vm.mockCall(
+            address(middleware), abi.encodeWithSelector(selector, epochStartTs, alice, vaults[1], 0), abi.encode(60)
+        );
+
+        address stakerRewards2 = makeAddr("newStakerRewards");
+        vm.startPrank(address(middleware));
+        operatorRewards.setStakerRewardContract(stakerRewards2, vaults[1]);
+        vm.mockCall(
+            stakerRewards2,
+            abi.encodeWithSelector(
+                IODefaultStakerRewards.distributeRewards.selector,
+                epoch,
+                eraIndex,
+                1000,
+                address(token),
+                REWARDS_ADDITIONAL_DATA
+            ),
+            abi.encode()
+        );
+
+        _distributeRewards(epoch, eraIndex, AMOUNT_TO_DISTRIBUTE, address(token));
+
+        address recipient = Middleware(middleware).operatorByKey(abi.encode(ALICE_KEY));
+        bytes32[] memory proof = _generateValidProof();
+
+        IODefaultOperatorRewards.ClaimRewardsInput memory claimRewardsData = IODefaultOperatorRewards.ClaimRewardsInput({
+            operatorKey: ALICE_KEY,
+            eraIndex: eraIndex,
+            totalPointsClaimable: AMOUNT_TO_CLAIM,
+            proof: proof,
+            data: REWARDS_ADDITIONAL_DATA
+        });
+
+        // 40% of the staker rewards are distributed to the first vault. Order is important due to rounding.
+        uint256 expectedAmountStakers = (EXPECTED_CLAIMABLE * 80) / 100;
+        uint256 expectedAmountVault1 = (expectedAmountStakers * 40) / 100; // 100 here is total power of operator: 40 + 60
+        uint256 expectedAmountVault2 = expectedAmountStakers - expectedAmountVault1;
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultStakerRewards.DistributeRewards(
+            tanssi, address(token), eraIndex, epoch, expectedAmountVault1, REWARDS_ADDITIONAL_DATA
+        );
+
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultOperatorRewards.ClaimRewards(
+            recipient, address(token), eraIndex, epoch, address(this), EXPECTED_CLAIMABLE
+        );
+
+        {
+            uint256 gasBefore = gasleft();
+            operatorRewards.claimRewards(claimRewardsData);
+            uint256 gasAfter = gasleft();
+
+            uint256 gasClaiming = gasBefore - gasAfter;
+            console2.log("Total gas used: ", gasClaiming);
+        }
+
+        uint256 amountClaimed_ = operatorRewards.claimed(eraIndex, alice);
+        assertEq(amountClaimed_, EXPECTED_CLAIMABLE);
+
+        uint256 stakerRewardsVault1Balance = token.balanceOf(address(stakerRewards));
+        assertEq(stakerRewardsVault1Balance, expectedAmountVault1);
+
+        // StakerRewards2 is a mock so it would not take the balance, but it should have the right allowance
+        uint256 stakerRewardsVault2Allowance = token.allowance(address(operatorRewards), address(stakerRewards2));
+        assertEq(stakerRewardsVault2Allowance, expectedAmountVault2);
+    }
+
+    function testClaimRewardsWithNoVaults() public {
+        uint48 epoch = 0;
+        uint48 eraIndex = 0;
+        uint48 epochStartTs = middleware.getEpochStart(epoch);
+        vm.mockCall(
+            address(middleware),
+            abi.encodeWithSelector(IOBaseMiddlewareReader.getOperatorVaults.selector, alice, epochStartTs),
+            abi.encode(0, new address[](0))
+        );
+
+        _distributeRewards(epoch, eraIndex, AMOUNT_TO_DISTRIBUTE, address(token));
+
+        address recipient = Middleware(middleware).operatorByKey(abi.encode(ALICE_KEY));
+        bytes32[] memory proof = _generateValidProof();
+
+        IODefaultOperatorRewards.ClaimRewardsInput memory claimRewardsData = IODefaultOperatorRewards.ClaimRewardsInput({
+            operatorKey: ALICE_KEY,
+            eraIndex: eraIndex,
+            totalPointsClaimable: AMOUNT_TO_CLAIM,
+            proof: proof,
+            data: REWARDS_ADDITIONAL_DATA
+        });
+
+        vm.expectRevert(IODefaultOperatorRewards.ODefaultOperatorRewards__NoVaults.selector);
+        operatorRewards.claimRewards(claimRewardsData);
+    }
+
     function testClaimRewardsRootNotSet() public {
         uint48 eraIndex = 0;
         bytes32[] memory proof = _generateValidProof();
@@ -497,7 +630,7 @@ contract RewardsTest is Test {
         uint48 eraIndex = 0;
 
         vm.warp(NETWORK_EPOCH_DURATION);
-        _mockVaultActiveSharesStakeAt(epoch, true, true);
+        _mockVaultActiveSharesStakeAt(address(vault), epoch, true, true);
         _mockGetOperatorVaults(epoch);
         _distributeRewards(epoch, eraIndex, AMOUNT_TO_DISTRIBUTE, address(token));
 
@@ -519,7 +652,7 @@ contract RewardsTest is Test {
         uint48 epoch = 0;
         uint48 eraIndex = 0;
 
-        _mockVaultActiveSharesStakeAt(epoch, true, true);
+        _mockVaultActiveSharesStakeAt(address(vault), epoch, true, true);
         _mockGetOperatorVaults(epoch);
         _distributeRewards(epoch, eraIndex, AMOUNT_TO_DISTRIBUTE, address(token));
 
@@ -540,7 +673,7 @@ contract RewardsTest is Test {
         uint48 eraIndex = 0;
 
         vm.warp(NETWORK_EPOCH_DURATION);
-        _mockVaultActiveSharesStakeAt(epoch, true, true);
+        _mockVaultActiveSharesStakeAt(address(vault), epoch, true, true);
         _mockGetOperatorVaults(epoch);
         _distributeRewards(epoch, eraIndex, AMOUNT_TO_DISTRIBUTE, address(token));
         bytes memory rewardsDataWithHighAdminFee =
@@ -585,7 +718,7 @@ contract RewardsTest is Test {
         vm.warp(NETWORK_EPOCH_DURATION);
         _distributeRewards(epoch, eraIndex, AMOUNT_TO_DISTRIBUTE, address(token));
         _mockGetOperatorVaults(epoch);
-        _mockVaultActiveSharesStakeAt(epoch, true, false);
+        _mockVaultActiveSharesStakeAt(address(vault), epoch, true, false);
 
         bytes32[] memory proof = _generateValidProof();
 
@@ -1168,7 +1301,7 @@ contract RewardsTest is Test {
     //                                      claimAdminFee
     //**************************************************************************************************
 
-    function testClaimAdminFeeX() public {
+    function testClaimAdminFee() public {
         uint48 epoch = 0;
         _setClaimableAdminFee(epoch, address(token));
 
diff --git a/test/unit/rewards_data.json b/test/unit/rewards_data.json
index 17eb0b21..6ba5315a 100644
--- a/test/unit/rewards_data.json
+++ b/test/unit/rewards_data.json
@@ -3,5 +3,6 @@
   "bob_key": "0x0505050505050505050505050505050505050505050505050505050505050505",
   "alice_rewards_proof": "0x27e610a11a547f210646001377ae223bc6bce387931f8153624d21f6478512d2",
   "rewards_additional_data": "0x00000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
-  "claim_rewards_additional_data": "0x0000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000"
+  "claim_rewards_additional_data": "0x0000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
+  "root": "0x4b0ddd8b9b8ec6aec84bcd2003c973254c41d976f6f29a163054eec4e7947810"
 }


Reviews:
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T11:03:22Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T11:05:43Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T11:08:17Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T11:14:14Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T11:22:15Z
user: login: steven2308
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T11:58:12Z
user: login: steven2308
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T12:04:01Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T12:23:24Z
user: login: steven2308
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T15:54:03Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T16:38:21Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T16:38:33Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T16:52:47Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T16:55:48Z
user: login: steven2308
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T19:52:00Z
user: login: NindoK
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T02:36:07Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T02:37:25Z

Review comments:
diff_hunk: @@ -196,27 +200,93 @@ contract ODefaultOperatorRewards is
         uint48 epoch,
         uint48 eraIndex,
         uint256 stakerAmount,
-        address recipient,
+        address operator,
         address middlewareAddress,
         address tokenAddress,
         bytes calldata data
     ) private {
         uint48 epochStartTs = EpochCapture(middlewareAddress).getEpochStart(epoch);
 
-        //TODO: For now this is expected to be a single vault. Change it to be able to handle multiple vaults.
-        (, address[] memory operatorVaults) = Middleware(middlewareAddress).getOperatorVaults(recipient, epochStartTs);
+        (, address[] memory operatorVaults) = Middleware(middlewareAddress).getOperatorVaults(operator, epochStartTs);
 
-        // TODO: Currently it's only for a specific vault. We don't care now about making it able to send rewards for multiple vaults. It's hardcoded to the first vault of the operator.
+        // The operation is divided into 3 steps which repeat the for loop over each vault.
+        // It was necessary to avoid stack too deep errors due to the high number of variables
+        uint256 totalVaults = operatorVaults.length;
+        (uint256[] memory vaultPowers, uint256 totalPower) =
+            _getOperatorPowerPerVault(operatorVaults, totalVaults, epochStartTs, operator, middlewareAddress);
+
+        uint256[] memory amountPerVault = _getRewardsAmountPerVault(stakerAmount, totalVaults, vaultPowers, totalPower);
+        _distributeRewardsPerVault(epoch, eraIndex, tokenAddress, totalVaults, operatorVaults, amountPerVault, data);
+    }
+
+    function _getOperatorPowerPerVault(
+        address[] memory operatorVaults,
+        uint256 totalVaults,
+        uint48 epochStartTs,
+        address operator,
+        address middlewareAddress
+    ) private view returns (uint256[] memory vaultPowers, uint256 totalPower) {
+        uint96 subnetwork = i_network.subnetwork(0).identifier();
+        BaseMiddlewareReader reader = BaseMiddlewareReader(middlewareAddress);
+
+        vaultPowers = new uint256[](totalVaults);
+        totalPower = 0;
+        for (uint256 i; i < totalVaults;) {
+            vaultPowers[i] = reader.getOperatorPowerAt(epochStartTs, operator, operatorVaults[i], subnetwork);
+            totalPower += vaultPowers[i];
+            unchecked {
+                ++i;
+            }
+        }
+    }
+
+    function _getRewardsAmountPerVault(
+        uint256 stakerAmount,
+        uint256 totalVaults,
+        uint256[] memory vaultPowers,
+        uint256 totalPower
+    ) private pure returns (uint256[] memory amountPerVault) {
+        uint256 distributedAmount;
+        amountPerVault = new uint256[](totalVaults);
+        for (uint256 i; i < totalVaults;) {
+            uint256 amountForVault;
+            // Last vault gets the remaining amount
+            if (i == totalVaults - 1) {
+                amountForVault = stakerAmount - distributedAmount;
+            } else {
+                amountForVault = vaultPowers[i].mulDiv(stakerAmount, totalPower);
+            }
+            amountPerVault[i] = amountForVault;
+            unchecked {
+                distributedAmount += amountForVault;
path: src/contracts/rewarder/ODefaultOperatorRewards.sol
user: login: NindoK
body: Hypothetically this shouldn't overflow right? i'm just thinking if there's an attack vector
created_at: 2025-04-01T11:03:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,27 +200,93 @@ contract ODefaultOperatorRewards is
         uint48 epoch,
         uint48 eraIndex,
         uint256 stakerAmount,
-        address recipient,
+        address operator,
         address middlewareAddress,
         address tokenAddress,
         bytes calldata data
     ) private {
         uint48 epochStartTs = EpochCapture(middlewareAddress).getEpochStart(epoch);
 
-        //TODO: For now this is expected to be a single vault. Change it to be able to handle multiple vaults.
-        (, address[] memory operatorVaults) = Middleware(middlewareAddress).getOperatorVaults(recipient, epochStartTs);
+        (, address[] memory operatorVaults) = Middleware(middlewareAddress).getOperatorVaults(operator, epochStartTs);
 
-        // TODO: Currently it's only for a specific vault. We don't care now about making it able to send rewards for multiple vaults. It's hardcoded to the first vault of the operator.
+        // The operation is divided into 3 steps which repeat the for loop over each vault.
+        // It was necessary to avoid stack too deep errors due to the high number of variables
+        uint256 totalVaults = operatorVaults.length;
+        (uint256[] memory vaultPowers, uint256 totalPower) =
+            _getOperatorPowerPerVault(operatorVaults, totalVaults, epochStartTs, operator, middlewareAddress);
+
+        uint256[] memory amountPerVault = _getRewardsAmountPerVault(stakerAmount, totalVaults, vaultPowers, totalPower);
path: src/contracts/rewarder/ODefaultOperatorRewards.sol
user: login: NindoK
body: Can you create a test to have a benchmark for gas? I'm really afraid of this function xD
created_at: 2025-04-01T11:05:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -456,6 +470,79 @@ contract RewardsTest is Test {
         assertEq(amountClaimed_, EXPECTED_CLAIMABLE);
     }
 
+    function testClaimRewardsWithMultipleVaults() public {
+        uint48 epoch = 0;
+        uint48 eraIndex = 0;
+        uint48 epochStartTs = middleware.getEpochStart(epoch);
+        address[] memory vaults = new address[](2);
+        vaults[0] = address(vault);
+        vaults[1] = makeAddr("vault2");
+
+        vm.warp(NETWORK_EPOCH_DURATION);
+        _mockVaultActiveSharesStakeAt(vaults[0], epoch, true, true);
+        _mockVaultActiveSharesStakeAt(vaults[1], epoch, true, true);
+
+        vm.mockCall(
+            address(middleware),
+            abi.encodeWithSelector(IMiddleware.getOperatorVaults.selector, alice, epochStartTs),
+            abi.encode(2, vaults)
+        );
+
+        // Selector is for IBaseMiddlewareReader.getOperatorPowerAt(uint48,address,address,uint96) returns (uint256). The method has 3 implementations
+        vm.mockCall(
+            address(middleware), abi.encodeWithSelector(0xbcdc52be, epochStartTs, alice, vaults[0], 0), abi.encode(40)
path: test/unit/Rewards.t.sol
user: login: NindoK
body: Let's do it like this, so we can easily modify it
`bytes4 selector = bytes4(keccak256("getOperatorPowerAt(uint48,address,address,uint96)"));`
created_at: 2025-04-01T11:08:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -456,6 +470,79 @@ contract RewardsTest is Test {
         assertEq(amountClaimed_, EXPECTED_CLAIMABLE);
     }
 
+    function testClaimRewardsWithMultipleVaults() public {
+        uint48 epoch = 0;
+        uint48 eraIndex = 0;
+        uint48 epochStartTs = middleware.getEpochStart(epoch);
+        address[] memory vaults = new address[](2);
+        vaults[0] = address(vault);
+        vaults[1] = makeAddr("vault2");
+
+        vm.warp(NETWORK_EPOCH_DURATION);
+        _mockVaultActiveSharesStakeAt(vaults[0], epoch, true, true);
+        _mockVaultActiveSharesStakeAt(vaults[1], epoch, true, true);
+
+        vm.mockCall(
+            address(middleware),
+            abi.encodeWithSelector(IMiddleware.getOperatorVaults.selector, alice, epochStartTs),
+            abi.encode(2, vaults)
+        );
+
+        // Selector is for IBaseMiddlewareReader.getOperatorPowerAt(uint48,address,address,uint96) returns (uint256). The method has 3 implementations
+        vm.mockCall(
+            address(middleware), abi.encodeWithSelector(0xbcdc52be, epochStartTs, alice, vaults[0], 0), abi.encode(40)
+        );
+        vm.mockCall(
+            address(middleware), abi.encodeWithSelector(0xbcdc52be, epochStartTs, alice, vaults[1], 0), abi.encode(60)
+        );
+
+        address stakerRewards2 = makeAddr("newStakerRewards");
+        vm.startPrank(address(middleware));
+        operatorRewards.setStakerRewardContract(stakerRewards2, vaults[1]);
+        vm.mockCall(
+            stakerRewards2,
+            abi.encodeWithSelector(
+                IODefaultStakerRewards.distributeRewards.selector,
+                epoch,
+                eraIndex,
+                1000,
+                address(token),
+                REWARDS_ADDITIONAL_DATA
+            ),
+            abi.encode()
+        );
+
+        _distributeRewards(epoch, eraIndex, AMOUNT_TO_DISTRIBUTE, address(token));
+
+        address recipient = Middleware(middleware).operatorByKey(abi.encode(ALICE_KEY));
+        bytes32[] memory proof = _generateValidProof();
+
+        IODefaultOperatorRewards.ClaimRewardsInput memory claimRewardsData = IODefaultOperatorRewards.ClaimRewardsInput({
+            operatorKey: ALICE_KEY,
+            eraIndex: eraIndex,
+            totalPointsClaimable: AMOUNT_TO_CLAIM,
+            proof: proof,
+            data: REWARDS_ADDITIONAL_DATA
+        });
+
+        // 40% of the staker rewards are distributed to the first vault. Order is important due to rounding.
+        uint256 expectedAmountStakers = (EXPECTED_CLAIMABLE * 80) / 100;
+        uint256 expectedAmountVault1 = (expectedAmountStakers * 40) / 100; // 100 here is total power of operator: 40 + 60
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultStakerRewards.DistributeRewards(
+            tanssi, address(token), eraIndex, epoch, expectedAmountVault1, REWARDS_ADDITIONAL_DATA
+        );
+
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultOperatorRewards.ClaimRewards(
+            recipient, address(token), eraIndex, epoch, address(this), EXPECTED_CLAIMABLE
+        );
+        operatorRewards.claimRewards(claimRewardsData);
+
path: test/unit/Rewards.t.sol
user: login: NindoK
body: We need to check that the amount is in that contract too. And since there are 2 vaults we should also check how much is going to the other vault right?

Let's use balanceOf to see how much the contract has
created_at: 2025-04-01T11:14:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -456,6 +470,79 @@ contract RewardsTest is Test {
         assertEq(amountClaimed_, EXPECTED_CLAIMABLE);
     }
 
+    function testClaimRewardsWithMultipleVaults() public {
path: test/unit/Rewards.t.sol
user: login: NindoK
body: Let's make some integrations tests too!
created_at: 2025-04-01T11:22:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,27 +200,93 @@ contract ODefaultOperatorRewards is
         uint48 epoch,
         uint48 eraIndex,
         uint256 stakerAmount,
-        address recipient,
+        address operator,
         address middlewareAddress,
         address tokenAddress,
         bytes calldata data
     ) private {
         uint48 epochStartTs = EpochCapture(middlewareAddress).getEpochStart(epoch);
 
-        //TODO: For now this is expected to be a single vault. Change it to be able to handle multiple vaults.
-        (, address[] memory operatorVaults) = Middleware(middlewareAddress).getOperatorVaults(recipient, epochStartTs);
+        (, address[] memory operatorVaults) = Middleware(middlewareAddress).getOperatorVaults(operator, epochStartTs);
 
-        // TODO: Currently it's only for a specific vault. We don't care now about making it able to send rewards for multiple vaults. It's hardcoded to the first vault of the operator.
+        // The operation is divided into 3 steps which repeat the for loop over each vault.
+        // It was necessary to avoid stack too deep errors due to the high number of variables
+        uint256 totalVaults = operatorVaults.length;
+        (uint256[] memory vaultPowers, uint256 totalPower) =
+            _getOperatorPowerPerVault(operatorVaults, totalVaults, epochStartTs, operator, middlewareAddress);
+
+        uint256[] memory amountPerVault = _getRewardsAmountPerVault(stakerAmount, totalVaults, vaultPowers, totalPower);
+        _distributeRewardsPerVault(epoch, eraIndex, tokenAddress, totalVaults, operatorVaults, amountPerVault, data);
+    }
+
+    function _getOperatorPowerPerVault(
+        address[] memory operatorVaults,
+        uint256 totalVaults,
+        uint48 epochStartTs,
+        address operator,
+        address middlewareAddress
+    ) private view returns (uint256[] memory vaultPowers, uint256 totalPower) {
+        uint96 subnetwork = i_network.subnetwork(0).identifier();
+        BaseMiddlewareReader reader = BaseMiddlewareReader(middlewareAddress);
+
+        vaultPowers = new uint256[](totalVaults);
+        totalPower = 0;
+        for (uint256 i; i < totalVaults;) {
+            vaultPowers[i] = reader.getOperatorPowerAt(epochStartTs, operator, operatorVaults[i], subnetwork);
+            totalPower += vaultPowers[i];
+            unchecked {
+                ++i;
+            }
+        }
+    }
+
+    function _getRewardsAmountPerVault(
+        uint256 stakerAmount,
+        uint256 totalVaults,
+        uint256[] memory vaultPowers,
+        uint256 totalPower
+    ) private pure returns (uint256[] memory amountPerVault) {
+        uint256 distributedAmount;
+        amountPerVault = new uint256[](totalVaults);
+        for (uint256 i; i < totalVaults;) {
+            uint256 amountForVault;
+            // Last vault gets the remaining amount
+            if (i == totalVaults - 1) {
+                amountForVault = stakerAmount - distributedAmount;
+            } else {
+                amountForVault = vaultPowers[i].mulDiv(stakerAmount, totalPower);
+            }
+            amountPerVault[i] = amountForVault;
+            unchecked {
+                distributedAmount += amountForVault;
path: src/contracts/rewarder/ODefaultOperatorRewards.sol
user: login: steven2308
body: The max value for this will be the rewards amount, which was already fitting in `uint256`
created_at: 2025-04-01T11:58:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -456,6 +470,79 @@ contract RewardsTest is Test {
         assertEq(amountClaimed_, EXPECTED_CLAIMABLE);
     }
 
+    function testClaimRewardsWithMultipleVaults() public {
+        uint48 epoch = 0;
+        uint48 eraIndex = 0;
+        uint48 epochStartTs = middleware.getEpochStart(epoch);
+        address[] memory vaults = new address[](2);
+        vaults[0] = address(vault);
+        vaults[1] = makeAddr("vault2");
+
+        vm.warp(NETWORK_EPOCH_DURATION);
+        _mockVaultActiveSharesStakeAt(vaults[0], epoch, true, true);
+        _mockVaultActiveSharesStakeAt(vaults[1], epoch, true, true);
+
+        vm.mockCall(
+            address(middleware),
+            abi.encodeWithSelector(IMiddleware.getOperatorVaults.selector, alice, epochStartTs),
+            abi.encode(2, vaults)
+        );
+
+        // Selector is for IBaseMiddlewareReader.getOperatorPowerAt(uint48,address,address,uint96) returns (uint256). The method has 3 implementations
+        vm.mockCall(
+            address(middleware), abi.encodeWithSelector(0xbcdc52be, epochStartTs, alice, vaults[0], 0), abi.encode(40)
path: test/unit/Rewards.t.sol
user: login: steven2308
body: I knew there was a better way, can't believe I missed it :laughing: 
created_at: 2025-04-01T12:04:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -456,6 +470,79 @@ contract RewardsTest is Test {
         assertEq(amountClaimed_, EXPECTED_CLAIMABLE);
     }
 
+    function testClaimRewardsWithMultipleVaults() public {
+        uint48 epoch = 0;
+        uint48 eraIndex = 0;
+        uint48 epochStartTs = middleware.getEpochStart(epoch);
+        address[] memory vaults = new address[](2);
+        vaults[0] = address(vault);
+        vaults[1] = makeAddr("vault2");
+
+        vm.warp(NETWORK_EPOCH_DURATION);
+        _mockVaultActiveSharesStakeAt(vaults[0], epoch, true, true);
+        _mockVaultActiveSharesStakeAt(vaults[1], epoch, true, true);
+
+        vm.mockCall(
+            address(middleware),
+            abi.encodeWithSelector(IMiddleware.getOperatorVaults.selector, alice, epochStartTs),
+            abi.encode(2, vaults)
+        );
+
+        // Selector is for IBaseMiddlewareReader.getOperatorPowerAt(uint48,address,address,uint96) returns (uint256). The method has 3 implementations
+        vm.mockCall(
+            address(middleware), abi.encodeWithSelector(0xbcdc52be, epochStartTs, alice, vaults[0], 0), abi.encode(40)
path: test/unit/Rewards.t.sol
user: login: NindoK
body: 😂 
created_at: 2025-04-01T12:23:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,27 +200,93 @@ contract ODefaultOperatorRewards is
         uint48 epoch,
         uint48 eraIndex,
         uint256 stakerAmount,
-        address recipient,
+        address operator,
         address middlewareAddress,
         address tokenAddress,
         bytes calldata data
     ) private {
         uint48 epochStartTs = EpochCapture(middlewareAddress).getEpochStart(epoch);
 
-        //TODO: For now this is expected to be a single vault. Change it to be able to handle multiple vaults.
-        (, address[] memory operatorVaults) = Middleware(middlewareAddress).getOperatorVaults(recipient, epochStartTs);
+        (, address[] memory operatorVaults) = Middleware(middlewareAddress).getOperatorVaults(operator, epochStartTs);
 
-        // TODO: Currently it's only for a specific vault. We don't care now about making it able to send rewards for multiple vaults. It's hardcoded to the first vault of the operator.
+        // The operation is divided into 3 steps which repeat the for loop over each vault.
+        // It was necessary to avoid stack too deep errors due to the high number of variables
+        uint256 totalVaults = operatorVaults.length;
+        (uint256[] memory vaultPowers, uint256 totalPower) =
+            _getOperatorPowerPerVault(operatorVaults, totalVaults, epochStartTs, operator, middlewareAddress);
+
+        uint256[] memory amountPerVault = _getRewardsAmountPerVault(stakerAmount, totalVaults, vaultPowers, totalPower);
path: src/contracts/rewarder/ODefaultOperatorRewards.sol
user: login: steven2308
body: On integration tests: 
I simply checked the gas consumption on the integration test and logged it, I don't think we need a separate test.

For an operator using 3 vaults the claim process takes:
`Total gas used:  856420`
Seems well below the limit. Do we have an estimation of the number a vaults operators will have?
 
created_at: 2025-04-03T15:54:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,24 +202,85 @@ contract ODefaultOperatorRewards is
         uint48 epoch,
         uint48 eraIndex,
         uint256 stakerAmount,
-        address recipient,
+        address operator,
         address middlewareAddress,
         address tokenAddress,
         bytes calldata data
     ) private {
         uint48 epochStartTs = EpochCapture(middlewareAddress).getEpochStart(epoch);
 
-        //TODO: For now this is expected to be a single vault. Change it to be able to handle multiple vaults.
         (, address[] memory operatorVaults) =
-            IOBaseMiddlewareReader(middlewareAddress).getOperatorVaults(recipient, epochStartTs);
+            IOBaseMiddlewareReader(middlewareAddress).getOperatorVaults(operator, epochStartTs);
+
+        uint256 totalVaults = operatorVaults.length;
+        uint256[] memory amountPerVault = _getRewardsAmountPerVault(
+            operatorVaults, totalVaults, epochStartTs, operator, middlewareAddress, stakerAmount
+        );
+
path: src/contracts/rewarder/ODefaultOperatorRewards.sol
user: login: NindoK
body: We should check that `amountPerVault.length > 0` right? Otherwise we should revert
created_at: 2025-04-07T16:38:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,24 +202,85 @@ contract ODefaultOperatorRewards is
         uint48 epoch,
         uint48 eraIndex,
         uint256 stakerAmount,
-        address recipient,
+        address operator,
         address middlewareAddress,
         address tokenAddress,
         bytes calldata data
     ) private {
         uint48 epochStartTs = EpochCapture(middlewareAddress).getEpochStart(epoch);
 
-        //TODO: For now this is expected to be a single vault. Change it to be able to handle multiple vaults.
         (, address[] memory operatorVaults) =
-            IOBaseMiddlewareReader(middlewareAddress).getOperatorVaults(recipient, epochStartTs);
+            IOBaseMiddlewareReader(middlewareAddress).getOperatorVaults(operator, epochStartTs);
+
+        uint256 totalVaults = operatorVaults.length;
+        uint256[] memory amountPerVault = _getRewardsAmountPerVault(
+            operatorVaults, totalVaults, epochStartTs, operator, middlewareAddress, stakerAmount
+        );
+
path: src/contracts/rewarder/ODefaultOperatorRewards.sol
user: login: NindoK
body: Or totalVaults. Both are fine
created_at: 2025-04-07T16:38:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,20 +90,33 @@ contract RewardsTest is Test {
     uint48 public constant VAULT_EPOCH_DURATION = 8 days;
     uint48 public constant NETWORK_EPOCH_DURATION = 6 days;
     uint48 public constant SLASHING_WINDOW = 7 days;
-    uint48 public constant VETO_DURATION = 1 days;
-    uint256 public constant SLASH_AMOUNT = 30 ether;
-    uint256 public constant OPERATOR_STAKE = 100 ether;
-    uint256 public constant DEFAULT_WITHDRAW_AMOUNT = 30 ether;
     uint256 public constant OPERATOR_INITIAL_BALANCE = 1000 ether;
-    uint256 public constant MIN_SLASHING_WINDOW = 1 days;
-    bytes32 public constant OPERATOR_KEY = bytes32(uint256(1));
-    bytes32 public constant OPERATOR2_KEY = bytes32(uint256(2));
-    bytes32 public constant OPERATOR3_KEY = bytes32(uint256(3));
-    uint48 public constant OPERATOR_SHARE = 2000;
-    uint256 public constant TOTAL_NETWORK_SHARES = 3;
-    uint256 public constant PARTS_PER_BILLION = 1_000_000_000;
+    uint48 public constant OPERATOR_SHARE = 2000; // 20%
     uint256 public constant ONE_DAY = 86_400;
 
+    // We use pregenerated keys and proofs for the operators, alice becomes operator 3
+    bytes32 public OPERATOR_KEY = bytes32(uint256(1));
path: test/integration/Rewards.t.sol
user: login: NindoK
body: Make it constant. 
created_at: 2025-04-07T16:52:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -589,4 +627,106 @@ contract RewardsTest is Test {
             makeMockProof()
         );
     }
+
+    function testClaimRewardsWithMultipleVaults() public {
+        uint48 epoch = 1;
+        uint48 eraIndex = 0;
+        uint48 epochStartTs = middleware.getEpochStart(epoch);
+
+        vm.warp(NETWORK_EPOCH_DURATION * 2 + 1);
+
+        Token rewardsToken = new Token("Rewards", 18);
+        rewardsToken.mint(address(middleware), AMOUNT_TO_DISTRIBUTE);
+
+        vm.startPrank(address(gateway));
+        middleware.distributeRewards(
+            epoch, eraIndex, AMOUNT_TO_DISTRIBUTE, AMOUNT_TO_DISTRIBUTE, REWARDS_ROOT, address(rewardsToken)
+        );
+        vm.stopPrank();
+
+        bytes32[] memory operatator3Proof = new bytes32[](1);
+        // Create a valid proof that matches the root we set
+        operatator3Proof[0] = OPERATOR3_PROOF;
+
+        IODefaultOperatorRewards.ClaimRewardsInput memory claimRewardsData = IODefaultOperatorRewards.ClaimRewardsInput({
+            operatorKey: OPERATOR3_KEY,
+            eraIndex: eraIndex,
+            totalPointsClaimable: AMOUNT_TO_CLAIM,
+            proof: operatator3Proof,
+            data: REWARDS_ADDITIONAL_DATA
+        });
+
+        // 40% of the staker rewards are distributed to the first vault. Order is important due to rounding.
+        uint256 expectedAmountStakers = (EXPECTED_CLAIMABLE * 80) / 100;
+        uint256[] memory expectedRewardsPerVault =
+            _getExpectedRewardsPerVault(operator3, epochStartTs, expectedAmountStakers);
+
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultStakerRewards.DistributeRewards(
+            tanssi, address(rewardsToken), eraIndex, epoch, expectedRewardsPerVault[0], REWARDS_ADDITIONAL_DATA
+        );
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultStakerRewards.DistributeRewards(
+            tanssi, address(rewardsToken), eraIndex, epoch, expectedRewardsPerVault[1], REWARDS_ADDITIONAL_DATA
+        );
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultStakerRewards.DistributeRewards(
+            tanssi, address(rewardsToken), eraIndex, epoch, expectedRewardsPerVault[2], REWARDS_ADDITIONAL_DATA
+        );
+
+        vm.expectEmit(true, true, false, true);
+        emit IODefaultOperatorRewards.ClaimRewards(
+            operator3, address(rewardsToken), eraIndex, epoch, address(this), EXPECTED_CLAIMABLE
+        );
+
+        {
+            uint256 gasBefore = gasleft();
+            operatorRewards.claimRewards(claimRewardsData);
+            uint256 gasAfter = gasleft();
+
+            uint256 gasClaiming = gasBefore - gasAfter;
+            console2.log("Total gas used: ", gasClaiming);
+        }
+
+        uint256 amountClaimed_ = operatorRewards.claimed(eraIndex, operator3);
+        assertEq(amountClaimed_, EXPECTED_CLAIMABLE);
+
+        _checkStakerRewardsBalanceForVault(rewardsToken, address(vault), expectedRewardsPerVault[0]);
+        _checkStakerRewardsBalanceForVault(rewardsToken, address(vaultSlashable), expectedRewardsPerVault[1]);
+        _checkStakerRewardsBalanceForVault(rewardsToken, address(vaultVetoed), expectedRewardsPerVault[2]);
+    }
+
+    function _getExpectedRewardsPerVault(
+        address operator_,
+        uint48 epochStartTs,
+        uint256 expectedAmountStakers
+    ) private view returns (uint256[] memory rewards) {
+        uint256 powerVault1 = IOBaseMiddlewareReader(address(middleware)).getOperatorPowerAt(
+            epochStartTs, operator_, address(vault), uint96(0)
+        );
+        uint256 powerVault2 = IOBaseMiddlewareReader(address(middleware)).getOperatorPowerAt(
+            epochStartTs, operator_, address(vaultSlashable), uint96(0)
+        );
+        uint256 powerVault3 = IOBaseMiddlewareReader(address(middleware)).getOperatorPowerAt(
+            epochStartTs, operator_, address(vaultVetoed), uint96(0)
+        );
+
+        uint256 totalPower = powerVault1 + powerVault2 + powerVault3;
+        rewards = new uint256[](3);
+
+        rewards[0] = (expectedAmountStakers * powerVault1) / totalPower;
+        rewards[1] = (expectedAmountStakers * powerVault2) / totalPower;
path: test/integration/Rewards.t.sol
user: login: NindoK
body: Use `mulDiv` to be consistent with Math
created_at: 2025-04-07T16:55:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,24 +202,85 @@ contract ODefaultOperatorRewards is
         uint48 epoch,
         uint48 eraIndex,
         uint256 stakerAmount,
-        address recipient,
+        address operator,
         address middlewareAddress,
         address tokenAddress,
         bytes calldata data
     ) private {
         uint48 epochStartTs = EpochCapture(middlewareAddress).getEpochStart(epoch);
 
-        //TODO: For now this is expected to be a single vault. Change it to be able to handle multiple vaults.
         (, address[] memory operatorVaults) =
-            IOBaseMiddlewareReader(middlewareAddress).getOperatorVaults(recipient, epochStartTs);
+            IOBaseMiddlewareReader(middlewareAddress).getOperatorVaults(operator, epochStartTs);
+
+        uint256 totalVaults = operatorVaults.length;
+        uint256[] memory amountPerVault = _getRewardsAmountPerVault(
+            operatorVaults, totalVaults, epochStartTs, operator, middlewareAddress, stakerAmount
+        );
+
path: src/contracts/rewarder/ODefaultOperatorRewards.sol
user: login: steven2308
body: Added. This will prevent the operator from claiming his rewards if he has no vaults, which I actually like. It's a good protection for stakers.
As it was before your suggestions, if for some reason the operator has no vaults we would still be able to get a chunk and leave nothing for stakers.
created_at: 2025-04-07T19:52:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,27 +200,93 @@ contract ODefaultOperatorRewards is
         uint48 epoch,
         uint48 eraIndex,
         uint256 stakerAmount,
-        address recipient,
+        address operator,
         address middlewareAddress,
         address tokenAddress,
         bytes calldata data
     ) private {
         uint48 epochStartTs = EpochCapture(middlewareAddress).getEpochStart(epoch);
 
-        //TODO: For now this is expected to be a single vault. Change it to be able to handle multiple vaults.
-        (, address[] memory operatorVaults) = Middleware(middlewareAddress).getOperatorVaults(recipient, epochStartTs);
+        (, address[] memory operatorVaults) = Middleware(middlewareAddress).getOperatorVaults(operator, epochStartTs);
 
-        // TODO: Currently it's only for a specific vault. We don't care now about making it able to send rewards for multiple vaults. It's hardcoded to the first vault of the operator.
+        // The operation is divided into 3 steps which repeat the for loop over each vault.
+        // It was necessary to avoid stack too deep errors due to the high number of variables
+        uint256 totalVaults = operatorVaults.length;
+        (uint256[] memory vaultPowers, uint256 totalPower) =
+            _getOperatorPowerPerVault(operatorVaults, totalVaults, epochStartTs, operator, middlewareAddress);
+
+        uint256[] memory amountPerVault = _getRewardsAmountPerVault(stakerAmount, totalVaults, vaultPowers, totalPower);
path: src/contracts/rewarder/ODefaultOperatorRewards.sol
user: login: NindoK
body: Looks nice. It's not such a big number 
created_at: 2025-04-08T02:37:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-07T20:10:38Z
body: <h2>> Coverage generated</h2>Coverage after merging <b>feat/973-rewards-distribution</b> into <b>staging</b> will be<table><tbody><tr><th>100.00%</th><th> 0.00%</th></tr></tbody></table>

<details><summary>Coverage Report</summary><table><tbody><tr><th>File</th><th>Stmts</th><th>Branches</th><th>Funcs</th><th>Lines</th><th>Uncovered Lines</th></tr><tr><td colspan='6'><b>script</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/script/DeployCollateral.s.sol'>DeployCollateral.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/script/DeployRewards.s.sol'>DeployRewards.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/script/DeploySymbiotic.s.sol'>DeploySymbiotic.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/script/DeployTanssiEcosystem.s.sol'>DeployTanssiEcosystem.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/script/DeployVault.s.sol'>DeployVault.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/script/HelperConfig.s.sol'>HelperConfig.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>src/contracts/extensions</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/src/contracts/extensions/OSharedVaults.sol'>OSharedVaults.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>src/contracts/libraries</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/src/contracts/libraries/QuickSort.sol'>QuickSort.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>src/contracts/middleware</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/src/contracts/middleware/Middleware.sol'>Middleware.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/src/contracts/middleware/MiddlewareStorage.sol'>MiddlewareStorage.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/src/contracts/middleware/OBaseMiddlewareReader.sol'>OBaseMiddlewareReader.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>src/contracts/rewarder</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/src/contracts/rewarder/ODefaultOperatorRewards.sol'>ODefaultOperatorRewards.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/src/contracts/rewarder/ODefaultStakerRewards.sol'>ODefaultStakerRewards.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/src/contracts/rewarder/ODefaultStakerRewardsFactory.sol'>ODefaultStakerRewardsFactory.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>test/mocks</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/test/mocks/FeeToken.sol'>FeeToken.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/test/mocks/Token.sol'>Token.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>test/mocks/symbiotic</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/test/mocks/symbiotic/DelegatorMock.sol'>DelegatorMock.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/test/mocks/symbiotic/RegistryMock.sol'>RegistryMock.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/test/mocks/symbiotic/SharedVaultMock.sol'>SharedVaultMock.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>test/unit/utils</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/test/unit/utils/MiddlewareV2.sol'>MiddlewareV2.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/35329d3307d31b8f4be15754afaa4d3bfd8e0954/test/unit/utils/MiddlewareV3.sol'>MiddlewareV3.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr></tbody></table></details>
author_association: NONE