diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index 465e806d4..a24e2aac9 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -71,7 +71,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @inheritdoc IRecurringCollector - * @notice Accept an indexing agreement. + * @notice Accept a Recurring Collection Agreement. * See {IRecurringCollector.accept}. * @dev Caller must be the data service the RCA was issued to. */ @@ -148,7 +148,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @inheritdoc IRecurringCollector - * @notice Cancel an indexing agreement. + * @notice Cancel a Recurring Collection Agreement. * See {IRecurringCollector.cancel}. * @dev Caller must be the data service for the agreement. */ @@ -181,7 +181,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @inheritdoc IRecurringCollector - * @notice Update an indexing agreement. + * @notice Update a Recurring Collection Agreement. * See {IRecurringCollector.update}. * @dev Caller must be the data service for the agreement. * @dev Note: Updated pricing terms apply immediately and will affect the next collection @@ -343,7 +343,10 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC slippage <= _params.maxSlippage, RecurringCollectorExcessiveSlippage(_params.tokens, tokensToCollect, _params.maxSlippage) ); + } + agreement.lastCollectionAt = uint64(block.timestamp); + if (tokensToCollect > 0) { _graphPaymentsEscrow().collect( _paymentType, agreement.payer, @@ -354,7 +357,6 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC _params.receiverDestination ); } - agreement.lastCollectionAt = uint64(block.timestamp); emit PaymentCollected( _paymentType, diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index c5901eda7..653149267 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -398,6 +398,13 @@ contract SubgraphService is emit CurationCutSet(curationCut); } + /// @inheritdoc ISubgraphService + function setIndexingFeesCut(uint256 indexingFeesCut_) external override onlyOwner { + require(PPMMath.isValidPPM(indexingFeesCut_), SubgraphServiceInvalidIndexingFeesCut(indexingFeesCut_)); + indexingFeesCut = indexingFeesCut_; + emit IndexingFeesCutSet(indexingFeesCut_); + } + /** * @inheritdoc ISubgraphService * @notice Accept an indexing agreement. @@ -793,7 +800,8 @@ contract SubgraphService is agreementId: _agreementId, currentEpoch: _graphEpochManager().currentEpoch(), receiverDestination: _paymentsDestination, - data: _data + data: _data, + indexingFeesCut: indexingFeesCut }) ); diff --git a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol index 06ada3a59..1e0b608d6 100644 --- a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol +++ b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol @@ -21,4 +21,7 @@ abstract contract SubgraphServiceV1Storage { /// @notice Destination of indexer payments mapping(address indexer => address destination) public paymentsDestination; + + /// @notice The cut data service takes from indexing fee payments. In PPM. + uint256 public indexingFeesCut; } diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol index 17ff4cbd0..54ebf4396 100644 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol @@ -69,12 +69,24 @@ interface ISubgraphService is IDataServiceFees { */ event CurationCutSet(uint256 curationCut); + /** + * @notice Emitted when indexing fees cut is set + * @param indexingFeesCut The indexing fees cut + */ + event IndexingFeesCutSet(uint256 indexingFeesCut); + /** * @notice Thrown when trying to set a curation cut that is not a valid PPM value * @param curationCut The curation cut value */ error SubgraphServiceInvalidCurationCut(uint256 curationCut); + /** + * @notice Thrown when trying to set an indexing fees cut that is not a valid PPM value + * @param indexingFeesCut The indexing fees cut value + */ + error SubgraphServiceInvalidIndexingFeesCut(uint256 indexingFeesCut); + /** * @notice Thrown when an indexer tries to register with an empty URL */ @@ -252,6 +264,13 @@ interface ISubgraphService is IDataServiceFees { */ function setCurationCut(uint256 curationCut) external; + /** + * @notice Sets the data service payment cut for indexing fees + * @dev Emits a {IndexingFeesCutSet} event + * @param indexingFeesCut The indexing fees cut for the payment type + */ + function setIndexingFeesCut(uint256 indexingFeesCut) external; + /** * @notice Sets the payments destination for an indexer to receive payments * @dev Emits a {PaymentsDestinationSet} event diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index f5f04c602..f9648e4fb 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -80,6 +80,7 @@ library IndexingAgreement { * @param currentEpoch The current epoch * @param receiverDestination The address where the collected fees should be sent * @param data The encoded data containing the number of entities indexed, proof of indexing, and epoch + * @param indexingFeesCut The indexing fees cut in PPM */ struct CollectParams { address indexer; @@ -87,6 +88,7 @@ library IndexingAgreement { uint256 currentEpoch; address receiverDestination; bytes data; + uint256 indexingFeesCut; } /** @@ -271,6 +273,13 @@ library IndexingAgreement { */ error IndexingAgreementNotAuthorized(bytes16 agreementId, address unauthorizedIndexer); + /** + * @notice Thrown when indexing agreement terms are invalid + * @param tokensPerSecond The indexing agreement tokens per second + * @param maxOngoingTokensPerSecond The RCA maximum tokens per second + */ + error IndexingAgreementInvalidTerms(uint256 tokensPerSecond, uint256 maxOngoingTokensPerSecond); + /** * @notice Accept an indexing agreement. * @@ -343,7 +352,7 @@ library IndexingAgreement { agreement.allocationId = allocationId; require(metadata.version == IndexingAgreementVersion.V1, IndexingAgreementInvalidVersion(metadata.version)); - _setTermsV1(self, agreementId, metadata.terms); + _setTermsV1(self, agreementId, metadata.terms, signedRCA.rca.maxOngoingTokensPerSecond); emit IndexingAgreementAccepted( signedRCA.rca.serviceProvider, @@ -392,7 +401,12 @@ library IndexingAgreement { require(wrapper.agreement.version == IndexingAgreementVersion.V1, "internal: invalid version"); require(metadata.version == IndexingAgreementVersion.V1, IndexingAgreementInvalidVersion(metadata.version)); - _setTermsV1(self, signedRCAU.rcau.agreementId, metadata.terms); + _setTermsV1( + self, + signedRCAU.rcau.agreementId, + metadata.terms, + wrapper.collectorAgreement.maxOngoingTokensPerSecond + ); emit IndexingAgreementUpdated({ indexer: wrapper.collectorAgreement.serviceProvider, @@ -565,7 +579,7 @@ library IndexingAgreement { agreementId: params.agreementId, collectionId: bytes32(uint256(uint160(wrapper.agreement.allocationId))), tokens: expectedTokens, - dataServiceCut: 0, + dataServiceCut: params.indexingFeesCut, receiverDestination: params.receiverDestination, maxSlippage: data.maxSlippage }) @@ -621,9 +635,16 @@ library IndexingAgreement { * @param _manager The indexing agreement storage manager * @param _agreementId The id of the agreement to update * @param _data The encoded terms data + * @param maxOngoingTokensPerSecond The RCA maximum tokens per second limit for validation */ - function _setTermsV1(StorageManager storage _manager, bytes16 _agreementId, bytes memory _data) private { + function _setTermsV1( + StorageManager storage _manager, + bytes16 _agreementId, + bytes memory _data, + uint256 maxOngoingTokensPerSecond + ) private { IndexingAgreementTermsV1 memory newTerms = IndexingAgreementDecoder.decodeIndexingAgreementTermsV1(_data); + _validateTermsAgainstRCA(newTerms, maxOngoingTokensPerSecond); _manager.termsV1[_agreementId].tokensPerSecond = newTerms.tokensPerSecond; _manager.termsV1[_agreementId].tokensPerEntityPerSecond = newTerms.tokensPerEntityPerSecond; } @@ -764,4 +785,19 @@ library IndexingAgreement { collectorAgreement: _directory().recurringCollector().getAgreement(agreementId) }); } + + /** + * @notice Validates indexing agreement terms against RCA limits + * @param terms The indexing agreement terms to validate + * @param maxOngoingTokensPerSecond The RCA maximum tokens per second limit + */ + function _validateTermsAgainstRCA( + IndexingAgreementTermsV1 memory terms, + uint256 maxOngoingTokensPerSecond + ) private pure { + require( + terms.tokensPerSecond <= maxOngoingTokensPerSecond, + IndexingAgreementInvalidTerms(terms.tokensPerSecond, maxOngoingTokensPerSecond) + ); + } } diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol index f8f5af811..a50e53f0d 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol @@ -95,7 +95,7 @@ library IndexingAgreementDecoder { ) { return decoded; } catch { - revert IndexingAgreementDecoderInvalidData("decodeCollectIndexingFeeData", data); + revert IndexingAgreementDecoderInvalidData("decodeIndexingAgreementTermsV1", data); } } } diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/accept.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/accept.t.sol index ac8981466..8e7cafdf6 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/accept.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/accept.t.sol @@ -275,7 +275,7 @@ contract SubgraphServiceIndexingAgreementAcceptTest is SubgraphServiceIndexingAg bytes memory expectedErr = abi.encodeWithSelector( IndexingAgreementDecoder.IndexingAgreementDecoderInvalidData.selector, - "decodeCollectIndexingFeeData", + "decodeIndexingAgreementTermsV1", invalidTermsData ); vm.expectRevert(expectedErr); diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/shared.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/shared.t.sol index b51008c20..09660ff57 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/shared.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/shared.t.sol @@ -288,7 +288,7 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun ); rcau.metadata = _encodeUpdateIndexingAgreementMetadataV1( _newUpdateIndexingAgreementMetadataV1( - _ctx.ctxInternal.seed.termsV1.tokensPerSecond, + bound(_ctx.ctxInternal.seed.termsV1.tokensPerSecond, 0, _rca.maxOngoingTokensPerSecond), _ctx.ctxInternal.seed.termsV1.tokensPerEntityPerSecond ) );