From 222be287cbdd624dcadb250a2b077dcc5c15b74b Mon Sep 17 00:00:00 2001
From: Juan Manuel Rodriguez Defago <juanma.rdefago@gmail.com>
Date: Wed, 26 Feb 2025 03:37:30 -0300
Subject: [PATCH 1/8] fix: add extra parameters to contract events (OZ CR-XX)

---
 .../interfaces/internal/IHorizonStakingMain.sol  |  4 +++-
 .../horizon/contracts/staking/HorizonStaking.sol |  2 +-
 .../horizon-staking/HorizonStakingShared.t.sol   |  2 +-
 .../contracts/SubgraphService.sol                |  9 ++++++++-
 .../contracts/interfaces/ISubgraphService.sol    |  4 ++++
 .../test/subgraphService/SubgraphService.t.sol   | 16 ++++++++++++----
 6 files changed, 29 insertions(+), 8 deletions(-)

diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
index 4fc677b83..f5bcc95c1 100644
--- a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
+++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
@@ -189,12 +189,14 @@ interface IHorizonStakingMain {
      * @param verifier The address of the verifier
      * @param delegator The address of the delegator
      * @param tokens The amount of tokens undelegated
+     * @param tokens The amount of shares undelegated
      */
     event TokensUndelegated(
         address indexed serviceProvider,
         address indexed verifier,
         address indexed delegator,
-        uint256 tokens
+        uint256 tokens,
+        uint256 shares
     );
 
     /**
diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol
index c495a7914..cc2c5f97c 100644
--- a/packages/horizon/contracts/staking/HorizonStaking.sol
+++ b/packages/horizon/contracts/staking/HorizonStaking.sol
@@ -947,7 +947,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
             pool.thawingNonce
         );
 
-        emit TokensUndelegated(_serviceProvider, _verifier, msg.sender, tokens);
+        emit TokensUndelegated(_serviceProvider, _verifier, msg.sender, tokens, _shares);
         return thawRequestId;
     }
 
diff --git a/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol
index 86817c144..0d84ba708 100644
--- a/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol
+++ b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol
@@ -1010,7 +1010,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
             calcValues.thawRequestId
         );
         vm.expectEmit();
-        emit IHorizonStakingMain.TokensUndelegated(serviceProvider, verifier, delegator, calcValues.tokens);
+        emit IHorizonStakingMain.TokensUndelegated(serviceProvider, verifier, delegator, calcValues.tokens, shares);
         if (legacy) {
             staking.undelegate(serviceProvider, shares);
         } else if (thawRequestType == IHorizonStakingTypes.ThawRequestType.Delegation) {
diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol
index edf918771..752cc9e3f 100644
--- a/packages/subgraph-service/contracts/SubgraphService.sol
+++ b/packages/subgraph-service/contracts/SubgraphService.sol
@@ -522,7 +522,14 @@ contract SubgraphService is
             }
         }
 
-        emit QueryFeesCollected(indexer, _signedRav.rav.payer, tokensCollected, tokensCurators);
+        emit QueryFeesCollected(
+            indexer,
+            _signedRav.rav.payer,
+            allocationId,
+            subgraphDeploymentId,
+            tokensCollected,
+            tokensCurators
+        );
         return tokensCollected;
     }
 
diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol
index 6ae78f5b7..af2a2a953 100644
--- a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol
+++ b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol
@@ -34,12 +34,16 @@ interface ISubgraphService is IDataServiceFees {
      * @notice Emitted when a subgraph service collects query fees from Graph Payments
      * @param serviceProvider The address of the service provider
      * @param payer The address paying for the query fees
+     * @param allocationId The id of the allocation
+     * @param subgraphDeploymentId The id of the subgraph deployment
      * @param tokensCollected The amount of tokens collected
      * @param tokensCurators The amount of tokens curators receive
      */
     event QueryFeesCollected(
         address indexed serviceProvider,
         address indexed payer,
+        address indexed allocationId,
+        bytes32 subgraphDeploymentId,
         uint256 tokensCollected,
         uint256 tokensCurators
     );
diff --git a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol
index 2053083c8..3d8fd1b7d 100644
--- a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol
+++ b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol
@@ -278,9 +278,10 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest {
         bytes memory _data
     ) private returns (uint256 paymentCollected) {
         IGraphTallyCollector.SignedRAV memory signedRav = abi.decode(_data, (IGraphTallyCollector.SignedRAV));
-        Allocation.State memory allocation = subgraphService.getAllocation(
-            address(uint160(uint256(signedRav.rav.collectionId)))
-        );
+        address allocationId = address(uint160(uint256(signedRav.rav.collectionId)));
+        Allocation.State memory allocation = subgraphService.getAllocation(allocationId);
+        bytes32 subgraphDeploymentId = allocation.subgraphDeploymentId;
+
         address payer = graphTallyCollector.isAuthorized(signedRav.rav.payer, _recoverRAVSigner(signedRav))
             ? signedRav.rav.payer
             : address(0);
@@ -298,7 +299,14 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest {
         uint256 tokensCurators = (paymentCollected - tokensProtocol).mulPPMRoundUp(queryFeeData.curationCut);
 
         vm.expectEmit(address(subgraphService));
-        emit ISubgraphService.QueryFeesCollected(_indexer, payer, paymentCollected, tokensCurators);
+        emit ISubgraphService.QueryFeesCollected(
+            _indexer,
+            payer,
+            allocationId,
+            subgraphDeploymentId,
+            paymentCollected,
+            tokensCurators
+        );
 
         return paymentCollected;
     }

From 5ea383dba39bcd59d5184bf588742b7618db395e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1s=20Migone?= <tomas@edgeandnode.com>
Date: Fri, 7 Mar 2025 15:51:05 -0300
Subject: [PATCH 2/8] fix: add collection id to rav typehash (OZ CR-01)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
---
 .../contracts/payments/collectors/GraphTallyCollector.sol      | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol
index 73f887527..b8e724f2a 100644
--- a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol
+++ b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol
@@ -28,7 +28,7 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall
     /// @notice The EIP712 typehash for the ReceiptAggregateVoucher struct
     bytes32 private constant EIP712_RAV_TYPEHASH =
         keccak256(
-            "ReceiptAggregateVoucher(address payer,address serviceProvider,address dataService,uint64 timestampNs,uint128 valueAggregate,bytes metadata)"
+            "ReceiptAggregateVoucher(bytes32 collectionId,address payer,address serviceProvider,address dataService,uint64 timestampNs,uint128 valueAggregate,bytes metadata)"
         );
 
     /// @notice Tracks the amount of tokens already collected by a data service from a payer to a receiver.
@@ -189,6 +189,7 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall
                 keccak256(
                     abi.encode(
                         EIP712_RAV_TYPEHASH,
+                        _rav.collectionId,
                         _rav.payer,
                         _rav.serviceProvider,
                         _rav.dataService,

From 64f4d77b41a0c7cbcfb80a38fd94b403965ae6e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1s=20Migone?= <tomas@edgeandnode.com>
Date: Fri, 7 Mar 2025 16:00:47 -0300
Subject: [PATCH 3/8] fix: rename misleading custom error name (OZ CR-XX)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
---
 .../contracts/interfaces/internal/IHorizonStakingMain.sol    | 5 +++++
 packages/horizon/contracts/staking/HorizonStaking.sol        | 2 +-
 packages/horizon/test/staking/slash/slash.t.sol              | 4 +---
 3 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
index f5bcc95c1..e76239ee9 100644
--- a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
+++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
@@ -510,6 +510,11 @@ interface IHorizonStakingMain {
      */
     error HorizonStakingLegacySlashFailed();
 
+    /**
+     * @notice Thrown when there attempting to slash a provision with no tokens to slash.
+     */
+    error HorizonStakingNoTokensToSlash();
+
     // -- Functions --
 
     /**
diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol
index cc2c5f97c..2407e7d21 100644
--- a/packages/horizon/contracts/staking/HorizonStaking.sol
+++ b/packages/horizon/contracts/staking/HorizonStaking.sol
@@ -410,7 +410,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
         Provision storage prov = _provisions[serviceProvider][verifier];
         DelegationPoolInternal storage pool = _getDelegationPool(serviceProvider, verifier);
         uint256 tokensProvisionTotal = prov.tokens + pool.tokens;
-        require(tokensProvisionTotal != 0, HorizonStakingInsufficientTokens(tokensProvisionTotal, tokens));
+        require(tokensProvisionTotal != 0, HorizonStakingNoTokensToSlash());
 
         uint256 tokensToSlash = MathUtils.min(tokens, tokensProvisionTotal);
 
diff --git a/packages/horizon/test/staking/slash/slash.t.sol b/packages/horizon/test/staking/slash/slash.t.sol
index 3f67a9e59..d621a24e9 100644
--- a/packages/horizon/test/staking/slash/slash.t.sol
+++ b/packages/horizon/test/staking/slash/slash.t.sol
@@ -97,9 +97,7 @@ contract HorizonStakingSlashTest is HorizonStakingTest {
     function testSlash_RevertWhen_NoProvision(uint256 tokens, uint256 slashTokens) public useIndexer useStake(tokens) {
         vm.assume(slashTokens > 0);
         bytes memory expectedError = abi.encodeWithSelector(
-            IHorizonStakingMain.HorizonStakingInsufficientTokens.selector,
-            0,
-            slashTokens
+            IHorizonStakingMain.HorizonStakingNoTokensToSlash.selector
         );
         vm.expectRevert(expectedError);
         vm.startPrank(subgraphDataServiceAddress);

From 3447bd955bd522d5aec512b759ddc3c7d3d80216 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1s=20Migone?= <tomas@edgeandnode.com>
Date: Fri, 7 Mar 2025 16:14:50 -0300
Subject: [PATCH 4/8] fix: prevent indexer lockout in subgraph service (OZ
 CR-02)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
---
 .../contracts/SubgraphService.sol             |  2 +-
 .../subgraphService/provision/accept.t.sol    | 23 +++++++++++++++----
 2 files changed, 19 insertions(+), 6 deletions(-)

diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol
index 752cc9e3f..de76cfb73 100644
--- a/packages/subgraph-service/contracts/SubgraphService.sol
+++ b/packages/subgraph-service/contracts/SubgraphService.sol
@@ -148,7 +148,7 @@ contract SubgraphService is
     function acceptProvisionPendingParameters(
         address indexer,
         bytes calldata
-    ) external override onlyAuthorizedForProvision(indexer) onlyRegisteredIndexer(indexer) whenNotPaused {
+    ) external override onlyAuthorizedForProvision(indexer) whenNotPaused {
         _checkProvisionTokens(indexer);
         _acceptProvisionParameters(indexer);
         emit ProvisionPendingParametersAccepted(indexer);
diff --git a/packages/subgraph-service/test/subgraphService/provision/accept.t.sol b/packages/subgraph-service/test/subgraphService/provision/accept.t.sol
index 4d421b979..6ddc2f953 100644
--- a/packages/subgraph-service/test/subgraphService/provision/accept.t.sol
+++ b/packages/subgraph-service/test/subgraphService/provision/accept.t.sol
@@ -34,11 +34,24 @@ contract SubgraphServiceProvisionAcceptTest is SubgraphServiceTest {
         _acceptProvision(users.indexer, "");
     }
 
-    function test_SubgraphService_Provision_Accept_RevertWhen_NotRegistered() public useIndexer {
-        vm.expectRevert(
-            abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, users.indexer)
-        );
-        subgraphService.acceptProvisionPendingParameters(users.indexer, "");
+    function test_SubgraphService_Provision_Accept_When_NotRegistered(
+        uint256 tokens,
+        uint32 newVerifierCut,
+        uint64 newDisputePeriod
+    ) public useIndexer {
+        tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS);
+        vm.assume(newVerifierCut >= fishermanRewardPercentage);
+        vm.assume(newVerifierCut <= MAX_PPM);
+        newDisputePeriod = uint64(bound(newDisputePeriod, disputePeriod, MAX_WAIT_PERIOD));
+
+        // Setup indexer but dont register
+        _createProvision(users.indexer, tokens, fishermanRewardPercentage, disputePeriod);
+
+        // Update parameters with new values
+        _setProvisionParameters(users.indexer, address(subgraphService), newVerifierCut, newDisputePeriod);
+
+        // Accept provision and check parameters
+        _acceptProvision(users.indexer, "");
     }
 
     function test_SubgraphService_Provision_Accept_RevertWhen_NotAuthorized() public {

From f81d8ff91457598b578f6846fe66a8e29198d6df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1s=20Migone?= <tomas@edgeandnode.com>
Date: Fri, 21 Mar 2025 14:38:50 -0300
Subject: [PATCH 5/8] fix: limit allowed thawing period range in the subgraph
 service (OZ M-01)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
---
 .../contracts/SubgraphService.sol             |  8 ++++---
 .../subgraphService/provision/accept.t.sol    | 24 +++++++++++++------
 2 files changed, 22 insertions(+), 10 deletions(-)

diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol
index de76cfb73..703fdc625 100644
--- a/packages/subgraph-service/contracts/SubgraphService.sol
+++ b/packages/subgraph-service/contracts/SubgraphService.sol
@@ -431,12 +431,14 @@ contract SubgraphService is
     // -- Data service parameter getters --
     /**
      * @notice Getter for the accepted thawing period range for provisions
+     * The accepted range is just the dispute period defined by {DisputeManager-getDisputePeriod}
      * @dev This override ensures {ProvisionManager} uses the thawing period from the {DisputeManager}
-     * @return The minimum thawing period which is defined by {DisputeManager-getDisputePeriod}
-     * @return The maximum is unbounded
+     * @return The minimum thawing period - the dispute period
+     * @return The maximum thawing period - the dispute period
      */
     function _getThawingPeriodRange() internal view override returns (uint64, uint64) {
-        return (_disputeManager().getDisputePeriod(), DEFAULT_MAX_THAWING_PERIOD);
+        uint64 disputePeriod = _disputeManager().getDisputePeriod();
+        return (disputePeriod, disputePeriod);
     }
 
     /**
diff --git a/packages/subgraph-service/test/subgraphService/provision/accept.t.sol b/packages/subgraph-service/test/subgraphService/provision/accept.t.sol
index 6ddc2f953..36b19732e 100644
--- a/packages/subgraph-service/test/subgraphService/provision/accept.t.sol
+++ b/packages/subgraph-service/test/subgraphService/provision/accept.t.sol
@@ -17,14 +17,19 @@ contract SubgraphServiceProvisionAcceptTest is SubgraphServiceTest {
         uint256 tokens,
         uint32 newVerifierCut,
         uint64 newDisputePeriod
-    ) public useIndexer {
+    ) public {
         tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS);
         vm.assume(newVerifierCut >= fishermanRewardPercentage);
         vm.assume(newVerifierCut <= MAX_PPM);
-        newDisputePeriod = uint64(bound(newDisputePeriod, disputePeriod, MAX_WAIT_PERIOD));
+        newDisputePeriod = uint64(bound(newDisputePeriod, 1, MAX_WAIT_PERIOD));
+
+        // Set the dispute period to the new value
+        resetPrank(users.governor);
+        disputeManager.setDisputePeriod(newDisputePeriod);
 
         // Setup indexer
-        _createProvision(users.indexer, tokens, fishermanRewardPercentage, disputePeriod);
+        resetPrank(users.indexer);
+        _createProvision(users.indexer, tokens, fishermanRewardPercentage, newDisputePeriod);
         _register(users.indexer, abi.encode("url", "geoHash", address(0)));
 
         // Update parameters with new values
@@ -38,14 +43,19 @@ contract SubgraphServiceProvisionAcceptTest is SubgraphServiceTest {
         uint256 tokens,
         uint32 newVerifierCut,
         uint64 newDisputePeriod
-    ) public useIndexer {
+    ) public {
         tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS);
         vm.assume(newVerifierCut >= fishermanRewardPercentage);
         vm.assume(newVerifierCut <= MAX_PPM);
-        newDisputePeriod = uint64(bound(newDisputePeriod, disputePeriod, MAX_WAIT_PERIOD));
+        newDisputePeriod = uint64(bound(newDisputePeriod, 1, MAX_WAIT_PERIOD));
+
+        // Set the dispute period to the new value
+        resetPrank(users.governor);
+        disputeManager.setDisputePeriod(newDisputePeriod);
 
         // Setup indexer but dont register
-        _createProvision(users.indexer, tokens, fishermanRewardPercentage, disputePeriod);
+        resetPrank(users.indexer);
+        _createProvision(users.indexer, tokens, fishermanRewardPercentage, newDisputePeriod);
 
         // Update parameters with new values
         _setProvisionParameters(users.indexer, address(subgraphService), newVerifierCut, newDisputePeriod);
@@ -114,7 +124,7 @@ contract SubgraphServiceProvisionAcceptTest is SubgraphServiceTest {
                 "thawingPeriod",
                 newDisputePeriod,
                 disputePeriod,
-                type(uint64).max
+                disputePeriod
             )
         );
         subgraphService.acceptProvisionPendingParameters(users.indexer, "");

From 3adab908745fcf5630dab024a775d723c5add7b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1s=20Migone?= <tomas@edgeandnode.com>
Date: Fri, 21 Mar 2025 16:24:00 -0300
Subject: [PATCH 6/8] fix: save last parameter staged timestamp on provisions
 (OZ M-01)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
---
 .../interfaces/internal/IHorizonStakingTypes.sol   |  3 +++
 .../horizon/contracts/staking/HorizonStaking.sol   |  2 ++
 .../horizon-staking/HorizonStakingShared.t.sol     | 14 +++++++++++++-
 packages/horizon/test/staking/slash/slash.t.sol    |  4 +---
 4 files changed, 19 insertions(+), 4 deletions(-)

diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol
index 6c3420cc7..5a9e7c055 100644
--- a/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol
+++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol
@@ -22,6 +22,8 @@ interface IHorizonStakingTypes {
      * @param createdAt Timestamp when the provision was created
      * @param maxVerifierCutPending Pending value for `maxVerifierCut`. Verifier needs to accept it before it becomes active.
      * @param thawingPeriodPending Pending value for `thawingPeriod`. Verifier needs to accept it before it becomes active.
+     * @param lastParametersStagedAt Timestamp when the provision parameters were last staged. Can be used by data service implementation to
+     * implement arbitrary parameter update logic.
      * @param thawingNonce Value of the current thawing nonce. Thaw requests with older nonces are invalid.
      */
     struct Provision {
@@ -33,6 +35,7 @@ interface IHorizonStakingTypes {
         uint64 createdAt;
         uint32 maxVerifierCutPending;
         uint64 thawingPeriodPending;
+        uint256 lastParametersStagedAt;
         uint256 thawingNonce;
     }
 
diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol
index 2407e7d21..4d9022f7e 100644
--- a/packages/horizon/contracts/staking/HorizonStaking.sol
+++ b/packages/horizon/contracts/staking/HorizonStaking.sol
@@ -215,6 +215,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
         if ((prov.maxVerifierCutPending != newMaxVerifierCut) || (prov.thawingPeriodPending != newThawingPeriod)) {
             prov.maxVerifierCutPending = newMaxVerifierCut;
             prov.thawingPeriodPending = newThawingPeriod;
+            prov.lastParametersStagedAt = block.timestamp;
             emit ProvisionParametersStaged(serviceProvider, verifier, newMaxVerifierCut, newThawingPeriod);
         }
     }
@@ -716,6 +717,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
             createdAt: uint64(block.timestamp),
             maxVerifierCutPending: _maxVerifierCut,
             thawingPeriodPending: _thawingPeriod,
+            lastParametersStagedAt: 0,
             thawingNonce: 0
         });
 
diff --git a/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol
index 0d84ba708..f34bbc850 100644
--- a/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol
+++ b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol
@@ -355,6 +355,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
         assertEq(afterProvision.createdAt, uint64(block.timestamp));
         assertEq(afterProvision.maxVerifierCutPending, maxVerifierCut);
         assertEq(afterProvision.thawingPeriodPending, thawingPeriod);
+        assertEq(afterProvision.lastParametersStagedAt, 0);
         assertEq(afterProvision.thawingNonce, 0);
         assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked);
         assertEq(afterServiceProvider.tokensProvisioned, tokens + beforeServiceProvider.tokensProvisioned);
@@ -387,6 +388,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
         assertEq(afterProvision.maxVerifierCut, beforeProvision.maxVerifierCut);
         assertEq(afterProvision.thawingPeriod, beforeProvision.thawingPeriod);
         assertEq(afterProvision.createdAt, beforeProvision.createdAt);
+        assertEq(afterProvision.lastParametersStagedAt, beforeProvision.lastParametersStagedAt);
         assertEq(afterProvision.maxVerifierCutPending, beforeProvision.maxVerifierCutPending);
         assertEq(afterProvision.thawingPeriodPending, beforeProvision.thawingPeriodPending);
         assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce);
@@ -461,6 +463,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
         assertEq(afterProvision.createdAt, beforeProvision.createdAt);
         assertEq(afterProvision.maxVerifierCutPending, beforeProvision.maxVerifierCutPending);
         assertEq(afterProvision.thawingPeriodPending, beforeProvision.thawingPeriodPending);
+        assertEq(afterProvision.lastParametersStagedAt, beforeProvision.lastParametersStagedAt);
         assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce);
         assertEq(thawRequestId, expectedThawRequestId);
         assertEq(afterThawRequest.shares, thawingShares);
