From ac4ffc800d002a9deb92132c8b21b55ef3898bfa Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 23 May 2025 08:50:57 -0300 Subject: [PATCH 01/90] HACK: fix linter and compiler warnings --- .../test/unit/subgraphService/SubgraphService.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol index 3b1a74e18..29b9ebba4 100644 --- a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol @@ -381,7 +381,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { CollectPaymentData memory collectPaymentDataBefore, CollectPaymentData memory collectPaymentDataAfter ) private view { - (IGraphTallyCollector.SignedRAV memory signedRav, uint256 tokensToCollect) = abi.decode( + (IGraphTallyCollector.SignedRAV memory signedRav, ) = abi.decode( _data, (IGraphTallyCollector.SignedRAV, uint256) ); From e3bdb35c8f0431c7b59955d0d1c20a5996e960b3 Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 13 Feb 2025 11:58:58 -0300 Subject: [PATCH 02/90] feat: implement Indexing Agreements --- IndexingPaymentsTodo.md | 45 ++ .../utilities/ProvisionManager.sol | 8 + .../interfaces/IRecurringCollector.sol | 347 +++++++++++++ .../collectors/RecurringCollector.sol | 473 ++++++++++++++++++ .../contracts/utilities/GraphDirectory.sol | 8 + packages/horizon/package.json | 3 +- .../PaymentsEscrowMock.t.sol | 25 + .../RecurringCollectorAuthorizableTest.t.sol | 20 + .../RecurringCollectorControllerMock.t.sol | 25 + .../RecurringCollectorHelper.t.sol | 141 ++++++ .../payments/recurring-collector/accept.t.sol | 50 ++ .../payments/recurring-collector/cancel.t.sol | 53 ++ .../recurring-collector/collect.t.sol | 292 +++++++++++ .../payments/recurring-collector/shared.t.sol | 194 +++++++ .../recurring-collector/upgrade.t.sol | 156 ++++++ .../test/unit/utilities/Authorizable.t.sol | 33 +- .../GraphDirectoryImplementation.sol | 5 +- .../horizon/test/unit/utils/Bounder.t.sol | 29 +- .../contracts/DisputeManager.sol | 105 ++++ .../contracts/SubgraphService.sol | 148 +++++- .../contracts/SubgraphServiceExtension.sol | 70 +++ .../contracts/interfaces/IDisputeManager.sol | 58 ++- .../contracts/interfaces/ISubgraphService.sol | 9 +- .../interfaces/ISubgraphServiceExtension.sol | 32 ++ .../contracts/libraries/Decoder.sol | 95 ++++ .../contracts/libraries/IndexingAgreement.sol | 459 +++++++++++++++++ .../libraries/SubgraphServiceLib.sol | 26 + .../contracts/libraries/UnsafeDecoder.sol | 49 ++ .../contracts/utilities/Directory.sol | 51 +- packages/subgraph-service/package.json | 3 +- .../test/unit/SubgraphBaseTest.t.sol | 14 +- .../subgraphService/SubgraphService.t.sol | 44 +- .../subgraphService/collect/collect.t.sol | 25 - .../indexing-agreement/accept.t.sol | 250 +++++++++ .../indexing-agreement/base.t.sol | 35 ++ .../indexing-agreement/cancel.t.sol | 215 ++++++++ .../indexing-agreement/collect.t.sol | 247 +++++++++ .../indexing-agreement/integration.t.sol | 133 +++++ .../indexing-agreement/shared.t.sol | 373 ++++++++++++++ .../indexing-agreement/upgrade.t.sol | 155 ++++++ 40 files changed, 4415 insertions(+), 88 deletions(-) create mode 100644 IndexingPaymentsTodo.md create mode 100644 packages/horizon/contracts/interfaces/IRecurringCollector.sol create mode 100644 packages/horizon/contracts/payments/collectors/RecurringCollector.sol create mode 100644 packages/horizon/test/unit/payments/recurring-collector/PaymentsEscrowMock.t.sol create mode 100644 packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorAuthorizableTest.t.sol create mode 100644 packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorControllerMock.t.sol create mode 100644 packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol create mode 100644 packages/horizon/test/unit/payments/recurring-collector/accept.t.sol create mode 100644 packages/horizon/test/unit/payments/recurring-collector/cancel.t.sol create mode 100644 packages/horizon/test/unit/payments/recurring-collector/collect.t.sol create mode 100644 packages/horizon/test/unit/payments/recurring-collector/shared.t.sol create mode 100644 packages/horizon/test/unit/payments/recurring-collector/upgrade.t.sol create mode 100644 packages/subgraph-service/contracts/SubgraphServiceExtension.sol create mode 100644 packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol create mode 100644 packages/subgraph-service/contracts/libraries/Decoder.sol create mode 100644 packages/subgraph-service/contracts/libraries/IndexingAgreement.sol create mode 100644 packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol create mode 100644 packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol delete mode 100644 packages/subgraph-service/test/unit/subgraphService/collect/collect.t.sol create mode 100644 packages/subgraph-service/test/unit/subgraphService/indexing-agreement/accept.t.sol create mode 100644 packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol create mode 100644 packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol create mode 100644 packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol create mode 100644 packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol create mode 100644 packages/subgraph-service/test/unit/subgraphService/indexing-agreement/shared.t.sol create mode 100644 packages/subgraph-service/test/unit/subgraphService/indexing-agreement/upgrade.t.sol diff --git a/IndexingPaymentsTodo.md b/IndexingPaymentsTodo.md new file mode 100644 index 000000000..935dc6f12 --- /dev/null +++ b/IndexingPaymentsTodo.md @@ -0,0 +1,45 @@ +# Still pending + +* Arbitration Charter: Update to support disputing IndexingFee. +* Check code coverage +* Check contract size +* Don't love cancel agreement on stop service / close stale allocation. +* Missing Upgrade event for subgraph service + +# Done + +* DONE: ~~Switch cancel event in recurring collector to use Enum~~ +* DONE: ~~Switch timestamps to uint64~~ +* DONE: ~~Check that UUID-v4 fits in `bytes16`~~ +* DONE: ~~Double check cancelation policy. Who can cancel when? Right now is either party at any time. Answer: If gateway cancels allow collection till that point.~~ +* DONE: ~~If an indexer closes an allocation, what should happen to the accepeted agreement? Answer: Look into canceling agreement as part of stop service.~~ +* DONE: ~~Switch `duration` for `endsAt`? Answer: Do it.~~ +* DONE: ~~Support a way for gateway to shop an agreement around? Deadline + dedup key? So only one agreement with the dedupe key can be accepted? Answer: No. Agreements will be "signaled" as approved or rejected on the API call that sends the agreement. We'll trust (and verify) that that's the case.~~ +* DONE: ~~Test `upgrade` paths~~ +* DONE: ~~Fix upgrade.t.sol, lots of comments~~ +* DONE: ~~How do we solve for the case where an indexer has reached their max expected payout for the initial sync but haven't reached the current epoch (thus their POI is incorrect)? Answer: Signal in the event that the max amount was collected, so that fisherman understand the case.~~ +* DONE: ~~Debate epoch check protocol team. Maybe don't revert but store it in event. Pablo suggest block number instead of epoch.~~ +* DONE: ~~Should we set a different param for initial collection time max? Some subgraphs take a lot to catch up. Answer: Do nothing. Make sure that zero POIs allow to eventually sync~~ +* DONE: ~~Since an allocation is required for collecting, do we want to expect that the allocation is not stale? Do we want to add code to collect rewards as part of the collection of fees? Make sure allocation is more than one epoch old if we attempt this. Answer: Ignore stale allocation~~ +* DONE: ~~If service wants to collect more than collector allows. Collector limits but doesn't tell the service? Currently reverts. Answer: Allow for max allowed~~ +* DONE: ~~What should happen if the escrow doesn't have enough funds? Answer: Reverts~~ +* DONE: ~~Don't pay for entities on initial collection? Where did we land in terms of payment terms? Answer: pay initial~~ +* DONE: ~~Test lock stake~~ +* DONE: ~~Reduce the number of errors declared and returned~~ +* DONE: ~~Support `DisputeManager`~~ +* DONE: ~~Check upgrade conditions. Support indexing agreement upgradability, so that there is a mechanism to adjust the rates without having to cancel and start over.~~ +* DONE: ~~Maybe check that the epoch the indexer is sending is the one the transaction will be run in?~~ +* DONE: ~~Should we deal with zero entities declared as a special case?~~ +* DONE: ~~Support for agreements that end up in `RecurringCollectorCollectionTooLate` or ways to avoid getting to that state.~~ +* DONE: ~~Make `agreementId` unique globally so that we don't need the full tuple (`payer`+`indexer`+`agreementId`) as key?~~ +* DONE: ~~Maybe IRecurringCollector.cancel(address payer, address serviceProvider, bytes16 agreementId) should only take in agreementId?~~ +* DONE: ~~Unify to one error in Decoder.sol~~ +* DONE: ~~Built-in upgrade path to indexing agreements v2~~ +* DONE: ~~Missing events for accept, cancel, upgrade RCAs.~~ + +# Won't Fix + +* Add upgrade path to v2 collector terms +* Expose a function that indexers can use to calculate the tokens to be collected and other collection params? +* Place all agreement terms into one struct +* It's more like a collect + cancel since the indexer is expected to stop work then and there. When posting a POI that's < N-1 epoch. Answer: Emit signal that the collection is meant to be final. Counter: Won't do since collector can't signal back to data service that payment is maxed out. Could emit an event from the collector, but is it really worth it? Right now any collection where epoch POI < current POI is suspect. diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol index 699394c8d..1aeb8f7c6 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -118,6 +118,14 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa _; } + modifier onlyAuthorizedForProvisionHack(address caller, address serviceProvider) { + require( + _graphStaking().isAuthorized(serviceProvider, address(this), caller), + ProvisionManagerNotAuthorized(serviceProvider, caller) + ); + _; + } + /** * @notice Checks if a provision of a service provider is valid according * to the parameter ranges established. diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol new file mode 100644 index 000000000..5a6badd42 --- /dev/null +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IPaymentsCollector } from "./IPaymentsCollector.sol"; +import { IGraphPayments } from "./IGraphPayments.sol"; +import { IAuthorizable } from "./IAuthorizable.sol"; + +/** + * @title Interface for the {RecurringCollector} contract + * @dev Implements the {IPaymentCollector} interface as defined by the Graph + * Horizon payments protocol. + * @notice Implements a payments collector contract that can be used to collect + * recurrent payments. + */ +interface IRecurringCollector is IAuthorizable, IPaymentsCollector { + enum AgreementState { + NotAccepted, + Accepted, + CanceledByServiceProvider, + CanceledByPayer + } + + enum CancelAgreementBy { + ServiceProvider, + Payer + } + + /// @notice A representation of a signed Recurring Collection Agreement (RCA) + struct SignedRCA { + // The RCA + RecurringCollectionAgreement rca; + // Signature - 65 bytes: r (32 Bytes) || s (32 Bytes) || v (1 Byte) + bytes signature; + } + + /// @notice The Recurring Collection Agreement (RCA) + struct RecurringCollectionAgreement { + // The agreement ID of the RCA + bytes16 agreementId; + // The deadline for accepting the RCA + uint64 deadline; + // The timestamp when the agreement ends + uint64 endsAt; + // The address of the payer the RCA was issued by + address payer; + // The address of the data service the RCA was issued to + address dataService; + // The address of the service provider the RCA was issued to + address serviceProvider; + // The maximum amount of tokens that can be collected in the first collection + // on top of the amount allowed for subsequent collections + uint256 maxInitialTokens; + // The maximum amount of tokens that can be collected per second + // except for the first collection + uint256 maxOngoingTokensPerSecond; + // The minimum amount of seconds that must pass between collections + uint32 minSecondsPerCollection; + // The maximum amount of seconds that can pass between collections + uint32 maxSecondsPerCollection; + // Arbitrary metadata to extend functionality if a data service requires it + bytes metadata; + } + + /// @notice A representation of a signed Recurring Collection Agreement Upgrade (RCAU) + struct SignedRCAU { + // The RCAU + RecurringCollectionAgreementUpgrade rcau; + // Signature - 65 bytes: r (32 Bytes) || s (32 Bytes) || v (1 Byte) + bytes signature; + } + + struct RecurringCollectionAgreementUpgrade { + // The agreement ID + bytes16 agreementId; + // The deadline for upgrading + uint64 deadline; + // The timestamp when the agreement ends + uint64 endsAt; + // The maximum amount of tokens that can be collected in the first collection + // on top of the amount allowed for subsequent collections + uint256 maxInitialTokens; + // The maximum amount of tokens that can be collected per second + // except for the first collection + uint256 maxOngoingTokensPerSecond; + // The minimum amount of seconds that must pass between collections + uint32 minSecondsPerCollection; + // The maximum amount of seconds that can pass between collections + uint32 maxSecondsPerCollection; + // Arbitrary metadata to extend functionality if a data service requires it + bytes metadata; + } + + /// @notice The data for an agreement + struct AgreementData { + // The address of the data service + address dataService; + // The address of the payer + address payer; + // The address of the service provider + address serviceProvider; + // The timestamp when the agreement was accepted + uint64 acceptedAt; + // The timestamp when the agreement was last collected at + uint64 lastCollectionAt; + // The timestamp when the agreement ends + uint64 endsAt; + // The maximum amount of tokens that can be collected in the first collection + // on top of the amount allowed for subsequent collections + uint256 maxInitialTokens; + // The maximum amount of tokens that can be collected per second + // except for the first collection + uint256 maxOngoingTokensPerSecond; + // The minimum amount of seconds that must pass between collections + uint32 minSecondsPerCollection; + // The maximum amount of seconds that can pass between collections + uint32 maxSecondsPerCollection; + // The timestamp when the agreement was canceled + uint64 canceledAt; + // The state of the agreement + AgreementState state; + } + + /// @notice The params for collecting an agreement + struct CollectParams { + bytes16 agreementId; + // The collection ID + bytes32 collectionId; + // The amount of tokens to collect + uint256 tokens; + // The data service cut in PPM + uint256 dataServiceCut; + } + + /** + * @notice Emitted when an agreement is accepted + * @param dataService The address of the data service + * @param payer The address of the payer + * @param serviceProvider The address of the service provider + * @param agreementId The agreement ID + * @param acceptedAt The timestamp when the agreement was accepted + * @param endsAt The timestamp when the agreement ends + * @param maxInitialTokens The maximum amount of tokens that can be collected in the first collection + * @param maxOngoingTokensPerSecond The maximum amount of tokens that can be collected per second + * @param minSecondsPerCollection The minimum amount of seconds that must pass between collections + * @param maxSecondsPerCollection The maximum amount of seconds that can pass between collections + */ + event AgreementAccepted( + address indexed dataService, + address indexed payer, + address indexed serviceProvider, + bytes16 agreementId, + uint64 acceptedAt, + uint64 endsAt, + uint256 maxInitialTokens, + uint256 maxOngoingTokensPerSecond, + uint32 minSecondsPerCollection, + uint32 maxSecondsPerCollection + ); + + /** + * @notice Emitted when an agreement is canceled + * @param dataService The address of the data service + * @param payer The address of the payer + * @param serviceProvider The address of the service provider + * @param agreementId The agreement ID + * @param canceledAt The timestamp when the agreement was canceled + * @param canceledBy The party that canceled the agreement + */ + event AgreementCanceled( + address indexed dataService, + address indexed payer, + address indexed serviceProvider, + bytes16 agreementId, + uint64 canceledAt, + CancelAgreementBy canceledBy + ); + + /** + * @notice Emitted when an agreement is upgraded + * @param dataService The address of the data service + * @param payer The address of the payer + * @param serviceProvider The address of the service provider + * @param agreementId The agreement ID + * @param upgradedAt The timestamp when the agreement was upgraded + * @param endsAt The timestamp when the agreement ends + * @param maxInitialTokens The maximum amount of tokens that can be collected in the first collection + * @param maxOngoingTokensPerSecond The maximum amount of tokens that can be collected per second + * @param minSecondsPerCollection The minimum amount of seconds that must pass between collections + * @param maxSecondsPerCollection The maximum amount of seconds that can pass between collections + */ + event AgreementUpgraded( + address indexed dataService, + address indexed payer, + address indexed serviceProvider, + bytes16 agreementId, + uint64 upgradedAt, + uint64 endsAt, + uint256 maxInitialTokens, + uint256 maxOngoingTokensPerSecond, + uint32 minSecondsPerCollection, + uint32 maxSecondsPerCollection + ); + + /** + * @notice Emitted when an RCA is collected + * @param dataService The address of the data service + * @param payer The address of the payer + * @param serviceProvider The address of the service provider + */ + event RCACollected( + address indexed dataService, + address indexed payer, + address indexed serviceProvider, + bytes16 agreementId, + bytes32 collectionId, + uint256 tokens, + uint256 dataServiceCut + ); + + /** + * Thrown when accepting an agreement with a zero ID + */ + error RecurringCollectorAgreementIdZero(); + + /** + * Thrown when interacting with an agreement not owned by the message sender + * @param agreementId The agreement ID + * @param unauthorizedDataService The address of the unauthorized data service + */ + error RecurringCollectorDataServiceNotAuthorized(bytes16 agreementId, address unauthorizedDataService); + + /** + * Thrown when interacting with an agreement with an elapsed deadline + * @param deadline The elapsed deadline timestamp + */ + error RecurringCollectorAgreementDeadlineElapsed(uint64 deadline); + + /** + * Thrown when the signer is invalid + */ + error RecurringCollectorInvalidSigner(); + + /** + * Thrown when the payment type is not IndexingFee + * @param invalidPaymentType The invalid payment type + */ + error RecurringCollectorInvalidPaymentType(IGraphPayments.PaymentTypes invalidPaymentType); + + /** + * Thrown when the caller is not the data service the RCA was issued to + * @param unauthorizedCaller The address of the caller + * @param dataService The address of the data service + */ + error RecurringCollectorUnauthorizedCaller(address unauthorizedCaller, address dataService); + + /** + * Thrown when calling collect() with invalid data + * @param invalidData The invalid data + */ + error RecurringCollectorInvalidCollectData(bytes invalidData); + + /** + * Thrown when interacting with an agreement that has an incorrect state + * @param agreementId The agreement ID + * @param incorrectState The incorrect state + */ + error RecurringCollectorAgreementIncorrectState(bytes16 agreementId, AgreementState incorrectState); + + /** + * Thrown when accepting or upgrading an agreement with invalid parameters + */ + error RecurringCollectorAgreementInvalidParameters(string message); + + /** + * Thrown when calling collect() on an elapsed agreement + * @param agreementId The agreement ID + * @param endsAt The agreement end timestamp + */ + error RecurringCollectorAgreementElapsed(bytes16 agreementId, uint64 endsAt); + + /** + * Thrown when calling collect() too soon + * @param agreementId The agreement ID + * @param secondsSinceLast Seconds since last collection + * @param minSeconds Minimum seconds between collections + */ + error RecurringCollectorCollectionTooSoon(bytes16 agreementId, uint32 secondsSinceLast, uint32 minSeconds); + + /** + * Thrown when calling collect() too late + * @param agreementId The agreement ID + * @param secondsSinceLast Seconds since last collection + * @param maxSeconds Maximum seconds between collections + */ + error RecurringCollectorCollectionTooLate(bytes16 agreementId, uint64 secondsSinceLast, uint32 maxSeconds); + + /** + * @dev Accept an indexing agreement. + * @param signedRCA The signed Recurring Collection Agreement which is to be accepted. + */ + function accept(SignedRCA calldata signedRCA) external; + + /** + * @dev Cancel an indexing agreement. + * @param agreementId The agreement's ID. + * @param by The party that is canceling the agreement. + */ + function cancel(bytes16 agreementId, CancelAgreementBy by) external; + + /** + * @dev Upgrade an indexing agreement. + */ + function upgrade(SignedRCAU calldata signedRCAU) external; + + /** + * @dev Computes the hash of a RecurringCollectionAgreement (RCA). + * @param rca The RCA for which to compute the hash. + * @return The hash of the RCA. + */ + function encodeRCA(RecurringCollectionAgreement calldata rca) external view returns (bytes32); + + /** + * @dev Computes the hash of a RecurringCollectionAgreementUpgrade (RCAU). + * @param rcau The RCAU for which to compute the hash. + * @return The hash of the RCAU. + */ + function encodeRCAU(RecurringCollectionAgreementUpgrade calldata rcau) external view returns (bytes32); + + /** + * @dev Recovers the signer address of a signed RecurringCollectionAgreement (RCA). + * @param signedRCA The SignedRCA containing the RCA and its signature. + * @return The address of the signer. + */ + function recoverRCASigner(SignedRCA calldata signedRCA) external view returns (address); + + /** + * @dev Recovers the signer address of a signed RecurringCollectionAgreementUpgrade (RCAU). + * @param signedRCAU The SignedRCAU containing the RCAU and its signature. + * @return The address of the signer. + */ + function recoverRCAUSigner(SignedRCAU calldata signedRCAU) external view returns (address); + + /** + * @notice Gets an agreement. + */ + function getAgreement(bytes16 agreementId) external view returns (AgreementData memory); +} diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol new file mode 100644 index 000000000..cdde83b3e --- /dev/null +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +import { Authorizable } from "../../utilities/Authorizable.sol"; +import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; +import { IRecurringCollector } from "../../interfaces/IRecurringCollector.sol"; +import { IGraphPayments } from "../../interfaces/IGraphPayments.sol"; +import { PPMMath } from "../../libraries/PPMMath.sol"; +import { MathUtils } from "../../libraries/MathUtils.sol"; + +/** + * @title RecurringCollector contract + * @dev Implements the {IRecurringCollector} interface. + * @notice A payments collector contract that can be used to collect payments using a RCA (Recurring Collection Agreement). + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringCollector { + using PPMMath for uint256; + + /// @notice The EIP712 typehash for the RecurringCollectionAgreement struct + bytes32 public constant EIP712_RCA_TYPEHASH = + keccak256( + "RecurringCollectionAgreement(bytes16 agreementId,uint256 deadline,uint256 endsAt,address payer,address dataService,address serviceProvider,uint256 maxInitialTokens,uint256 maxOngoingTokensPerSecond,uint32 minSecondsPerCollection,uint32 maxSecondsPerCollection,bytes metadata)" + ); + + /// @notice The EIP712 typehash for the RecurringCollectionAgreementUpgrade struct + bytes32 public constant EIP712_RCAU_TYPEHASH = + keccak256( + "RecurringCollectionAgreementUpgrade(bytes16 agreementId,uint256 deadline,uint256 endsAt,uint256 maxInitialTokens,uint256 maxOngoingTokensPerSecond,uint32 minSecondsPerCollection,uint32 maxSecondsPerCollection,bytes metadata)" + ); + + /// @notice Tracks agreements + mapping(bytes16 agreementId => AgreementData data) public agreements; + + /** + * @notice Constructs a new instance of the RecurringCollector contract. + * @param eip712Name The name of the EIP712 domain. + * @param eip712Version The version of the EIP712 domain. + * @param controller The address of the Graph controller. + * @param revokeSignerThawingPeriod The duration (in seconds) in which a signer is thawing before they can be revoked. + */ + constructor( + string memory eip712Name, + string memory eip712Version, + address controller, + uint256 revokeSignerThawingPeriod + ) EIP712(eip712Name, eip712Version) GraphDirectory(controller) Authorizable(revokeSignerThawingPeriod) {} + + /** + * @notice Initiate a payment collection through the payments protocol. + * See {IGraphPayments.collect}. + * @dev Caller must be the data service the RCA was issued to. + */ + function collect(IGraphPayments.PaymentTypes paymentType, bytes calldata data) external returns (uint256) { + require( + paymentType == IGraphPayments.PaymentTypes.IndexingFee, + RecurringCollectorInvalidPaymentType(paymentType) + ); + try this.decodeCollectData(data) returns (CollectParams memory collectParams) { + return _collect(collectParams); + } catch { + revert RecurringCollectorInvalidCollectData(data); + } + } + + /** + * @notice Accept an indexing agreement. + * See {IRecurringCollector.accept}. + * @dev Caller must be the data service the RCA was issued to. + */ + function accept(SignedRCA calldata signedRCA) external { + require(signedRCA.rca.agreementId != bytes16(0), RecurringCollectorAgreementIdZero()); + require( + msg.sender == signedRCA.rca.dataService, + RecurringCollectorUnauthorizedCaller(msg.sender, signedRCA.rca.dataService) + ); + require( + signedRCA.rca.deadline >= block.timestamp, + RecurringCollectorAgreementDeadlineElapsed(signedRCA.rca.deadline) + ); + + // check that the voucher is signed by the payer (or proxy) + _requireAuthorizedRCASigner(signedRCA); + + AgreementData storage agreement = _getForUpdateAgreement(signedRCA.rca.agreementId); + // check that the agreement is not already accepted + require( + agreement.state == AgreementState.NotAccepted, + RecurringCollectorAgreementIncorrectState(signedRCA.rca.agreementId, agreement.state) + ); + + // accept the agreement + agreement.acceptedAt = uint64(block.timestamp); + agreement.state = AgreementState.Accepted; + agreement.dataService = signedRCA.rca.dataService; + agreement.payer = signedRCA.rca.payer; + agreement.serviceProvider = signedRCA.rca.serviceProvider; + agreement.endsAt = signedRCA.rca.endsAt; + agreement.maxInitialTokens = signedRCA.rca.maxInitialTokens; + agreement.maxOngoingTokensPerSecond = signedRCA.rca.maxOngoingTokensPerSecond; + agreement.minSecondsPerCollection = signedRCA.rca.minSecondsPerCollection; + agreement.maxSecondsPerCollection = signedRCA.rca.maxSecondsPerCollection; + _requireValidAgreement(agreement); + + emit AgreementAccepted( + agreement.dataService, + agreement.payer, + agreement.serviceProvider, + signedRCA.rca.agreementId, + agreement.acceptedAt, + agreement.endsAt, + agreement.maxInitialTokens, + agreement.maxOngoingTokensPerSecond, + agreement.minSecondsPerCollection, + agreement.maxSecondsPerCollection + ); + } + + /** + * @notice Cancel an indexing agreement. + * See {IRecurringCollector.cancel}. + * @dev Caller must be the data service for the agreement. + */ + function cancel(bytes16 agreementId, CancelAgreementBy by) external { + AgreementData storage agreement = _getForUpdateAgreement(agreementId); + require( + agreement.state == AgreementState.Accepted, + RecurringCollectorAgreementIncorrectState(agreementId, agreement.state) + ); + require( + agreement.dataService == msg.sender, + RecurringCollectorDataServiceNotAuthorized(agreementId, msg.sender) + ); + agreement.canceledAt = uint64(block.timestamp); + agreement.state = by == CancelAgreementBy.Payer + ? AgreementState.CanceledByPayer + : AgreementState.CanceledByServiceProvider; + + emit AgreementCanceled( + agreement.dataService, + agreement.payer, + agreement.serviceProvider, + agreementId, + agreement.canceledAt, + by + ); + } + + /** + * @notice Upgrade an indexing agreement. + * See {IRecurringCollector.upgrade}. + * @dev Caller must be the data service for the agreement. + */ + function upgrade(SignedRCAU calldata signedRCAU) external { + require( + signedRCAU.rcau.deadline >= block.timestamp, + RecurringCollectorAgreementDeadlineElapsed(signedRCAU.rcau.deadline) + ); + + AgreementData storage agreement = _getForUpdateAgreement(signedRCAU.rcau.agreementId); + require( + agreement.state == AgreementState.Accepted, + RecurringCollectorAgreementIncorrectState(signedRCAU.rcau.agreementId, agreement.state) + ); + require( + agreement.dataService == msg.sender, + RecurringCollectorDataServiceNotAuthorized(signedRCAU.rcau.agreementId, msg.sender) + ); + + // check that the voucher is signed by the payer (or proxy) + _requireAuthorizedRCAUSigner(signedRCAU, agreement.payer); + + // upgrade the agreement + agreement.endsAt = signedRCAU.rcau.endsAt; + agreement.maxInitialTokens = signedRCAU.rcau.maxInitialTokens; + agreement.maxOngoingTokensPerSecond = signedRCAU.rcau.maxOngoingTokensPerSecond; + agreement.minSecondsPerCollection = signedRCAU.rcau.minSecondsPerCollection; + agreement.maxSecondsPerCollection = signedRCAU.rcau.maxSecondsPerCollection; + _requireValidAgreement(agreement); + + emit AgreementUpgraded( + agreement.dataService, + agreement.payer, + agreement.serviceProvider, + signedRCAU.rcau.agreementId, + uint64(block.timestamp), + agreement.endsAt, + agreement.maxInitialTokens, + agreement.maxOngoingTokensPerSecond, + agreement.minSecondsPerCollection, + agreement.maxSecondsPerCollection + ); + } + + /** + * @notice See {IRecurringCollector.recoverRCASigner} + */ + function recoverRCASigner(SignedRCA calldata signedRCA) external view returns (address) { + return _recoverRCASigner(signedRCA); + } + + /** + * @notice See {IRecurringCollector.recoverRCAUSigner} + */ + function recoverRCAUSigner(SignedRCAU calldata signedRCAU) external view returns (address) { + return _recoverRCAUSigner(signedRCAU); + } + + /** + * @notice See {IRecurringCollector.encodeRCA} + */ + function encodeRCA(RecurringCollectionAgreement calldata rca) external view returns (bytes32) { + return _encodeRCA(rca); + } + + /** + * @notice See {IRecurringCollector.encodeRCAU} + */ + function encodeRCAU(RecurringCollectionAgreementUpgrade calldata rcau) external view returns (bytes32) { + return _encodeRCAU(rcau); + } + + /** + * @notice See {IRecurringCollector.getAgreement} + */ + function getAgreement(bytes16 agreementId) external view returns (AgreementData memory) { + return _getAgreement(agreementId); + } + + /** + * @notice Decodes the collect data. + */ + function decodeCollectData(bytes calldata data) public pure returns (CollectParams memory) { + return abi.decode(data, (CollectParams)); + } + + /** + * @notice Collect payment through the payments protocol. + * @dev Caller must be the data service the RCA was issued to. + * + * Emits {PaymentCollected} and {RCACollected} events. + * + * @param _params The decoded parameters for the collection + * @return The amount of tokens collected + */ + function _collect(CollectParams memory _params) private returns (uint256) { + AgreementData storage agreement = _getForUpdateAgreement(_params.agreementId); + require( + agreement.state == AgreementState.Accepted || agreement.state == AgreementState.CanceledByPayer, + RecurringCollectorAgreementIncorrectState(_params.agreementId, agreement.state) + ); + + require( + msg.sender == agreement.dataService, + RecurringCollectorDataServiceNotAuthorized(_params.agreementId, msg.sender) + ); + + require( + agreement.endsAt >= block.timestamp, + RecurringCollectorAgreementElapsed(_params.agreementId, agreement.endsAt) + ); + + uint256 tokensToCollect = 0; + if (_params.tokens != 0) { + tokensToCollect = _requireValidCollect(agreement, _params.agreementId, _params.tokens); + + _graphPaymentsEscrow().collect( + IGraphPayments.PaymentTypes.IndexingFee, + agreement.payer, + agreement.serviceProvider, + tokensToCollect, + agreement.dataService, + _params.dataServiceCut, + agreement.serviceProvider // FIX-ME + ); + } + agreement.lastCollectionAt = uint64(block.timestamp); + + emit PaymentCollected( + IGraphPayments.PaymentTypes.IndexingFee, + _params.collectionId, + agreement.payer, + agreement.serviceProvider, + agreement.dataService, + tokensToCollect + ); + + emit RCACollected( + agreement.dataService, + agreement.payer, + agreement.serviceProvider, + _params.agreementId, + _params.collectionId, + tokensToCollect, + _params.dataServiceCut + ); + + return tokensToCollect; + } + + function _requireValidAgreement(AgreementData memory _agreement) private view { + require( + _agreement.dataService != address(0) && + _agreement.payer != address(0) && + _agreement.serviceProvider != address(0), + RecurringCollectorAgreementInvalidParameters("zero address") + ); + + // Agreement needs to end in the future + require( + _agreement.endsAt > block.timestamp, + RecurringCollectorAgreementInvalidParameters("endsAt not in future") + ); + + // Collection window needs to be at least 2 hours + require( + _agreement.maxSecondsPerCollection > _agreement.minSecondsPerCollection && + (_agreement.maxSecondsPerCollection - _agreement.minSecondsPerCollection >= 7200), + RecurringCollectorAgreementInvalidParameters("too small collection window") + ); + + // Agreement needs to last at least one min collection window + require( + _agreement.endsAt - block.timestamp >= _agreement.minSecondsPerCollection + 7200, + RecurringCollectorAgreementInvalidParameters("too small agreement window") + ); + } + + /** + * @notice Requires that the collection params are valid. + */ + function _requireValidCollect( + AgreementData memory _agreement, + bytes16 _agreementId, + uint256 _tokens + ) private view returns (uint256) { + uint256 collectionSeconds = _agreement.state == AgreementState.CanceledByPayer + ? _agreement.canceledAt + : block.timestamp; + collectionSeconds -= _agreementCollectionStartAt(_agreement); + require( + collectionSeconds >= _agreement.minSecondsPerCollection, + RecurringCollectorCollectionTooSoon( + _agreementId, + uint32(collectionSeconds), + _agreement.minSecondsPerCollection + ) + ); + require( + collectionSeconds <= _agreement.maxSecondsPerCollection, + RecurringCollectorCollectionTooLate( + _agreementId, + uint64(collectionSeconds), + _agreement.maxSecondsPerCollection + ) + ); + + uint256 maxTokens = _agreement.maxOngoingTokensPerSecond * collectionSeconds; + maxTokens += _agreement.lastCollectionAt == 0 ? _agreement.maxInitialTokens : 0; + + return MathUtils.min(_tokens, maxTokens); + } + + /** + * @notice See {IRecurringCollector.recoverRCASigner} + */ + function _recoverRCASigner(SignedRCA memory _signedRCA) private view returns (address) { + bytes32 messageHash = _encodeRCA(_signedRCA.rca); + return ECDSA.recover(messageHash, _signedRCA.signature); + } + + /** + * @notice See {IRecurringCollector.recoverRCAUSigner} + */ + function _recoverRCAUSigner(SignedRCAU memory _signedRCAU) private view returns (address) { + bytes32 messageHash = _encodeRCAU(_signedRCAU.rcau); + return ECDSA.recover(messageHash, _signedRCAU.signature); + } + + /** + * @notice See {IRecurringCollector.encodeRCA} + */ + function _encodeRCA(RecurringCollectionAgreement memory _rca) private view returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + EIP712_RCA_TYPEHASH, + _rca.agreementId, + _rca.deadline, + _rca.endsAt, + _rca.payer, + _rca.dataService, + _rca.serviceProvider, + _rca.maxInitialTokens, + _rca.maxOngoingTokensPerSecond, + _rca.minSecondsPerCollection, + _rca.maxSecondsPerCollection, + keccak256(_rca.metadata) + ) + ) + ); + } + + /** + * @notice See {IRecurringCollector.encodeRCAU} + */ + function _encodeRCAU(RecurringCollectionAgreementUpgrade memory _rcau) private view returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + EIP712_RCAU_TYPEHASH, + _rcau.agreementId, + _rcau.deadline, + _rcau.endsAt, + _rcau.maxInitialTokens, + _rcau.maxOngoingTokensPerSecond, + _rcau.minSecondsPerCollection, + _rcau.maxSecondsPerCollection, + keccak256(_rcau.metadata) + ) + ) + ); + } + + /** + * @notice Requires that the signer for the RCA is authorized + * by the payer of the RCA. + */ + function _requireAuthorizedRCASigner(SignedRCA memory _signedRCA) private view returns (address) { + address signer = _recoverRCASigner(_signedRCA); + require(_isAuthorized(_signedRCA.rca.payer, signer), RecurringCollectorInvalidSigner()); + + return signer; + } + + /** + * @notice Requires that the signer for the RCAU is authorized + * by the payer. + */ + function _requireAuthorizedRCAUSigner( + SignedRCAU memory _signedRCAU, + address _payer + ) private view returns (address) { + address signer = _recoverRCAUSigner(_signedRCAU); + require(_isAuthorized(_payer, signer), RecurringCollectorInvalidSigner()); + + return signer; + } + + /** + * @notice Gets an agreement to be updated. + */ + function _getForUpdateAgreement(bytes16 _agreementId) private view returns (AgreementData storage) { + return agreements[_agreementId]; + } + + /** + * @notice See {IRecurringCollector.getAgreement} + */ + function _getAgreement(bytes16 _agreementId) private view returns (AgreementData memory) { + return agreements[_agreementId]; + } + + function _agreementCollectionStartAt(AgreementData memory _agreement) private pure returns (uint256) { + return _agreement.lastCollectionAt > 0 ? _agreement.lastCollectionAt : _agreement.acceptedAt; + } +} diff --git a/packages/horizon/contracts/utilities/GraphDirectory.sol b/packages/horizon/contracts/utilities/GraphDirectory.sol index 418b58619..88322fa10 100644 --- a/packages/horizon/contracts/utilities/GraphDirectory.sol +++ b/packages/horizon/contracts/utilities/GraphDirectory.sol @@ -131,6 +131,14 @@ abstract contract GraphDirectory { ); } + /** + * @notice Get the Epoch Manager contract + * @return The Epoch Manager contract + */ + function graphEpochManager() external view returns (IEpochManager) { + return _graphEpochManager(); + } + /** * @notice Get the Graph Token contract * @return The Graph Token contract diff --git a/packages/horizon/package.json b/packages/horizon/package.json index c3f960433..641fc61dd 100644 --- a/packages/horizon/package.json +++ b/packages/horizon/package.json @@ -17,9 +17,10 @@ "scripts": { "lint": "pnpm lint:ts && pnpm lint:sol", "lint:ts": "eslint '**/*.{js,ts}' --fix --no-warn-ignored", - "lint:sol": "pnpm lint:sol:prettier && pnpm lint:sol:solhint", + "lint:sol": "pnpm lint:sol:prettier && pnpm lint:sol:solhint && pnpm lint:sol:solhint:test", "lint:sol:prettier": "prettier --write \"contracts/**/*.sol\" \"test/**/*.sol\"", "lint:sol:solhint": "solhint --noPrompt --fix \"contracts/**/*.sol\" --config node_modules/solhint-graph-config/index.js", + "lint:sol:solhint:test": "solhint --noPrompt --fix \"test/unit/payments/recurring-collector/*\" --config node_modules/solhint-graph-config/index.js", "lint:sol:natspec": "natspec-smells --config natspec-smells.config.js", "clean": "rm -rf build dist cache cache_forge typechain-types", "build": "hardhat compile", diff --git a/packages/horizon/test/unit/payments/recurring-collector/PaymentsEscrowMock.t.sol b/packages/horizon/test/unit/payments/recurring-collector/PaymentsEscrowMock.t.sol new file mode 100644 index 000000000..36ebdda18 --- /dev/null +++ b/packages/horizon/test/unit/payments/recurring-collector/PaymentsEscrowMock.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IGraphPayments } from "../../../../contracts/interfaces/IGraphPayments.sol"; +import { IPaymentsEscrow } from "../../../../contracts/interfaces/IPaymentsEscrow.sol"; + +contract PaymentsEscrowMock is IPaymentsEscrow { + function initialize() external {} + + function collect(IGraphPayments.PaymentTypes, address, address, uint256, address, uint256, address) external {} + + function deposit(address, address, uint256) external {} + + function depositTo(address, address, address, uint256) external {} + + function thaw(address, address, uint256) external {} + + function cancelThaw(address, address) external {} + + function withdraw(address, address) external {} + + function getBalance(address, address, address) external pure returns (uint256) { + return 0; + } +} diff --git a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorAuthorizableTest.t.sol b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorAuthorizableTest.t.sol new file mode 100644 index 000000000..ff5e39848 --- /dev/null +++ b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorAuthorizableTest.t.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IAuthorizable } from "../../../../contracts/interfaces/IAuthorizable.sol"; +import { RecurringCollector } from "../../../../contracts/payments/collectors/RecurringCollector.sol"; + +import { AuthorizableTest } from "../../../unit/utilities/Authorizable.t.sol"; +import { RecurringCollectorControllerMock } from "./RecurringCollectorControllerMock.t.sol"; + +contract RecurringCollectorAuthorizableTest is AuthorizableTest { + function newAuthorizable(uint256 thawPeriod) public override returns (IAuthorizable) { + return + new RecurringCollector( + "RecurringCollector", + "1", + address(new RecurringCollectorControllerMock(address(1))), + thawPeriod + ); + } +} diff --git a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorControllerMock.t.sol b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorControllerMock.t.sol new file mode 100644 index 000000000..3425e8b01 --- /dev/null +++ b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorControllerMock.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; + +import { IPaymentsEscrow } from "../../../../contracts/interfaces/IPaymentsEscrow.sol"; +import { ControllerMock } from "../../../../contracts/mocks/ControllerMock.sol"; + +contract RecurringCollectorControllerMock is ControllerMock, Test { + address private _invalidContractAddress; + IPaymentsEscrow private _paymentsEscrow; + + constructor(address paymentsEscrow) ControllerMock(address(0)) { + _invalidContractAddress = makeAddr("invalidContractAddress"); + _paymentsEscrow = IPaymentsEscrow(paymentsEscrow); + } + + function getContractProxy(bytes32 data) external view override returns (address) { + return data == keccak256("PaymentsEscrow") ? address(_paymentsEscrow) : _invalidContractAddress; + } + + function getPaymentsEscrow() external view returns (address) { + return address(_paymentsEscrow); + } +} diff --git a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol new file mode 100644 index 000000000..754c2e796 --- /dev/null +++ b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IRecurringCollector } from "../../../../contracts/interfaces/IRecurringCollector.sol"; +import { RecurringCollector } from "../../../../contracts/payments/collectors/RecurringCollector.sol"; +import { AuthorizableHelper } from "../../../unit/utilities/Authorizable.t.sol"; +import { Bounder } from "../../../unit/utils/Bounder.t.sol"; + +contract RecurringCollectorHelper is AuthorizableHelper, Bounder { + RecurringCollector public collector; + + constructor( + RecurringCollector collector_ + ) AuthorizableHelper(collector_, collector_.REVOKE_AUTHORIZATION_THAWING_PERIOD()) { + collector = collector_; + } + + function generateSignedRCA( + IRecurringCollector.RecurringCollectionAgreement memory rca, + uint256 signerPrivateKey + ) public view returns (IRecurringCollector.SignedRCA memory) { + bytes32 messageHash = collector.encodeRCA(rca); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + IRecurringCollector.SignedRCA memory signedRCA = IRecurringCollector.SignedRCA({ + rca: rca, + signature: signature + }); + + return signedRCA; + } + + function generateSignedRCAU( + IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau, + uint256 signerPrivateKey + ) public view returns (IRecurringCollector.SignedRCAU memory) { + bytes32 messageHash = collector.encodeRCAU(rcau); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + IRecurringCollector.SignedRCAU memory signedRCAU = IRecurringCollector.SignedRCAU({ + rcau: rcau, + signature: signature + }); + + return signedRCAU; + } + + function withElapsedAcceptDeadline( + IRecurringCollector.RecurringCollectionAgreement memory rca + ) public view returns (IRecurringCollector.RecurringCollectionAgreement memory) { + require(block.timestamp > 0, "block.timestamp can't be zero"); + require(block.timestamp <= type(uint64).max, "block.timestamp can't be huge"); + rca.deadline = uint64(bound(rca.deadline, 0, block.timestamp - 1)); + return rca; + } + + function withOKAcceptDeadline( + IRecurringCollector.RecurringCollectionAgreement memory rca + ) public view returns (IRecurringCollector.RecurringCollectionAgreement memory) { + require(block.timestamp <= type(uint64).max, "block.timestamp can't be huge"); + rca.deadline = uint64(boundTimestampMin(rca.deadline, block.timestamp)); + return rca; + } + + function sensibleRCA( + IRecurringCollector.RecurringCollectionAgreement memory rca + ) public view returns (IRecurringCollector.RecurringCollectionAgreement memory) { + vm.assume(rca.agreementId != bytes16(0)); + vm.assume(rca.dataService != address(0)); + vm.assume(rca.payer != address(0)); + vm.assume(rca.serviceProvider != address(0)); + + rca.minSecondsPerCollection = _sensibleMinSecondsPerCollection(rca.minSecondsPerCollection); + rca.maxSecondsPerCollection = _sensibleMaxSecondsPerCollection( + rca.maxSecondsPerCollection, + rca.minSecondsPerCollection + ); + + rca.deadline = _sensibleDeadline(rca.deadline); + rca.endsAt = _sensibleEndsAt(rca.endsAt, rca.maxSecondsPerCollection); + + rca.maxInitialTokens = _sensibleMaxInitialTokens(rca.maxInitialTokens); + rca.maxOngoingTokensPerSecond = _sensibleMaxOngoingTokensPerSecond(rca.maxOngoingTokensPerSecond); + + return rca; + } + + function sensibleRCAU( + IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau + ) public view returns (IRecurringCollector.RecurringCollectionAgreementUpgrade memory) { + rcau.minSecondsPerCollection = _sensibleMinSecondsPerCollection(rcau.minSecondsPerCollection); + rcau.maxSecondsPerCollection = _sensibleMaxSecondsPerCollection( + rcau.maxSecondsPerCollection, + rcau.minSecondsPerCollection + ); + + rcau.deadline = _sensibleDeadline(rcau.deadline); + rcau.endsAt = _sensibleEndsAt(rcau.endsAt, rcau.maxSecondsPerCollection); + rcau.maxInitialTokens = _sensibleMaxInitialTokens(rcau.maxInitialTokens); + rcau.maxOngoingTokensPerSecond = _sensibleMaxOngoingTokensPerSecond(rcau.maxOngoingTokensPerSecond); + + return rcau; + } + + function _sensibleDeadline(uint256 _seed) internal view returns (uint64) { + return uint64(bound(_seed, block.timestamp + 1, block.timestamp + 7200)); // between now and 2h + } + + function _sensibleEndsAt(uint256 _seed, uint32 _maxSecondsPerCollection) internal view returns (uint64) { + return + uint64( + bound( + _seed, + block.timestamp + (10 * uint256(_maxSecondsPerCollection)), + block.timestamp + (1_000_000 * uint256(_maxSecondsPerCollection)) + ) + ); // between 10 and 1M max collections + } + + function _sensibleMaxInitialTokens(uint256 _seed) internal pure returns (uint256) { + return bound(_seed, 0, 1e18 * 100_000_000); // between 0 and 100M tokens + } + + function _sensibleMaxOngoingTokensPerSecond(uint256 _seed) internal pure returns (uint256) { + return bound(_seed, 1, 1e18); // between 1 and 1e18 tokens per second + } + + function _sensibleMinSecondsPerCollection(uint32 _seed) internal pure returns (uint32) { + return uint32(bound(_seed, 10 * 60, 24 * 60 * 60)); // between 10 min and 24h + } + + function _sensibleMaxSecondsPerCollection( + uint32 _seed, + uint32 _minSecondsPerCollection + ) internal pure returns (uint32) { + return + uint32( + bound(_seed, _minSecondsPerCollection + 7200, 60 * 60 * 24 * 30) // between minSecondsPerCollection + 2h and 30 days + ); + } +} diff --git a/packages/horizon/test/unit/payments/recurring-collector/accept.t.sol b/packages/horizon/test/unit/payments/recurring-collector/accept.t.sol new file mode 100644 index 000000000..c551197af --- /dev/null +++ b/packages/horizon/test/unit/payments/recurring-collector/accept.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IRecurringCollector } from "../../../../contracts/interfaces/IRecurringCollector.sol"; + +import { RecurringCollectorSharedTest } from "./shared.t.sol"; + +contract RecurringCollectorAcceptTest is RecurringCollectorSharedTest { + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + + function test_Accept(FuzzyTestAccept calldata fuzzyTestAccept) public { + _sensibleAuthorizeAndAccept(fuzzyTestAccept); + } + + function test_Accept_Revert_WhenAcceptanceDeadlineElapsed( + IRecurringCollector.SignedRCA memory fuzzySignedRCA, + uint256 unboundedSkip + ) public { + vm.assume(fuzzySignedRCA.rca.agreementId != bytes16(0)); + skip(boundSkip(unboundedSkip, 1, type(uint64).max - block.timestamp)); + fuzzySignedRCA.rca = _recurringCollectorHelper.withElapsedAcceptDeadline(fuzzySignedRCA.rca); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorAgreementDeadlineElapsed.selector, + fuzzySignedRCA.rca.deadline + ); + vm.expectRevert(expectedErr); + vm.prank(fuzzySignedRCA.rca.dataService); + _recurringCollector.accept(fuzzySignedRCA); + } + + function test_Accept_Revert_WhenAlreadyAccepted(FuzzyTestAccept calldata fuzzyTestAccept) public { + (IRecurringCollector.SignedRCA memory accepted, ) = _sensibleAuthorizeAndAccept(fuzzyTestAccept); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorAgreementIncorrectState.selector, + accepted.rca.agreementId, + IRecurringCollector.AgreementState.Accepted + ); + vm.expectRevert(expectedErr); + vm.prank(accepted.rca.dataService); + _recurringCollector.accept(accepted); + } + + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/horizon/test/unit/payments/recurring-collector/cancel.t.sol b/packages/horizon/test/unit/payments/recurring-collector/cancel.t.sol new file mode 100644 index 000000000..fe938c825 --- /dev/null +++ b/packages/horizon/test/unit/payments/recurring-collector/cancel.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IRecurringCollector } from "../../../../contracts/interfaces/IRecurringCollector.sol"; + +import { RecurringCollectorSharedTest } from "./shared.t.sol"; + +contract RecurringCollectorCancelTest is RecurringCollectorSharedTest { + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + + function test_Cancel(FuzzyTestAccept calldata fuzzyTestAccept, uint8 unboundedCanceler) public { + _sensibleAuthorizeAndAccept(fuzzyTestAccept); + _cancel(fuzzyTestAccept.rca, _fuzzyCancelAgreementBy(unboundedCanceler)); + } + + function test_Cancel_Revert_WhenNotAccepted( + IRecurringCollector.RecurringCollectionAgreement memory fuzzyRCA, + uint8 unboundedCanceler + ) public { + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorAgreementIncorrectState.selector, + fuzzyRCA.agreementId, + IRecurringCollector.AgreementState.NotAccepted + ); + vm.expectRevert(expectedErr); + vm.prank(fuzzyRCA.dataService); + _recurringCollector.cancel(fuzzyRCA.agreementId, _fuzzyCancelAgreementBy(unboundedCanceler)); + } + + function test_Cancel_Revert_WhenNotDataService( + FuzzyTestAccept calldata fuzzyTestAccept, + uint8 unboundedCanceler, + address notDataService + ) public { + vm.assume(fuzzyTestAccept.rca.dataService != notDataService); + + _sensibleAuthorizeAndAccept(fuzzyTestAccept); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorDataServiceNotAuthorized.selector, + fuzzyTestAccept.rca.agreementId, + notDataService + ); + vm.expectRevert(expectedErr); + vm.prank(notDataService); + _recurringCollector.cancel(fuzzyTestAccept.rca.agreementId, _fuzzyCancelAgreementBy(unboundedCanceler)); + } + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol b/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol new file mode 100644 index 000000000..de4535e31 --- /dev/null +++ b/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IGraphPayments } from "../../../../contracts/interfaces/IGraphPayments.sol"; + +import { IRecurringCollector } from "../../../../contracts/interfaces/IRecurringCollector.sol"; + +import { RecurringCollectorSharedTest } from "./shared.t.sol"; + +contract RecurringCollectorCollectTest is RecurringCollectorSharedTest { + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + + function test_Collect_Revert_WhenInvalidPaymentType(uint8 unboundedPaymentType, bytes memory data) public { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes( + bound( + unboundedPaymentType, + uint256(type(IGraphPayments.PaymentTypes).min), + uint256(type(IGraphPayments.PaymentTypes).max) + ) + ); + vm.assume(paymentType != IGraphPayments.PaymentTypes.IndexingFee); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorInvalidPaymentType.selector, + paymentType + ); + vm.expectRevert(expectedErr); + _recurringCollector.collect(paymentType, data); + } + + function test_Collect_Revert_WhenInvalidData(address caller, bytes memory data) public { + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorInvalidCollectData.selector, + data + ); + vm.expectRevert(expectedErr); + vm.prank(caller); + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + } + + function test_Collect_Revert_WhenCallerNotDataService( + FuzzyTestCollect calldata fuzzy, + address notDataService + ) public { + vm.assume(fuzzy.fuzzyTestAccept.rca.dataService != notDataService); + + (IRecurringCollector.SignedRCA memory accepted, ) = _sensibleAuthorizeAndAccept(fuzzy.fuzzyTestAccept); + IRecurringCollector.CollectParams memory collectParams = fuzzy.collectParams; + + collectParams.agreementId = accepted.rca.agreementId; + bytes memory data = _generateCollectData(collectParams); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorDataServiceNotAuthorized.selector, + collectParams.agreementId, + notDataService + ); + vm.expectRevert(expectedErr); + vm.prank(notDataService); + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + } + + function test_Collect_Revert_WhenUnknownAgreement(FuzzyTestCollect memory fuzzy, address dataService) public { + bytes memory data = _generateCollectData(fuzzy.collectParams); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorAgreementIncorrectState.selector, + fuzzy.collectParams.agreementId, + IRecurringCollector.AgreementState.NotAccepted + ); + vm.expectRevert(expectedErr); + vm.prank(dataService); + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + } + + function test_Collect_Revert_WhenCanceledAgreementByServiceProvider(FuzzyTestCollect calldata fuzzy) public { + (IRecurringCollector.SignedRCA memory accepted, ) = _sensibleAuthorizeAndAccept(fuzzy.fuzzyTestAccept); + _cancel(accepted.rca, IRecurringCollector.CancelAgreementBy.ServiceProvider); + IRecurringCollector.CollectParams memory collectData = fuzzy.collectParams; + collectData.tokens = bound(collectData.tokens, 1, type(uint256).max); + IRecurringCollector.CollectParams memory collectParams = _generateCollectParams( + accepted.rca, + collectData.collectionId, + collectData.tokens, + collectData.dataServiceCut + ); + bytes memory data = _generateCollectData(collectParams); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorAgreementIncorrectState.selector, + collectParams.agreementId, + IRecurringCollector.AgreementState.CanceledByServiceProvider + ); + vm.expectRevert(expectedErr); + vm.prank(accepted.rca.dataService); + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + } + + function test_Collect_Revert_WhenAgreementElapsed( + FuzzyTestCollect calldata fuzzy, + uint256 unboundedCollectionSeconds + ) public { + (IRecurringCollector.SignedRCA memory accepted, ) = _sensibleAuthorizeAndAccept(fuzzy.fuzzyTestAccept); + + // skip to sometime after agreement elapsed + skip(boundSkipFloor(unboundedCollectionSeconds, accepted.rca.endsAt - block.timestamp + 1)); + + IRecurringCollector.CollectParams memory collectParams = fuzzy.collectParams; + collectParams.agreementId = accepted.rca.agreementId; + + bytes memory data = _generateCollectData(collectParams); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorAgreementElapsed.selector, + collectParams.agreementId, + accepted.rca.endsAt + ); + vm.expectRevert(expectedErr); + vm.prank(accepted.rca.dataService); + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + } + + function test_Collect_Revert_WhenCollectingTooSoon( + FuzzyTestCollect calldata fuzzy, + uint256 unboundedCollectionSeconds + ) public { + (IRecurringCollector.SignedRCA memory accepted, ) = _sensibleAuthorizeAndAccept(fuzzy.fuzzyTestAccept); + + skip(accepted.rca.minSecondsPerCollection); + bytes memory data = _generateCollectData( + _generateCollectParams( + accepted.rca, + fuzzy.collectParams.collectionId, + 1, + fuzzy.collectParams.dataServiceCut + ) + ); + vm.prank(accepted.rca.dataService); + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + + uint256 collectionSeconds = boundSkipCeil(unboundedCollectionSeconds, accepted.rca.minSecondsPerCollection - 1); + skip(collectionSeconds); + + IRecurringCollector.CollectParams memory collectParams = _generateCollectParams( + accepted.rca, + fuzzy.collectParams.collectionId, + bound(fuzzy.collectParams.tokens, 1, type(uint256).max), + fuzzy.collectParams.dataServiceCut + ); + data = _generateCollectData(collectParams); + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorCollectionTooSoon.selector, + collectParams.agreementId, + collectionSeconds, + accepted.rca.minSecondsPerCollection + ); + vm.expectRevert(expectedErr); + vm.prank(accepted.rca.dataService); + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + } + + function test_Collect_Revert_WhenCollectingTooLate( + FuzzyTestCollect calldata fuzzy, + uint256 unboundedFirstCollectionSeconds, + uint256 unboundedSecondCollectionSeconds + ) public { + (IRecurringCollector.SignedRCA memory accepted, ) = _sensibleAuthorizeAndAccept(fuzzy.fuzzyTestAccept); + + // skip to collectable time + skip( + boundSkip( + unboundedFirstCollectionSeconds, + accepted.rca.minSecondsPerCollection, + accepted.rca.maxSecondsPerCollection + ) + ); + bytes memory data = _generateCollectData( + _generateCollectParams( + accepted.rca, + fuzzy.collectParams.collectionId, + 1, + fuzzy.collectParams.dataServiceCut + ) + ); + vm.prank(accepted.rca.dataService); + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + + // skip beyond collectable time but still within the agreement endsAt + uint256 collectionSeconds = boundSkip( + unboundedSecondCollectionSeconds, + accepted.rca.maxSecondsPerCollection + 1, + accepted.rca.endsAt - block.timestamp + ); + skip(collectionSeconds); + + data = _generateCollectData( + _generateCollectParams( + accepted.rca, + fuzzy.collectParams.collectionId, + bound(fuzzy.collectParams.tokens, 1, type(uint256).max), + fuzzy.collectParams.dataServiceCut + ) + ); + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorCollectionTooLate.selector, + accepted.rca.agreementId, + collectionSeconds, + accepted.rca.maxSecondsPerCollection + ); + vm.expectRevert(expectedErr); + vm.prank(accepted.rca.dataService); + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + } + + function test_Collect_OK_WhenCollectingTooMuch( + FuzzyTestCollect calldata fuzzy, + uint256 unboundedInitialCollectionSeconds, + uint256 unboundedCollectionSeconds, + uint256 unboundedTokens, + bool testInitialCollection + ) public { + (IRecurringCollector.SignedRCA memory accepted, ) = _sensibleAuthorizeAndAccept(fuzzy.fuzzyTestAccept); + + if (!testInitialCollection) { + // skip to collectable time + skip( + boundSkip( + unboundedInitialCollectionSeconds, + accepted.rca.minSecondsPerCollection, + accepted.rca.maxSecondsPerCollection + ) + ); + bytes memory initialData = _generateCollectData( + _generateCollectParams( + accepted.rca, + fuzzy.collectParams.collectionId, + 1, + fuzzy.collectParams.dataServiceCut + ) + ); + vm.prank(accepted.rca.dataService); + _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, initialData); + } + + // skip to collectable time + uint256 collectionSeconds = boundSkip( + unboundedCollectionSeconds, + accepted.rca.minSecondsPerCollection, + accepted.rca.maxSecondsPerCollection + ); + skip(collectionSeconds); + uint256 maxTokens = accepted.rca.maxOngoingTokensPerSecond * collectionSeconds; + maxTokens += testInitialCollection ? accepted.rca.maxInitialTokens : 0; + uint256 tokens = bound(unboundedTokens, maxTokens + 1, type(uint256).max); + IRecurringCollector.CollectParams memory collectParams = _generateCollectParams( + accepted.rca, + fuzzy.collectParams.collectionId, + tokens, + fuzzy.collectParams.dataServiceCut + ); + bytes memory data = _generateCollectData(collectParams); + vm.prank(accepted.rca.dataService); + uint256 collected = _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + assertEq(collected, maxTokens); + } + + function test_Collect_OK( + FuzzyTestCollect calldata fuzzy, + uint256 unboundedCollectionSeconds, + uint256 unboundedTokens + ) public { + (IRecurringCollector.SignedRCA memory accepted, ) = _sensibleAuthorizeAndAccept(fuzzy.fuzzyTestAccept); + + (bytes memory data, uint256 collectionSeconds, uint256 tokens) = _generateValidCollection( + accepted.rca, + fuzzy.collectParams, + unboundedCollectionSeconds, + unboundedTokens + ); + skip(collectionSeconds); + _expectCollectCallAndEmit(accepted.rca, fuzzy.collectParams, tokens); + vm.prank(accepted.rca.dataService); + uint256 collected = _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); + assertEq(collected, tokens); + } + + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol b/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol new file mode 100644 index 000000000..b4b172277 --- /dev/null +++ b/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; + +import { IGraphPayments } from "../../../../contracts/interfaces/IGraphPayments.sol"; +import { IPaymentsCollector } from "../../../../contracts/interfaces/IPaymentsCollector.sol"; +import { IRecurringCollector } from "../../../../contracts/interfaces/IRecurringCollector.sol"; +import { RecurringCollector } from "../../../../contracts/payments/collectors/RecurringCollector.sol"; + +import { Bounder } from "../../../unit/utils/Bounder.t.sol"; +import { RecurringCollectorControllerMock } from "./RecurringCollectorControllerMock.t.sol"; +import { PaymentsEscrowMock } from "./PaymentsEscrowMock.t.sol"; +import { RecurringCollectorHelper } from "./RecurringCollectorHelper.t.sol"; + +contract RecurringCollectorSharedTest is Test, Bounder { + struct FuzzyTestCollect { + FuzzyTestAccept fuzzyTestAccept; + IRecurringCollector.CollectParams collectParams; + } + + struct FuzzyTestAccept { + IRecurringCollector.RecurringCollectionAgreement rca; + uint256 unboundedSignerKey; + } + + struct FuzzyTestUpgrade { + FuzzyTestAccept fuzzyTestAccept; + IRecurringCollector.RecurringCollectionAgreementUpgrade rcau; + } + + RecurringCollector internal _recurringCollector; + PaymentsEscrowMock internal _paymentsEscrow; + RecurringCollectorHelper internal _recurringCollectorHelper; + + function setUp() public { + _paymentsEscrow = new PaymentsEscrowMock(); + _recurringCollector = new RecurringCollector( + "RecurringCollector", + "1", + address(new RecurringCollectorControllerMock(address(_paymentsEscrow))), + 1 + ); + _recurringCollectorHelper = new RecurringCollectorHelper(_recurringCollector); + } + + function _sensibleAuthorizeAndAccept( + FuzzyTestAccept calldata _fuzzyTestAccept + ) internal returns (IRecurringCollector.SignedRCA memory, uint256 key) { + IRecurringCollector.RecurringCollectionAgreement memory rca = _recurringCollectorHelper.sensibleRCA( + _fuzzyTestAccept.rca + ); + key = boundKey(_fuzzyTestAccept.unboundedSignerKey); + return (_authorizeAndAccept(rca, key), key); + } + + // authorizes signer, signs the RCA, and accepts it + function _authorizeAndAccept( + IRecurringCollector.RecurringCollectionAgreement memory _rca, + uint256 _signerKey + ) internal returns (IRecurringCollector.SignedRCA memory) { + _recurringCollectorHelper.authorizeSignerWithChecks(_rca.payer, _signerKey); + IRecurringCollector.SignedRCA memory signedRCA = _recurringCollectorHelper.generateSignedRCA(_rca, _signerKey); + + _accept(signedRCA); + + return signedRCA; + } + + function _accept(IRecurringCollector.SignedRCA memory _signedRCA) internal { + vm.expectEmit(address(_recurringCollector)); + emit IRecurringCollector.AgreementAccepted( + _signedRCA.rca.dataService, + _signedRCA.rca.payer, + _signedRCA.rca.serviceProvider, + _signedRCA.rca.agreementId, + uint64(block.timestamp), + _signedRCA.rca.endsAt, + _signedRCA.rca.maxInitialTokens, + _signedRCA.rca.maxOngoingTokensPerSecond, + _signedRCA.rca.minSecondsPerCollection, + _signedRCA.rca.maxSecondsPerCollection + ); + vm.prank(_signedRCA.rca.dataService); + _recurringCollector.accept(_signedRCA); + } + + function _cancel( + IRecurringCollector.RecurringCollectionAgreement memory _rca, + IRecurringCollector.CancelAgreementBy _by + ) internal { + vm.expectEmit(address(_recurringCollector)); + emit IRecurringCollector.AgreementCanceled( + _rca.dataService, + _rca.payer, + _rca.serviceProvider, + _rca.agreementId, + uint64(block.timestamp), + _by + ); + vm.prank(_rca.dataService); + _recurringCollector.cancel(_rca.agreementId, _by); + } + + function _expectCollectCallAndEmit( + IRecurringCollector.RecurringCollectionAgreement memory _rca, + IRecurringCollector.CollectParams memory _fuzzyParams, + uint256 _tokens + ) internal { + vm.expectCall( + address(_paymentsEscrow), + abi.encodeCall( + _paymentsEscrow.collect, + ( + IGraphPayments.PaymentTypes.IndexingFee, + _rca.payer, + _rca.serviceProvider, + _tokens, + _rca.dataService, + _fuzzyParams.dataServiceCut, + _rca.serviceProvider + ) + ) + ); + vm.expectEmit(address(_recurringCollector)); + emit IPaymentsCollector.PaymentCollected( + IGraphPayments.PaymentTypes.IndexingFee, + _fuzzyParams.collectionId, + _rca.payer, + _rca.serviceProvider, + _rca.dataService, + _tokens + ); + + vm.expectEmit(address(_recurringCollector)); + emit IRecurringCollector.RCACollected( + _rca.dataService, + _rca.payer, + _rca.serviceProvider, + _rca.agreementId, + _fuzzyParams.collectionId, + _tokens, + _fuzzyParams.dataServiceCut + ); + } + + function _generateValidCollection( + IRecurringCollector.RecurringCollectionAgreement memory _rca, + IRecurringCollector.CollectParams memory _fuzzyParams, + uint256 _unboundedCollectionSkip, + uint256 _unboundedTokens + ) internal view returns (bytes memory, uint256, uint256) { + uint256 collectionSeconds = boundSkip( + _unboundedCollectionSkip, + _rca.minSecondsPerCollection, + _rca.maxSecondsPerCollection + ); + uint256 tokens = bound(_unboundedTokens, 1, _rca.maxOngoingTokensPerSecond * collectionSeconds); + bytes memory data = _generateCollectData( + _generateCollectParams(_rca, _fuzzyParams.collectionId, tokens, _fuzzyParams.dataServiceCut) + ); + + return (data, collectionSeconds, tokens); + } + + // Do I need this? + function _generateCollectParams( + IRecurringCollector.RecurringCollectionAgreement memory _rca, + bytes32 _collectionId, + uint256 _tokens, + uint256 _dataServiceCut + ) internal pure returns (IRecurringCollector.CollectParams memory) { + return + IRecurringCollector.CollectParams({ + agreementId: _rca.agreementId, + collectionId: _collectionId, + tokens: _tokens, + dataServiceCut: _dataServiceCut + }); + } + + function _generateCollectData( + IRecurringCollector.CollectParams memory _params + ) internal pure returns (bytes memory) { + return abi.encode(_params); + } + + function _fuzzyCancelAgreementBy(uint8 _seed) internal pure returns (IRecurringCollector.CancelAgreementBy) { + return + IRecurringCollector.CancelAgreementBy( + bound(_seed, 0, uint256(IRecurringCollector.CancelAgreementBy.Payer)) + ); + } +} diff --git a/packages/horizon/test/unit/payments/recurring-collector/upgrade.t.sol b/packages/horizon/test/unit/payments/recurring-collector/upgrade.t.sol new file mode 100644 index 000000000..d5b77bcaf --- /dev/null +++ b/packages/horizon/test/unit/payments/recurring-collector/upgrade.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IRecurringCollector } from "../../../../contracts/interfaces/IRecurringCollector.sol"; + +import { RecurringCollectorSharedTest } from "./shared.t.sol"; + +contract RecurringCollectorUpgradeTest is RecurringCollectorSharedTest { + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + + function test_Upgrade_Revert_WhenUpgradeElapsed( + IRecurringCollector.RecurringCollectionAgreement memory rca, + IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau, + uint256 unboundedUpgradeSkip + ) public { + rca = _recurringCollectorHelper.sensibleRCA(rca); + rcau = _recurringCollectorHelper.sensibleRCAU(rcau); + rcau.agreementId = rca.agreementId; + + boundSkipCeil(unboundedUpgradeSkip, type(uint64).max); + rcau.deadline = uint64(bound(rcau.deadline, 0, block.timestamp - 1)); + IRecurringCollector.SignedRCAU memory signedRCAU = IRecurringCollector.SignedRCAU({ + rcau: rcau, + signature: "" + }); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorAgreementDeadlineElapsed.selector, + rcau.deadline + ); + vm.expectRevert(expectedErr); + vm.prank(rca.dataService); + _recurringCollector.upgrade(signedRCAU); + } + + function test_Upgrade_Revert_WhenNeverAccepted( + IRecurringCollector.RecurringCollectionAgreement memory rca, + IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau + ) public { + rca = _recurringCollectorHelper.sensibleRCA(rca); + rcau = _recurringCollectorHelper.sensibleRCAU(rcau); + rcau.agreementId = rca.agreementId; + + rcau.deadline = uint64(block.timestamp); + IRecurringCollector.SignedRCAU memory signedRCAU = IRecurringCollector.SignedRCAU({ + rcau: rcau, + signature: "" + }); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorAgreementIncorrectState.selector, + rcau.agreementId, + IRecurringCollector.AgreementState.NotAccepted + ); + vm.expectRevert(expectedErr); + vm.prank(rca.dataService); + _recurringCollector.upgrade(signedRCAU); + } + + function test_Upgrade_Revert_WhenDataServiceNotAuthorized( + FuzzyTestUpgrade calldata fuzzyTestUpgrade, + address notDataService + ) public { + vm.assume(fuzzyTestUpgrade.fuzzyTestAccept.rca.dataService != notDataService); + (IRecurringCollector.SignedRCA memory accepted, uint256 signerKey) = _sensibleAuthorizeAndAccept( + fuzzyTestUpgrade.fuzzyTestAccept + ); + + IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau = _recurringCollectorHelper.sensibleRCAU( + fuzzyTestUpgrade.rcau + ); + rcau.agreementId = accepted.rca.agreementId; + + IRecurringCollector.SignedRCAU memory signedRCAU = _recurringCollectorHelper.generateSignedRCAU( + rcau, + signerKey + ); + + bytes memory expectedErr = abi.encodeWithSelector( + IRecurringCollector.RecurringCollectorDataServiceNotAuthorized.selector, + signedRCAU.rcau.agreementId, + notDataService + ); + vm.expectRevert(expectedErr); + vm.prank(notDataService); + _recurringCollector.upgrade(signedRCAU); + } + + function test_Upgrade_Revert_WhenInvalidSigner( + FuzzyTestUpgrade calldata fuzzyTestUpgrade, + uint256 unboundedInvalidSignerKey + ) public { + (IRecurringCollector.SignedRCA memory accepted, uint256 signerKey) = _sensibleAuthorizeAndAccept( + fuzzyTestUpgrade.fuzzyTestAccept + ); + uint256 invalidSignerKey = boundKey(unboundedInvalidSignerKey); + vm.assume(signerKey != invalidSignerKey); + + IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau = _recurringCollectorHelper.sensibleRCAU( + fuzzyTestUpgrade.rcau + ); + rcau.agreementId = accepted.rca.agreementId; + + IRecurringCollector.SignedRCAU memory signedRCAU = _recurringCollectorHelper.generateSignedRCAU( + rcau, + invalidSignerKey + ); + + vm.expectRevert(IRecurringCollector.RecurringCollectorInvalidSigner.selector); + vm.prank(accepted.rca.dataService); + _recurringCollector.upgrade(signedRCAU); + } + + function test_Upgrade_OK(FuzzyTestUpgrade calldata fuzzyTestUpgrade) public { + (IRecurringCollector.SignedRCA memory accepted, uint256 signerKey) = _sensibleAuthorizeAndAccept( + fuzzyTestUpgrade.fuzzyTestAccept + ); + IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau = _recurringCollectorHelper.sensibleRCAU( + fuzzyTestUpgrade.rcau + ); + rcau.agreementId = accepted.rca.agreementId; + IRecurringCollector.SignedRCAU memory signedRCAU = _recurringCollectorHelper.generateSignedRCAU( + rcau, + signerKey + ); + + vm.expectEmit(address(_recurringCollector)); + emit IRecurringCollector.AgreementUpgraded( + accepted.rca.dataService, + accepted.rca.payer, + accepted.rca.serviceProvider, + rcau.agreementId, + uint64(block.timestamp), + rcau.endsAt, + rcau.maxInitialTokens, + rcau.maxOngoingTokensPerSecond, + rcau.minSecondsPerCollection, + rcau.maxSecondsPerCollection + ); + vm.prank(accepted.rca.dataService); + _recurringCollector.upgrade(signedRCAU); + + IRecurringCollector.AgreementData memory agreement = _recurringCollector.getAgreement(accepted.rca.agreementId); + assertEq(rcau.endsAt, agreement.endsAt); + assertEq(rcau.maxInitialTokens, agreement.maxInitialTokens); + assertEq(rcau.maxOngoingTokensPerSecond, agreement.maxOngoingTokensPerSecond); + assertEq(rcau.minSecondsPerCollection, agreement.minSecondsPerCollection); + assertEq(rcau.maxSecondsPerCollection, agreement.maxSecondsPerCollection); + } + + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/horizon/test/unit/utilities/Authorizable.t.sol b/packages/horizon/test/unit/utilities/Authorizable.t.sol index 4528b339d..20ca7e2b9 100644 --- a/packages/horizon/test/unit/utilities/Authorizable.t.sol +++ b/packages/horizon/test/unit/utilities/Authorizable.t.sol @@ -14,23 +14,27 @@ contract AuthorizableImp is Authorizable { } contract AuthorizableTest is Test, Bounder { - AuthorizableImp public authorizable; + IAuthorizable public authorizable; AuthorizableHelper authHelper; modifier withFuzzyThaw(uint256 _thawPeriod) { // Max thaw period is 1 year to allow for thawing tests _thawPeriod = bound(_thawPeriod, 1, 60 * 60 * 24 * 365); - setupAuthorizable(new AuthorizableImp(_thawPeriod)); + setupAuthorizable(_thawPeriod); _; } - function setUp() public virtual { - setupAuthorizable(new AuthorizableImp(0)); + function setUp() public { + setupAuthorizable(0); } - function setupAuthorizable(AuthorizableImp _authorizable) internal { - authorizable = _authorizable; - authHelper = new AuthorizableHelper(authorizable); + function setupAuthorizable(uint256 _thawPeriod) internal { + authorizable = newAuthorizable(_thawPeriod); + authHelper = new AuthorizableHelper(authorizable, _thawPeriod); + } + + function newAuthorizable(uint256 _thawPeriod) public virtual returns (IAuthorizable) { + return new AuthorizableImp(_thawPeriod); } function test_AuthorizeSigner(uint256 _unboundedKey, address _authorizer) public { @@ -303,12 +307,12 @@ contract AuthorizableTest is Test, Bounder { authHelper.authorizeAndThawSignerWithChecks(_authorizer, signerKey); - _skip = bound(_skip, 0, authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD() - 1); + _skip = bound(_skip, 0, authHelper.revokeAuthorizationThawingPeriod() - 1); skip(_skip); bytes memory expectedErr = abi.encodeWithSelector( IAuthorizable.AuthorizableSignerStillThawing.selector, block.timestamp, - block.timestamp - _skip + authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD() + block.timestamp - _skip + authHelper.revokeAuthorizationThawingPeriod() ); vm.expectRevert(expectedErr); vm.prank(_authorizer); @@ -321,17 +325,19 @@ contract AuthorizableTest is Test, Bounder { } contract AuthorizableHelper is Test { - AuthorizableImp internal authorizable; + IAuthorizable internal authorizable; + uint256 public revokeAuthorizationThawingPeriod; - constructor(AuthorizableImp _authorizable) { + constructor(IAuthorizable _authorizable, uint256 _thawPeriod) { authorizable = _authorizable; + revokeAuthorizationThawingPeriod = _thawPeriod; } function authorizeAndThawSignerWithChecks(address _authorizer, uint256 _signerKey) public { address signer = vm.addr(_signerKey); authorizeSignerWithChecks(_authorizer, _signerKey); - uint256 thawEndTimestamp = block.timestamp + authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD(); + uint256 thawEndTimestamp = block.timestamp + revokeAuthorizationThawingPeriod; vm.expectEmit(address(authorizable)); emit IAuthorizable.SignerThawing(_authorizer, signer, thawEndTimestamp); vm.prank(_authorizer); @@ -343,7 +349,7 @@ contract AuthorizableHelper is Test { function authorizeAndRevokeSignerWithChecks(address _authorizer, uint256 _signerKey) public { address signer = vm.addr(_signerKey); authorizeAndThawSignerWithChecks(_authorizer, _signerKey); - skip(authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD() + 1); + skip(revokeAuthorizationThawingPeriod + 1); vm.expectEmit(address(authorizable)); emit IAuthorizable.SignerRevoked(_authorizer, signer); vm.prank(_authorizer); @@ -356,6 +362,7 @@ contract AuthorizableHelper is Test { address signer = vm.addr(_signerKey); assertNotAuthorized(_authorizer, signer); + require(block.timestamp < type(uint256).max, "Test cannot be run at the end of time"); uint256 proofDeadline = block.timestamp + 1; bytes memory proof = generateAuthorizationProof( block.chainid, diff --git a/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol b/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol index bf40a35b8..b30a5f825 100644 --- a/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol +++ b/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol @@ -22,6 +22,7 @@ contract GraphDirectoryImplementation is GraphDirectory { function getContractFromController(bytes memory contractName) external view returns (address) { return _graphController().getContractProxy(keccak256(contractName)); } + function graphToken() external view returns (IGraphToken) { return _graphToken(); } @@ -42,10 +43,6 @@ contract GraphDirectoryImplementation is GraphDirectory { return _graphController(); } - function graphEpochManager() external view returns (IEpochManager) { - return _graphEpochManager(); - } - function graphRewardsManager() external view returns (IRewardsManager) { return _graphRewardsManager(); } diff --git a/packages/horizon/test/unit/utils/Bounder.t.sol b/packages/horizon/test/unit/utils/Bounder.t.sol index 44e977f57..9b95a3425 100644 --- a/packages/horizon/test/unit/utils/Bounder.t.sol +++ b/packages/horizon/test/unit/utils/Bounder.t.sol @@ -6,18 +6,22 @@ import { Test } from "forge-std/Test.sol"; contract Bounder is Test { uint256 constant SECP256K1_CURVE_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; + function boundKeyAndAddr(uint256 _value) internal pure returns (uint256, address) { + uint256 key = bound(_value, 1, SECP256K1_CURVE_ORDER - 1); + return (key, vm.addr(key)); + } + function boundAddrAndKey(uint256 _value) internal pure returns (uint256, address) { - uint256 signerKey = bound(_value, 1, SECP256K1_CURVE_ORDER - 1); - return (signerKey, vm.addr(signerKey)); + return boundKeyAndAddr(_value); } function boundAddr(uint256 _value) internal pure returns (address) { - (, address addr) = boundAddrAndKey(_value); + (, address addr) = boundKeyAndAddr(_value); return addr; } function boundKey(uint256 _value) internal pure returns (uint256) { - (uint256 key, ) = boundAddrAndKey(_value); + (uint256 key, ) = boundKeyAndAddr(_value); return key; } @@ -28,4 +32,21 @@ contract Bounder is Test { function boundTimestampMin(uint256 _value, uint256 _min) internal pure returns (uint256) { return bound(_value, _min, type(uint256).max); } + + function boundSkipFloor(uint256 _value, uint256 _min) internal view returns (uint256) { + return boundSkip(_value, _min, type(uint256).max); + } + + function boundSkipCeil(uint256 _value, uint256 _max) internal view returns (uint256) { + return boundSkip(_value, 0, _max); + } + + function boundSkip(uint256 _value, uint256 _min, uint256 _max) internal view returns (uint256) { + return bound(_value, orTillEndOfTime(_min), orTillEndOfTime(_max)); + } + + function orTillEndOfTime(uint256 _value) internal view returns (uint256) { + uint256 tillEndOfTime = type(uint256).max - block.timestamp; + return _value < tillEndOfTime ? _value : tillEndOfTime; + } } diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 573e8f67e..9f39ab49f 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -5,12 +5,14 @@ import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToke import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; import { IDisputeManager } from "./interfaces/IDisputeManager.sol"; import { ISubgraphService } from "./interfaces/ISubgraphService.sol"; +import { ISubgraphServiceExtension } from "./interfaces/ISubgraphServiceExtension.sol"; import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; import { MathUtils } from "@graphprotocol/horizon/contracts/libraries/MathUtils.sol"; import { Allocation } from "./libraries/Allocation.sol"; import { Attestation } from "./libraries/Attestation.sol"; +import { IndexingAgreement } from "./libraries/IndexingAgreement.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; @@ -132,6 +134,19 @@ contract DisputeManager is return _createIndexingDisputeWithAllocation(msg.sender, disputeDeposit, allocationId, poi, blockNumber); } + /// @inheritdoc IDisputeManager + function createIndexingFeeDisputeV1( + bytes16 agreementId, + bytes32 poi, + uint256 entities + ) external override returns (bytes32) { + // Get funds from fisherman + _graphToken().pullTokens(msg.sender, disputeDeposit); + + // Create a dispute + return _createIndexingFeeDisputeV1(msg.sender, disputeDeposit, agreementId, poi, entities); + } + /// @inheritdoc IDisputeManager function createQueryDispute(bytes calldata attestationData) external override returns (bytes32) { // Get funds from fisherman @@ -501,6 +516,86 @@ contract DisputeManager is return disputeId; } + /** + * @notice Create indexing fee (version 1) dispute internal function. + * @param _fisherman The fisherman creating the dispute + * @param _deposit Amount of tokens staked as deposit + * @param _agreementId The agreement id being disputed + * @param _poi The POI being disputed + * @param _entities The number of entities disputed + * @return The dispute id + */ + function _createIndexingFeeDisputeV1( + address _fisherman, + uint256 _deposit, + bytes16 _agreementId, + bytes32 _poi, + uint256 _entities + ) private returns (bytes32) { + IndexingAgreement.AgreementWrapper memory wrapper = _getSubgraphServiceExtension().getIndexingAgreement( + _agreementId + ); + + // Agreement must have been collected on and be a version 1 + require( + wrapper.collectorAgreement.lastCollectionAt > 0, + DisputeManagerIndexingAgreementNotDisputable(_agreementId) + ); + require( + wrapper.agreement.version == IndexingAgreement.IndexingAgreementVersion.V1, + DisputeManagerIndexingAgreementInvalidVersion(wrapper.agreement.version) + ); + + // Create a disputeId + bytes32 disputeId = keccak256( + abi.encodePacked( + "IndexingFeeDisputeWithAgreement", + _agreementId, + wrapper.collectorAgreement.serviceProvider, + wrapper.collectorAgreement.payer, + _poi, + _entities + ) + ); + + // Only one dispute at a time + require(!isDisputeCreated(disputeId), DisputeManagerDisputeAlreadyCreated(disputeId)); + + // The indexer must be disputable + IHorizonStaking.Provision memory provision = _graphStaking().getProvision( + wrapper.collectorAgreement.serviceProvider, + address(_getSubgraphService()) + ); + require(provision.tokens != 0, DisputeManagerZeroTokens()); + + uint256 stakeSnapshot = _getStakeSnapshot(wrapper.collectorAgreement.serviceProvider, provision.tokens); + disputes[disputeId] = Dispute( + wrapper.collectorAgreement.serviceProvider, + _fisherman, + _deposit, + 0, // no related dispute, + DisputeType.IndexingFeeDispute, + IDisputeManager.DisputeStatus.Pending, + block.timestamp, + block.timestamp + disputePeriod, + stakeSnapshot + ); + + emit IndexingFeeDisputeCreated( + disputeId, + wrapper.collectorAgreement.serviceProvider, + _fisherman, + _deposit, + wrapper.collectorAgreement.payer, + _agreementId, + _poi, + _entities, + stakeSnapshot + ); + + return disputeId; + } + /** * @notice Accept a dispute * @param _disputeId The id of the dispute @@ -668,6 +763,16 @@ contract DisputeManager is return subgraphService; } + /** + * @notice Get the address of the subgraph service extension + * @dev Will revert if the subgraph service is not set + * @return The subgraph service address + */ + function _getSubgraphServiceExtension() private view returns (ISubgraphServiceExtension) { + require(address(subgraphService) != address(0), DisputeManagerSubgraphServiceNotSet()); + return ISubgraphServiceExtension(address(subgraphService)); + } + /** * @notice Returns whether the dispute is for a conflicting attestation or not. * @param _dispute Dispute diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 140ab9c34..ae9108b3d 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -23,6 +23,10 @@ import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol" import { Allocation } from "./libraries/Allocation.sol"; import { LegacyAllocation } from "./libraries/LegacyAllocation.sol"; +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; +import { Decoder } from "./libraries/Decoder.sol"; +import { IndexingAgreement } from "./libraries/IndexingAgreement.sol"; + /** * @title SubgraphService contract * @custom:security-contact Please email security+contracts@thegraph.com if you find any @@ -45,6 +49,7 @@ contract SubgraphService is using Allocation for mapping(address => Allocation.State); using Allocation for Allocation.State; using TokenUtils for IGraphToken; + using IndexingAgreement for IndexingAgreement.Manager; /** * @notice Checks that an indexer is registered @@ -67,8 +72,13 @@ contract SubgraphService is address graphController, address disputeManager, address graphTallyCollector, - address curation - ) DataService(graphController) Directory(address(this), disputeManager, graphTallyCollector, curation) { + address curation, + address recurringCollector, + address extension + ) + DataService(graphController) + Directory(address(this), disputeManager, graphTallyCollector, curation, recurringCollector, extension) + { _disableInitializers(); } @@ -226,6 +236,7 @@ contract SubgraphService is _allocations.get(allocationId).indexer == indexer, SubgraphServiceAllocationNotAuthorized(indexer, allocationId) ); + _cancelAllocationIndexingAgreement(allocationId); _closeAllocation(allocationId, false); emit ServiceStopped(indexer, data); } @@ -265,10 +276,10 @@ contract SubgraphService is ) external override + whenNotPaused onlyAuthorizedForProvision(indexer) onlyValidProvision(indexer) onlyRegisteredIndexer(indexer) - whenNotPaused returns (uint256) { uint256 paymentCollected = 0; @@ -277,6 +288,9 @@ contract SubgraphService is paymentCollected = _collectQueryFees(indexer, data); } else if (paymentType == IGraphPayments.PaymentTypes.IndexingRewards) { paymentCollected = _collectIndexingRewards(indexer, data); + } else if (paymentType == IGraphPayments.PaymentTypes.IndexingFee) { + (bytes16 agreementId, bytes memory iaCollectionData) = Decoder.decodeCollectIndexingFeeData(data); + paymentCollected = _collectIndexingFees(agreementId, iaCollectionData); } else { revert SubgraphServiceInvalidPaymentType(paymentType); } @@ -302,6 +316,7 @@ contract SubgraphService is Allocation.State memory allocation = _allocations.get(allocationId); require(allocation.isStale(maxPOIStaleness), SubgraphServiceCannotForceCloseAllocation(allocationId)); require(!allocation.isAltruistic(), SubgraphServiceAllocationIsAltruistic(allocationId)); + _cancelAllocationIndexingAgreement(allocationId); _closeAllocation(allocationId, true); } @@ -601,4 +616,131 @@ contract SubgraphService is ) private view returns (bytes memory) { return abi.encode(_signedRav, _curationCut, paymentsDestination[_signedRav.rav.serviceProvider]); } + + function _cancelAllocationIndexingAgreement(address _allocationId) internal { + IndexingAgreement._getManager().cancelForAllocation(_allocationId); + } + + /** + * @notice Accept an indexing agreement. + * See {ISubgraphService.acceptIndexingAgreement}. + * + * Requirements: + * - The agreement's indexer must be registered + * - The caller must be authorized by the agreement's indexer + * - The provision must be valid according to the subgraph service rules + * - Allocation must belong to the indexer and be open + * - Agreement must be for this data service + * - Agreement's subgraph deployment must match the allocation's subgraph deployment + * - Agreement must not have been accepted before + * - Allocation must not have an agreement already + * + * @dev signedRCA.rca.metadata is an encoding of {IndexingAgreement.AcceptIndexingAgreementMetadata} + * + * Emits {IndexingAgreementAccepted} event + * + * @param allocationId The id of the allocation + * @param signedRCA The signed Recurring Collection Agreement + */ + function acceptIndexingAgreement( + address allocationId, + IRecurringCollector.SignedRCA calldata signedRCA + ) + external + whenNotPaused + onlyAuthorizedForProvision(signedRCA.rca.serviceProvider) + onlyValidProvision(signedRCA.rca.serviceProvider) + onlyRegisteredIndexer(signedRCA.rca.serviceProvider) + { + IndexingAgreement._getManager().accept(_allocations, allocationId, signedRCA); + } + + /** + * @notice Collect Indexing fees + * Stake equal to the amount being collected times the `stakeToFeesRatio` is locked into a stake claim. + * This claim can be released at a later stage once expired. + * + * It's important to note that before collecting this function will attempt to release any expired stake claims. + * This could lead to an out of gas error if there are too many expired claims. In that case, the indexer will need to + * manually release the claims, see {IDataServiceFees-releaseStake}, before attempting to collect again. + * + * @dev Uses the {RecurringCollector} to collect payment from Graph Horizon payments protocol. + * Fees are distributed to service provider and delegators by {GraphPayments} + * + * Requirements: + * - Indexer must have enough available tokens to lock as economic security for fees + * - Allocation must be open + * + * Emits a {StakeClaimsReleased} event, and a {StakeClaimReleased} event for each claim released. + * Emits a {StakeClaimLocked} event. + * Emits a {IndexingFeesCollectedV1} event. + * + * @param _agreementId The id of the indexing agreement + * @param _data The indexing agreement collection data + * @return The amount of fees collected + */ + function _collectIndexingFees(bytes16 _agreementId, bytes memory _data) private returns (uint256) { + (address indexer, uint256 tokensCollected) = IndexingAgreement._getManager().collect( + _allocations, + _agreementId, + _data + ); + + _releaseAndLockStake(indexer, tokensCollected); + + return tokensCollected; + } + + function _releaseAndLockStake(address _indexer, uint256 _tokensCollected) private { + _releaseStake(_indexer, 0); + if (_tokensCollected > 0) { + // lock stake as economic security for fees + _lockStake( + _indexer, + _tokensCollected * stakeToFeesRatio, + block.timestamp + _disputeManager().getDisputePeriod() + ); + } + } + + function modifiersHack( + address caller, + address indexer + ) + external + view + whenNotPaused + onlyAuthorizedForProvisionHack(caller, indexer) + onlyValidProvision(indexer) + onlyRegisteredIndexer(indexer) + {} + + /** + * @notice Delegates the call to the SubgraphServiceExtension implementation. + * @dev This function does not return to its internal call site, it will return directly to the + * external caller. + */ + // solhint-disable-next-line payable-fallback, no-complex-fallback + fallback() external { + address extImpl = _subgraphServiceExtensionImpl(); + require(extImpl != address(0), "only through proxy"); + + // solhint-disable-next-line no-inline-assembly + assembly { + // copy function selector and any arguments + calldatacopy(0, 0, calldatasize()) + // execute function call using the extension implementation + let result := delegatecall(gas(), extImpl, 0, calldatasize(), 0, 0) + // get any return value + returndatacopy(0, 0, returndatasize()) + // return any return value or error back to the caller + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } } diff --git a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol new file mode 100644 index 000000000..c4e81d6ae --- /dev/null +++ b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; + +import { IndexingAgreement } from "./libraries/IndexingAgreement.sol"; +import { SubgraphService } from "./SubgraphService.sol"; + +contract SubgraphServiceExtension is PausableUpgradeable { + using IndexingAgreement for IndexingAgreement.Manager; + + modifier modifiersHack(address indexer) { + SubgraphService(address(this)).modifiersHack(msg.sender, indexer); + _; + } + + function upgradeIndexingAgreement( + address indexer, + IRecurringCollector.SignedRCAU calldata signedRCAU + ) external modifiersHack(indexer) { + IndexingAgreement._getManager().upgrade(indexer, signedRCAU); + } + + /** + * @notice Cancel an indexing agreement by indexer / operator. + * See {ISubgraphService.cancelIndexingAgreement}. + * + * @dev Can only be canceled on behalf of a valid indexer. + * + * Requirements: + * - The indexer must be registered + * - The caller must be authorized by the indexer + * - The provision must be valid according to the subgraph service rules + * - The agreement must be active + * + * Emits {IndexingAgreementCanceled} event + * + * @param agreementId The id of the agreement + */ + function cancelIndexingAgreement(address indexer, bytes16 agreementId) external modifiersHack(indexer) { + IndexingAgreement._getManager().cancel(indexer, agreementId); + } + + /** + * @notice Cancel an indexing agreement by payer / signer. + * See {ISubgraphService.cancelIndexingAgreementByPayer}. + * + * Requirements: + * - The caller must be authorized by the payer + * - The agreement must be active + * + * Emits {IndexingAgreementCanceled} event + * + * @param agreementId The id of the agreement + */ + function cancelIndexingAgreementByPayer(bytes16 agreementId) external whenNotPaused { + IndexingAgreement._getManager().cancelByPayer(agreementId); + } + + function getIndexingAgreement( + bytes16 agreementId + ) external view returns (IndexingAgreement.AgreementWrapper memory) { + return IndexingAgreement._getManager().get(agreementId); + } + + function _cancelAllocationIndexingAgreement(address _allocationId) internal { + IndexingAgreement._getManager().cancelForAllocation(_allocationId); + } +} diff --git a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol index 217b1c154..8f271bf94 100644 --- a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol +++ b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; import { Attestation } from "../libraries/Attestation.sol"; +import { IndexingAgreement } from "../libraries/IndexingAgreement.sol"; /** * @title IDisputeManager @@ -16,7 +17,8 @@ interface IDisputeManager { Null, IndexingDispute, QueryDispute, - LegacyDispute + LegacyDispute, + IndexingFeeDispute } /// @notice Status of a dispute @@ -113,6 +115,31 @@ interface IDisputeManager { uint256 cancellableAt ); + /** + * @dev Emitted when an indexing fee dispute is created for `agreementId` and `indexer` + * by `fisherman`. + * The event emits the amount of `tokens` deposited by the fisherman. + * @param disputeId The dispute id + * @param indexer The indexer address + * @param fisherman The fisherman address + * @param tokens The amount of tokens deposited by the fisherman + * @param agreementId The agreement id + * @param poi The POI disputed + * @param entities The entities disputed + * @param stakeSnapshot The stake snapshot of the indexer at the time of the dispute + */ + event IndexingFeeDisputeCreated( + bytes32 indexed disputeId, + address indexed indexer, + address indexed fisherman, + uint256 tokens, + address payer, + bytes16 agreementId, + bytes32 poi, + uint256 entities, + uint256 stakeSnapshot + ); + /** * @dev Emitted when an indexing dispute is created for `allocationId` and `indexer` * by `fisherman`. @@ -352,6 +379,18 @@ interface IDisputeManager { */ error DisputeManagerSubgraphServiceNotSet(); + /** + * @notice Thrown when the Indexing Agreement is not disputable + * @param agreementId The indexing agreement id + */ + error DisputeManagerIndexingAgreementNotDisputable(bytes16 agreementId); + + /** + * @notice Thrown when the Indexing Agreement is not disputable + * @param version The indexing agreement version + */ + error DisputeManagerIndexingAgreementInvalidVersion(IndexingAgreement.IndexingAgreementVersion version); + /** * @notice Initialize this contract. * @param owner The owner of the contract @@ -498,6 +537,23 @@ interface IDisputeManager { uint256 tokensRewards ) external returns (bytes32); + /** + * @notice Create an indexing fee (version 1) dispute for the arbitrator to resolve. + * The disputes are created in reference to a version 1 indexing agreement and specifically + * a POI and entities provided when collecting that agreement. + * This function is called by a fisherman and it will pull `disputeDeposit` GRT tokens. + * + * Requirements: + * - fisherman must have previously approved this contract to pull `disputeDeposit` amount + * of tokens from their balance. + * + * @param agreementId The indexing agreement to dispute + * @param poi The Proof of Indexing (POI) being disputed + * @param entities The number of entities disputed + * @return The dispute id + */ + function createIndexingFeeDisputeV1(bytes16 agreementId, bytes32 poi, uint256 entities) external returns (bytes32); + // -- Arbitrator -- /** diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol index 5c35296f2..2f63d7240 100644 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol @@ -7,6 +7,8 @@ import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGra import { Allocation } from "../libraries/Allocation.sol"; import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; + /** * @title Interface for the {SubgraphService} contract * @dev This interface extends {IDataServiceFees} and {IDataService}. @@ -108,7 +110,7 @@ interface ISubgraphService is IDataServiceFees { error SubgraphServiceInconsistentCollection(uint256 balanceBefore, uint256 balanceAfter); /** - * @notice @notice Thrown when the service provider in the RAV does not match the expected indexer. + * @notice @notice Thrown when the service provider does not match the expected indexer. * @param providedIndexer The address of the provided indexer. * @param expectedIndexer The address of the expected indexer. */ @@ -305,4 +307,9 @@ interface ISubgraphService is IDataServiceFees { * @return The address of the curation contract */ function getCuration() external view returns (address); + + /** + * @notice Accept an indexing agreement. + */ + function acceptIndexingAgreement(address allocationId, IRecurringCollector.SignedRCA calldata signedRCA) external; } diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol new file mode 100644 index 000000000..40ec722a0 --- /dev/null +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; + +import { IndexingAgreement } from "../libraries/IndexingAgreement.sol"; + +interface ISubgraphServiceExtension { + /** + * @notice Accept an indexing agreement. + */ + // function acceptIndexingAgreement(address allocationId, IRecurringCollector.SignedRCA calldata signedRCA) external; + + /** + * @notice Upgrade an indexing agreement. + */ + function upgradeIndexingAgreement(address indexer, IRecurringCollector.SignedRCAU calldata signedRCAU) external; + + /** + * @notice Cancel an indexing agreement by indexer / operator. + */ + function cancelIndexingAgreement(address indexer, bytes16 agreementId) external; + + /** + * @notice Cancel an indexing agreement by payer / signer. + */ + function cancelIndexingAgreementByPayer(bytes16 agreementId) external; + + function getIndexingAgreement( + bytes16 agreementId + ) external view returns (IndexingAgreement.AgreementWrapper memory); +} diff --git a/packages/subgraph-service/contracts/libraries/Decoder.sol b/packages/subgraph-service/contracts/libraries/Decoder.sol new file mode 100644 index 000000000..3d65fd463 --- /dev/null +++ b/packages/subgraph-service/contracts/libraries/Decoder.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { UnsafeDecoder } from "./UnsafeDecoder.sol"; +import { IndexingAgreement } from "./IndexingAgreement.sol"; + +library Decoder { + /** + * @notice Thrown when the data can't be decoded as expected + * @param t The type of data that was expected + * @param data The invalid data + */ + error DecoderInvalidData(string t, bytes data); + + /** + * @notice Decodes the data for collecting indexing fees. + * + * @param data The data to decode. + * @return The decoded data + */ + function decodeCollectIndexingFeeData(bytes memory data) public pure returns (bytes16, bytes memory) { + try UnsafeDecoder.decodeCollectIndexingFeeData_(data) returns (bytes16 agreementId, bytes memory nestedData) { + return (agreementId, nestedData); + } catch { + revert DecoderInvalidData("decodeCollectIndexingFeeData", data); + } + } + + /** + * @notice Decodes the RCA metadata. + * + * @param data The data to decode. See {IndexingAgreement.AcceptIndexingAgreementMetadata} + * @return The decoded data + */ + function decodeRCAMetadata( + bytes memory data + ) public pure returns (IndexingAgreement.AcceptIndexingAgreementMetadata memory) { + try UnsafeDecoder.decodeRCAMetadata_(data) returns ( + IndexingAgreement.AcceptIndexingAgreementMetadata memory metadata + ) { + return metadata; + } catch { + revert DecoderInvalidData("decodeRCAMetadata", data); + } + } + + /** + * @notice Decodes the RCAU metadata. + * + * @param data The data to decode. See {IndexingAgreement.UpgradeIndexingAgreementMetadata} + * @return The decoded data + */ + function decodeRCAUMetadata( + bytes memory data + ) public pure returns (IndexingAgreement.UpgradeIndexingAgreementMetadata memory) { + try UnsafeDecoder.decodeRCAUMetadata_(data) returns ( + IndexingAgreement.UpgradeIndexingAgreementMetadata memory metadata + ) { + return metadata; + } catch { + revert DecoderInvalidData("decodeRCAUMetadata", data); + } + } + + /** + * @notice Decodes the collect data for indexing fees V1. + * + * @param data The data to decode. + */ + function decodeCollectIndexingFeeDataV1(bytes memory data) public pure returns (uint256, bytes32, uint256) { + try UnsafeDecoder.decodeCollectIndexingFeeDataV1_(data) returns (uint256 entities, bytes32 poi, uint256 epoch) { + return (entities, poi, epoch); + } catch { + revert DecoderInvalidData("decodeCollectIndexingFeeDataV1", data); + } + } + + /** + * @notice Decodes the data for indexing agreement terms V1. + * + * @param data The data to decode. See {IndexingAgreement.IndexingAgreementTermsV1} + * @return The decoded data + */ + function decodeIndexingAgreementTermsV1( + bytes memory data + ) public pure returns (IndexingAgreement.IndexingAgreementTermsV1 memory) { + try UnsafeDecoder.decodeIndexingAgreementTermsV1_(data) returns ( + IndexingAgreement.IndexingAgreementTermsV1 memory terms + ) { + return terms; + } catch { + revert DecoderInvalidData("decodeCollectIndexingFeeData", data); + } + } +} diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol new file mode 100644 index 000000000..842890fb8 --- /dev/null +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; +import { GraphDirectory } from "@graphprotocol/horizon/contracts/utilities/GraphDirectory.sol"; + +import { SubgraphService } from "../SubgraphService.sol"; +import { Directory } from "../utilities/Directory.sol"; +import { Allocation } from "./Allocation.sol"; +import { SubgraphServiceLib } from "./SubgraphServiceLib.sol"; +import { Decoder } from "./Decoder.sol"; + +library IndexingAgreement { + using IndexingAgreement for Manager; + using Allocation for mapping(address => Allocation.State); + using SubgraphServiceLib for mapping(address => Allocation.State); + + /// @notice Versions of Indexing Agreement Metadata + enum IndexingAgreementVersion { + V1 + } + + struct Manager { + mapping(bytes16 => State) agreements; + mapping(bytes16 agreementId => IndexingAgreementTermsV1 data) termsV1; + mapping(address allocationId => bytes16 agreementId) allocationToActiveAgreementId; + } + + /** + * @notice Indexer Agreement Data + * @param allocationId The allocation ID + * @param version The indexing agreement version + */ + struct State { + address allocationId; + IndexingAgreementVersion version; + } + + struct AgreementWrapper { + State agreement; + IRecurringCollector.AgreementData collectorAgreement; + } + + /** + * @notice Accept Indexing Agreement metadata + * @param subgraphDeploymentId The subgraph deployment ID + * @param version The indexing agreement version + * @param terms The indexing agreement terms + */ + struct AcceptIndexingAgreementMetadata { + bytes32 subgraphDeploymentId; + IndexingAgreementVersion version; + bytes terms; + } + + /** + * @notice Upgrade Indexing Agreement metadata + * @param version The indexing agreement version + * @param terms The indexing agreement terms + */ + struct UpgradeIndexingAgreementMetadata { + IndexingAgreementVersion version; + bytes terms; + } + + /** + * @notice Indexing Agreement Terms (Version 1) + * @param tokensPerSecond The amount of tokens per second + * @param tokensPerEntityPerSecond The amount of tokens per entity per second + */ + struct IndexingAgreementTermsV1 { + uint256 tokensPerSecond; + uint256 tokensPerEntityPerSecond; + } + + bytes32 private constant INDEXING_AGREEMENT_MANAGER_STORAGE_V1_SLOT = keccak256("v1.manager.indexing-agreement"); + + /** + * @notice Emitted when an indexer collects indexing fees from a V1 agreement + * @param indexer The address of the indexer + * @param payer The address paying for the indexing fees + * @param agreementId The id of the agreement + * @param currentEpoch The current epoch + * @param tokensCollected The amount of tokens collected + * @param entities The number of entities indexed + * @param poi The proof of indexing + * @param poiEpoch The epoch of the proof of indexing + */ + event IndexingFeesCollectedV1( + address indexed indexer, + address indexed payer, + bytes16 indexed agreementId, + address allocationId, + bytes32 subgraphDeploymentId, + uint256 currentEpoch, + uint256 tokensCollected, + uint256 entities, + bytes32 poi, + uint256 poiEpoch + ); + + /** + * @notice Emitted when an indexing agreement is canceled + * @param indexer The address of the indexer + * @param payer The address of the payer + * @param agreementId The id of the agreement + * @param canceledOnBehalfOf The address of the entity that canceled the agreement + */ + event IndexingAgreementCanceled( + address indexed indexer, + address indexed payer, + bytes16 indexed agreementId, + address canceledOnBehalfOf + ); + + /** + * @notice Emitted when an indexing agreement is accepted + * @param indexer The address of the indexer + * @param payer The address of the payer + * @param agreementId The id of the agreement + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param version The version of the indexing agreement + * @param versionTerms The version data of the indexing agreement + */ + event IndexingAgreementAccepted( + address indexed indexer, + address indexed payer, + bytes16 indexed agreementId, + address allocationId, + bytes32 subgraphDeploymentId, + IndexingAgreementVersion version, + bytes versionTerms + ); + + /** + * @notice Thrown when trying to interact with an agreement with an invalid version + * @param version The invalid version + */ + error InvalidIndexingAgreementVersion(IndexingAgreementVersion version); + + /** + * @notice Thrown when an agreement is not for the subgraph data service + * @param wrongDataService The wrong data service + */ + error IndexingAgreementWrongDataService(address wrongDataService); + + /** + * @notice Thrown when an agreement and the allocation correspond to different deployment IDs + * @param agreementDeploymentId The agreement's deployment ID + * @param allocationId The allocation ID + * @param allocationDeploymentId The allocation's deployment ID + */ + error IndexingAgreementDeploymentIdMismatch( + bytes32 agreementDeploymentId, + address allocationId, + bytes32 allocationDeploymentId + ); + + /** + * @notice Thrown when the agreement is already accepted + * @param agreementId The agreement ID + */ + error IndexingAgreementAlreadyAccepted(bytes16 agreementId); + + /** + * @notice Thrown when an allocation already has an active agreement + * @param allocationId The allocation ID + */ + error AllocationAlreadyHasIndexingAgreement(address allocationId); + + /** + * @notice Thrown when caller or proxy can not cancel an agreement + * @param owner The address of the owner of the agreement + * @param unauthorized The unauthorized caller + */ + error IndexingAgreementNonCancelableBy(address owner, address unauthorized); + + /** + * @notice Thrown when the agreement is not active + * @param agreementId The agreement ID + */ + error IndexingAgreementNotActive(bytes16 agreementId); + + /** + * @notice Thrown when trying to interact with an agreement not owned by the indexer + * @param agreementId The agreement ID + * @param unauthorizedIndexer The unauthorized indexer + */ + error IndexingAgreementNotAuthorized(bytes16 agreementId, address unauthorizedIndexer); + + function accept( + Manager storage self, + mapping(address allocationId => Allocation.State allocation) storage allocations, + address allocationId, + IRecurringCollector.SignedRCA calldata signedRCA + ) external { + Allocation.State memory allocation = allocations.requireValidAllocation( + allocationId, + signedRCA.rca.serviceProvider + ); + + require( + signedRCA.rca.dataService == address(this), + IndexingAgreementWrongDataService(signedRCA.rca.dataService) + ); + + AcceptIndexingAgreementMetadata memory metadata = Decoder.decodeRCAMetadata(signedRCA.rca.metadata); + + State storage agreement = self.agreements[signedRCA.rca.agreementId]; + + require(agreement.allocationId == address(0), IndexingAgreementAlreadyAccepted(signedRCA.rca.agreementId)); + + require( + allocation.subgraphDeploymentId == metadata.subgraphDeploymentId, + IndexingAgreementDeploymentIdMismatch( + metadata.subgraphDeploymentId, + allocationId, + allocation.subgraphDeploymentId + ) + ); + + require( + self.allocationToActiveAgreementId[allocationId] == bytes16(0), + AllocationAlreadyHasIndexingAgreement(allocationId) + ); + self.allocationToActiveAgreementId[allocationId] = signedRCA.rca.agreementId; + + agreement.version = metadata.version; + agreement.allocationId = allocationId; + + require(metadata.version == IndexingAgreementVersion.V1, InvalidIndexingAgreementVersion(metadata.version)); + _setTermsV1(self, signedRCA.rca.agreementId, metadata.terms); + + emit IndexingAgreementAccepted( + signedRCA.rca.serviceProvider, + signedRCA.rca.payer, + signedRCA.rca.agreementId, + allocationId, + metadata.subgraphDeploymentId, + metadata.version, + metadata.terms + ); + + _directory().recurringCollector().accept(signedRCA); + } + + function upgrade( + Manager storage self, + address indexer, + IRecurringCollector.SignedRCAU calldata signedRCAU + ) external { + AgreementWrapper memory wrapper = _get(self, signedRCAU.rcau.agreementId); + require(_isActive(wrapper), IndexingAgreementNotActive(signedRCAU.rcau.agreementId)); + require( + wrapper.collectorAgreement.serviceProvider == indexer, + IndexingAgreementNotAuthorized(signedRCAU.rcau.agreementId, indexer) + ); + + UpgradeIndexingAgreementMetadata memory metadata = Decoder.decodeRCAUMetadata(signedRCAU.rcau.metadata); + + wrapper.agreement.version = metadata.version; + + require(metadata.version == IndexingAgreementVersion.V1, InvalidIndexingAgreementVersion(metadata.version)); + _setTermsV1(self, signedRCAU.rcau.agreementId, metadata.terms); + + _directory().recurringCollector().upgrade(signedRCAU); + } + + function cancel(Manager storage self, address indexer, bytes16 agreementId) external { + AgreementWrapper memory wrapper = _get(self, agreementId); + require(_isActive(wrapper), IndexingAgreementNotActive(agreementId)); + require( + wrapper.collectorAgreement.serviceProvider == indexer, + IndexingAgreementNonCancelableBy(wrapper.collectorAgreement.serviceProvider, indexer) + ); + _cancel( + self, + agreementId, + wrapper.agreement, + wrapper.collectorAgreement, + IRecurringCollector.CancelAgreementBy.ServiceProvider + ); + } + + function cancelForAllocation(Manager storage self, address _allocationId) external { + bytes16 agreementId = self.allocationToActiveAgreementId[_allocationId]; + if (agreementId == bytes16(0)) { + return; + } + + AgreementWrapper memory wrapper = _get(self, agreementId); + if (!_isActive(wrapper)) { + return; + } + + _cancel( + self, + agreementId, + wrapper.agreement, + wrapper.collectorAgreement, + IRecurringCollector.CancelAgreementBy.ServiceProvider + ); + } + + function cancelByPayer(Manager storage self, bytes16 agreementId) external { + AgreementWrapper memory wrapper = _get(self, agreementId); + require(_isActive(wrapper), IndexingAgreementNotActive(agreementId)); + require( + _directory().recurringCollector().isAuthorized(wrapper.collectorAgreement.payer, msg.sender), + IndexingAgreementNonCancelableBy(wrapper.collectorAgreement.payer, msg.sender) + ); + _cancel( + self, + agreementId, + wrapper.agreement, + wrapper.collectorAgreement, + IRecurringCollector.CancelAgreementBy.Payer + ); + } + + function collect( + Manager storage self, + mapping(address allocationId => Allocation.State allocation) storage allocations, + bytes16 agreementId, + bytes memory data + ) external returns (address, uint256) { + AgreementWrapper memory wrapper = _get(self, agreementId); + Allocation.State memory allocation = allocations.requireValidAllocation( + wrapper.agreement.allocationId, + wrapper.collectorAgreement.serviceProvider + ); + require(_isActive(wrapper), IndexingAgreementNotActive(agreementId)); + + require( + wrapper.agreement.version == IndexingAgreementVersion.V1, + InvalidIndexingAgreementVersion(wrapper.agreement.version) + ); + + (uint256 entities, bytes32 poi, uint256 poiEpoch) = Decoder.decodeCollectIndexingFeeDataV1(data); + + uint256 expectedTokens = (entities == 0 && poi == bytes32(0)) + ? 0 + : _tokensToCollect(self, agreementId, wrapper.collectorAgreement, entities); + + uint256 tokensCollected = _directory().recurringCollector().collect( + IGraphPayments.PaymentTypes.IndexingFee, + abi.encode( + IRecurringCollector.CollectParams({ + agreementId: agreementId, + collectionId: bytes32(uint256(uint160(wrapper.agreement.allocationId))), + tokens: expectedTokens, + dataServiceCut: 0 + }) + ) + ); + + emit IndexingFeesCollectedV1( + wrapper.collectorAgreement.serviceProvider, + wrapper.collectorAgreement.payer, + agreementId, + wrapper.agreement.allocationId, + allocation.subgraphDeploymentId, + _graphDirectory().graphEpochManager().currentEpoch(), + tokensCollected, + entities, + poi, + poiEpoch + ); + + return (wrapper.collectorAgreement.serviceProvider, tokensCollected); + } + + function get(Manager storage self, bytes16 agreementId) external view returns (AgreementWrapper memory) { + AgreementWrapper memory wrapper = _get(self, agreementId); + require(wrapper.collectorAgreement.dataService == address(this), IndexingAgreementNotActive(agreementId)); + + return wrapper; + } + + function _getManager() internal pure returns (Manager storage manager) { + bytes32 slot = INDEXING_AGREEMENT_MANAGER_STORAGE_V1_SLOT; + + // solhint-disable-next-line no-inline-assembly + assembly { + manager.slot := slot + } + } + + function _setTermsV1(Manager storage _manager, bytes16 _agreementId, bytes memory _data) private { + IndexingAgreementTermsV1 memory newTerms = Decoder.decodeIndexingAgreementTermsV1(_data); + _manager.termsV1[_agreementId].tokensPerSecond = newTerms.tokensPerSecond; + _manager.termsV1[_agreementId].tokensPerEntityPerSecond = newTerms.tokensPerEntityPerSecond; + } + + function _cancel( + Manager storage _manager, + bytes16 _agreementId, + State memory _agreement, + IRecurringCollector.AgreementData memory _collectorAgreement, + IRecurringCollector.CancelAgreementBy _cancelBy + ) private { + delete _manager.allocationToActiveAgreementId[_agreement.allocationId]; + + emit IndexingAgreementCanceled( + _collectorAgreement.serviceProvider, + _collectorAgreement.payer, + _agreementId, + _cancelBy == IRecurringCollector.CancelAgreementBy.Payer + ? _collectorAgreement.payer + : _collectorAgreement.serviceProvider + ); + + _directory().recurringCollector().cancel(_agreementId, _cancelBy); + } + + function _tokensToCollect( + Manager storage _manager, + bytes16 _agreementId, + IRecurringCollector.AgreementData memory _agreement, + uint256 _entities + ) private view returns (uint256) { + IndexingAgreementTermsV1 memory termsV1 = _manager.termsV1[_agreementId]; + + uint256 collectionSeconds = block.timestamp; + collectionSeconds -= _agreement.lastCollectionAt > 0 ? _agreement.lastCollectionAt : _agreement.acceptedAt; + + // FIX-ME: this is bad because it encourages indexers to collect at max seconds allowed to maximize collection. + return collectionSeconds * (termsV1.tokensPerSecond + termsV1.tokensPerEntityPerSecond * _entities); + } + + function _isActive(AgreementWrapper memory wrapper) private view returns (bool) { + return + wrapper.collectorAgreement.dataService == address(this) && + wrapper.collectorAgreement.state == IRecurringCollector.AgreementState.Accepted && + wrapper.agreement.allocationId != address(0); + } + + function _directory() private view returns (Directory) { + return Directory(address(this)); + } + + function _graphDirectory() private view returns (GraphDirectory) { + return GraphDirectory(address(this)); + } + + function _subgraphService() private view returns (SubgraphService) { + return SubgraphService(address(this)); + } + + function _get(Manager storage self, bytes16 agreementId) private view returns (AgreementWrapper memory) { + return + AgreementWrapper({ + agreement: self.agreements[agreementId], + collectorAgreement: _directory().recurringCollector().getAgreement(agreementId) + }); + } +} diff --git a/packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol b/packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol new file mode 100644 index 000000000..47f2846d2 --- /dev/null +++ b/packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { ISubgraphService } from "../interfaces/ISubgraphService.sol"; +import { AllocationManager } from "../utilities/AllocationManager.sol"; +import { Allocation } from "./Allocation.sol"; + +library SubgraphServiceLib { + using Allocation for mapping(address => Allocation.State); + using Allocation for Allocation.State; + + function requireValidAllocation( + mapping(address => Allocation.State) storage self, + address allocationId, + address indexer + ) external view returns (Allocation.State memory) { + Allocation.State memory allocation = self.get(allocationId); + require( + allocation.indexer == indexer, + ISubgraphService.SubgraphServiceAllocationNotAuthorized(indexer, allocationId) + ); + require(allocation.isOpen(), AllocationManager.AllocationManagerAllocationClosed(allocationId)); + + return allocation; + } +} diff --git a/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol b/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol new file mode 100644 index 000000000..e32f7c657 --- /dev/null +++ b/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IndexingAgreement } from "./IndexingAgreement.sol"; + +library UnsafeDecoder { + /** + * @notice See {Decoder.decodeCollectIndexingFeeData} + */ + function decodeCollectIndexingFeeData_(bytes calldata data) public pure returns (bytes16, bytes memory) { + return abi.decode(data, (bytes16, bytes)); + } + + /** + * @notice See {Decoder.decodeRCAMetadata} + */ + function decodeRCAMetadata_( + bytes calldata data + ) public pure returns (IndexingAgreement.AcceptIndexingAgreementMetadata memory) { + return abi.decode(data, (IndexingAgreement.AcceptIndexingAgreementMetadata)); + } + + /** + * @notice See {Decoder.decodeRCAUMetadata} + */ + function decodeRCAUMetadata_( + bytes calldata data + ) public pure returns (IndexingAgreement.UpgradeIndexingAgreementMetadata memory) { + return abi.decode(data, (IndexingAgreement.UpgradeIndexingAgreementMetadata)); + } + + /** + * @notice See {Decoder.decodeCollectIndexingFeeDataV1} + */ + function decodeCollectIndexingFeeDataV1_( + bytes memory data + ) public pure returns (uint256 entities, bytes32 poi, uint256 epoch) { + return abi.decode(data, (uint256, bytes32, uint256)); + } + + /** + * @notice See {Decoder.decodeIndexingAgreementTermsV1} + */ + function decodeIndexingAgreementTermsV1_( + bytes memory data + ) public pure returns (IndexingAgreement.IndexingAgreementTermsV1 memory) { + return abi.decode(data, (IndexingAgreement.IndexingAgreementTermsV1)); + } +} diff --git a/packages/subgraph-service/contracts/utilities/Directory.sol b/packages/subgraph-service/contracts/utilities/Directory.sol index d068c74b3..2e6cbf2dd 100644 --- a/packages/subgraph-service/contracts/utilities/Directory.sol +++ b/packages/subgraph-service/contracts/utilities/Directory.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.27; import { IDisputeManager } from "../interfaces/IDisputeManager.sol"; import { ISubgraphService } from "../interfaces/ISubgraphService.sol"; import { IGraphTallyCollector } from "@graphprotocol/horizon/contracts/interfaces/IGraphTallyCollector.sol"; +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; import { ICuration } from "@graphprotocol/contracts/contracts/curation/ICuration.sol"; /** @@ -25,6 +26,12 @@ abstract contract Directory { /// @dev Required to collect payments via Graph Horizon payments protocol IGraphTallyCollector private immutable GRAPH_TALLY_COLLECTOR; + /// @notice The Recurring Collector contract address + /// @dev Required to collect indexing agreement payments via Graph Horizon payments protocol + IRecurringCollector private immutable RECURRING_COLLECTOR; + + address private immutable SUBGRAPH_SERVICE_EXTENSION_IMPL; + /// @notice The Curation contract address /// @dev Required for curation fees distribution ICuration private immutable CURATION; @@ -40,7 +47,9 @@ abstract contract Directory { address subgraphService, address disputeManager, address graphTallyCollector, - address curation + address curation, + address recurringCollector, + address subgraphServiceExtensionImpl ); /** @@ -67,14 +76,50 @@ abstract contract Directory { * @param disputeManager The Dispute Manager contract address * @param graphTallyCollector The Graph Tally Collector contract address * @param curation The Curation contract address + * @param recurringCollector_ The Recurring Collector contract address + * @param subgraphServiceExtensionImpl_ The Subgraph Service Extension contract address */ - constructor(address subgraphService, address disputeManager, address graphTallyCollector, address curation) { + constructor( + address subgraphService, + address disputeManager, + address graphTallyCollector, + address curation, + address recurringCollector_, + address subgraphServiceExtensionImpl_ + ) { SUBGRAPH_SERVICE = ISubgraphService(subgraphService); DISPUTE_MANAGER = IDisputeManager(disputeManager); GRAPH_TALLY_COLLECTOR = IGraphTallyCollector(graphTallyCollector); CURATION = ICuration(curation); + RECURRING_COLLECTOR = IRecurringCollector(recurringCollector_); + SUBGRAPH_SERVICE_EXTENSION_IMPL = subgraphServiceExtensionImpl_; + + emit SubgraphServiceDirectoryInitialized( + subgraphService, + disputeManager, + graphTallyCollector, + curation, + recurringCollector_, + subgraphServiceExtensionImpl_ + ); + } + + /** + * @notice Returns the Recurring Collector contract address + */ + function recurringCollector() external view returns (IRecurringCollector) { + return RECURRING_COLLECTOR; + } + + /** + * @notice Returns the Subgraph Service Extension implementation contract address + */ + function _subgraphServiceExtensionImpl() internal view returns (address) { + return SUBGRAPH_SERVICE_EXTENSION_IMPL; + } - emit SubgraphServiceDirectoryInitialized(subgraphService, disputeManager, graphTallyCollector, curation); + function _directory() internal view returns (Directory) { + return Directory(address(this)); } /** diff --git a/packages/subgraph-service/package.json b/packages/subgraph-service/package.json index c9b23e0f5..0b000778c 100644 --- a/packages/subgraph-service/package.json +++ b/packages/subgraph-service/package.json @@ -17,9 +17,10 @@ "scripts": { "lint": "pnpm lint:ts && pnpm lint:sol", "lint:ts": "eslint '**/*.{js,ts}' --fix --no-warn-ignored", - "lint:sol": "pnpm lint:sol:prettier && pnpm lint:sol:solhint", + "lint:sol": "pnpm lint:sol:prettier && pnpm lint:sol:solhint && pnpm lint:sol:solhint:test", "lint:sol:prettier": "prettier --write \"contracts/**/*.sol\" \"test/**/*.sol\"", "lint:sol:solhint": "solhint --noPrompt --fix \"contracts/**/*.sol\" --config node_modules/solhint-graph-config/index.js", + "lint:sol:solhint:test": "solhint --noPrompt --fix \"test/unit/subgraphService/indexing-agreement/*\" --config node_modules/solhint-graph-config/index.js", "lint:sol:natspec": "natspec-smells --config natspec-smells.config.js", "clean": "rm -rf build dist cache cache_forge typechain-types", "build": "hardhat compile", diff --git a/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol b/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol index 0f59013be..146790b22 100644 --- a/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol +++ b/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol @@ -14,12 +14,14 @@ import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHo import { IPaymentsEscrow } from "@graphprotocol/horizon/contracts/interfaces/IPaymentsEscrow.sol"; import { IGraphTallyCollector } from "@graphprotocol/horizon/contracts/interfaces/IGraphTallyCollector.sol"; import { GraphTallyCollector } from "@graphprotocol/horizon/contracts/payments/collectors/GraphTallyCollector.sol"; +import { RecurringCollector } from "@graphprotocol/horizon/contracts/payments/collectors/RecurringCollector.sol"; import { PaymentsEscrow } from "@graphprotocol/horizon/contracts/payments/PaymentsEscrow.sol"; import { UnsafeUpgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; import { Constants } from "./utils/Constants.sol"; import { DisputeManager } from "../../contracts/DisputeManager.sol"; import { SubgraphService } from "../../contracts/SubgraphService.sol"; +import { SubgraphServiceExtension } from "../../contracts/SubgraphServiceExtension.sol"; import { Users } from "./utils/Users.sol"; import { Utils } from "./utils/Utils.sol"; @@ -43,6 +45,7 @@ abstract contract SubgraphBaseTest is Utils, Constants { GraphPayments graphPayments; IPaymentsEscrow escrow; GraphTallyCollector graphTallyCollector; + RecurringCollector recurringCollector; HorizonStaking private stakingBase; HorizonStakingExtension private stakingExtension; @@ -156,12 +159,21 @@ abstract contract SubgraphBaseTest is Utils, Constants { address(controller), revokeSignerThawingPeriod ); + recurringCollector = new RecurringCollector( + "RecurringCollector", + "1", + address(controller), + revokeSignerThawingPeriod + ); + address subgraphServiceImplementation = address( new SubgraphService( address(controller), address(disputeManager), address(graphTallyCollector), - address(curation) + address(curation), + address(recurringCollector), + address(new SubgraphServiceExtension()) ) ); address subgraphServiceProxy = UnsafeUpgrades.deployTransparentProxy( diff --git a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol index 29b9ebba4..bd9ccc8d3 100644 --- a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol @@ -205,7 +205,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { uint256 paymentCollected = 0; address allocationId; IndexingRewardsData memory indexingRewardsData; - CollectPaymentData memory collectPaymentDataBefore = _collectPaymentDataBefore(_indexer); + CollectPaymentData memory collectPaymentDataBefore = _collectPaymentData(_indexer); if (_paymentType == IGraphPayments.PaymentTypes.QueryFee) { paymentCollected = _handleQueryFeeCollection(_indexer, _data); @@ -219,7 +219,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { // collect rewards subgraphService.collect(_indexer, _paymentType, _data); - CollectPaymentData memory collectPaymentDataAfter = _collectPaymentDataAfter(_indexer); + CollectPaymentData memory collectPaymentDataAfter = _collectPaymentData(_indexer); if (_paymentType == IGraphPayments.PaymentTypes.QueryFee) { _verifyQueryFeeCollection( @@ -240,42 +240,24 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { } } - function _collectPaymentDataBefore(address _indexer) private view returns (CollectPaymentData memory) { + function _collectPaymentData( + address _indexer + ) internal view returns (CollectPaymentData memory collectPaymentData) { address paymentsDestination = subgraphService.paymentsDestination(_indexer); - CollectPaymentData memory collectPaymentDataBefore; - collectPaymentDataBefore.rewardsDestinationBalance = token.balanceOf(paymentsDestination); - collectPaymentDataBefore.indexerProvisionBalance = staking.getProviderTokensAvailable( + collectPaymentData.rewardsDestinationBalance = token.balanceOf(paymentsDestination); + collectPaymentData.indexerProvisionBalance = staking.getProviderTokensAvailable( _indexer, address(subgraphService) ); - collectPaymentDataBefore.delegationPoolBalance = staking.getDelegatedTokensAvailable( + collectPaymentData.delegationPoolBalance = staking.getDelegatedTokensAvailable( _indexer, address(subgraphService) ); - collectPaymentDataBefore.indexerBalance = token.balanceOf(_indexer); - collectPaymentDataBefore.curationBalance = token.balanceOf(address(curation)); - collectPaymentDataBefore.lockedTokens = subgraphService.feesProvisionTracker(_indexer); - collectPaymentDataBefore.indexerStake = staking.getStake(_indexer); - return collectPaymentDataBefore; - } - - function _collectPaymentDataAfter(address _indexer) private view returns (CollectPaymentData memory) { - CollectPaymentData memory collectPaymentDataAfter; - address paymentsDestination = subgraphService.paymentsDestination(_indexer); - collectPaymentDataAfter.rewardsDestinationBalance = token.balanceOf(paymentsDestination); - collectPaymentDataAfter.indexerProvisionBalance = staking.getProviderTokensAvailable( - _indexer, - address(subgraphService) - ); - collectPaymentDataAfter.delegationPoolBalance = staking.getDelegatedTokensAvailable( - _indexer, - address(subgraphService) - ); - collectPaymentDataAfter.indexerBalance = token.balanceOf(_indexer); - collectPaymentDataAfter.curationBalance = token.balanceOf(address(curation)); - collectPaymentDataAfter.lockedTokens = subgraphService.feesProvisionTracker(_indexer); - collectPaymentDataAfter.indexerStake = staking.getStake(_indexer); - return collectPaymentDataAfter; + collectPaymentData.indexerBalance = token.balanceOf(_indexer); + collectPaymentData.curationBalance = token.balanceOf(address(curation)); + collectPaymentData.lockedTokens = subgraphService.feesProvisionTracker(_indexer); + collectPaymentData.indexerStake = staking.getStake(_indexer); + return collectPaymentData; } function _handleQueryFeeCollection( diff --git a/packages/subgraph-service/test/unit/subgraphService/collect/collect.t.sol b/packages/subgraph-service/test/unit/subgraphService/collect/collect.t.sol deleted file mode 100644 index aff11d578..000000000 --- a/packages/subgraph-service/test/unit/subgraphService/collect/collect.t.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import "forge-std/Test.sol"; - -import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; - -import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; -import { SubgraphServiceTest } from "../SubgraphService.t.sol"; - -contract SubgraphServiceCollectTest is SubgraphServiceTest { - /* - * TESTS - */ - - function test_SubgraphService_Collect_RevertWhen_InvalidPayment( - uint256 tokens - ) public useIndexer useAllocation(tokens) { - IGraphPayments.PaymentTypes invalidPaymentType = IGraphPayments.PaymentTypes.IndexingFee; - vm.expectRevert( - abi.encodeWithSelector(ISubgraphService.SubgraphServiceInvalidPaymentType.selector, invalidPaymentType) - ); - subgraphService.collect(users.indexer, invalidPaymentType, ""); - } -} 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 new file mode 100644 index 000000000..793bb2438 --- /dev/null +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/accept.t.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; + +import { Allocation } from "../../../../contracts/libraries/Allocation.sol"; +import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; +import { Decoder } from "../../../../contracts/libraries/Decoder.sol"; +import { AllocationManager } from "../../../../contracts/utilities/AllocationManager.sol"; +import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; + +import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; + +contract SubgraphServiceIndexingAgreementAcceptTest is SubgraphServiceIndexingAgreementSharedTest { + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenPaused( + address allocationId, + address operator, + IRecurringCollector.SignedRCA calldata signedRCA + ) public withSafeIndexerOrOperator(operator) { + resetPrank(users.pauseGuardian); + subgraphService.pause(); + + resetPrank(operator); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + subgraphService.acceptIndexingAgreement(allocationId, signedRCA); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenNotAuthorized( + address allocationId, + address operator, + IRecurringCollector.SignedRCA calldata signedRCA + ) public withSafeIndexerOrOperator(operator) { + vm.assume(operator != signedRCA.rca.serviceProvider); + resetPrank(operator); + bytes memory expectedErr = abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + signedRCA.rca.serviceProvider, + operator + ); + vm.expectRevert(expectedErr); + subgraphService.acceptIndexingAgreement(allocationId, signedRCA); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenInvalidProvision( + address indexer, + uint256 unboundedTokens, + address allocationId, + IRecurringCollector.SignedRCA memory signedRCA + ) public withSafeIndexerOrOperator(indexer) { + uint256 tokens = bound(unboundedTokens, 1, minimumProvisionTokens - 1); + mint(indexer, tokens); + resetPrank(indexer); + _createProvision(indexer, tokens, fishermanRewardPercentage, disputePeriod); + + signedRCA.rca.serviceProvider = indexer; + bytes memory expectedErr = abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "tokens", + tokens, + minimumProvisionTokens, + maximumProvisionTokens + ); + vm.expectRevert(expectedErr); + subgraphService.acceptIndexingAgreement(allocationId, signedRCA); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenIndexerNotRegistered( + address indexer, + uint256 unboundedTokens, + address allocationId, + IRecurringCollector.SignedRCA memory signedRCA + ) public withSafeIndexerOrOperator(indexer) { + uint256 tokens = bound(unboundedTokens, minimumProvisionTokens, MAX_TOKENS); + mint(indexer, tokens); + resetPrank(indexer); + _createProvision(indexer, tokens, fishermanRewardPercentage, disputePeriod); + signedRCA.rca.serviceProvider = indexer; + bytes memory expectedErr = abi.encodeWithSelector( + ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, + indexer + ); + vm.expectRevert(expectedErr); + subgraphService.acceptIndexingAgreement(allocationId, signedRCA); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenNotDataService( + Seed memory seed, + address incorrectDataService + ) public { + vm.assume(incorrectDataService != address(subgraphService)); + + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory acceptable = _generateAcceptableSignedRCA(ctx, indexerState.addr); + acceptable.rca.dataService = incorrectDataService; + IRecurringCollector.SignedRCA memory unacceptable = _recurringCollectorHelper.generateSignedRCA( + acceptable.rca, + ctx.payer.signerPrivateKey + ); + + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreement.IndexingAgreementWrongDataService.selector, + unacceptable.rca.dataService + ); + vm.expectRevert(expectedErr); + vm.prank(indexerState.addr); + subgraphService.acceptIndexingAgreement(indexerState.allocationId, unacceptable); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenInvalidMetadata(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory acceptable = _generateAcceptableSignedRCA(ctx, indexerState.addr); + acceptable.rca.metadata = bytes("invalid"); + IRecurringCollector.SignedRCA memory unacceptable = _recurringCollectorHelper.generateSignedRCA( + acceptable.rca, + ctx.payer.signerPrivateKey + ); + + bytes memory expectedErr = abi.encodeWithSelector( + Decoder.DecoderInvalidData.selector, + "decodeRCAMetadata", + unacceptable.rca.metadata + ); + vm.expectRevert(expectedErr); + vm.prank(indexerState.addr); + subgraphService.acceptIndexingAgreement(indexerState.allocationId, unacceptable); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenInvalidAllocation( + Seed memory seed, + address invalidAllocationId + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory acceptable = _generateAcceptableSignedRCA(ctx, indexerState.addr); + + bytes memory expectedErr = abi.encodeWithSelector( + Allocation.AllocationDoesNotExist.selector, + invalidAllocationId + ); + vm.expectRevert(expectedErr); + vm.prank(indexerState.addr); + subgraphService.acceptIndexingAgreement(invalidAllocationId, acceptable); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenAllocationNotAuthorized(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerStateA = _withIndexer(ctx); + IndexerState memory indexerStateB = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory acceptableA = _generateAcceptableSignedRCA(ctx, indexerStateA.addr); + + bytes memory expectedErr = abi.encodeWithSelector( + ISubgraphService.SubgraphServiceAllocationNotAuthorized.selector, + indexerStateA.addr, + indexerStateB.allocationId + ); + vm.expectRevert(expectedErr); + vm.prank(indexerStateA.addr); + subgraphService.acceptIndexingAgreement(indexerStateB.allocationId, acceptableA); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenAllocationClosed(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory acceptable = _generateAcceptableSignedRCA(ctx, indexerState.addr); + + resetPrank(indexerState.addr); + subgraphService.stopService(indexerState.addr, abi.encode(indexerState.allocationId)); + + bytes memory expectedErr = abi.encodeWithSelector( + AllocationManager.AllocationManagerAllocationClosed.selector, + indexerState.allocationId + ); + vm.expectRevert(expectedErr); + subgraphService.acceptIndexingAgreement(indexerState.allocationId, acceptable); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenDeploymentIdMismatch( + Seed memory seed, + bytes32 wrongSubgraphDeploymentId + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + vm.assume(indexerState.subgraphDeploymentId != wrongSubgraphDeploymentId); + IRecurringCollector.SignedRCA memory acceptable = _generateAcceptableSignedRCA(ctx, indexerState.addr); + acceptable.rca.metadata = abi.encode(_newAcceptIndexingAgreementMetadataV1(wrongSubgraphDeploymentId)); + IRecurringCollector.SignedRCA memory unacceptable = _recurringCollectorHelper.generateSignedRCA( + acceptable.rca, + ctx.payer.signerPrivateKey + ); + + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreement.IndexingAgreementDeploymentIdMismatch.selector, + wrongSubgraphDeploymentId, + indexerState.allocationId, + indexerState.subgraphDeploymentId + ); + vm.expectRevert(expectedErr); + vm.prank(indexerState.addr); + subgraphService.acceptIndexingAgreement(indexerState.allocationId, unacceptable); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenAgreementAlreadyAccepted(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); + + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreement.IndexingAgreementAlreadyAccepted.selector, + accepted.rca.agreementId + ); + vm.expectRevert(expectedErr); + resetPrank(ctx.indexers[0].addr); + subgraphService.acceptIndexingAgreement(ctx.indexers[0].allocationId, accepted); + } + + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenAgreementAlreadyAllocated() public {} + + function test_SubgraphService_AcceptIndexingAgreement(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory acceptable = _generateAcceptableSignedRCA(ctx, indexerState.addr); + IndexingAgreement.AcceptIndexingAgreementMetadata memory metadata = abi.decode( + acceptable.rca.metadata, + (IndexingAgreement.AcceptIndexingAgreementMetadata) + ); + vm.expectEmit(address(subgraphService)); + emit IndexingAgreement.IndexingAgreementAccepted( + acceptable.rca.serviceProvider, + acceptable.rca.payer, + acceptable.rca.agreementId, + indexerState.allocationId, + metadata.subgraphDeploymentId, + metadata.version, + metadata.terms + ); + + resetPrank(indexerState.addr); + subgraphService.acceptIndexingAgreement(indexerState.allocationId, acceptable); + } + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol new file mode 100644 index 000000000..497b935b4 --- /dev/null +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; + +contract SubgraphServiceIndexingAgreementBaseTest is SubgraphServiceIndexingAgreementSharedTest { + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + function test_SubgraphService_Revert_WhenUnsafeAddress_WhenProxyAdmin(address indexer, bytes16 agreementId) public { + address operator = _transparentUpgradeableProxyAdmin(); + assertFalse(_isSafeSubgraphServiceCaller(operator)); + + vm.expectRevert(TransparentUpgradeableProxy.ProxyDeniedAdminAccess.selector); + resetPrank(address(operator)); + _getSubgraphServiceExtension().cancelIndexingAgreement(indexer, agreementId); + } + + function test_SubgraphService_Revert_WhenUnsafeAddress_WhenGraphProxyAdmin(uint256 unboundedTokens) public { + address indexer = GRAPH_PROXY_ADMIN_ADDRESS; + assertFalse(_isSafeSubgraphServiceCaller(indexer)); + + uint256 tokens = bound(unboundedTokens, minimumProvisionTokens, MAX_TOKENS); + mint(indexer, tokens); + resetPrank(indexer); + vm.expectRevert("Cannot fallback to proxy target"); + staking.provision(indexer, address(subgraphService), tokens, maxSlashingPercentage, disputePeriod); + } + + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol new file mode 100644 index 000000000..52b56532e --- /dev/null +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; + +import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; +import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; + +import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; + +contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAgreementSharedTest { + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + function test_SubgraphService_CancelIndexingAgreementByPayer_Revert_WhenPaused( + address rando, + bytes16 agreementId + ) public withSafeIndexerOrOperator(rando) { + resetPrank(users.pauseGuardian); + subgraphService.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + resetPrank(rando); + _getSubgraphServiceExtension().cancelIndexingAgreementByPayer(agreementId); + } + + function test_SubgraphService_CancelIndexingAgreementByPayer_Revert_WhenNotAuthorized( + Seed memory seed, + address rando + ) public withSafeIndexerOrOperator(rando) { + Context storage ctx = _newCtx(seed); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, _withIndexer(ctx)); + + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreement.IndexingAgreementNonCancelableBy.selector, + accepted.rca.payer, + rando + ); + vm.expectRevert(expectedErr); + resetPrank(rando); + _getSubgraphServiceExtension().cancelIndexingAgreementByPayer(accepted.rca.agreementId); + } + + function test_SubgraphService_CancelIndexingAgreementByPayer_Revert_WhenNotAccepted( + Seed memory seed, + bytes16 agreementId + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + + resetPrank(indexerState.addr); + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreement.IndexingAgreementNotActive.selector, + agreementId + ); + vm.expectRevert(expectedErr); + _getSubgraphServiceExtension().cancelIndexingAgreementByPayer(agreementId); + } + + function test_SubgraphService_CancelIndexingAgreementByPayer_Revert_WhenCanceled( + Seed memory seed, + bool cancelSource + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); + IRecurringCollector.CancelAgreementBy by = cancelSource + ? IRecurringCollector.CancelAgreementBy.ServiceProvider + : IRecurringCollector.CancelAgreementBy.Payer; + _cancelAgreement(ctx, accepted.rca.agreementId, indexerState.addr, accepted.rca.payer, by); + + resetPrank(indexerState.addr); + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreement.IndexingAgreementNotActive.selector, + accepted.rca.agreementId + ); + vm.expectRevert(expectedErr); + _getSubgraphServiceExtension().cancelIndexingAgreementByPayer(accepted.rca.agreementId); + } + + function test_SubgraphService_CancelIndexingAgreementByPayer(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, _withIndexer(ctx)); + + _cancelAgreement( + ctx, + accepted.rca.agreementId, + accepted.rca.serviceProvider, + accepted.rca.payer, + IRecurringCollector.CancelAgreementBy.Payer + ); + } + + function test_SubgraphService_CancelIndexingAgreement_Revert_WhenPaused( + address operator, + address indexer, + bytes16 agreementId + ) public withSafeIndexerOrOperator(operator) { + resetPrank(users.pauseGuardian); + subgraphService.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + resetPrank(operator); + _getSubgraphServiceExtension().cancelIndexingAgreement(indexer, agreementId); + } + + function test_SubgraphService_CancelIndexingAgreement_Revert_WhenNotAuthorized( + address operator, + address indexer, + bytes16 agreementId + ) public withSafeIndexerOrOperator(operator) { + vm.assume(operator != indexer); + resetPrank(operator); + bytes memory expectedErr = abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + indexer, + operator + ); + vm.expectRevert(expectedErr); + _getSubgraphServiceExtension().cancelIndexingAgreement(indexer, agreementId); + } + + function test_SubgraphService_CancelIndexingAgreement_Revert_WhenInvalidProvision( + address indexer, + bytes16 agreementId, + uint256 unboundedTokens + ) public withSafeIndexerOrOperator(indexer) { + uint256 tokens = bound(unboundedTokens, 1, minimumProvisionTokens - 1); + mint(indexer, tokens); + resetPrank(indexer); + _createProvision(indexer, tokens, fishermanRewardPercentage, disputePeriod); + + bytes memory expectedErr = abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "tokens", + tokens, + minimumProvisionTokens, + maximumProvisionTokens + ); + vm.expectRevert(expectedErr); + _getSubgraphServiceExtension().cancelIndexingAgreement(indexer, agreementId); + } + + function test_SubgraphService_CancelIndexingAgreement_Revert_WhenIndexerNotRegistered( + address indexer, + bytes16 agreementId, + uint256 unboundedTokens + ) public withSafeIndexerOrOperator(indexer) { + uint256 tokens = bound(unboundedTokens, minimumProvisionTokens, MAX_TOKENS); + mint(indexer, tokens); + resetPrank(indexer); + _createProvision(indexer, tokens, fishermanRewardPercentage, disputePeriod); + bytes memory expectedErr = abi.encodeWithSelector( + ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, + indexer + ); + vm.expectRevert(expectedErr); + _getSubgraphServiceExtension().cancelIndexingAgreement(indexer, agreementId); + } + + function test_SubgraphService_CancelIndexingAgreement_Revert_WhenNotAccepted( + Seed memory seed, + bytes16 agreementId + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + + resetPrank(indexerState.addr); + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreement.IndexingAgreementNotActive.selector, + agreementId + ); + vm.expectRevert(expectedErr); + _getSubgraphServiceExtension().cancelIndexingAgreement(indexerState.addr, agreementId); + } + + function test_SubgraphService_CancelIndexingAgreement_Revert_WhenCanceled( + Seed memory seed, + bool cancelSource + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); + IRecurringCollector.CancelAgreementBy by = cancelSource + ? IRecurringCollector.CancelAgreementBy.ServiceProvider + : IRecurringCollector.CancelAgreementBy.Payer; + _cancelAgreement(ctx, accepted.rca.agreementId, accepted.rca.serviceProvider, accepted.rca.payer, by); + + resetPrank(indexerState.addr); + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreement.IndexingAgreementNotActive.selector, + accepted.rca.agreementId + ); + vm.expectRevert(expectedErr); + _getSubgraphServiceExtension().cancelIndexingAgreement(indexerState.addr, accepted.rca.agreementId); + } + + function test_SubgraphService_CancelIndexingAgreement_OK(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, _withIndexer(ctx)); + + _cancelAgreement( + ctx, + accepted.rca.agreementId, + accepted.rca.serviceProvider, + accepted.rca.payer, + IRecurringCollector.CancelAgreementBy.ServiceProvider + ); + } + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol new file mode 100644 index 000000000..c272041eb --- /dev/null +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; +import { IPaymentsCollector } from "@graphprotocol/horizon/contracts/interfaces/IPaymentsCollector.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; + +import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; +import { Allocation } from "../../../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../../../contracts/utilities/AllocationManager.sol"; +import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; + +import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; + +contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingAgreementSharedTest { + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + function test_SubgraphService_CollectIndexingFees_OK( + Seed memory seed, + uint256 entities, + bytes32 poi, + uint256 unboundedTokensCollected + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); + + assertEq(subgraphService.feesProvisionTracker(indexerState.addr), 0, "Should be 0 before collect"); + + resetPrank(indexerState.addr); + bytes memory data = abi.encode( + IRecurringCollector.CollectParams({ + agreementId: accepted.rca.agreementId, + collectionId: bytes32(uint256(uint160(indexerState.allocationId))), + tokens: 0, + dataServiceCut: 0 + }) + ); + uint256 tokensCollected = bound(unboundedTokensCollected, 1, indexerState.tokens / stakeToFeesRatio); + vm.mockCall( + address(recurringCollector), + abi.encodeWithSelector(IPaymentsCollector.collect.selector, IGraphPayments.PaymentTypes.IndexingFee, data), + abi.encode(tokensCollected) + ); + vm.expectCall( + address(recurringCollector), + abi.encodeCall(IPaymentsCollector.collect, (IGraphPayments.PaymentTypes.IndexingFee, data)) + ); + vm.expectEmit(address(subgraphService)); + emit IndexingAgreement.IndexingFeesCollectedV1( + indexerState.addr, + accepted.rca.payer, + accepted.rca.agreementId, + indexerState.allocationId, + indexerState.subgraphDeploymentId, + epochManager.currentEpoch(), + tokensCollected, + entities, + poi, + epochManager.currentEpoch() + ); + subgraphService.collect( + indexerState.addr, + IGraphPayments.PaymentTypes.IndexingFee, + _encodeCollectDataV1(accepted.rca.agreementId, entities, poi, epochManager.currentEpoch()) + ); + + assertEq( + subgraphService.feesProvisionTracker(indexerState.addr), + tokensCollected * stakeToFeesRatio, + "Should be exactly locked tokens" + ); + } + + function test_SubgraphService_CollectIndexingFees_Revert_WhenPaused( + address indexer, + bytes16 agreementId, + uint256 entities, + bytes32 poi + ) public withSafeIndexerOrOperator(indexer) { + uint256 currentEpoch = epochManager.currentEpoch(); + resetPrank(users.pauseGuardian); + subgraphService.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + resetPrank(indexer); + subgraphService.collect( + indexer, + IGraphPayments.PaymentTypes.IndexingFee, + _encodeCollectDataV1(agreementId, entities, poi, currentEpoch) + ); + } + + function test_SubgraphService_CollectIndexingFees_Revert_WhenNotAuthorized( + address operator, + address indexer, + bytes16 agreementId, + uint256 entities, + bytes32 poi + ) public withSafeIndexerOrOperator(operator) { + vm.assume(operator != indexer); + uint256 currentEpoch = epochManager.currentEpoch(); + resetPrank(operator); + bytes memory expectedErr = abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + indexer, + operator + ); + vm.expectRevert(expectedErr); + subgraphService.collect( + indexer, + IGraphPayments.PaymentTypes.IndexingFee, + _encodeCollectDataV1(agreementId, entities, poi, currentEpoch) + ); + } + + function test_SubgraphService_CollectIndexingFees_Revert_WhenInvalidProvision( + uint256 unboundedTokens, + address indexer, + bytes16 agreementId, + uint256 entities, + bytes32 poi + ) public withSafeIndexerOrOperator(indexer) { + uint256 tokens = bound(unboundedTokens, 1, minimumProvisionTokens - 1); + uint256 currentEpoch = epochManager.currentEpoch(); + mint(indexer, tokens); + resetPrank(indexer); + _createProvision(indexer, tokens, fishermanRewardPercentage, disputePeriod); + + bytes memory expectedErr = abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "tokens", + tokens, + minimumProvisionTokens, + maximumProvisionTokens + ); + vm.expectRevert(expectedErr); + subgraphService.collect( + indexer, + IGraphPayments.PaymentTypes.IndexingFee, + _encodeCollectDataV1(agreementId, entities, poi, currentEpoch) + ); + } + + function test_SubgraphService_CollectIndexingFees_Revert_WhenIndexerNotRegistered( + uint256 unboundedTokens, + address indexer, + bytes16 agreementId, + uint256 entities, + bytes32 poi + ) public withSafeIndexerOrOperator(indexer) { + uint256 tokens = bound(unboundedTokens, minimumProvisionTokens, MAX_TOKENS); + uint256 currentEpoch = epochManager.currentEpoch(); + mint(indexer, tokens); + resetPrank(indexer); + _createProvision(indexer, tokens, fishermanRewardPercentage, disputePeriod); + bytes memory expectedErr = abi.encodeWithSelector( + ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, + indexer + ); + vm.expectRevert(expectedErr); + subgraphService.collect( + indexer, + IGraphPayments.PaymentTypes.IndexingFee, + _encodeCollectDataV1(agreementId, entities, poi, currentEpoch) + ); + } + + function test_SubgraphService_CollectIndexingFees_Revert_WhenInvalidAgreement( + Seed memory seed, + bytes16 agreementId, + uint256 entities, + bytes32 poi + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + uint256 currentEpoch = epochManager.currentEpoch(); + + bytes memory expectedErr = abi.encodeWithSelector(Allocation.AllocationDoesNotExist.selector, address(0)); + vm.expectRevert(expectedErr); + resetPrank(indexerState.addr); + subgraphService.collect( + indexerState.addr, + IGraphPayments.PaymentTypes.IndexingFee, + _encodeCollectDataV1(agreementId, entities, poi, currentEpoch) + ); + } + + function test_SubgraphService_CollectIndexingFees_Reverts_WhenStopService( + Seed memory seed, + uint256 entities, + bytes32 poi + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); + + resetPrank(indexerState.addr); + subgraphService.stopService(indexerState.addr, abi.encode(indexerState.allocationId)); + + uint256 currentEpoch = epochManager.currentEpoch(); + + bytes memory expectedErr = abi.encodeWithSelector( + AllocationManager.AllocationManagerAllocationClosed.selector, + indexerState.allocationId + ); + vm.expectRevert(expectedErr); + subgraphService.collect( + indexerState.addr, + IGraphPayments.PaymentTypes.IndexingFee, + _encodeCollectDataV1(accepted.rca.agreementId, entities, poi, currentEpoch) + ); + } + + function test_SubgraphService_CollectIndexingFees_Reverts_WhenCloseStaleAllocation( + Seed memory seed, + uint256 entities, + bytes32 poi + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); + + skip(maxPOIStaleness + 1); + resetPrank(indexerState.addr); + subgraphService.closeStaleAllocation(indexerState.allocationId); + + uint256 currentEpoch = epochManager.currentEpoch(); + + bytes memory expectedErr = abi.encodeWithSelector( + AllocationManager.AllocationManagerAllocationClosed.selector, + indexerState.allocationId + ); + vm.expectRevert(expectedErr); + subgraphService.collect( + indexerState.addr, + IGraphPayments.PaymentTypes.IndexingFee, + _encodeCollectDataV1(accepted.rca.agreementId, entities, poi, currentEpoch) + ); + } + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol new file mode 100644 index 000000000..7d5a4399c --- /dev/null +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; + +import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; + +contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAgreementSharedTest { + using PPMMath for uint256; + + struct TestState { + uint256 escrowBalance; + uint256 indexerBalance; + uint256 indexerTokensLocked; + } + + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + function test_SubgraphService_CollectIndexingFee_Integration( + Seed memory seed, + uint256 fuzzyTokensCollected + ) public { + uint256 expectedTotalTokensCollected = bound(fuzzyTokensCollected, 1000, 1_000_000); + uint256 expectedTokensLocked = stakeToFeesRatio * expectedTotalTokensCollected; + uint256 expectedProtocolTokensBurnt = expectedTotalTokensCollected.mulPPMRoundUp( + graphPayments.PROTOCOL_PAYMENT_CUT() + ); + uint256 expectedIndexerTokensCollected = expectedTotalTokensCollected - expectedProtocolTokensBurnt; + + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + _addTokensToProvision(indexerState, expectedTokensLocked); + IRecurringCollector.RecurringCollectionAgreement memory rca = _recurringCollectorHelper.sensibleRCA( + ctx.ctxInternal.seed.rca + ); + uint256 agreementTokensPerSecond = 1; + rca.deadline = uint64(block.timestamp); // accept now + rca.endsAt = type(uint64).max; // no expiration + rca.maxInitialTokens = 0; // no initial payment + rca.maxOngoingTokensPerSecond = type(uint32).max; // unlimited tokens per second + rca.minSecondsPerCollection = 1; // 1 second between collections + rca.maxSecondsPerCollection = type(uint32).max; // no maximum time between collections + rca.serviceProvider = indexerState.addr; // service provider is the indexer + rca.dataService = address(subgraphService); // data service is the subgraph service + rca.metadata = _encodeAcceptIndexingAgreementMetadataV1( + indexerState.subgraphDeploymentId, + IndexingAgreement.IndexingAgreementTermsV1({ + tokensPerSecond: agreementTokensPerSecond, + tokensPerEntityPerSecond: 0 // no payment for entities + }) + ); + + _setupPayerWithEscrow(rca.payer, ctx.payer.signerPrivateKey, indexerState.addr, expectedTotalTokensCollected); + + resetPrank(indexerState.addr); + // Accept the Indexing Agreement + subgraphService.acceptIndexingAgreement( + indexerState.allocationId, + _recurringCollectorHelper.generateSignedRCA(rca, ctx.payer.signerPrivateKey) + ); + // Skip ahead to collection point + skip(expectedTotalTokensCollected / agreementTokensPerSecond); + // vm.assume(block.timestamp < type(uint64).max); + TestState memory beforeCollect = _getState(rca.payer, indexerState.addr); + bytes16 agreementId = rca.agreementId; + uint256 tokensCollected = subgraphService.collect( + indexerState.addr, + IGraphPayments.PaymentTypes.IndexingFee, + _encodeCollectDataV1(agreementId, 1, keccak256(abi.encodePacked("poi")), epochManager.currentEpoch()) + ); + TestState memory afterCollect = _getState(rca.payer, indexerState.addr); + uint256 indexerTokensCollected = afterCollect.indexerBalance - beforeCollect.indexerBalance; + uint256 protocolTokensBurnt = tokensCollected - indexerTokensCollected; + assertEq( + afterCollect.escrowBalance, + beforeCollect.escrowBalance - tokensCollected, + "Escrow balance should be reduced by the amount collected" + ); + assertEq(tokensCollected, expectedTotalTokensCollected, "Total tokens collected should match"); + assertEq(expectedProtocolTokensBurnt, protocolTokensBurnt, "Protocol tokens burnt should match"); + assertEq(indexerTokensCollected, expectedIndexerTokensCollected, "Indexer tokens collected should match"); + assertEq( + afterCollect.indexerTokensLocked, + beforeCollect.indexerTokensLocked + expectedTokensLocked, + "Locked tokens should match" + ); + } + + /* solhint-enable graph/func-name-mixedcase */ + + function _addTokensToProvision(IndexerState memory _indexerState, uint256 _tokensToAddToProvision) private { + deal({ token: address(token), to: _indexerState.addr, give: _tokensToAddToProvision }); + vm.startPrank(_indexerState.addr); + _addToProvision(_indexerState.addr, _tokensToAddToProvision); + vm.stopPrank(); + } + + function _setupPayerWithEscrow( + address _payer, + uint256 _signerPrivateKey, + address _indexer, + uint256 _escrowTokens + ) private { + _recurringCollectorHelper.authorizeSignerWithChecks(_payer, _signerPrivateKey); + + deal({ token: address(token), to: _payer, give: _escrowTokens }); + vm.startPrank(_payer); + _escrow(_escrowTokens, _indexer); + vm.stopPrank(); + } + + function _escrow(uint256 _tokens, address _indexer) private { + token.approve(address(escrow), _tokens); + escrow.deposit(address(recurringCollector), _indexer, _tokens); + } + + function _getState(address _payer, address _indexer) private view returns (TestState memory) { + CollectPaymentData memory collect = _collectPaymentData(_indexer); + + return + TestState({ + escrowBalance: escrow.getBalance(_payer, address(recurringCollector), _indexer), + indexerBalance: collect.indexerBalance, + indexerTokensLocked: collect.lockedTokens + }); + } +} 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 new file mode 100644 index 000000000..050301307 --- /dev/null +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/shared.t.sol @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; +import { ISubgraphServiceExtension } from "../../../../contracts/interfaces/ISubgraphServiceExtension.sol"; + +import { Bounder } from "@graphprotocol/horizon/test/unit/utils/Bounder.t.sol"; +import { RecurringCollectorHelper } from "@graphprotocol/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Bounder { + struct Context { + PayerState payer; + IndexerState[] indexers; + mapping(address allocationId => address indexer) allocations; + ContextInternal ctxInternal; + } + + struct IndexerState { + address addr; + address allocationId; + bytes32 subgraphDeploymentId; + uint256 tokens; + } + + struct PayerState { + address signer; + uint256 signerPrivateKey; + } + + struct ContextInternal { + IndexerSeed[] indexers; + Seed seed; + bool initialized; + } + + struct Seed { + IndexerSeed indexer0; + IndexerSeed indexer1; + IRecurringCollector.RecurringCollectionAgreement rca; + IRecurringCollector.RecurringCollectionAgreementUpgrade rcau; + IndexingAgreement.IndexingAgreementTermsV1 termsV1; + PayerSeed payer; + } + + struct IndexerSeed { + address addr; + string label; + uint256 unboundedProvisionTokens; + uint256 unboundedAllocationPrivateKey; + bytes32 subgraphDeploymentId; + } + + struct PayerSeed { + uint256 unboundedSignerPrivateKey; + } + + Context internal _context; + + bytes32 internal constant TRANSPARENT_UPGRADEABLE_PROXY_ADMIN_ADDRESS_SLOT = + 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + address internal constant GRAPH_PROXY_ADMIN_ADDRESS = 0x15c603B7eaA8eE1a272a69C4af3462F926de777F; + + RecurringCollectorHelper internal _recurringCollectorHelper; + + modifier withSafeIndexerOrOperator(address operator) { + vm.assume(_isSafeSubgraphServiceCaller(operator)); + _; + } + + function setUp() public override { + super.setUp(); + + _recurringCollectorHelper = new RecurringCollectorHelper(recurringCollector); + } + + /* + * HELPERS + */ + + function _subgraphServiceSafePrank(address _addr) internal returns (address) { + address originalPrankAddress = msg.sender; + vm.assume(_isSafeSubgraphServiceCaller(_addr)); + resetPrank(_addr); + + return originalPrankAddress; + } + + function _stopOrResetPrank(address _originalSender) internal { + if (_originalSender == 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38) { + vm.stopPrank(); + } else { + resetPrank(_originalSender); + } + } + + function _cancelAgreement( + Context storage _ctx, + bytes16 _agreementId, + address _indexer, + address _payer, + IRecurringCollector.CancelAgreementBy _by + ) internal { + bool byIndexer = _by == IRecurringCollector.CancelAgreementBy.ServiceProvider; + vm.expectEmit(address(subgraphService)); + emit IndexingAgreement.IndexingAgreementCanceled(_indexer, _payer, _agreementId, byIndexer ? _indexer : _payer); + + if (byIndexer) { + _subgraphServiceSafePrank(_indexer); + _getSubgraphServiceExtension().cancelIndexingAgreement(_indexer, _agreementId); + } else { + _subgraphServiceSafePrank(_ctx.payer.signer); + _getSubgraphServiceExtension().cancelIndexingAgreementByPayer(_agreementId); + } + } + + function _withIndexer(Context storage _ctx) internal returns (IndexerState memory) { + require(_ctx.ctxInternal.indexers.length > 0, "No indexer seeds available"); + + IndexerSeed memory indexerSeed = _ctx.ctxInternal.indexers[_ctx.ctxInternal.indexers.length - 1]; + _ctx.ctxInternal.indexers.pop(); + + indexerSeed.label = string.concat("_withIndexer-", Strings.toString(_ctx.ctxInternal.indexers.length)); + + return _setupIndexer(_ctx, indexerSeed); + } + + function _setupIndexer(Context storage _ctx, IndexerSeed memory _seed) internal returns (IndexerState memory) { + vm.assume(_getIndexer(_ctx, _seed.addr).addr == address(0)); + + (uint256 allocationKey, address allocationId) = boundKeyAndAddr(_seed.unboundedAllocationPrivateKey); + vm.assume(_ctx.allocations[allocationId] == address(0)); + _ctx.allocations[allocationId] = _seed.addr; + + uint256 tokens = bound(_seed.unboundedProvisionTokens, minimumProvisionTokens, MAX_TOKENS); + + IndexerState memory indexer = IndexerState({ + addr: _seed.addr, + allocationId: allocationId, + subgraphDeploymentId: _seed.subgraphDeploymentId, + tokens: tokens + }); + vm.label(indexer.addr, string.concat("_setupIndexer-", _seed.label)); + + // Mint tokens to the indexer + mint(_seed.addr, tokens); + + // Create the indexer + address originalPrank = _subgraphServiceSafePrank(indexer.addr); + _createProvision(indexer.addr, indexer.tokens, fishermanRewardPercentage, disputePeriod); + _register(indexer.addr, abi.encode("url", "geoHash", address(0))); + bytes memory data = _createSubgraphAllocationData( + indexer.addr, + indexer.subgraphDeploymentId, + allocationKey, + indexer.tokens + ); + _startService(indexer.addr, data); + + _ctx.indexers.push(indexer); + + _stopOrResetPrank(originalPrank); + + return indexer; + } + + function _withAcceptedIndexingAgreement( + Context storage _ctx, + IndexerState memory _indexerState + ) internal returns (IRecurringCollector.SignedRCA memory) { + IRecurringCollector.RecurringCollectionAgreement memory rca = _ctx.ctxInternal.seed.rca; + + IndexingAgreement.AcceptIndexingAgreementMetadata memory metadata = _newAcceptIndexingAgreementMetadataV1( + _indexerState.subgraphDeploymentId + ); + rca.serviceProvider = _indexerState.addr; + rca.dataService = address(subgraphService); + rca.metadata = abi.encode(metadata); + + rca = _recurringCollectorHelper.sensibleRCA(rca); + + IRecurringCollector.SignedRCA memory signedRCA = _recurringCollectorHelper.generateSignedRCA( + rca, + _ctx.payer.signerPrivateKey + ); + _recurringCollectorHelper.authorizeSignerWithChecks(rca.payer, _ctx.payer.signerPrivateKey); + + vm.expectEmit(address(subgraphService)); + emit IndexingAgreement.IndexingAgreementAccepted( + rca.serviceProvider, + rca.payer, + rca.agreementId, + _indexerState.allocationId, + metadata.subgraphDeploymentId, + metadata.version, + metadata.terms + ); + _subgraphServiceSafePrank(_indexerState.addr); + subgraphService.acceptIndexingAgreement(_indexerState.allocationId, signedRCA); + + return signedRCA; + } + + function _newCtx(Seed memory _seed) internal returns (Context storage) { + require(_context.ctxInternal.initialized == false, "Context already initialized"); + Context storage ctx = _context; + + // Initialize + ctx.ctxInternal.initialized = true; + + // Setup seeds + ctx.ctxInternal.seed = _seed; + ctx.ctxInternal.indexers.push(_seed.indexer0); + ctx.ctxInternal.indexers.push(_seed.indexer1); + + // Setup payer + ctx.payer.signerPrivateKey = boundKey(ctx.ctxInternal.seed.payer.unboundedSignerPrivateKey); + ctx.payer.signer = vm.addr(ctx.payer.signerPrivateKey); + + return ctx; + } + + function _generateAcceptableSignedRCA( + Context storage _ctx, + address _indexerAddress + ) internal returns (IRecurringCollector.SignedRCA memory) { + IRecurringCollector.RecurringCollectionAgreement memory rca = _generateAcceptableRecurringCollectionAgreement( + _ctx, + _indexerAddress + ); + _recurringCollectorHelper.authorizeSignerWithChecks(rca.payer, _ctx.payer.signerPrivateKey); + + return _recurringCollectorHelper.generateSignedRCA(rca, _ctx.payer.signerPrivateKey); + } + + function _generateAcceptableRecurringCollectionAgreement( + Context storage _ctx, + address _indexerAddress + ) internal view returns (IRecurringCollector.RecurringCollectionAgreement memory) { + IndexerState memory indexer = _requireIndexer(_ctx, _indexerAddress); + IndexingAgreement.AcceptIndexingAgreementMetadata memory metadata = _newAcceptIndexingAgreementMetadataV1( + indexer.subgraphDeploymentId + ); + IRecurringCollector.RecurringCollectionAgreement memory rca = _ctx.ctxInternal.seed.rca; + rca.serviceProvider = indexer.addr; + rca.dataService = address(subgraphService); + rca.metadata = abi.encode(metadata); + + return _recurringCollectorHelper.sensibleRCA(rca); + } + + function _generateAcceptableSignedRCAU( + Context storage _ctx, + IRecurringCollector.RecurringCollectionAgreement memory _rca + ) internal view returns (IRecurringCollector.SignedRCAU memory) { + return + _recurringCollectorHelper.generateSignedRCAU( + _generateAcceptableRecurringCollectionAgreementUpgrade(_ctx, _rca), + _ctx.payer.signerPrivateKey + ); + } + + function _generateAcceptableRecurringCollectionAgreementUpgrade( + Context storage _ctx, + IRecurringCollector.RecurringCollectionAgreement memory _rca + ) internal view returns (IRecurringCollector.RecurringCollectionAgreementUpgrade memory) { + IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau = _ctx.ctxInternal.seed.rcau; + rcau.agreementId = _rca.agreementId; + rcau.metadata = _encodeUpgradeIndexingAgreementMetadataV1( + _newUpgradeIndexingAgreementMetadataV1( + _ctx.ctxInternal.seed.termsV1.tokensPerSecond, + _ctx.ctxInternal.seed.termsV1.tokensPerEntityPerSecond + ) + ); + return _recurringCollectorHelper.sensibleRCAU(rcau); + } + + function _requireIndexer(Context storage _ctx, address _indexer) internal view returns (IndexerState memory) { + IndexerState memory indexerState = _getIndexer(_ctx, _indexer); + require(indexerState.addr != address(0), "Indexer not found in context"); + + return indexerState; + } + + function _getIndexer(Context storage _ctx, address _indexer) internal view returns (IndexerState memory zero) { + for (uint256 i = 0; i < _ctx.indexers.length; i++) { + if (_ctx.indexers[i].addr == _indexer) { + return _ctx.indexers[i]; + } + } + + return zero; + } + + function _isSafeSubgraphServiceCaller(address _candidate) internal view returns (bool) { + return + _candidate != address(0) && + _candidate != address(_transparentUpgradeableProxyAdmin()) && + _candidate != address(proxyAdmin); + } + + function _transparentUpgradeableProxyAdmin() internal view returns (address) { + return + address( + uint160(uint256(vm.load(address(subgraphService), TRANSPARENT_UPGRADEABLE_PROXY_ADMIN_ADDRESS_SLOT))) + ); + } + + function _getSubgraphServiceExtension() internal view returns (ISubgraphServiceExtension) { + return ISubgraphServiceExtension(address(subgraphService)); + } + + function _newAcceptIndexingAgreementMetadataV1( + bytes32 _subgraphDeploymentId + ) internal pure returns (IndexingAgreement.AcceptIndexingAgreementMetadata memory) { + return + IndexingAgreement.AcceptIndexingAgreementMetadata({ + subgraphDeploymentId: _subgraphDeploymentId, + version: IndexingAgreement.IndexingAgreementVersion.V1, + terms: abi.encode( + IndexingAgreement.IndexingAgreementTermsV1({ tokensPerSecond: 0, tokensPerEntityPerSecond: 0 }) + ) + }); + } + + function _newUpgradeIndexingAgreementMetadataV1( + uint256 _tokensPerSecond, + uint256 _tokensPerEntityPerSecond + ) internal pure returns (IndexingAgreement.UpgradeIndexingAgreementMetadata memory) { + return + IndexingAgreement.UpgradeIndexingAgreementMetadata({ + version: IndexingAgreement.IndexingAgreementVersion.V1, + terms: abi.encode( + IndexingAgreement.IndexingAgreementTermsV1({ + tokensPerSecond: _tokensPerSecond, + tokensPerEntityPerSecond: _tokensPerEntityPerSecond + }) + ) + }); + } + + function _encodeCollectDataV1( + bytes16 _agreementId, + uint256 _entities, + bytes32 _poi, + uint256 _epoch + ) internal pure returns (bytes memory) { + return abi.encode(_agreementId, abi.encode(_entities, _poi, _epoch)); + } + + function _encodeAcceptIndexingAgreementMetadataV1( + bytes32 _subgraphDeploymentId, + IndexingAgreement.IndexingAgreementTermsV1 memory _terms + ) internal pure returns (bytes memory) { + return + abi.encode( + IndexingAgreement.AcceptIndexingAgreementMetadata({ + subgraphDeploymentId: _subgraphDeploymentId, + version: IndexingAgreement.IndexingAgreementVersion.V1, + terms: abi.encode(_terms) + }) + ); + } + + function _encodeUpgradeIndexingAgreementMetadataV1( + IndexingAgreement.UpgradeIndexingAgreementMetadata memory _t + ) internal pure returns (bytes memory) { + return abi.encode(_t); + } +} diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/upgrade.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/upgrade.t.sol new file mode 100644 index 000000000..910e36b07 --- /dev/null +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/upgrade.t.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; + +import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; +import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; +import { Decoder } from "../../../../contracts/libraries/Decoder.sol"; + +import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; + +contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingAgreementSharedTest { + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + function test_SubgraphService_UpgradeIndexingAgreementIndexingAgreement_Revert_WhenPaused( + address operator, + IRecurringCollector.SignedRCAU calldata signedRCAU + ) public withSafeIndexerOrOperator(operator) { + resetPrank(users.pauseGuardian); + subgraphService.pause(); + + resetPrank(operator); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + _getSubgraphServiceExtension().upgradeIndexingAgreement(operator, signedRCAU); + } + + function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenNotAuthorized( + address indexer, + address notAuthorized, + IRecurringCollector.SignedRCAU calldata signedRCAU + ) public withSafeIndexerOrOperator(notAuthorized) { + vm.assume(notAuthorized != indexer); + resetPrank(notAuthorized); + bytes memory expectedErr = abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + indexer, + notAuthorized + ); + vm.expectRevert(expectedErr); + _getSubgraphServiceExtension().upgradeIndexingAgreement(indexer, signedRCAU); + } + + function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenInvalidProvision( + address indexer, + uint256 unboundedTokens, + IRecurringCollector.SignedRCAU memory signedRCAU + ) public withSafeIndexerOrOperator(indexer) { + uint256 tokens = bound(unboundedTokens, 1, minimumProvisionTokens - 1); + mint(indexer, tokens); + resetPrank(indexer); + _createProvision(indexer, tokens, fishermanRewardPercentage, disputePeriod); + + bytes memory expectedErr = abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "tokens", + tokens, + minimumProvisionTokens, + maximumProvisionTokens + ); + vm.expectRevert(expectedErr); + _getSubgraphServiceExtension().upgradeIndexingAgreement(indexer, signedRCAU); + } + + function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenIndexerNotRegistered( + address indexer, + uint256 unboundedTokens, + IRecurringCollector.SignedRCAU memory signedRCAU + ) public withSafeIndexerOrOperator(indexer) { + uint256 tokens = bound(unboundedTokens, minimumProvisionTokens, MAX_TOKENS); + mint(indexer, tokens); + resetPrank(indexer); + _createProvision(indexer, tokens, fishermanRewardPercentage, disputePeriod); + + bytes memory expectedErr = abi.encodeWithSelector( + ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, + indexer + ); + vm.expectRevert(expectedErr); + _getSubgraphServiceExtension().upgradeIndexingAgreement(indexer, signedRCAU); + } + + function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenNotAccepted(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCAU memory acceptableUpgrade = _generateAcceptableSignedRCAU( + ctx, + _generateAcceptableRecurringCollectionAgreement(ctx, indexerState.addr) + ); + + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreement.IndexingAgreementNotActive.selector, + acceptableUpgrade.rcau.agreementId + ); + vm.expectRevert(expectedErr); + resetPrank(indexerState.addr); + _getSubgraphServiceExtension().upgradeIndexingAgreement(indexerState.addr, acceptableUpgrade); + } + + function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenNotAuthorizedForAgreement( + Seed memory seed + ) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerStateA = _withIndexer(ctx); + IndexerState memory indexerStateB = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerStateA); + IRecurringCollector.SignedRCAU memory acceptableUpgrade = _generateAcceptableSignedRCAU(ctx, accepted.rca); + + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreement.IndexingAgreementNotAuthorized.selector, + acceptableUpgrade.rcau.agreementId, + indexerStateB.addr + ); + vm.expectRevert(expectedErr); + resetPrank(indexerStateB.addr); + _getSubgraphServiceExtension().upgradeIndexingAgreement(indexerStateB.addr, acceptableUpgrade); + } + + function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenInvalidMetadata(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); + IRecurringCollector.RecurringCollectionAgreementUpgrade + memory acceptableUpgrade = _generateAcceptableRecurringCollectionAgreementUpgrade(ctx, accepted.rca); + acceptableUpgrade.metadata = bytes("invalid"); + IRecurringCollector.SignedRCAU memory unacceptableUpgrade = _recurringCollectorHelper.generateSignedRCAU( + acceptableUpgrade, + ctx.payer.signerPrivateKey + ); + + bytes memory expectedErr = abi.encodeWithSelector( + Decoder.DecoderInvalidData.selector, + "decodeRCAUMetadata", + unacceptableUpgrade.rcau.metadata + ); + vm.expectRevert(expectedErr); + resetPrank(indexerState.addr); + _getSubgraphServiceExtension().upgradeIndexingAgreement(indexerState.addr, unacceptableUpgrade); + } + + function test_SubgraphService_UpgradeIndexingAgreement_OK(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); + IRecurringCollector.SignedRCAU memory acceptableUpgrade = _generateAcceptableSignedRCAU(ctx, accepted.rca); + + resetPrank(indexerState.addr); + _getSubgraphServiceExtension().upgradeIndexingAgreement(indexerState.addr, acceptableUpgrade); + } + /* solhint-enable graph/func-name-mixedcase */ +} From 6f6cbdb70b3edeaab1537d27e334b8b97a28ffd7 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 23 May 2025 09:59:00 -0300 Subject: [PATCH 03/90] f: rename upgrade to update --- .../interfaces/IRecurringCollector.sol | 24 ++++---- .../collectors/RecurringCollector.sol | 18 +++--- .../RecurringCollectorHelper.t.sol | 6 +- .../payments/recurring-collector/shared.t.sol | 4 +- .../{upgrade.t.sol => update.t.sol} | 56 +++++++++---------- .../contracts/SubgraphServiceExtension.sol | 4 +- .../interfaces/ISubgraphServiceExtension.sol | 4 +- .../contracts/libraries/Decoder.sol | 6 +- .../contracts/libraries/IndexingAgreement.sol | 10 ++-- .../contracts/libraries/UnsafeDecoder.sol | 4 +- .../indexing-agreement/shared.t.sol | 24 ++++---- .../{upgrade.t.sol => update.sol} | 54 +++++++++--------- 12 files changed, 107 insertions(+), 107 deletions(-) rename packages/horizon/test/unit/payments/recurring-collector/{upgrade.t.sol => update.t.sol} (72%) rename packages/subgraph-service/test/unit/subgraphService/indexing-agreement/{upgrade.t.sol => update.sol} (66%) diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index 5a6badd42..19beda438 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -61,15 +61,15 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { bytes metadata; } - /// @notice A representation of a signed Recurring Collection Agreement Upgrade (RCAU) + /// @notice A representation of a signed Recurring Collection Agreement Update (RCAU) struct SignedRCAU { // The RCAU - RecurringCollectionAgreementUpgrade rcau; + RecurringCollectionAgreementUpdate rcau; // Signature - 65 bytes: r (32 Bytes) || s (32 Bytes) || v (1 Byte) bytes signature; } - struct RecurringCollectionAgreementUpgrade { + struct RecurringCollectionAgreementUpdate { // The agreement ID bytes16 agreementId; // The deadline for upgrading @@ -176,24 +176,24 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { ); /** - * @notice Emitted when an agreement is upgraded + * @notice Emitted when an agreement is updated * @param dataService The address of the data service * @param payer The address of the payer * @param serviceProvider The address of the service provider * @param agreementId The agreement ID - * @param upgradedAt The timestamp when the agreement was upgraded + * @param updatedAt The timestamp when the agreement was updated * @param endsAt The timestamp when the agreement ends * @param maxInitialTokens The maximum amount of tokens that can be collected in the first collection * @param maxOngoingTokensPerSecond The maximum amount of tokens that can be collected per second * @param minSecondsPerCollection The minimum amount of seconds that must pass between collections * @param maxSecondsPerCollection The maximum amount of seconds that can pass between collections */ - event AgreementUpgraded( + event AgreementUpdated( address indexed dataService, address indexed payer, address indexed serviceProvider, bytes16 agreementId, - uint64 upgradedAt, + uint64 updatedAt, uint64 endsAt, uint256 maxInitialTokens, uint256 maxOngoingTokensPerSecond, @@ -308,9 +308,9 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { function cancel(bytes16 agreementId, CancelAgreementBy by) external; /** - * @dev Upgrade an indexing agreement. + * @dev Update an indexing agreement. */ - function upgrade(SignedRCAU calldata signedRCAU) external; + function update(SignedRCAU calldata signedRCAU) external; /** * @dev Computes the hash of a RecurringCollectionAgreement (RCA). @@ -320,11 +320,11 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { function encodeRCA(RecurringCollectionAgreement calldata rca) external view returns (bytes32); /** - * @dev Computes the hash of a RecurringCollectionAgreementUpgrade (RCAU). + * @dev Computes the hash of a RecurringCollectionAgreementUpdate (RCAU). * @param rcau The RCAU for which to compute the hash. * @return The hash of the RCAU. */ - function encodeRCAU(RecurringCollectionAgreementUpgrade calldata rcau) external view returns (bytes32); + function encodeRCAU(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32); /** * @dev Recovers the signer address of a signed RecurringCollectionAgreement (RCA). @@ -334,7 +334,7 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { function recoverRCASigner(SignedRCA calldata signedRCA) external view returns (address); /** - * @dev Recovers the signer address of a signed RecurringCollectionAgreementUpgrade (RCAU). + * @dev Recovers the signer address of a signed RecurringCollectionAgreementUpdate (RCAU). * @param signedRCAU The SignedRCAU containing the RCAU and its signature. * @return The address of the signer. */ diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index cdde83b3e..a26780ba1 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -27,10 +27,10 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC "RecurringCollectionAgreement(bytes16 agreementId,uint256 deadline,uint256 endsAt,address payer,address dataService,address serviceProvider,uint256 maxInitialTokens,uint256 maxOngoingTokensPerSecond,uint32 minSecondsPerCollection,uint32 maxSecondsPerCollection,bytes metadata)" ); - /// @notice The EIP712 typehash for the RecurringCollectionAgreementUpgrade struct + /// @notice The EIP712 typehash for the RecurringCollectionAgreementUpdate struct bytes32 public constant EIP712_RCAU_TYPEHASH = keccak256( - "RecurringCollectionAgreementUpgrade(bytes16 agreementId,uint256 deadline,uint256 endsAt,uint256 maxInitialTokens,uint256 maxOngoingTokensPerSecond,uint32 minSecondsPerCollection,uint32 maxSecondsPerCollection,bytes metadata)" + "RecurringCollectionAgreementUpdate(bytes16 agreementId,uint256 deadline,uint256 endsAt,uint256 maxInitialTokens,uint256 maxOngoingTokensPerSecond,uint32 minSecondsPerCollection,uint32 maxSecondsPerCollection,bytes metadata)" ); /// @notice Tracks agreements @@ -151,11 +151,11 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** - * @notice Upgrade an indexing agreement. - * See {IRecurringCollector.upgrade}. + * @notice Update an indexing agreement. + * See {IRecurringCollector.update}. * @dev Caller must be the data service for the agreement. */ - function upgrade(SignedRCAU calldata signedRCAU) external { + function update(SignedRCAU calldata signedRCAU) external { require( signedRCAU.rcau.deadline >= block.timestamp, RecurringCollectorAgreementDeadlineElapsed(signedRCAU.rcau.deadline) @@ -174,7 +174,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC // check that the voucher is signed by the payer (or proxy) _requireAuthorizedRCAUSigner(signedRCAU, agreement.payer); - // upgrade the agreement + // update the agreement agreement.endsAt = signedRCAU.rcau.endsAt; agreement.maxInitialTokens = signedRCAU.rcau.maxInitialTokens; agreement.maxOngoingTokensPerSecond = signedRCAU.rcau.maxOngoingTokensPerSecond; @@ -182,7 +182,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC agreement.maxSecondsPerCollection = signedRCAU.rcau.maxSecondsPerCollection; _requireValidAgreement(agreement); - emit AgreementUpgraded( + emit AgreementUpdated( agreement.dataService, agreement.payer, agreement.serviceProvider, @@ -220,7 +220,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @notice See {IRecurringCollector.encodeRCAU} */ - function encodeRCAU(RecurringCollectionAgreementUpgrade calldata rcau) external view returns (bytes32) { + function encodeRCAU(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32) { return _encodeRCAU(rcau); } @@ -409,7 +409,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @notice See {IRecurringCollector.encodeRCAU} */ - function _encodeRCAU(RecurringCollectionAgreementUpgrade memory _rcau) private view returns (bytes32) { + function _encodeRCAU(RecurringCollectionAgreementUpdate memory _rcau) private view returns (bytes32) { return _hashTypedDataV4( keccak256( diff --git a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol index 754c2e796..7fa49150f 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol @@ -31,7 +31,7 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder { } function generateSignedRCAU( - IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau, + IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau, uint256 signerPrivateKey ) public view returns (IRecurringCollector.SignedRCAU memory) { bytes32 messageHash = collector.encodeRCAU(rcau); @@ -86,8 +86,8 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder { } function sensibleRCAU( - IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau - ) public view returns (IRecurringCollector.RecurringCollectionAgreementUpgrade memory) { + IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau + ) public view returns (IRecurringCollector.RecurringCollectionAgreementUpdate memory) { rcau.minSecondsPerCollection = _sensibleMinSecondsPerCollection(rcau.minSecondsPerCollection); rcau.maxSecondsPerCollection = _sensibleMaxSecondsPerCollection( rcau.maxSecondsPerCollection, diff --git a/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol b/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol index b4b172277..20fb1edf7 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol @@ -24,9 +24,9 @@ contract RecurringCollectorSharedTest is Test, Bounder { uint256 unboundedSignerKey; } - struct FuzzyTestUpgrade { + struct FuzzyTestUpdate { FuzzyTestAccept fuzzyTestAccept; - IRecurringCollector.RecurringCollectionAgreementUpgrade rcau; + IRecurringCollector.RecurringCollectionAgreementUpdate rcau; } RecurringCollector internal _recurringCollector; diff --git a/packages/horizon/test/unit/payments/recurring-collector/upgrade.t.sol b/packages/horizon/test/unit/payments/recurring-collector/update.t.sol similarity index 72% rename from packages/horizon/test/unit/payments/recurring-collector/upgrade.t.sol rename to packages/horizon/test/unit/payments/recurring-collector/update.t.sol index d5b77bcaf..bb01fbfd7 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/upgrade.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/update.t.sol @@ -5,23 +5,23 @@ import { IRecurringCollector } from "../../../../contracts/interfaces/IRecurring import { RecurringCollectorSharedTest } from "./shared.t.sol"; -contract RecurringCollectorUpgradeTest is RecurringCollectorSharedTest { +contract RecurringCollectorUpdateTest is RecurringCollectorSharedTest { /* * TESTS */ /* solhint-disable graph/func-name-mixedcase */ - function test_Upgrade_Revert_WhenUpgradeElapsed( + function test_Update_Revert_WhenUpdateElapsed( IRecurringCollector.RecurringCollectionAgreement memory rca, - IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau, - uint256 unboundedUpgradeSkip + IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau, + uint256 unboundedUpdateSkip ) public { rca = _recurringCollectorHelper.sensibleRCA(rca); rcau = _recurringCollectorHelper.sensibleRCAU(rcau); rcau.agreementId = rca.agreementId; - boundSkipCeil(unboundedUpgradeSkip, type(uint64).max); + boundSkipCeil(unboundedUpdateSkip, type(uint64).max); rcau.deadline = uint64(bound(rcau.deadline, 0, block.timestamp - 1)); IRecurringCollector.SignedRCAU memory signedRCAU = IRecurringCollector.SignedRCAU({ rcau: rcau, @@ -34,12 +34,12 @@ contract RecurringCollectorUpgradeTest is RecurringCollectorSharedTest { ); vm.expectRevert(expectedErr); vm.prank(rca.dataService); - _recurringCollector.upgrade(signedRCAU); + _recurringCollector.update(signedRCAU); } - function test_Upgrade_Revert_WhenNeverAccepted( + function test_Update_Revert_WhenNeverAccepted( IRecurringCollector.RecurringCollectionAgreement memory rca, - IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau + IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau ) public { rca = _recurringCollectorHelper.sensibleRCA(rca); rcau = _recurringCollectorHelper.sensibleRCAU(rcau); @@ -58,20 +58,20 @@ contract RecurringCollectorUpgradeTest is RecurringCollectorSharedTest { ); vm.expectRevert(expectedErr); vm.prank(rca.dataService); - _recurringCollector.upgrade(signedRCAU); + _recurringCollector.update(signedRCAU); } - function test_Upgrade_Revert_WhenDataServiceNotAuthorized( - FuzzyTestUpgrade calldata fuzzyTestUpgrade, + function test_Update_Revert_WhenDataServiceNotAuthorized( + FuzzyTestUpdate calldata fuzzyTestUpdate, address notDataService ) public { - vm.assume(fuzzyTestUpgrade.fuzzyTestAccept.rca.dataService != notDataService); + vm.assume(fuzzyTestUpdate.fuzzyTestAccept.rca.dataService != notDataService); (IRecurringCollector.SignedRCA memory accepted, uint256 signerKey) = _sensibleAuthorizeAndAccept( - fuzzyTestUpgrade.fuzzyTestAccept + fuzzyTestUpdate.fuzzyTestAccept ); - IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau = _recurringCollectorHelper.sensibleRCAU( - fuzzyTestUpgrade.rcau + IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau = _recurringCollectorHelper.sensibleRCAU( + fuzzyTestUpdate.rcau ); rcau.agreementId = accepted.rca.agreementId; @@ -87,21 +87,21 @@ contract RecurringCollectorUpgradeTest is RecurringCollectorSharedTest { ); vm.expectRevert(expectedErr); vm.prank(notDataService); - _recurringCollector.upgrade(signedRCAU); + _recurringCollector.update(signedRCAU); } - function test_Upgrade_Revert_WhenInvalidSigner( - FuzzyTestUpgrade calldata fuzzyTestUpgrade, + function test_Update_Revert_WhenInvalidSigner( + FuzzyTestUpdate calldata fuzzyTestUpdate, uint256 unboundedInvalidSignerKey ) public { (IRecurringCollector.SignedRCA memory accepted, uint256 signerKey) = _sensibleAuthorizeAndAccept( - fuzzyTestUpgrade.fuzzyTestAccept + fuzzyTestUpdate.fuzzyTestAccept ); uint256 invalidSignerKey = boundKey(unboundedInvalidSignerKey); vm.assume(signerKey != invalidSignerKey); - IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau = _recurringCollectorHelper.sensibleRCAU( - fuzzyTestUpgrade.rcau + IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau = _recurringCollectorHelper.sensibleRCAU( + fuzzyTestUpdate.rcau ); rcau.agreementId = accepted.rca.agreementId; @@ -112,15 +112,15 @@ contract RecurringCollectorUpgradeTest is RecurringCollectorSharedTest { vm.expectRevert(IRecurringCollector.RecurringCollectorInvalidSigner.selector); vm.prank(accepted.rca.dataService); - _recurringCollector.upgrade(signedRCAU); + _recurringCollector.update(signedRCAU); } - function test_Upgrade_OK(FuzzyTestUpgrade calldata fuzzyTestUpgrade) public { + function test_Update_OK(FuzzyTestUpdate calldata fuzzyTestUpdate) public { (IRecurringCollector.SignedRCA memory accepted, uint256 signerKey) = _sensibleAuthorizeAndAccept( - fuzzyTestUpgrade.fuzzyTestAccept + fuzzyTestUpdate.fuzzyTestAccept ); - IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau = _recurringCollectorHelper.sensibleRCAU( - fuzzyTestUpgrade.rcau + IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau = _recurringCollectorHelper.sensibleRCAU( + fuzzyTestUpdate.rcau ); rcau.agreementId = accepted.rca.agreementId; IRecurringCollector.SignedRCAU memory signedRCAU = _recurringCollectorHelper.generateSignedRCAU( @@ -129,7 +129,7 @@ contract RecurringCollectorUpgradeTest is RecurringCollectorSharedTest { ); vm.expectEmit(address(_recurringCollector)); - emit IRecurringCollector.AgreementUpgraded( + emit IRecurringCollector.AgreementUpdated( accepted.rca.dataService, accepted.rca.payer, accepted.rca.serviceProvider, @@ -142,7 +142,7 @@ contract RecurringCollectorUpgradeTest is RecurringCollectorSharedTest { rcau.maxSecondsPerCollection ); vm.prank(accepted.rca.dataService); - _recurringCollector.upgrade(signedRCAU); + _recurringCollector.update(signedRCAU); IRecurringCollector.AgreementData memory agreement = _recurringCollector.getAgreement(accepted.rca.agreementId); assertEq(rcau.endsAt, agreement.endsAt); diff --git a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol index c4e81d6ae..a9616357e 100644 --- a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol +++ b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol @@ -15,11 +15,11 @@ contract SubgraphServiceExtension is PausableUpgradeable { _; } - function upgradeIndexingAgreement( + function updateIndexingAgreement( address indexer, IRecurringCollector.SignedRCAU calldata signedRCAU ) external modifiersHack(indexer) { - IndexingAgreement._getManager().upgrade(indexer, signedRCAU); + IndexingAgreement._getManager().update(indexer, signedRCAU); } /** diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol index 40ec722a0..4e2fb2873 100644 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol @@ -12,9 +12,9 @@ interface ISubgraphServiceExtension { // function acceptIndexingAgreement(address allocationId, IRecurringCollector.SignedRCA calldata signedRCA) external; /** - * @notice Upgrade an indexing agreement. + * @notice Update an indexing agreement. */ - function upgradeIndexingAgreement(address indexer, IRecurringCollector.SignedRCAU calldata signedRCAU) external; + function updateIndexingAgreement(address indexer, IRecurringCollector.SignedRCAU calldata signedRCAU) external; /** * @notice Cancel an indexing agreement by indexer / operator. diff --git a/packages/subgraph-service/contracts/libraries/Decoder.sol b/packages/subgraph-service/contracts/libraries/Decoder.sol index 3d65fd463..82b9da673 100644 --- a/packages/subgraph-service/contracts/libraries/Decoder.sol +++ b/packages/subgraph-service/contracts/libraries/Decoder.sol @@ -47,14 +47,14 @@ library Decoder { /** * @notice Decodes the RCAU metadata. * - * @param data The data to decode. See {IndexingAgreement.UpgradeIndexingAgreementMetadata} + * @param data The data to decode. See {IndexingAgreement.UpdateIndexingAgreementMetadata} * @return The decoded data */ function decodeRCAUMetadata( bytes memory data - ) public pure returns (IndexingAgreement.UpgradeIndexingAgreementMetadata memory) { + ) public pure returns (IndexingAgreement.UpdateIndexingAgreementMetadata memory) { try UnsafeDecoder.decodeRCAUMetadata_(data) returns ( - IndexingAgreement.UpgradeIndexingAgreementMetadata memory metadata + IndexingAgreement.UpdateIndexingAgreementMetadata memory metadata ) { return metadata; } catch { diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 842890fb8..f6ce06616 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -55,11 +55,11 @@ library IndexingAgreement { } /** - * @notice Upgrade Indexing Agreement metadata + * @notice Update Indexing Agreement metadata * @param version The indexing agreement version * @param terms The indexing agreement terms */ - struct UpgradeIndexingAgreementMetadata { + struct UpdateIndexingAgreementMetadata { IndexingAgreementVersion version; bytes terms; } @@ -246,7 +246,7 @@ library IndexingAgreement { _directory().recurringCollector().accept(signedRCA); } - function upgrade( + function update( Manager storage self, address indexer, IRecurringCollector.SignedRCAU calldata signedRCAU @@ -258,14 +258,14 @@ library IndexingAgreement { IndexingAgreementNotAuthorized(signedRCAU.rcau.agreementId, indexer) ); - UpgradeIndexingAgreementMetadata memory metadata = Decoder.decodeRCAUMetadata(signedRCAU.rcau.metadata); + UpdateIndexingAgreementMetadata memory metadata = Decoder.decodeRCAUMetadata(signedRCAU.rcau.metadata); wrapper.agreement.version = metadata.version; require(metadata.version == IndexingAgreementVersion.V1, InvalidIndexingAgreementVersion(metadata.version)); _setTermsV1(self, signedRCAU.rcau.agreementId, metadata.terms); - _directory().recurringCollector().upgrade(signedRCAU); + _directory().recurringCollector().update(signedRCAU); } function cancel(Manager storage self, address indexer, bytes16 agreementId) external { diff --git a/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol b/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol index e32f7c657..b30af224f 100644 --- a/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol +++ b/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol @@ -25,8 +25,8 @@ library UnsafeDecoder { */ function decodeRCAUMetadata_( bytes calldata data - ) public pure returns (IndexingAgreement.UpgradeIndexingAgreementMetadata memory) { - return abi.decode(data, (IndexingAgreement.UpgradeIndexingAgreementMetadata)); + ) public pure returns (IndexingAgreement.UpdateIndexingAgreementMetadata memory) { + return abi.decode(data, (IndexingAgreement.UpdateIndexingAgreementMetadata)); } /** 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 050301307..2d3c19b46 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 @@ -41,7 +41,7 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun IndexerSeed indexer0; IndexerSeed indexer1; IRecurringCollector.RecurringCollectionAgreement rca; - IRecurringCollector.RecurringCollectionAgreementUpgrade rcau; + IRecurringCollector.RecurringCollectionAgreementUpdate rcau; IndexingAgreement.IndexingAgreementTermsV1 termsV1; PayerSeed payer; } @@ -258,19 +258,19 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun ) internal view returns (IRecurringCollector.SignedRCAU memory) { return _recurringCollectorHelper.generateSignedRCAU( - _generateAcceptableRecurringCollectionAgreementUpgrade(_ctx, _rca), + _generateAcceptableRecurringCollectionAgreementUpdate(_ctx, _rca), _ctx.payer.signerPrivateKey ); } - function _generateAcceptableRecurringCollectionAgreementUpgrade( + function _generateAcceptableRecurringCollectionAgreementUpdate( Context storage _ctx, IRecurringCollector.RecurringCollectionAgreement memory _rca - ) internal view returns (IRecurringCollector.RecurringCollectionAgreementUpgrade memory) { - IRecurringCollector.RecurringCollectionAgreementUpgrade memory rcau = _ctx.ctxInternal.seed.rcau; + ) internal view returns (IRecurringCollector.RecurringCollectionAgreementUpdate memory) { + IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau = _ctx.ctxInternal.seed.rcau; rcau.agreementId = _rca.agreementId; - rcau.metadata = _encodeUpgradeIndexingAgreementMetadataV1( - _newUpgradeIndexingAgreementMetadataV1( + rcau.metadata = _encodeUpdateIndexingAgreementMetadataV1( + _newUpdateIndexingAgreementMetadataV1( _ctx.ctxInternal.seed.termsV1.tokensPerSecond, _ctx.ctxInternal.seed.termsV1.tokensPerEntityPerSecond ) @@ -326,12 +326,12 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun }); } - function _newUpgradeIndexingAgreementMetadataV1( + function _newUpdateIndexingAgreementMetadataV1( uint256 _tokensPerSecond, uint256 _tokensPerEntityPerSecond - ) internal pure returns (IndexingAgreement.UpgradeIndexingAgreementMetadata memory) { + ) internal pure returns (IndexingAgreement.UpdateIndexingAgreementMetadata memory) { return - IndexingAgreement.UpgradeIndexingAgreementMetadata({ + IndexingAgreement.UpdateIndexingAgreementMetadata({ version: IndexingAgreement.IndexingAgreementVersion.V1, terms: abi.encode( IndexingAgreement.IndexingAgreementTermsV1({ @@ -365,8 +365,8 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun ); } - function _encodeUpgradeIndexingAgreementMetadataV1( - IndexingAgreement.UpgradeIndexingAgreementMetadata memory _t + function _encodeUpdateIndexingAgreementMetadataV1( + IndexingAgreement.UpdateIndexingAgreementMetadata memory _t ) internal pure returns (bytes memory) { return abi.encode(_t); } diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/upgrade.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.sol similarity index 66% rename from packages/subgraph-service/test/unit/subgraphService/indexing-agreement/upgrade.t.sol rename to packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.sol index 910e36b07..6c73b0c25 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/upgrade.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.sol @@ -17,7 +17,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA */ /* solhint-disable graph/func-name-mixedcase */ - function test_SubgraphService_UpgradeIndexingAgreementIndexingAgreement_Revert_WhenPaused( + function test_SubgraphService_UpdateIndexingAgreementIndexingAgreement_Revert_WhenPaused( address operator, IRecurringCollector.SignedRCAU calldata signedRCAU ) public withSafeIndexerOrOperator(operator) { @@ -26,10 +26,10 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA resetPrank(operator); vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - _getSubgraphServiceExtension().upgradeIndexingAgreement(operator, signedRCAU); + _getSubgraphServiceExtension().updateIndexingAgreement(operator, signedRCAU); } - function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenNotAuthorized( + function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenNotAuthorized( address indexer, address notAuthorized, IRecurringCollector.SignedRCAU calldata signedRCAU @@ -42,10 +42,10 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA notAuthorized ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().upgradeIndexingAgreement(indexer, signedRCAU); + _getSubgraphServiceExtension().updateIndexingAgreement(indexer, signedRCAU); } - function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenInvalidProvision( + function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenInvalidProvision( address indexer, uint256 unboundedTokens, IRecurringCollector.SignedRCAU memory signedRCAU @@ -63,10 +63,10 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA maximumProvisionTokens ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().upgradeIndexingAgreement(indexer, signedRCAU); + _getSubgraphServiceExtension().updateIndexingAgreement(indexer, signedRCAU); } - function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenIndexerNotRegistered( + function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenIndexerNotRegistered( address indexer, uint256 unboundedTokens, IRecurringCollector.SignedRCAU memory signedRCAU @@ -81,75 +81,75 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA indexer ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().upgradeIndexingAgreement(indexer, signedRCAU); + _getSubgraphServiceExtension().updateIndexingAgreement(indexer, signedRCAU); } - function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenNotAccepted(Seed memory seed) public { + function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenNotAccepted(Seed memory seed) public { Context storage ctx = _newCtx(seed); IndexerState memory indexerState = _withIndexer(ctx); - IRecurringCollector.SignedRCAU memory acceptableUpgrade = _generateAcceptableSignedRCAU( + IRecurringCollector.SignedRCAU memory acceptableUpdate = _generateAcceptableSignedRCAU( ctx, _generateAcceptableRecurringCollectionAgreement(ctx, indexerState.addr) ); bytes memory expectedErr = abi.encodeWithSelector( IndexingAgreement.IndexingAgreementNotActive.selector, - acceptableUpgrade.rcau.agreementId + acceptableUpdate.rcau.agreementId ); vm.expectRevert(expectedErr); resetPrank(indexerState.addr); - _getSubgraphServiceExtension().upgradeIndexingAgreement(indexerState.addr, acceptableUpgrade); + _getSubgraphServiceExtension().updateIndexingAgreement(indexerState.addr, acceptableUpdate); } - function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenNotAuthorizedForAgreement( + function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenNotAuthorizedForAgreement( Seed memory seed ) public { Context storage ctx = _newCtx(seed); IndexerState memory indexerStateA = _withIndexer(ctx); IndexerState memory indexerStateB = _withIndexer(ctx); IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerStateA); - IRecurringCollector.SignedRCAU memory acceptableUpgrade = _generateAcceptableSignedRCAU(ctx, accepted.rca); + IRecurringCollector.SignedRCAU memory acceptableUpdate = _generateAcceptableSignedRCAU(ctx, accepted.rca); bytes memory expectedErr = abi.encodeWithSelector( IndexingAgreement.IndexingAgreementNotAuthorized.selector, - acceptableUpgrade.rcau.agreementId, + acceptableUpdate.rcau.agreementId, indexerStateB.addr ); vm.expectRevert(expectedErr); resetPrank(indexerStateB.addr); - _getSubgraphServiceExtension().upgradeIndexingAgreement(indexerStateB.addr, acceptableUpgrade); + _getSubgraphServiceExtension().updateIndexingAgreement(indexerStateB.addr, acceptableUpdate); } - function test_SubgraphService_UpgradeIndexingAgreement_Revert_WhenInvalidMetadata(Seed memory seed) public { + function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenInvalidMetadata(Seed memory seed) public { Context storage ctx = _newCtx(seed); IndexerState memory indexerState = _withIndexer(ctx); IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); - IRecurringCollector.RecurringCollectionAgreementUpgrade - memory acceptableUpgrade = _generateAcceptableRecurringCollectionAgreementUpgrade(ctx, accepted.rca); - acceptableUpgrade.metadata = bytes("invalid"); - IRecurringCollector.SignedRCAU memory unacceptableUpgrade = _recurringCollectorHelper.generateSignedRCAU( - acceptableUpgrade, + IRecurringCollector.RecurringCollectionAgreementUpdate + memory acceptableUpdate = _generateAcceptableRecurringCollectionAgreementUpdate(ctx, accepted.rca); + acceptableUpdate.metadata = bytes("invalid"); + IRecurringCollector.SignedRCAU memory unacceptableUpdate = _recurringCollectorHelper.generateSignedRCAU( + acceptableUpdate, ctx.payer.signerPrivateKey ); bytes memory expectedErr = abi.encodeWithSelector( Decoder.DecoderInvalidData.selector, "decodeRCAUMetadata", - unacceptableUpgrade.rcau.metadata + unacceptableUpdate.rcau.metadata ); vm.expectRevert(expectedErr); resetPrank(indexerState.addr); - _getSubgraphServiceExtension().upgradeIndexingAgreement(indexerState.addr, unacceptableUpgrade); + _getSubgraphServiceExtension().updateIndexingAgreement(indexerState.addr, unacceptableUpdate); } - function test_SubgraphService_UpgradeIndexingAgreement_OK(Seed memory seed) public { + function test_SubgraphService_UpdateIndexingAgreement_OK(Seed memory seed) public { Context storage ctx = _newCtx(seed); IndexerState memory indexerState = _withIndexer(ctx); IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); - IRecurringCollector.SignedRCAU memory acceptableUpgrade = _generateAcceptableSignedRCAU(ctx, accepted.rca); + IRecurringCollector.SignedRCAU memory acceptableUpdate = _generateAcceptableSignedRCAU(ctx, accepted.rca); resetPrank(indexerState.addr); - _getSubgraphServiceExtension().upgradeIndexingAgreement(indexerState.addr, acceptableUpgrade); + _getSubgraphServiceExtension().updateIndexingAgreement(indexerState.addr, acceptableUpdate); } /* solhint-enable graph/func-name-mixedcase */ } From b19727b520a2f014044ef7a4780f5b6cc7bb69b9 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 23 May 2025 11:52:09 -0300 Subject: [PATCH 04/90] f: inject currentEpoch --- .../contracts/utilities/GraphDirectory.sol | 8 ------- .../GraphDirectoryImplementation.sol | 4 ++++ .../contracts/SubgraphService.sol | 7 ++++-- .../contracts/libraries/IndexingAgreement.sol | 23 +++++++++++-------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/horizon/contracts/utilities/GraphDirectory.sol b/packages/horizon/contracts/utilities/GraphDirectory.sol index 88322fa10..418b58619 100644 --- a/packages/horizon/contracts/utilities/GraphDirectory.sol +++ b/packages/horizon/contracts/utilities/GraphDirectory.sol @@ -131,14 +131,6 @@ abstract contract GraphDirectory { ); } - /** - * @notice Get the Epoch Manager contract - * @return The Epoch Manager contract - */ - function graphEpochManager() external view returns (IEpochManager) { - return _graphEpochManager(); - } - /** * @notice Get the Graph Token contract * @return The Graph Token contract diff --git a/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol b/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol index b30a5f825..8e06d2875 100644 --- a/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol +++ b/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol @@ -43,6 +43,10 @@ contract GraphDirectoryImplementation is GraphDirectory { return _graphController(); } + function graphEpochManager() external view returns (IEpochManager) { + return _graphEpochManager(); + } + function graphRewardsManager() external view returns (IRewardsManager) { return _graphRewardsManager(); } diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index ae9108b3d..ff4ece331 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -682,8 +682,11 @@ contract SubgraphService is function _collectIndexingFees(bytes16 _agreementId, bytes memory _data) private returns (uint256) { (address indexer, uint256 tokensCollected) = IndexingAgreement._getManager().collect( _allocations, - _agreementId, - _data + IndexingAgreement.CollectParams({ + agreementId: _agreementId, + currentEpoch: _graphEpochManager().currentEpoch(), + data: _data + }) ); _releaseAndLockStake(indexer, tokensCollected); diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index f6ce06616..2c6101a64 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -74,6 +74,12 @@ library IndexingAgreement { uint256 tokensPerEntityPerSecond; } + struct CollectParams { + bytes16 agreementId; + uint256 currentEpoch; + bytes data; + } + bytes32 private constant INDEXING_AGREEMENT_MANAGER_STORAGE_V1_SLOT = keccak256("v1.manager.indexing-agreement"); /** @@ -323,32 +329,31 @@ library IndexingAgreement { function collect( Manager storage self, mapping(address allocationId => Allocation.State allocation) storage allocations, - bytes16 agreementId, - bytes memory data + CollectParams memory params ) external returns (address, uint256) { - AgreementWrapper memory wrapper = _get(self, agreementId); + AgreementWrapper memory wrapper = _get(self, params.agreementId); Allocation.State memory allocation = allocations.requireValidAllocation( wrapper.agreement.allocationId, wrapper.collectorAgreement.serviceProvider ); - require(_isActive(wrapper), IndexingAgreementNotActive(agreementId)); + require(_isActive(wrapper), IndexingAgreementNotActive(params.agreementId)); require( wrapper.agreement.version == IndexingAgreementVersion.V1, InvalidIndexingAgreementVersion(wrapper.agreement.version) ); - (uint256 entities, bytes32 poi, uint256 poiEpoch) = Decoder.decodeCollectIndexingFeeDataV1(data); + (uint256 entities, bytes32 poi, uint256 poiEpoch) = Decoder.decodeCollectIndexingFeeDataV1(params.data); uint256 expectedTokens = (entities == 0 && poi == bytes32(0)) ? 0 - : _tokensToCollect(self, agreementId, wrapper.collectorAgreement, entities); + : _tokensToCollect(self, params.agreementId, wrapper.collectorAgreement, entities); uint256 tokensCollected = _directory().recurringCollector().collect( IGraphPayments.PaymentTypes.IndexingFee, abi.encode( IRecurringCollector.CollectParams({ - agreementId: agreementId, + agreementId: params.agreementId, collectionId: bytes32(uint256(uint160(wrapper.agreement.allocationId))), tokens: expectedTokens, dataServiceCut: 0 @@ -359,10 +364,10 @@ library IndexingAgreement { emit IndexingFeesCollectedV1( wrapper.collectorAgreement.serviceProvider, wrapper.collectorAgreement.payer, - agreementId, + params.agreementId, wrapper.agreement.allocationId, allocation.subgraphDeploymentId, - _graphDirectory().graphEpochManager().currentEpoch(), + params.currentEpoch, tokensCollected, entities, poi, From a3808a02591a2530a4e3e91063aa0d5628a3eedf Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 23 May 2025 16:32:26 -0300 Subject: [PATCH 05/90] f: rename _getAgreementStorage --- .../payments/collectors/RecurringCollector.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index a26780ba1..7257c94be 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -86,7 +86,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC // check that the voucher is signed by the payer (or proxy) _requireAuthorizedRCASigner(signedRCA); - AgreementData storage agreement = _getForUpdateAgreement(signedRCA.rca.agreementId); + AgreementData storage agreement = _getAgreementStorage(signedRCA.rca.agreementId); // check that the agreement is not already accepted require( agreement.state == AgreementState.NotAccepted, @@ -126,7 +126,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC * @dev Caller must be the data service for the agreement. */ function cancel(bytes16 agreementId, CancelAgreementBy by) external { - AgreementData storage agreement = _getForUpdateAgreement(agreementId); + AgreementData storage agreement = _getAgreementStorage(agreementId); require( agreement.state == AgreementState.Accepted, RecurringCollectorAgreementIncorrectState(agreementId, agreement.state) @@ -161,7 +161,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC RecurringCollectorAgreementDeadlineElapsed(signedRCAU.rcau.deadline) ); - AgreementData storage agreement = _getForUpdateAgreement(signedRCAU.rcau.agreementId); + AgreementData storage agreement = _getAgreementStorage(signedRCAU.rcau.agreementId); require( agreement.state == AgreementState.Accepted, RecurringCollectorAgreementIncorrectState(signedRCAU.rcau.agreementId, agreement.state) @@ -248,7 +248,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC * @return The amount of tokens collected */ function _collect(CollectParams memory _params) private returns (uint256) { - AgreementData storage agreement = _getForUpdateAgreement(_params.agreementId); + AgreementData storage agreement = _getAgreementStorage(_params.agreementId); require( agreement.state == AgreementState.Accepted || agreement.state == AgreementState.CanceledByPayer, RecurringCollectorAgreementIncorrectState(_params.agreementId, agreement.state) @@ -456,7 +456,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @notice Gets an agreement to be updated. */ - function _getForUpdateAgreement(bytes16 _agreementId) private view returns (AgreementData storage) { + function _getAgreementStorage(bytes16 _agreementId) private view returns (AgreementData storage) { return agreements[_agreementId]; } From 5633395bb7a5fc509abc55e80819609c4596d642 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 23 May 2025 16:34:25 -0300 Subject: [PATCH 06/90] f: remove ternary --- .../contracts/payments/collectors/RecurringCollector.sol | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index 7257c94be..b8e8e7f78 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -136,9 +136,11 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC RecurringCollectorDataServiceNotAuthorized(agreementId, msg.sender) ); agreement.canceledAt = uint64(block.timestamp); - agreement.state = by == CancelAgreementBy.Payer - ? AgreementState.CanceledByPayer - : AgreementState.CanceledByServiceProvider; + if (by == CancelAgreementBy.Payer) { + agreement.state = AgreementState.CanceledByPayer; + } else { + agreement.state = AgreementState.CanceledByServiceProvider; + } emit AgreementCanceled( agreement.dataService, From 30acf7514cc109ad8c9e5a4a408272117bf7aab4 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 23 May 2025 16:44:22 -0300 Subject: [PATCH 07/90] f: rename _encodeRCAU --- .../interfaces/IRecurringCollector.sol | 4 ++-- .../collectors/RecurringCollector.sol | 24 +++++++++---------- .../RecurringCollectorHelper.t.sol | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index 19beda438..4320efbb6 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -317,14 +317,14 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { * @param rca The RCA for which to compute the hash. * @return The hash of the RCA. */ - function encodeRCA(RecurringCollectionAgreement calldata rca) external view returns (bytes32); + function hashRCA712(RecurringCollectionAgreement calldata rca) external view returns (bytes32); /** * @dev Computes the hash of a RecurringCollectionAgreementUpdate (RCAU). * @param rcau The RCAU for which to compute the hash. * @return The hash of the RCAU. */ - function encodeRCAU(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32); + function hashRCAU712(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32); /** * @dev Recovers the signer address of a signed RecurringCollectionAgreement (RCA). diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index b8e8e7f78..72eff9930 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -213,17 +213,17 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** - * @notice See {IRecurringCollector.encodeRCA} + * @notice See {IRecurringCollector.hashRCA712} */ - function encodeRCA(RecurringCollectionAgreement calldata rca) external view returns (bytes32) { - return _encodeRCA(rca); + function hashRCA712(RecurringCollectionAgreement calldata rca) external view returns (bytes32) { + return _hashRCA712(rca); } /** - * @notice See {IRecurringCollector.encodeRCAU} + * @notice See {IRecurringCollector.hashRCAU712} */ - function encodeRCAU(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32) { - return _encodeRCAU(rcau); + function hashRCAU712(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32) { + return _hashRCAU712(rcau); } /** @@ -371,7 +371,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC * @notice See {IRecurringCollector.recoverRCASigner} */ function _recoverRCASigner(SignedRCA memory _signedRCA) private view returns (address) { - bytes32 messageHash = _encodeRCA(_signedRCA.rca); + bytes32 messageHash = _hashRCA712(_signedRCA.rca); return ECDSA.recover(messageHash, _signedRCA.signature); } @@ -379,14 +379,14 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC * @notice See {IRecurringCollector.recoverRCAUSigner} */ function _recoverRCAUSigner(SignedRCAU memory _signedRCAU) private view returns (address) { - bytes32 messageHash = _encodeRCAU(_signedRCAU.rcau); + bytes32 messageHash = _hashRCAU712(_signedRCAU.rcau); return ECDSA.recover(messageHash, _signedRCAU.signature); } /** - * @notice See {IRecurringCollector.encodeRCA} + * @notice See {IRecurringCollector.hashRCA712} */ - function _encodeRCA(RecurringCollectionAgreement memory _rca) private view returns (bytes32) { + function _hashRCA712(RecurringCollectionAgreement memory _rca) private view returns (bytes32) { return _hashTypedDataV4( keccak256( @@ -409,9 +409,9 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** - * @notice See {IRecurringCollector.encodeRCAU} + * @notice See {IRecurringCollector.hashRCAU712} */ - function _encodeRCAU(RecurringCollectionAgreementUpdate memory _rcau) private view returns (bytes32) { + function _hashRCAU712(RecurringCollectionAgreementUpdate memory _rcau) private view returns (bytes32) { return _hashTypedDataV4( keccak256( diff --git a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol index 7fa49150f..6abfc9f99 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol @@ -19,7 +19,7 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder { IRecurringCollector.RecurringCollectionAgreement memory rca, uint256 signerPrivateKey ) public view returns (IRecurringCollector.SignedRCA memory) { - bytes32 messageHash = collector.encodeRCA(rca); + bytes32 messageHash = collector.hashRCA712(rca); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); bytes memory signature = abi.encodePacked(r, s, v); IRecurringCollector.SignedRCA memory signedRCA = IRecurringCollector.SignedRCA({ @@ -34,7 +34,7 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder { IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau, uint256 signerPrivateKey ) public view returns (IRecurringCollector.SignedRCAU memory) { - bytes32 messageHash = collector.encodeRCAU(rcau); + bytes32 messageHash = collector.hashRCAU712(rcau); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); bytes memory signature = abi.encodePacked(r, s, v); IRecurringCollector.SignedRCAU memory signedRCAU = IRecurringCollector.SignedRCAU({ From 713b4d97317b69c8f5dec01c497b63a8297a4c11 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 08:54:36 -0300 Subject: [PATCH 08/90] f: remove comment --- .../subgraph-service/contracts/libraries/IndexingAgreement.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 2c6101a64..ae3888089 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -431,7 +431,6 @@ library IndexingAgreement { uint256 collectionSeconds = block.timestamp; collectionSeconds -= _agreement.lastCollectionAt > 0 ? _agreement.lastCollectionAt : _agreement.acceptedAt; - // FIX-ME: this is bad because it encourages indexers to collect at max seconds allowed to maximize collection. return collectionSeconds * (termsV1.tokensPerSecond + termsV1.tokensPerEntityPerSecond * _entities); } From 939d9bbf97d29d2ddfa2ec6b5c4c805eb046beda Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 09:17:41 -0300 Subject: [PATCH 09/90] f: use erc7201 --- .../contracts/libraries/IndexingAgreement.sol | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index ae3888089..5200dd84c 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -21,12 +21,6 @@ library IndexingAgreement { V1 } - struct Manager { - mapping(bytes16 => State) agreements; - mapping(bytes16 agreementId => IndexingAgreementTermsV1 data) termsV1; - mapping(address allocationId => bytes16 agreementId) allocationToActiveAgreementId; - } - /** * @notice Indexer Agreement Data * @param allocationId The allocation ID @@ -80,7 +74,16 @@ library IndexingAgreement { bytes data; } - bytes32 private constant INDEXING_AGREEMENT_MANAGER_STORAGE_V1_SLOT = keccak256("v1.manager.indexing-agreement"); + /// @custom:storage-location erc7201:graphprotocol.subgraph-service.storage.Manager.IndexingAgreement + struct Manager { + mapping(bytes16 => State) agreements; + mapping(bytes16 agreementId => IndexingAgreementTermsV1 data) termsV1; + mapping(address allocationId => bytes16 agreementId) allocationToActiveAgreementId; + } + + // keccak256(abi.encode(uint256(keccak256("graphprotocol.subgraph-service.storage.Manager.IndexingAgreement")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant INDEXING_AGREEMENT_MANAGER_STORAGE_LOCATION = + 0xfdb6fb5d1a390e01387ce73642e517880d8e0fedd0e7e26ac9194788a7a85200; /** * @notice Emitted when an indexer collects indexing fees from a V1 agreement @@ -384,12 +387,10 @@ library IndexingAgreement { return wrapper; } - function _getManager() internal pure returns (Manager storage manager) { - bytes32 slot = INDEXING_AGREEMENT_MANAGER_STORAGE_V1_SLOT; - + function _getManager() internal pure returns (Manager storage $) { // solhint-disable-next-line no-inline-assembly assembly { - manager.slot := slot + $.slot := INDEXING_AGREEMENT_MANAGER_STORAGE_LOCATION } } From 05b50f283c7f9fdff3730ce2a2db36acc0bd2c6a Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 10:01:54 -0300 Subject: [PATCH 10/90] f: cleanup modifiers --- .../libraries/ProvisionManagerLib.sol | 19 +++++++++++++++++ .../utilities/ProvisionManager.sol | 20 +++++++----------- .../contracts/SubgraphService.sol | 20 +++++++----------- .../contracts/SubgraphServiceExtension.sol | 21 +++++++++++++++---- 4 files changed, 51 insertions(+), 29 deletions(-) create mode 100644 packages/horizon/contracts/data-service/libraries/ProvisionManagerLib.sol diff --git a/packages/horizon/contracts/data-service/libraries/ProvisionManagerLib.sol b/packages/horizon/contracts/data-service/libraries/ProvisionManagerLib.sol new file mode 100644 index 000000000..3e821c4b2 --- /dev/null +++ b/packages/horizon/contracts/data-service/libraries/ProvisionManagerLib.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IHorizonStaking } from "../../interfaces/IHorizonStaking.sol"; +import { ProvisionManager } from "../utilities/ProvisionManager.sol"; + +library ProvisionManagerLib { + function requireAuthorizedForProvision( + IHorizonStaking graphStaking, + address serviceProvider, + address dataService, + address operator + ) external view { + require( + graphStaking.isAuthorized(serviceProvider, dataService, operator), + ProvisionManager.ProvisionManagerNotAuthorized(serviceProvider, operator) + ); + } +} diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol index 1aeb8f7c6..eab23bcae 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -9,6 +9,7 @@ import { PPMMath } from "../../libraries/PPMMath.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; import { ProvisionManagerV1Storage } from "./ProvisionManagerStorage.sol"; +import { ProvisionManagerLib } from "../libraries/ProvisionManagerLib.sol"; /** * @title ProvisionManager contract @@ -111,18 +112,7 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa * @param serviceProvider The address of the service provider. */ modifier onlyAuthorizedForProvision(address serviceProvider) { - require( - _graphStaking().isAuthorized(serviceProvider, address(this), msg.sender), - ProvisionManagerNotAuthorized(serviceProvider, msg.sender) - ); - _; - } - - modifier onlyAuthorizedForProvisionHack(address caller, address serviceProvider) { - require( - _graphStaking().isAuthorized(serviceProvider, address(this), caller), - ProvisionManagerNotAuthorized(serviceProvider, caller) - ); + ProvisionManagerLib.requireAuthorizedForProvision(_graphStaking(), serviceProvider, address(this), msg.sender); _; } @@ -132,10 +122,14 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa * @param serviceProvider The address of the service provider. */ modifier onlyValidProvision(address serviceProvider) virtual { + this.requireValidProvision(serviceProvider); + _; + } + + function requireValidProvision(address serviceProvider) external view { IHorizonStaking.Provision memory provision = _getProvision(serviceProvider); _checkProvisionTokens(provision); _checkProvisionParameters(provision, false); - _; } /** diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index ff4ece331..5dd3fa806 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -56,10 +56,14 @@ contract SubgraphService is * @param indexer The address of the indexer */ modifier onlyRegisteredIndexer(address indexer) { - require(indexers[indexer].registeredAt != 0, SubgraphServiceIndexerNotRegistered(indexer)); + this.requireRegisteredIndexer(indexer); _; } + function requireRegisteredIndexer(address indexer) external view { + require(indexers[indexer].registeredAt != 0, SubgraphServiceIndexerNotRegistered(indexer)); + } + /** * @notice Constructor for the SubgraphService contract * @dev DataService and Directory constructors set a bunch of immutable variables @@ -706,17 +710,9 @@ contract SubgraphService is } } - function modifiersHack( - address caller, - address indexer - ) - external - view - whenNotPaused - onlyAuthorizedForProvisionHack(caller, indexer) - onlyValidProvision(indexer) - onlyRegisteredIndexer(indexer) - {} + function getGraphStaking() external view returns (address) { + return address(_graphStaking()); + } /** * @notice Delegates the call to the SubgraphServiceExtension implementation. diff --git a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol index a9616357e..cb5995bd5 100644 --- a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol +++ b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.27; import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; +import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; +import { ProvisionManagerLib } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionManagerLib.sol"; import { IndexingAgreement } from "./libraries/IndexingAgreement.sol"; import { SubgraphService } from "./SubgraphService.sol"; @@ -10,15 +12,22 @@ import { SubgraphService } from "./SubgraphService.sol"; contract SubgraphServiceExtension is PausableUpgradeable { using IndexingAgreement for IndexingAgreement.Manager; - modifier modifiersHack(address indexer) { - SubgraphService(address(this)).modifiersHack(msg.sender, indexer); + modifier onlyValid(address indexer) { + ProvisionManagerLib.requireAuthorizedForProvision( + IHorizonStaking(_getBase().getGraphStaking()), + indexer, + address(this), + msg.sender + ); + _getBase().requireValidProvision(indexer); + _getBase().requireRegisteredIndexer(indexer); _; } function updateIndexingAgreement( address indexer, IRecurringCollector.SignedRCAU calldata signedRCAU - ) external modifiersHack(indexer) { + ) external whenNotPaused onlyValid(indexer) { IndexingAgreement._getManager().update(indexer, signedRCAU); } @@ -38,7 +47,7 @@ contract SubgraphServiceExtension is PausableUpgradeable { * * @param agreementId The id of the agreement */ - function cancelIndexingAgreement(address indexer, bytes16 agreementId) external modifiersHack(indexer) { + function cancelIndexingAgreement(address indexer, bytes16 agreementId) external whenNotPaused onlyValid(indexer) { IndexingAgreement._getManager().cancel(indexer, agreementId); } @@ -67,4 +76,8 @@ contract SubgraphServiceExtension is PausableUpgradeable { function _cancelAllocationIndexingAgreement(address _allocationId) internal { IndexingAgreement._getManager().cancelForAllocation(_allocationId); } + + function _getBase() internal view returns (SubgraphService) { + return SubgraphService(address(this)); + } } From 211cc0024c83968d42d7fdfc609483caa64b45c7 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 10:07:31 -0300 Subject: [PATCH 11/90] f: rename _hashRCA*712 --- .../interfaces/IRecurringCollector.sol | 4 ++-- .../collectors/RecurringCollector.sol | 24 +++++++++---------- .../RecurringCollectorHelper.t.sol | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index 4320efbb6..0599167e2 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -317,14 +317,14 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { * @param rca The RCA for which to compute the hash. * @return The hash of the RCA. */ - function hashRCA712(RecurringCollectionAgreement calldata rca) external view returns (bytes32); + function hashRCA(RecurringCollectionAgreement calldata rca) external view returns (bytes32); /** * @dev Computes the hash of a RecurringCollectionAgreementUpdate (RCAU). * @param rcau The RCAU for which to compute the hash. * @return The hash of the RCAU. */ - function hashRCAU712(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32); + function hashRCAU(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32); /** * @dev Recovers the signer address of a signed RecurringCollectionAgreement (RCA). diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index 72eff9930..c3fce4c30 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -213,17 +213,17 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** - * @notice See {IRecurringCollector.hashRCA712} + * @notice See {IRecurringCollector.hashRCA} */ - function hashRCA712(RecurringCollectionAgreement calldata rca) external view returns (bytes32) { - return _hashRCA712(rca); + function hashRCA(RecurringCollectionAgreement calldata rca) external view returns (bytes32) { + return _hashRCA(rca); } /** - * @notice See {IRecurringCollector.hashRCAU712} + * @notice See {IRecurringCollector.hashRCAU} */ - function hashRCAU712(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32) { - return _hashRCAU712(rcau); + function hashRCAU(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32) { + return _hashRCAU(rcau); } /** @@ -371,7 +371,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC * @notice See {IRecurringCollector.recoverRCASigner} */ function _recoverRCASigner(SignedRCA memory _signedRCA) private view returns (address) { - bytes32 messageHash = _hashRCA712(_signedRCA.rca); + bytes32 messageHash = _hashRCA(_signedRCA.rca); return ECDSA.recover(messageHash, _signedRCA.signature); } @@ -379,14 +379,14 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC * @notice See {IRecurringCollector.recoverRCAUSigner} */ function _recoverRCAUSigner(SignedRCAU memory _signedRCAU) private view returns (address) { - bytes32 messageHash = _hashRCAU712(_signedRCAU.rcau); + bytes32 messageHash = _hashRCAU(_signedRCAU.rcau); return ECDSA.recover(messageHash, _signedRCAU.signature); } /** - * @notice See {IRecurringCollector.hashRCA712} + * @notice See {IRecurringCollector.hashRCA} */ - function _hashRCA712(RecurringCollectionAgreement memory _rca) private view returns (bytes32) { + function _hashRCA(RecurringCollectionAgreement memory _rca) private view returns (bytes32) { return _hashTypedDataV4( keccak256( @@ -409,9 +409,9 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** - * @notice See {IRecurringCollector.hashRCAU712} + * @notice See {IRecurringCollector.hashRCAU} */ - function _hashRCAU712(RecurringCollectionAgreementUpdate memory _rcau) private view returns (bytes32) { + function _hashRCAU(RecurringCollectionAgreementUpdate memory _rcau) private view returns (bytes32) { return _hashTypedDataV4( keccak256( diff --git a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol index 6abfc9f99..7dd9702c3 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol @@ -19,7 +19,7 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder { IRecurringCollector.RecurringCollectionAgreement memory rca, uint256 signerPrivateKey ) public view returns (IRecurringCollector.SignedRCA memory) { - bytes32 messageHash = collector.hashRCA712(rca); + bytes32 messageHash = collector.hashRCA(rca); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); bytes memory signature = abi.encodePacked(r, s, v); IRecurringCollector.SignedRCA memory signedRCA = IRecurringCollector.SignedRCA({ @@ -34,7 +34,7 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder { IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau, uint256 signerPrivateKey ) public view returns (IRecurringCollector.SignedRCAU memory) { - bytes32 messageHash = collector.hashRCAU712(rcau); + bytes32 messageHash = collector.hashRCAU(rcau); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); bytes memory signature = abi.encodePacked(r, s, v); IRecurringCollector.SignedRCAU memory signedRCAU = IRecurringCollector.SignedRCAU({ From 3a4bdcccb0751c14f97f8c61d877095af8f457ca Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 10:13:41 -0300 Subject: [PATCH 12/90] f: add to collect() NatSpec --- packages/subgraph-service/contracts/SubgraphService.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 5dd3fa806..c32bbc51d 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -247,7 +247,7 @@ contract SubgraphService is /** * @notice Collects payment for the service provided by the indexer - * Allows collecting different types of payments such as query fees and indexing rewards. + * Allows collecting different types of payments such as query fees, indexing rewards and indexining fees. * It uses Graph Horizon payments protocol to process payments. * Reverts if the payment type is not supported. * @dev This function is the equivalent of the `collect` function for query fees and the `closeAllocation` function @@ -261,6 +261,7 @@ contract SubgraphService is * * For query fees, see {SubgraphService-_collectQueryFees} for more details. * For indexing rewards, see {AllocationManager-_collectIndexingRewards} for more details. + * For indexing fees, see {SubgraphService-_collectIndexingFees} for more details. * * @param indexer The address of the indexer * @param paymentType The type of payment to collect as defined in {IGraphPayments} @@ -271,6 +272,9 @@ contract SubgraphService is * - address `allocationId`: The id of the allocation * - bytes32 `poi`: The POI being presented * - bytes `poiMetadata`: The metadata associated with the POI. See {AllocationManager-_collectIndexingRewards} for more details. + * - For indexing fees: + * - bytes16 `agreementId`: The id of the indexing agreement + * - bytes `agreementCollectionMetadata`: The metadata required by the indexing agreement version. */ /// @inheritdoc IDataService function collect( From a6433f58cb6ce5e589601f30732c120d76debaff Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 11:08:47 -0300 Subject: [PATCH 13/90] f: add blockNumber for disputes --- .../subgraph-service/contracts/DisputeManager.sol | 11 +++++++---- .../contracts/interfaces/IDisputeManager.sol | 8 +++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 9f39ab49f..fad0eec28 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -138,13 +138,14 @@ contract DisputeManager is function createIndexingFeeDisputeV1( bytes16 agreementId, bytes32 poi, - uint256 entities + uint256 entities, + uint256 blockNumber ) external override returns (bytes32) { // Get funds from fisherman _graphToken().pullTokens(msg.sender, disputeDeposit); // Create a dispute - return _createIndexingFeeDisputeV1(msg.sender, disputeDeposit, agreementId, poi, entities); + return _createIndexingFeeDisputeV1(msg.sender, disputeDeposit, agreementId, poi, entities, blockNumber); } /// @inheritdoc IDisputeManager @@ -530,7 +531,8 @@ contract DisputeManager is uint256 _deposit, bytes16 _agreementId, bytes32 _poi, - uint256 _entities + uint256 _entities, + uint256 _blockNumber ) private returns (bytes32) { IndexingAgreement.AgreementWrapper memory wrapper = _getSubgraphServiceExtension().getIndexingAgreement( _agreementId @@ -554,7 +556,8 @@ contract DisputeManager is wrapper.collectorAgreement.serviceProvider, wrapper.collectorAgreement.payer, _poi, - _entities + _entities, + _blockNumber ) ); diff --git a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol index 8f271bf94..194b14a21 100644 --- a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol +++ b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol @@ -550,9 +550,15 @@ interface IDisputeManager { * @param agreementId The indexing agreement to dispute * @param poi The Proof of Indexing (POI) being disputed * @param entities The number of entities disputed + * @param blockNumber The block number at which the indexing fee was collected * @return The dispute id */ - function createIndexingFeeDisputeV1(bytes16 agreementId, bytes32 poi, uint256 entities) external returns (bytes32); + function createIndexingFeeDisputeV1( + bytes16 agreementId, + bytes32 poi, + uint256 entities, + uint256 blockNumber + ) external returns (bytes32); // -- Arbitrator -- From 5e4c6d3a152cd675f0dd9e75e1eb379b3e4f17cd Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 11:31:07 -0300 Subject: [PATCH 14/90] f: fix underflow --- .../horizon/contracts/interfaces/IRecurringCollector.sol | 8 ++++++++ .../contracts/payments/collectors/RecurringCollector.sol | 7 +++++-- .../test/unit/payments/recurring-collector/collect.t.sol | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index 0599167e2..4d78e0958 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -259,6 +259,14 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { */ error RecurringCollectorInvalidCollectData(bytes invalidData); + /** + * Thrown when calling collect() on a payer canceled agreement + * where the final collection has already been done + * @param agreementId The agreement ID + * @param finalCollectionAt The timestamp when the final collection was done + */ + error RecurringCollectorFinalCollectionDone(bytes16 agreementId, uint256 finalCollectionAt); + /** * Thrown when interacting with an agreement that has an incorrect state * @param agreementId The agreement ID diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index c3fce4c30..03aa586c9 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -340,10 +340,13 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC bytes16 _agreementId, uint256 _tokens ) private view returns (uint256) { - uint256 collectionSeconds = _agreement.state == AgreementState.CanceledByPayer + // if canceled by the payer allow collection up to the cancelation time + uint256 collectionEnd = _agreement.state == AgreementState.CanceledByPayer ? _agreement.canceledAt : block.timestamp; - collectionSeconds -= _agreementCollectionStartAt(_agreement); + uint256 collectionStart = _agreementCollectionStartAt(_agreement); + require(collectionEnd > collectionStart, RecurringCollectorFinalCollectionDone(_agreementId, collectionStart)); + uint256 collectionSeconds = collectionEnd - collectionStart; require( collectionSeconds >= _agreement.minSecondsPerCollection, RecurringCollectorCollectionTooSoon( diff --git a/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol b/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol index de4535e31..65efe85a7 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol @@ -142,7 +142,7 @@ contract RecurringCollectorCollectTest is RecurringCollectorSharedTest { vm.prank(accepted.rca.dataService); _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); - uint256 collectionSeconds = boundSkipCeil(unboundedCollectionSeconds, accepted.rca.minSecondsPerCollection - 1); + uint256 collectionSeconds = boundSkip(unboundedCollectionSeconds, 1, accepted.rca.minSecondsPerCollection - 1); skip(collectionSeconds); IRecurringCollector.CollectParams memory collectParams = _generateCollectParams( From c423fe511b4bbaca4761875ed11e83ae39508952 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 13:41:37 -0300 Subject: [PATCH 15/90] f: add MIN_SECONDS_COLLECTION_WINDOW --- .../payments/collectors/RecurringCollector.sol | 9 ++++++--- .../RecurringCollectorHelper.t.sol | 13 ++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index 03aa586c9..d003914d5 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -21,6 +21,8 @@ import { MathUtils } from "../../libraries/MathUtils.sol"; contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringCollector { using PPMMath for uint256; + uint32 public constant MIN_SECONDS_COLLECTION_WINDOW = 600; + /// @notice The EIP712 typehash for the RecurringCollectionAgreement struct bytes32 public constant EIP712_RCA_TYPEHASH = keccak256( @@ -318,16 +320,17 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC RecurringCollectorAgreementInvalidParameters("endsAt not in future") ); - // Collection window needs to be at least 2 hours + // Collection window needs to be at least MIN_SECONDS_COLLECTION_WINDOW require( _agreement.maxSecondsPerCollection > _agreement.minSecondsPerCollection && - (_agreement.maxSecondsPerCollection - _agreement.minSecondsPerCollection >= 7200), + (_agreement.maxSecondsPerCollection - _agreement.minSecondsPerCollection >= + MIN_SECONDS_COLLECTION_WINDOW), RecurringCollectorAgreementInvalidParameters("too small collection window") ); // Agreement needs to last at least one min collection window require( - _agreement.endsAt - block.timestamp >= _agreement.minSecondsPerCollection + 7200, + _agreement.endsAt - block.timestamp >= _agreement.minSecondsPerCollection + MIN_SECONDS_COLLECTION_WINDOW, RecurringCollectorAgreementInvalidParameters("too small agreement window") ); } diff --git a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol index 7dd9702c3..b292f3a89 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol @@ -103,7 +103,10 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder { } function _sensibleDeadline(uint256 _seed) internal view returns (uint64) { - return uint64(bound(_seed, block.timestamp + 1, block.timestamp + 7200)); // between now and 2h + return + uint64( + bound(_seed, block.timestamp + 1, block.timestamp + uint256(collector.MIN_SECONDS_COLLECTION_WINDOW())) + ); // between now and +MIN_SECONDS_COLLECTION_WINDOW } function _sensibleEndsAt(uint256 _seed, uint32 _maxSecondsPerCollection) internal view returns (uint64) { @@ -132,10 +135,14 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder { function _sensibleMaxSecondsPerCollection( uint32 _seed, uint32 _minSecondsPerCollection - ) internal pure returns (uint32) { + ) internal view returns (uint32) { return uint32( - bound(_seed, _minSecondsPerCollection + 7200, 60 * 60 * 24 * 30) // between minSecondsPerCollection + 2h and 30 days + bound( + _seed, + _minSecondsPerCollection + uint256(collector.MIN_SECONDS_COLLECTION_WINDOW()), + 60 * 60 * 24 * 30 + ) // between minSecondsPerCollection + 2h and 30 days ); } } From 9f41c45b88c755635ca18ad0af1a3e84f9496b88 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 30 May 2025 15:25:44 -0300 Subject: [PATCH 16/90] f: linter --- .../RecurringCollectorHelper.t.sol | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol index b292f3a89..b3ccbc3b8 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol @@ -120,18 +120,6 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder { ); // between 10 and 1M max collections } - function _sensibleMaxInitialTokens(uint256 _seed) internal pure returns (uint256) { - return bound(_seed, 0, 1e18 * 100_000_000); // between 0 and 100M tokens - } - - function _sensibleMaxOngoingTokensPerSecond(uint256 _seed) internal pure returns (uint256) { - return bound(_seed, 1, 1e18); // between 1 and 1e18 tokens per second - } - - function _sensibleMinSecondsPerCollection(uint32 _seed) internal pure returns (uint32) { - return uint32(bound(_seed, 10 * 60, 24 * 60 * 60)); // between 10 min and 24h - } - function _sensibleMaxSecondsPerCollection( uint32 _seed, uint32 _minSecondsPerCollection @@ -145,4 +133,16 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder { ) // between minSecondsPerCollection + 2h and 30 days ); } + + function _sensibleMaxInitialTokens(uint256 _seed) internal pure returns (uint256) { + return bound(_seed, 0, 1e18 * 100_000_000); // between 0 and 100M tokens + } + + function _sensibleMaxOngoingTokensPerSecond(uint256 _seed) internal pure returns (uint256) { + return bound(_seed, 1, 1e18); // between 1 and 1e18 tokens per second + } + + function _sensibleMinSecondsPerCollection(uint32 _seed) internal pure returns (uint32) { + return uint32(bound(_seed, 10 * 60, 24 * 60 * 60)); // between 10 min and 24h + } } From 15ed5df2af6941070d9bf1dcdb3effcd7cd6e033 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 23 May 2025 09:07:05 -0300 Subject: [PATCH 17/90] HACK: make lib external --- .../subgraph-service/contracts/libraries/Allocation.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/Allocation.sol b/packages/subgraph-service/contracts/libraries/Allocation.sol index 6f6563068..b4043f5b9 100644 --- a/packages/subgraph-service/contracts/libraries/Allocation.sol +++ b/packages/subgraph-service/contracts/libraries/Allocation.sol @@ -76,7 +76,7 @@ library Allocation { uint256 tokens, uint256 accRewardsPerAllocatedToken, uint256 createdAtEpoch - ) internal returns (State memory) { + ) external returns (State memory) { require(!self[allocationId].exists(), AllocationAlreadyExists(allocationId)); State memory allocation = State({ @@ -104,7 +104,7 @@ library Allocation { * @param self The allocation list mapping * @param allocationId The allocation id */ - function presentPOI(mapping(address => State) storage self, address allocationId) internal { + function presentPOI(mapping(address => State) storage self, address allocationId) external { State storage allocation = _get(self, allocationId); require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); allocation.lastPOIPresentedAt = block.timestamp; @@ -135,7 +135,7 @@ library Allocation { * @param self The allocation list mapping * @param allocationId The allocation id */ - function clearPendingRewards(mapping(address => State) storage self, address allocationId) internal { + function clearPendingRewards(mapping(address => State) storage self, address allocationId) external { State storage allocation = _get(self, allocationId); require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); allocation.accRewardsPending = 0; @@ -148,7 +148,7 @@ library Allocation { * @param self The allocation list mapping * @param allocationId The allocation id */ - function close(mapping(address => State) storage self, address allocationId) internal { + function close(mapping(address => State) storage self, address allocationId) external { State storage allocation = _get(self, allocationId); require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); allocation.closedAt = block.timestamp; From ad3204e3584ab700813a74e660ac74a059d0b6de Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 13:49:33 -0300 Subject: [PATCH 18/90] HACK: more external libs --- .../contracts/data-service/libraries/ProvisionTracker.sol | 2 +- packages/horizon/contracts/libraries/LinkedList.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol index 2fe271833..e0ccfa645 100644 --- a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol +++ b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol @@ -73,7 +73,7 @@ library ProvisionTracker { IHorizonStaking graphStaking, address serviceProvider, uint32 delegationRatio - ) internal view returns (bool) { + ) external view returns (bool) { uint256 tokensAvailable = graphStaking.getTokensAvailable(serviceProvider, address(this), delegationRatio); return self[serviceProvider] <= tokensAvailable; } diff --git a/packages/horizon/contracts/libraries/LinkedList.sol b/packages/horizon/contracts/libraries/LinkedList.sol index af0f1dad9..1edebd783 100644 --- a/packages/horizon/contracts/libraries/LinkedList.sol +++ b/packages/horizon/contracts/libraries/LinkedList.sol @@ -72,7 +72,7 @@ library LinkedList { * @param self The list metadata * @param id The id of the item to add */ - function addTail(List storage self, bytes32 id) internal { + function addTail(List storage self, bytes32 id) external { require(self.count < MAX_ITEMS, LinkedListMaxElementsExceeded()); require(id != bytes32(0), LinkedListInvalidZeroId()); self.tail = id; From f7441d6db93664e39ba3cf569aac7d4ec6d2ba1a Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 14:42:10 -0300 Subject: [PATCH 19/90] HACK: DataServiceFeesLib --- .../extensions/DataServiceFees.sol | 106 ++++++++++-------- .../libraries/DataServiceFeesLib.sol | 103 +++++++++++++++++ 2 files changed, 160 insertions(+), 49 deletions(-) create mode 100644 packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol index a1c38a99a..1b0766ba7 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -5,6 +5,7 @@ import { IDataServiceFees } from "../interfaces/IDataServiceFees.sol"; import { ProvisionTracker } from "../libraries/ProvisionTracker.sol"; import { LinkedList } from "../../libraries/LinkedList.sol"; +import { DataServiceFeesLib } from "../libraries/DataServiceFeesLib.sol"; import { DataService } from "../DataService.sol"; import { DataServiceFeesV1Storage } from "./DataServiceFeesStorage.sol"; @@ -41,23 +42,34 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat * @param _unlockTimestamp The timestamp when the tokens can be released */ function _lockStake(address _serviceProvider, uint256 _tokens, uint256 _unlockTimestamp) internal { - require(_tokens != 0, DataServiceFeesZeroTokens()); - feesProvisionTracker.lock(_graphStaking(), _serviceProvider, _tokens, _delegationRatio); - - LinkedList.List storage claimsList = claimsLists[_serviceProvider]; - - // Save item and add to list - bytes32 claimId = _buildStakeClaimId(_serviceProvider, claimsList.nonce); - claims[claimId] = StakeClaim({ - tokens: _tokens, - createdAt: block.timestamp, - releasableAt: _unlockTimestamp, - nextClaim: bytes32(0) - }); - if (claimsList.count != 0) claims[claimsList.tail].nextClaim = claimId; - claimsList.addTail(claimId); - - emit StakeClaimLocked(_serviceProvider, claimId, _tokens, _unlockTimestamp); + // require(_tokens != 0, DataServiceFeesZeroTokens()); + // feesProvisionTracker.lock(_graphStaking(), _serviceProvider, _tokens, _delegationRatio); + + // LinkedList.List storage claimsList = claimsLists[_serviceProvider]; + + // // Save item and add to list + // bytes32 claimId = _buildStakeClaimId(_serviceProvider, claimsList.nonce); + // claims[claimId] = StakeClaim({ + // tokens: _tokens, + // createdAt: block.timestamp, + // releasableAt: _unlockTimestamp, + // nextClaim: bytes32(0) + // }); + // if (claimsList.count != 0) claims[claimsList.tail].nextClaim = claimId; + // claimsList.addTail(claimId); + + // emit StakeClaimLocked(_serviceProvider, claimId, _tokens, _unlockTimestamp); + + DataServiceFeesLib.lockStake( + _delegationRatio, + feesProvisionTracker, + claims, + claimsLists, + _graphStaking(), + _serviceProvider, + _tokens, + _unlockTimestamp + ); } /** @@ -92,23 +104,25 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat * @return The updated accumulator data */ function _processStakeClaim(bytes32 _claimId, bytes memory _acc) private returns (bool, bytes memory) { - StakeClaim memory claim = _getStakeClaim(_claimId); + // StakeClaim memory claim = _getStakeClaim(_claimId); - // early exit - if (claim.releasableAt > block.timestamp) { - return (true, LinkedList.NULL_BYTES); - } + // // early exit + // if (claim.releasableAt > block.timestamp) { + // return (true, LinkedList.NULL_BYTES); + // } - // decode - (uint256 tokensClaimed, address serviceProvider) = abi.decode(_acc, (uint256, address)); + // // decode + // (uint256 tokensClaimed, address serviceProvider) = abi.decode(_acc, (uint256, address)); - // process - feesProvisionTracker.release(serviceProvider, claim.tokens); - emit StakeClaimReleased(serviceProvider, _claimId, claim.tokens, claim.releasableAt); + // // process + // feesProvisionTracker.release(serviceProvider, claim.tokens); + // emit StakeClaimReleased(serviceProvider, _claimId, claim.tokens, claim.releasableAt); - // encode - _acc = abi.encode(tokensClaimed + claim.tokens, serviceProvider); - return (false, _acc); + // // encode + // _acc = abi.encode(tokensClaimed + claim.tokens, serviceProvider); + // return (false, _acc); + + return DataServiceFeesLib.processStakeClaim(feesProvisionTracker, claims, _claimId, _acc); } /** @@ -120,16 +134,16 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat delete claims[_claimId]; } - /** - * @notice Gets the details of a stake claim - * @param _claimId The ID of the stake claim - * @return The stake claim details - */ - function _getStakeClaim(bytes32 _claimId) private view returns (StakeClaim memory) { - StakeClaim memory claim = claims[_claimId]; - require(claim.createdAt != 0, DataServiceFeesClaimNotFound(_claimId)); - return claim; - } + // /** + // * @notice Gets the details of a stake claim + // * @param _claimId The ID of the stake claim + // * @return The stake claim details + // */ + // function _getStakeClaim(bytes32 _claimId) private view returns (StakeClaim memory) { + // StakeClaim memory claim = claims[_claimId]; + // require(claim.createdAt != 0, DataServiceFeesClaimNotFound(_claimId)); + // return claim; + // } /** * @notice Gets the next stake claim in the linked list @@ -141,13 +155,7 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat return claims[_claimId].nextClaim; } - /** - * @notice Builds a stake claim ID - * @param _serviceProvider The address of the service provider - * @param _nonce A nonce of the stake claim - * @return The stake claim ID - */ - function _buildStakeClaimId(address _serviceProvider, uint256 _nonce) private view returns (bytes32) { - return keccak256(abi.encodePacked(address(this), _serviceProvider, _nonce)); - } + // function _buildStakeClaimId(address serviceProvider, uint256 nonce) private view returns (bytes32) { + // return keccak256(abi.encodePacked(address(this), serviceProvider, nonce)); + // } } diff --git a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol new file mode 100644 index 000000000..ff0868321 --- /dev/null +++ b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { ProvisionTracker } from "./ProvisionTracker.sol"; +import { IDataServiceFees } from "../interfaces/IDataServiceFees.sol"; +import { IHorizonStaking } from "../../interfaces/IHorizonStaking.sol"; +import { LinkedList } from "../../libraries/LinkedList.sol"; + +library DataServiceFeesLib { + using ProvisionTracker for mapping(address => uint256); + using LinkedList for LinkedList.List; + + /** + * @notice Builds a stake claim ID + * @param serviceProvider The address of the service provider + * @param nonce A nonce of the stake claim + * @return The stake claim ID + */ + function _buildStakeClaimId(address serviceProvider, uint256 nonce) internal view returns (bytes32) { + return keccak256(abi.encodePacked(address(this), serviceProvider, nonce)); + } + + struct Storage { + ProvisionManagerStorage provisionManagerStorage; + } + + struct ProvisionManagerStorage { + uint256 _minimumProvisionTokens; + uint256 _maximumProvisionTokens; + uint64 _minimumThawingPeriod; + uint64 _maximumThawingPeriod; + uint32 _minimumVerifierCut; + uint32 _maximumVerifierCut; + uint32 _delegationRatio; + } + + /** + * @notice Locks stake for a service provider to back a payment. + * Creates a stake claim, which is stored in a linked list by service provider. + * @dev Requirements: + * - The associated provision must have enough available tokens to lock the stake. + * + * Emits a {StakeClaimLocked} event. + * + * @param _serviceProvider The address of the service provider + * @param _tokens The amount of tokens to lock in the claim + * @param _unlockTimestamp The timestamp when the tokens can be released + */ + function lockStake( + uint32 _delegationRatio, + mapping(address => uint256) storage feesProvisionTracker, + mapping(bytes32 => IDataServiceFees.StakeClaim) storage claims, + mapping(address serviceProvider => LinkedList.List list) storage claimsLists, + IHorizonStaking graphStaking, + address _serviceProvider, + uint256 _tokens, + uint256 _unlockTimestamp + ) external { + require(_tokens != 0, IDataServiceFees.DataServiceFeesZeroTokens()); + feesProvisionTracker.lock(graphStaking, _serviceProvider, _tokens, _delegationRatio); + + LinkedList.List storage claimsList = claimsLists[_serviceProvider]; + + // Save item and add to list + bytes32 claimId = _buildStakeClaimId(_serviceProvider, claimsList.nonce); + claims[claimId] = IDataServiceFees.StakeClaim({ + tokens: _tokens, + createdAt: block.timestamp, + releasableAt: _unlockTimestamp, + nextClaim: bytes32(0) + }); + if (claimsList.count != 0) claims[claimsList.tail].nextClaim = claimId; + claimsList.addTail(claimId); + + emit IDataServiceFees.StakeClaimLocked(_serviceProvider, claimId, _tokens, _unlockTimestamp); + } + + function processStakeClaim( + mapping(address serviceProvider => uint256 tokens) storage feesProvisionTracker, + mapping(bytes32 claimId => IDataServiceFees.StakeClaim claim) storage claims, + bytes32 _claimId, + bytes memory _acc + ) external returns (bool, bytes memory) { + IDataServiceFees.StakeClaim memory claim = claims[_claimId]; + require(claim.createdAt != 0, IDataServiceFees.DataServiceFeesClaimNotFound(_claimId)); + + // early exit + if (claim.releasableAt > block.timestamp) { + return (true, LinkedList.NULL_BYTES); + } + + // decode + (uint256 tokensClaimed, address serviceProvider) = abi.decode(_acc, (uint256, address)); + + // process + feesProvisionTracker.release(serviceProvider, claim.tokens); + emit IDataServiceFees.StakeClaimReleased(serviceProvider, _claimId, claim.tokens, claim.releasableAt); + + // encode + _acc = abi.encode(tokensClaimed + claim.tokens, serviceProvider); + return (false, _acc); + } +} From f2de2bb0f59abe6a0544d649460250744c57e5c5 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 15:14:33 -0300 Subject: [PATCH 20/90] HACK: ProvisionManager optimization - 25.595 --- .../data-service/utilities/ProvisionManager.sol | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol index eab23bcae..ba1d2fe50 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -178,7 +178,7 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa * @param _max The maximum allowed value for the provision tokens. */ function _setProvisionTokensRange(uint256 _min, uint256 _max) internal { - require(_min <= _max, ProvisionManagerInvalidRange(_min, _max)); + _requireLTE(_min, _max); _minimumProvisionTokens = _min; _maximumProvisionTokens = _max; emit ProvisionTokensRangeSet(_min, _max); @@ -190,7 +190,7 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa * @param _max The maximum allowed value for the max verifier cut. */ function _setVerifierCutRange(uint32 _min, uint32 _max) internal { - require(_min <= _max, ProvisionManagerInvalidRange(_min, _max)); + _requireLTE(_min, _max); require(PPMMath.isValidPPM(_max), ProvisionManagerInvalidRange(_min, _max)); _minimumVerifierCut = _min; _maximumVerifierCut = _max; @@ -203,7 +203,7 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa * @param _max The maximum allowed value for the thawing period. */ function _setThawingPeriodRange(uint64 _min, uint64 _max) internal { - require(_min <= _max, ProvisionManagerInvalidRange(_min, _max)); + _requireLTE(_min, _max); _minimumThawingPeriod = _min; _maximumThawingPeriod = _max; emit ThawingPeriodRangeSet(_min, _max); @@ -216,8 +216,7 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa * @param _serviceProvider The address of the service provider. */ function _checkProvisionTokens(address _serviceProvider) internal view virtual { - IHorizonStaking.Provision memory provision = _getProvision(_serviceProvider); - _checkProvisionTokens(provision); + _checkProvisionTokens(_getProvision(_serviceProvider)); } /** @@ -240,8 +239,7 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa * @param _checkPending If true, checks the pending provision parameters. */ function _checkProvisionParameters(address _serviceProvider, bool _checkPending) internal view virtual { - IHorizonStaking.Provision memory provision = _getProvision(_serviceProvider); - _checkProvisionParameters(provision, _checkPending); + _checkProvisionParameters(_getProvision(_serviceProvider), _checkPending); } /** @@ -322,4 +320,8 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa function _checkValueInRange(uint256 _value, uint256 _min, uint256 _max, bytes memory _revertMessage) private pure { require(_value.isInRange(_min, _max), ProvisionManagerInvalidValue(_revertMessage, _value, _min, _max)); } + + function _requireLTE(uint256 _min, uint256 _max) private pure { + require(_min <= _max, ProvisionManagerInvalidRange(_min, _max)); + } } From 8b0392b4c317bc5d9b2ca20d0f4b0afe31ce6d8b Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 26 May 2025 19:27:36 -0300 Subject: [PATCH 21/90] HACK: partial AllocationManager to lib - 24.520 --- .../libraries/AllocationManagerLib.sol | 106 ++++++++++++++++++ .../contracts/utilities/AllocationManager.sol | 74 +++++++----- 2 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol diff --git a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol new file mode 100644 index 000000000..892c82a86 --- /dev/null +++ b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; +import { IRewardsManager } from "@graphprotocol/contracts/contracts/rewards/IRewardsManager.sol"; +import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; + +import { Allocation } from "../libraries/Allocation.sol"; +import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; +import { AllocationManager } from "../utilities/AllocationManager.sol"; + +library AllocationManagerLib { + using ProvisionTracker for mapping(address => uint256); + using Allocation for mapping(address => Allocation.State); + using LegacyAllocation for mapping(address => LegacyAllocation.State); + + ///@dev EIP712 typehash for allocation id proof + bytes32 private constant EIP712_ALLOCATION_ID_PROOF_TYPEHASH = + keccak256("AllocationIdProof(address indexer,address allocationId)"); + + struct AllocateParams { + uint256 currentEpoch; + IHorizonStaking graphStaking; + IRewardsManager graphRewardsManager; + bytes32 _encodeAllocationProof; + address _indexer; + address _allocationId; + bytes32 _subgraphDeploymentId; + uint256 _tokens; + bytes _allocationProof; + uint32 _delegationRatio; + } + + /** + * @notice Create an allocation + * @dev The `_allocationProof` is a 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationId)` + * + * Requirements: + * - `_allocationId` must not be the zero address + * + * Emits a {AllocationCreated} event + * + * @param _allocations The mapping of allocation ids to allocation states + */ + function allocate( + mapping(address allocationId => Allocation.State allocation) storage _allocations, + mapping(address allocationId => LegacyAllocation.State allocation) storage _legacyAllocations, + mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, + mapping(bytes32 subgraphDeploymentId => uint256 tokens) storage _subgraphAllocatedTokens, + AllocateParams memory params + ) external { + require(params._allocationId != address(0), AllocationManager.AllocationManagerInvalidZeroAllocationId()); + + _verifyAllocationProof(params._encodeAllocationProof, params._allocationId, params._allocationProof); + + // Ensure allocation id is not reused + // need to check both subgraph service (on allocations.create()) and legacy allocations + _legacyAllocations.revertIfExists(params.graphStaking, params._allocationId); + + Allocation.State memory allocation = _allocations.create( + params._indexer, + params._allocationId, + params._subgraphDeploymentId, + params._tokens, + params.graphRewardsManager.onSubgraphAllocationUpdate(params._subgraphDeploymentId), + params.currentEpoch + ); + + // Check that the indexer has enough tokens available + // Note that the delegation ratio ensures overdelegation cannot be used + allocationProvisionTracker.lock(params.graphStaking, params._indexer, params._tokens, params._delegationRatio); + + // Update total allocated tokens for the subgraph deployment + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] = + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] + + allocation.tokens; + + emit AllocationManager.AllocationCreated( + params._indexer, + params._allocationId, + params._subgraphDeploymentId, + allocation.tokens, + params.currentEpoch + ); + } + + /** + * @notice Verifies ownership of an allocation id by verifying an EIP712 allocation proof + * @dev Requirements: + * - Signer must be the allocation id address + * @param _allocationId The id of the allocation + * @param _proof The EIP712 proof, an EIP712 signed message of (indexer,allocationId) + */ + function _verifyAllocationProof( + bytes32 _encodeAllocationProof, + address _allocationId, + bytes memory _proof + ) private pure { + address signer = ECDSA.recover(_encodeAllocationProof, _proof); + require( + signer == _allocationId, + AllocationManager.AllocationManagerInvalidAllocationProof(signer, _allocationId) + ); + } +} diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 78e5fa190..d34e11bc6 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -15,6 +15,7 @@ import { Allocation } from "../libraries/Allocation.sol"; import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; +import { AllocationManagerLib } from "../libraries/AllocationManagerLib.sol"; /** * @title AllocationManager contract @@ -204,34 +205,53 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca bytes memory _allocationProof, uint32 _delegationRatio ) internal { - require(_allocationId != address(0), AllocationManagerInvalidZeroAllocationId()); - - _verifyAllocationProof(_indexer, _allocationId, _allocationProof); - - // Ensure allocation id is not reused - // need to check both subgraph service (on allocations.create()) and legacy allocations - _legacyAllocations.revertIfExists(_graphStaking(), _allocationId); - - uint256 currentEpoch = _graphEpochManager().currentEpoch(); - Allocation.State memory allocation = _allocations.create( - _indexer, - _allocationId, - _subgraphDeploymentId, - _tokens, - _graphRewardsManager().onSubgraphAllocationUpdate(_subgraphDeploymentId), - currentEpoch + // require(_allocationId != address(0), AllocationManagerInvalidZeroAllocationId()); + + // _verifyAllocationProof(_indexer, _allocationId, _allocationProof); + + // // Ensure allocation id is not reused + // // need to check both subgraph service (on allocations.create()) and legacy allocations + // _legacyAllocations.revertIfExists(_graphStaking(), _allocationId); + + // uint256 currentEpoch = _graphEpochManager().currentEpoch(); + // Allocation.State memory allocation = _allocations.create( + // _indexer, + // _allocationId, + // _subgraphDeploymentId, + // _tokens, + // _graphRewardsManager().onSubgraphAllocationUpdate(_subgraphDeploymentId), + // currentEpoch + // ); + + // // Check that the indexer has enough tokens available + // // Note that the delegation ratio ensures overdelegation cannot be used + // allocationProvisionTracker.lock(_graphStaking(), _indexer, _tokens, _delegationRatio); + + // // Update total allocated tokens for the subgraph deployment + // _subgraphAllocatedTokens[allocation.subgraphDeploymentId] = + // _subgraphAllocatedTokens[allocation.subgraphDeploymentId] + + // allocation.tokens; + + // emit AllocationCreated(_indexer, _allocationId, _subgraphDeploymentId, allocation.tokens, currentEpoch); + + AllocationManagerLib.allocate( + _allocations, + _legacyAllocations, + allocationProvisionTracker, + _subgraphAllocatedTokens, + AllocationManagerLib.AllocateParams({ + _allocationId: _allocationId, + _allocationProof: _allocationProof, + _encodeAllocationProof: _encodeAllocationProof(_indexer, _allocationId), + _delegationRatio: _delegationRatio, + _indexer: _indexer, + _subgraphDeploymentId: _subgraphDeploymentId, + _tokens: _tokens, + currentEpoch: _graphEpochManager().currentEpoch(), + graphRewardsManager: _graphRewardsManager(), + graphStaking: _graphStaking() + }) ); - - // Check that the indexer has enough tokens available - // Note that the delegation ratio ensures overdelegation cannot be used - allocationProvisionTracker.lock(_graphStaking(), _indexer, _tokens, _delegationRatio); - - // Update total allocated tokens for the subgraph deployment - _subgraphAllocatedTokens[allocation.subgraphDeploymentId] = - _subgraphAllocatedTokens[allocation.subgraphDeploymentId] + - allocation.tokens; - - emit AllocationCreated(_indexer, _allocationId, _subgraphDeploymentId, allocation.tokens, currentEpoch); } /** From 85f3bea17755f5af269601ed3ba9f664917e3924 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 30 May 2025 15:21:18 -0300 Subject: [PATCH 22/90] HACK: partial AllocationManager to lib - 22.521 --- .../libraries/AllocationManagerLib.sol | 199 ++++++++++++++++- .../contracts/utilities/AllocationManager.sol | 206 ++++++++++-------- 2 files changed, 313 insertions(+), 92 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol index 892c82a86..8e9a5f7bf 100644 --- a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol +++ b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol @@ -2,9 +2,15 @@ pragma solidity 0.8.27; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; +import { IEpochManager } from "@graphprotocol/contracts/contracts/epochs/IEpochManager.sol"; +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; import { IRewardsManager } from "@graphprotocol/contracts/contracts/rewards/IRewardsManager.sol"; +import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; +import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; import { Allocation } from "../libraries/Allocation.sol"; import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; @@ -13,11 +19,10 @@ import { AllocationManager } from "../utilities/AllocationManager.sol"; library AllocationManagerLib { using ProvisionTracker for mapping(address => uint256); using Allocation for mapping(address => Allocation.State); + using Allocation for Allocation.State; using LegacyAllocation for mapping(address => LegacyAllocation.State); - - ///@dev EIP712 typehash for allocation id proof - bytes32 private constant EIP712_ALLOCATION_ID_PROOF_TYPEHASH = - keccak256("AllocationIdProof(address indexer,address allocationId)"); + using PPMMath for uint256; + using TokenUtils for IGraphToken; struct AllocateParams { uint256 currentEpoch; @@ -32,6 +37,23 @@ library AllocationManagerLib { uint32 _delegationRatio; } + struct PresentParams { + uint256 maxPOIStaleness; + IEpochManager graphEpochManager; + IHorizonStaking graphStaking; + IRewardsManager graphRewardsManager; + IGraphToken graphToken; + address _allocationId; + bytes32 _poi; + bytes _poiMetadata; + uint32 _delegationRatio; + address _paymentsDestination; + } + + ///@dev EIP712 typehash for allocation id proof + bytes32 private constant EIP712_ALLOCATION_ID_PROOF_TYPEHASH = + keccak256("AllocationIdProof(address indexer,address allocationId)"); + /** * @notice Create an allocation * @dev The `_allocationProof` is a 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationId)` @@ -85,6 +107,173 @@ library AllocationManagerLib { ); } + function presentPOI( + mapping(address allocationId => Allocation.State allocation) storage _allocations, + mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, + mapping(bytes32 subgraphDeploymentId => uint256 tokens) storage _subgraphAllocatedTokens, + PresentParams memory params + ) external returns (uint256) { + Allocation.State memory allocation = _allocations.get(params._allocationId); + require(allocation.isOpen(), AllocationManager.AllocationManagerAllocationClosed(params._allocationId)); + + // Mint indexing rewards if all conditions are met + uint256 tokensRewards = (!allocation.isStale(params.maxPOIStaleness) && + !allocation.isAltruistic() && + params._poi != bytes32(0)) && params.graphEpochManager.currentEpoch() > allocation.createdAtEpoch + ? params.graphRewardsManager.takeRewards(params._allocationId) + : 0; + + // ... but we still take a snapshot to ensure the rewards are not accumulated for the next valid POI + _allocations.snapshotRewards( + params._allocationId, + params.graphRewardsManager.onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) + ); + _allocations.presentPOI(params._allocationId); + + // Any pending rewards should have been collected now + _allocations.clearPendingRewards(params._allocationId); + + uint256 tokensIndexerRewards = 0; + uint256 tokensDelegationRewards = 0; + if (tokensRewards != 0) { + // Distribute rewards to delegators + uint256 delegatorCut = params.graphStaking.getDelegationFeeCut( + allocation.indexer, + address(this), + IGraphPayments.PaymentTypes.IndexingRewards + ); + IHorizonStakingTypes.DelegationPool memory delegationPool = params.graphStaking.getDelegationPool( + allocation.indexer, + address(this) + ); + // If delegation pool has no shares then we don't need to distribute rewards to delegators + tokensDelegationRewards = delegationPool.shares > 0 ? tokensRewards.mulPPM(delegatorCut) : 0; + if (tokensDelegationRewards > 0) { + params.graphToken.approve(address(params.graphStaking), tokensDelegationRewards); + params.graphStaking.addToDelegationPool(allocation.indexer, address(this), tokensDelegationRewards); + } + + // Distribute rewards to indexer + tokensIndexerRewards = tokensRewards - tokensDelegationRewards; + if (tokensIndexerRewards > 0) { + if (params._paymentsDestination == address(0)) { + params.graphToken.approve(address(params.graphStaking), tokensIndexerRewards); + params.graphStaking.stakeToProvision(allocation.indexer, address(this), tokensIndexerRewards); + } else { + params.graphToken.pushTokens(params._paymentsDestination, tokensIndexerRewards); + } + } + } + + emit AllocationManager.IndexingRewardsCollected( + allocation.indexer, + params._allocationId, + allocation.subgraphDeploymentId, + tokensRewards, + tokensIndexerRewards, + tokensDelegationRewards, + params._poi, + params._poiMetadata, + params.graphEpochManager.currentEpoch() + ); + + // Check if the indexer is over-allocated and force close the allocation if necessary + if ( + _isOverAllocated( + allocationProvisionTracker, + params.graphStaking, + allocation.indexer, + params._delegationRatio + ) + ) { + _closeAllocation( + _allocations, + allocationProvisionTracker, + _subgraphAllocatedTokens, + params.graphRewardsManager, + params._allocationId, + true + ); + } + + return tokensRewards; + } + + function closeAllocation( + mapping(address allocationId => Allocation.State allocation) storage _allocations, + mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, + mapping(bytes32 subgraphDeploymentId => uint256 tokens) storage _subgraphAllocatedTokens, + IRewardsManager graphRewardsManager, + address _allocationId, + bool _forceClosed + ) external { + _closeAllocation( + _allocations, + allocationProvisionTracker, + _subgraphAllocatedTokens, + graphRewardsManager, + _allocationId, + _forceClosed + ); + } + + /** + * @notice Checks if an allocation is over-allocated + * @param _indexer The address of the indexer + * @param _delegationRatio The delegation ratio to consider when locking tokens + * @return True if the allocation is over-allocated, false otherwise + */ + function isOverAllocated( + mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, + IHorizonStaking graphStaking, + address _indexer, + uint32 _delegationRatio + ) external view returns (bool) { + return _isOverAllocated(allocationProvisionTracker, graphStaking, _indexer, _delegationRatio); + } + + function _closeAllocation( + mapping(address allocationId => Allocation.State allocation) storage _allocations, + mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, + mapping(bytes32 subgraphDeploymentId => uint256 tokens) storage _subgraphAllocatedTokens, + IRewardsManager graphRewardsManager, + address _allocationId, + bool _forceClosed + ) private { + Allocation.State memory allocation = _allocations.get(_allocationId); + + // Take rewards snapshot to prevent other allos from counting tokens from this allo + _allocations.snapshotRewards( + _allocationId, + graphRewardsManager.onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) + ); + + _allocations.close(_allocationId); + allocationProvisionTracker.release(allocation.indexer, allocation.tokens); + + // Update total allocated tokens for the subgraph deployment + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] = + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] - + allocation.tokens; + + emit AllocationManager.AllocationClosed( + allocation.indexer, + _allocationId, + allocation.subgraphDeploymentId, + allocation.tokens, + _forceClosed + ); + } + + function _isOverAllocated( + mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, + IHorizonStaking graphStaking, + address _indexer, + uint32 _delegationRatio + ) private view returns (bool) { + return !allocationProvisionTracker.check(graphStaking, _indexer, _delegationRatio); + } + /** * @notice Verifies ownership of an allocation id by verifying an EIP712 allocation proof * @dev Requirements: diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index d34e11bc6..4a89a0a98 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; -import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; -import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; import { GraphDirectory } from "@graphprotocol/horizon/contracts/utilities/GraphDirectory.sol"; import { AllocationManagerV1Storage } from "./AllocationManagerStorage.sol"; @@ -288,76 +286,95 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca uint32 _delegationRatio, address _paymentsDestination ) internal returns (uint256) { - Allocation.State memory allocation = _allocations.get(_allocationId); - require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId)); - - // Mint indexing rewards if all conditions are met - uint256 tokensRewards = (!allocation.isStale(maxPOIStaleness) && - !allocation.isAltruistic() && - _poi != bytes32(0)) && _graphEpochManager().currentEpoch() > allocation.createdAtEpoch - ? _graphRewardsManager().takeRewards(_allocationId) - : 0; + // Allocation.State memory allocation = _allocations.get(_allocationId); + // require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId)); + + // // Mint indexing rewards if all conditions are met + // uint256 tokensRewards = (!allocation.isStale(maxPOIStaleness) && + // !allocation.isAltruistic() && + // _poi != bytes32(0)) && _graphEpochManager().currentEpoch() > allocation.createdAtEpoch + // ? _graphRewardsManager().takeRewards(_allocationId) + // : 0; + + // // ... but we still take a snapshot to ensure the rewards are not accumulated for the next valid POI + // _allocations.snapshotRewards( + // _allocationId, + // _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) + // ); + // _allocations.presentPOI(_allocationId); + + // // Any pending rewards should have been collected now + // _allocations.clearPendingRewards(_allocationId); + + // uint256 tokensIndexerRewards = 0; + // uint256 tokensDelegationRewards = 0; + // if (tokensRewards != 0) { + // // Distribute rewards to delegators + // uint256 delegatorCut = _graphStaking().getDelegationFeeCut( + // allocation.indexer, + // address(this), + // IGraphPayments.PaymentTypes.IndexingRewards + // ); + // IHorizonStakingTypes.DelegationPool memory delegationPool = _graphStaking().getDelegationPool( + // allocation.indexer, + // address(this) + // ); + // // If delegation pool has no shares then we don't need to distribute rewards to delegators + // tokensDelegationRewards = delegationPool.shares > 0 ? tokensRewards.mulPPM(delegatorCut) : 0; + // if (tokensDelegationRewards > 0) { + // _graphToken().approve(address(_graphStaking()), tokensDelegationRewards); + // _graphStaking().addToDelegationPool(allocation.indexer, address(this), tokensDelegationRewards); + // } + + // // Distribute rewards to indexer + // tokensIndexerRewards = tokensRewards - tokensDelegationRewards; + // if (tokensIndexerRewards > 0) { + // if (_paymentsDestination == address(0)) { + // _graphToken().approve(address(_graphStaking()), tokensIndexerRewards); + // _graphStaking().stakeToProvision(allocation.indexer, address(this), tokensIndexerRewards); + // } else { + // _graphToken().pushTokens(_paymentsDestination, tokensIndexerRewards); + // } + // } + // } + + // emit IndexingRewardsCollected( + // allocation.indexer, + // _allocationId, + // allocation.subgraphDeploymentId, + // tokensRewards, + // tokensIndexerRewards, + // tokensDelegationRewards, + // _poi, + // _poiMetadata, + // _graphEpochManager().currentEpoch() + // ); - // ... but we still take a snapshot to ensure the rewards are not accumulated for the next valid POI - _allocations.snapshotRewards( - _allocationId, - _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) - ); - _allocations.presentPOI(_allocationId); - - // Any pending rewards should have been collected now - _allocations.clearPendingRewards(_allocationId); - - uint256 tokensIndexerRewards = 0; - uint256 tokensDelegationRewards = 0; - if (tokensRewards != 0) { - // Distribute rewards to delegators - uint256 delegatorCut = _graphStaking().getDelegationFeeCut( - allocation.indexer, - address(this), - IGraphPayments.PaymentTypes.IndexingRewards - ); - IHorizonStakingTypes.DelegationPool memory delegationPool = _graphStaking().getDelegationPool( - allocation.indexer, - address(this) + // // Check if the indexer is over-allocated and force close the allocation if necessary + // if (_isOverAllocated(allocation.indexer, _delegationRatio)) { + // _closeAllocation(_allocationId, true); + // } + + // return tokensRewards; + + return + AllocationManagerLib.presentPOI( + _allocations, + allocationProvisionTracker, + _subgraphAllocatedTokens, + AllocationManagerLib.PresentParams({ + maxPOIStaleness: maxPOIStaleness, + graphEpochManager: _graphEpochManager(), + graphStaking: _graphStaking(), + graphRewardsManager: _graphRewardsManager(), + graphToken: _graphToken(), + _allocationId: _allocationId, + _poi: _poi, + _poiMetadata: _poiMetadata, + _delegationRatio: _delegationRatio, + _paymentsDestination: _paymentsDestination + }) ); - // If delegation pool has no shares then we don't need to distribute rewards to delegators - tokensDelegationRewards = delegationPool.shares > 0 ? tokensRewards.mulPPM(delegatorCut) : 0; - if (tokensDelegationRewards > 0) { - _graphToken().approve(address(_graphStaking()), tokensDelegationRewards); - _graphStaking().addToDelegationPool(allocation.indexer, address(this), tokensDelegationRewards); - } - - // Distribute rewards to indexer - tokensIndexerRewards = tokensRewards - tokensDelegationRewards; - if (tokensIndexerRewards > 0) { - if (_paymentsDestination == address(0)) { - _graphToken().approve(address(_graphStaking()), tokensIndexerRewards); - _graphStaking().stakeToProvision(allocation.indexer, address(this), tokensIndexerRewards); - } else { - _graphToken().pushTokens(_paymentsDestination, tokensIndexerRewards); - } - } - } - - emit IndexingRewardsCollected( - allocation.indexer, - _allocationId, - allocation.subgraphDeploymentId, - tokensRewards, - tokensIndexerRewards, - tokensDelegationRewards, - _poi, - _poiMetadata, - _graphEpochManager().currentEpoch() - ); - - // Check if the indexer is over-allocated and force close the allocation if necessary - if (_isOverAllocated(allocation.indexer, _delegationRatio)) { - _closeAllocation(_allocationId, true); - } - - return tokensRewards; } /** @@ -429,27 +446,36 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca * @param _forceClosed Whether the allocation was force closed */ function _closeAllocation(address _allocationId, bool _forceClosed) internal { - Allocation.State memory allocation = _allocations.get(_allocationId); + // Allocation.State memory allocation = _allocations.get(_allocationId); - // Take rewards snapshot to prevent other allos from counting tokens from this allo - _allocations.snapshotRewards( - _allocationId, - _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) - ); + // // Take rewards snapshot to prevent other allos from counting tokens from this allo + // _allocations.snapshotRewards( + // _allocationId, + // _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) + // ); - _allocations.close(_allocationId); - allocationProvisionTracker.release(allocation.indexer, allocation.tokens); + // _allocations.close(_allocationId); + // allocationProvisionTracker.release(allocation.indexer, allocation.tokens); - // Update total allocated tokens for the subgraph deployment - _subgraphAllocatedTokens[allocation.subgraphDeploymentId] = - _subgraphAllocatedTokens[allocation.subgraphDeploymentId] - - allocation.tokens; + // // Update total allocated tokens for the subgraph deployment + // _subgraphAllocatedTokens[allocation.subgraphDeploymentId] = + // _subgraphAllocatedTokens[allocation.subgraphDeploymentId] - + // allocation.tokens; + + // emit AllocationClosed( + // allocation.indexer, + // _allocationId, + // allocation.subgraphDeploymentId, + // allocation.tokens, + // _forceClosed + // ); - emit AllocationClosed( - allocation.indexer, + AllocationManagerLib.closeAllocation( + _allocations, + allocationProvisionTracker, + _subgraphAllocatedTokens, + _graphRewardsManager(), _allocationId, - allocation.subgraphDeploymentId, - allocation.tokens, _forceClosed ); } @@ -481,7 +507,13 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca * @return True if the allocation is over-allocated, false otherwise */ function _isOverAllocated(address _indexer, uint32 _delegationRatio) internal view returns (bool) { - return !allocationProvisionTracker.check(_graphStaking(), _indexer, _delegationRatio); + return + AllocationManagerLib.isOverAllocated( + allocationProvisionTracker, + _graphStaking(), + _indexer, + _delegationRatio + ); } /** From d518bdd079a4a89f472624efdbe38baf46d42d8e Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 3 Jun 2025 10:40:30 -0300 Subject: [PATCH 23/90] f: add update indexing agreement event --- .../contracts/libraries/IndexingAgreement.sol | 27 +++++++++++++++++++ .../{update.sol => update.t.sol} | 15 +++++++++++ 2 files changed, 42 insertions(+) rename packages/subgraph-service/test/unit/subgraphService/indexing-agreement/{update.sol => update.t.sol} (92%) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 5200dd84c..8eb092a55 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -143,6 +143,24 @@ library IndexingAgreement { bytes versionTerms ); + /** + * @notice Emitted when an indexing agreement is updated + * @param indexer The address of the indexer + * @param payer The address of the payer + * @param agreementId The id of the agreement + * @param allocationId The id of the allocation + * @param version The version of the indexing agreement + * @param versionTerms The version data of the indexing agreement + */ + event IndexingAgreementUpdated( + address indexed indexer, + address indexed payer, + bytes16 indexed agreementId, + address allocationId, + IndexingAgreementVersion version, + bytes versionTerms + ); + /** * @notice Thrown when trying to interact with an agreement with an invalid version * @param version The invalid version @@ -274,6 +292,15 @@ library IndexingAgreement { require(metadata.version == IndexingAgreementVersion.V1, InvalidIndexingAgreementVersion(metadata.version)); _setTermsV1(self, signedRCAU.rcau.agreementId, metadata.terms); + emit IndexingAgreementUpdated({ + indexer: wrapper.collectorAgreement.serviceProvider, + payer: wrapper.collectorAgreement.payer, + agreementId: signedRCAU.rcau.agreementId, + allocationId: wrapper.agreement.allocationId, + version: metadata.version, + versionTerms: metadata.terms + }); + _directory().recurringCollector().update(signedRCAU); } diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol similarity index 92% rename from packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.sol rename to packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol index 6c73b0c25..b7831ad04 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol @@ -148,6 +148,21 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); IRecurringCollector.SignedRCAU memory acceptableUpdate = _generateAcceptableSignedRCAU(ctx, accepted.rca); + IndexingAgreement.UpdateIndexingAgreementMetadata memory metadata = abi.decode( + acceptableUpdate.rcau.metadata, + (IndexingAgreement.UpdateIndexingAgreementMetadata) + ); + + vm.expectEmit(address(subgraphService)); + emit IndexingAgreement.IndexingAgreementUpdated( + accepted.rca.serviceProvider, + accepted.rca.payer, + acceptableUpdate.rcau.agreementId, + indexerState.allocationId, + metadata.version, + metadata.terms + ); + resetPrank(indexerState.addr); _getSubgraphServiceExtension().updateIndexingAgreement(indexerState.addr, acceptableUpdate); } From d8802abb5bfbb5139e784f4347e42ae4e39a8a32 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 3 Jun 2025 10:42:12 -0300 Subject: [PATCH 24/90] f: TODO.md --- IndexingPaymentsTodo.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/IndexingPaymentsTodo.md b/IndexingPaymentsTodo.md index 935dc6f12..bdb82dfcd 100644 --- a/IndexingPaymentsTodo.md +++ b/IndexingPaymentsTodo.md @@ -1,13 +1,16 @@ # Still pending -* Arbitration Charter: Update to support disputing IndexingFee. +* Remove extension if I can fit everything in one service? +* One Interface for all subgraph +* `require(provision.tokens != 0, DisputeManagerZeroTokens());` - Document or fix? * Check code coverage -* Check contract size * Don't love cancel agreement on stop service / close stale allocation. -* Missing Upgrade event for subgraph service +* Arbitration Charter: Update to support disputing IndexingFee. # Done +* DONE: ~~* Missing Upgrade event for subgraph service~~ +* DONE: ~~* Check contract size~~ * DONE: ~~Switch cancel event in recurring collector to use Enum~~ * DONE: ~~Switch timestamps to uint64~~ * DONE: ~~Check that UUID-v4 fits in `bytes16`~~ From db6fb210aea35eeb160c4a4347442a4ce7fc8072 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 3 Jun 2025 10:49:16 -0300 Subject: [PATCH 25/90] f: add extended interface --- IndexingPaymentsTodo.md | 3 ++- .../contracts/DisputeManager.sol | 12 +++++------ .../contracts/SubgraphServiceExtension.sol | 3 ++- .../interfaces/ISubgraphServiceExtended.sol | 7 +++++++ .../indexing-agreement/base.t.sol | 2 +- .../indexing-agreement/cancel.t.sol | 20 +++++++++---------- .../indexing-agreement/shared.t.sol | 10 +++++----- .../indexing-agreement/update.t.sol | 16 +++++++-------- 8 files changed, 41 insertions(+), 32 deletions(-) create mode 100644 packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtended.sol diff --git a/IndexingPaymentsTodo.md b/IndexingPaymentsTodo.md index bdb82dfcd..83e93db41 100644 --- a/IndexingPaymentsTodo.md +++ b/IndexingPaymentsTodo.md @@ -1,7 +1,7 @@ # Still pending +* Remove linter warnings * Remove extension if I can fit everything in one service? -* One Interface for all subgraph * `require(provision.tokens != 0, DisputeManagerZeroTokens());` - Document or fix? * Check code coverage * Don't love cancel agreement on stop service / close stale allocation. @@ -9,6 +9,7 @@ # Done +* DONE: ~~* One Interface for all subgraph~~ * DONE: ~~* Missing Upgrade event for subgraph service~~ * DONE: ~~* Check contract size~~ * DONE: ~~Switch cancel event in recurring collector to use Enum~~ diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index fad0eec28..095e8e9f9 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -5,7 +5,7 @@ import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToke import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; import { IDisputeManager } from "./interfaces/IDisputeManager.sol"; import { ISubgraphService } from "./interfaces/ISubgraphService.sol"; -import { ISubgraphServiceExtension } from "./interfaces/ISubgraphServiceExtension.sol"; +import { ISubgraphServiceExtended } from "./interfaces/ISubgraphServiceExtended.sol"; import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; @@ -534,7 +534,7 @@ contract DisputeManager is uint256 _entities, uint256 _blockNumber ) private returns (bytes32) { - IndexingAgreement.AgreementWrapper memory wrapper = _getSubgraphServiceExtension().getIndexingAgreement( + IndexingAgreement.AgreementWrapper memory wrapper = _getSubgraphServiceExtended().getIndexingAgreement( _agreementId ); @@ -767,13 +767,13 @@ contract DisputeManager is } /** - * @notice Get the address of the subgraph service extension + * @notice Get the address of the extended subgraph service * @dev Will revert if the subgraph service is not set - * @return The subgraph service address + * @return The extended subgraph service address */ - function _getSubgraphServiceExtension() private view returns (ISubgraphServiceExtension) { + function _getSubgraphServiceExtended() private view returns (ISubgraphServiceExtended) { require(address(subgraphService) != address(0), DisputeManagerSubgraphServiceNotSet()); - return ISubgraphServiceExtension(address(subgraphService)); + return ISubgraphServiceExtended(address(subgraphService)); } /** diff --git a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol index cb5995bd5..16911c1c8 100644 --- a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol +++ b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol @@ -8,8 +8,9 @@ import { ProvisionManagerLib } from "@graphprotocol/horizon/contracts/data-servi import { IndexingAgreement } from "./libraries/IndexingAgreement.sol"; import { SubgraphService } from "./SubgraphService.sol"; +import { ISubgraphServiceExtension } from "./interfaces/ISubgraphServiceExtension.sol"; -contract SubgraphServiceExtension is PausableUpgradeable { +contract SubgraphServiceExtension is PausableUpgradeable, ISubgraphServiceExtension { using IndexingAgreement for IndexingAgreement.Manager; modifier onlyValid(address indexer) { diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtended.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtended.sol new file mode 100644 index 000000000..4ee94eac8 --- /dev/null +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtended.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { ISubgraphService } from "./ISubgraphService.sol"; +import { ISubgraphServiceExtension } from "./ISubgraphServiceExtension.sol"; + +interface ISubgraphServiceExtended is ISubgraphService, ISubgraphServiceExtension {} diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol index 497b935b4..d23e510ca 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol @@ -17,7 +17,7 @@ contract SubgraphServiceIndexingAgreementBaseTest is SubgraphServiceIndexingAgre vm.expectRevert(TransparentUpgradeableProxy.ProxyDeniedAdminAccess.selector); resetPrank(address(operator)); - _getSubgraphServiceExtension().cancelIndexingAgreement(indexer, agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreement(indexer, agreementId); } function test_SubgraphService_Revert_WhenUnsafeAddress_WhenGraphProxyAdmin(uint256 unboundedTokens) public { diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol index 52b56532e..261aeb55d 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol @@ -25,7 +25,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); resetPrank(rando); - _getSubgraphServiceExtension().cancelIndexingAgreementByPayer(agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreementByPayer(agreementId); } function test_SubgraphService_CancelIndexingAgreementByPayer_Revert_WhenNotAuthorized( @@ -42,7 +42,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg ); vm.expectRevert(expectedErr); resetPrank(rando); - _getSubgraphServiceExtension().cancelIndexingAgreementByPayer(accepted.rca.agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreementByPayer(accepted.rca.agreementId); } function test_SubgraphService_CancelIndexingAgreementByPayer_Revert_WhenNotAccepted( @@ -58,7 +58,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg agreementId ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().cancelIndexingAgreementByPayer(agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreementByPayer(agreementId); } function test_SubgraphService_CancelIndexingAgreementByPayer_Revert_WhenCanceled( @@ -79,7 +79,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg accepted.rca.agreementId ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().cancelIndexingAgreementByPayer(accepted.rca.agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreementByPayer(accepted.rca.agreementId); } function test_SubgraphService_CancelIndexingAgreementByPayer(Seed memory seed) public { @@ -105,7 +105,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); resetPrank(operator); - _getSubgraphServiceExtension().cancelIndexingAgreement(indexer, agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreement(indexer, agreementId); } function test_SubgraphService_CancelIndexingAgreement_Revert_WhenNotAuthorized( @@ -121,7 +121,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg operator ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().cancelIndexingAgreement(indexer, agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreement(indexer, agreementId); } function test_SubgraphService_CancelIndexingAgreement_Revert_WhenInvalidProvision( @@ -142,7 +142,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg maximumProvisionTokens ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().cancelIndexingAgreement(indexer, agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreement(indexer, agreementId); } function test_SubgraphService_CancelIndexingAgreement_Revert_WhenIndexerNotRegistered( @@ -159,7 +159,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg indexer ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().cancelIndexingAgreement(indexer, agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreement(indexer, agreementId); } function test_SubgraphService_CancelIndexingAgreement_Revert_WhenNotAccepted( @@ -175,7 +175,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg agreementId ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().cancelIndexingAgreement(indexerState.addr, agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreement(indexerState.addr, agreementId); } function test_SubgraphService_CancelIndexingAgreement_Revert_WhenCanceled( @@ -196,7 +196,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg accepted.rca.agreementId ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().cancelIndexingAgreement(indexerState.addr, accepted.rca.agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreement(indexerState.addr, accepted.rca.agreementId); } function test_SubgraphService_CancelIndexingAgreement_OK(Seed memory seed) public { 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 2d3c19b46..3115492c3 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 @@ -5,7 +5,7 @@ import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; -import { ISubgraphServiceExtension } from "../../../../contracts/interfaces/ISubgraphServiceExtension.sol"; +import { ISubgraphServiceExtended } from "../../../../contracts/interfaces/ISubgraphServiceExtended.sol"; import { Bounder } from "@graphprotocol/horizon/test/unit/utils/Bounder.t.sol"; import { RecurringCollectorHelper } from "@graphprotocol/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol"; @@ -110,10 +110,10 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun if (byIndexer) { _subgraphServiceSafePrank(_indexer); - _getSubgraphServiceExtension().cancelIndexingAgreement(_indexer, _agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreement(_indexer, _agreementId); } else { _subgraphServiceSafePrank(_ctx.payer.signer); - _getSubgraphServiceExtension().cancelIndexingAgreementByPayer(_agreementId); + _getSubgraphServiceExtended().cancelIndexingAgreementByPayer(_agreementId); } } @@ -309,8 +309,8 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun ); } - function _getSubgraphServiceExtension() internal view returns (ISubgraphServiceExtension) { - return ISubgraphServiceExtension(address(subgraphService)); + function _getSubgraphServiceExtended() internal view returns (ISubgraphServiceExtended) { + return ISubgraphServiceExtended(address(subgraphService)); } function _newAcceptIndexingAgreementMetadataV1( diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol index b7831ad04..1c828d431 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol @@ -26,7 +26,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA resetPrank(operator); vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - _getSubgraphServiceExtension().updateIndexingAgreement(operator, signedRCAU); + _getSubgraphServiceExtended().updateIndexingAgreement(operator, signedRCAU); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenNotAuthorized( @@ -42,7 +42,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA notAuthorized ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().updateIndexingAgreement(indexer, signedRCAU); + _getSubgraphServiceExtended().updateIndexingAgreement(indexer, signedRCAU); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenInvalidProvision( @@ -63,7 +63,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA maximumProvisionTokens ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().updateIndexingAgreement(indexer, signedRCAU); + _getSubgraphServiceExtended().updateIndexingAgreement(indexer, signedRCAU); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenIndexerNotRegistered( @@ -81,7 +81,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA indexer ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtension().updateIndexingAgreement(indexer, signedRCAU); + _getSubgraphServiceExtended().updateIndexingAgreement(indexer, signedRCAU); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenNotAccepted(Seed memory seed) public { @@ -98,7 +98,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA ); vm.expectRevert(expectedErr); resetPrank(indexerState.addr); - _getSubgraphServiceExtension().updateIndexingAgreement(indexerState.addr, acceptableUpdate); + _getSubgraphServiceExtended().updateIndexingAgreement(indexerState.addr, acceptableUpdate); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenNotAuthorizedForAgreement( @@ -117,7 +117,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA ); vm.expectRevert(expectedErr); resetPrank(indexerStateB.addr); - _getSubgraphServiceExtension().updateIndexingAgreement(indexerStateB.addr, acceptableUpdate); + _getSubgraphServiceExtended().updateIndexingAgreement(indexerStateB.addr, acceptableUpdate); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenInvalidMetadata(Seed memory seed) public { @@ -139,7 +139,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA ); vm.expectRevert(expectedErr); resetPrank(indexerState.addr); - _getSubgraphServiceExtension().updateIndexingAgreement(indexerState.addr, unacceptableUpdate); + _getSubgraphServiceExtended().updateIndexingAgreement(indexerState.addr, unacceptableUpdate); } function test_SubgraphService_UpdateIndexingAgreement_OK(Seed memory seed) public { @@ -164,7 +164,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA ); resetPrank(indexerState.addr); - _getSubgraphServiceExtension().updateIndexingAgreement(indexerState.addr, acceptableUpdate); + _getSubgraphServiceExtended().updateIndexingAgreement(indexerState.addr, acceptableUpdate); } /* solhint-enable graph/func-name-mixedcase */ } From b631afe36e50e501ef646554a0a62f4ac09b09c6 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 3 Jun 2025 15:12:52 -0300 Subject: [PATCH 26/90] f: remove comments --- .../extensions/DataServiceFees.sol | 51 ------- .../contracts/utilities/AllocationManager.sol | 124 ------------------ 2 files changed, 175 deletions(-) diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol index 1b0766ba7..1dfea9ad0 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -42,24 +42,6 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat * @param _unlockTimestamp The timestamp when the tokens can be released */ function _lockStake(address _serviceProvider, uint256 _tokens, uint256 _unlockTimestamp) internal { - // require(_tokens != 0, DataServiceFeesZeroTokens()); - // feesProvisionTracker.lock(_graphStaking(), _serviceProvider, _tokens, _delegationRatio); - - // LinkedList.List storage claimsList = claimsLists[_serviceProvider]; - - // // Save item and add to list - // bytes32 claimId = _buildStakeClaimId(_serviceProvider, claimsList.nonce); - // claims[claimId] = StakeClaim({ - // tokens: _tokens, - // createdAt: block.timestamp, - // releasableAt: _unlockTimestamp, - // nextClaim: bytes32(0) - // }); - // if (claimsList.count != 0) claims[claimsList.tail].nextClaim = claimId; - // claimsList.addTail(claimId); - - // emit StakeClaimLocked(_serviceProvider, claimId, _tokens, _unlockTimestamp); - DataServiceFeesLib.lockStake( _delegationRatio, feesProvisionTracker, @@ -104,24 +86,6 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat * @return The updated accumulator data */ function _processStakeClaim(bytes32 _claimId, bytes memory _acc) private returns (bool, bytes memory) { - // StakeClaim memory claim = _getStakeClaim(_claimId); - - // // early exit - // if (claim.releasableAt > block.timestamp) { - // return (true, LinkedList.NULL_BYTES); - // } - - // // decode - // (uint256 tokensClaimed, address serviceProvider) = abi.decode(_acc, (uint256, address)); - - // // process - // feesProvisionTracker.release(serviceProvider, claim.tokens); - // emit StakeClaimReleased(serviceProvider, _claimId, claim.tokens, claim.releasableAt); - - // // encode - // _acc = abi.encode(tokensClaimed + claim.tokens, serviceProvider); - // return (false, _acc); - return DataServiceFeesLib.processStakeClaim(feesProvisionTracker, claims, _claimId, _acc); } @@ -134,17 +98,6 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat delete claims[_claimId]; } - // /** - // * @notice Gets the details of a stake claim - // * @param _claimId The ID of the stake claim - // * @return The stake claim details - // */ - // function _getStakeClaim(bytes32 _claimId) private view returns (StakeClaim memory) { - // StakeClaim memory claim = claims[_claimId]; - // require(claim.createdAt != 0, DataServiceFeesClaimNotFound(_claimId)); - // return claim; - // } - /** * @notice Gets the next stake claim in the linked list * @dev This function is used as a callback in the stake claims linked list traversal. @@ -154,8 +107,4 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat function _getNextStakeClaim(bytes32 _claimId) private view returns (bytes32) { return claims[_claimId].nextClaim; } - - // function _buildStakeClaimId(address serviceProvider, uint256 nonce) private view returns (bytes32) { - // return keccak256(abi.encodePacked(address(this), serviceProvider, nonce)); - // } } diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 4a89a0a98..e85c9a1b0 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -203,35 +203,6 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca bytes memory _allocationProof, uint32 _delegationRatio ) internal { - // require(_allocationId != address(0), AllocationManagerInvalidZeroAllocationId()); - - // _verifyAllocationProof(_indexer, _allocationId, _allocationProof); - - // // Ensure allocation id is not reused - // // need to check both subgraph service (on allocations.create()) and legacy allocations - // _legacyAllocations.revertIfExists(_graphStaking(), _allocationId); - - // uint256 currentEpoch = _graphEpochManager().currentEpoch(); - // Allocation.State memory allocation = _allocations.create( - // _indexer, - // _allocationId, - // _subgraphDeploymentId, - // _tokens, - // _graphRewardsManager().onSubgraphAllocationUpdate(_subgraphDeploymentId), - // currentEpoch - // ); - - // // Check that the indexer has enough tokens available - // // Note that the delegation ratio ensures overdelegation cannot be used - // allocationProvisionTracker.lock(_graphStaking(), _indexer, _tokens, _delegationRatio); - - // // Update total allocated tokens for the subgraph deployment - // _subgraphAllocatedTokens[allocation.subgraphDeploymentId] = - // _subgraphAllocatedTokens[allocation.subgraphDeploymentId] + - // allocation.tokens; - - // emit AllocationCreated(_indexer, _allocationId, _subgraphDeploymentId, allocation.tokens, currentEpoch); - AllocationManagerLib.allocate( _allocations, _legacyAllocations, @@ -286,77 +257,6 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca uint32 _delegationRatio, address _paymentsDestination ) internal returns (uint256) { - // Allocation.State memory allocation = _allocations.get(_allocationId); - // require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId)); - - // // Mint indexing rewards if all conditions are met - // uint256 tokensRewards = (!allocation.isStale(maxPOIStaleness) && - // !allocation.isAltruistic() && - // _poi != bytes32(0)) && _graphEpochManager().currentEpoch() > allocation.createdAtEpoch - // ? _graphRewardsManager().takeRewards(_allocationId) - // : 0; - - // // ... but we still take a snapshot to ensure the rewards are not accumulated for the next valid POI - // _allocations.snapshotRewards( - // _allocationId, - // _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) - // ); - // _allocations.presentPOI(_allocationId); - - // // Any pending rewards should have been collected now - // _allocations.clearPendingRewards(_allocationId); - - // uint256 tokensIndexerRewards = 0; - // uint256 tokensDelegationRewards = 0; - // if (tokensRewards != 0) { - // // Distribute rewards to delegators - // uint256 delegatorCut = _graphStaking().getDelegationFeeCut( - // allocation.indexer, - // address(this), - // IGraphPayments.PaymentTypes.IndexingRewards - // ); - // IHorizonStakingTypes.DelegationPool memory delegationPool = _graphStaking().getDelegationPool( - // allocation.indexer, - // address(this) - // ); - // // If delegation pool has no shares then we don't need to distribute rewards to delegators - // tokensDelegationRewards = delegationPool.shares > 0 ? tokensRewards.mulPPM(delegatorCut) : 0; - // if (tokensDelegationRewards > 0) { - // _graphToken().approve(address(_graphStaking()), tokensDelegationRewards); - // _graphStaking().addToDelegationPool(allocation.indexer, address(this), tokensDelegationRewards); - // } - - // // Distribute rewards to indexer - // tokensIndexerRewards = tokensRewards - tokensDelegationRewards; - // if (tokensIndexerRewards > 0) { - // if (_paymentsDestination == address(0)) { - // _graphToken().approve(address(_graphStaking()), tokensIndexerRewards); - // _graphStaking().stakeToProvision(allocation.indexer, address(this), tokensIndexerRewards); - // } else { - // _graphToken().pushTokens(_paymentsDestination, tokensIndexerRewards); - // } - // } - // } - - // emit IndexingRewardsCollected( - // allocation.indexer, - // _allocationId, - // allocation.subgraphDeploymentId, - // tokensRewards, - // tokensIndexerRewards, - // tokensDelegationRewards, - // _poi, - // _poiMetadata, - // _graphEpochManager().currentEpoch() - // ); - - // // Check if the indexer is over-allocated and force close the allocation if necessary - // if (_isOverAllocated(allocation.indexer, _delegationRatio)) { - // _closeAllocation(_allocationId, true); - // } - - // return tokensRewards; - return AllocationManagerLib.presentPOI( _allocations, @@ -446,30 +346,6 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca * @param _forceClosed Whether the allocation was force closed */ function _closeAllocation(address _allocationId, bool _forceClosed) internal { - // Allocation.State memory allocation = _allocations.get(_allocationId); - - // // Take rewards snapshot to prevent other allos from counting tokens from this allo - // _allocations.snapshotRewards( - // _allocationId, - // _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) - // ); - - // _allocations.close(_allocationId); - // allocationProvisionTracker.release(allocation.indexer, allocation.tokens); - - // // Update total allocated tokens for the subgraph deployment - // _subgraphAllocatedTokens[allocation.subgraphDeploymentId] = - // _subgraphAllocatedTokens[allocation.subgraphDeploymentId] - - // allocation.tokens; - - // emit AllocationClosed( - // allocation.indexer, - // _allocationId, - // allocation.subgraphDeploymentId, - // allocation.tokens, - // _forceClosed - // ); - AllocationManagerLib.closeAllocation( _allocations, allocationProvisionTracker, From 014c4a0f2f02c0a4fa5d6e1906f948f2072712fe Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 3 Jun 2025 15:17:18 -0300 Subject: [PATCH 27/90] f: remove linter warnings --- IndexingPaymentsTodo.md | 1 - .../libraries/DataServiceFeesLib.sol | 28 ++- .../libraries/ProvisionTracker.sol | 36 ++-- .../contracts/SubgraphService.sol | 190 +++++++++--------- .../contracts/interfaces/ISubgraphService.sol | 13 +- .../contracts/libraries/Allocation.sol | 28 +-- 6 files changed, 153 insertions(+), 143 deletions(-) diff --git a/IndexingPaymentsTodo.md b/IndexingPaymentsTodo.md index 83e93db41..37403d618 100644 --- a/IndexingPaymentsTodo.md +++ b/IndexingPaymentsTodo.md @@ -1,6 +1,5 @@ # Still pending -* Remove linter warnings * Remove extension if I can fit everything in one service? * `require(provision.tokens != 0, DisputeManagerZeroTokens());` - Document or fix? * Check code coverage diff --git a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol index ff0868321..c816ee4a1 100644 --- a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol +++ b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol @@ -10,20 +10,12 @@ library DataServiceFeesLib { using ProvisionTracker for mapping(address => uint256); using LinkedList for LinkedList.List; - /** - * @notice Builds a stake claim ID - * @param serviceProvider The address of the service provider - * @param nonce A nonce of the stake claim - * @return The stake claim ID - */ - function _buildStakeClaimId(address serviceProvider, uint256 nonce) internal view returns (bytes32) { - return keccak256(abi.encodePacked(address(this), serviceProvider, nonce)); - } - + // @notice Storage structure for the data service fees library struct Storage { ProvisionManagerStorage provisionManagerStorage; } + // @notice Storage structure for the provision manager struct ProvisionManagerStorage { uint256 _minimumProvisionTokens; uint256 _maximumProvisionTokens; @@ -75,6 +67,12 @@ library DataServiceFeesLib { emit IDataServiceFees.StakeClaimLocked(_serviceProvider, claimId, _tokens, _unlockTimestamp); } + /** + * @notice Processes a stake claim, releasing the tokens if the claim has expired. + * @dev This function is used as a callback in the stake claims linked list traversal. + * @return Whether the stake claim is still locked, indicating that the traversal should continue or stop. + * @return The updated accumulator data + */ function processStakeClaim( mapping(address serviceProvider => uint256 tokens) storage feesProvisionTracker, mapping(bytes32 claimId => IDataServiceFees.StakeClaim claim) storage claims, @@ -100,4 +98,14 @@ library DataServiceFeesLib { _acc = abi.encode(tokensClaimed + claim.tokens, serviceProvider); return (false, _acc); } + + /** + * @notice Builds a stake claim ID + * @param serviceProvider The address of the service provider + * @param nonce A nonce of the stake claim + * @return The stake claim ID + */ + function _buildStakeClaimId(address serviceProvider, uint256 nonce) internal view returns (bytes32) { + return keccak256(abi.encodePacked(address(this), serviceProvider, nonce)); + } } diff --git a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol index e0ccfa645..cb26b7ffc 100644 --- a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol +++ b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol @@ -21,6 +21,24 @@ library ProvisionTracker { */ error ProvisionTrackerInsufficientTokens(uint256 tokensAvailable, uint256 tokensRequired); + /** + * @notice Checks if a service provider has enough tokens available to lock + * @param self The provision tracker mapping + * @param graphStaking The HorizonStaking contract + * @param serviceProvider The service provider address + * @param delegationRatio A delegation ratio to limit the amount of delegation that's usable + * @return true if the service provider has enough tokens available to lock, false otherwise + */ + function check( + mapping(address => uint256) storage self, + IHorizonStaking graphStaking, + address serviceProvider, + uint32 delegationRatio + ) external view returns (bool) { + uint256 tokensAvailable = graphStaking.getTokensAvailable(serviceProvider, address(this), delegationRatio); + return self[serviceProvider] <= tokensAvailable; + } + /** * @notice Locks tokens for a service provider * @dev Requirements: @@ -59,22 +77,4 @@ library ProvisionTracker { require(self[serviceProvider] >= tokens, ProvisionTrackerInsufficientTokens(self[serviceProvider], tokens)); self[serviceProvider] -= tokens; } - - /** - * @notice Checks if a service provider has enough tokens available to lock - * @param self The provision tracker mapping - * @param graphStaking The HorizonStaking contract - * @param serviceProvider The service provider address - * @param delegationRatio A delegation ratio to limit the amount of delegation that's usable - * @return true if the service provider has enough tokens available to lock, false otherwise - */ - function check( - mapping(address => uint256) storage self, - IHorizonStaking graphStaking, - address serviceProvider, - uint32 delegationRatio - ) external view returns (bool) { - uint256 tokensAvailable = graphStaking.getTokensAvailable(serviceProvider, address(this), delegationRatio); - return self[serviceProvider] <= tokensAvailable; - } } diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index c32bbc51d..f8504d39d 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -60,10 +60,6 @@ contract SubgraphService is _; } - function requireRegisteredIndexer(address indexer) external view { - require(indexers[indexer].registeredAt != 0, SubgraphServiceIndexerNotRegistered(indexer)); - } - /** * @notice Constructor for the SubgraphService contract * @dev DataService and Directory constructors set a bunch of immutable variables @@ -104,6 +100,35 @@ contract SubgraphService is _setStakeToFeesRatio(stakeToFeesRatio_); } + /** + * @notice Delegates the call to the SubgraphServiceExtension implementation. + * @dev This function does not return to its internal call site, it will return directly to the + * external caller. + */ + // solhint-disable-next-line payable-fallback, no-complex-fallback + fallback() external { + address extImpl = _subgraphServiceExtensionImpl(); + require(extImpl != address(0), "only through proxy"); + + // solhint-disable-next-line no-inline-assembly + assembly { + // copy function selector and any arguments + calldatacopy(0, 0, calldatasize()) + // execute function call using the extension implementation + let result := delegatecall(gas(), extImpl, 0, calldatasize(), 0, 0) + // get any return value + returndatacopy(0, 0, returndatasize()) + // return any return value or error back to the caller + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } + /** * @notice * @dev Implements {IDataService.register} @@ -393,6 +418,44 @@ contract SubgraphService is emit CurationCutSet(curationCut); } + /** + * @notice Accept an indexing agreement. + * See {ISubgraphService.acceptIndexingAgreement}. + * + * Requirements: + * - The agreement's indexer must be registered + * - The caller must be authorized by the agreement's indexer + * - The provision must be valid according to the subgraph service rules + * - Allocation must belong to the indexer and be open + * - Agreement must be for this data service + * - Agreement's subgraph deployment must match the allocation's subgraph deployment + * - Agreement must not have been accepted before + * - Allocation must not have an agreement already + * + * @dev signedRCA.rca.metadata is an encoding of {IndexingAgreement.AcceptIndexingAgreementMetadata} + * + * Emits {IndexingAgreementAccepted} event + * + * @param allocationId The id of the allocation + * @param signedRCA The signed Recurring Collection Agreement + */ + function acceptIndexingAgreement( + address allocationId, + IRecurringCollector.SignedRCA calldata signedRCA + ) + external + whenNotPaused + onlyAuthorizedForProvision(signedRCA.rca.serviceProvider) + onlyValidProvision(signedRCA.rca.serviceProvider) + onlyRegisteredIndexer(signedRCA.rca.serviceProvider) + { + IndexingAgreement._getManager().accept(_allocations, allocationId, signedRCA); + } + + function requireRegisteredIndexer(address indexer) external view { + require(indexers[indexer].registeredAt != 0, SubgraphServiceIndexerNotRegistered(indexer)); + } + /// @inheritdoc ISubgraphService function getAllocation(address allocationId) external view override returns (Allocation.State memory) { return _allocations[allocationId]; @@ -448,6 +511,14 @@ contract SubgraphService is return _isOverAllocated(indexer, _delegationRatio); } + function getGraphStaking() external view returns (address) { + return address(_graphStaking()); + } + + function _cancelAllocationIndexingAgreement(address _allocationId) internal { + IndexingAgreement._getManager().cancelForAllocation(_allocationId); + } + /** * @notice Sets the payments destination for an indexer to receive payments * @dev Emits a {PaymentsDestinationSet} event @@ -601,68 +672,6 @@ contract SubgraphService is return _presentPOI(allocationId, poi_, poiMetadata_, _delegationRatio, paymentsDestination[_indexer]); } - /** - * @notice Set the stake to fees ratio. - * @param _stakeToFeesRatio The stake to fees ratio - */ - function _setStakeToFeesRatio(uint256 _stakeToFeesRatio) private { - require(_stakeToFeesRatio != 0, SubgraphServiceInvalidZeroStakeToFeesRatio()); - stakeToFeesRatio = _stakeToFeesRatio; - emit StakeToFeesRatioSet(_stakeToFeesRatio); - } - - /** - * @notice Encodes the data for the GraphTallyCollector - * @dev The purpose of this function is just to avoid stack too deep errors - * @param _signedRav The signed RAV - * @param _curationCut The curation cut - * @return The encoded data - */ - function _encodeGraphTallyData( - IGraphTallyCollector.SignedRAV memory _signedRav, - uint256 _curationCut - ) private view returns (bytes memory) { - return abi.encode(_signedRav, _curationCut, paymentsDestination[_signedRav.rav.serviceProvider]); - } - - function _cancelAllocationIndexingAgreement(address _allocationId) internal { - IndexingAgreement._getManager().cancelForAllocation(_allocationId); - } - - /** - * @notice Accept an indexing agreement. - * See {ISubgraphService.acceptIndexingAgreement}. - * - * Requirements: - * - The agreement's indexer must be registered - * - The caller must be authorized by the agreement's indexer - * - The provision must be valid according to the subgraph service rules - * - Allocation must belong to the indexer and be open - * - Agreement must be for this data service - * - Agreement's subgraph deployment must match the allocation's subgraph deployment - * - Agreement must not have been accepted before - * - Allocation must not have an agreement already - * - * @dev signedRCA.rca.metadata is an encoding of {IndexingAgreement.AcceptIndexingAgreementMetadata} - * - * Emits {IndexingAgreementAccepted} event - * - * @param allocationId The id of the allocation - * @param signedRCA The signed Recurring Collection Agreement - */ - function acceptIndexingAgreement( - address allocationId, - IRecurringCollector.SignedRCA calldata signedRCA - ) - external - whenNotPaused - onlyAuthorizedForProvision(signedRCA.rca.serviceProvider) - onlyValidProvision(signedRCA.rca.serviceProvider) - onlyRegisteredIndexer(signedRCA.rca.serviceProvider) - { - IndexingAgreement._getManager().accept(_allocations, allocationId, signedRCA); - } - /** * @notice Collect Indexing fees * Stake equal to the amount being collected times the `stakeToFeesRatio` is locked into a stake claim. @@ -702,6 +711,16 @@ contract SubgraphService is return tokensCollected; } + /** + * @notice Set the stake to fees ratio. + * @param _stakeToFeesRatio The stake to fees ratio + */ + function _setStakeToFeesRatio(uint256 _stakeToFeesRatio) private { + require(_stakeToFeesRatio != 0, SubgraphServiceInvalidZeroStakeToFeesRatio()); + stakeToFeesRatio = _stakeToFeesRatio; + emit StakeToFeesRatioSet(_stakeToFeesRatio); + } + function _releaseAndLockStake(address _indexer, uint256 _tokensCollected) private { _releaseStake(_indexer, 0); if (_tokensCollected > 0) { @@ -714,36 +733,17 @@ contract SubgraphService is } } - function getGraphStaking() external view returns (address) { - return address(_graphStaking()); - } - /** - * @notice Delegates the call to the SubgraphServiceExtension implementation. - * @dev This function does not return to its internal call site, it will return directly to the - * external caller. + * @notice Encodes the data for the GraphTallyCollector + * @dev The purpose of this function is just to avoid stack too deep errors + * @param _signedRav The signed RAV + * @param _curationCut The curation cut + * @return The encoded data */ - // solhint-disable-next-line payable-fallback, no-complex-fallback - fallback() external { - address extImpl = _subgraphServiceExtensionImpl(); - require(extImpl != address(0), "only through proxy"); - - // solhint-disable-next-line no-inline-assembly - assembly { - // copy function selector and any arguments - calldatacopy(0, 0, calldatasize()) - // execute function call using the extension implementation - let result := delegatecall(gas(), extImpl, 0, calldatasize(), 0, 0) - // get any return value - returndatacopy(0, 0, returndatasize()) - // return any return value or error back to the caller - switch result - case 0 { - revert(0, returndatasize()) - } - default { - return(0, returndatasize()) - } - } + function _encodeGraphTallyData( + IGraphTallyCollector.SignedRAV memory _signedRav, + uint256 _curationCut + ) private view returns (bytes memory) { + return abi.encode(_signedRav, _curationCut, paymentsDestination[_signedRav.rav.serviceProvider]); } } diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol index 2f63d7240..8e806bd10 100644 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol @@ -259,6 +259,14 @@ interface ISubgraphService is IDataServiceFees { */ function setPaymentsDestination(address paymentsDestination) external; + /** + * @notice Accept an indexing agreement. + * @dev Emits a {IndexingAgreement.IndexingAgreementAccepted} event + * @param allocationId The id of the allocation + * @param signedRCA The signed recurring collector agreement (RCA) that the indexer accepts + */ + function acceptIndexingAgreement(address allocationId, IRecurringCollector.SignedRCA calldata signedRCA) external; + /** * @notice Gets the details of an allocation * For legacy allocations use {getLegacyAllocation} @@ -307,9 +315,4 @@ interface ISubgraphService is IDataServiceFees { * @return The address of the curation contract */ function getCuration() external view returns (address); - - /** - * @notice Accept an indexing agreement. - */ - function acceptIndexingAgreement(address allocationId, IRecurringCollector.SignedRCA calldata signedRCA) external; } diff --git a/packages/subgraph-service/contracts/libraries/Allocation.sol b/packages/subgraph-service/contracts/libraries/Allocation.sol index b4043f5b9..eb91da92d 100644 --- a/packages/subgraph-service/contracts/libraries/Allocation.sol +++ b/packages/subgraph-service/contracts/libraries/Allocation.sol @@ -111,47 +111,47 @@ library Allocation { } /** - * @notice Update the accumulated rewards per allocated token for an allocation + * @notice Update the accumulated rewards pending to be claimed for an allocation * @dev Requirements: * - The allocation must be open * @param self The allocation list mapping * @param allocationId The allocation id - * @param accRewardsPerAllocatedToken The new accumulated rewards per allocated token */ - function snapshotRewards( - mapping(address => State) storage self, - address allocationId, - uint256 accRewardsPerAllocatedToken - ) internal { + function clearPendingRewards(mapping(address => State) storage self, address allocationId) external { State storage allocation = _get(self, allocationId); require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); - allocation.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + allocation.accRewardsPending = 0; } /** - * @notice Update the accumulated rewards pending to be claimed for an allocation + * @notice Close an allocation * @dev Requirements: * - The allocation must be open * @param self The allocation list mapping * @param allocationId The allocation id */ - function clearPendingRewards(mapping(address => State) storage self, address allocationId) external { + function close(mapping(address => State) storage self, address allocationId) external { State storage allocation = _get(self, allocationId); require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); - allocation.accRewardsPending = 0; + allocation.closedAt = block.timestamp; } /** - * @notice Close an allocation + * @notice Update the accumulated rewards per allocated token for an allocation * @dev Requirements: * - The allocation must be open * @param self The allocation list mapping * @param allocationId The allocation id + * @param accRewardsPerAllocatedToken The new accumulated rewards per allocated token */ - function close(mapping(address => State) storage self, address allocationId) external { + function snapshotRewards( + mapping(address => State) storage self, + address allocationId, + uint256 accRewardsPerAllocatedToken + ) internal { State storage allocation = _get(self, allocationId); require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); - allocation.closedAt = block.timestamp; + allocation.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; } /** From 7a72df95fa2518254d10adacbb9a0d24b62b5915 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 11:06:52 -0300 Subject: [PATCH 28/90] f: minor cleanup --- .../subgraph-service/contracts/utilities/Directory.sol | 8 ++++---- .../subgraphService/indexing-agreement/integration.t.sol | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/subgraph-service/contracts/utilities/Directory.sol b/packages/subgraph-service/contracts/utilities/Directory.sol index 2e6cbf2dd..f90fe9beb 100644 --- a/packages/subgraph-service/contracts/utilities/Directory.sol +++ b/packages/subgraph-service/contracts/utilities/Directory.sol @@ -77,7 +77,7 @@ abstract contract Directory { * @param graphTallyCollector The Graph Tally Collector contract address * @param curation The Curation contract address * @param recurringCollector_ The Recurring Collector contract address - * @param subgraphServiceExtensionImpl_ The Subgraph Service Extension contract address + * @param subgraphServiceExtensionImpl The Subgraph Service Extension contract address */ constructor( address subgraphService, @@ -85,14 +85,14 @@ abstract contract Directory { address graphTallyCollector, address curation, address recurringCollector_, - address subgraphServiceExtensionImpl_ + address subgraphServiceExtensionImpl ) { SUBGRAPH_SERVICE = ISubgraphService(subgraphService); DISPUTE_MANAGER = IDisputeManager(disputeManager); GRAPH_TALLY_COLLECTOR = IGraphTallyCollector(graphTallyCollector); CURATION = ICuration(curation); RECURRING_COLLECTOR = IRecurringCollector(recurringCollector_); - SUBGRAPH_SERVICE_EXTENSION_IMPL = subgraphServiceExtensionImpl_; + SUBGRAPH_SERVICE_EXTENSION_IMPL = subgraphServiceExtensionImpl; emit SubgraphServiceDirectoryInitialized( subgraphService, @@ -100,7 +100,7 @@ abstract contract Directory { graphTallyCollector, curation, recurringCollector_, - subgraphServiceExtensionImpl_ + subgraphServiceExtensionImpl ); } diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol index 7d5a4399c..399d3c312 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol @@ -4,11 +4,12 @@ pragma solidity 0.8.27; import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; + import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; -contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAgreementSharedTest { +contract SubgraphServiceIndexingAgreementIntegrationTest is SubgraphServiceIndexingAgreementSharedTest { using PPMMath for uint256; struct TestState { From 0eb9ccb86682f274ccd7623b3ad679c1623cc444 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 12:01:27 -0300 Subject: [PATCH 29/90] f: more natSpec --- .../interfaces/IRecurringCollector.sol | 11 +++++++ .../contracts/SubgraphService.sol | 2 +- .../contracts/SubgraphServiceExtension.sol | 32 +++++++++++++++---- .../contracts/interfaces/ISubgraphService.sol | 1 - .../interfaces/ISubgraphServiceExtension.sol | 8 ++--- .../contracts/libraries/IndexingAgreement.sol | 28 ++++++++++++++++ 6 files changed, 68 insertions(+), 14 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index 4d78e0958..c95488129 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -13,6 +13,7 @@ import { IAuthorizable } from "./IAuthorizable.sol"; * recurrent payments. */ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { + // @notice The state of an agreement enum AgreementState { NotAccepted, Accepted, @@ -20,6 +21,7 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { CanceledByPayer } + // @notice The party that can cancel an agreement enum CancelAgreementBy { ServiceProvider, Payer @@ -69,6 +71,7 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { bytes signature; } + /// @notice The Recurring Collection Agreement Update (RCAU) struct RecurringCollectionAgreementUpdate { // The agreement ID bytes16 agreementId; @@ -206,6 +209,10 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { * @param dataService The address of the data service * @param payer The address of the payer * @param serviceProvider The address of the service provider + * @param agreementId The agreement ID + * @param collectionId The collection ID + * @param tokens The amount of tokens collected + * @param dataServiceCut The tokens cut for the data service */ event RCACollected( address indexed dataService, @@ -276,6 +283,7 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { /** * Thrown when accepting or upgrading an agreement with invalid parameters + * @param message A descriptive error message */ error RecurringCollectorAgreementInvalidParameters(string message); @@ -317,6 +325,7 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { /** * @dev Update an indexing agreement. + * @param signedRCAU The signed Recurring Collection Agreement Update which is to be applied. */ function update(SignedRCAU calldata signedRCAU) external; @@ -350,6 +359,8 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { /** * @notice Gets an agreement. + * @param agreementId The ID of the agreement to retrieve. + * @return The AgreementData struct containing the agreement's data. */ function getAgreement(bytes16 agreementId) external view returns (AgreementData memory); } diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index f8504d39d..faefc14d2 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -420,7 +420,7 @@ contract SubgraphService is /** * @notice Accept an indexing agreement. - * See {ISubgraphService.acceptIndexingAgreement}. + * See {ISubgraphServiceExtended.acceptIndexingAgreement}. * * Requirements: * - The agreement's indexer must be registered diff --git a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol index 16911c1c8..0a569bf05 100644 --- a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol +++ b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol @@ -13,6 +13,16 @@ import { ISubgraphServiceExtension } from "./interfaces/ISubgraphServiceExtensio contract SubgraphServiceExtension is PausableUpgradeable, ISubgraphServiceExtension { using IndexingAgreement for IndexingAgreement.Manager; + /** + * @notice Checks that an indexer is valid + * @param indexer The address of the indexer + * + * Requirements: + * - The caller must be authorized by the indexer + * - The provision must be valid according to the subgraph service rules + * - The indexer must be registered + * + */ modifier onlyValid(address indexer) { ProvisionManagerLib.requireAuthorizedForProvision( IHorizonStaking(_getBase().getGraphStaking()), @@ -25,6 +35,17 @@ contract SubgraphServiceExtension is PausableUpgradeable, ISubgraphServiceExtens _; } + /** + * @notice Update an indexing agreement. + * See {IndexingAgreement.update}. + * + * Requirements: + * - The contract must not be paused + * - The indexer must be valid + * + * @param indexer The indexer address + * @param signedRCAU The signed Recurring Collection Agreement Update + */ function updateIndexingAgreement( address indexer, IRecurringCollector.SignedRCAU calldata signedRCAU @@ -34,18 +55,15 @@ contract SubgraphServiceExtension is PausableUpgradeable, ISubgraphServiceExtens /** * @notice Cancel an indexing agreement by indexer / operator. - * See {ISubgraphService.cancelIndexingAgreement}. + * See {IndexingAgreement.cancel}. * * @dev Can only be canceled on behalf of a valid indexer. * * Requirements: - * - The indexer must be registered - * - The caller must be authorized by the indexer - * - The provision must be valid according to the subgraph service rules - * - The agreement must be active - * - * Emits {IndexingAgreementCanceled} event + * - The contract must not be paused + * - The indexer must be valid * + * @param indexer The indexer address * @param agreementId The id of the agreement */ function cancelIndexingAgreement(address indexer, bytes16 agreementId) external whenNotPaused onlyValid(indexer) { diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol index 8e806bd10..348552253 100644 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol @@ -261,7 +261,6 @@ interface ISubgraphService is IDataServiceFees { /** * @notice Accept an indexing agreement. - * @dev Emits a {IndexingAgreement.IndexingAgreementAccepted} event * @param allocationId The id of the allocation * @param signedRCA The signed recurring collector agreement (RCA) that the indexer accepts */ diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol index 4e2fb2873..9f8377bfe 100644 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol @@ -6,11 +6,6 @@ import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces import { IndexingAgreement } from "../libraries/IndexingAgreement.sol"; interface ISubgraphServiceExtension { - /** - * @notice Accept an indexing agreement. - */ - // function acceptIndexingAgreement(address allocationId, IRecurringCollector.SignedRCA calldata signedRCA) external; - /** * @notice Update an indexing agreement. */ @@ -26,6 +21,9 @@ interface ISubgraphServiceExtension { */ function cancelIndexingAgreementByPayer(bytes16 agreementId) external; + /** + * @notice Get the indexing agreement for a given agreement ID. + */ function getIndexingAgreement( bytes16 agreementId ) external view returns (IndexingAgreement.AgreementWrapper memory); diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 8eb092a55..10b84ac0e 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -273,6 +273,21 @@ library IndexingAgreement { _directory().recurringCollector().accept(signedRCA); } + /** + * @notice Update an indexing agreement. + * + * Requirements: + * - Agreement must be active + * - The indexer must be the service provider of the agreement + * + * @dev signedRCA.rcau.metadata is an encoding of {IndexingAgreement.UpdateIndexingAgreementMetadata} + * + * Emits {IndexingAgreementUpdated} event + * + * @param self The indexing agreement manager storage + * @param indexer The indexer address + * @param signedRCAU The signed Recurring Collection Agreement Update + */ function update( Manager storage self, address indexer, @@ -304,6 +319,19 @@ library IndexingAgreement { _directory().recurringCollector().update(signedRCAU); } + /** + * @notice Cancel an indexing agreement. + * + * Requirements: + * - Agreement must be active + * - The indexer must be the service provider of the agreement + * + * Emits {IndexingAgreementCanceled} event + * + * @param self The indexing agreement manager storage + * @param indexer The indexer address + * @param agreementId The id of the agreement to cancel + */ function cancel(Manager storage self, address indexer, bytes16 agreementId) external { AgreementWrapper memory wrapper = _get(self, agreementId); require(_isActive(wrapper), IndexingAgreementNotActive(agreementId)); From c6dee3cb9164f52ff9aa189647ba2cb06aa3bfe7 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 12:05:51 -0300 Subject: [PATCH 30/90] f: all external ProvisionTracker --- .../libraries/ProvisionTracker.sol | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol index cb26b7ffc..a152b0958 100644 --- a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol +++ b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol @@ -21,24 +21,6 @@ library ProvisionTracker { */ error ProvisionTrackerInsufficientTokens(uint256 tokensAvailable, uint256 tokensRequired); - /** - * @notice Checks if a service provider has enough tokens available to lock - * @param self The provision tracker mapping - * @param graphStaking The HorizonStaking contract - * @param serviceProvider The service provider address - * @param delegationRatio A delegation ratio to limit the amount of delegation that's usable - * @return true if the service provider has enough tokens available to lock, false otherwise - */ - function check( - mapping(address => uint256) storage self, - IHorizonStaking graphStaking, - address serviceProvider, - uint32 delegationRatio - ) external view returns (bool) { - uint256 tokensAvailable = graphStaking.getTokensAvailable(serviceProvider, address(this), delegationRatio); - return self[serviceProvider] <= tokensAvailable; - } - /** * @notice Locks tokens for a service provider * @dev Requirements: @@ -55,7 +37,7 @@ library ProvisionTracker { address serviceProvider, uint256 tokens, uint32 delegationRatio - ) internal { + ) external { if (tokens == 0) return; uint256 tokensRequired = self[serviceProvider] + tokens; @@ -72,9 +54,27 @@ library ProvisionTracker { * @param serviceProvider The service provider address * @param tokens The amount of tokens to release */ - function release(mapping(address => uint256) storage self, address serviceProvider, uint256 tokens) internal { + function release(mapping(address => uint256) storage self, address serviceProvider, uint256 tokens) external { if (tokens == 0) return; require(self[serviceProvider] >= tokens, ProvisionTrackerInsufficientTokens(self[serviceProvider], tokens)); self[serviceProvider] -= tokens; } + + /** + * @notice Checks if a service provider has enough tokens available to lock + * @param self The provision tracker mapping + * @param graphStaking The HorizonStaking contract + * @param serviceProvider The service provider address + * @param delegationRatio A delegation ratio to limit the amount of delegation that's usable + * @return true if the service provider has enough tokens available to lock, false otherwise + */ + function check( + mapping(address => uint256) storage self, + IHorizonStaking graphStaking, + address serviceProvider, + uint32 delegationRatio + ) external view returns (bool) { + uint256 tokensAvailable = graphStaking.getTokensAvailable(serviceProvider, address(this), delegationRatio); + return self[serviceProvider] <= tokensAvailable; + } } From a1d1a79e25f68942bf1768d6b4a2215715c56136 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 12:07:30 -0300 Subject: [PATCH 31/90] f: remove DataServiceFeesLib.Storage --- .../contracts/data-service/libraries/DataServiceFeesLib.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol index c816ee4a1..0c0a272bc 100644 --- a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol +++ b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol @@ -10,11 +10,6 @@ library DataServiceFeesLib { using ProvisionTracker for mapping(address => uint256); using LinkedList for LinkedList.List; - // @notice Storage structure for the data service fees library - struct Storage { - ProvisionManagerStorage provisionManagerStorage; - } - // @notice Storage structure for the provision manager struct ProvisionManagerStorage { uint256 _minimumProvisionTokens; From e6550a334aed7c2927212d4f03ab5687b93aa714 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 12:11:01 -0300 Subject: [PATCH 32/90] f: correct params order --- .../contracts/data-service/extensions/DataServiceFees.sol | 2 +- .../contracts/data-service/libraries/DataServiceFeesLib.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol index 1dfea9ad0..df42623cf 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -43,11 +43,11 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat */ function _lockStake(address _serviceProvider, uint256 _tokens, uint256 _unlockTimestamp) internal { DataServiceFeesLib.lockStake( - _delegationRatio, feesProvisionTracker, claims, claimsLists, _graphStaking(), + _delegationRatio, _serviceProvider, _tokens, _unlockTimestamp diff --git a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol index 0c0a272bc..ee6759143 100644 --- a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol +++ b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol @@ -34,11 +34,11 @@ library DataServiceFeesLib { * @param _unlockTimestamp The timestamp when the tokens can be released */ function lockStake( - uint32 _delegationRatio, mapping(address => uint256) storage feesProvisionTracker, mapping(bytes32 => IDataServiceFees.StakeClaim) storage claims, mapping(address serviceProvider => LinkedList.List list) storage claimsLists, IHorizonStaking graphStaking, + uint32 _delegationRatio, address _serviceProvider, uint256 _tokens, uint256 _unlockTimestamp From 23537accd973754e675856f8550f710fc4b12335 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 13:13:51 -0300 Subject: [PATCH 33/90] f: remove this. from ProvisionManager --- .../contracts/data-service/utilities/ProvisionManager.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol index ba1d2fe50..2d282f137 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -122,11 +122,15 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa * @param serviceProvider The address of the service provider. */ modifier onlyValidProvision(address serviceProvider) virtual { - this.requireValidProvision(serviceProvider); + _requireValidProvision(serviceProvider); _; } function requireValidProvision(address serviceProvider) external view { + _requireValidProvision(serviceProvider); + } + + function _requireValidProvision(address serviceProvider) internal view { IHorizonStaking.Provision memory provision = _getProvision(serviceProvider); _checkProvisionTokens(provision); _checkProvisionParameters(provision, false); From 5c7268d26d0d2e6c064a19eed68e7959e170cdb7 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 13:17:43 -0300 Subject: [PATCH 34/90] f: revert LinkedList to internal --- packages/horizon/contracts/libraries/LinkedList.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/horizon/contracts/libraries/LinkedList.sol b/packages/horizon/contracts/libraries/LinkedList.sol index 1edebd783..af0f1dad9 100644 --- a/packages/horizon/contracts/libraries/LinkedList.sol +++ b/packages/horizon/contracts/libraries/LinkedList.sol @@ -72,7 +72,7 @@ library LinkedList { * @param self The list metadata * @param id The id of the item to add */ - function addTail(List storage self, bytes32 id) external { + function addTail(List storage self, bytes32 id) internal { require(self.count < MAX_ITEMS, LinkedListMaxElementsExceeded()); require(id != bytes32(0), LinkedListInvalidZeroId()); self.tail = id; From fb83a038a91c33c46b564ac9cb134717d74dd6b1 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 13:23:49 -0300 Subject: [PATCH 35/90] f: remove this. from SubgraphService --- packages/subgraph-service/contracts/SubgraphService.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index faefc14d2..5262b6f33 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -56,7 +56,7 @@ contract SubgraphService is * @param indexer The address of the indexer */ modifier onlyRegisteredIndexer(address indexer) { - this.requireRegisteredIndexer(indexer); + _requireRegisteredIndexer(indexer); _; } @@ -453,7 +453,7 @@ contract SubgraphService is } function requireRegisteredIndexer(address indexer) external view { - require(indexers[indexer].registeredAt != 0, SubgraphServiceIndexerNotRegistered(indexer)); + _requireRegisteredIndexer(indexer); } /// @inheritdoc ISubgraphService @@ -515,6 +515,10 @@ contract SubgraphService is return address(_graphStaking()); } + function _requireRegisteredIndexer(address indexer) internal view { + require(indexers[indexer].registeredAt != 0, SubgraphServiceIndexerNotRegistered(indexer)); + } + function _cancelAllocationIndexingAgreement(address _allocationId) internal { IndexingAgreement._getManager().cancelForAllocation(_allocationId); } From ac7390510ec15380da80da3dfb7e7d3f158cb106 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 13:31:37 -0300 Subject: [PATCH 36/90] f: remove _verifyAllocationProof duplicated --- .../contracts/libraries/AllocationManagerLib.sol | 1 + .../contracts/utilities/AllocationManager.sol | 14 -------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol index 8e9a5f7bf..28eecbbe2 100644 --- a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol +++ b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol @@ -278,6 +278,7 @@ library AllocationManagerLib { * @notice Verifies ownership of an allocation id by verifying an EIP712 allocation proof * @dev Requirements: * - Signer must be the allocation id address + * @param _encodeAllocationProof The EIP712 encoded allocation proof * @param _allocationId The id of the allocation * @param _proof The EIP712 proof, an EIP712 signed message of (indexer,allocationId) */ diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index e85c9a1b0..f1a9a02ee 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -7,7 +7,6 @@ import { GraphDirectory } from "@graphprotocol/horizon/contracts/utilities/Graph import { AllocationManagerV1Storage } from "./AllocationManagerStorage.sol"; import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { EIP712Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; import { Allocation } from "../libraries/Allocation.sol"; import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; @@ -391,17 +390,4 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca _delegationRatio ); } - - /** - * @notice Verifies ownership of an allocation id by verifying an EIP712 allocation proof - * @dev Requirements: - * - Signer must be the allocation id address - * @param _indexer The address of the indexer - * @param _allocationId The id of the allocation - * @param _proof The EIP712 proof, an EIP712 signed message of (indexer,allocationId) - */ - function _verifyAllocationProof(address _indexer, address _allocationId, bytes memory _proof) private view { - address signer = ECDSA.recover(_encodeAllocationProof(_indexer, _allocationId), _proof); - require(signer == _allocationId, AllocationManagerInvalidAllocationProof(signer, _allocationId)); - } } From 88301f9b2adb4f0532aa8181c3662b62d644b887 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 13:35:03 -0300 Subject: [PATCH 37/90] f: lint warnings --- .../data-service/utilities/ProvisionManager.sol | 12 ++++++------ .../subgraph-service/contracts/SubgraphService.sol | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol index 2d282f137..eee3054ca 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -130,12 +130,6 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa _requireValidProvision(serviceProvider); } - function _requireValidProvision(address serviceProvider) internal view { - IHorizonStaking.Provision memory provision = _getProvision(serviceProvider); - _checkProvisionTokens(provision); - _checkProvisionParameters(provision, false); - } - /** * @notice Initializes the contract and any parent contracts. */ @@ -213,6 +207,12 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa emit ThawingPeriodRangeSet(_min, _max); } + function _requireValidProvision(address _serviceProvider) internal view { + IHorizonStaking.Provision memory provision = _getProvision(_serviceProvider); + _checkProvisionTokens(provision); + _checkProvisionParameters(provision, false); + } + // -- checks -- /** diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 5262b6f33..f9c329af0 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -515,10 +515,6 @@ contract SubgraphService is return address(_graphStaking()); } - function _requireRegisteredIndexer(address indexer) internal view { - require(indexers[indexer].registeredAt != 0, SubgraphServiceIndexerNotRegistered(indexer)); - } - function _cancelAllocationIndexingAgreement(address _allocationId) internal { IndexingAgreement._getManager().cancelForAllocation(_allocationId); } @@ -534,6 +530,10 @@ contract SubgraphService is emit PaymentsDestinationSet(_indexer, _paymentsDestination); } + function _requireRegisteredIndexer(address _indexer) internal view { + require(indexers[_indexer].registeredAt != 0, SubgraphServiceIndexerNotRegistered(_indexer)); + } + // -- Data service parameter getters -- /** * @notice Getter for the accepted thawing period range for provisions From 71315b49f48fdc2eae0c42e72eddaaad79132675 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 13:35:51 -0300 Subject: [PATCH 38/90] f: removed EIP712_ALLOCATION_ID_PROOF_TYPEHASH --- .../contracts/libraries/AllocationManagerLib.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol index 28eecbbe2..ef0df9cd2 100644 --- a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol +++ b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol @@ -50,10 +50,6 @@ library AllocationManagerLib { address _paymentsDestination; } - ///@dev EIP712 typehash for allocation id proof - bytes32 private constant EIP712_ALLOCATION_ID_PROOF_TYPEHASH = - keccak256("AllocationIdProof(address indexer,address allocationId)"); - /** * @notice Create an allocation * @dev The `_allocationProof` is a 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationId)` From 73191bdcda9ef7d34dea77da37edd86637759e28 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 14:25:03 -0300 Subject: [PATCH 39/90] HACK: final AllocationManager to lib - 20.785 --- .../libraries/AllocationManagerLib.sol | 74 +++++++++++++++++++ .../contracts/utilities/AllocationManager.sol | 44 +++-------- 2 files changed, 83 insertions(+), 35 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol index ef0df9cd2..428c54e39 100644 --- a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol +++ b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol @@ -213,6 +213,80 @@ library AllocationManagerLib { ); } + /** + * @notice Resize an allocation + * @dev Will lock or release tokens in the provision tracker depending on the new allocation size. + * Rewards accrued but not issued before the resize will be accounted for as pending rewards. + * These will be paid out when the indexer presents a POI. + * + * Requirements: + * - `_indexer` must be the owner of the allocation + * - Allocation must be open + * - `_tokens` must be different from the current allocation size + * + * Emits a {AllocationResized} event. + * + * @param _allocationId The id of the allocation to be resized + * @param _tokens The new amount of tokens to allocate + * @param _delegationRatio The delegation ratio to consider when locking tokens + */ + function resizeAllocation( + mapping(address allocationId => Allocation.State allocation) storage _allocations, + mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, + mapping(bytes32 subgraphDeploymentId => uint256 tokens) storage _subgraphAllocatedTokens, + IHorizonStaking graphStaking, + IRewardsManager graphRewardsManager, + address _allocationId, + uint256 _tokens, + uint32 _delegationRatio + ) external { + Allocation.State memory allocation = _allocations.get(_allocationId); + require(allocation.isOpen(), AllocationManager.AllocationManagerAllocationClosed(_allocationId)); + require( + _tokens != allocation.tokens, + AllocationManager.AllocationManagerAllocationSameSize(_allocationId, _tokens) + ); + + // Update provision tracker + uint256 oldTokens = allocation.tokens; + if (_tokens > oldTokens) { + allocationProvisionTracker.lock(graphStaking, allocation.indexer, _tokens - oldTokens, _delegationRatio); + } else { + allocationProvisionTracker.release(allocation.indexer, oldTokens - _tokens); + } + + // Calculate rewards that have been accrued since the last snapshot but not yet issued + uint256 accRewardsPerAllocatedToken = graphRewardsManager.onSubgraphAllocationUpdate( + allocation.subgraphDeploymentId + ); + uint256 accRewardsPerAllocatedTokenPending = !allocation.isAltruistic() + ? accRewardsPerAllocatedToken - allocation.accRewardsPerAllocatedToken + : 0; + + // Update the allocation + _allocations[_allocationId].tokens = _tokens; + _allocations[_allocationId].accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + _allocations[_allocationId].accRewardsPending += graphRewardsManager.calcRewards( + oldTokens, + accRewardsPerAllocatedTokenPending + ); + + // Update total allocated tokens for the subgraph deployment + if (_tokens > oldTokens) { + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] += (_tokens - oldTokens); + } else { + _subgraphAllocatedTokens[allocation.subgraphDeploymentId] -= (oldTokens - _tokens); + } + + emit AllocationManager.AllocationResized( + allocation.indexer, + _allocationId, + allocation.subgraphDeploymentId, + _tokens, + oldTokens + ); + } + /** * @notice Checks if an allocation is over-allocated * @param _indexer The address of the indexer diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index f1a9a02ee..ccc4676f9 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -294,42 +294,16 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca * @param _delegationRatio The delegation ratio to consider when locking tokens */ function _resizeAllocation(address _allocationId, uint256 _tokens, uint32 _delegationRatio) internal { - Allocation.State memory allocation = _allocations.get(_allocationId); - require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId)); - require(_tokens != allocation.tokens, AllocationManagerAllocationSameSize(_allocationId, _tokens)); - - // Update provision tracker - uint256 oldTokens = allocation.tokens; - if (_tokens > oldTokens) { - allocationProvisionTracker.lock(_graphStaking(), allocation.indexer, _tokens - oldTokens, _delegationRatio); - } else { - allocationProvisionTracker.release(allocation.indexer, oldTokens - _tokens); - } - - // Calculate rewards that have been accrued since the last snapshot but not yet issued - uint256 accRewardsPerAllocatedToken = _graphRewardsManager().onSubgraphAllocationUpdate( - allocation.subgraphDeploymentId - ); - uint256 accRewardsPerAllocatedTokenPending = !allocation.isAltruistic() - ? accRewardsPerAllocatedToken - allocation.accRewardsPerAllocatedToken - : 0; - - // Update the allocation - _allocations[_allocationId].tokens = _tokens; - _allocations[_allocationId].accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; - _allocations[_allocationId].accRewardsPending += _graphRewardsManager().calcRewards( - oldTokens, - accRewardsPerAllocatedTokenPending + AllocationManagerLib.resizeAllocation( + _allocations, + allocationProvisionTracker, + _subgraphAllocatedTokens, + _graphStaking(), + _graphRewardsManager(), + _allocationId, + _tokens, + _delegationRatio ); - - // Update total allocated tokens for the subgraph deployment - if (_tokens > oldTokens) { - _subgraphAllocatedTokens[allocation.subgraphDeploymentId] += (_tokens - oldTokens); - } else { - _subgraphAllocatedTokens[allocation.subgraphDeploymentId] -= (oldTokens - _tokens); - } - - emit AllocationResized(allocation.indexer, _allocationId, allocation.subgraphDeploymentId, _tokens, oldTokens); } /** From c9cf24de33f4a0162f83767658396eb9d88e923c Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 14:52:51 -0300 Subject: [PATCH 40/90] HACK: remove extension - 22.884 --- .../libraries/ProvisionManagerLib.sol | 19 --- .../utilities/ProvisionManager.sol | 10 +- .../contracts/DisputeManager.sol | 15 +-- .../contracts/SubgraphService.sol | 108 ++++++++++++------ .../contracts/SubgraphServiceExtension.sol | 102 ----------------- .../contracts/interfaces/ISubgraphService.sol | 29 +++++ .../interfaces/ISubgraphServiceExtended.sol | 7 -- .../interfaces/ISubgraphServiceExtension.sol | 30 ----- .../contracts/utilities/Directory.sol | 11 +- .../test/unit/SubgraphBaseTest.t.sol | 4 +- .../indexing-agreement/base.t.sol | 2 +- .../indexing-agreement/cancel.t.sol | 20 ++-- .../indexing-agreement/shared.t.sol | 9 +- .../indexing-agreement/update.t.sol | 16 +-- 14 files changed, 132 insertions(+), 250 deletions(-) delete mode 100644 packages/horizon/contracts/data-service/libraries/ProvisionManagerLib.sol delete mode 100644 packages/subgraph-service/contracts/SubgraphServiceExtension.sol delete mode 100644 packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtended.sol delete mode 100644 packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol diff --git a/packages/horizon/contracts/data-service/libraries/ProvisionManagerLib.sol b/packages/horizon/contracts/data-service/libraries/ProvisionManagerLib.sol deleted file mode 100644 index 3e821c4b2..000000000 --- a/packages/horizon/contracts/data-service/libraries/ProvisionManagerLib.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; - -import { IHorizonStaking } from "../../interfaces/IHorizonStaking.sol"; -import { ProvisionManager } from "../utilities/ProvisionManager.sol"; - -library ProvisionManagerLib { - function requireAuthorizedForProvision( - IHorizonStaking graphStaking, - address serviceProvider, - address dataService, - address operator - ) external view { - require( - graphStaking.isAuthorized(serviceProvider, dataService, operator), - ProvisionManager.ProvisionManagerNotAuthorized(serviceProvider, operator) - ); - } -} diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol index eee3054ca..d57f5c1ce 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -9,7 +9,6 @@ import { PPMMath } from "../../libraries/PPMMath.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; import { ProvisionManagerV1Storage } from "./ProvisionManagerStorage.sol"; -import { ProvisionManagerLib } from "../libraries/ProvisionManagerLib.sol"; /** * @title ProvisionManager contract @@ -112,7 +111,10 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa * @param serviceProvider The address of the service provider. */ modifier onlyAuthorizedForProvision(address serviceProvider) { - ProvisionManagerLib.requireAuthorizedForProvision(_graphStaking(), serviceProvider, address(this), msg.sender); + require( + _graphStaking().isAuthorized(serviceProvider, address(this), msg.sender), + ProvisionManagerNotAuthorized(serviceProvider, msg.sender) + ); _; } @@ -126,10 +128,6 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa _; } - function requireValidProvision(address serviceProvider) external view { - _requireValidProvision(serviceProvider); - } - /** * @notice Initializes the contract and any parent contracts. */ diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 095e8e9f9..76b2342df 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -5,7 +5,6 @@ import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToke import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; import { IDisputeManager } from "./interfaces/IDisputeManager.sol"; import { ISubgraphService } from "./interfaces/ISubgraphService.sol"; -import { ISubgraphServiceExtended } from "./interfaces/ISubgraphServiceExtended.sol"; import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; @@ -534,9 +533,7 @@ contract DisputeManager is uint256 _entities, uint256 _blockNumber ) private returns (bytes32) { - IndexingAgreement.AgreementWrapper memory wrapper = _getSubgraphServiceExtended().getIndexingAgreement( - _agreementId - ); + IndexingAgreement.AgreementWrapper memory wrapper = _getSubgraphService().getIndexingAgreement(_agreementId); // Agreement must have been collected on and be a version 1 require( @@ -766,16 +763,6 @@ contract DisputeManager is return subgraphService; } - /** - * @notice Get the address of the extended subgraph service - * @dev Will revert if the subgraph service is not set - * @return The extended subgraph service address - */ - function _getSubgraphServiceExtended() private view returns (ISubgraphServiceExtended) { - require(address(subgraphService) != address(0), DisputeManagerSubgraphServiceNotSet()); - return ISubgraphServiceExtended(address(subgraphService)); - } - /** * @notice Returns whether the dispute is for a conflicting attestation or not. * @param _dispute Dispute diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index f9c329af0..e03d1afca 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -73,11 +73,10 @@ contract SubgraphService is address disputeManager, address graphTallyCollector, address curation, - address recurringCollector, - address extension + address recurringCollector ) DataService(graphController) - Directory(address(this), disputeManager, graphTallyCollector, curation, recurringCollector, extension) + Directory(address(this), disputeManager, graphTallyCollector, curation, recurringCollector) { _disableInitializers(); } @@ -100,35 +99,6 @@ contract SubgraphService is _setStakeToFeesRatio(stakeToFeesRatio_); } - /** - * @notice Delegates the call to the SubgraphServiceExtension implementation. - * @dev This function does not return to its internal call site, it will return directly to the - * external caller. - */ - // solhint-disable-next-line payable-fallback, no-complex-fallback - fallback() external { - address extImpl = _subgraphServiceExtensionImpl(); - require(extImpl != address(0), "only through proxy"); - - // solhint-disable-next-line no-inline-assembly - assembly { - // copy function selector and any arguments - calldatacopy(0, 0, calldatasize()) - // execute function call using the extension implementation - let result := delegatecall(gas(), extImpl, 0, calldatasize(), 0, 0) - // get any return value - returndatacopy(0, 0, returndatasize()) - // return any return value or error back to the caller - switch result - case 0 { - revert(0, returndatasize()) - } - default { - return(0, returndatasize()) - } - } - } - /** * @notice * @dev Implements {IDataService.register} @@ -420,7 +390,7 @@ contract SubgraphService is /** * @notice Accept an indexing agreement. - * See {ISubgraphServiceExtended.acceptIndexingAgreement}. + * See {ISubgraphService.acceptIndexingAgreement}. * * Requirements: * - The agreement's indexer must be registered @@ -452,8 +422,76 @@ contract SubgraphService is IndexingAgreement._getManager().accept(_allocations, allocationId, signedRCA); } - function requireRegisteredIndexer(address indexer) external view { - _requireRegisteredIndexer(indexer); + /** + * @notice Update an indexing agreement. + * See {IndexingAgreement.update}. + * + * Requirements: + * - The contract must not be paused + * - The indexer must be valid + * + * @param indexer The indexer address + * @param signedRCAU The signed Recurring Collection Agreement Update + */ + function updateIndexingAgreement( + address indexer, + IRecurringCollector.SignedRCAU calldata signedRCAU + ) + external + whenNotPaused + onlyAuthorizedForProvision(indexer) + onlyValidProvision(indexer) + onlyRegisteredIndexer(indexer) + { + IndexingAgreement._getManager().update(indexer, signedRCAU); + } + + /** + * @notice Cancel an indexing agreement by indexer / operator. + * See {IndexingAgreement.cancel}. + * + * @dev Can only be canceled on behalf of a valid indexer. + * + * Requirements: + * - The contract must not be paused + * - The indexer must be valid + * + * @param indexer The indexer address + * @param agreementId The id of the agreement + */ + function cancelIndexingAgreement( + address indexer, + bytes16 agreementId + ) + external + whenNotPaused + onlyAuthorizedForProvision(indexer) + onlyValidProvision(indexer) + onlyRegisteredIndexer(indexer) + { + IndexingAgreement._getManager().cancel(indexer, agreementId); + } + + /** + * @notice Cancel an indexing agreement by payer / signer. + * See {ISubgraphService.cancelIndexingAgreementByPayer}. + * + * Requirements: + * - The caller must be authorized by the payer + * - The agreement must be active + * + * Emits {IndexingAgreementCanceled} event + * + * @param agreementId The id of the agreement + */ + function cancelIndexingAgreementByPayer(bytes16 agreementId) external whenNotPaused { + IndexingAgreement._getManager().cancelByPayer(agreementId); + } + + function getIndexingAgreement( + bytes16 agreementId + ) external view returns (IndexingAgreement.AgreementWrapper memory) { + return IndexingAgreement._getManager().get(agreementId); } /// @inheritdoc ISubgraphService diff --git a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol b/packages/subgraph-service/contracts/SubgraphServiceExtension.sol deleted file mode 100644 index 0a569bf05..000000000 --- a/packages/subgraph-service/contracts/SubgraphServiceExtension.sol +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; - -import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; -import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; -import { ProvisionManagerLib } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionManagerLib.sol"; - -import { IndexingAgreement } from "./libraries/IndexingAgreement.sol"; -import { SubgraphService } from "./SubgraphService.sol"; -import { ISubgraphServiceExtension } from "./interfaces/ISubgraphServiceExtension.sol"; - -contract SubgraphServiceExtension is PausableUpgradeable, ISubgraphServiceExtension { - using IndexingAgreement for IndexingAgreement.Manager; - - /** - * @notice Checks that an indexer is valid - * @param indexer The address of the indexer - * - * Requirements: - * - The caller must be authorized by the indexer - * - The provision must be valid according to the subgraph service rules - * - The indexer must be registered - * - */ - modifier onlyValid(address indexer) { - ProvisionManagerLib.requireAuthorizedForProvision( - IHorizonStaking(_getBase().getGraphStaking()), - indexer, - address(this), - msg.sender - ); - _getBase().requireValidProvision(indexer); - _getBase().requireRegisteredIndexer(indexer); - _; - } - - /** - * @notice Update an indexing agreement. - * See {IndexingAgreement.update}. - * - * Requirements: - * - The contract must not be paused - * - The indexer must be valid - * - * @param indexer The indexer address - * @param signedRCAU The signed Recurring Collection Agreement Update - */ - function updateIndexingAgreement( - address indexer, - IRecurringCollector.SignedRCAU calldata signedRCAU - ) external whenNotPaused onlyValid(indexer) { - IndexingAgreement._getManager().update(indexer, signedRCAU); - } - - /** - * @notice Cancel an indexing agreement by indexer / operator. - * See {IndexingAgreement.cancel}. - * - * @dev Can only be canceled on behalf of a valid indexer. - * - * Requirements: - * - The contract must not be paused - * - The indexer must be valid - * - * @param indexer The indexer address - * @param agreementId The id of the agreement - */ - function cancelIndexingAgreement(address indexer, bytes16 agreementId) external whenNotPaused onlyValid(indexer) { - IndexingAgreement._getManager().cancel(indexer, agreementId); - } - - /** - * @notice Cancel an indexing agreement by payer / signer. - * See {ISubgraphService.cancelIndexingAgreementByPayer}. - * - * Requirements: - * - The caller must be authorized by the payer - * - The agreement must be active - * - * Emits {IndexingAgreementCanceled} event - * - * @param agreementId The id of the agreement - */ - function cancelIndexingAgreementByPayer(bytes16 agreementId) external whenNotPaused { - IndexingAgreement._getManager().cancelByPayer(agreementId); - } - - function getIndexingAgreement( - bytes16 agreementId - ) external view returns (IndexingAgreement.AgreementWrapper memory) { - return IndexingAgreement._getManager().get(agreementId); - } - - function _cancelAllocationIndexingAgreement(address _allocationId) internal { - IndexingAgreement._getManager().cancelForAllocation(_allocationId); - } - - function _getBase() internal view returns (SubgraphService) { - return SubgraphService(address(this)); - } -} diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol index 348552253..4f0115530 100644 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.27; import { IDataServiceFees } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataServiceFees.sol"; import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IndexingAgreement } from "../libraries/IndexingAgreement.sol"; import { Allocation } from "../libraries/Allocation.sol"; import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; @@ -266,6 +267,34 @@ interface ISubgraphService is IDataServiceFees { */ function acceptIndexingAgreement(address allocationId, IRecurringCollector.SignedRCA calldata signedRCA) external; + /** + * @notice Update an indexing agreement. + * @param indexer The address of the indexer + * @param signedRCAU The signed recurring collector agreement update (RCAU) that the indexer accepts + */ + function updateIndexingAgreement(address indexer, IRecurringCollector.SignedRCAU calldata signedRCAU) external; + + /** + * @notice Cancel an indexing agreement by indexer / operator. + * @param indexer The address of the indexer + * @param agreementId The id of the indexing agreement + */ + function cancelIndexingAgreement(address indexer, bytes16 agreementId) external; + + /** + * @notice Cancel an indexing agreement by payer / signer. + * @param agreementId The id of the indexing agreement + */ + function cancelIndexingAgreementByPayer(bytes16 agreementId) external; + + /** + * @notice Get the indexing agreement for a given agreement ID. + * @param agreementId The id of the indexing agreement + */ + function getIndexingAgreement( + bytes16 agreementId + ) external view returns (IndexingAgreement.AgreementWrapper memory); + /** * @notice Gets the details of an allocation * For legacy allocations use {getLegacyAllocation} diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtended.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtended.sol deleted file mode 100644 index 4ee94eac8..000000000 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtended.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; - -import { ISubgraphService } from "./ISubgraphService.sol"; -import { ISubgraphServiceExtension } from "./ISubgraphServiceExtension.sol"; - -interface ISubgraphServiceExtended is ISubgraphService, ISubgraphServiceExtension {} diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol deleted file mode 100644 index 9f8377bfe..000000000 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphServiceExtension.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; - -import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; - -import { IndexingAgreement } from "../libraries/IndexingAgreement.sol"; - -interface ISubgraphServiceExtension { - /** - * @notice Update an indexing agreement. - */ - function updateIndexingAgreement(address indexer, IRecurringCollector.SignedRCAU calldata signedRCAU) external; - - /** - * @notice Cancel an indexing agreement by indexer / operator. - */ - function cancelIndexingAgreement(address indexer, bytes16 agreementId) external; - - /** - * @notice Cancel an indexing agreement by payer / signer. - */ - function cancelIndexingAgreementByPayer(bytes16 agreementId) external; - - /** - * @notice Get the indexing agreement for a given agreement ID. - */ - function getIndexingAgreement( - bytes16 agreementId - ) external view returns (IndexingAgreement.AgreementWrapper memory); -} diff --git a/packages/subgraph-service/contracts/utilities/Directory.sol b/packages/subgraph-service/contracts/utilities/Directory.sol index f90fe9beb..adf154482 100644 --- a/packages/subgraph-service/contracts/utilities/Directory.sol +++ b/packages/subgraph-service/contracts/utilities/Directory.sol @@ -48,8 +48,7 @@ abstract contract Directory { address disputeManager, address graphTallyCollector, address curation, - address recurringCollector, - address subgraphServiceExtensionImpl + address recurringCollector ); /** @@ -77,30 +76,26 @@ abstract contract Directory { * @param graphTallyCollector The Graph Tally Collector contract address * @param curation The Curation contract address * @param recurringCollector_ The Recurring Collector contract address - * @param subgraphServiceExtensionImpl The Subgraph Service Extension contract address */ constructor( address subgraphService, address disputeManager, address graphTallyCollector, address curation, - address recurringCollector_, - address subgraphServiceExtensionImpl + address recurringCollector_ ) { SUBGRAPH_SERVICE = ISubgraphService(subgraphService); DISPUTE_MANAGER = IDisputeManager(disputeManager); GRAPH_TALLY_COLLECTOR = IGraphTallyCollector(graphTallyCollector); CURATION = ICuration(curation); RECURRING_COLLECTOR = IRecurringCollector(recurringCollector_); - SUBGRAPH_SERVICE_EXTENSION_IMPL = subgraphServiceExtensionImpl; emit SubgraphServiceDirectoryInitialized( subgraphService, disputeManager, graphTallyCollector, curation, - recurringCollector_, - subgraphServiceExtensionImpl + recurringCollector_ ); } diff --git a/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol b/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol index 146790b22..639f183d1 100644 --- a/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol +++ b/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol @@ -21,7 +21,6 @@ import { UnsafeUpgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; import { Constants } from "./utils/Constants.sol"; import { DisputeManager } from "../../contracts/DisputeManager.sol"; import { SubgraphService } from "../../contracts/SubgraphService.sol"; -import { SubgraphServiceExtension } from "../../contracts/SubgraphServiceExtension.sol"; import { Users } from "./utils/Users.sol"; import { Utils } from "./utils/Utils.sol"; @@ -172,8 +171,7 @@ abstract contract SubgraphBaseTest is Utils, Constants { address(disputeManager), address(graphTallyCollector), address(curation), - address(recurringCollector), - address(new SubgraphServiceExtension()) + address(recurringCollector) ) ); address subgraphServiceProxy = UnsafeUpgrades.deployTransparentProxy( diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol index d23e510ca..822cc21d7 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol @@ -17,7 +17,7 @@ contract SubgraphServiceIndexingAgreementBaseTest is SubgraphServiceIndexingAgre vm.expectRevert(TransparentUpgradeableProxy.ProxyDeniedAdminAccess.selector); resetPrank(address(operator)); - _getSubgraphServiceExtended().cancelIndexingAgreement(indexer, agreementId); + subgraphService.cancelIndexingAgreement(indexer, agreementId); } function test_SubgraphService_Revert_WhenUnsafeAddress_WhenGraphProxyAdmin(uint256 unboundedTokens) public { diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol index 261aeb55d..60a28169c 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/cancel.t.sol @@ -25,7 +25,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); resetPrank(rando); - _getSubgraphServiceExtended().cancelIndexingAgreementByPayer(agreementId); + subgraphService.cancelIndexingAgreementByPayer(agreementId); } function test_SubgraphService_CancelIndexingAgreementByPayer_Revert_WhenNotAuthorized( @@ -42,7 +42,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg ); vm.expectRevert(expectedErr); resetPrank(rando); - _getSubgraphServiceExtended().cancelIndexingAgreementByPayer(accepted.rca.agreementId); + subgraphService.cancelIndexingAgreementByPayer(accepted.rca.agreementId); } function test_SubgraphService_CancelIndexingAgreementByPayer_Revert_WhenNotAccepted( @@ -58,7 +58,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg agreementId ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtended().cancelIndexingAgreementByPayer(agreementId); + subgraphService.cancelIndexingAgreementByPayer(agreementId); } function test_SubgraphService_CancelIndexingAgreementByPayer_Revert_WhenCanceled( @@ -79,7 +79,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg accepted.rca.agreementId ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtended().cancelIndexingAgreementByPayer(accepted.rca.agreementId); + subgraphService.cancelIndexingAgreementByPayer(accepted.rca.agreementId); } function test_SubgraphService_CancelIndexingAgreementByPayer(Seed memory seed) public { @@ -105,7 +105,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); resetPrank(operator); - _getSubgraphServiceExtended().cancelIndexingAgreement(indexer, agreementId); + subgraphService.cancelIndexingAgreement(indexer, agreementId); } function test_SubgraphService_CancelIndexingAgreement_Revert_WhenNotAuthorized( @@ -121,7 +121,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg operator ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtended().cancelIndexingAgreement(indexer, agreementId); + subgraphService.cancelIndexingAgreement(indexer, agreementId); } function test_SubgraphService_CancelIndexingAgreement_Revert_WhenInvalidProvision( @@ -142,7 +142,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg maximumProvisionTokens ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtended().cancelIndexingAgreement(indexer, agreementId); + subgraphService.cancelIndexingAgreement(indexer, agreementId); } function test_SubgraphService_CancelIndexingAgreement_Revert_WhenIndexerNotRegistered( @@ -159,7 +159,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg indexer ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtended().cancelIndexingAgreement(indexer, agreementId); + subgraphService.cancelIndexingAgreement(indexer, agreementId); } function test_SubgraphService_CancelIndexingAgreement_Revert_WhenNotAccepted( @@ -175,7 +175,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg agreementId ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtended().cancelIndexingAgreement(indexerState.addr, agreementId); + subgraphService.cancelIndexingAgreement(indexerState.addr, agreementId); } function test_SubgraphService_CancelIndexingAgreement_Revert_WhenCanceled( @@ -196,7 +196,7 @@ contract SubgraphServiceIndexingAgreementCancelTest is SubgraphServiceIndexingAg accepted.rca.agreementId ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtended().cancelIndexingAgreement(indexerState.addr, accepted.rca.agreementId); + subgraphService.cancelIndexingAgreement(indexerState.addr, accepted.rca.agreementId); } function test_SubgraphService_CancelIndexingAgreement_OK(Seed memory seed) public { 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 3115492c3..e6643883e 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 @@ -5,7 +5,6 @@ import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; -import { ISubgraphServiceExtended } from "../../../../contracts/interfaces/ISubgraphServiceExtended.sol"; import { Bounder } from "@graphprotocol/horizon/test/unit/utils/Bounder.t.sol"; import { RecurringCollectorHelper } from "@graphprotocol/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol"; @@ -110,10 +109,10 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun if (byIndexer) { _subgraphServiceSafePrank(_indexer); - _getSubgraphServiceExtended().cancelIndexingAgreement(_indexer, _agreementId); + subgraphService.cancelIndexingAgreement(_indexer, _agreementId); } else { _subgraphServiceSafePrank(_ctx.payer.signer); - _getSubgraphServiceExtended().cancelIndexingAgreementByPayer(_agreementId); + subgraphService.cancelIndexingAgreementByPayer(_agreementId); } } @@ -309,10 +308,6 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun ); } - function _getSubgraphServiceExtended() internal view returns (ISubgraphServiceExtended) { - return ISubgraphServiceExtended(address(subgraphService)); - } - function _newAcceptIndexingAgreementMetadataV1( bytes32 _subgraphDeploymentId ) internal pure returns (IndexingAgreement.AcceptIndexingAgreementMetadata memory) { diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol index 1c828d431..f3589a4d3 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol @@ -26,7 +26,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA resetPrank(operator); vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - _getSubgraphServiceExtended().updateIndexingAgreement(operator, signedRCAU); + subgraphService.updateIndexingAgreement(operator, signedRCAU); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenNotAuthorized( @@ -42,7 +42,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA notAuthorized ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtended().updateIndexingAgreement(indexer, signedRCAU); + subgraphService.updateIndexingAgreement(indexer, signedRCAU); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenInvalidProvision( @@ -63,7 +63,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA maximumProvisionTokens ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtended().updateIndexingAgreement(indexer, signedRCAU); + subgraphService.updateIndexingAgreement(indexer, signedRCAU); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenIndexerNotRegistered( @@ -81,7 +81,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA indexer ); vm.expectRevert(expectedErr); - _getSubgraphServiceExtended().updateIndexingAgreement(indexer, signedRCAU); + subgraphService.updateIndexingAgreement(indexer, signedRCAU); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenNotAccepted(Seed memory seed) public { @@ -98,7 +98,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA ); vm.expectRevert(expectedErr); resetPrank(indexerState.addr); - _getSubgraphServiceExtended().updateIndexingAgreement(indexerState.addr, acceptableUpdate); + subgraphService.updateIndexingAgreement(indexerState.addr, acceptableUpdate); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenNotAuthorizedForAgreement( @@ -117,7 +117,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA ); vm.expectRevert(expectedErr); resetPrank(indexerStateB.addr); - _getSubgraphServiceExtended().updateIndexingAgreement(indexerStateB.addr, acceptableUpdate); + subgraphService.updateIndexingAgreement(indexerStateB.addr, acceptableUpdate); } function test_SubgraphService_UpdateIndexingAgreement_Revert_WhenInvalidMetadata(Seed memory seed) public { @@ -139,7 +139,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA ); vm.expectRevert(expectedErr); resetPrank(indexerState.addr); - _getSubgraphServiceExtended().updateIndexingAgreement(indexerState.addr, unacceptableUpdate); + subgraphService.updateIndexingAgreement(indexerState.addr, unacceptableUpdate); } function test_SubgraphService_UpdateIndexingAgreement_OK(Seed memory seed) public { @@ -164,7 +164,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA ); resetPrank(indexerState.addr); - _getSubgraphServiceExtended().updateIndexingAgreement(indexerState.addr, acceptableUpdate); + subgraphService.updateIndexingAgreement(indexerState.addr, acceptableUpdate); } /* solhint-enable graph/func-name-mixedcase */ } From f5c724b2a362ea829eefa82baaaa465ab8a3a317 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 15:14:09 -0300 Subject: [PATCH 41/90] f: update todo md --- IndexingPaymentsTodo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IndexingPaymentsTodo.md b/IndexingPaymentsTodo.md index 37403d618..f3ab72e62 100644 --- a/IndexingPaymentsTodo.md +++ b/IndexingPaymentsTodo.md @@ -1,6 +1,5 @@ # Still pending -* Remove extension if I can fit everything in one service? * `require(provision.tokens != 0, DisputeManagerZeroTokens());` - Document or fix? * Check code coverage * Don't love cancel agreement on stop service / close stale allocation. @@ -8,6 +7,7 @@ # Done +* DONE: ~~* Remove extension if I can fit everything in one service?~~ * DONE: ~~* One Interface for all subgraph~~ * DONE: ~~* Missing Upgrade event for subgraph service~~ * DONE: ~~* Check contract size~~ From d38c623037ad4e2fbc5b3301bbae2905b485b160 Mon Sep 17 00:00:00 2001 From: Matias Date: Wed, 4 Jun 2025 15:17:35 -0300 Subject: [PATCH 42/90] f: revert ProvisionTRacker --- .../contracts/data-service/libraries/ProvisionTracker.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol index a152b0958..2fe271833 100644 --- a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol +++ b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol @@ -37,7 +37,7 @@ library ProvisionTracker { address serviceProvider, uint256 tokens, uint32 delegationRatio - ) external { + ) internal { if (tokens == 0) return; uint256 tokensRequired = self[serviceProvider] + tokens; @@ -54,7 +54,7 @@ library ProvisionTracker { * @param serviceProvider The service provider address * @param tokens The amount of tokens to release */ - function release(mapping(address => uint256) storage self, address serviceProvider, uint256 tokens) external { + function release(mapping(address => uint256) storage self, address serviceProvider, uint256 tokens) internal { if (tokens == 0) return; require(self[serviceProvider] >= tokens, ProvisionTrackerInsufficientTokens(self[serviceProvider], tokens)); self[serviceProvider] -= tokens; @@ -73,7 +73,7 @@ library ProvisionTracker { IHorizonStaking graphStaking, address serviceProvider, uint32 delegationRatio - ) external view returns (bool) { + ) internal view returns (bool) { uint256 tokensAvailable = graphStaking.getTokensAvailable(serviceProvider, address(this), delegationRatio); return self[serviceProvider] <= tokensAvailable; } From c1898d130265932ef1c9a7ce05ff394ddc758496 Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 12:22:22 -0300 Subject: [PATCH 43/90] f: remove unused extension references --- .../contracts/utilities/Directory.sol | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/subgraph-service/contracts/utilities/Directory.sol b/packages/subgraph-service/contracts/utilities/Directory.sol index adf154482..cc2675a68 100644 --- a/packages/subgraph-service/contracts/utilities/Directory.sol +++ b/packages/subgraph-service/contracts/utilities/Directory.sol @@ -30,8 +30,6 @@ abstract contract Directory { /// @dev Required to collect indexing agreement payments via Graph Horizon payments protocol IRecurringCollector private immutable RECURRING_COLLECTOR; - address private immutable SUBGRAPH_SERVICE_EXTENSION_IMPL; - /// @notice The Curation contract address /// @dev Required for curation fees distribution ICuration private immutable CURATION; @@ -106,17 +104,6 @@ abstract contract Directory { return RECURRING_COLLECTOR; } - /** - * @notice Returns the Subgraph Service Extension implementation contract address - */ - function _subgraphServiceExtensionImpl() internal view returns (address) { - return SUBGRAPH_SERVICE_EXTENSION_IMPL; - } - - function _directory() internal view returns (Directory) { - return Directory(address(this)); - } - /** * @notice Returns the Subgraph Service contract address * @return The Subgraph Service contract From 0c9780519815bb95ad23a3a3e9e5a0460b020bfe Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 13:27:16 -0300 Subject: [PATCH 44/90] f: rename Manager to StorageManager --- .../contracts/SubgraphService.sol | 16 ++++---- .../contracts/libraries/IndexingAgreement.sol | 38 +++++++++---------- .../test/unit/libraries/IndexingAgreement.sol | 18 +++++++++ 3 files changed, 45 insertions(+), 27 deletions(-) create mode 100644 packages/subgraph-service/test/unit/libraries/IndexingAgreement.sol diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index e03d1afca..4b0a6ed7f 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -49,7 +49,7 @@ contract SubgraphService is using Allocation for mapping(address => Allocation.State); using Allocation for Allocation.State; using TokenUtils for IGraphToken; - using IndexingAgreement for IndexingAgreement.Manager; + using IndexingAgreement for IndexingAgreement.StorageManager; /** * @notice Checks that an indexer is registered @@ -419,7 +419,7 @@ contract SubgraphService is onlyValidProvision(signedRCA.rca.serviceProvider) onlyRegisteredIndexer(signedRCA.rca.serviceProvider) { - IndexingAgreement._getManager().accept(_allocations, allocationId, signedRCA); + IndexingAgreement._getStorageManager().accept(_allocations, allocationId, signedRCA); } /** @@ -443,7 +443,7 @@ contract SubgraphService is onlyValidProvision(indexer) onlyRegisteredIndexer(indexer) { - IndexingAgreement._getManager().update(indexer, signedRCAU); + IndexingAgreement._getStorageManager().update(indexer, signedRCAU); } /** @@ -469,7 +469,7 @@ contract SubgraphService is onlyValidProvision(indexer) onlyRegisteredIndexer(indexer) { - IndexingAgreement._getManager().cancel(indexer, agreementId); + IndexingAgreement._getStorageManager().cancel(indexer, agreementId); } /** @@ -485,13 +485,13 @@ contract SubgraphService is * @param agreementId The id of the agreement */ function cancelIndexingAgreementByPayer(bytes16 agreementId) external whenNotPaused { - IndexingAgreement._getManager().cancelByPayer(agreementId); + IndexingAgreement._getStorageManager().cancelByPayer(agreementId); } function getIndexingAgreement( bytes16 agreementId ) external view returns (IndexingAgreement.AgreementWrapper memory) { - return IndexingAgreement._getManager().get(agreementId); + return IndexingAgreement._getStorageManager().get(agreementId); } /// @inheritdoc ISubgraphService @@ -554,7 +554,7 @@ contract SubgraphService is } function _cancelAllocationIndexingAgreement(address _allocationId) internal { - IndexingAgreement._getManager().cancelForAllocation(_allocationId); + IndexingAgreement._getStorageManager().cancelForAllocation(_allocationId); } /** @@ -739,7 +739,7 @@ contract SubgraphService is * @return The amount of fees collected */ function _collectIndexingFees(bytes16 _agreementId, bytes memory _data) private returns (uint256) { - (address indexer, uint256 tokensCollected) = IndexingAgreement._getManager().collect( + (address indexer, uint256 tokensCollected) = IndexingAgreement._getStorageManager().collect( _allocations, IndexingAgreement.CollectParams({ agreementId: _agreementId, diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 10b84ac0e..e32861840 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -12,7 +12,7 @@ import { SubgraphServiceLib } from "./SubgraphServiceLib.sol"; import { Decoder } from "./Decoder.sol"; library IndexingAgreement { - using IndexingAgreement for Manager; + using IndexingAgreement for StorageManager; using Allocation for mapping(address => Allocation.State); using SubgraphServiceLib for mapping(address => Allocation.State); @@ -74,16 +74,16 @@ library IndexingAgreement { bytes data; } - /// @custom:storage-location erc7201:graphprotocol.subgraph-service.storage.Manager.IndexingAgreement - struct Manager { + /// @custom:storage-location erc7201:graphprotocol.subgraph-service.storage.StorageManager.IndexingAgreement + struct StorageManager { mapping(bytes16 => State) agreements; mapping(bytes16 agreementId => IndexingAgreementTermsV1 data) termsV1; mapping(address allocationId => bytes16 agreementId) allocationToActiveAgreementId; } - // keccak256(abi.encode(uint256(keccak256("graphprotocol.subgraph-service.storage.Manager.IndexingAgreement")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant INDEXING_AGREEMENT_MANAGER_STORAGE_LOCATION = - 0xfdb6fb5d1a390e01387ce73642e517880d8e0fedd0e7e26ac9194788a7a85200; + // keccak256(abi.encode(uint256(keccak256("graphprotocol.subgraph-service.storage.StorageManager.IndexingAgreement")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 public constant INDEXING_AGREEMENT_STORAGE_MANAGER_LOCATION = + 0xb59b65b7215c7fb95ac34d2ad5aed7c775c8bc77ad936b1b43e17b95efc8e400; /** * @notice Emitted when an indexer collects indexing fees from a V1 agreement @@ -218,7 +218,7 @@ library IndexingAgreement { error IndexingAgreementNotAuthorized(bytes16 agreementId, address unauthorizedIndexer); function accept( - Manager storage self, + StorageManager storage self, mapping(address allocationId => Allocation.State allocation) storage allocations, address allocationId, IRecurringCollector.SignedRCA calldata signedRCA @@ -289,7 +289,7 @@ library IndexingAgreement { * @param signedRCAU The signed Recurring Collection Agreement Update */ function update( - Manager storage self, + StorageManager storage self, address indexer, IRecurringCollector.SignedRCAU calldata signedRCAU ) external { @@ -332,7 +332,7 @@ library IndexingAgreement { * @param indexer The indexer address * @param agreementId The id of the agreement to cancel */ - function cancel(Manager storage self, address indexer, bytes16 agreementId) external { + function cancel(StorageManager storage self, address indexer, bytes16 agreementId) external { AgreementWrapper memory wrapper = _get(self, agreementId); require(_isActive(wrapper), IndexingAgreementNotActive(agreementId)); require( @@ -348,7 +348,7 @@ library IndexingAgreement { ); } - function cancelForAllocation(Manager storage self, address _allocationId) external { + function cancelForAllocation(StorageManager storage self, address _allocationId) external { bytes16 agreementId = self.allocationToActiveAgreementId[_allocationId]; if (agreementId == bytes16(0)) { return; @@ -368,7 +368,7 @@ library IndexingAgreement { ); } - function cancelByPayer(Manager storage self, bytes16 agreementId) external { + function cancelByPayer(StorageManager storage self, bytes16 agreementId) external { AgreementWrapper memory wrapper = _get(self, agreementId); require(_isActive(wrapper), IndexingAgreementNotActive(agreementId)); require( @@ -385,7 +385,7 @@ library IndexingAgreement { } function collect( - Manager storage self, + StorageManager storage self, mapping(address allocationId => Allocation.State allocation) storage allocations, CollectParams memory params ) external returns (address, uint256) { @@ -435,28 +435,28 @@ library IndexingAgreement { return (wrapper.collectorAgreement.serviceProvider, tokensCollected); } - function get(Manager storage self, bytes16 agreementId) external view returns (AgreementWrapper memory) { + function get(StorageManager storage self, bytes16 agreementId) external view returns (AgreementWrapper memory) { AgreementWrapper memory wrapper = _get(self, agreementId); require(wrapper.collectorAgreement.dataService == address(this), IndexingAgreementNotActive(agreementId)); return wrapper; } - function _getManager() internal pure returns (Manager storage $) { + function _getStorageManager() internal pure returns (StorageManager storage $) { // solhint-disable-next-line no-inline-assembly assembly { - $.slot := INDEXING_AGREEMENT_MANAGER_STORAGE_LOCATION + $.slot := INDEXING_AGREEMENT_STORAGE_MANAGER_LOCATION } } - function _setTermsV1(Manager storage _manager, bytes16 _agreementId, bytes memory _data) private { + function _setTermsV1(StorageManager storage _manager, bytes16 _agreementId, bytes memory _data) private { IndexingAgreementTermsV1 memory newTerms = Decoder.decodeIndexingAgreementTermsV1(_data); _manager.termsV1[_agreementId].tokensPerSecond = newTerms.tokensPerSecond; _manager.termsV1[_agreementId].tokensPerEntityPerSecond = newTerms.tokensPerEntityPerSecond; } function _cancel( - Manager storage _manager, + StorageManager storage _manager, bytes16 _agreementId, State memory _agreement, IRecurringCollector.AgreementData memory _collectorAgreement, @@ -477,7 +477,7 @@ library IndexingAgreement { } function _tokensToCollect( - Manager storage _manager, + StorageManager storage _manager, bytes16 _agreementId, IRecurringCollector.AgreementData memory _agreement, uint256 _entities @@ -509,7 +509,7 @@ library IndexingAgreement { return SubgraphService(address(this)); } - function _get(Manager storage self, bytes16 agreementId) private view returns (AgreementWrapper memory) { + function _get(StorageManager storage self, bytes16 agreementId) private view returns (AgreementWrapper memory) { return AgreementWrapper({ agreement: self.agreements[agreementId], diff --git a/packages/subgraph-service/test/unit/libraries/IndexingAgreement.sol b/packages/subgraph-service/test/unit/libraries/IndexingAgreement.sol new file mode 100644 index 000000000..4afc6707e --- /dev/null +++ b/packages/subgraph-service/test/unit/libraries/IndexingAgreement.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; +import { IndexingAgreement } from "../../../contracts/libraries/IndexingAgreement.sol"; + +contract IndexingAgreementTest is Test { + function test_StorageManagerLocation() public pure { + assertEq( + IndexingAgreement.INDEXING_AGREEMENT_STORAGE_MANAGER_LOCATION, + keccak256( + abi.encode( + uint256(keccak256("graphprotocol.subgraph-service.storage.StorageManager.IndexingAgreement")) - 1 + ) + ) & ~bytes32(uint256(0xff)) + ); + } +} From 54ae89779205adc429937917179cba776546de59 Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 13:31:50 -0300 Subject: [PATCH 45/90] f: add to IndexingAgreementWrongDataService --- .../contracts/libraries/IndexingAgreement.sol | 7 ++++--- .../unit/subgraphService/indexing-agreement/accept.t.sol | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index e32861840..f17cbebbf 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -169,9 +169,10 @@ library IndexingAgreement { /** * @notice Thrown when an agreement is not for the subgraph data service - * @param wrongDataService The wrong data service + * @param expectedDataService The expected data service address + * @param wrongDataService The wrong data service address */ - error IndexingAgreementWrongDataService(address wrongDataService); + error IndexingAgreementWrongDataService(address expectedDataService, address wrongDataService); /** * @notice Thrown when an agreement and the allocation correspond to different deployment IDs @@ -230,7 +231,7 @@ library IndexingAgreement { require( signedRCA.rca.dataService == address(this), - IndexingAgreementWrongDataService(signedRCA.rca.dataService) + IndexingAgreementWrongDataService(address(this), signedRCA.rca.dataService) ); AcceptIndexingAgreementMetadata memory metadata = Decoder.decodeRCAMetadata(signedRCA.rca.metadata); 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 793bb2438..a8f410da7 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 @@ -107,6 +107,7 @@ contract SubgraphServiceIndexingAgreementAcceptTest is SubgraphServiceIndexingAg bytes memory expectedErr = abi.encodeWithSelector( IndexingAgreement.IndexingAgreementWrongDataService.selector, + address(subgraphService), unacceptable.rca.dataService ); vm.expectRevert(expectedErr); From d1d57837a188779c341b1f10155c53976549985a Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 13:38:39 -0300 Subject: [PATCH 46/90] f: comment one agreement per alloc --- .../subgraph-service/contracts/libraries/IndexingAgreement.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index f17cbebbf..e772b2782 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -249,6 +249,7 @@ library IndexingAgreement { ) ); + // Ensure that an allocation can only have one active indexing agreement require( self.allocationToActiveAgreementId[allocationId] == bytes16(0), AllocationAlreadyHasIndexingAgreement(allocationId) From 5483a8d0f2118a993b473d024516695a302d68dd Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 17:48:13 -0300 Subject: [PATCH 47/90] f: remove RecurringCollectorAgreementInvalidParameters --- .../interfaces/IRecurringCollector.sol | 31 +++++++++++++++++-- .../collectors/RecurringCollector.sol | 15 ++++++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index c95488129..e60b93a78 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -282,10 +282,35 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { error RecurringCollectorAgreementIncorrectState(bytes16 agreementId, AgreementState incorrectState); /** - * Thrown when accepting or upgrading an agreement with invalid parameters - * @param message A descriptive error message + * Thrown when accepting an agreement with an address that is not set */ - error RecurringCollectorAgreementInvalidParameters(string message); + error RecurringCollectorAgreementAddressNotSet(); + + /** + * Thrown when accepting or upgrading an agreement with an elapsed endsAt + * @param currentTimestamp The current timestamp + * @param endsAt The agreement end timestamp + */ + error RecurringCollectorAgreementElapsedEndsAt(uint256 currentTimestamp, uint64 endsAt); + + /** + * Thrown when accepting or upgrading an agreement with an elapsed endsAt + * @param allowedMinCollectionWindow The allowed minimum collection window + * @param minSecondsPerCollection The minimum seconds per collection + * @param maxSecondsPerCollection The maximum seconds per collection + */ + error RecurringCollectorAgreementInvalidCollectionWindow( + uint32 allowedMinCollectionWindow, + uint32 minSecondsPerCollection, + uint32 maxSecondsPerCollection + ); + + /** + * Thrown when accepting or upgrading an agreement with an invalid duration + * @param requiredMinDuration The required minimum duration + * @param invalidDuration The invalid duration + */ + error RecurringCollectorAgreementInvalidDuration(uint32 requiredMinDuration, uint256 invalidDuration); /** * Thrown when calling collect() on an elapsed agreement diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index d003914d5..7f7f5133c 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -311,13 +311,13 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC _agreement.dataService != address(0) && _agreement.payer != address(0) && _agreement.serviceProvider != address(0), - RecurringCollectorAgreementInvalidParameters("zero address") + RecurringCollectorAgreementAddressNotSet() ); // Agreement needs to end in the future require( _agreement.endsAt > block.timestamp, - RecurringCollectorAgreementInvalidParameters("endsAt not in future") + RecurringCollectorAgreementElapsedEndsAt(block.timestamp, _agreement.endsAt) ); // Collection window needs to be at least MIN_SECONDS_COLLECTION_WINDOW @@ -325,13 +325,20 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC _agreement.maxSecondsPerCollection > _agreement.minSecondsPerCollection && (_agreement.maxSecondsPerCollection - _agreement.minSecondsPerCollection >= MIN_SECONDS_COLLECTION_WINDOW), - RecurringCollectorAgreementInvalidParameters("too small collection window") + RecurringCollectorAgreementInvalidCollectionWindow( + MIN_SECONDS_COLLECTION_WINDOW, + _agreement.minSecondsPerCollection, + _agreement.maxSecondsPerCollection + ) ); // Agreement needs to last at least one min collection window require( _agreement.endsAt - block.timestamp >= _agreement.minSecondsPerCollection + MIN_SECONDS_COLLECTION_WINDOW, - RecurringCollectorAgreementInvalidParameters("too small agreement window") + RecurringCollectorAgreementInvalidDuration( + _agreement.minSecondsPerCollection + MIN_SECONDS_COLLECTION_WINDOW, + _agreement.endsAt - block.timestamp + ) ); } From 4c1db5c538d829c2c09ca7b7defc5015df522d5c Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 19:03:00 -0300 Subject: [PATCH 48/90] f: validate before setting in storage --- .../collectors/RecurringCollector.sol | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index 7f7f5133c..6ab4be9f8 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -88,6 +88,19 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC // check that the voucher is signed by the payer (or proxy) _requireAuthorizedRCASigner(signedRCA); + require( + signedRCA.rca.dataService != address(0) && + signedRCA.rca.payer != address(0) && + signedRCA.rca.serviceProvider != address(0), + RecurringCollectorAgreementAddressNotSet() + ); + + _requireValidCollectionWindowParams( + signedRCA.rca.endsAt, + signedRCA.rca.minSecondsPerCollection, + signedRCA.rca.maxSecondsPerCollection + ); + AgreementData storage agreement = _getAgreementStorage(signedRCA.rca.agreementId); // check that the agreement is not already accepted require( @@ -106,7 +119,6 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC agreement.maxOngoingTokensPerSecond = signedRCA.rca.maxOngoingTokensPerSecond; agreement.minSecondsPerCollection = signedRCA.rca.minSecondsPerCollection; agreement.maxSecondsPerCollection = signedRCA.rca.maxSecondsPerCollection; - _requireValidAgreement(agreement); emit AgreementAccepted( agreement.dataService, @@ -178,13 +190,18 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC // check that the voucher is signed by the payer (or proxy) _requireAuthorizedRCAUSigner(signedRCAU, agreement.payer); + _requireValidCollectionWindowParams( + signedRCAU.rcau.endsAt, + signedRCAU.rcau.minSecondsPerCollection, + signedRCAU.rcau.maxSecondsPerCollection + ); + // update the agreement agreement.endsAt = signedRCAU.rcau.endsAt; agreement.maxInitialTokens = signedRCAU.rcau.maxInitialTokens; agreement.maxOngoingTokensPerSecond = signedRCAU.rcau.maxOngoingTokensPerSecond; agreement.minSecondsPerCollection = signedRCAU.rcau.minSecondsPerCollection; agreement.maxSecondsPerCollection = signedRCAU.rcau.maxSecondsPerCollection; - _requireValidAgreement(agreement); emit AgreementUpdated( agreement.dataService, @@ -306,38 +323,31 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC return tokensToCollect; } - function _requireValidAgreement(AgreementData memory _agreement) private view { - require( - _agreement.dataService != address(0) && - _agreement.payer != address(0) && - _agreement.serviceProvider != address(0), - RecurringCollectorAgreementAddressNotSet() - ); - + function _requireValidCollectionWindowParams( + uint64 _endsAt, + uint32 _minSecondsPerCollection, + uint32 _maxSecondsPerCollection + ) private view { // Agreement needs to end in the future - require( - _agreement.endsAt > block.timestamp, - RecurringCollectorAgreementElapsedEndsAt(block.timestamp, _agreement.endsAt) - ); + require(_endsAt > block.timestamp, RecurringCollectorAgreementElapsedEndsAt(block.timestamp, _endsAt)); // Collection window needs to be at least MIN_SECONDS_COLLECTION_WINDOW require( - _agreement.maxSecondsPerCollection > _agreement.minSecondsPerCollection && - (_agreement.maxSecondsPerCollection - _agreement.minSecondsPerCollection >= - MIN_SECONDS_COLLECTION_WINDOW), + _maxSecondsPerCollection > _minSecondsPerCollection && + (_maxSecondsPerCollection - _minSecondsPerCollection >= MIN_SECONDS_COLLECTION_WINDOW), RecurringCollectorAgreementInvalidCollectionWindow( MIN_SECONDS_COLLECTION_WINDOW, - _agreement.minSecondsPerCollection, - _agreement.maxSecondsPerCollection + _minSecondsPerCollection, + _maxSecondsPerCollection ) ); // Agreement needs to last at least one min collection window require( - _agreement.endsAt - block.timestamp >= _agreement.minSecondsPerCollection + MIN_SECONDS_COLLECTION_WINDOW, + _endsAt - block.timestamp >= _minSecondsPerCollection + MIN_SECONDS_COLLECTION_WINDOW, RecurringCollectorAgreementInvalidDuration( - _agreement.minSecondsPerCollection + MIN_SECONDS_COLLECTION_WINDOW, - _agreement.endsAt - block.timestamp + _minSecondsPerCollection + MIN_SECONDS_COLLECTION_WINDOW, + _endsAt - block.timestamp ) ); } From 0f415371a4e8797ffa0f19680d46200dc030ad12 Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 20:52:31 -0300 Subject: [PATCH 49/90] f: natspec --- .../horizon/contracts/payments/collectors/RecurringCollector.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index 6ab4be9f8..ab9f5d671 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -21,6 +21,7 @@ import { MathUtils } from "../../libraries/MathUtils.sol"; contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringCollector { using PPMMath for uint256; + /// @notice The minimum number of seconds that must be between two collections uint32 public constant MIN_SECONDS_COLLECTION_WINDOW = 600; /// @notice The EIP712 typehash for the RecurringCollectionAgreement struct From 4009021614342e32101e6fd6f61df6432f236e1b Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 20:52:59 -0300 Subject: [PATCH 50/90] f: add now to RecurringCollectorAgreementDeadlineElapsed --- packages/horizon/contracts/interfaces/IRecurringCollector.sol | 3 ++- .../contracts/payments/collectors/RecurringCollector.sol | 4 ++-- .../test/unit/payments/recurring-collector/accept.t.sol | 1 + .../test/unit/payments/recurring-collector/update.t.sol | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index e60b93a78..5b4967dab 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -238,9 +238,10 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { /** * Thrown when interacting with an agreement with an elapsed deadline + * @param currentTimestamp The current timestamp * @param deadline The elapsed deadline timestamp */ - error RecurringCollectorAgreementDeadlineElapsed(uint64 deadline); + error RecurringCollectorAgreementDeadlineElapsed(uint256 currentTimestamp, uint64 deadline); /** * Thrown when the signer is invalid diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index ab9f5d671..8438fe56a 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -83,7 +83,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC ); require( signedRCA.rca.deadline >= block.timestamp, - RecurringCollectorAgreementDeadlineElapsed(signedRCA.rca.deadline) + RecurringCollectorAgreementDeadlineElapsed(block.timestamp, signedRCA.rca.deadline) ); // check that the voucher is signed by the payer (or proxy) @@ -175,7 +175,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC function update(SignedRCAU calldata signedRCAU) external { require( signedRCAU.rcau.deadline >= block.timestamp, - RecurringCollectorAgreementDeadlineElapsed(signedRCAU.rcau.deadline) + RecurringCollectorAgreementDeadlineElapsed(block.timestamp, signedRCAU.rcau.deadline) ); AgreementData storage agreement = _getAgreementStorage(signedRCAU.rcau.agreementId); diff --git a/packages/horizon/test/unit/payments/recurring-collector/accept.t.sol b/packages/horizon/test/unit/payments/recurring-collector/accept.t.sol index c551197af..d9479b955 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/accept.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/accept.t.sol @@ -26,6 +26,7 @@ contract RecurringCollectorAcceptTest is RecurringCollectorSharedTest { bytes memory expectedErr = abi.encodeWithSelector( IRecurringCollector.RecurringCollectorAgreementDeadlineElapsed.selector, + block.timestamp, fuzzySignedRCA.rca.deadline ); vm.expectRevert(expectedErr); diff --git a/packages/horizon/test/unit/payments/recurring-collector/update.t.sol b/packages/horizon/test/unit/payments/recurring-collector/update.t.sol index bb01fbfd7..4fd8af1e7 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/update.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/update.t.sol @@ -30,6 +30,7 @@ contract RecurringCollectorUpdateTest is RecurringCollectorSharedTest { bytes memory expectedErr = abi.encodeWithSelector( IRecurringCollector.RecurringCollectorAgreementDeadlineElapsed.selector, + block.timestamp, rcau.deadline ); vm.expectRevert(expectedErr); From 6ef571a31702b011939605567ed87e369e258c8b Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 20:59:30 -0300 Subject: [PATCH 51/90] f: natspec --- .../contracts/libraries/IndexingAgreement.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index e772b2782..95b1e77e5 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -492,6 +492,13 @@ library IndexingAgreement { return collectionSeconds * (termsV1.tokensPerSecond + termsV1.tokensPerEntityPerSecond * _entities); } + /** + * @notice Checks if the agreement is active + * Requirements: + * - The underlying collector agreement has been accepted + * - The underlying collector agreement's data service is this contract + * - The indexing agreement has been accepted and has a valid allocation ID + **/ function _isActive(AgreementWrapper memory wrapper) private view returns (bool) { return wrapper.collectorAgreement.dataService == address(this) && From 2838d54485e7fecd9a2ea4e80d0a63c8008af7ee Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 21:06:23 -0300 Subject: [PATCH 52/90] f: add CancelAgreementBy.ThirdParty --- .../contracts/interfaces/IRecurringCollector.sol | 3 ++- .../subgraph-service/contracts/SubgraphService.sol | 11 +++++++---- .../contracts/libraries/IndexingAgreement.sol | 14 ++++++-------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index 5b4967dab..1a7fe367f 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -24,7 +24,8 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { // @notice The party that can cancel an agreement enum CancelAgreementBy { ServiceProvider, - Payer + Payer, + ThirdParty } /// @notice A representation of a signed Recurring Collection Agreement (RCA) diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 4b0a6ed7f..6f8056fb1 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -235,7 +235,7 @@ contract SubgraphService is _allocations.get(allocationId).indexer == indexer, SubgraphServiceAllocationNotAuthorized(indexer, allocationId) ); - _cancelAllocationIndexingAgreement(allocationId); + _cancelAllocationIndexingAgreement(allocationId, IRecurringCollector.CancelAgreementBy.ServiceProvider); _closeAllocation(allocationId, false); emit ServiceStopped(indexer, data); } @@ -319,7 +319,7 @@ contract SubgraphService is Allocation.State memory allocation = _allocations.get(allocationId); require(allocation.isStale(maxPOIStaleness), SubgraphServiceCannotForceCloseAllocation(allocationId)); require(!allocation.isAltruistic(), SubgraphServiceAllocationIsAltruistic(allocationId)); - _cancelAllocationIndexingAgreement(allocationId); + _cancelAllocationIndexingAgreement(allocationId, IRecurringCollector.CancelAgreementBy.ThirdParty); _closeAllocation(allocationId, true); } @@ -553,8 +553,11 @@ contract SubgraphService is return address(_graphStaking()); } - function _cancelAllocationIndexingAgreement(address _allocationId) internal { - IndexingAgreement._getStorageManager().cancelForAllocation(_allocationId); + function _cancelAllocationIndexingAgreement( + address _allocationId, + IRecurringCollector.CancelAgreementBy _by + ) internal { + IndexingAgreement._getStorageManager().cancelForAllocation(_allocationId, _by); } /** diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 95b1e77e5..4b174b252 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -350,7 +350,11 @@ library IndexingAgreement { ); } - function cancelForAllocation(StorageManager storage self, address _allocationId) external { + function cancelForAllocation( + StorageManager storage self, + address _allocationId, + IRecurringCollector.CancelAgreementBy by + ) external { bytes16 agreementId = self.allocationToActiveAgreementId[_allocationId]; if (agreementId == bytes16(0)) { return; @@ -361,13 +365,7 @@ library IndexingAgreement { return; } - _cancel( - self, - agreementId, - wrapper.agreement, - wrapper.collectorAgreement, - IRecurringCollector.CancelAgreementBy.ServiceProvider - ); + _cancel(self, agreementId, wrapper.agreement, wrapper.collectorAgreement, by); } function cancelByPayer(StorageManager storage self, bytes16 agreementId) external { From 2e474be3977420d1b39b9c005345eb62a74e2dce Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 21:22:40 -0300 Subject: [PATCH 53/90] f: reword and document onCloseAllocation --- .../contracts/SubgraphService.sol | 11 ++-- .../contracts/libraries/IndexingAgreement.sol | 65 ++++++++++++++++--- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 6f8056fb1..4f3b09e8e 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -235,7 +235,7 @@ contract SubgraphService is _allocations.get(allocationId).indexer == indexer, SubgraphServiceAllocationNotAuthorized(indexer, allocationId) ); - _cancelAllocationIndexingAgreement(allocationId, IRecurringCollector.CancelAgreementBy.ServiceProvider); + _onCloseAllocation(allocationId, false); _closeAllocation(allocationId, false); emit ServiceStopped(indexer, data); } @@ -319,7 +319,7 @@ contract SubgraphService is Allocation.State memory allocation = _allocations.get(allocationId); require(allocation.isStale(maxPOIStaleness), SubgraphServiceCannotForceCloseAllocation(allocationId)); require(!allocation.isAltruistic(), SubgraphServiceAllocationIsAltruistic(allocationId)); - _cancelAllocationIndexingAgreement(allocationId, IRecurringCollector.CancelAgreementBy.ThirdParty); + _onCloseAllocation(allocationId, true); _closeAllocation(allocationId, true); } @@ -553,11 +553,8 @@ contract SubgraphService is return address(_graphStaking()); } - function _cancelAllocationIndexingAgreement( - address _allocationId, - IRecurringCollector.CancelAgreementBy _by - ) internal { - IndexingAgreement._getStorageManager().cancelForAllocation(_allocationId, _by); + function _onCloseAllocation(address _allocationId, bool _stale) internal { + IndexingAgreement._getStorageManager().onCloseAllocation(_allocationId, _stale); } /** diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 4b174b252..1444e05e5 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -286,7 +286,7 @@ library IndexingAgreement { * * Emits {IndexingAgreementUpdated} event * - * @param self The indexing agreement manager storage + * @param self The indexing agreement storage manager * @param indexer The indexer address * @param signedRCAU The signed Recurring Collection Agreement Update */ @@ -324,13 +324,15 @@ library IndexingAgreement { /** * @notice Cancel an indexing agreement. * + * @dev This function allows the indexer to cancel an indexing agreement. + * * Requirements: * - Agreement must be active * - The indexer must be the service provider of the agreement * * Emits {IndexingAgreementCanceled} event * - * @param self The indexing agreement manager storage + * @param self The indexing agreement storage manager * @param indexer The indexer address * @param agreementId The id of the agreement to cancel */ @@ -350,11 +352,23 @@ library IndexingAgreement { ); } - function cancelForAllocation( - StorageManager storage self, - address _allocationId, - IRecurringCollector.CancelAgreementBy by - ) external { + /** + * @notice Cancel an allocation's indexing agreement if it exists. + * + * @dev This function is to be called by the data service when an allocation is closed. + * + * Requirements: + * - The allocation must have an active agreement + * - Agreement must be active + * + * Emits {IndexingAgreementCanceled} event + * + * @param self The indexing agreement storage manager + * @param _allocationId The allocation ID + * @param stale Whether the allocation is stale or not + * + */ + function onCloseAllocation(StorageManager storage self, address _allocationId, bool stale) external { bytes16 agreementId = self.allocationToActiveAgreementId[_allocationId]; if (agreementId == bytes16(0)) { return; @@ -365,9 +379,31 @@ library IndexingAgreement { return; } - _cancel(self, agreementId, wrapper.agreement, wrapper.collectorAgreement, by); + _cancel( + self, + agreementId, + wrapper.agreement, + wrapper.collectorAgreement, + stale + ? IRecurringCollector.CancelAgreementBy.ThirdParty + : IRecurringCollector.CancelAgreementBy.ServiceProvider + ); } + /** + * @notice Cancel an indexing agreement by the payer. + * + * @dev This function allows the payer to cancel an indexing agreement. + * + * Requirements: + * - Agreement must be active + * - The caller must be authorized to cancel the agreement in the collector on the payer's behalf + * + * Emits {IndexingAgreementCanceled} event + * + * @param self The indexing agreement storage manager + * @param agreementId The id of the agreement to cancel + */ function cancelByPayer(StorageManager storage self, bytes16 agreementId) external { AgreementWrapper memory wrapper = _get(self, agreementId); require(_isActive(wrapper), IndexingAgreementNotActive(agreementId)); @@ -455,6 +491,19 @@ library IndexingAgreement { _manager.termsV1[_agreementId].tokensPerEntityPerSecond = newTerms.tokensPerEntityPerSecond; } + /** + * @notice Cancel an indexing agreement. + * + * @dev This function does the actual agreement cancelation. + * + * Emits {IndexingAgreementCanceled} event + * + * @param _manager The indexing agreement storage manager + * @param _agreementId The id of the agreement to cancel + * @param _agreement The indexing agreement state + * @param _collectorAgreement The collector agreement data + * @param _cancelBy The entity that is canceling the agreement + */ function _cancel( StorageManager storage _manager, bytes16 _agreementId, From 8812e9fb5582b2ea2a7988850cc0bfb63dbf978c Mon Sep 17 00:00:00 2001 From: Matias Date: Thu, 5 Jun 2025 21:37:37 -0300 Subject: [PATCH 54/90] f: add natspec explaining the stake lock mechanism --- packages/subgraph-service/contracts/SubgraphService.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 4f3b09e8e..41eb5b8c3 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -258,6 +258,11 @@ contract SubgraphService is * For indexing rewards, see {AllocationManager-_collectIndexingRewards} for more details. * For indexing fees, see {SubgraphService-_collectIndexingFees} for more details. * + * Note that collecting any type of payment will require locking provisioned stake as collateral for a period of time. + * All types of payment share the same pool of provisioned stake however they each have separate accounting: + * - Indexing rewards can make full use of the available stake + * - Query and indexing fees share the pool, combined they can also make full use of the available stake + * * @param indexer The address of the indexer * @param paymentType The type of payment to collect as defined in {IGraphPayments} * @param data Encoded data: From 696f3b4ea495a9d68d07d118fc4d6b6343c33f0c Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 08:47:30 -0300 Subject: [PATCH 55/90] f: add natspec from smells --- .../libraries/DataServiceFeesLib.sol | 20 ++- .../utilities/ProvisionManager.sol | 14 +- .../interfaces/IRecurringCollector.sol | 137 +++++++++++------- .../collectors/RecurringCollector.sol | 40 +++-- 4 files changed, 128 insertions(+), 83 deletions(-) diff --git a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol index ee6759143..d208bee98 100644 --- a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol +++ b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol @@ -10,17 +10,6 @@ library DataServiceFeesLib { using ProvisionTracker for mapping(address => uint256); using LinkedList for LinkedList.List; - // @notice Storage structure for the provision manager - struct ProvisionManagerStorage { - uint256 _minimumProvisionTokens; - uint256 _maximumProvisionTokens; - uint64 _minimumThawingPeriod; - uint64 _maximumThawingPeriod; - uint32 _minimumVerifierCut; - uint32 _maximumVerifierCut; - uint32 _delegationRatio; - } - /** * @notice Locks stake for a service provider to back a payment. * Creates a stake claim, which is stored in a linked list by service provider. @@ -29,6 +18,11 @@ library DataServiceFeesLib { * * Emits a {StakeClaimLocked} event. * + * @param feesProvisionTracker The mapping that tracks the provision tokens for each service provider + * @param claims The mapping that stores stake claims by their ID + * @param claimsLists The mapping that stores linked lists of stake claims by service provider + * @param graphStaking The Horizon staking contract used to lock the tokens + * @param _delegationRatio The delegation ratio to use for the stake claim * @param _serviceProvider The address of the service provider * @param _tokens The amount of tokens to lock in the claim * @param _unlockTimestamp The timestamp when the tokens can be released @@ -65,6 +59,10 @@ library DataServiceFeesLib { /** * @notice Processes a stake claim, releasing the tokens if the claim has expired. * @dev This function is used as a callback in the stake claims linked list traversal. + * @param feesProvisionTracker The mapping that tracks the provision tokens for each service provider. + * @param claims The mapping that stores stake claims by their ID. + * @param _claimId The ID of the stake claim to process. + * @param _acc The accumulator data, which contains the total tokens claimed and the service provider address. * @return Whether the stake claim is still locked, indicating that the traversal should continue or stop. * @return The updated accumulator data */ diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol index d57f5c1ce..a8f5de172 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -205,6 +205,11 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa emit ThawingPeriodRangeSet(_min, _max); } + /** + * @notice Checks if a provision of a service provider is valid according + * to the parameter ranges established. + * @param _serviceProvider The address of the service provider. + */ function _requireValidProvision(address _serviceProvider) internal view { IHorizonStaking.Provision memory provision = _getProvision(_serviceProvider); _checkProvisionTokens(provision); @@ -323,7 +328,12 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa require(_value.isInRange(_min, _max), ProvisionManagerInvalidValue(_revertMessage, _value, _min, _max)); } - function _requireLTE(uint256 _min, uint256 _max) private pure { - require(_min <= _max, ProvisionManagerInvalidRange(_min, _max)); + /** + * @notice Requires that a value is less than or equal to another value. + * @param _a The value to check. + * @param _b The value to compare against. + */ + function _requireLTE(uint256 _a, uint256 _b) private pure { + require(_a <= _b, ProvisionManagerInvalidRange(_a, _b)); } } diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index 1a7fe367f..258904ce9 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -13,7 +13,7 @@ import { IAuthorizable } from "./IAuthorizable.sol"; * recurrent payments. */ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { - // @notice The state of an agreement + /// @notice The state of an agreement enum AgreementState { NotAccepted, Accepted, @@ -21,80 +21,106 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { CanceledByPayer } - // @notice The party that can cancel an agreement + /// @notice The party that can cancel an agreement enum CancelAgreementBy { ServiceProvider, Payer, ThirdParty } - /// @notice A representation of a signed Recurring Collection Agreement (RCA) + /** + * @notice A representation of a signed Recurring Collection Agreement (RCA) + * @param rca The Recurring Collection Agreement to be signed + * @param signature The signature of the RCA - 65 bytes: r (32 Bytes) || s (32 Bytes) || v (1 Byte) + */ struct SignedRCA { - // The RCA RecurringCollectionAgreement rca; - // Signature - 65 bytes: r (32 Bytes) || s (32 Bytes) || v (1 Byte) bytes signature; } - /// @notice The Recurring Collection Agreement (RCA) + /** + * @notice The Recurring Collection Agreement (RCA) + * @param agreementId The agreement ID of the RCA + * @param deadline The deadline for accepting the RCA + * @param endsAt The timestamp when the agreement ends + * @param payer The address of the payer the RCA was issued by + * @param dataService The address of the data service the RCA was issued to + * @param serviceProvider The address of the service provider the RCA was issued to + * @param maxInitialTokens The maximum amount of tokens that can be collected in the first collection + * on top of the amount allowed for subsequent collections + * @param maxOngoingTokensPerSecond The maximum amount of tokens that can be collected per second + * except for the first collection + * @param minSecondsPerCollection The minimum amount of seconds that must pass between collections + * @param maxSecondsPerCollection The maximum amount of seconds that can pass between collections + * @param metadata Arbitrary metadata to extend functionality if a data service requires it + * + */ struct RecurringCollectionAgreement { - // The agreement ID of the RCA bytes16 agreementId; - // The deadline for accepting the RCA uint64 deadline; - // The timestamp when the agreement ends uint64 endsAt; - // The address of the payer the RCA was issued by address payer; - // The address of the data service the RCA was issued to address dataService; - // The address of the service provider the RCA was issued to address serviceProvider; - // The maximum amount of tokens that can be collected in the first collection - // on top of the amount allowed for subsequent collections uint256 maxInitialTokens; - // The maximum amount of tokens that can be collected per second - // except for the first collection uint256 maxOngoingTokensPerSecond; - // The minimum amount of seconds that must pass between collections uint32 minSecondsPerCollection; - // The maximum amount of seconds that can pass between collections uint32 maxSecondsPerCollection; - // Arbitrary metadata to extend functionality if a data service requires it bytes metadata; } - /// @notice A representation of a signed Recurring Collection Agreement Update (RCAU) + /** + * @notice A representation of a signed Recurring Collection Agreement Update (RCAU) + * @param rcau The Recurring Collection Agreement Update to be signed + * @param signature The signature of the RCAU - 65 bytes: r (32 Bytes) || s (32 Bytes) || v (1 Byte) + */ struct SignedRCAU { - // The RCAU RecurringCollectionAgreementUpdate rcau; - // Signature - 65 bytes: r (32 Bytes) || s (32 Bytes) || v (1 Byte) bytes signature; } - /// @notice The Recurring Collection Agreement Update (RCAU) + /** + * @notice The Recurring Collection Agreement Update (RCAU) + * @param agreementId The agreement ID of the RCAU + * @param deadline The deadline for upgrading the RCA + * @param endsAt The timestamp when the agreement ends + * @param maxInitialTokens The maximum amount of tokens that can be collected in the first collection + * on top of the amount allowed for subsequent collections + * @param maxOngoingTokensPerSecond The maximum amount of tokens that can be collected per second + * except for the first collection + * @param minSecondsPerCollection The minimum amount of seconds that must pass between collections + * @param maxSecondsPerCollection The maximum amount of seconds that can pass between collections + * @param metadata Arbitrary metadata to extend functionality if a data service requires it + */ struct RecurringCollectionAgreementUpdate { - // The agreement ID bytes16 agreementId; - // The deadline for upgrading uint64 deadline; - // The timestamp when the agreement ends uint64 endsAt; - // The maximum amount of tokens that can be collected in the first collection - // on top of the amount allowed for subsequent collections uint256 maxInitialTokens; - // The maximum amount of tokens that can be collected per second - // except for the first collection uint256 maxOngoingTokensPerSecond; - // The minimum amount of seconds that must pass between collections uint32 minSecondsPerCollection; - // The maximum amount of seconds that can pass between collections uint32 maxSecondsPerCollection; - // Arbitrary metadata to extend functionality if a data service requires it bytes metadata; } - /// @notice The data for an agreement + /** + * @notice The data for an agreement + * @dev This struct is used to store the data of an agreement in the contract + * @param dataService The address of the data service + * @param payer The address of the payer + * @param serviceProvider The address of the service provider + * @param acceptedAt The timestamp when the agreement was accepted + * @param lastCollectionAt The timestamp when the agreement was last collected at + * @param endsAt The timestamp when the agreement ends + * @param maxInitialTokens The maximum amount of tokens that can be collected in the first collection + * on top of the amount allowed for subsequent collections + * @param maxOngoingTokensPerSecond The maximum amount of tokens that can be collected per second + * except for the first collection + * @param minSecondsPerCollection The minimum amount of seconds that must pass between collections + * @param maxSecondsPerCollection The maximum amount of seconds that can pass between collections + * @param canceledAt The timestamp when the agreement was canceled + * @param state The state of the agreement + */ struct AgreementData { // The address of the data service address dataService; @@ -124,14 +150,17 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { AgreementState state; } - /// @notice The params for collecting an agreement + /** + * @notice The params for collecting an agreement + * @param agreementId The agreement ID of the RCA + * @param collectionId The collection ID of the RCA + * @param tokens The amount of tokens to collect + * @param dataServiceCut The data service cut in parts per million + */ struct CollectParams { bytes16 agreementId; - // The collection ID bytes32 collectionId; - // The amount of tokens to collect uint256 tokens; - // The data service cut in PPM uint256 dataServiceCut; } @@ -226,50 +255,50 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { ); /** - * Thrown when accepting an agreement with a zero ID + * @notice Thrown when accepting an agreement with a zero ID */ error RecurringCollectorAgreementIdZero(); /** - * Thrown when interacting with an agreement not owned by the message sender + * @notice Thrown when interacting with an agreement not owned by the message sender * @param agreementId The agreement ID * @param unauthorizedDataService The address of the unauthorized data service */ error RecurringCollectorDataServiceNotAuthorized(bytes16 agreementId, address unauthorizedDataService); /** - * Thrown when interacting with an agreement with an elapsed deadline + * @notice Thrown when interacting with an agreement with an elapsed deadline * @param currentTimestamp The current timestamp * @param deadline The elapsed deadline timestamp */ error RecurringCollectorAgreementDeadlineElapsed(uint256 currentTimestamp, uint64 deadline); /** - * Thrown when the signer is invalid + * @notice Thrown when the signer is invalid */ error RecurringCollectorInvalidSigner(); /** - * Thrown when the payment type is not IndexingFee + * @notice Thrown when the payment type is not IndexingFee * @param invalidPaymentType The invalid payment type */ error RecurringCollectorInvalidPaymentType(IGraphPayments.PaymentTypes invalidPaymentType); /** - * Thrown when the caller is not the data service the RCA was issued to + * @notice Thrown when the caller is not the data service the RCA was issued to * @param unauthorizedCaller The address of the caller * @param dataService The address of the data service */ error RecurringCollectorUnauthorizedCaller(address unauthorizedCaller, address dataService); /** - * Thrown when calling collect() with invalid data + * @notice Thrown when calling collect() with invalid data * @param invalidData The invalid data */ error RecurringCollectorInvalidCollectData(bytes invalidData); /** - * Thrown when calling collect() on a payer canceled agreement + * @notice Thrown when calling collect() on a payer canceled agreement * where the final collection has already been done * @param agreementId The agreement ID * @param finalCollectionAt The timestamp when the final collection was done @@ -277,26 +306,26 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { error RecurringCollectorFinalCollectionDone(bytes16 agreementId, uint256 finalCollectionAt); /** - * Thrown when interacting with an agreement that has an incorrect state + * @notice Thrown when interacting with an agreement that has an incorrect state * @param agreementId The agreement ID * @param incorrectState The incorrect state */ error RecurringCollectorAgreementIncorrectState(bytes16 agreementId, AgreementState incorrectState); /** - * Thrown when accepting an agreement with an address that is not set + * @notice Thrown when accepting an agreement with an address that is not set */ error RecurringCollectorAgreementAddressNotSet(); /** - * Thrown when accepting or upgrading an agreement with an elapsed endsAt + * @notice Thrown when accepting or upgrading an agreement with an elapsed endsAt * @param currentTimestamp The current timestamp * @param endsAt The agreement end timestamp */ error RecurringCollectorAgreementElapsedEndsAt(uint256 currentTimestamp, uint64 endsAt); /** - * Thrown when accepting or upgrading an agreement with an elapsed endsAt + * @notice Thrown when accepting or upgrading an agreement with an elapsed endsAt * @param allowedMinCollectionWindow The allowed minimum collection window * @param minSecondsPerCollection The minimum seconds per collection * @param maxSecondsPerCollection The maximum seconds per collection @@ -308,21 +337,21 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { ); /** - * Thrown when accepting or upgrading an agreement with an invalid duration + * @notice Thrown when accepting or upgrading an agreement with an invalid duration * @param requiredMinDuration The required minimum duration * @param invalidDuration The invalid duration */ error RecurringCollectorAgreementInvalidDuration(uint32 requiredMinDuration, uint256 invalidDuration); /** - * Thrown when calling collect() on an elapsed agreement + * @notice Thrown when calling collect() on an elapsed agreement * @param agreementId The agreement ID * @param endsAt The agreement end timestamp */ error RecurringCollectorAgreementElapsed(bytes16 agreementId, uint64 endsAt); /** - * Thrown when calling collect() too soon + * @notice Thrown when calling collect() too soon * @param agreementId The agreement ID * @param secondsSinceLast Seconds since last collection * @param minSeconds Minimum seconds between collections @@ -330,7 +359,7 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { error RecurringCollectorCollectionTooSoon(bytes16 agreementId, uint32 secondsSinceLast, uint32 minSeconds); /** - * Thrown when calling collect() too late + * @notice Thrown when calling collect() too late * @param agreementId The agreement ID * @param secondsSinceLast Seconds since last collection * @param maxSeconds Maximum seconds between collections diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index 8438fe56a..2e0e65102 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -6,6 +6,8 @@ import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { Authorizable } from "../../utilities/Authorizable.sol"; import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; +// solhint-disable-next-line no-unused-import +import { IPaymentsCollector } from "../../interfaces/IPaymentsCollector.sol"; // for @inheritdoc import { IRecurringCollector } from "../../interfaces/IRecurringCollector.sol"; import { IGraphPayments } from "../../interfaces/IGraphPayments.sol"; import { PPMMath } from "../../libraries/PPMMath.sol"; @@ -54,8 +56,9 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC ) EIP712(eip712Name, eip712Version) GraphDirectory(controller) Authorizable(revokeSignerThawingPeriod) {} /** + * @inheritdoc IPaymentsCollector * @notice Initiate a payment collection through the payments protocol. - * See {IGraphPayments.collect}. + * See {IPaymentsCollector.collect}. * @dev Caller must be the data service the RCA was issued to. */ function collect(IGraphPayments.PaymentTypes paymentType, bytes calldata data) external returns (uint256) { @@ -71,6 +74,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** + * @inheritdoc IRecurringCollector * @notice Accept an indexing agreement. * See {IRecurringCollector.accept}. * @dev Caller must be the data service the RCA was issued to. @@ -136,6 +140,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** + * @inheritdoc IRecurringCollector * @notice Cancel an indexing agreement. * See {IRecurringCollector.cancel}. * @dev Caller must be the data service for the agreement. @@ -168,6 +173,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** + * @inheritdoc IRecurringCollector * @notice Update an indexing agreement. * See {IRecurringCollector.update}. * @dev Caller must be the data service for the agreement. @@ -218,43 +224,35 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC ); } - /** - * @notice See {IRecurringCollector.recoverRCASigner} - */ + /// @inheritdoc IRecurringCollector function recoverRCASigner(SignedRCA calldata signedRCA) external view returns (address) { return _recoverRCASigner(signedRCA); } - /** - * @notice See {IRecurringCollector.recoverRCAUSigner} - */ + /// @inheritdoc IRecurringCollector function recoverRCAUSigner(SignedRCAU calldata signedRCAU) external view returns (address) { return _recoverRCAUSigner(signedRCAU); } - /** - * @notice See {IRecurringCollector.hashRCA} - */ + /// @inheritdoc IRecurringCollector function hashRCA(RecurringCollectionAgreement calldata rca) external view returns (bytes32) { return _hashRCA(rca); } - /** - * @notice See {IRecurringCollector.hashRCAU} - */ + /// @inheritdoc IRecurringCollector function hashRCAU(RecurringCollectionAgreementUpdate calldata rcau) external view returns (bytes32) { return _hashRCAU(rcau); } - /** - * @notice See {IRecurringCollector.getAgreement} - */ + /// @inheritdoc IRecurringCollector function getAgreement(bytes16 agreementId) external view returns (AgreementData memory) { return _getAgreement(agreementId); } /** * @notice Decodes the collect data. + * @param data The encoded collect parameters. + * @return The decoded collect parameters. */ function decodeCollectData(bytes calldata data) public pure returns (CollectParams memory) { return abi.decode(data, (CollectParams)); @@ -355,6 +353,10 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @notice Requires that the collection params are valid. + * @param _agreement The agreement data + * @param _agreementId The ID of the agreement + * @param _tokens The number of tokens to collect + * @return The number of tokens that can be collected */ function _requireValidCollect( AgreementData memory _agreement, @@ -457,6 +459,8 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @notice Requires that the signer for the RCA is authorized * by the payer of the RCA. + * @param _signedRCA The signed RCA to verify + * @return The address of the authorized signer */ function _requireAuthorizedRCASigner(SignedRCA memory _signedRCA) private view returns (address) { address signer = _recoverRCASigner(_signedRCA); @@ -481,6 +485,8 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @notice Gets an agreement to be updated. + * @param _agreementId The ID of the agreement to get + * @return The storage reference to the agreement data */ function _getAgreementStorage(bytes16 _agreementId) private view returns (AgreementData storage) { return agreements[_agreementId]; @@ -488,6 +494,8 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @notice See {IRecurringCollector.getAgreement} + * @param _agreementId The ID of the agreement to get + * @return The agreement data */ function _getAgreement(bytes16 _agreementId) private view returns (AgreementData memory) { return agreements[_agreementId]; From 7b209b7821dacbb1162ce66f425514f87cfe5642 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 13:57:44 -0300 Subject: [PATCH 56/90] f: fully commit to DataServiceFeesLib --- .../extensions/DataServiceFees.sol | 4 +-- .../libraries/DataServiceFeesLib.sol | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol index df42623cf..9c862bf75 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -95,7 +95,7 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat * @param _claimId The ID of the stake claim to delete */ function _deleteStakeClaim(bytes32 _claimId) private { - delete claims[_claimId]; + DataServiceFeesLib.deleteStakeClaim(claims, _claimId); } /** @@ -105,6 +105,6 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat * @return The next stake claim ID */ function _getNextStakeClaim(bytes32 _claimId) private view returns (bytes32) { - return claims[_claimId].nextClaim; + return DataServiceFeesLib.getNextStakeClaim(claims, _claimId); } } diff --git a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol index d208bee98..372cd30c5 100644 --- a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol +++ b/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol @@ -92,6 +92,33 @@ library DataServiceFeesLib { return (false, _acc); } + /** + * @notice Deletes a stake claim. + * @dev This function is used as a callback in the stake claims linked list traversal. + * @param claims The mapping that stores stake claims by their ID + * @param claimId The ID of the stake claim to delete + */ + function deleteStakeClaim( + mapping(bytes32 claimId => IDataServiceFees.StakeClaim claim) storage claims, + bytes32 claimId + ) external { + delete claims[claimId]; + } + + /** + * @notice Gets the next stake claim in the linked list + * @dev This function is used as a callback in the stake claims linked list traversal. + * @param claims The mapping that stores stake claims by their ID + * @param claimId The ID of the stake claim + * @return The next stake claim ID + */ + function getNextStakeClaim( + mapping(bytes32 claimId => IDataServiceFees.StakeClaim claim) storage claims, + bytes32 claimId + ) external view returns (bytes32) { + return claims[claimId].nextClaim; + } + /** * @notice Builds a stake claim ID * @param serviceProvider The address of the service provider From 18989b114eb40e83cd52322e8086621e69e448c7 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 14:33:26 -0300 Subject: [PATCH 57/90] f: remove unused getGraphStaking() --- packages/subgraph-service/contracts/SubgraphService.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 41eb5b8c3..b67e3bff7 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -554,10 +554,6 @@ contract SubgraphService is return _isOverAllocated(indexer, _delegationRatio); } - function getGraphStaking() external view returns (address) { - return address(_graphStaking()); - } - function _onCloseAllocation(address _allocationId, bool _stale) internal { IndexingAgreement._getStorageManager().onCloseAllocation(_allocationId, _stale); } From 01c84d73ad4530394be165d55cdb994a56de08bd Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 14:33:47 -0300 Subject: [PATCH 58/90] f: more NatSpec --- .../collectors/RecurringCollector.sol | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index 2e0e65102..079a8c6f7 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -322,6 +322,13 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC return tokensToCollect; } + /** + * @notice Requires that the collection window parameters are valid. + * + * @param _endsAt The end time of the agreement + * @param _minSecondsPerCollection The minimum seconds per collection + * @param _maxSecondsPerCollection The maximum seconds per collection + */ function _requireValidCollectionWindowParams( uint64 _endsAt, uint32 _minSecondsPerCollection, @@ -394,7 +401,9 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** - * @notice See {IRecurringCollector.recoverRCASigner} + * @notice See {recoverRCASigner} + * @param _signedRCA The signed RCA to recover the signer from + * @return The address of the signer */ function _recoverRCASigner(SignedRCA memory _signedRCA) private view returns (address) { bytes32 messageHash = _hashRCA(_signedRCA.rca); @@ -402,7 +411,9 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** - * @notice See {IRecurringCollector.recoverRCAUSigner} + * @notice See {recoverRCAUSigner} + * @param _signedRCAU The signed RCAU to recover the signer from + * @return The address of the signer */ function _recoverRCAUSigner(SignedRCAU memory _signedRCAU) private view returns (address) { bytes32 messageHash = _hashRCAU(_signedRCAU.rcau); @@ -410,7 +421,9 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** - * @notice See {IRecurringCollector.hashRCA} + * @notice See {hashRCA} + * @param _rca The RCA to hash + * @return The EIP712 hash of the RCA */ function _hashRCA(RecurringCollectionAgreement memory _rca) private view returns (bytes32) { return @@ -435,7 +448,9 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** - * @notice See {IRecurringCollector.hashRCAU} + * @notice See {hashRCAU} + * @param _rcau The RCAU to hash + * @return The EIP712 hash of the RCAU */ function _hashRCAU(RecurringCollectionAgreementUpdate memory _rcau) private view returns (bytes32) { return @@ -472,6 +487,9 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC /** * @notice Requires that the signer for the RCAU is authorized * by the payer. + * @param _signedRCAU The signed RCAU to verify + * @param _payer The address of the payer + * @return The address of the authorized signer */ function _requireAuthorizedRCAUSigner( SignedRCAU memory _signedRCAU, @@ -493,7 +511,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC } /** - * @notice See {IRecurringCollector.getAgreement} + * @notice See {getAgreement} * @param _agreementId The ID of the agreement to get * @return The agreement data */ @@ -501,6 +519,11 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC return agreements[_agreementId]; } + /** + * @notice Gets the start time for the collection of an agreement. + * @param _agreement The agreement data + * @return The start time for the collection of the agreement + */ function _agreementCollectionStartAt(AgreementData memory _agreement) private pure returns (uint256) { return _agreement.lastCollectionAt > 0 ? _agreement.lastCollectionAt : _agreement.acceptedAt; } From 7cb4a0c674a4e78630714c5cc49672d600c065f9 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 14:58:41 -0300 Subject: [PATCH 59/90] f: more NatSpec --- .../contracts/DisputeManager.sol | 1 + .../contracts/SubgraphService.sol | 27 +++++++ .../contracts/interfaces/IDisputeManager.sol | 1 + .../contracts/interfaces/ISubgraphService.sol | 1 + .../libraries/AllocationManagerLib.sol | 70 +++++++++++++++++++ 5 files changed, 100 insertions(+) diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 76b2342df..18db10523 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -523,6 +523,7 @@ contract DisputeManager is * @param _agreementId The agreement id being disputed * @param _poi The POI being disputed * @param _entities The number of entities disputed + * @param _blockNumber The block number of the disputed POI * @return The dispute id */ function _createIndexingFeeDisputeV1( diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index b67e3bff7..c9d766d0f 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -67,6 +67,7 @@ contract SubgraphService is * @param disputeManager The address of the DisputeManager contract * @param graphTallyCollector The address of the GraphTallyCollector contract * @param curation The address of the Curation contract + * @param recurringCollector The address of the RecurringCollector contract */ constructor( address graphController, @@ -394,7 +395,9 @@ contract SubgraphService is } /** + * @inheritdoc ISubgraphService * @notice Accept an indexing agreement. + * * See {ISubgraphService.acceptIndexingAgreement}. * * Requirements: @@ -428,7 +431,9 @@ contract SubgraphService is } /** + * @inheritdoc ISubgraphService * @notice Update an indexing agreement. + * * See {IndexingAgreement.update}. * * Requirements: @@ -452,7 +457,9 @@ contract SubgraphService is } /** + * @inheritdoc ISubgraphService * @notice Cancel an indexing agreement by indexer / operator. + * * See {IndexingAgreement.cancel}. * * @dev Can only be canceled on behalf of a valid indexer. @@ -478,7 +485,9 @@ contract SubgraphService is } /** + * @inheritdoc ISubgraphService * @notice Cancel an indexing agreement by payer / signer. + * * See {ISubgraphService.cancelIndexingAgreementByPayer}. * * Requirements: @@ -493,6 +502,7 @@ contract SubgraphService is IndexingAgreement._getStorageManager().cancelByPayer(agreementId); } + /// @inheritdoc ISubgraphService function getIndexingAgreement( bytes16 agreementId ) external view returns (IndexingAgreement.AgreementWrapper memory) { @@ -554,6 +564,12 @@ contract SubgraphService is return _isOverAllocated(indexer, _delegationRatio); } + /** + * @notice Internal function to handle closing an allocation + * @dev This function is called when an allocation is closed, either by the indexer or by a third party + * @param _allocationId The id of the allocation being closed + * @param _stale Whether the allocation is stale or not + */ function _onCloseAllocation(address _allocationId, bool _stale) internal { IndexingAgreement._getStorageManager().onCloseAllocation(_allocationId, _stale); } @@ -569,6 +585,10 @@ contract SubgraphService is emit PaymentsDestinationSet(_indexer, _paymentsDestination); } + /** + * @notice Requires that the indexer is registered + * @param _indexer The address of the indexer + */ function _requireRegisteredIndexer(address _indexer) internal view { require(indexers[_indexer].registeredAt != 0, SubgraphServiceIndexerNotRegistered(_indexer)); } @@ -764,6 +784,13 @@ contract SubgraphService is emit StakeToFeesRatioSet(_stakeToFeesRatio); } + /** + * @notice Release stake claims and lock new stake as economic security for fees + * @dev This function releases all expired stake claims and locks new stake as economic security for fees. + * It is called after collecting query fees or indexing fees. + * @param _indexer The address of the indexer + * @param _tokensCollected The amount of tokens collected from fees + */ function _releaseAndLockStake(address _indexer, uint256 _tokensCollected) private { _releaseStake(_indexer, 0); if (_tokensCollected > 0) { diff --git a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol index 194b14a21..5133b38a0 100644 --- a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol +++ b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol @@ -123,6 +123,7 @@ interface IDisputeManager { * @param indexer The indexer address * @param fisherman The fisherman address * @param tokens The amount of tokens deposited by the fisherman + * @param payer The address of the payer of the indexing fee * @param agreementId The agreement id * @param poi The POI disputed * @param entities The entities disputed diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol index 4f0115530..1ba5e647a 100644 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol @@ -290,6 +290,7 @@ interface ISubgraphService is IDataServiceFees { /** * @notice Get the indexing agreement for a given agreement ID. * @param agreementId The id of the indexing agreement + * @return The indexing agreement details */ function getIndexingAgreement( bytes16 agreementId diff --git a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol index 428c54e39..5efa9165b 100644 --- a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol +++ b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol @@ -60,6 +60,10 @@ library AllocationManagerLib { * Emits a {AllocationCreated} event * * @param _allocations The mapping of allocation ids to allocation states + * @param _legacyAllocations The mapping of legacy allocation ids to legacy allocation states + * @param allocationProvisionTracker The mapping of indexers to their locked tokens + * @param _subgraphAllocatedTokens The mapping of subgraph deployment ids to their allocated tokens + * @param params The parameters for the allocation */ function allocate( mapping(address allocationId => Allocation.State allocation) storage _allocations, @@ -103,6 +107,32 @@ library AllocationManagerLib { ); } + /** + * @notice Present a POI to collect indexing rewards for an allocation + * This function will mint indexing rewards using the {RewardsManager} and distribute them to the indexer and delegators. + * + * Conditions to qualify for indexing rewards: + * - POI must be non-zero + * - POI must not be stale, i.e: older than `maxPOIStaleness` + * - allocation must not be altruistic (allocated tokens = 0) + * - allocation must be open for at least one epoch + * + * Note that indexers are required to periodically (at most every `maxPOIStaleness`) present POIs to collect rewards. + * Rewards will not be issued to stale POIs, which means that indexers are advised to present a zero POI if they are + * unable to present a valid one to prevent being locked out of future rewards. + * + * Note on allocation duration restriction: this is required to ensure that non protocol chains have a valid block number for + * which to calculate POIs. EBO posts once per epoch typically at each epoch change, so we restrict rewards to allocations + * that have gone through at least one epoch change. + * + * Emits a {IndexingRewardsCollected} event. + * + * @param _allocations The mapping of allocation ids to allocation states + * @param allocationProvisionTracker The mapping of indexers to their locked tokens + * @param _subgraphAllocatedTokens The mapping of subgraph deployment ids to their allocated tokens + * @param params The parameters for the POI presentation + * @return The amount of tokens collected + */ function presentPOI( mapping(address allocationId => Allocation.State allocation) storage _allocations, mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, @@ -195,6 +225,22 @@ library AllocationManagerLib { return tokensRewards; } + /** + * @notice Close an allocation + * Does not require presenting a POI, use {_collectIndexingRewards} to present a POI and collect rewards + * @dev Note that allocations are nowlong lived. All service payments, including indexing rewards, should be collected periodically + * without the need of closing the allocation. Allocations should only be closed when indexers want to reclaim the allocated + * tokens for other purposes. + * + * Emits a {AllocationClosed} event + * + * @param _allocations The mapping of allocation ids to allocation states + * @param allocationProvisionTracker The mapping of indexers to their locked tokens + * @param _subgraphAllocatedTokens The mapping of subgraph deployment ids to their allocated tokens + * @param graphRewardsManager The rewards manager to handle rewards distribution + * @param _allocationId The id of the allocation to be closed + * @param _forceClosed Whether the allocation was force closed + */ function closeAllocation( mapping(address allocationId => Allocation.State allocation) storage _allocations, mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, @@ -302,6 +348,22 @@ library AllocationManagerLib { return _isOverAllocated(allocationProvisionTracker, graphStaking, _indexer, _delegationRatio); } + /** + * @notice Close an allocation + * Does not require presenting a POI, use {_collectIndexingRewards} to present a POI and collect rewards + * @dev Note that allocations are nowlong lived. All service payments, including indexing rewards, should be collected periodically + * without the need of closing the allocation. Allocations should only be closed when indexers want to reclaim the allocated + * tokens for other purposes. + * + * Emits a {AllocationClosed} event + * + * @param _allocations The mapping of allocation ids to allocation states + * @param allocationProvisionTracker The mapping of indexers to their locked tokens + * @param _subgraphAllocatedTokens The mapping of subgraph deployment ids to their allocated tokens + * @param graphRewardsManager The rewards manager to handle rewards distribution + * @param _allocationId The id of the allocation to be closed + * @param _forceClosed Whether the allocation was force closed + */ function _closeAllocation( mapping(address allocationId => Allocation.State allocation) storage _allocations, mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, @@ -335,6 +397,14 @@ library AllocationManagerLib { ); } + /** + * @notice Checks if an allocation is over-allocated + * @param allocationProvisionTracker The mapping of indexers to their locked tokens + * @param graphStaking The Horizon staking contract to check delegation ratios + * @param _indexer The address of the indexer + * @param _delegationRatio The delegation ratio to consider when locking tokens + * @return True if the allocation is over-allocated, false otherwise + */ function _isOverAllocated( mapping(address indexer => uint256 tokens) storage allocationProvisionTracker, IHorizonStaking graphStaking, From f124dce52b0f902890c831d8b5478cc1fba070e7 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 15:01:49 -0300 Subject: [PATCH 60/90] f: more NatSpec --- .../libraries/AllocationManagerLib.sol | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol index 5efa9165b..a231ed1ba 100644 --- a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol +++ b/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol @@ -24,6 +24,19 @@ library AllocationManagerLib { using PPMMath for uint256; using TokenUtils for IGraphToken; + /** + * @notice Parameters for the allocation creation + * @param currentEpoch The current epoch at the time of allocation creation + * @param graphStaking The Horizon staking contract to handle token locking + * @param graphRewardsManager The rewards manager to handle rewards distribution + * @param _encodeAllocationProof The EIP712 encoded allocation proof + * @param _indexer The address of the indexer creating the allocation + * @param _allocationId The id of the allocation to be created + * @param _subgraphDeploymentId The id of the subgraph deployment for which the allocation is created + * @param _tokens The amount of tokens to allocate + * @param _allocationProof The EIP712 proof, an EIP712 signed message of (indexer,allocationId) + * @param _delegationRatio The delegation ratio to consider when locking tokens + */ struct AllocateParams { uint256 currentEpoch; IHorizonStaking graphStaking; @@ -37,6 +50,19 @@ library AllocationManagerLib { uint32 _delegationRatio; } + /** + * @notice Parameters for the POI presentation + * @param maxPOIStaleness The maximum staleness of the POI in epochs + * @param graphEpochManager The epoch manager to get the current epoch + * @param graphStaking The Horizon staking contract to handle token locking + * @param graphRewardsManager The rewards manager to handle rewards distribution + * @param graphToken The Graph token contract to handle token transfers + * @param _allocationId The id of the allocation for which the POI is presented + * @param _poi The proof of indexing (POI) to be presented + * @param _poiMetadata The metadata associated with the POI + * @param _delegationRatio The delegation ratio to consider when locking tokens + * @param _paymentsDestination The address to which the indexing rewards should be sent + */ struct PresentParams { uint256 maxPOIStaleness; IEpochManager graphEpochManager; @@ -272,6 +298,11 @@ library AllocationManagerLib { * * Emits a {AllocationResized} event. * + * @param _allocations The mapping of allocation ids to allocation states + * @param allocationProvisionTracker The mapping of indexers to their locked tokens + * @param _subgraphAllocatedTokens The mapping of subgraph deployment ids to their allocated tokens + * @param graphStaking The Horizon staking contract to handle token locking + * @param graphRewardsManager The rewards manager to handle rewards distribution * @param _allocationId The id of the allocation to be resized * @param _tokens The new amount of tokens to allocate * @param _delegationRatio The delegation ratio to consider when locking tokens @@ -335,6 +366,8 @@ library AllocationManagerLib { /** * @notice Checks if an allocation is over-allocated + * @param allocationProvisionTracker The mapping of indexers to their locked tokens + * @param graphStaking The Horizon staking contract to check delegation ratios * @param _indexer The address of the indexer * @param _delegationRatio The delegation ratio to consider when locking tokens * @return True if the allocation is over-allocated, false otherwise From f7298187833dca2f2265be0146eeca465158905a Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 15:08:47 -0300 Subject: [PATCH 61/90] f: more NatSpec --- packages/subgraph-service/contracts/libraries/Decoder.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/subgraph-service/contracts/libraries/Decoder.sol b/packages/subgraph-service/contracts/libraries/Decoder.sol index 82b9da673..edb5cfb6e 100644 --- a/packages/subgraph-service/contracts/libraries/Decoder.sol +++ b/packages/subgraph-service/contracts/libraries/Decoder.sol @@ -16,7 +16,8 @@ library Decoder { * @notice Decodes the data for collecting indexing fees. * * @param data The data to decode. - * @return The decoded data + * @return agreementId The agreement ID + * @return nestedData The nested encoded data */ function decodeCollectIndexingFeeData(bytes memory data) public pure returns (bytes16, bytes memory) { try UnsafeDecoder.decodeCollectIndexingFeeData_(data) returns (bytes16 agreementId, bytes memory nestedData) { @@ -66,6 +67,9 @@ library Decoder { * @notice Decodes the collect data for indexing fees V1. * * @param data The data to decode. + * @return entities The number of entities + * @return poi The proof of indexing (POI) + * @return epoch The epoch of the POI */ function decodeCollectIndexingFeeDataV1(bytes memory data) public pure returns (uint256, bytes32, uint256) { try UnsafeDecoder.decodeCollectIndexingFeeDataV1_(data) returns (uint256 entities, bytes32 poi, uint256 epoch) { From 1735d137c8213af506d15c6ae7bb8d5d272c8995 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 15:15:44 -0300 Subject: [PATCH 62/90] f: more NatSpec --- .../contracts/SubgraphService.sol | 2 +- .../contracts/libraries/IndexingAgreement.sol | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index c9d766d0f..642a13718 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -412,7 +412,7 @@ contract SubgraphService is * * @dev signedRCA.rca.metadata is an encoding of {IndexingAgreement.AcceptIndexingAgreementMetadata} * - * Emits {IndexingAgreementAccepted} event + * Emits {IndexingAgreement.IndexingAgreementAccepted} event * * @param allocationId The id of the allocation * @param signedRCA The signed Recurring Collection Agreement diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 1444e05e5..be4c1e475 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -90,6 +90,8 @@ library IndexingAgreement { * @param indexer The address of the indexer * @param payer The address paying for the indexing fees * @param agreementId The id of the agreement + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment * @param currentEpoch The current epoch * @param tokensCollected The amount of tokens collected * @param entities The number of entities indexed @@ -218,6 +220,24 @@ library IndexingAgreement { */ error IndexingAgreementNotAuthorized(bytes16 agreementId, address unauthorizedIndexer); + /** + * @notice Accept an indexing agreement. + * + * Requirements: + * - Allocation must belong to the indexer and be open + * - Agreement must be for this data service + * - Agreement's subgraph deployment must match the allocation's subgraph deployment + * - Agreement must not have been accepted before + * - Allocation must not have an agreement already + * + * @dev signedRCA.rca.metadata is an encoding of {IndexingAgreement.AcceptIndexingAgreementMetadata} + * + * Emits {IndexingAgreementAccepted} event + * + * @param self The indexing agreement storage manager + * @param allocationId The id of the allocation + * @param signedRCA The signed Recurring Collection Agreement + */ function accept( StorageManager storage self, mapping(address allocationId => Allocation.State allocation) storage allocations, @@ -420,6 +440,25 @@ library IndexingAgreement { ); } + /** + * @notice Collect Indexing fees + * @dev Uses the {RecurringCollector} to collect payment from Graph Horizon payments protocol. + * Fees are distributed to service provider and delegators by {GraphPayments} + * + * Requirements: + * - Allocation must be open + * - Agreement must be active + * - Agreement must be of version V1 + * - The data must be encoded as per {Decoder.decodeCollectIndexingFeeDataV1} + * + * Emits a {IndexingFeesCollectedV1} event. + * + * @param self The indexing agreement storage manager + * @param allocations The mapping of allocation IDs to their states + * @param params The parameters for collecting indexing fees + * @return The address of the service provider that collected the fees + * @return The amount of fees collected + */ function collect( StorageManager storage self, mapping(address allocationId => Allocation.State allocation) storage allocations, @@ -471,6 +510,13 @@ library IndexingAgreement { return (wrapper.collectorAgreement.serviceProvider, tokensCollected); } + /** + * @notice Get the indexing agreement for a given agreement ID. + * + * @param self The indexing agreement storage manager + * @param agreementId The id of the indexing agreement + * @return The indexing agreement wrapper containing the agreement state and collector agreement data + */ function get(StorageManager storage self, bytes16 agreementId) external view returns (AgreementWrapper memory) { AgreementWrapper memory wrapper = _get(self, agreementId); require(wrapper.collectorAgreement.dataService == address(this), IndexingAgreementNotActive(agreementId)); From 5ce9b4e17349f98d151fafd401072784683c6078 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 15:35:17 -0300 Subject: [PATCH 63/90] f: more NatSpec --- .../contracts/libraries/IndexingAgreement.sol | 1 + .../contracts/libraries/SubgraphServiceLib.sol | 12 ++++++++++++ .../contracts/libraries/UnsafeDecoder.sol | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index be4c1e475..2f637d4f2 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -235,6 +235,7 @@ library IndexingAgreement { * Emits {IndexingAgreementAccepted} event * * @param self The indexing agreement storage manager + * @param allocations The mapping of allocation IDs to their states * @param allocationId The id of the allocation * @param signedRCA The signed Recurring Collection Agreement */ diff --git a/packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol b/packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol index 47f2846d2..0b702380e 100644 --- a/packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol +++ b/packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol @@ -9,6 +9,18 @@ library SubgraphServiceLib { using Allocation for mapping(address => Allocation.State); using Allocation for Allocation.State; + /** + * @notice Requires that the allocation is valid and owned by the indexer. + * + * Requirements: + * - Allocation must belong to the indexer + * - Allocation must be open + * + * @param self The allocation storage manager + * @param allocationId The id of the allocation + * @param indexer The address of the indexer + * @return The allocation state + */ function requireValidAllocation( mapping(address => Allocation.State) storage self, address allocationId, diff --git a/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol b/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol index b30af224f..4b8b29460 100644 --- a/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol +++ b/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol @@ -6,6 +6,9 @@ import { IndexingAgreement } from "./IndexingAgreement.sol"; library UnsafeDecoder { /** * @notice See {Decoder.decodeCollectIndexingFeeData} + * @param data The data to decode + * @return agreementId The agreement ID + * @return nestedData The nested encoded data */ function decodeCollectIndexingFeeData_(bytes calldata data) public pure returns (bytes16, bytes memory) { return abi.decode(data, (bytes16, bytes)); @@ -13,6 +16,10 @@ library UnsafeDecoder { /** * @notice See {Decoder.decodeRCAMetadata} + * @dev The data should be encoded as {IndexingAgreement.AcceptIndexingAgreementMetadata} + * @param data The data to decode + * @return The decoded data + * */ function decodeRCAMetadata_( bytes calldata data @@ -22,6 +29,9 @@ library UnsafeDecoder { /** * @notice See {Decoder.decodeRCAUMetadata} + * @dev The data should be encoded as {IndexingAgreement.UpdateIndexingAgreementMetadata} + * @param data The data to decode + * @return The decoded data */ function decodeRCAUMetadata_( bytes calldata data @@ -31,6 +41,11 @@ library UnsafeDecoder { /** * @notice See {Decoder.decodeCollectIndexingFeeDataV1} + * @dev The data should be encoded as (uint256 entities, bytes32 poi, uint256 epoch) + * @param data The data to decode + * @return entities The number of entities indexed + * @return poi The proof of indexing + * @return epoch The current epoch */ function decodeCollectIndexingFeeDataV1_( bytes memory data @@ -40,6 +55,9 @@ library UnsafeDecoder { /** * @notice See {Decoder.decodeIndexingAgreementTermsV1} + * @dev The data should be encoded as {IndexingAgreement.IndexingAgreementTermsV1} + * @param data The data to decode + * @return The decoded indexing agreement terms */ function decodeIndexingAgreementTermsV1_( bytes memory data From b3a461abd567e9eb4ee0744f6bbd97616c4438a4 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 15:36:41 -0300 Subject: [PATCH 64/90] f: more NatSpec --- packages/subgraph-service/contracts/utilities/Directory.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/subgraph-service/contracts/utilities/Directory.sol b/packages/subgraph-service/contracts/utilities/Directory.sol index cc2675a68..8b58d31b4 100644 --- a/packages/subgraph-service/contracts/utilities/Directory.sol +++ b/packages/subgraph-service/contracts/utilities/Directory.sol @@ -40,6 +40,7 @@ abstract contract Directory { * @param disputeManager The Dispute Manager contract address * @param graphTallyCollector The Graph Tally Collector contract address * @param curation The Curation contract address + * @param recurringCollector The Recurring Collector contract address */ event SubgraphServiceDirectoryInitialized( address subgraphService, @@ -99,6 +100,7 @@ abstract contract Directory { /** * @notice Returns the Recurring Collector contract address + * @return The Recurring Collector contract */ function recurringCollector() external view returns (IRecurringCollector) { return RECURRING_COLLECTOR; From 4c9ae6944c63b1b029acf02dae6018f024a6574d Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 15:41:55 -0300 Subject: [PATCH 65/90] f: more NatSpec --- .../contracts/libraries/IndexingAgreement.sol | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 2f637d4f2..c51b0eef4 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -525,6 +525,11 @@ library IndexingAgreement { return wrapper; } + /** + * @notice Get the storage manager for indexing agreements. + * @dev This function retrieves the storage manager for indexing agreements. + * @return $ The storage manager for indexing agreements + */ function _getStorageManager() internal pure returns (StorageManager storage $) { // solhint-disable-next-line no-inline-assembly assembly { @@ -532,6 +537,13 @@ library IndexingAgreement { } } + /** + * @notice Set the terms for an indexing agreement of version V1. + * @dev This function updates the terms of an indexing agreement in the storage manager. + * @param _manager The indexing agreement storage manager + * @param _agreementId The id of the agreement to update + * @param _data The encoded terms data + */ function _setTermsV1(StorageManager storage _manager, bytes16 _agreementId, bytes memory _data) private { IndexingAgreementTermsV1 memory newTerms = Decoder.decodeIndexingAgreementTermsV1(_data); _manager.termsV1[_agreementId].tokensPerSecond = newTerms.tokensPerSecond; @@ -572,6 +584,17 @@ library IndexingAgreement { _directory().recurringCollector().cancel(_agreementId, _cancelBy); } + /** + * @notice Calculate the number of tokens to collect for an indexing agreement. + * + * @dev This function calculates the number of tokens to collect based on the agreement terms and the collection time. + * + * @param _manager The indexing agreement storage manager + * @param _agreementId The id of the agreement + * @param _agreement The collector agreement data + * @param _entities The number of entities indexed + * @return The number of tokens to collect + */ function _tokensToCollect( StorageManager storage _manager, bytes16 _agreementId, @@ -600,18 +623,37 @@ library IndexingAgreement { wrapper.agreement.allocationId != address(0); } + /** + * @notice Gets the Directory + * @return The Directory contract + */ function _directory() private view returns (Directory) { return Directory(address(this)); } + /** + * @notice Gets the Graph Directory + * @return The Graph Directory contract + */ function _graphDirectory() private view returns (GraphDirectory) { return GraphDirectory(address(this)); } + /** + * @notice Gets the Subgraph Service + * @return The Subgraph Service contract + */ function _subgraphService() private view returns (SubgraphService) { return SubgraphService(address(this)); } + /** + * @notice Gets the indexing agreement wrapper for a given agreement ID. + * @dev This function retrieves the indexing agreement wrapper containing the agreement state and collector agreement data. + * @param self The indexing agreement storage manager + * @param agreementId The id of the indexing agreement + * @return The indexing agreement wrapper containing the agreement state and collector agreement data + */ function _get(StorageManager storage self, bytes16 agreementId) private view returns (AgreementWrapper memory) { return AgreementWrapper({ From 866c98229720e8339cf9c424e24d5ce94c61d086 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 15:45:40 -0300 Subject: [PATCH 66/90] f: more NatSpec --- .../contracts/libraries/IndexingAgreement.sol | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index c51b0eef4..0b9def8aa 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -68,20 +68,34 @@ library IndexingAgreement { uint256 tokensPerEntityPerSecond; } + /** + * @notice Parameters for collecting indexing fees + * @param agreementId The ID of the indexing agreement + * @param currentEpoch The current epoch + * @param data The encoded data containing the number of entities indexed, proof of indexing, and epoch + */ struct CollectParams { bytes16 agreementId; uint256 currentEpoch; bytes data; } - /// @custom:storage-location erc7201:graphprotocol.subgraph-service.storage.StorageManager.IndexingAgreement + /** + * @notice Storage manager for indexing agreements + * @dev This struct holds the state of indexing agreements and their terms. + * It is used to manage the lifecycle of indexing agreements in the subgraph service. + * @custom:storage-location erc7201:graphprotocol.subgraph-service.storage.StorageManager.IndexingAgreement + */ struct StorageManager { mapping(bytes16 => State) agreements; mapping(bytes16 agreementId => IndexingAgreementTermsV1 data) termsV1; mapping(address allocationId => bytes16 agreementId) allocationToActiveAgreementId; } - // keccak256(abi.encode(uint256(keccak256("graphprotocol.subgraph-service.storage.StorageManager.IndexingAgreement")) - 1)) & ~bytes32(uint256(0xff)) + /** + * @notice Storage location for the indexing agreement storage manager + * @dev Equals keccak256(abi.encode(uint256(keccak256("graphprotocol.subgraph-service.storage.StorageManager.IndexingAgreement")) - 1)) & ~bytes32(uint256(0xff)) + */ bytes32 public constant INDEXING_AGREEMENT_STORAGE_MANAGER_LOCATION = 0xb59b65b7215c7fb95ac34d2ad5aed7c775c8bc77ad936b1b43e17b95efc8e400; @@ -528,12 +542,12 @@ library IndexingAgreement { /** * @notice Get the storage manager for indexing agreements. * @dev This function retrieves the storage manager for indexing agreements. - * @return $ The storage manager for indexing agreements + * @return m The storage manager for indexing agreements */ - function _getStorageManager() internal pure returns (StorageManager storage $) { + function _getStorageManager() internal pure returns (StorageManager storage m) { // solhint-disable-next-line no-inline-assembly assembly { - $.slot := INDEXING_AGREEMENT_STORAGE_MANAGER_LOCATION + m.slot := INDEXING_AGREEMENT_STORAGE_MANAGER_LOCATION } } @@ -615,6 +629,8 @@ library IndexingAgreement { * - The underlying collector agreement has been accepted * - The underlying collector agreement's data service is this contract * - The indexing agreement has been accepted and has a valid allocation ID + * @param wrapper The agreement wrapper containing the indexing agreement and collector agreement data + * @return True if the agreement is active, false otherwise **/ function _isActive(AgreementWrapper memory wrapper) private view returns (bool) { return From da25b4b0ef0584409eaaabf349c266bbbf9946b7 Mon Sep 17 00:00:00 2001 From: Matias Date: Fri, 6 Jun 2025 15:47:25 -0300 Subject: [PATCH 67/90] f: more NatSpec --- .../contracts/libraries/IndexingAgreement.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 0b9def8aa..c4656f636 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -31,6 +31,12 @@ library IndexingAgreement { IndexingAgreementVersion version; } + /** + * @notice Wrapper for Indexing Agreement and Collector Agreement Data + * @dev This struct is used to encapsulate the state of an indexing agreement + * @param agreement The indexing agreement state + * @param collectorAgreement The collector agreement data + */ struct AgreementWrapper { State agreement; IRecurringCollector.AgreementData collectorAgreement; @@ -84,6 +90,9 @@ library IndexingAgreement { * @notice Storage manager for indexing agreements * @dev This struct holds the state of indexing agreements and their terms. * It is used to manage the lifecycle of indexing agreements in the subgraph service. + * @param agreements Mapping of agreement IDs to their states + * @param termsV1 Mapping of agreement IDs to their terms for version 1 agreements + * @param allocationToActiveAgreementId Mapping of allocation IDs to their active agreement IDs * @custom:storage-location erc7201:graphprotocol.subgraph-service.storage.StorageManager.IndexingAgreement */ struct StorageManager { From 907010c992b44cbc1f11223bb6b4ade60e8c9807 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 10:00:10 -0300 Subject: [PATCH 68/90] f: lint horizon changes --- .../payments/collectors/GraphTallyCollector.sol | 5 ++++- .../test/unit/disputeManager/DisputeManager.t.sol | 8 ++++++-- .../unit/disputeManager/disputes/indexing/create.t.sol | 9 +++++---- .../test/unit/disputeManager/disputes/query/create.t.sol | 5 ++++- .../unit/subgraphService/collect/indexing/indexing.t.sol | 4 +++- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol index bab1be09e..6eda16b5f 100644 --- a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol +++ b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol @@ -102,7 +102,10 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall bytes calldata _data, uint256 _tokensToCollect ) private returns (uint256) { - require(_paymentType == IGraphPayments.PaymentTypes.QueryFee, GraphTallyCollectorInvalidPaymentType(_paymentType)); + require( + _paymentType == IGraphPayments.PaymentTypes.QueryFee, + GraphTallyCollectorInvalidPaymentType(_paymentType) + ); (SignedRAV memory signedRAV, uint256 dataServiceCut, address receiverDestination) = abi.decode( _data, diff --git a/packages/subgraph-service/test/unit/disputeManager/DisputeManager.t.sol b/packages/subgraph-service/test/unit/disputeManager/DisputeManager.t.sol index 720460bc4..6df3474b2 100644 --- a/packages/subgraph-service/test/unit/disputeManager/DisputeManager.t.sol +++ b/packages/subgraph-service/test/unit/disputeManager/DisputeManager.t.sol @@ -69,7 +69,11 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { assertEq(address(disputeManager.subgraphService()), _subgraphService, "Subgraph service should be set."); } - function _createIndexingDispute(address _allocationId, bytes32 _poi, uint256 _blockNumber) internal returns (bytes32) { + function _createIndexingDispute( + address _allocationId, + bytes32 _poi, + uint256 _blockNumber + ) internal returns (bytes32) { (, address fisherman, ) = vm.readCallers(); bytes32 expectedDisputeId = keccak256(abi.encodePacked(_allocationId, _poi, _blockNumber)); uint256 disputeDeposit = disputeManager.disputeDeposit(); @@ -88,7 +92,7 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { fisherman, disputeDeposit, _allocationId, - _poi, + _poi, _blockNumber, stakeSnapshot, cancellableAt diff --git a/packages/subgraph-service/test/unit/disputeManager/disputes/indexing/create.t.sol b/packages/subgraph-service/test/unit/disputeManager/disputes/indexing/create.t.sol index 62b368835..6ebafebed 100644 --- a/packages/subgraph-service/test/unit/disputeManager/disputes/indexing/create.t.sol +++ b/packages/subgraph-service/test/unit/disputeManager/disputes/indexing/create.t.sol @@ -105,9 +105,7 @@ contract DisputeManagerIndexingCreateDisputeTest is DisputeManagerTest { vm.stopPrank(); } - function test_Indexing_Create_DisputesSamePOIAndAllo( - uint256 tokens - ) public useIndexer useAllocation(tokens) { + function test_Indexing_Create_DisputesSamePOIAndAllo(uint256 tokens) public useIndexer useAllocation(tokens) { resetPrank(users.fisherman); bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1"), block.number); @@ -158,7 +156,10 @@ contract DisputeManagerIndexingCreateDisputeTest is DisputeManagerTest { disputeManager.createIndexingDispute(allocationID, bytes32("POI1"), block.number); } - function test_Indexing_Create_DontRevertIf_IndexerIsBelowStake_WithDelegation(uint256 tokens, uint256 delegationTokens) public useIndexer useAllocation(tokens) { + function test_Indexing_Create_DontRevertIf_IndexerIsBelowStake_WithDelegation( + uint256 tokens, + uint256 delegationTokens + ) public useIndexer useAllocation(tokens) { // Close allocation bytes memory data = abi.encode(allocationID); _stopService(users.indexer, data); diff --git a/packages/subgraph-service/test/unit/disputeManager/disputes/query/create.t.sol b/packages/subgraph-service/test/unit/disputeManager/disputes/query/create.t.sol index 94f2fe615..c2b8f1ab3 100644 --- a/packages/subgraph-service/test/unit/disputeManager/disputes/query/create.t.sol +++ b/packages/subgraph-service/test/unit/disputeManager/disputes/query/create.t.sol @@ -156,7 +156,10 @@ contract DisputeManagerQueryCreateDisputeTest is DisputeManagerTest { disputeManager.createQueryDispute(attestationData); } - function test_Query_Create_DontRevertIf_IndexerIsBelowStake_WithDelegation(uint256 tokens, uint256 delegationTokens) public useIndexer useAllocation(tokens) { + function test_Query_Create_DontRevertIf_IndexerIsBelowStake_WithDelegation( + uint256 tokens, + uint256 delegationTokens + ) public useIndexer useAllocation(tokens) { // Close allocation bytes memory data = abi.encode(allocationID); _stopService(users.indexer, data); diff --git a/packages/subgraph-service/test/unit/subgraphService/collect/indexing/indexing.t.sol b/packages/subgraph-service/test/unit/subgraphService/collect/indexing/indexing.t.sol index c97416157..a1beb5f30 100644 --- a/packages/subgraph-service/test/unit/subgraphService/collect/indexing/indexing.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/collect/indexing/indexing.t.sol @@ -172,7 +172,9 @@ contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { subgraphService.collect(newIndexer, paymentType, data); } - function test_SubgraphService_Collect_Indexing_RevertWhen_IncorrectPaymentType(uint256 tokens) public useIndexer useAllocation(tokens) { + function test_SubgraphService_Collect_Indexing_RevertWhen_IncorrectPaymentType( + uint256 tokens + ) public useIndexer useAllocation(tokens) { bytes memory data = abi.encode(allocationID, bytes32("POI"), _getHardcodedPOIMetadata()); // skip time to ensure allocation gets rewards From 11a49fe02bc77408e9c54a96fd11ec87a5595fdf Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 10:15:21 -0300 Subject: [PATCH 69/90] f: port fix from tmigone/horizon-fixes-june --- packages/subgraph-service/contracts/DisputeManager.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 18db10523..70275ec47 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -563,13 +563,9 @@ contract DisputeManager is require(!isDisputeCreated(disputeId), DisputeManagerDisputeAlreadyCreated(disputeId)); // The indexer must be disputable - IHorizonStaking.Provision memory provision = _graphStaking().getProvision( - wrapper.collectorAgreement.serviceProvider, - address(_getSubgraphService()) - ); - require(provision.tokens != 0, DisputeManagerZeroTokens()); + uint256 stakeSnapshot = _getStakeSnapshot(wrapper.collectorAgreement.serviceProvider); + require(stakeSnapshot != 0, DisputeManagerZeroTokens()); - uint256 stakeSnapshot = _getStakeSnapshot(wrapper.collectorAgreement.serviceProvider, provision.tokens); disputes[disputeId] = Dispute( wrapper.collectorAgreement.serviceProvider, _fisherman, From bfa82ca59861bc3f29c5d44f06b551a4ddd51045 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 10:42:45 -0300 Subject: [PATCH 70/90] f: rename to IndexingAgreementDecoder/Raw --- .../contracts/SubgraphService.sol | 4 +-- .../contracts/libraries/IndexingAgreement.sol | 18 +++++++---- ...coder.sol => IndexingAgreementDecoder.sol} | 32 +++++++++++-------- ...er.sol => IndexingAgreementDecoderRaw.sol} | 23 +++++++------ .../indexing-agreement/accept.t.sol | 4 +-- .../indexing-agreement/update.t.sol | 4 +-- 6 files changed, 47 insertions(+), 38 deletions(-) rename packages/subgraph-service/contracts/libraries/{Decoder.sol => IndexingAgreementDecoder.sol} (67%) rename packages/subgraph-service/contracts/libraries/{UnsafeDecoder.sol => IndexingAgreementDecoderRaw.sol} (75%) diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 642a13718..186079bb0 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -24,7 +24,7 @@ import { Allocation } from "./libraries/Allocation.sol"; import { LegacyAllocation } from "./libraries/LegacyAllocation.sol"; import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; -import { Decoder } from "./libraries/Decoder.sol"; +import { IndexingAgreementDecoder } from "./libraries/IndexingAgreementDecoder.sol"; import { IndexingAgreement } from "./libraries/IndexingAgreement.sol"; /** @@ -298,7 +298,7 @@ contract SubgraphService is } else if (paymentType == IGraphPayments.PaymentTypes.IndexingRewards) { paymentCollected = _collectIndexingRewards(indexer, data); } else if (paymentType == IGraphPayments.PaymentTypes.IndexingFee) { - (bytes16 agreementId, bytes memory iaCollectionData) = Decoder.decodeCollectIndexingFeeData(data); + (bytes16 agreementId, bytes memory iaCollectionData) = IndexingAgreementDecoder.decodeCollectData(data); paymentCollected = _collectIndexingFees(agreementId, iaCollectionData); } else { revert SubgraphServiceInvalidPaymentType(paymentType); diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index c4656f636..343a4d1ab 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -9,7 +9,7 @@ import { SubgraphService } from "../SubgraphService.sol"; import { Directory } from "../utilities/Directory.sol"; import { Allocation } from "./Allocation.sol"; import { SubgraphServiceLib } from "./SubgraphServiceLib.sol"; -import { Decoder } from "./Decoder.sol"; +import { IndexingAgreementDecoder } from "./IndexingAgreementDecoder.sol"; library IndexingAgreement { using IndexingAgreement for StorageManager; @@ -278,7 +278,9 @@ library IndexingAgreement { IndexingAgreementWrongDataService(address(this), signedRCA.rca.dataService) ); - AcceptIndexingAgreementMetadata memory metadata = Decoder.decodeRCAMetadata(signedRCA.rca.metadata); + AcceptIndexingAgreementMetadata memory metadata = IndexingAgreementDecoder.decodeRCAMetadata( + signedRCA.rca.metadata + ); State storage agreement = self.agreements[signedRCA.rca.agreementId]; @@ -346,7 +348,9 @@ library IndexingAgreement { IndexingAgreementNotAuthorized(signedRCAU.rcau.agreementId, indexer) ); - UpdateIndexingAgreementMetadata memory metadata = Decoder.decodeRCAUMetadata(signedRCAU.rcau.metadata); + UpdateIndexingAgreementMetadata memory metadata = IndexingAgreementDecoder.decodeRCAUMetadata( + signedRCAU.rcau.metadata + ); wrapper.agreement.version = metadata.version; @@ -473,7 +477,7 @@ library IndexingAgreement { * - Allocation must be open * - Agreement must be active * - Agreement must be of version V1 - * - The data must be encoded as per {Decoder.decodeCollectIndexingFeeDataV1} + * - The data must be encoded as per {IndexingAgreementDecoder.decodeCollectIndexingFeeDataV1} * * Emits a {IndexingFeesCollectedV1} event. * @@ -500,7 +504,9 @@ library IndexingAgreement { InvalidIndexingAgreementVersion(wrapper.agreement.version) ); - (uint256 entities, bytes32 poi, uint256 poiEpoch) = Decoder.decodeCollectIndexingFeeDataV1(params.data); + (uint256 entities, bytes32 poi, uint256 poiEpoch) = IndexingAgreementDecoder.decodeCollectIndexingFeeDataV1( + params.data + ); uint256 expectedTokens = (entities == 0 && poi == bytes32(0)) ? 0 @@ -568,7 +574,7 @@ library IndexingAgreement { * @param _data The encoded terms data */ function _setTermsV1(StorageManager storage _manager, bytes16 _agreementId, bytes memory _data) private { - IndexingAgreementTermsV1 memory newTerms = Decoder.decodeIndexingAgreementTermsV1(_data); + IndexingAgreementTermsV1 memory newTerms = IndexingAgreementDecoder.decodeIndexingAgreementTermsV1(_data); _manager.termsV1[_agreementId].tokensPerSecond = newTerms.tokensPerSecond; _manager.termsV1[_agreementId].tokensPerEntityPerSecond = newTerms.tokensPerEntityPerSecond; } diff --git a/packages/subgraph-service/contracts/libraries/Decoder.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol similarity index 67% rename from packages/subgraph-service/contracts/libraries/Decoder.sol rename to packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol index edb5cfb6e..c8b2ed370 100644 --- a/packages/subgraph-service/contracts/libraries/Decoder.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; -import { UnsafeDecoder } from "./UnsafeDecoder.sol"; +import { IndexingAgreementDecoderRaw } from "./IndexingAgreementDecoderRaw.sol"; import { IndexingAgreement } from "./IndexingAgreement.sol"; -library Decoder { +library IndexingAgreementDecoder { /** * @notice Thrown when the data can't be decoded as expected * @param t The type of data that was expected * @param data The invalid data */ - error DecoderInvalidData(string t, bytes data); + error IndexingAgreementDecoderInvalidData(string t, bytes data); /** * @notice Decodes the data for collecting indexing fees. @@ -19,11 +19,11 @@ library Decoder { * @return agreementId The agreement ID * @return nestedData The nested encoded data */ - function decodeCollectIndexingFeeData(bytes memory data) public pure returns (bytes16, bytes memory) { - try UnsafeDecoder.decodeCollectIndexingFeeData_(data) returns (bytes16 agreementId, bytes memory nestedData) { + function decodeCollectData(bytes memory data) public pure returns (bytes16, bytes memory) { + try IndexingAgreementDecoderRaw.decodeCollectData(data) returns (bytes16 agreementId, bytes memory nestedData) { return (agreementId, nestedData); } catch { - revert DecoderInvalidData("decodeCollectIndexingFeeData", data); + revert IndexingAgreementDecoderInvalidData("decodeCollectData", data); } } @@ -36,12 +36,12 @@ library Decoder { function decodeRCAMetadata( bytes memory data ) public pure returns (IndexingAgreement.AcceptIndexingAgreementMetadata memory) { - try UnsafeDecoder.decodeRCAMetadata_(data) returns ( + try IndexingAgreementDecoderRaw.decodeRCAMetadata(data) returns ( IndexingAgreement.AcceptIndexingAgreementMetadata memory metadata ) { return metadata; } catch { - revert DecoderInvalidData("decodeRCAMetadata", data); + revert IndexingAgreementDecoderInvalidData("decodeRCAMetadata", data); } } @@ -54,12 +54,12 @@ library Decoder { function decodeRCAUMetadata( bytes memory data ) public pure returns (IndexingAgreement.UpdateIndexingAgreementMetadata memory) { - try UnsafeDecoder.decodeRCAUMetadata_(data) returns ( + try IndexingAgreementDecoderRaw.decodeRCAUMetadata(data) returns ( IndexingAgreement.UpdateIndexingAgreementMetadata memory metadata ) { return metadata; } catch { - revert DecoderInvalidData("decodeRCAUMetadata", data); + revert IndexingAgreementDecoderInvalidData("decodeRCAUMetadata", data); } } @@ -72,10 +72,14 @@ library Decoder { * @return epoch The epoch of the POI */ function decodeCollectIndexingFeeDataV1(bytes memory data) public pure returns (uint256, bytes32, uint256) { - try UnsafeDecoder.decodeCollectIndexingFeeDataV1_(data) returns (uint256 entities, bytes32 poi, uint256 epoch) { + try IndexingAgreementDecoderRaw.decodeCollectIndexingFeeDataV1(data) returns ( + uint256 entities, + bytes32 poi, + uint256 epoch + ) { return (entities, poi, epoch); } catch { - revert DecoderInvalidData("decodeCollectIndexingFeeDataV1", data); + revert IndexingAgreementDecoderInvalidData("decodeCollectIndexingFeeDataV1", data); } } @@ -88,12 +92,12 @@ library Decoder { function decodeIndexingAgreementTermsV1( bytes memory data ) public pure returns (IndexingAgreement.IndexingAgreementTermsV1 memory) { - try UnsafeDecoder.decodeIndexingAgreementTermsV1_(data) returns ( + try IndexingAgreementDecoderRaw.decodeIndexingAgreementTermsV1(data) returns ( IndexingAgreement.IndexingAgreementTermsV1 memory terms ) { return terms; } catch { - revert DecoderInvalidData("decodeCollectIndexingFeeData", data); + revert IndexingAgreementDecoderInvalidData("decodeCollectIndexingFeeData", data); } } } diff --git a/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoderRaw.sol similarity index 75% rename from packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol rename to packages/subgraph-service/contracts/libraries/IndexingAgreementDecoderRaw.sol index 4b8b29460..23f791f68 100644 --- a/packages/subgraph-service/contracts/libraries/UnsafeDecoder.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoderRaw.sol @@ -3,63 +3,62 @@ pragma solidity 0.8.27; import { IndexingAgreement } from "./IndexingAgreement.sol"; -library UnsafeDecoder { +library IndexingAgreementDecoderRaw { /** - * @notice See {Decoder.decodeCollectIndexingFeeData} + * @notice See {IndexingAgreementDecoder.decodeCollectIndexingFeeData} * @param data The data to decode * @return agreementId The agreement ID * @return nestedData The nested encoded data */ - function decodeCollectIndexingFeeData_(bytes calldata data) public pure returns (bytes16, bytes memory) { + function decodeCollectData(bytes calldata data) public pure returns (bytes16, bytes memory) { return abi.decode(data, (bytes16, bytes)); } /** - * @notice See {Decoder.decodeRCAMetadata} + * @notice See {IndexingAgreementDecoder.decodeRCAMetadata} * @dev The data should be encoded as {IndexingAgreement.AcceptIndexingAgreementMetadata} * @param data The data to decode * @return The decoded data - * */ - function decodeRCAMetadata_( + function decodeRCAMetadata( bytes calldata data ) public pure returns (IndexingAgreement.AcceptIndexingAgreementMetadata memory) { return abi.decode(data, (IndexingAgreement.AcceptIndexingAgreementMetadata)); } /** - * @notice See {Decoder.decodeRCAUMetadata} + * @notice See {IndexingAgreementDecoder.decodeRCAUMetadata} * @dev The data should be encoded as {IndexingAgreement.UpdateIndexingAgreementMetadata} * @param data The data to decode * @return The decoded data */ - function decodeRCAUMetadata_( + function decodeRCAUMetadata( bytes calldata data ) public pure returns (IndexingAgreement.UpdateIndexingAgreementMetadata memory) { return abi.decode(data, (IndexingAgreement.UpdateIndexingAgreementMetadata)); } /** - * @notice See {Decoder.decodeCollectIndexingFeeDataV1} + * @notice See {IndexingAgreementDecoder.decodeCollectIndexingFeeDataV1} * @dev The data should be encoded as (uint256 entities, bytes32 poi, uint256 epoch) * @param data The data to decode * @return entities The number of entities indexed * @return poi The proof of indexing * @return epoch The current epoch */ - function decodeCollectIndexingFeeDataV1_( + function decodeCollectIndexingFeeDataV1( bytes memory data ) public pure returns (uint256 entities, bytes32 poi, uint256 epoch) { return abi.decode(data, (uint256, bytes32, uint256)); } /** - * @notice See {Decoder.decodeIndexingAgreementTermsV1} + * @notice See {IndexingAgreementDecoder.decodeIndexingAgreementTermsV1} * @dev The data should be encoded as {IndexingAgreement.IndexingAgreementTermsV1} * @param data The data to decode * @return The decoded indexing agreement terms */ - function decodeIndexingAgreementTermsV1_( + function decodeIndexingAgreementTermsV1( bytes memory data ) public pure returns (IndexingAgreement.IndexingAgreementTermsV1 memory) { return abi.decode(data, (IndexingAgreement.IndexingAgreementTermsV1)); 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 a8f410da7..08be7c86d 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 @@ -7,7 +7,7 @@ import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces import { Allocation } from "../../../../contracts/libraries/Allocation.sol"; import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; -import { Decoder } from "../../../../contracts/libraries/Decoder.sol"; +import { IndexingAgreementDecoder } from "../../../../contracts/libraries/IndexingAgreementDecoder.sol"; import { AllocationManager } from "../../../../contracts/utilities/AllocationManager.sol"; import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; @@ -126,7 +126,7 @@ contract SubgraphServiceIndexingAgreementAcceptTest is SubgraphServiceIndexingAg ); bytes memory expectedErr = abi.encodeWithSelector( - Decoder.DecoderInvalidData.selector, + IndexingAgreementDecoder.IndexingAgreementDecoderInvalidData.selector, "decodeRCAMetadata", unacceptable.rca.metadata ); diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol index f3589a4d3..336ef97de 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/update.t.sol @@ -7,7 +7,7 @@ import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/ import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; -import { Decoder } from "../../../../contracts/libraries/Decoder.sol"; +import { IndexingAgreementDecoder } from "../../../../contracts/libraries/IndexingAgreementDecoder.sol"; import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; @@ -133,7 +133,7 @@ contract SubgraphServiceIndexingAgreementUpgradeTest is SubgraphServiceIndexingA ); bytes memory expectedErr = abi.encodeWithSelector( - Decoder.DecoderInvalidData.selector, + IndexingAgreementDecoder.IndexingAgreementDecoderInvalidData.selector, "decodeRCAUMetadata", unacceptableUpdate.rcau.metadata ); From 6f371dce06724e6a7d00e32ed4c5ac22152a8411 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 10:50:23 -0300 Subject: [PATCH 71/90] f: simplify IndexingFeeDisputeWithAgreement --- packages/subgraph-service/contracts/DisputeManager.sol | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 70275ec47..e509f1410 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -548,15 +548,7 @@ contract DisputeManager is // Create a disputeId bytes32 disputeId = keccak256( - abi.encodePacked( - "IndexingFeeDisputeWithAgreement", - _agreementId, - wrapper.collectorAgreement.serviceProvider, - wrapper.collectorAgreement.payer, - _poi, - _entities, - _blockNumber - ) + abi.encodePacked("IndexingFeeDisputeWithAgreement", _agreementId, _poi, _entities, _blockNumber) ); // Only one dispute at a time From 4a7774e55b8312dce54f7af11954549189841346 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 10:52:09 -0300 Subject: [PATCH 72/90] f: rename InvalidIndexingAgreementVersion --- .../contracts/libraries/IndexingAgreement.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 343a4d1ab..eede14929 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -190,7 +190,7 @@ library IndexingAgreement { * @notice Thrown when trying to interact with an agreement with an invalid version * @param version The invalid version */ - error InvalidIndexingAgreementVersion(IndexingAgreementVersion version); + error IndexingAgreementInvalidVersion(IndexingAgreementVersion version); /** * @notice Thrown when an agreement is not for the subgraph data service @@ -305,7 +305,7 @@ library IndexingAgreement { agreement.version = metadata.version; agreement.allocationId = allocationId; - require(metadata.version == IndexingAgreementVersion.V1, InvalidIndexingAgreementVersion(metadata.version)); + require(metadata.version == IndexingAgreementVersion.V1, IndexingAgreementInvalidVersion(metadata.version)); _setTermsV1(self, signedRCA.rca.agreementId, metadata.terms); emit IndexingAgreementAccepted( @@ -354,7 +354,7 @@ library IndexingAgreement { wrapper.agreement.version = metadata.version; - require(metadata.version == IndexingAgreementVersion.V1, InvalidIndexingAgreementVersion(metadata.version)); + require(metadata.version == IndexingAgreementVersion.V1, IndexingAgreementInvalidVersion(metadata.version)); _setTermsV1(self, signedRCAU.rcau.agreementId, metadata.terms); emit IndexingAgreementUpdated({ @@ -501,7 +501,7 @@ library IndexingAgreement { require( wrapper.agreement.version == IndexingAgreementVersion.V1, - InvalidIndexingAgreementVersion(wrapper.agreement.version) + IndexingAgreementInvalidVersion(wrapper.agreement.version) ); (uint256 entities, bytes32 poi, uint256 poiEpoch) = IndexingAgreementDecoder.decodeCollectIndexingFeeDataV1( From 3e5e237d14e5c95c8a3c14fb8409cd2748bdfe58 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 10:54:08 -0300 Subject: [PATCH 73/90] f: comment on delete --- .../subgraph-service/contracts/libraries/IndexingAgreement.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index eede14929..ced7fea58 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -599,6 +599,8 @@ library IndexingAgreement { IRecurringCollector.AgreementData memory _collectorAgreement, IRecurringCollector.CancelAgreementBy _cancelBy ) private { + // Delete the allocation to active agreement link, so that the allocation + // can be assigned a new indexing agreement in the future. delete _manager.allocationToActiveAgreementId[_agreement.allocationId]; emit IndexingAgreementCanceled( From f46a76aca176062db8df072c4b453415186cceb8 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 11:35:32 -0300 Subject: [PATCH 74/90] f: allow collection (once) for elapsed agreements --- .../interfaces/IRecurringCollector.sol | 12 ++++++--- .../collectors/RecurringCollector.sol | 19 ++++++++------ .../recurring-collector/collect.t.sol | 25 ------------------- 3 files changed, 20 insertions(+), 36 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index 258904ce9..30293de9c 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -344,11 +344,17 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { error RecurringCollectorAgreementInvalidDuration(uint32 requiredMinDuration, uint256 invalidDuration); /** - * @notice Thrown when calling collect() on an elapsed agreement + * @notice Thrown when calling collect() with a zero collection seconds * @param agreementId The agreement ID - * @param endsAt The agreement end timestamp + * @param currentTimestamp The current timestamp + * @param lastCollectionAt The timestamp when the last collection was done + * */ - error RecurringCollectorAgreementElapsed(bytes16 agreementId, uint64 endsAt); + error RecurringCollectorZeroCollectionSeconds( + bytes16 agreementId, + uint256 currentTimestamp, + uint64 lastCollectionAt + ); /** * @notice Thrown when calling collect() too soon diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index 079a8c6f7..3809fdba5 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { Authorizable } from "../../utilities/Authorizable.sol"; import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; @@ -11,7 +12,6 @@ import { IPaymentsCollector } from "../../interfaces/IPaymentsCollector.sol"; // import { IRecurringCollector } from "../../interfaces/IRecurringCollector.sol"; import { IGraphPayments } from "../../interfaces/IGraphPayments.sol"; import { PPMMath } from "../../libraries/PPMMath.sol"; -import { MathUtils } from "../../libraries/MathUtils.sol"; /** * @title RecurringCollector contract @@ -279,11 +279,6 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC RecurringCollectorDataServiceNotAuthorized(_params.agreementId, msg.sender) ); - require( - agreement.endsAt >= block.timestamp, - RecurringCollectorAgreementElapsed(_params.agreementId, agreement.endsAt) - ); - uint256 tokensToCollect = 0; if (_params.tokens != 0) { tokensToCollect = _requireValidCollect(agreement, _params.agreementId, _params.tokens); @@ -371,11 +366,19 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC uint256 _tokens ) private view returns (uint256) { // if canceled by the payer allow collection up to the cancelation time - uint256 collectionEnd = _agreement.state == AgreementState.CanceledByPayer + uint256 canceledOrNow = _agreement.state == AgreementState.CanceledByPayer ? _agreement.canceledAt : block.timestamp; + + // allow collection till endsAt (at most) + uint256 collectionEnd = Math.min(canceledOrNow, _agreement.endsAt); uint256 collectionStart = _agreementCollectionStartAt(_agreement); + require( + collectionEnd != collectionStart, + RecurringCollectorZeroCollectionSeconds(_agreementId, block.timestamp, uint64(collectionStart)) + ); require(collectionEnd > collectionStart, RecurringCollectorFinalCollectionDone(_agreementId, collectionStart)); + uint256 collectionSeconds = collectionEnd - collectionStart; require( collectionSeconds >= _agreement.minSecondsPerCollection, @@ -397,7 +400,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC uint256 maxTokens = _agreement.maxOngoingTokensPerSecond * collectionSeconds; maxTokens += _agreement.lastCollectionAt == 0 ? _agreement.maxInitialTokens : 0; - return MathUtils.min(_tokens, maxTokens); + return Math.min(_tokens, maxTokens); } /** diff --git a/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol b/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol index 65efe85a7..8942c21bf 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/collect.t.sol @@ -100,30 +100,6 @@ contract RecurringCollectorCollectTest is RecurringCollectorSharedTest { _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); } - function test_Collect_Revert_WhenAgreementElapsed( - FuzzyTestCollect calldata fuzzy, - uint256 unboundedCollectionSeconds - ) public { - (IRecurringCollector.SignedRCA memory accepted, ) = _sensibleAuthorizeAndAccept(fuzzy.fuzzyTestAccept); - - // skip to sometime after agreement elapsed - skip(boundSkipFloor(unboundedCollectionSeconds, accepted.rca.endsAt - block.timestamp + 1)); - - IRecurringCollector.CollectParams memory collectParams = fuzzy.collectParams; - collectParams.agreementId = accepted.rca.agreementId; - - bytes memory data = _generateCollectData(collectParams); - - bytes memory expectedErr = abi.encodeWithSelector( - IRecurringCollector.RecurringCollectorAgreementElapsed.selector, - collectParams.agreementId, - accepted.rca.endsAt - ); - vm.expectRevert(expectedErr); - vm.prank(accepted.rca.dataService); - _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); - } - function test_Collect_Revert_WhenCollectingTooSoon( FuzzyTestCollect calldata fuzzy, uint256 unboundedCollectionSeconds @@ -287,6 +263,5 @@ contract RecurringCollectorCollectTest is RecurringCollectorSharedTest { uint256 collected = _recurringCollector.collect(IGraphPayments.PaymentTypes.IndexingFee, data); assertEq(collected, tokens); } - /* solhint-enable graph/func-name-mixedcase */ } From b028a7e837f55005c310f375c07399260ccf5694 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 13:59:53 -0300 Subject: [PATCH 75/90] f: add poiBlock and metadata --- .../contracts/libraries/IndexingAgreement.sol | 36 +++++++++++++----- .../libraries/IndexingAgreementDecoder.sol | 38 +++++++++---------- .../libraries/IndexingAgreementDecoderRaw.sol | 9 ++--- .../indexing-agreement/collect.t.sol | 33 ++++++++-------- .../indexing-agreement/integration.t.sol | 8 +++- .../indexing-agreement/shared.t.sol | 16 +++++++- 6 files changed, 86 insertions(+), 54 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index ced7fea58..65a2b0c9f 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -86,6 +86,21 @@ library IndexingAgreement { bytes data; } + /** + * @notice Nested data for collecting indexing fees V1. + * + * @param entities The number of entities + * @param poi The proof of indexing (POI) + * @param poiBlockNumber The block number of the POI + * @param metadata Additional metadata associated with the collection + */ + struct CollectIndexingFeeDataV1 { + uint256 entities; + bytes32 poi; + uint256 poiBlockNumber; + bytes metadata; + } + /** * @notice Storage manager for indexing agreements * @dev This struct holds the state of indexing agreements and their terms. @@ -119,7 +134,8 @@ library IndexingAgreement { * @param tokensCollected The amount of tokens collected * @param entities The number of entities indexed * @param poi The proof of indexing - * @param poiEpoch The epoch of the proof of indexing + * @param poiBlockNumber The block number of the proof of indexing + * @param metadata Additional metadata associated with the collection */ event IndexingFeesCollectedV1( address indexed indexer, @@ -131,7 +147,8 @@ library IndexingAgreement { uint256 tokensCollected, uint256 entities, bytes32 poi, - uint256 poiEpoch + uint256 poiBlockNumber, + bytes metadata ); /** @@ -504,13 +521,11 @@ library IndexingAgreement { IndexingAgreementInvalidVersion(wrapper.agreement.version) ); - (uint256 entities, bytes32 poi, uint256 poiEpoch) = IndexingAgreementDecoder.decodeCollectIndexingFeeDataV1( - params.data - ); + CollectIndexingFeeDataV1 memory data = IndexingAgreementDecoder.decodeCollectIndexingFeeDataV1(params.data); - uint256 expectedTokens = (entities == 0 && poi == bytes32(0)) + uint256 expectedTokens = (data.entities == 0 && data.poi == bytes32(0)) ? 0 - : _tokensToCollect(self, params.agreementId, wrapper.collectorAgreement, entities); + : _tokensToCollect(self, params.agreementId, wrapper.collectorAgreement, data.entities); uint256 tokensCollected = _directory().recurringCollector().collect( IGraphPayments.PaymentTypes.IndexingFee, @@ -532,9 +547,10 @@ library IndexingAgreement { allocation.subgraphDeploymentId, params.currentEpoch, tokensCollected, - entities, - poi, - poiEpoch + data.entities, + data.poi, + data.poiBlockNumber, + data.metadata ); return (wrapper.collectorAgreement.serviceProvider, tokensCollected); diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol index c8b2ed370..f8f5af811 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoder.sol @@ -30,16 +30,16 @@ library IndexingAgreementDecoder { /** * @notice Decodes the RCA metadata. * - * @param data The data to decode. See {IndexingAgreement.AcceptIndexingAgreementMetadata} - * @return The decoded data + * @param data The data to decode. + * @return The decoded data. See {IndexingAgreement.AcceptIndexingAgreementMetadata} */ function decodeRCAMetadata( bytes memory data ) public pure returns (IndexingAgreement.AcceptIndexingAgreementMetadata memory) { try IndexingAgreementDecoderRaw.decodeRCAMetadata(data) returns ( - IndexingAgreement.AcceptIndexingAgreementMetadata memory metadata + IndexingAgreement.AcceptIndexingAgreementMetadata memory decoded ) { - return metadata; + return decoded; } catch { revert IndexingAgreementDecoderInvalidData("decodeRCAMetadata", data); } @@ -48,16 +48,16 @@ library IndexingAgreementDecoder { /** * @notice Decodes the RCAU metadata. * - * @param data The data to decode. See {IndexingAgreement.UpdateIndexingAgreementMetadata} - * @return The decoded data + * @param data The data to decode. + * @return The decoded data. See {IndexingAgreement.UpdateIndexingAgreementMetadata} */ function decodeRCAUMetadata( bytes memory data ) public pure returns (IndexingAgreement.UpdateIndexingAgreementMetadata memory) { try IndexingAgreementDecoderRaw.decodeRCAUMetadata(data) returns ( - IndexingAgreement.UpdateIndexingAgreementMetadata memory metadata + IndexingAgreement.UpdateIndexingAgreementMetadata memory decoded ) { - return metadata; + return decoded; } catch { revert IndexingAgreementDecoderInvalidData("decodeRCAUMetadata", data); } @@ -67,17 +67,15 @@ library IndexingAgreementDecoder { * @notice Decodes the collect data for indexing fees V1. * * @param data The data to decode. - * @return entities The number of entities - * @return poi The proof of indexing (POI) - * @return epoch The epoch of the POI + * @return The decoded data structure. See {IndexingAgreement.CollectIndexingFeeDataV1} */ - function decodeCollectIndexingFeeDataV1(bytes memory data) public pure returns (uint256, bytes32, uint256) { + function decodeCollectIndexingFeeDataV1( + bytes memory data + ) public pure returns (IndexingAgreement.CollectIndexingFeeDataV1 memory) { try IndexingAgreementDecoderRaw.decodeCollectIndexingFeeDataV1(data) returns ( - uint256 entities, - bytes32 poi, - uint256 epoch + IndexingAgreement.CollectIndexingFeeDataV1 memory decoded ) { - return (entities, poi, epoch); + return decoded; } catch { revert IndexingAgreementDecoderInvalidData("decodeCollectIndexingFeeDataV1", data); } @@ -86,16 +84,16 @@ library IndexingAgreementDecoder { /** * @notice Decodes the data for indexing agreement terms V1. * - * @param data The data to decode. See {IndexingAgreement.IndexingAgreementTermsV1} - * @return The decoded data + * @param data The data to decode. + * @return The decoded data structure. See {IndexingAgreement.IndexingAgreementTermsV1} */ function decodeIndexingAgreementTermsV1( bytes memory data ) public pure returns (IndexingAgreement.IndexingAgreementTermsV1 memory) { try IndexingAgreementDecoderRaw.decodeIndexingAgreementTermsV1(data) returns ( - IndexingAgreement.IndexingAgreementTermsV1 memory terms + IndexingAgreement.IndexingAgreementTermsV1 memory decoded ) { - return terms; + return decoded; } catch { revert IndexingAgreementDecoderInvalidData("decodeCollectIndexingFeeData", data); } diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoderRaw.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoderRaw.sol index 23f791f68..93b1718bf 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoderRaw.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreementDecoderRaw.sol @@ -42,14 +42,13 @@ library IndexingAgreementDecoderRaw { * @notice See {IndexingAgreementDecoder.decodeCollectIndexingFeeDataV1} * @dev The data should be encoded as (uint256 entities, bytes32 poi, uint256 epoch) * @param data The data to decode - * @return entities The number of entities indexed - * @return poi The proof of indexing - * @return epoch The current epoch + * @return The decoded collect indexing fee V1 data + * */ function decodeCollectIndexingFeeDataV1( bytes memory data - ) public pure returns (uint256 entities, bytes32 poi, uint256 epoch) { - return abi.decode(data, (uint256, bytes32, uint256)); + ) public pure returns (IndexingAgreement.CollectIndexingFeeDataV1 memory) { + return abi.decode(data, (IndexingAgreement.CollectIndexingFeeDataV1)); } /** diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol index c272041eb..684bdb622 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol @@ -62,12 +62,13 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA tokensCollected, entities, poi, - epochManager.currentEpoch() + epochManager.currentEpochBlock(), + bytes("") ); subgraphService.collect( indexerState.addr, IGraphPayments.PaymentTypes.IndexingFee, - _encodeCollectDataV1(accepted.rca.agreementId, entities, poi, epochManager.currentEpoch()) + _encodeCollectDataV1(accepted.rca.agreementId, entities, poi, epochManager.currentEpochBlock(), bytes("")) ); assertEq( @@ -83,7 +84,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA uint256 entities, bytes32 poi ) public withSafeIndexerOrOperator(indexer) { - uint256 currentEpoch = epochManager.currentEpoch(); + uint256 currentEpochBlock = epochManager.currentEpochBlock(); resetPrank(users.pauseGuardian); subgraphService.pause(); @@ -92,7 +93,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA subgraphService.collect( indexer, IGraphPayments.PaymentTypes.IndexingFee, - _encodeCollectDataV1(agreementId, entities, poi, currentEpoch) + _encodeCollectDataV1(agreementId, entities, poi, currentEpochBlock, bytes("")) ); } @@ -104,7 +105,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA bytes32 poi ) public withSafeIndexerOrOperator(operator) { vm.assume(operator != indexer); - uint256 currentEpoch = epochManager.currentEpoch(); + uint256 currentEpochBlock = epochManager.currentEpochBlock(); resetPrank(operator); bytes memory expectedErr = abi.encodeWithSelector( ProvisionManager.ProvisionManagerNotAuthorized.selector, @@ -115,7 +116,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA subgraphService.collect( indexer, IGraphPayments.PaymentTypes.IndexingFee, - _encodeCollectDataV1(agreementId, entities, poi, currentEpoch) + _encodeCollectDataV1(agreementId, entities, poi, currentEpochBlock, bytes("")) ); } @@ -127,7 +128,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA bytes32 poi ) public withSafeIndexerOrOperator(indexer) { uint256 tokens = bound(unboundedTokens, 1, minimumProvisionTokens - 1); - uint256 currentEpoch = epochManager.currentEpoch(); + uint256 currentEpochBlock = epochManager.currentEpochBlock(); mint(indexer, tokens); resetPrank(indexer); _createProvision(indexer, tokens, fishermanRewardPercentage, disputePeriod); @@ -143,7 +144,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA subgraphService.collect( indexer, IGraphPayments.PaymentTypes.IndexingFee, - _encodeCollectDataV1(agreementId, entities, poi, currentEpoch) + _encodeCollectDataV1(agreementId, entities, poi, currentEpochBlock, bytes("")) ); } @@ -155,7 +156,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA bytes32 poi ) public withSafeIndexerOrOperator(indexer) { uint256 tokens = bound(unboundedTokens, minimumProvisionTokens, MAX_TOKENS); - uint256 currentEpoch = epochManager.currentEpoch(); + uint256 currentEpochBlock = epochManager.currentEpochBlock(); mint(indexer, tokens); resetPrank(indexer); _createProvision(indexer, tokens, fishermanRewardPercentage, disputePeriod); @@ -167,7 +168,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA subgraphService.collect( indexer, IGraphPayments.PaymentTypes.IndexingFee, - _encodeCollectDataV1(agreementId, entities, poi, currentEpoch) + _encodeCollectDataV1(agreementId, entities, poi, currentEpochBlock, bytes("")) ); } @@ -179,7 +180,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA ) public { Context storage ctx = _newCtx(seed); IndexerState memory indexerState = _withIndexer(ctx); - uint256 currentEpoch = epochManager.currentEpoch(); + uint256 currentEpochBlock = epochManager.currentEpochBlock(); bytes memory expectedErr = abi.encodeWithSelector(Allocation.AllocationDoesNotExist.selector, address(0)); vm.expectRevert(expectedErr); @@ -187,7 +188,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA subgraphService.collect( indexerState.addr, IGraphPayments.PaymentTypes.IndexingFee, - _encodeCollectDataV1(agreementId, entities, poi, currentEpoch) + _encodeCollectDataV1(agreementId, entities, poi, currentEpochBlock, bytes("")) ); } @@ -203,7 +204,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA resetPrank(indexerState.addr); subgraphService.stopService(indexerState.addr, abi.encode(indexerState.allocationId)); - uint256 currentEpoch = epochManager.currentEpoch(); + uint256 currentEpochBlock = epochManager.currentEpochBlock(); bytes memory expectedErr = abi.encodeWithSelector( AllocationManager.AllocationManagerAllocationClosed.selector, @@ -213,7 +214,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA subgraphService.collect( indexerState.addr, IGraphPayments.PaymentTypes.IndexingFee, - _encodeCollectDataV1(accepted.rca.agreementId, entities, poi, currentEpoch) + _encodeCollectDataV1(accepted.rca.agreementId, entities, poi, currentEpochBlock, bytes("")) ); } @@ -230,7 +231,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA resetPrank(indexerState.addr); subgraphService.closeStaleAllocation(indexerState.allocationId); - uint256 currentEpoch = epochManager.currentEpoch(); + uint256 currentEpochBlock = epochManager.currentEpochBlock(); bytes memory expectedErr = abi.encodeWithSelector( AllocationManager.AllocationManagerAllocationClosed.selector, @@ -240,7 +241,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA subgraphService.collect( indexerState.addr, IGraphPayments.PaymentTypes.IndexingFee, - _encodeCollectDataV1(accepted.rca.agreementId, entities, poi, currentEpoch) + _encodeCollectDataV1(accepted.rca.agreementId, entities, poi, currentEpochBlock, bytes("")) ); } /* solhint-enable graph/func-name-mixedcase */ diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol index 399d3c312..428962394 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol @@ -73,7 +73,13 @@ contract SubgraphServiceIndexingAgreementIntegrationTest is SubgraphServiceIndex uint256 tokensCollected = subgraphService.collect( indexerState.addr, IGraphPayments.PaymentTypes.IndexingFee, - _encodeCollectDataV1(agreementId, 1, keccak256(abi.encodePacked("poi")), epochManager.currentEpoch()) + _encodeCollectDataV1( + agreementId, + 1, + keccak256(abi.encodePacked("poi")), + epochManager.currentEpochBlock(), + bytes("") + ) ); TestState memory afterCollect = _getState(rca.payer, indexerState.addr); uint256 indexerTokensCollected = afterCollect.indexerBalance - beforeCollect.indexerBalance; 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 e6643883e..8574e60e7 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 @@ -341,9 +341,21 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun bytes16 _agreementId, uint256 _entities, bytes32 _poi, - uint256 _epoch + uint256 _poiBlock, + bytes memory _metadata ) internal pure returns (bytes memory) { - return abi.encode(_agreementId, abi.encode(_entities, _poi, _epoch)); + return + abi.encode( + _agreementId, + abi.encode( + IndexingAgreement.CollectIndexingFeeDataV1({ + entities: _entities, + poi: _poi, + poiBlockNumber: _poiBlock, + metadata: _metadata + }) + ) + ); } function _encodeAcceptIndexingAgreementMetadataV1( From 22a8e5a3010e2089de9eb42f442303bb83388bef Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 14:03:07 -0300 Subject: [PATCH 76/90] f: comment on expectedTokens --- .../subgraph-service/contracts/libraries/IndexingAgreement.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 65a2b0c9f..b56384677 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -527,6 +527,8 @@ library IndexingAgreement { ? 0 : _tokensToCollect(self, params.agreementId, wrapper.collectorAgreement, data.entities); + // `tokensCollected` <= `expectedTokens` because the recurring collector will further narrow + // down the tokens allowed, based on the RCA terms. uint256 tokensCollected = _directory().recurringCollector().collect( IGraphPayments.PaymentTypes.IndexingFee, abi.encode( From 650a7c000c3a9ccbcc88ec4162761c7ec988d16a Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 14:36:13 -0300 Subject: [PATCH 77/90] f: set receiverDestination --- .../contracts/interfaces/IRecurringCollector.sol | 2 ++ .../payments/collectors/RecurringCollector.sol | 2 +- .../unit/payments/recurring-collector/shared.t.sol | 4 ++-- .../subgraph-service/contracts/SubgraphService.sol | 10 ++++++++-- .../contracts/libraries/IndexingAgreement.sol | 5 ++++- .../subgraphService/indexing-agreement/collect.t.sol | 5 ++++- .../indexing-agreement/integration.t.sol | 2 ++ 7 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IRecurringCollector.sol b/packages/horizon/contracts/interfaces/IRecurringCollector.sol index 30293de9c..9f3c3a1bf 100644 --- a/packages/horizon/contracts/interfaces/IRecurringCollector.sol +++ b/packages/horizon/contracts/interfaces/IRecurringCollector.sol @@ -156,12 +156,14 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector { * @param collectionId The collection ID of the RCA * @param tokens The amount of tokens to collect * @param dataServiceCut The data service cut in parts per million + * @param receiverDestination The address where the collected fees should be sent */ struct CollectParams { bytes16 agreementId; bytes32 collectionId; uint256 tokens; uint256 dataServiceCut; + address receiverDestination; } /** diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index 3809fdba5..a80dfe719 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -290,7 +290,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC tokensToCollect, agreement.dataService, _params.dataServiceCut, - agreement.serviceProvider // FIX-ME + _params.receiverDestination ); } agreement.lastCollectionAt = uint64(block.timestamp); diff --git a/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol b/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol index 20fb1edf7..397925600 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol @@ -163,7 +163,6 @@ contract RecurringCollectorSharedTest is Test, Bounder { return (data, collectionSeconds, tokens); } - // Do I need this? function _generateCollectParams( IRecurringCollector.RecurringCollectionAgreement memory _rca, bytes32 _collectionId, @@ -175,7 +174,8 @@ contract RecurringCollectorSharedTest is Test, Bounder { agreementId: _rca.agreementId, collectionId: _collectionId, tokens: _tokens, - dataServiceCut: _dataServiceCut + dataServiceCut: _dataServiceCut, + receiverDestination: _rca.serviceProvider }); } diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 186079bb0..02a0c6cda 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -299,7 +299,7 @@ contract SubgraphService is paymentCollected = _collectIndexingRewards(indexer, data); } else if (paymentType == IGraphPayments.PaymentTypes.IndexingFee) { (bytes16 agreementId, bytes memory iaCollectionData) = IndexingAgreementDecoder.decodeCollectData(data); - paymentCollected = _collectIndexingFees(agreementId, iaCollectionData); + paymentCollected = _collectIndexingFees(agreementId, paymentsDestination[indexer], iaCollectionData); } else { revert SubgraphServiceInvalidPaymentType(paymentType); } @@ -756,15 +756,21 @@ contract SubgraphService is * Emits a {IndexingFeesCollectedV1} event. * * @param _agreementId The id of the indexing agreement + * @param _paymentsDestination The address where the fees should be sent * @param _data The indexing agreement collection data * @return The amount of fees collected */ - function _collectIndexingFees(bytes16 _agreementId, bytes memory _data) private returns (uint256) { + function _collectIndexingFees( + bytes16 _agreementId, + address _paymentsDestination, + bytes memory _data + ) private returns (uint256) { (address indexer, uint256 tokensCollected) = IndexingAgreement._getStorageManager().collect( _allocations, IndexingAgreement.CollectParams({ agreementId: _agreementId, currentEpoch: _graphEpochManager().currentEpoch(), + receiverDestination: _paymentsDestination, data: _data }) ); diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index b56384677..0495174eb 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -78,11 +78,13 @@ library IndexingAgreement { * @notice Parameters for collecting indexing fees * @param agreementId The ID of the indexing agreement * @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 */ struct CollectParams { bytes16 agreementId; uint256 currentEpoch; + address receiverDestination; bytes data; } @@ -536,7 +538,8 @@ library IndexingAgreement { agreementId: params.agreementId, collectionId: bytes32(uint256(uint160(wrapper.agreement.allocationId))), tokens: expectedTokens, - dataServiceCut: 0 + dataServiceCut: 0, + receiverDestination: params.receiverDestination }) ) ); diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol index 684bdb622..2063b0441 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol @@ -33,12 +33,15 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA assertEq(subgraphService.feesProvisionTracker(indexerState.addr), 0, "Should be 0 before collect"); resetPrank(indexerState.addr); + subgraphService.setPaymentsDestination(indexerState.addr); + bytes memory data = abi.encode( IRecurringCollector.CollectParams({ agreementId: accepted.rca.agreementId, collectionId: bytes32(uint256(uint160(indexerState.allocationId))), tokens: 0, - dataServiceCut: 0 + dataServiceCut: 0, + receiverDestination: indexerState.addr }) ); uint256 tokensCollected = bound(unboundedTokensCollected, 1, indexerState.tokens / stakeToFeesRatio); diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol index 428962394..433ee0103 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/integration.t.sol @@ -60,6 +60,8 @@ contract SubgraphServiceIndexingAgreementIntegrationTest is SubgraphServiceIndex _setupPayerWithEscrow(rca.payer, ctx.payer.signerPrivateKey, indexerState.addr, expectedTotalTokensCollected); resetPrank(indexerState.addr); + // Set the payments destination to the indexer address + subgraphService.setPaymentsDestination(indexerState.addr); // Accept the Indexing Agreement subgraphService.acceptIndexingAgreement( indexerState.allocationId, From a60db1719c262c1b2d99a3e82564bf16fa21a807 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 14:43:16 -0300 Subject: [PATCH 78/90] f: remove SubgraphServiceLib --- .../contracts/libraries/IndexingAgreement.sol | 38 +++++++++++++++++-- .../libraries/SubgraphServiceLib.sol | 38 ------------------- 2 files changed, 34 insertions(+), 42 deletions(-) delete mode 100644 packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 0495174eb..1afa672be 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -5,16 +5,17 @@ import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGra import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; import { GraphDirectory } from "@graphprotocol/horizon/contracts/utilities/GraphDirectory.sol"; +import { ISubgraphService } from "../interfaces/ISubgraphService.sol"; +import { AllocationManager } from "../utilities/AllocationManager.sol"; import { SubgraphService } from "../SubgraphService.sol"; import { Directory } from "../utilities/Directory.sol"; import { Allocation } from "./Allocation.sol"; -import { SubgraphServiceLib } from "./SubgraphServiceLib.sol"; import { IndexingAgreementDecoder } from "./IndexingAgreementDecoder.sol"; library IndexingAgreement { using IndexingAgreement for StorageManager; + using Allocation for Allocation.State; using Allocation for mapping(address => Allocation.State); - using SubgraphServiceLib for mapping(address => Allocation.State); /// @notice Versions of Indexing Agreement Metadata enum IndexingAgreementVersion { @@ -287,7 +288,8 @@ library IndexingAgreement { address allocationId, IRecurringCollector.SignedRCA calldata signedRCA ) external { - Allocation.State memory allocation = allocations.requireValidAllocation( + Allocation.State memory allocation = _requireValidAllocation( + allocations, allocationId, signedRCA.rca.serviceProvider ); @@ -512,7 +514,8 @@ library IndexingAgreement { CollectParams memory params ) external returns (address, uint256) { AgreementWrapper memory wrapper = _get(self, params.agreementId); - Allocation.State memory allocation = allocations.requireValidAllocation( + Allocation.State memory allocation = _requireValidAllocation( + allocations, wrapper.agreement.allocationId, wrapper.collectorAgreement.serviceProvider ); @@ -636,6 +639,33 @@ library IndexingAgreement { _directory().recurringCollector().cancel(_agreementId, _cancelBy); } + /** + * @notice Requires that the allocation is valid and owned by the indexer. + * + * Requirements: + * - Allocation must belong to the indexer + * - Allocation must be open + * + * @param _allocations The mapping of allocation IDs to their states + * @param _allocationId The id of the allocation + * @param _indexer The address of the indexer + * @return The allocation state + */ + function _requireValidAllocation( + mapping(address => Allocation.State) storage _allocations, + address _allocationId, + address _indexer + ) private view returns (Allocation.State memory) { + Allocation.State memory allocation = _allocations.get(_allocationId); + require( + allocation.indexer == _indexer, + ISubgraphService.SubgraphServiceAllocationNotAuthorized(_indexer, _allocationId) + ); + require(allocation.isOpen(), AllocationManager.AllocationManagerAllocationClosed(_allocationId)); + + return allocation; + } + /** * @notice Calculate the number of tokens to collect for an indexing agreement. * diff --git a/packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol b/packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol deleted file mode 100644 index 0b702380e..000000000 --- a/packages/subgraph-service/contracts/libraries/SubgraphServiceLib.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; - -import { ISubgraphService } from "../interfaces/ISubgraphService.sol"; -import { AllocationManager } from "../utilities/AllocationManager.sol"; -import { Allocation } from "./Allocation.sol"; - -library SubgraphServiceLib { - using Allocation for mapping(address => Allocation.State); - using Allocation for Allocation.State; - - /** - * @notice Requires that the allocation is valid and owned by the indexer. - * - * Requirements: - * - Allocation must belong to the indexer - * - Allocation must be open - * - * @param self The allocation storage manager - * @param allocationId The id of the allocation - * @param indexer The address of the indexer - * @return The allocation state - */ - function requireValidAllocation( - mapping(address => Allocation.State) storage self, - address allocationId, - address indexer - ) external view returns (Allocation.State memory) { - Allocation.State memory allocation = self.get(allocationId); - require( - allocation.indexer == indexer, - ISubgraphService.SubgraphServiceAllocationNotAuthorized(indexer, allocationId) - ); - require(allocation.isOpen(), AllocationManager.AllocationManagerAllocationClosed(allocationId)); - - return allocation; - } -} From dae9cd924021dacb889196f7c5f8c105c7145186 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 9 Jun 2025 14:47:58 -0300 Subject: [PATCH 79/90] f: remove StakeClaims --- .../data-service/extensions/DataServiceFees.sol | 10 +++++----- .../{DataServiceFeesLib.sol => StakeClaims.sol} | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename packages/horizon/contracts/data-service/libraries/{DataServiceFeesLib.sol => StakeClaims.sol} (99%) diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol index 9c862bf75..7fe1d8f51 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -5,7 +5,7 @@ import { IDataServiceFees } from "../interfaces/IDataServiceFees.sol"; import { ProvisionTracker } from "../libraries/ProvisionTracker.sol"; import { LinkedList } from "../../libraries/LinkedList.sol"; -import { DataServiceFeesLib } from "../libraries/DataServiceFeesLib.sol"; +import { StakeClaims } from "../libraries/StakeClaims.sol"; import { DataService } from "../DataService.sol"; import { DataServiceFeesV1Storage } from "./DataServiceFeesStorage.sol"; @@ -42,7 +42,7 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat * @param _unlockTimestamp The timestamp when the tokens can be released */ function _lockStake(address _serviceProvider, uint256 _tokens, uint256 _unlockTimestamp) internal { - DataServiceFeesLib.lockStake( + StakeClaims.lockStake( feesProvisionTracker, claims, claimsLists, @@ -86,7 +86,7 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat * @return The updated accumulator data */ function _processStakeClaim(bytes32 _claimId, bytes memory _acc) private returns (bool, bytes memory) { - return DataServiceFeesLib.processStakeClaim(feesProvisionTracker, claims, _claimId, _acc); + return StakeClaims.processStakeClaim(feesProvisionTracker, claims, _claimId, _acc); } /** @@ -95,7 +95,7 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat * @param _claimId The ID of the stake claim to delete */ function _deleteStakeClaim(bytes32 _claimId) private { - DataServiceFeesLib.deleteStakeClaim(claims, _claimId); + StakeClaims.deleteStakeClaim(claims, _claimId); } /** @@ -105,6 +105,6 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat * @return The next stake claim ID */ function _getNextStakeClaim(bytes32 _claimId) private view returns (bytes32) { - return DataServiceFeesLib.getNextStakeClaim(claims, _claimId); + return StakeClaims.getNextStakeClaim(claims, _claimId); } } diff --git a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol b/packages/horizon/contracts/data-service/libraries/StakeClaims.sol similarity index 99% rename from packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol rename to packages/horizon/contracts/data-service/libraries/StakeClaims.sol index 372cd30c5..465d6ddb8 100644 --- a/packages/horizon/contracts/data-service/libraries/DataServiceFeesLib.sol +++ b/packages/horizon/contracts/data-service/libraries/StakeClaims.sol @@ -6,7 +6,7 @@ import { IDataServiceFees } from "../interfaces/IDataServiceFees.sol"; import { IHorizonStaking } from "../../interfaces/IHorizonStaking.sol"; import { LinkedList } from "../../libraries/LinkedList.sol"; -library DataServiceFeesLib { +library StakeClaims { using ProvisionTracker for mapping(address => uint256); using LinkedList for LinkedList.List; From 8c1944613d3d8ef819de7dd64a6893242cdc3042 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 12:11:27 -0300 Subject: [PATCH 80/90] f: revert Allocation vis changes --- .../contracts/libraries/Allocation.sol | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/Allocation.sol b/packages/subgraph-service/contracts/libraries/Allocation.sol index eb91da92d..6f6563068 100644 --- a/packages/subgraph-service/contracts/libraries/Allocation.sol +++ b/packages/subgraph-service/contracts/libraries/Allocation.sol @@ -76,7 +76,7 @@ library Allocation { uint256 tokens, uint256 accRewardsPerAllocatedToken, uint256 createdAtEpoch - ) external returns (State memory) { + ) internal returns (State memory) { require(!self[allocationId].exists(), AllocationAlreadyExists(allocationId)); State memory allocation = State({ @@ -104,54 +104,54 @@ library Allocation { * @param self The allocation list mapping * @param allocationId The allocation id */ - function presentPOI(mapping(address => State) storage self, address allocationId) external { + function presentPOI(mapping(address => State) storage self, address allocationId) internal { State storage allocation = _get(self, allocationId); require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); allocation.lastPOIPresentedAt = block.timestamp; } /** - * @notice Update the accumulated rewards pending to be claimed for an allocation + * @notice Update the accumulated rewards per allocated token for an allocation * @dev Requirements: * - The allocation must be open * @param self The allocation list mapping * @param allocationId The allocation id + * @param accRewardsPerAllocatedToken The new accumulated rewards per allocated token */ - function clearPendingRewards(mapping(address => State) storage self, address allocationId) external { + function snapshotRewards( + mapping(address => State) storage self, + address allocationId, + uint256 accRewardsPerAllocatedToken + ) internal { State storage allocation = _get(self, allocationId); require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); - allocation.accRewardsPending = 0; + allocation.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; } /** - * @notice Close an allocation + * @notice Update the accumulated rewards pending to be claimed for an allocation * @dev Requirements: * - The allocation must be open * @param self The allocation list mapping * @param allocationId The allocation id */ - function close(mapping(address => State) storage self, address allocationId) external { + function clearPendingRewards(mapping(address => State) storage self, address allocationId) internal { State storage allocation = _get(self, allocationId); require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); - allocation.closedAt = block.timestamp; + allocation.accRewardsPending = 0; } /** - * @notice Update the accumulated rewards per allocated token for an allocation + * @notice Close an allocation * @dev Requirements: * - The allocation must be open * @param self The allocation list mapping * @param allocationId The allocation id - * @param accRewardsPerAllocatedToken The new accumulated rewards per allocated token */ - function snapshotRewards( - mapping(address => State) storage self, - address allocationId, - uint256 accRewardsPerAllocatedToken - ) internal { + function close(mapping(address => State) storage self, address allocationId) internal { State storage allocation = _get(self, allocationId); require(allocation.isOpen(), AllocationClosed(allocationId, allocation.closedAt)); - allocation.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + allocation.closedAt = block.timestamp; } /** From c663a41bbf784dba5af8624776ab4943cdac1fbb Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 12:11:57 -0300 Subject: [PATCH 81/90] f: remove unused getters --- .../contracts/libraries/IndexingAgreement.sol | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 1afa672be..0dc594575 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -3,11 +3,9 @@ pragma solidity 0.8.27; import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; -import { GraphDirectory } from "@graphprotocol/horizon/contracts/utilities/GraphDirectory.sol"; import { ISubgraphService } from "../interfaces/ISubgraphService.sol"; import { AllocationManager } from "../utilities/AllocationManager.sol"; -import { SubgraphService } from "../SubgraphService.sol"; import { Directory } from "../utilities/Directory.sol"; import { Allocation } from "./Allocation.sol"; import { IndexingAgreementDecoder } from "./IndexingAgreementDecoder.sol"; @@ -715,22 +713,6 @@ library IndexingAgreement { return Directory(address(this)); } - /** - * @notice Gets the Graph Directory - * @return The Graph Directory contract - */ - function _graphDirectory() private view returns (GraphDirectory) { - return GraphDirectory(address(this)); - } - - /** - * @notice Gets the Subgraph Service - * @return The Subgraph Service contract - */ - function _subgraphService() private view returns (SubgraphService) { - return SubgraphService(address(this)); - } - /** * @notice Gets the indexing agreement wrapper for a given agreement ID. * @dev This function retrieves the indexing agreement wrapper containing the agreement state and collector agreement data. From f5cbb1c9e490a95a4f940c0066d4dee7577f2c89 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 12:13:47 -0300 Subject: [PATCH 82/90] f: typo indexining --- packages/subgraph-service/contracts/SubgraphService.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 02a0c6cda..4f96d5223 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -243,7 +243,7 @@ contract SubgraphService is /** * @notice Collects payment for the service provided by the indexer - * Allows collecting different types of payments such as query fees, indexing rewards and indexining fees. + * Allows collecting different types of payments such as query fees, indexing rewards and indexing fees. * It uses Graph Horizon payments protocol to process payments. * Reverts if the payment type is not supported. * @dev This function is the equivalent of the `collect` function for query fees and the `closeAllocation` function From 32fec56f69380d382ced4ca2a7201110f2c66420 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 13:30:05 -0300 Subject: [PATCH 83/90] f: allow short final collection --- .../collectors/RecurringCollector.sol | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol index a80dfe719..99122a348 100644 --- a/packages/horizon/contracts/payments/collectors/RecurringCollector.sol +++ b/packages/horizon/contracts/payments/collectors/RecurringCollector.sol @@ -365,13 +365,16 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC bytes16 _agreementId, uint256 _tokens ) private view returns (uint256) { - // if canceled by the payer allow collection up to the cancelation time + bool canceledOrElapsed = _agreement.state == AgreementState.CanceledByPayer || + block.timestamp > _agreement.endsAt; uint256 canceledOrNow = _agreement.state == AgreementState.CanceledByPayer ? _agreement.canceledAt : block.timestamp; - // allow collection till endsAt (at most) - uint256 collectionEnd = Math.min(canceledOrNow, _agreement.endsAt); + // if canceled by the payer allow collection till canceledAt + // if elapsed allow collection till endsAt + // if both are true, use the earlier one + uint256 collectionEnd = canceledOrElapsed ? Math.min(canceledOrNow, _agreement.endsAt) : block.timestamp; uint256 collectionStart = _agreementCollectionStartAt(_agreement); require( collectionEnd != collectionStart, @@ -380,14 +383,18 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC require(collectionEnd > collectionStart, RecurringCollectorFinalCollectionDone(_agreementId, collectionStart)); uint256 collectionSeconds = collectionEnd - collectionStart; - require( - collectionSeconds >= _agreement.minSecondsPerCollection, - RecurringCollectorCollectionTooSoon( - _agreementId, - uint32(collectionSeconds), - _agreement.minSecondsPerCollection - ) - ); + // Check that the collection window is long enough + // If the agreement is canceled or elapsed, allow a shorter collection window + if (!canceledOrElapsed) { + require( + collectionSeconds >= _agreement.minSecondsPerCollection, + RecurringCollectorCollectionTooSoon( + _agreementId, + uint32(collectionSeconds), + _agreement.minSecondsPerCollection + ) + ); + } require( collectionSeconds <= _agreement.maxSecondsPerCollection, RecurringCollectorCollectionTooLate( From 966a202bb4eec36117fcb9c658c923a3e064485f Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 13:35:36 -0300 Subject: [PATCH 84/90] f: delete IndexingPaymentsTodo.md --- IndexingPaymentsTodo.md | 48 ----------------------------------------- 1 file changed, 48 deletions(-) delete mode 100644 IndexingPaymentsTodo.md diff --git a/IndexingPaymentsTodo.md b/IndexingPaymentsTodo.md deleted file mode 100644 index f3ab72e62..000000000 --- a/IndexingPaymentsTodo.md +++ /dev/null @@ -1,48 +0,0 @@ -# Still pending - -* `require(provision.tokens != 0, DisputeManagerZeroTokens());` - Document or fix? -* Check code coverage -* Don't love cancel agreement on stop service / close stale allocation. -* Arbitration Charter: Update to support disputing IndexingFee. - -# Done - -* DONE: ~~* Remove extension if I can fit everything in one service?~~ -* DONE: ~~* One Interface for all subgraph~~ -* DONE: ~~* Missing Upgrade event for subgraph service~~ -* DONE: ~~* Check contract size~~ -* DONE: ~~Switch cancel event in recurring collector to use Enum~~ -* DONE: ~~Switch timestamps to uint64~~ -* DONE: ~~Check that UUID-v4 fits in `bytes16`~~ -* DONE: ~~Double check cancelation policy. Who can cancel when? Right now is either party at any time. Answer: If gateway cancels allow collection till that point.~~ -* DONE: ~~If an indexer closes an allocation, what should happen to the accepeted agreement? Answer: Look into canceling agreement as part of stop service.~~ -* DONE: ~~Switch `duration` for `endsAt`? Answer: Do it.~~ -* DONE: ~~Support a way for gateway to shop an agreement around? Deadline + dedup key? So only one agreement with the dedupe key can be accepted? Answer: No. Agreements will be "signaled" as approved or rejected on the API call that sends the agreement. We'll trust (and verify) that that's the case.~~ -* DONE: ~~Test `upgrade` paths~~ -* DONE: ~~Fix upgrade.t.sol, lots of comments~~ -* DONE: ~~How do we solve for the case where an indexer has reached their max expected payout for the initial sync but haven't reached the current epoch (thus their POI is incorrect)? Answer: Signal in the event that the max amount was collected, so that fisherman understand the case.~~ -* DONE: ~~Debate epoch check protocol team. Maybe don't revert but store it in event. Pablo suggest block number instead of epoch.~~ -* DONE: ~~Should we set a different param for initial collection time max? Some subgraphs take a lot to catch up. Answer: Do nothing. Make sure that zero POIs allow to eventually sync~~ -* DONE: ~~Since an allocation is required for collecting, do we want to expect that the allocation is not stale? Do we want to add code to collect rewards as part of the collection of fees? Make sure allocation is more than one epoch old if we attempt this. Answer: Ignore stale allocation~~ -* DONE: ~~If service wants to collect more than collector allows. Collector limits but doesn't tell the service? Currently reverts. Answer: Allow for max allowed~~ -* DONE: ~~What should happen if the escrow doesn't have enough funds? Answer: Reverts~~ -* DONE: ~~Don't pay for entities on initial collection? Where did we land in terms of payment terms? Answer: pay initial~~ -* DONE: ~~Test lock stake~~ -* DONE: ~~Reduce the number of errors declared and returned~~ -* DONE: ~~Support `DisputeManager`~~ -* DONE: ~~Check upgrade conditions. Support indexing agreement upgradability, so that there is a mechanism to adjust the rates without having to cancel and start over.~~ -* DONE: ~~Maybe check that the epoch the indexer is sending is the one the transaction will be run in?~~ -* DONE: ~~Should we deal with zero entities declared as a special case?~~ -* DONE: ~~Support for agreements that end up in `RecurringCollectorCollectionTooLate` or ways to avoid getting to that state.~~ -* DONE: ~~Make `agreementId` unique globally so that we don't need the full tuple (`payer`+`indexer`+`agreementId`) as key?~~ -* DONE: ~~Maybe IRecurringCollector.cancel(address payer, address serviceProvider, bytes16 agreementId) should only take in agreementId?~~ -* DONE: ~~Unify to one error in Decoder.sol~~ -* DONE: ~~Built-in upgrade path to indexing agreements v2~~ -* DONE: ~~Missing events for accept, cancel, upgrade RCAs.~~ - -# Won't Fix - -* Add upgrade path to v2 collector terms -* Expose a function that indexers can use to calculate the tokens to be collected and other collection params? -* Place all agreement terms into one struct -* It's more like a collect + cancel since the indexer is expected to stop work then and there. When posting a POI that's < N-1 epoch. Answer: Emit signal that the collection is meant to be final. Counter: Won't do since collector can't signal back to data service that payment is maxed out. Could emit an event from the collector, but is it really worth it? Right now any collection where epoch POI < current POI is suspect. From 0ddb6154ffd143f6ccea1374f1eb71866aee6467 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 15:33:36 -0300 Subject: [PATCH 85/90] f: inline release and lock stake --- .../contracts/SubgraphService.sol | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 4f96d5223..5c557a908 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -775,7 +775,15 @@ contract SubgraphService is }) ); - _releaseAndLockStake(indexer, tokensCollected); + _releaseStake(indexer, 0); + if (tokensCollected > 0) { + // lock stake as economic security for fees + _lockStake( + indexer, + tokensCollected * stakeToFeesRatio, + block.timestamp + _disputeManager().getDisputePeriod() + ); + } return tokensCollected; } @@ -790,25 +798,6 @@ contract SubgraphService is emit StakeToFeesRatioSet(_stakeToFeesRatio); } - /** - * @notice Release stake claims and lock new stake as economic security for fees - * @dev This function releases all expired stake claims and locks new stake as economic security for fees. - * It is called after collecting query fees or indexing fees. - * @param _indexer The address of the indexer - * @param _tokensCollected The amount of tokens collected from fees - */ - function _releaseAndLockStake(address _indexer, uint256 _tokensCollected) private { - _releaseStake(_indexer, 0); - if (_tokensCollected > 0) { - // lock stake as economic security for fees - _lockStake( - _indexer, - _tokensCollected * stakeToFeesRatio, - block.timestamp + _disputeManager().getDisputePeriod() - ); - } - } - /** * @notice Encodes the data for the GraphTallyCollector * @dev The purpose of this function is just to avoid stack too deep errors From 9a296b684b7829f5d3116ba63ae36e5ca3d6a986 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 16:22:26 -0300 Subject: [PATCH 86/90] f: public buildStakeClaimId --- .../extensions/DataServiceFees.sol | 1 + .../data-service/libraries/StakeClaims.sol | 28 +++++++++++++++++-- .../subgraphService/SubgraphService.t.sol | 3 +- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol index 7fe1d8f51..13e85326e 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -47,6 +47,7 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat claims, claimsLists, _graphStaking(), + address(this), _delegationRatio, _serviceProvider, _tokens, diff --git a/packages/horizon/contracts/data-service/libraries/StakeClaims.sol b/packages/horizon/contracts/data-service/libraries/StakeClaims.sol index 465d6ddb8..89762db25 100644 --- a/packages/horizon/contracts/data-service/libraries/StakeClaims.sol +++ b/packages/horizon/contracts/data-service/libraries/StakeClaims.sol @@ -22,6 +22,7 @@ library StakeClaims { * @param claims The mapping that stores stake claims by their ID * @param claimsLists The mapping that stores linked lists of stake claims by service provider * @param graphStaking The Horizon staking contract used to lock the tokens + * @param _dataService The address of the data service * @param _delegationRatio The delegation ratio to use for the stake claim * @param _serviceProvider The address of the service provider * @param _tokens The amount of tokens to lock in the claim @@ -32,6 +33,7 @@ library StakeClaims { mapping(bytes32 => IDataServiceFees.StakeClaim) storage claims, mapping(address serviceProvider => LinkedList.List list) storage claimsLists, IHorizonStaking graphStaking, + address _dataService, uint32 _delegationRatio, address _serviceProvider, uint256 _tokens, @@ -43,7 +45,7 @@ library StakeClaims { LinkedList.List storage claimsList = claimsLists[_serviceProvider]; // Save item and add to list - bytes32 claimId = _buildStakeClaimId(_serviceProvider, claimsList.nonce); + bytes32 claimId = _buildStakeClaimId(_dataService, _serviceProvider, claimsList.nonce); claims[claimId] = IDataServiceFees.StakeClaim({ tokens: _tokens, createdAt: block.timestamp, @@ -121,11 +123,31 @@ library StakeClaims { /** * @notice Builds a stake claim ID + * @param dataService The address of the data service * @param serviceProvider The address of the service provider * @param nonce A nonce of the stake claim * @return The stake claim ID */ - function _buildStakeClaimId(address serviceProvider, uint256 nonce) internal view returns (bytes32) { - return keccak256(abi.encodePacked(address(this), serviceProvider, nonce)); + function buildStakeClaimId( + address dataService, + address serviceProvider, + uint256 nonce + ) public pure returns (bytes32) { + return _buildStakeClaimId(dataService, serviceProvider, nonce); + } + + /** + * @notice Builds a stake claim ID + * @param _dataService The address of the data service + * @param _serviceProvider The address of the service provider + * @param _nonce A nonce of the stake claim + * @return The stake claim ID + */ + function _buildStakeClaimId( + address _dataService, + address _serviceProvider, + uint256 _nonce + ) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(_dataService, _serviceProvider, _nonce)); } } diff --git a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol index bd9ccc8d3..ab9d657f5 100644 --- a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol @@ -12,6 +12,7 @@ import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { LinkedList } from "@graphprotocol/horizon/contracts/libraries/LinkedList.sol"; import { IDataServiceFees } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataServiceFees.sol"; import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { StakeClaims } from "@graphprotocol/horizon/contracts/data-service/libraries/StakeClaims.sol"; import { Allocation } from "../../../contracts/libraries/Allocation.sol"; import { AllocationManager } from "../../../contracts/utilities/AllocationManager.sol"; @@ -513,7 +514,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { } function _buildStakeClaimId(address _indexer, uint256 _nonce) private view returns (bytes32) { - return keccak256(abi.encodePacked(address(subgraphService), _indexer, _nonce)); + return StakeClaims.buildStakeClaimId(address(subgraphService), _indexer, _nonce); } function _getStakeClaim(bytes32 _claimId) private view returns (IDataServiceFees.StakeClaim memory) { From 91b341a623a5fbd9ce3ac5e4971fce38d1b5029f Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 16:34:58 -0300 Subject: [PATCH 87/90] f: move StakeClaim stuff --- .../extensions/DataServiceFees.sol | 2 +- .../extensions/DataServiceFeesStorage.sol | 4 +- .../interfaces/IDataServiceFees.sol | 64 -------------- .../data-service/libraries/StakeClaims.sol | 88 ++++++++++++++++--- .../extensions/DataServiceFees.t.sol | 12 +-- .../subgraphService/SubgraphService.t.sol | 7 +- 6 files changed, 87 insertions(+), 90 deletions(-) diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol index 13e85326e..7b978794b 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -75,7 +75,7 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat _numClaimsToRelease ); - emit StakeClaimsReleased(_serviceProvider, claimsReleased, abi.decode(data, (uint256))); + emit StakeClaims.StakeClaimsReleased(_serviceProvider, claimsReleased, abi.decode(data, (uint256))); } /** diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol index 30d1aa4ee..795206151 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; -import { IDataServiceFees } from "../interfaces/IDataServiceFees.sol"; +import { StakeClaims } from "../libraries/StakeClaims.sol"; import { LinkedList } from "../../libraries/LinkedList.sol"; @@ -15,7 +15,7 @@ abstract contract DataServiceFeesV1Storage { mapping(address serviceProvider => uint256 tokens) public feesProvisionTracker; /// @notice List of all locked stake claims to be released to service providers - mapping(bytes32 claimId => IDataServiceFees.StakeClaim claim) public claims; + mapping(bytes32 claimId => StakeClaims.StakeClaim claim) public claims; /// @notice Service providers registered in the data service mapping(address serviceProvider => LinkedList.List list) public claimsLists; diff --git a/packages/horizon/contracts/data-service/interfaces/IDataServiceFees.sol b/packages/horizon/contracts/data-service/interfaces/IDataServiceFees.sol index 9d235f4f7..58dfd95f5 100644 --- a/packages/horizon/contracts/data-service/interfaces/IDataServiceFees.sol +++ b/packages/horizon/contracts/data-service/interfaces/IDataServiceFees.sol @@ -22,70 +22,6 @@ import { IDataService } from "./IDataService.sol"; * bugs. We may have an active bug bounty program. */ interface IDataServiceFees is IDataService { - /** - * @notice A stake claim, representing provisioned stake that gets locked - * to be released to a service provider. - * @dev StakeClaims are stored in linked lists by service provider, ordered by - * creation timestamp. - * @param tokens The amount of tokens to be locked in the claim - * @param createdAt The timestamp when the claim was created - * @param releasableAt The timestamp when the tokens can be released - * @param nextClaim The next claim in the linked list - */ - struct StakeClaim { - uint256 tokens; - uint256 createdAt; - uint256 releasableAt; - bytes32 nextClaim; - } - - /** - * @notice Emitted when a stake claim is created and stake is locked. - * @param serviceProvider The address of the service provider - * @param claimId The id of the stake claim - * @param tokens The amount of tokens to lock in the claim - * @param unlockTimestamp The timestamp when the tokens can be released - */ - event StakeClaimLocked( - address indexed serviceProvider, - bytes32 indexed claimId, - uint256 tokens, - uint256 unlockTimestamp - ); - - /** - * @notice Emitted when a stake claim is released and stake is unlocked. - * @param serviceProvider The address of the service provider - * @param claimId The id of the stake claim - * @param tokens The amount of tokens released - * @param releasableAt The timestamp when the tokens were released - */ - event StakeClaimReleased( - address indexed serviceProvider, - bytes32 indexed claimId, - uint256 tokens, - uint256 releasableAt - ); - - /** - * @notice Emitted when a series of stake claims are released. - * @param serviceProvider The address of the service provider - * @param claimsCount The number of stake claims being released - * @param tokensReleased The total amount of tokens being released - */ - event StakeClaimsReleased(address indexed serviceProvider, uint256 claimsCount, uint256 tokensReleased); - - /** - * @notice Thrown when attempting to get a stake claim that does not exist. - * @param claimId The id of the stake claim - */ - error DataServiceFeesClaimNotFound(bytes32 claimId); - - /** - * @notice Emitted when trying to lock zero tokens in a stake claim - */ - error DataServiceFeesZeroTokens(); - /** * @notice Releases expired stake claims for the caller. * @dev This function is only meant to be called if the service provider has enough diff --git a/packages/horizon/contracts/data-service/libraries/StakeClaims.sol b/packages/horizon/contracts/data-service/libraries/StakeClaims.sol index 89762db25..5269d7ec4 100644 --- a/packages/horizon/contracts/data-service/libraries/StakeClaims.sol +++ b/packages/horizon/contracts/data-service/libraries/StakeClaims.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.27; import { ProvisionTracker } from "./ProvisionTracker.sol"; -import { IDataServiceFees } from "../interfaces/IDataServiceFees.sol"; import { IHorizonStaking } from "../../interfaces/IHorizonStaking.sol"; import { LinkedList } from "../../libraries/LinkedList.sol"; @@ -10,6 +9,70 @@ library StakeClaims { using ProvisionTracker for mapping(address => uint256); using LinkedList for LinkedList.List; + /** + * @notice A stake claim, representing provisioned stake that gets locked + * to be released to a service provider. + * @dev StakeClaims are stored in linked lists by service provider, ordered by + * creation timestamp. + * @param tokens The amount of tokens to be locked in the claim + * @param createdAt The timestamp when the claim was created + * @param releasableAt The timestamp when the tokens can be released + * @param nextClaim The next claim in the linked list + */ + struct StakeClaim { + uint256 tokens; + uint256 createdAt; + uint256 releasableAt; + bytes32 nextClaim; + } + + /** + * @notice Emitted when a stake claim is created and stake is locked. + * @param serviceProvider The address of the service provider + * @param claimId The id of the stake claim + * @param tokens The amount of tokens to lock in the claim + * @param unlockTimestamp The timestamp when the tokens can be released + */ + event StakeClaimLocked( + address indexed serviceProvider, + bytes32 indexed claimId, + uint256 tokens, + uint256 unlockTimestamp + ); + + /** + * @notice Emitted when a stake claim is released and stake is unlocked. + * @param serviceProvider The address of the service provider + * @param claimId The id of the stake claim + * @param tokens The amount of tokens released + * @param releasableAt The timestamp when the tokens were released + */ + event StakeClaimReleased( + address indexed serviceProvider, + bytes32 indexed claimId, + uint256 tokens, + uint256 releasableAt + ); + + /** + * @notice Emitted when a series of stake claims are released. + * @param serviceProvider The address of the service provider + * @param claimsCount The number of stake claims being released + * @param tokensReleased The total amount of tokens being released + */ + event StakeClaimsReleased(address indexed serviceProvider, uint256 claimsCount, uint256 tokensReleased); + + /** + * @notice Thrown when attempting to get a stake claim that does not exist. + * @param claimId The id of the stake claim + */ + error StakeClaimsClaimNotFound(bytes32 claimId); + + /** + * @notice Emitted when trying to lock zero tokens in a stake claim + */ + error StakeClaimsZeroTokens(); + /** * @notice Locks stake for a service provider to back a payment. * Creates a stake claim, which is stored in a linked list by service provider. @@ -30,7 +93,7 @@ library StakeClaims { */ function lockStake( mapping(address => uint256) storage feesProvisionTracker, - mapping(bytes32 => IDataServiceFees.StakeClaim) storage claims, + mapping(bytes32 => StakeClaim) storage claims, mapping(address serviceProvider => LinkedList.List list) storage claimsLists, IHorizonStaking graphStaking, address _dataService, @@ -39,14 +102,14 @@ library StakeClaims { uint256 _tokens, uint256 _unlockTimestamp ) external { - require(_tokens != 0, IDataServiceFees.DataServiceFeesZeroTokens()); + require(_tokens != 0, StakeClaimsZeroTokens()); feesProvisionTracker.lock(graphStaking, _serviceProvider, _tokens, _delegationRatio); LinkedList.List storage claimsList = claimsLists[_serviceProvider]; // Save item and add to list bytes32 claimId = _buildStakeClaimId(_dataService, _serviceProvider, claimsList.nonce); - claims[claimId] = IDataServiceFees.StakeClaim({ + claims[claimId] = StakeClaim({ tokens: _tokens, createdAt: block.timestamp, releasableAt: _unlockTimestamp, @@ -55,7 +118,7 @@ library StakeClaims { if (claimsList.count != 0) claims[claimsList.tail].nextClaim = claimId; claimsList.addTail(claimId); - emit IDataServiceFees.StakeClaimLocked(_serviceProvider, claimId, _tokens, _unlockTimestamp); + emit StakeClaimLocked(_serviceProvider, claimId, _tokens, _unlockTimestamp); } /** @@ -70,12 +133,12 @@ library StakeClaims { */ function processStakeClaim( mapping(address serviceProvider => uint256 tokens) storage feesProvisionTracker, - mapping(bytes32 claimId => IDataServiceFees.StakeClaim claim) storage claims, + mapping(bytes32 claimId => StakeClaim claim) storage claims, bytes32 _claimId, bytes memory _acc ) external returns (bool, bytes memory) { - IDataServiceFees.StakeClaim memory claim = claims[_claimId]; - require(claim.createdAt != 0, IDataServiceFees.DataServiceFeesClaimNotFound(_claimId)); + StakeClaim memory claim = claims[_claimId]; + require(claim.createdAt != 0, StakeClaimsClaimNotFound(_claimId)); // early exit if (claim.releasableAt > block.timestamp) { @@ -87,7 +150,7 @@ library StakeClaims { // process feesProvisionTracker.release(serviceProvider, claim.tokens); - emit IDataServiceFees.StakeClaimReleased(serviceProvider, _claimId, claim.tokens, claim.releasableAt); + emit StakeClaimReleased(serviceProvider, _claimId, claim.tokens, claim.releasableAt); // encode _acc = abi.encode(tokensClaimed + claim.tokens, serviceProvider); @@ -100,10 +163,7 @@ library StakeClaims { * @param claims The mapping that stores stake claims by their ID * @param claimId The ID of the stake claim to delete */ - function deleteStakeClaim( - mapping(bytes32 claimId => IDataServiceFees.StakeClaim claim) storage claims, - bytes32 claimId - ) external { + function deleteStakeClaim(mapping(bytes32 claimId => StakeClaim claim) storage claims, bytes32 claimId) external { delete claims[claimId]; } @@ -115,7 +175,7 @@ library StakeClaims { * @return The next stake claim ID */ function getNextStakeClaim( - mapping(bytes32 claimId => IDataServiceFees.StakeClaim claim) storage claims, + mapping(bytes32 claimId => StakeClaim claim) storage claims, bytes32 claimId ) external view returns (bytes32) { return claims[claimId].nextClaim; diff --git a/packages/horizon/test/unit/data-service/extensions/DataServiceFees.t.sol b/packages/horizon/test/unit/data-service/extensions/DataServiceFees.t.sol index cd6e7bf46..6657ac315 100644 --- a/packages/horizon/test/unit/data-service/extensions/DataServiceFees.t.sol +++ b/packages/horizon/test/unit/data-service/extensions/DataServiceFees.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.27; import { HorizonStakingSharedTest } from "../../shared/horizon-staking/HorizonStakingShared.t.sol"; import { DataServiceImpFees } from "../implementations/DataServiceImpFees.sol"; -import { IDataServiceFees } from "../../../../contracts/data-service/interfaces/IDataServiceFees.sol"; +import { StakeClaims } from "../../../../contracts/data-service/libraries/StakeClaims.sol"; import { ProvisionTracker } from "../../../../contracts/data-service/libraries/ProvisionTracker.sol"; import { LinkedList } from "../../../../contracts/libraries/LinkedList.sol"; @@ -13,7 +13,7 @@ contract DataServiceFeesTest is HorizonStakingSharedTest { useIndexer useProvisionDataService(address(dataService), PROVISION_TOKENS, 0, 0) { - vm.expectRevert(abi.encodeWithSignature("DataServiceFeesZeroTokens()")); + vm.expectRevert(abi.encodeWithSignature("StakeClaimsZeroTokens()")); dataService.lockStake(users.indexer, 0); } @@ -132,6 +132,7 @@ contract DataServiceFeesTest is HorizonStakingSharedTest { uint256 stakeToLock; bytes32 predictedClaimId; } + function _assert_lockStake(address serviceProvider, uint256 tokens) private { // before state (bytes32 beforeHead, , uint256 beforeNonce, uint256 beforeCount) = dataService.claimsLists(serviceProvider); @@ -146,7 +147,7 @@ contract DataServiceFeesTest is HorizonStakingSharedTest { // it should emit a an event vm.expectEmit(); - emit IDataServiceFees.StakeClaimLocked( + emit StakeClaims.StakeClaimLocked( serviceProvider, calcValues.predictedClaimId, calcValues.stakeToLock, @@ -185,6 +186,7 @@ contract DataServiceFeesTest is HorizonStakingSharedTest { uint256 tokensReleased; bytes32 head; } + function _assert_releaseStake(address serviceProvider, uint256 numClaimsToRelease) private { // before state (bytes32 beforeHead, bytes32 beforeTail, uint256 beforeNonce, uint256 beforeCount) = dataService.claimsLists( @@ -208,14 +210,14 @@ contract DataServiceFeesTest is HorizonStakingSharedTest { break; } - emit IDataServiceFees.StakeClaimReleased(serviceProvider, calcValues.head, claimTokens, releasableAt); + emit StakeClaims.StakeClaimReleased(serviceProvider, calcValues.head, claimTokens, releasableAt); calcValues.head = nextClaim; calcValues.tokensReleased += claimTokens; calcValues.claimsCount++; } // it should emit a an event - emit IDataServiceFees.StakeClaimsReleased(serviceProvider, calcValues.claimsCount, calcValues.tokensReleased); + emit StakeClaims.StakeClaimsReleased(serviceProvider, calcValues.claimsCount, calcValues.tokensReleased); dataService.releaseStake(numClaimsToRelease); // after state diff --git a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol index ab9d657f5..ecaf82a16 100644 --- a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol @@ -10,7 +10,6 @@ import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interface import { IGraphTallyCollector } from "@graphprotocol/horizon/contracts/interfaces/IGraphTallyCollector.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { LinkedList } from "@graphprotocol/horizon/contracts/libraries/LinkedList.sol"; -import { IDataServiceFees } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataServiceFees.sol"; import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; import { StakeClaims } from "@graphprotocol/horizon/contracts/data-service/libraries/StakeClaims.sol"; @@ -405,7 +404,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { // Check the stake claim LinkedList.List memory claimsList = _getClaimList(_indexer); bytes32 claimId = _buildStakeClaimId(_indexer, claimsList.nonce - 1); - IDataServiceFees.StakeClaim memory stakeClaim = _getStakeClaim(claimId); + StakeClaims.StakeClaim memory stakeClaim = _getStakeClaim(claimId); uint64 disputePeriod = disputeManager.getDisputePeriod(); assertEq(stakeClaim.tokens, tokensToLock); assertEq(stakeClaim.createdAt, block.timestamp); @@ -517,9 +516,9 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { return StakeClaims.buildStakeClaimId(address(subgraphService), _indexer, _nonce); } - function _getStakeClaim(bytes32 _claimId) private view returns (IDataServiceFees.StakeClaim memory) { + function _getStakeClaim(bytes32 _claimId) private view returns (StakeClaims.StakeClaim memory) { (uint256 tokens, uint256 createdAt, uint256 releasableAt, bytes32 nextClaim) = subgraphService.claims(_claimId); - return IDataServiceFees.StakeClaim(tokens, createdAt, releasableAt, nextClaim); + return StakeClaims.StakeClaim(tokens, createdAt, releasableAt, nextClaim); } // This doesn't matter for testing because the metadata is not decoded onchain but it's expected to be of the form: From 54ef3ebf542524b48eb441ce6053553fe4f9dc2d Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 16:39:48 -0300 Subject: [PATCH 88/90] f: rename to AllocationHandler --- ...onManagerLib.sol => AllocationHandler.sol} | 2 +- .../contracts/utilities/AllocationManager.sol | 21 +++++++------------ 2 files changed, 9 insertions(+), 14 deletions(-) rename packages/subgraph-service/contracts/libraries/{AllocationManagerLib.sol => AllocationHandler.sol} (99%) diff --git a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol b/packages/subgraph-service/contracts/libraries/AllocationHandler.sol similarity index 99% rename from packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol rename to packages/subgraph-service/contracts/libraries/AllocationHandler.sol index a231ed1ba..48c7df070 100644 --- a/packages/subgraph-service/contracts/libraries/AllocationManagerLib.sol +++ b/packages/subgraph-service/contracts/libraries/AllocationHandler.sol @@ -16,7 +16,7 @@ import { Allocation } from "../libraries/Allocation.sol"; import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; import { AllocationManager } from "../utilities/AllocationManager.sol"; -library AllocationManagerLib { +library AllocationHandler { using ProvisionTracker for mapping(address => uint256); using Allocation for mapping(address => Allocation.State); using Allocation for Allocation.State; diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index ccc4676f9..b5acbcc78 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -12,7 +12,7 @@ import { Allocation } from "../libraries/Allocation.sol"; import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; -import { AllocationManagerLib } from "../libraries/AllocationManagerLib.sol"; +import { AllocationHandler } from "../libraries/AllocationHandler.sol"; /** * @title AllocationManager contract @@ -202,12 +202,12 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca bytes memory _allocationProof, uint32 _delegationRatio ) internal { - AllocationManagerLib.allocate( + AllocationHandler.allocate( _allocations, _legacyAllocations, allocationProvisionTracker, _subgraphAllocatedTokens, - AllocationManagerLib.AllocateParams({ + AllocationHandler.AllocateParams({ _allocationId: _allocationId, _allocationProof: _allocationProof, _encodeAllocationProof: _encodeAllocationProof(_indexer, _allocationId), @@ -257,11 +257,11 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca address _paymentsDestination ) internal returns (uint256) { return - AllocationManagerLib.presentPOI( + AllocationHandler.presentPOI( _allocations, allocationProvisionTracker, _subgraphAllocatedTokens, - AllocationManagerLib.PresentParams({ + AllocationHandler.PresentParams({ maxPOIStaleness: maxPOIStaleness, graphEpochManager: _graphEpochManager(), graphStaking: _graphStaking(), @@ -294,7 +294,7 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca * @param _delegationRatio The delegation ratio to consider when locking tokens */ function _resizeAllocation(address _allocationId, uint256 _tokens, uint32 _delegationRatio) internal { - AllocationManagerLib.resizeAllocation( + AllocationHandler.resizeAllocation( _allocations, allocationProvisionTracker, _subgraphAllocatedTokens, @@ -319,7 +319,7 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca * @param _forceClosed Whether the allocation was force closed */ function _closeAllocation(address _allocationId, bool _forceClosed) internal { - AllocationManagerLib.closeAllocation( + AllocationHandler.closeAllocation( _allocations, allocationProvisionTracker, _subgraphAllocatedTokens, @@ -357,11 +357,6 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca */ function _isOverAllocated(address _indexer, uint32 _delegationRatio) internal view returns (bool) { return - AllocationManagerLib.isOverAllocated( - allocationProvisionTracker, - _graphStaking(), - _indexer, - _delegationRatio - ); + AllocationHandler.isOverAllocated(allocationProvisionTracker, _graphStaking(), _indexer, _delegationRatio); } } From d4a2db0f91f14cad8e44d2a84e90497a2d36ca16 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 16:50:18 -0300 Subject: [PATCH 89/90] f: move errors et al to AllocationHandler --- .../contracts/libraries/AllocationHandler.sol | 143 ++++++++++++++++-- .../contracts/libraries/IndexingAgreement.sol | 4 +- .../contracts/utilities/AllocationManager.sol | 120 +-------------- .../unit/shared/SubgraphServiceShared.t.sol | 6 +- .../subgraphService/SubgraphService.t.sol | 10 +- .../subgraphService/allocation/resize.t.sol | 6 +- .../subgraphService/allocation/start.t.sol | 6 +- .../subgraphService/allocation/stop.t.sol | 1 - .../indexing-agreement/accept.t.sol | 4 +- .../indexing-agreement/collect.t.sol | 6 +- 10 files changed, 156 insertions(+), 150 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/AllocationHandler.sol b/packages/subgraph-service/contracts/libraries/AllocationHandler.sol index 48c7df070..a7a71f5d0 100644 --- a/packages/subgraph-service/contracts/libraries/AllocationHandler.sol +++ b/packages/subgraph-service/contracts/libraries/AllocationHandler.sol @@ -14,8 +14,15 @@ import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol" import { Allocation } from "../libraries/Allocation.sol"; import { LegacyAllocation } from "../libraries/LegacyAllocation.sol"; -import { AllocationManager } from "../utilities/AllocationManager.sol"; +/** + * @title AllocationHandler contract + * @notice A helper contract implementing allocation lifecycle management. + * Allows opening, resizing, and closing allocations, as well as collecting indexing rewards by presenting a Proof + * of Indexing (POI). + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ library AllocationHandler { using ProvisionTracker for mapping(address => uint256); using Allocation for mapping(address => Allocation.State); @@ -76,6 +83,122 @@ library AllocationHandler { address _paymentsDestination; } + /** + * @notice Emitted when an indexer creates an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param tokens The amount of tokens allocated + * @param currentEpoch The current epoch + */ + event AllocationCreated( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 tokens, + uint256 currentEpoch + ); + + /** + * @notice Emitted when an indexer collects indexing rewards for an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param tokensRewards The amount of tokens collected + * @param tokensIndexerRewards The amount of tokens collected for the indexer + * @param tokensDelegationRewards The amount of tokens collected for delegators + * @param poi The POI presented + * @param currentEpoch The current epoch + * @param poiMetadata The metadata associated with the POI + */ + event IndexingRewardsCollected( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 tokensRewards, + uint256 tokensIndexerRewards, + uint256 tokensDelegationRewards, + bytes32 poi, + bytes poiMetadata, + uint256 currentEpoch + ); + + /** + * @notice Emitted when an indexer resizes an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param newTokens The new amount of tokens allocated + * @param oldTokens The old amount of tokens allocated + */ + event AllocationResized( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 newTokens, + uint256 oldTokens + ); + + /** + * @dev Emitted when an indexer closes an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param tokens The amount of tokens allocated + * @param forceClosed Whether the allocation was force closed + */ + event AllocationClosed( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 tokens, + bool forceClosed + ); + + /** + * @notice Emitted when a legacy allocation is migrated into the subgraph service + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + */ + event LegacyAllocationMigrated( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId + ); + + /** + * @notice Emitted when the maximum POI staleness is updated + * @param maxPOIStaleness The max POI staleness in seconds + */ + event MaxPOIStalenessSet(uint256 maxPOIStaleness); + + /** + * @notice Thrown when an allocation proof is invalid + * Both `signer` and `allocationId` should match for a valid proof. + * @param signer The address that signed the proof + * @param allocationId The id of the allocation + */ + error AllocationHandlerInvalidAllocationProof(address signer, address allocationId); + + /** + * @notice Thrown when attempting to create an allocation with a zero allocation id + */ + error AllocationHandlerInvalidZeroAllocationId(); + + /** + * @notice Thrown when attempting to collect indexing rewards on a closed allocationl + * @param allocationId The id of the allocation + */ + error AllocationHandlerAllocationClosed(address allocationId); + + /** + * @notice Thrown when attempting to resize an allocation with the same size + * @param allocationId The id of the allocation + * @param tokens The amount of tokens + */ + error AllocationHandlerAllocationSameSize(address allocationId, uint256 tokens); + /** * @notice Create an allocation * @dev The `_allocationProof` is a 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationId)` @@ -98,7 +221,7 @@ library AllocationHandler { mapping(bytes32 subgraphDeploymentId => uint256 tokens) storage _subgraphAllocatedTokens, AllocateParams memory params ) external { - require(params._allocationId != address(0), AllocationManager.AllocationManagerInvalidZeroAllocationId()); + require(params._allocationId != address(0), AllocationHandler.AllocationHandlerInvalidZeroAllocationId()); _verifyAllocationProof(params._encodeAllocationProof, params._allocationId, params._allocationProof); @@ -124,7 +247,7 @@ library AllocationHandler { _subgraphAllocatedTokens[allocation.subgraphDeploymentId] + allocation.tokens; - emit AllocationManager.AllocationCreated( + emit AllocationHandler.AllocationCreated( params._indexer, params._allocationId, params._subgraphDeploymentId, @@ -166,7 +289,7 @@ library AllocationHandler { PresentParams memory params ) external returns (uint256) { Allocation.State memory allocation = _allocations.get(params._allocationId); - require(allocation.isOpen(), AllocationManager.AllocationManagerAllocationClosed(params._allocationId)); + require(allocation.isOpen(), AllocationHandler.AllocationHandlerAllocationClosed(params._allocationId)); // Mint indexing rewards if all conditions are met uint256 tokensRewards = (!allocation.isStale(params.maxPOIStaleness) && @@ -217,7 +340,7 @@ library AllocationHandler { } } - emit AllocationManager.IndexingRewardsCollected( + emit AllocationHandler.IndexingRewardsCollected( allocation.indexer, params._allocationId, allocation.subgraphDeploymentId, @@ -318,10 +441,10 @@ library AllocationHandler { uint32 _delegationRatio ) external { Allocation.State memory allocation = _allocations.get(_allocationId); - require(allocation.isOpen(), AllocationManager.AllocationManagerAllocationClosed(_allocationId)); + require(allocation.isOpen(), AllocationHandler.AllocationHandlerAllocationClosed(_allocationId)); require( _tokens != allocation.tokens, - AllocationManager.AllocationManagerAllocationSameSize(_allocationId, _tokens) + AllocationHandler.AllocationHandlerAllocationSameSize(_allocationId, _tokens) ); // Update provision tracker @@ -355,7 +478,7 @@ library AllocationHandler { _subgraphAllocatedTokens[allocation.subgraphDeploymentId] -= (oldTokens - _tokens); } - emit AllocationManager.AllocationResized( + emit AllocationHandler.AllocationResized( allocation.indexer, _allocationId, allocation.subgraphDeploymentId, @@ -421,7 +544,7 @@ library AllocationHandler { _subgraphAllocatedTokens[allocation.subgraphDeploymentId] - allocation.tokens; - emit AllocationManager.AllocationClosed( + emit AllocationHandler.AllocationClosed( allocation.indexer, _allocationId, allocation.subgraphDeploymentId, @@ -463,7 +586,7 @@ library AllocationHandler { address signer = ECDSA.recover(_encodeAllocationProof, _proof); require( signer == _allocationId, - AllocationManager.AllocationManagerInvalidAllocationProof(signer, _allocationId) + AllocationHandler.AllocationHandlerInvalidAllocationProof(signer, _allocationId) ); } } diff --git a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol index 0dc594575..a3669fffc 100644 --- a/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol +++ b/packages/subgraph-service/contracts/libraries/IndexingAgreement.sol @@ -5,7 +5,7 @@ import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGra import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; import { ISubgraphService } from "../interfaces/ISubgraphService.sol"; -import { AllocationManager } from "../utilities/AllocationManager.sol"; +import { AllocationHandler } from "../libraries/AllocationHandler.sol"; import { Directory } from "../utilities/Directory.sol"; import { Allocation } from "./Allocation.sol"; import { IndexingAgreementDecoder } from "./IndexingAgreementDecoder.sol"; @@ -659,7 +659,7 @@ library IndexingAgreement { allocation.indexer == _indexer, ISubgraphService.SubgraphServiceAllocationNotAuthorized(_indexer, _allocationId) ); - require(allocation.isOpen(), AllocationManager.AllocationManagerAllocationClosed(_allocationId)); + require(allocation.isOpen(), AllocationHandler.AllocationHandlerAllocationClosed(_allocationId)); return allocation; } diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index b5acbcc78..6ea422325 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -34,122 +34,6 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca bytes32 private constant EIP712_ALLOCATION_ID_PROOF_TYPEHASH = keccak256("AllocationIdProof(address indexer,address allocationId)"); - /** - * @notice Emitted when an indexer creates an allocation - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - * @param tokens The amount of tokens allocated - * @param currentEpoch The current epoch - */ - event AllocationCreated( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 tokens, - uint256 currentEpoch - ); - - /** - * @notice Emitted when an indexer collects indexing rewards for an allocation - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - * @param tokensRewards The amount of tokens collected - * @param tokensIndexerRewards The amount of tokens collected for the indexer - * @param tokensDelegationRewards The amount of tokens collected for delegators - * @param poi The POI presented - * @param currentEpoch The current epoch - * @param poiMetadata The metadata associated with the POI - */ - event IndexingRewardsCollected( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 tokensRewards, - uint256 tokensIndexerRewards, - uint256 tokensDelegationRewards, - bytes32 poi, - bytes poiMetadata, - uint256 currentEpoch - ); - - /** - * @notice Emitted when an indexer resizes an allocation - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - * @param newTokens The new amount of tokens allocated - * @param oldTokens The old amount of tokens allocated - */ - event AllocationResized( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 newTokens, - uint256 oldTokens - ); - - /** - * @dev Emitted when an indexer closes an allocation - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - * @param tokens The amount of tokens allocated - * @param forceClosed Whether the allocation was force closed - */ - event AllocationClosed( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 tokens, - bool forceClosed - ); - - /** - * @notice Emitted when a legacy allocation is migrated into the subgraph service - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - */ - event LegacyAllocationMigrated( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId - ); - - /** - * @notice Emitted when the maximum POI staleness is updated - * @param maxPOIStaleness The max POI staleness in seconds - */ - event MaxPOIStalenessSet(uint256 maxPOIStaleness); - - /** - * @notice Thrown when an allocation proof is invalid - * Both `signer` and `allocationId` should match for a valid proof. - * @param signer The address that signed the proof - * @param allocationId The id of the allocation - */ - error AllocationManagerInvalidAllocationProof(address signer, address allocationId); - - /** - * @notice Thrown when attempting to create an allocation with a zero allocation id - */ - error AllocationManagerInvalidZeroAllocationId(); - - /** - * @notice Thrown when attempting to collect indexing rewards on a closed allocationl - * @param allocationId The id of the allocation - */ - error AllocationManagerAllocationClosed(address allocationId); - - /** - * @notice Thrown when attempting to resize an allocation with the same size - * @param allocationId The id of the allocation - * @param tokens The amount of tokens - */ - error AllocationManagerAllocationSameSize(address allocationId, uint256 tokens); - /** * @notice Initializes the contract and parent contracts * @param _name The name to use for EIP712 domain separation @@ -175,7 +59,7 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca */ function _migrateLegacyAllocation(address _indexer, address _allocationId, bytes32 _subgraphDeploymentId) internal { _legacyAllocations.migrate(_indexer, _allocationId, _subgraphDeploymentId); - emit LegacyAllocationMigrated(_indexer, _allocationId, _subgraphDeploymentId); + emit AllocationHandler.LegacyAllocationMigrated(_indexer, _allocationId, _subgraphDeploymentId); } /** @@ -336,7 +220,7 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca */ function _setMaxPOIStaleness(uint256 _maxPOIStaleness) internal { maxPOIStaleness = _maxPOIStaleness; - emit MaxPOIStalenessSet(_maxPOIStaleness); + emit AllocationHandler.MaxPOIStalenessSet(_maxPOIStaleness); } /** diff --git a/packages/subgraph-service/test/unit/shared/SubgraphServiceShared.t.sol b/packages/subgraph-service/test/unit/shared/SubgraphServiceShared.t.sol index 9c018d282..8a1c403bb 100644 --- a/packages/subgraph-service/test/unit/shared/SubgraphServiceShared.t.sol +++ b/packages/subgraph-service/test/unit/shared/SubgraphServiceShared.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; import { Allocation } from "../../../contracts/libraries/Allocation.sol"; -import { AllocationManager } from "../../../contracts/utilities/AllocationManager.sol"; +import { AllocationHandler } from "../../../contracts/libraries/AllocationHandler.sol"; import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; @@ -103,7 +103,7 @@ abstract contract SubgraphServiceSharedTest is HorizonStakingSharedTest { vm.expectEmit(address(subgraphService)); emit IDataService.ServiceStarted(_indexer, _data); - emit AllocationManager.AllocationCreated(_indexer, allocationId, subgraphDeploymentId, tokens, currentEpoch); + emit AllocationHandler.AllocationCreated(_indexer, allocationId, subgraphDeploymentId, tokens, currentEpoch); // TODO: improve this uint256 accRewardsPerAllocatedToken = 0; @@ -141,7 +141,7 @@ abstract contract SubgraphServiceSharedTest is HorizonStakingSharedTest { ); vm.expectEmit(address(subgraphService)); - emit AllocationManager.AllocationClosed( + emit AllocationHandler.AllocationClosed( _indexer, allocationId, allocation.subgraphDeploymentId, diff --git a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol index ecaf82a16..4f3444c62 100644 --- a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol @@ -14,7 +14,7 @@ import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interface import { StakeClaims } from "@graphprotocol/horizon/contracts/data-service/libraries/StakeClaims.sol"; import { Allocation } from "../../../contracts/libraries/Allocation.sol"; -import { AllocationManager } from "../../../contracts/utilities/AllocationManager.sol"; +import { AllocationHandler } from "../../../contracts/libraries/AllocationHandler.sol"; import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; import { LegacyAllocation } from "../../../contracts/libraries/LegacyAllocation.sol"; import { SubgraphServiceSharedTest } from "../shared/SubgraphServiceShared.t.sol"; @@ -114,7 +114,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { } vm.expectEmit(address(subgraphService)); - emit AllocationManager.AllocationResized( + emit AllocationHandler.AllocationResized( _indexer, _allocationId, subgraphDeploymentId, @@ -156,7 +156,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { ); vm.expectEmit(address(subgraphService)); - emit AllocationManager.AllocationClosed( + emit AllocationHandler.AllocationClosed( allocation.indexer, _allocationId, allocation.subgraphDeploymentId, @@ -341,7 +341,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { indexingRewardsData.tokensIndexerRewards = paymentCollected - indexingRewardsData.tokensDelegationRewards; vm.expectEmit(address(subgraphService)); - emit AllocationManager.IndexingRewardsCollected( + emit AllocationHandler.IndexingRewardsCollected( allocation.indexer, allocationId, allocation.subgraphDeploymentId, @@ -468,7 +468,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { function _migrateLegacyAllocation(address _indexer, address _allocationId, bytes32 _subgraphDeploymentID) internal { vm.expectEmit(address(subgraphService)); - emit AllocationManager.LegacyAllocationMigrated(_indexer, _allocationId, _subgraphDeploymentID); + emit AllocationHandler.LegacyAllocationMigrated(_indexer, _allocationId, _subgraphDeploymentID); subgraphService.migrateLegacyAllocation(_indexer, _allocationId, _subgraphDeploymentID); diff --git a/packages/subgraph-service/test/unit/subgraphService/allocation/resize.t.sol b/packages/subgraph-service/test/unit/subgraphService/allocation/resize.t.sol index c9984bdba..ad70e3abc 100644 --- a/packages/subgraph-service/test/unit/subgraphService/allocation/resize.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/allocation/resize.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; import { Allocation } from "../../../../contracts/libraries/Allocation.sol"; -import { AllocationManager } from "../../../../contracts/utilities/AllocationManager.sol"; +import { AllocationHandler } from "../../../../contracts/libraries/AllocationHandler.sol"; import { SubgraphServiceTest } from "../SubgraphService.t.sol"; import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; @@ -86,7 +86,7 @@ contract SubgraphServiceAllocationResizeTest is SubgraphServiceTest { uint256 tokens ) public useIndexer useAllocation(tokens) { vm.expectRevert( - abi.encodeWithSelector(AllocationManager.AllocationManagerAllocationSameSize.selector, allocationID, tokens) + abi.encodeWithSelector(AllocationHandler.AllocationHandlerAllocationSameSize.selector, allocationID, tokens) ); subgraphService.resizeAllocation(users.indexer, allocationID, tokens); } @@ -99,7 +99,7 @@ contract SubgraphServiceAllocationResizeTest is SubgraphServiceTest { bytes memory data = abi.encode(allocationID); _stopService(users.indexer, data); vm.expectRevert( - abi.encodeWithSelector(AllocationManager.AllocationManagerAllocationClosed.selector, allocationID) + abi.encodeWithSelector(AllocationHandler.AllocationHandlerAllocationClosed.selector, allocationID) ); subgraphService.resizeAllocation(users.indexer, allocationID, resizeTokens); } diff --git a/packages/subgraph-service/test/unit/subgraphService/allocation/start.t.sol b/packages/subgraph-service/test/unit/subgraphService/allocation/start.t.sol index 2f132e132..0a762e958 100644 --- a/packages/subgraph-service/test/unit/subgraphService/allocation/start.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/allocation/start.t.sol @@ -8,7 +8,7 @@ import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/ import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; import { Allocation } from "../../../../contracts/libraries/Allocation.sol"; -import { AllocationManager } from "../../../../contracts/utilities/AllocationManager.sol"; +import { AllocationHandler } from "../../../../contracts/libraries/AllocationHandler.sol"; import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; import { LegacyAllocation } from "../../../../contracts/libraries/LegacyAllocation.sol"; import { SubgraphServiceTest } from "../SubgraphService.t.sol"; @@ -97,7 +97,7 @@ contract SubgraphServiceAllocationStartTest is SubgraphServiceTest { bytes32 digest = subgraphService.encodeAllocationProof(users.indexer, address(0)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationIDPrivateKey, digest); bytes memory data = abi.encode(subgraphDeployment, tokens, address(0), abi.encodePacked(r, s, v)); - vm.expectRevert(abi.encodeWithSelector(AllocationManager.AllocationManagerInvalidZeroAllocationId.selector)); + vm.expectRevert(abi.encodeWithSelector(AllocationHandler.AllocationHandlerInvalidZeroAllocationId.selector)); subgraphService.startService(users.indexer, data); } @@ -113,7 +113,7 @@ contract SubgraphServiceAllocationStartTest is SubgraphServiceTest { bytes memory data = abi.encode(subgraphDeployment, tokens, allocationID, abi.encodePacked(r, s, v)); vm.expectRevert( abi.encodeWithSelector( - AllocationManager.AllocationManagerInvalidAllocationProof.selector, + AllocationHandler.AllocationHandlerInvalidAllocationProof.selector, signer, allocationID ) diff --git a/packages/subgraph-service/test/unit/subgraphService/allocation/stop.t.sol b/packages/subgraph-service/test/unit/subgraphService/allocation/stop.t.sol index 2c4391cb2..456ed081f 100644 --- a/packages/subgraph-service/test/unit/subgraphService/allocation/stop.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/allocation/stop.t.sol @@ -8,7 +8,6 @@ import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/ import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; import { Allocation } from "../../../../contracts/libraries/Allocation.sol"; -import { AllocationManager } from "../../../../contracts/utilities/AllocationManager.sol"; import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; import { LegacyAllocation } from "../../../../contracts/libraries/LegacyAllocation.sol"; import { SubgraphServiceTest } from "../SubgraphService.t.sol"; 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 08be7c86d..29f83126c 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 @@ -8,7 +8,7 @@ import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces import { Allocation } from "../../../../contracts/libraries/Allocation.sol"; import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; import { IndexingAgreementDecoder } from "../../../../contracts/libraries/IndexingAgreementDecoder.sol"; -import { AllocationManager } from "../../../../contracts/utilities/AllocationManager.sol"; +import { AllocationHandler } from "../../../../contracts/libraries/AllocationHandler.sol"; import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; @@ -177,7 +177,7 @@ contract SubgraphServiceIndexingAgreementAcceptTest is SubgraphServiceIndexingAg subgraphService.stopService(indexerState.addr, abi.encode(indexerState.allocationId)); bytes memory expectedErr = abi.encodeWithSelector( - AllocationManager.AllocationManagerAllocationClosed.selector, + AllocationHandler.AllocationHandlerAllocationClosed.selector, indexerState.allocationId ); vm.expectRevert(expectedErr); diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol index 2063b0441..85c203b6e 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol @@ -9,7 +9,7 @@ import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/ import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; import { Allocation } from "../../../../contracts/libraries/Allocation.sol"; -import { AllocationManager } from "../../../../contracts/utilities/AllocationManager.sol"; +import { AllocationHandler } from "../../../../contracts/libraries/AllocationHandler.sol"; import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; @@ -210,7 +210,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA uint256 currentEpochBlock = epochManager.currentEpochBlock(); bytes memory expectedErr = abi.encodeWithSelector( - AllocationManager.AllocationManagerAllocationClosed.selector, + AllocationHandler.AllocationHandlerAllocationClosed.selector, indexerState.allocationId ); vm.expectRevert(expectedErr); @@ -237,7 +237,7 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA uint256 currentEpochBlock = epochManager.currentEpochBlock(); bytes memory expectedErr = abi.encodeWithSelector( - AllocationManager.AllocationManagerAllocationClosed.selector, + AllocationHandler.AllocationHandlerAllocationClosed.selector, indexerState.allocationId ); vm.expectRevert(expectedErr); From ba64efd3cf0b5f13ca89fb8a47827f83b16b2ff5 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 10 Jun 2025 16:55:57 -0300 Subject: [PATCH 90/90] f: fix 'this' ref in lib --- .../contracts/libraries/AllocationHandler.sol | 13 +++++++++---- .../contracts/utilities/AllocationManager.sol | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/subgraph-service/contracts/libraries/AllocationHandler.sol b/packages/subgraph-service/contracts/libraries/AllocationHandler.sol index a7a71f5d0..394430cad 100644 --- a/packages/subgraph-service/contracts/libraries/AllocationHandler.sol +++ b/packages/subgraph-service/contracts/libraries/AllocationHandler.sol @@ -76,6 +76,7 @@ library AllocationHandler { IHorizonStaking graphStaking; IRewardsManager graphRewardsManager; IGraphToken graphToken; + address dataService; address _allocationId; bytes32 _poi; bytes _poiMetadata; @@ -314,18 +315,22 @@ library AllocationHandler { // Distribute rewards to delegators uint256 delegatorCut = params.graphStaking.getDelegationFeeCut( allocation.indexer, - address(this), + params.dataService, IGraphPayments.PaymentTypes.IndexingRewards ); IHorizonStakingTypes.DelegationPool memory delegationPool = params.graphStaking.getDelegationPool( allocation.indexer, - address(this) + params.dataService ); // If delegation pool has no shares then we don't need to distribute rewards to delegators tokensDelegationRewards = delegationPool.shares > 0 ? tokensRewards.mulPPM(delegatorCut) : 0; if (tokensDelegationRewards > 0) { params.graphToken.approve(address(params.graphStaking), tokensDelegationRewards); - params.graphStaking.addToDelegationPool(allocation.indexer, address(this), tokensDelegationRewards); + params.graphStaking.addToDelegationPool( + allocation.indexer, + params.dataService, + tokensDelegationRewards + ); } // Distribute rewards to indexer @@ -333,7 +338,7 @@ library AllocationHandler { if (tokensIndexerRewards > 0) { if (params._paymentsDestination == address(0)) { params.graphToken.approve(address(params.graphStaking), tokensIndexerRewards); - params.graphStaking.stakeToProvision(allocation.indexer, address(this), tokensIndexerRewards); + params.graphStaking.stakeToProvision(allocation.indexer, params.dataService, tokensIndexerRewards); } else { params.graphToken.pushTokens(params._paymentsDestination, tokensIndexerRewards); } diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 6ea422325..bc64d0eb6 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -151,6 +151,7 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca graphStaking: _graphStaking(), graphRewardsManager: _graphRewardsManager(), graphToken: _graphToken(), + dataService: address(this), _allocationId: _allocationId, _poi: _poi, _poiMetadata: _poiMetadata,