Skip to content

feat: implement Indexing Agreements #1134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: ma/indexing-payments
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions IndexingPaymentsTodo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Still pending

* Double-check it supports paying per byte instead of per entity and eventually a subgraph-gas metric. DONE: ~~Built-in upgrade path to indexing agreements v2~~
* Update Arbitration Charter to support disputing Indexing Fees. DONE: ~~Support `DisputeManager`~~
* Economics
* If service wants to collect more than collector allows. Collector limits but doesn't tell the service?
* Support for agreements that end up in `RecurringCollectorCollectionTooLate` or ways to avoid getting to that state.
* Should we deal with zero entities declared as a special case?
* 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.
* Reject Zero POIs?
* What happens if the escrow doesn't have enough funds? Since you can't collect that means you lose out forever?
* Don't pay for entities on initial collection?
* Should we set a different param for initial collection time max? Some subgraphs take a lot to catch up.
* 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)?
* Double check cancelation policy. Who can cancel when? Right now is either party at any time.
* Expose a function that indexers can use to calculate the tokens to be collected and other collection params?
* Support a way for gateway to shop an agreement around? Deadline + dedup key? So only one agreement with the dedupe key can be accepted?
* Maybe check that the epoch the indexer is sending is the one the transaction will be run in?
* Check upgrade conditions. Support indexing agreement upgadeability, so that there is a mechanism to adjust the rates without having to cancel and start over.
* If an indexer closes an allocation, what should happen to the accepeted agreement?
* test_SubgraphService_CollectIndexingFee_Integration fails with PaymentsEscrowInconsistentCollection
* Reduce the number of errors declared and returned
* 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~~
* Missing events for accept, cancel, upgrade RCAs.
275 changes: 275 additions & 0 deletions packages/horizon/contracts/interfaces/IRecurringCollector.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
// 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 {
/// @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
uint256 acceptDeadline;
// The duration of the RCA in seconds
uint256 duration;
// 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
uint256 upgradeDeadline;
// The duration of the agreement in seconds
uint256 duration;
// 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
uint256 acceptedAt;
// The timestamp when the agreement was last collected at
uint256 lastCollectionAt;
// The duration of the agreement in seconds
uint256 duration;
// 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;
}

/// @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 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,
bytes32 collectionId,
uint256 tokens,
uint256 dataServiceCut
);

/**
* Thrown when calling cancel() for an agreement not owned by the calling data service
* @param agreementId The agreement ID
* @param dataService The address of the data service
*/
error RecurringCollectorDataServiceNotAuthorized(bytes16 agreementId, address dataService);

/**
* Thrown when calling accept() for an agreement with an elapsed acceptance deadline
* @param elapsedAt The timestamp when the acceptance deadline elapsed
*/
error RecurringCollectorAgreementAcceptanceElapsed(uint256 elapsedAt);

/**
* Thrown when calling upgrade() for an agreement with an elapsed upgrade deadline
* @param elapsedAt The timestamp when the upgrade deadline elapsed
*/
error RecurringCollectorAgreementUpgradeElapsed(uint256 elapsedAt);

/**
* Thrown when the signer is invalid
*/
error RecurringCollectorInvalidSigner();

/**
* Thrown when the payment type is not IndexingFee
* @param paymentType The provided payment type
*/
error RecurringCollectorInvalidPaymentType(IGraphPayments.PaymentTypes paymentType);

/**
* Thrown when the caller is not the data service the RCA was issued to
* @param caller The address of the caller
* @param dataService The address of the data service
*/
error RecurringCollectorCallerNotDataService(address caller, address dataService);

/**
* Thrown when calling collect() with invalid data
* @param data The invalid data
*/
error RecurringCollectorInvalidCollectData(bytes data);

/**
* Thrown when calling accept() for an already accepted agreement
* @param agreementId The agreement ID
*/
error RecurringCollectorAgreementAlreadyAccepted(bytes16 agreementId);

/**
* Thrown when calling cancel() for a never accepted agreement
* @param agreementId The agreement ID
*/
error RecurringCollectorAgreementNeverAccepted(bytes16 agreementId);

/**
* Thrown when calling collect() on an invalid agreement
* @param agreementId The agreement ID
* @param acceptedAt The agreement accepted timestamp
*/
error RecurringCollectorAgreementInvalid(bytes16 agreementId, uint256 acceptedAt);

/**
* Thrown when calling collect() on an elapsed agreement
* @param agreementId The agreement ID
* @param agreementEnd The agreement end timestamp
*/
error RecurringCollectorAgreementElapsed(bytes16 agreementId, uint256 agreementEnd);

/**
* 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, uint256 secondsSinceLast, uint256 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, uint256 secondsSinceLast, uint256 maxSeconds);

/**
* Thrown when calling collect() too late
* @param agreementId The agreement ID
* @param tokens The amount of tokens to collect
* @param maxTokens The maximum amount of tokens allowed to collect
*/
error RecurringCollectorCollectAmountTooHigh(bytes16 agreementId, uint256 tokens, uint256 maxTokens);

/**
* @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.
*/
function cancel(bytes16 agreementId) 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);
}
Loading