@@ -546,6 +549,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
         assertEq(afterProvision.createdAt, beforeProvision.createdAt);
         assertEq(afterProvision.maxVerifierCutPending, beforeProvision.maxVerifierCutPending);
         assertEq(afterProvision.thawingPeriodPending, beforeProvision.thawingPeriodPending);
+        assertEq(afterProvision.lastParametersStagedAt, beforeProvision.lastParametersStagedAt);
         assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce);
         assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked);
         assertEq(
@@ -672,6 +676,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
         assertEq(afterProvision.createdAt, beforeValues.provision.createdAt);
         assertEq(afterProvision.maxVerifierCutPending, beforeValues.provision.maxVerifierCutPending);
         assertEq(afterProvision.thawingPeriodPending, beforeValues.provision.thawingPeriodPending);
+        assertEq(afterProvision.lastParametersStagedAt, beforeValues.provision.lastParametersStagedAt);
         assertEq(afterProvision.thawingNonce, beforeValues.provision.thawingNonce);
 
         // assert: provision new verifier
@@ -753,7 +758,9 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
         Provision memory beforeProvision = staking.getProvision(serviceProvider, verifier);
 
         // setProvisionParameters
-        if (beforeProvision.maxVerifierCut != maxVerifierCut || beforeProvision.thawingPeriod != thawingPeriod) {
+        bool paramsChanged = beforeProvision.maxVerifierCut != maxVerifierCut ||
+            beforeProvision.thawingPeriod != thawingPeriod;
+        if (paramsChanged) {
             vm.expectEmit();
             emit IHorizonStakingMain.ProvisionParametersStaged(
                 serviceProvider,
@@ -776,6 +783,10 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
         assertEq(afterProvision.createdAt, beforeProvision.createdAt);
         assertEq(afterProvision.maxVerifierCutPending, maxVerifierCut);
         assertEq(afterProvision.thawingPeriodPending, thawingPeriod);
+        assertEq(
+            afterProvision.lastParametersStagedAt,
+            paramsChanged ? block.timestamp : beforeProvision.lastParametersStagedAt
+        );
         assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce);
     }
 
@@ -812,6 +823,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
         assertEq(afterProvision.thawingPeriod, beforeProvision.thawingPeriodPending);
         assertEq(afterProvision.thawingPeriod, afterProvision.thawingPeriodPending);
         assertEq(afterProvision.createdAt, beforeProvision.createdAt);
+        assertEq(afterProvision.lastParametersStagedAt, beforeProvision.lastParametersStagedAt);
         assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce);
     }
 
