Skip to content

[audit-13] fix: [TRST-M-2] improve _getCollectionInfo() #1212

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

Open
wants to merge 1 commit into
base: ma/indexing-payments-audit-fixes-12-R-part-I
Choose a base branch
from
Open
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
18 changes: 17 additions & 1 deletion packages/horizon/contracts/interfaces/IRecurringCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector {
ThirdParty
}

/// @notice Reasons why an agreement is not collectable
enum AgreementNotCollectableReason {
None,
InvalidAgreementState,
ZeroCollectionSeconds,
InvalidTemporalWindow
}

/**
* @notice A representation of a signed Recurring Collection Agreement (RCA)
* @param rca The Recurring Collection Agreement to be signed
Expand Down Expand Up @@ -303,6 +311,13 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector {
*/
error RecurringCollectorAgreementIncorrectState(bytes16 agreementId, AgreementState incorrectState);

/**
* @notice Thrown when an agreement is not collectable
* @param agreementId The agreement ID
* @param reason The reason why the agreement is not collectable
*/
error RecurringCollectorAgreementNotCollectable(bytes16 agreementId, AgreementNotCollectableReason reason);

/**
* @notice Thrown when accepting an agreement with an address that is not set
*/
Expand Down Expand Up @@ -440,10 +455,11 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector {
* @return isCollectable Whether the agreement is in a valid state that allows collection attempts,
* not that there are necessarily funds available to collect.
* @return collectionSeconds The valid collection duration in seconds (0 if not collectable)
* @return reason The reason why the agreement is not collectable (None if collectable)
*/
function getCollectionInfo(
AgreementData memory agreement
) external view returns (bool isCollectable, uint256 collectionSeconds);
) external view returns (bool isCollectable, uint256 collectionSeconds, AgreementNotCollectableReason reason);

/**
* @notice Generate a deterministic agreement ID from agreement parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
/// @inheritdoc IRecurringCollector
function getCollectionInfo(
AgreementData memory agreement
) external view returns (bool isCollectable, uint256 collectionSeconds) {
) external view returns (bool isCollectable, uint256 collectionSeconds, AgreementNotCollectableReason reason) {
return _getCollectionInfo(agreement);
}

Expand Down Expand Up @@ -309,14 +309,11 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
) private returns (uint256) {
AgreementData storage agreement = _getAgreementStorage(_params.agreementId);

// Check if agreement exists first (for unknown agreements)
(bool isCollectable, uint256 collectionSeconds) = _getCollectionInfo(agreement);
require(isCollectable, RecurringCollectorAgreementIncorrectState(_params.agreementId, agreement.state));

require(
collectionSeconds > 0,
RecurringCollectorZeroCollectionSeconds(_params.agreementId, block.timestamp, agreement.lastCollectionAt)
// Check if agreement is collectable first
(bool isCollectable, uint256 collectionSeconds, AgreementNotCollectableReason reason) = _getCollectionInfo(
agreement
);
require(isCollectable, RecurringCollectorAgreementNotCollectable(_params.agreementId, reason));

require(
msg.sender == agreement.dataService,
Expand Down Expand Up @@ -583,17 +580,17 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
* @param _agreement The agreement data
* @return isCollectable Whether the agreement can be collected from
* @return collectionSeconds The valid collection duration in seconds (0 if not collectable)
* @return reason The reason why the agreement is not collectable (None if collectable)
*/
function _getCollectionInfo(
AgreementData memory _agreement
) private view returns (bool isCollectable, uint256 collectionSeconds) {
) private view returns (bool, uint256, AgreementNotCollectableReason) {
// Check if agreement is in collectable state
isCollectable =
_agreement.state == AgreementState.Accepted ||
bool hasValidState = _agreement.state == AgreementState.Accepted ||
_agreement.state == AgreementState.CanceledByPayer;

if (!isCollectable) {
return (false, 0);
if (!hasValidState) {
return (false, 0, AgreementNotCollectableReason.InvalidAgreementState);
}

bool canceledOrElapsed = _agreement.state == AgreementState.CanceledByPayer ||
Expand All @@ -606,11 +603,14 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
uint256 collectionStart = _agreementCollectionStartAt(_agreement);

if (collectionEnd < collectionStart) {
return (false, 0);
return (false, 0, AgreementNotCollectableReason.InvalidTemporalWindow);
}

if (collectionStart == collectionEnd) {
return (false, 0, AgreementNotCollectableReason.ZeroCollectionSeconds);
}

collectionSeconds = collectionEnd - collectionStart;
return (isCollectable, collectionSeconds);
return (true, collectionEnd - collectionStart, AgreementNotCollectableReason.None);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ contract RecurringCollectorCollectTest is RecurringCollectorSharedTest {
bytes memory data = _generateCollectData(fuzzy.collectParams);

bytes memory expectedErr = abi.encodeWithSelector(
IRecurringCollector.RecurringCollectorAgreementIncorrectState.selector,
IRecurringCollector.RecurringCollectorAgreementNotCollectable.selector,
fuzzy.collectParams.agreementId,
IRecurringCollector.AgreementState.NotAccepted
IRecurringCollector.AgreementNotCollectableReason.InvalidAgreementState
);
vm.expectRevert(expectedErr);
vm.prank(dataService);
Expand All @@ -116,9 +116,9 @@ contract RecurringCollectorCollectTest is RecurringCollectorSharedTest {
bytes memory data = _generateCollectData(collectParams);

bytes memory expectedErr = abi.encodeWithSelector(
IRecurringCollector.RecurringCollectorAgreementIncorrectState.selector,
IRecurringCollector.RecurringCollectorAgreementNotCollectable.selector,
collectParams.agreementId,
IRecurringCollector.AgreementState.CanceledByServiceProvider
IRecurringCollector.AgreementNotCollectableReason.InvalidAgreementState
);
vm.expectRevert(expectedErr);
vm.prank(accepted.rca.dataService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ library IndexingAgreement {
IndexingAgreementNotAuthorized(params.agreementId, params.indexer)
);
// Get collection info from RecurringCollector (single source of truth for temporal logic)
(bool isCollectable, uint256 collectionSeconds) = _directory().recurringCollector().getCollectionInfo(
(bool isCollectable, uint256 collectionSeconds, ) = _directory().recurringCollector().getCollectionInfo(
wrapper.collectorAgreement
);
require(_isValid(wrapper) && isCollectable, IndexingAgreementNotCollectable(params.agreementId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA
);
_expectCollectCallAndEmit(data, indexerState, accepted, acceptedAgreementId, tokensCollected, entities, poi);

skip(1); // To make agreement collectable

subgraphService.collect(
indexerState.addr,
IGraphPayments.PaymentTypes.IndexingFee,
Expand Down Expand Up @@ -215,6 +217,8 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA
);
vm.expectRevert(expectedErr);

skip(1); // To make agreement collectable

subgraphService.collect(
indexerState.addr,
IGraphPayments.PaymentTypes.IndexingFee,
Expand Down