diff --git a/packages/horizon/test/staking/slash/slash.t.sol b/packages/horizon/test/staking/slash/slash.t.sol
index d621a24e9..89dfe55d5 100644
--- a/packages/horizon/test/staking/slash/slash.t.sol
+++ b/packages/horizon/test/staking/slash/slash.t.sol
@@ -96,9 +96,7 @@ contract HorizonStakingSlashTest is HorizonStakingTest {
 
     function testSlash_RevertWhen_NoProvision(uint256 tokens, uint256 slashTokens) public useIndexer useStake(tokens) {
         vm.assume(slashTokens > 0);
-        bytes memory expectedError = abi.encodeWithSelector(
-            IHorizonStakingMain.HorizonStakingNoTokensToSlash.selector
-        );
+        bytes memory expectedError = abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingNoTokensToSlash.selector);
         vm.expectRevert(expectedError);
         vm.startPrank(subgraphDataServiceAddress);
         staking.slash(users.indexer, slashTokens, 0, subgraphDataServiceAddress);

From 2f80dc4014bd1b252551aead2df5952005cdc1a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1s=20Migone?= <tomas@edgeandnode.com>
Date: Fri, 21 Mar 2025 17:23:17 -0300
Subject: [PATCH 7/8] fix: ensure invalid thaw requests are ignored by
 getThawedTokens (OZ L-01)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
---
 .../internal/IHorizonStakingMain.sol          |  4 +-
 .../contracts/staking/HorizonStaking.sol      |  4 +-
 .../contracts/staking/HorizonStakingBase.sol  | 18 +++--
 .../HorizonStakingShared.t.sol                |  6 +-
 .../horizon/test/staking/provision/thaw.t.sol | 72 +++++++++++++++++++
 5 files changed, 93 insertions(+), 11 deletions(-)

diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
index e76239ee9..619397439 100644
--- a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
+++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
@@ -256,6 +256,7 @@ interface IHorizonStakingMain {
      * @param shares The amount of shares being thawed
      * @param thawingUntil The timestamp until the stake is thawed
      * @param thawRequestId The ID of the thaw request
+     * @param nonce The nonce of the thaw request
      */
     event ThawRequestCreated(
         IHorizonStakingTypes.ThawRequestType indexed requestType,
@@ -264,7 +265,8 @@ interface IHorizonStakingMain {
         address owner,
         uint256 shares,
         uint64 thawingUntil,
-        bytes32 thawRequestId
+        bytes32 thawRequestId,
+        uint256 nonce
     );
 
     /**
diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol
index 4d9022f7e..a8f7adbaf 100644
--- a/packages/horizon/contracts/staking/HorizonStaking.sol
+++ b/packages/horizon/contracts/staking/HorizonStaking.sol
@@ -1065,7 +1065,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
             _owner,
             _shares,
             _thawingUntil,
-            thawRequestId
+            thawRequestId,
+            _thawingNonce
         );
         return thawRequestId;
     }
@@ -1173,6 +1174,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
         uint256 tokens = 0;
         bool validThawRequest = thawRequest.thawingNonce == thawingNonce;
         if (validThawRequest) {
+            // sharesThawing cannot be zero if there is a valid thaw request so the next division is safe
             tokens = (thawRequest.shares * tokensThawing) / sharesThawing;
             tokensThawing = tokensThawing - tokens;
             sharesThawing = sharesThawing - thawRequest.shares;
diff --git a/packages/horizon/contracts/staking/HorizonStakingBase.sol b/packages/horizon/contracts/staking/HorizonStakingBase.sol
index 5ccbddfa2..4f9f6a00d 100644
--- a/packages/horizon/contracts/staking/HorizonStakingBase.sol
+++ b/packages/horizon/contracts/staking/HorizonStakingBase.sol
@@ -182,14 +182,18 @@ abstract contract HorizonStakingBase is
         bytes32 thawRequestId = thawRequestList.head;
         while (thawRequestId != bytes32(0)) {
             ThawRequest storage thawRequest = _getThawRequest(requestType, thawRequestId);
-            if (thawRequest.thawingUntil <= block.timestamp) {
-                uint256 tokens = (thawRequest.shares * tokensThawing) / sharesThawing;
-                tokensThawing = tokensThawing - tokens;
-                sharesThawing = sharesThawing - thawRequest.shares;
-                thawedTokens = thawedTokens + tokens;
-            } else {
-                break;
+            if (thawRequest.thawingNonce == prov.thawingNonce) {
+                if (thawRequest.thawingUntil <= block.timestamp) {
+                    // sharesThawing cannot be zero if there is a valid thaw request so the next division is safe
+                    uint256 tokens = (thawRequest.shares * tokensThawing) / sharesThawing;
+                    tokensThawing = tokensThawing - tokens;
+                    sharesThawing = sharesThawing - thawRequest.shares;
+                    thawedTokens = thawedTokens + tokens;
+                } else {
+                    break;
+                }
             }
+
             thawRequestId = thawRequest.next;
         }
         return thawedTokens;
diff --git a/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol
index f34bbc850..ada3b1370 100644
--- a/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol
+++ b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol
@@ -428,7 +428,8 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
             serviceProvider,
             thawingShares,
             uint64(block.timestamp + beforeProvision.thawingPeriod),
-            expectedThawRequestId
+            expectedThawRequestId,
+            beforeProvision.thawingNonce
         );
         vm.expectEmit(address(staking));
         emit IHorizonStakingMain.ProvisionThawed(serviceProvider, verifier, tokens);
@@ -1019,7 +1020,8 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
             beneficiary,
             calcValues.thawingShares,
             calcValues.thawingUntil,
-            calcValues.thawRequestId
+            calcValues.thawRequestId,
+            beforeValues.pool.thawingNonce
         );
         vm.expectEmit();
         emit IHorizonStakingMain.TokensUndelegated(serviceProvider, verifier, delegator, calcValues.tokens, shares);
diff --git a/packages/horizon/test/staking/provision/thaw.t.sol b/packages/horizon/test/staking/provision/thaw.t.sol
index a16dd7253..46c666618 100644
--- a/packages/horizon/test/staking/provision/thaw.t.sol
+++ b/packages/horizon/test/staking/provision/thaw.t.sol
@@ -162,4 +162,76 @@ contract HorizonStakingThawTest is HorizonStakingTest {
         );
         vm.assertEq(thawedTokens, thawAmount * thawSteps);
     }
+
+    function testThaw_GetThawedTokens_AfterProvisionFullySlashed(
+        uint256 amount,
+        uint64 thawingPeriod,
+        uint256 thawAmount
+    ) public useIndexer useProvision(amount, 0, thawingPeriod) {
+        // thaw some funds so there are some shares thawing and tokens thawing
+        thawAmount = bound(thawAmount, 1, amount);
+        _thaw(users.indexer, subgraphDataServiceAddress, thawAmount);
+
+        // skip to after the thawing period has passed
+        skip(thawingPeriod + 1);
+
+        // slash all of it
+        resetPrank(subgraphDataServiceAddress);
+        _slash(users.indexer, subgraphDataServiceAddress, amount, 0);
+
+        // get the thawed tokens - should be zero now as the pool was reset
+        uint256 thawedTokens = staking.getThawedTokens(
+            ThawRequestType.Provision,
+            users.indexer,
+            subgraphDataServiceAddress,
+            users.indexer
+        );
+        vm.assertEq(thawedTokens, 0);
+    }
+
+    function testThaw_GetThawedTokens_AfterRecoveringProvision(
+        uint256 amount,
+        uint64 thawingPeriod,
+        uint256 thawAmount
+    ) public useIndexer useProvision(amount, 0, thawingPeriod) {
+        // thaw some funds so there are some shares thawing and tokens thawing
+        thawAmount = bound(thawAmount, 1, amount);
+        _thaw(users.indexer, subgraphDataServiceAddress, thawAmount);
+
+        // skip to after the thawing period has passed
+        skip(thawingPeriod + 1);
+
+        // slash all of it
+        resetPrank(subgraphDataServiceAddress);
+        _slash(users.indexer, subgraphDataServiceAddress, amount, 0);
+
+        // get the thawed tokens - should be zero now as the pool was reset
+        uint256 thawedTokens = staking.getThawedTokens(
+            ThawRequestType.Provision,
+            users.indexer,
+            subgraphDataServiceAddress,
+            users.indexer
+        );
+        vm.assertEq(thawedTokens, 0);
+
+        // put some funds back in
+        resetPrank(users.indexer);
+        _stake(amount);
+        _addToProvision(users.indexer, subgraphDataServiceAddress, amount);
+
+        // thaw some more funds
+        _thaw(users.indexer, subgraphDataServiceAddress, thawAmount);
+
+        // skip to after the thawing period has passed
+        skip(block.timestamp + thawingPeriod + 1);
+
+        // get the thawed tokens - should be the amount we thawed
+        thawedTokens = staking.getThawedTokens(
+            ThawRequestType.Provision,
+            users.indexer,
+            subgraphDataServiceAddress,
+            users.indexer
+        );
+        vm.assertEq(thawedTokens, thawAmount);
+    }
 }

From d0132874b88f7e5d1929897cefb8546a9e5f48db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1s=20Migone?= <tomas@edgeandnode.com>
Date: Fri, 21 Mar 2025 17:51:08 -0300
Subject: [PATCH 8/8] fix: document possible temporary thaw requests blockage
 when thawing period is shortened (OZ L-02)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
---
 .../interfaces/internal/IHorizonStakingMain.sol          | 6 ++++--
 packages/horizon/contracts/staking/HorizonStaking.sol    | 9 +++++++--
 2 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
index 619397439..5c100e162 100644
--- a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
+++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol
@@ -666,7 +666,8 @@ interface IHorizonStakingMain {
     /**
      * @notice Remove tokens from a provision and move them back to the service provider's idle stake.
      * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
-     * requests in the event that fulfilling all of them results in a gas limit error.
+     * requests in the event that fulfilling all of them results in a gas limit error. Otherwise, the function
+     * will attempt to fulfill all thaw requests until the first one that is not yet expired is found.
      *
      * Requirements:
      * - Must have previously initiated a thaw request using {thaw}.
@@ -790,7 +791,8 @@ interface IHorizonStakingMain {
     /**
      * @notice Withdraw undelegated tokens from a provision after thawing.
      * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
-     * requests in the event that fulfilling all of them results in a gas limit error.
+     * requests in the event that fulfilling all of them results in a gas limit error. Otherwise, the function
+     * will attempt to fulfill all thaw requests until the first one that is not yet expired is found.
      * @dev If the delegation pool was completely slashed before withdrawing, calling this function will fulfill
      * the thaw requests with an amount equal to zero.
      *
diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol
index a8f7adbaf..0b6bcd615 100644
--- a/packages/horizon/contracts/staking/HorizonStaking.sol
+++ b/packages/horizon/contracts/staking/HorizonStaking.sol
@@ -800,7 +800,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
     /**
      * @notice Remove tokens from a provision and move them back to the service provider's idle stake.
      * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
-     * requests in the event that fulfilling all of them results in a gas limit error.
+     * requests in the event that fulfilling all of them results in a gas limit error. Otherwise, the function
+     * will attempt to fulfill all thaw requests until the first one that is not yet expired is found.
      * @param _serviceProvider The service provider address
      * @param _verifier The verifier address
      * @param _nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests.
@@ -956,7 +957,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
     /**
      * @notice Withdraw undelegated tokens from a provision after thawing.
      * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
-     * requests in the event that fulfilling all of them results in a gas limit error.
+     * requests in the event that fulfilling all of them results in a gas limit error. Otherwise, the function
+     * will attempt to fulfill all thaw requests until the first one that is not yet expired is found.
      * @dev If the delegation pool was completely slashed before withdrawing, calling this function will fulfill
      * the thaw requests with an amount equal to zero.
      * @param _requestType The type of thaw request (Provision or Delegation).
@@ -1073,6 +1075,9 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
 
     /**
      * @notice Traverses a thaw request list and fulfills expired thaw requests.
+     * @dev Note that the list is traversed by creation date not by thawing until date. Traversing will stop
+     * when the first thaw request that is not yet expired is found even if later thaw requests have expired. This
+     * could happen for example when the thawing period is shortened.
      * @param _params The parameters for fulfilling thaw requests
      * @return The amount of thawed tokens
      * @return The amount of tokens still thawing