Skip to content

Commit e7d19d7

Browse files
fix: [TRST-L-3] Add deterministic agreement ID
1 parent 117672c commit e7d19d7

File tree

18 files changed

+447
-170
lines changed

18 files changed

+447
-170
lines changed

packages/horizon/contracts/interfaces/IRecurringCollector.sol

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector {
4040

4141
/**
4242
* @notice The Recurring Collection Agreement (RCA)
43-
* @param agreementId The agreement ID of the RCA
4443
* @param deadline The deadline for accepting the RCA
4544
* @param endsAt The timestamp when the agreement ends
4645
* @param payer The address of the payer the RCA was issued by
@@ -52,11 +51,11 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector {
5251
* except for the first collection
5352
* @param minSecondsPerCollection The minimum amount of seconds that must pass between collections
5453
* @param maxSecondsPerCollection The maximum amount of seconds that can pass between collections
54+
* @param nonce A unique nonce for preventing collisions (user-chosen)
5555
* @param metadata Arbitrary metadata to extend functionality if a data service requires it
5656
*
5757
*/
5858
struct RecurringCollectionAgreement {
59-
bytes16 agreementId;
6059
uint64 deadline;
6160
uint64 endsAt;
6261
address payer;
@@ -66,6 +65,7 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector {
6665
uint256 maxOngoingTokensPerSecond;
6766
uint32 minSecondsPerCollection;
6867
uint32 maxSecondsPerCollection;
68+
uint256 nonce;
6969
bytes metadata;
7070
}
7171

@@ -372,8 +372,9 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector {
372372
/**
373373
* @dev Accept an indexing agreement.
374374
* @param signedRCA The signed Recurring Collection Agreement which is to be accepted.
375+
* @return agreementId The deterministically generated agreement ID
375376
*/
376-
function accept(SignedRCA calldata signedRCA) external;
377+
function accept(SignedRCA calldata signedRCA) external returns (bytes16 agreementId);
377378

378379
/**
379380
* @dev Cancel an indexing agreement.
@@ -432,4 +433,21 @@ interface IRecurringCollector is IAuthorizable, IPaymentsCollector {
432433
function getCollectionInfo(
433434
AgreementData memory agreement
434435
) external view returns (bool isCollectable, uint256 collectionSeconds);
436+
437+
/**
438+
* @notice Generate a deterministic agreement ID from agreement parameters
439+
* @param payer The address of the payer
440+
* @param dataService The address of the data service
441+
* @param serviceProvider The address of the service provider
442+
* @param deadline The deadline for accepting the agreement
443+
* @param nonce A unique nonce for preventing collisions
444+
* @return agreementId The deterministically generated agreement ID
445+
*/
446+
function generateAgreementId(
447+
address payer,
448+
address dataService,
449+
address serviceProvider,
450+
uint64 deadline,
451+
uint256 nonce
452+
) external pure returns (bytes16 agreementId);
435453
}

packages/horizon/contracts/payments/collectors/RecurringCollector.sol

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
2929
/// @notice The EIP712 typehash for the RecurringCollectionAgreement struct
3030
bytes32 public constant EIP712_RCA_TYPEHASH =
3131
keccak256(
32-
"RecurringCollectionAgreement(bytes16 agreementId,uint256 deadline,uint256 endsAt,address payer,address dataService,address serviceProvider,uint256 maxInitialTokens,uint256 maxOngoingTokensPerSecond,uint32 minSecondsPerCollection,uint32 maxSecondsPerCollection,bytes metadata)"
32+
"RecurringCollectionAgreement(uint64 deadline,uint64 endsAt,address payer,address dataService,address serviceProvider,uint256 maxInitialTokens,uint256 maxOngoingTokensPerSecond,uint32 minSecondsPerCollection,uint32 maxSecondsPerCollection,uint256 nonce,bytes metadata)"
3333
);
3434

3535
/// @notice The EIP712 typehash for the RecurringCollectionAgreementUpdate struct
@@ -75,8 +75,16 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
7575
* See {IRecurringCollector.accept}.
7676
* @dev Caller must be the data service the RCA was issued to.
7777
*/
78-
function accept(SignedRCA calldata signedRCA) external {
79-
require(signedRCA.rca.agreementId != bytes16(0), RecurringCollectorAgreementIdZero());
78+
function accept(SignedRCA calldata signedRCA) external returns (bytes16) {
79+
bytes16 agreementId = _generateAgreementId(
80+
signedRCA.rca.payer,
81+
signedRCA.rca.dataService,
82+
signedRCA.rca.serviceProvider,
83+
signedRCA.rca.deadline,
84+
signedRCA.rca.nonce
85+
);
86+
87+
require(agreementId != bytes16(0), RecurringCollectorAgreementIdZero());
8088
require(
8189
msg.sender == signedRCA.rca.dataService,
8290
RecurringCollectorUnauthorizedCaller(msg.sender, signedRCA.rca.dataService)
@@ -102,11 +110,11 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
102110
signedRCA.rca.maxSecondsPerCollection
103111
);
104112

105-
AgreementData storage agreement = _getAgreementStorage(signedRCA.rca.agreementId);
113+
AgreementData storage agreement = _getAgreementStorage(agreementId);
106114
// check that the agreement is not already accepted
107115
require(
108116
agreement.state == AgreementState.NotAccepted,
109-
RecurringCollectorAgreementIncorrectState(signedRCA.rca.agreementId, agreement.state)
117+
RecurringCollectorAgreementIncorrectState(agreementId, agreement.state)
110118
);
111119

112120
// accept the agreement
@@ -126,14 +134,16 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
126134
agreement.dataService,
127135
agreement.payer,
128136
agreement.serviceProvider,
129-
signedRCA.rca.agreementId,
137+
agreementId,
130138
agreement.acceptedAt,
131139
agreement.endsAt,
132140
agreement.maxInitialTokens,
133141
agreement.maxOngoingTokensPerSecond,
134142
agreement.minSecondsPerCollection,
135143
agreement.maxSecondsPerCollection
136144
);
145+
146+
return agreementId;
137147
}
138148

139149
/**
@@ -261,6 +271,17 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
261271
return _getCollectionInfo(agreement);
262272
}
263273

274+
/// @inheritdoc IRecurringCollector
275+
function generateAgreementId(
276+
address payer,
277+
address dataService,
278+
address serviceProvider,
279+
uint64 deadline,
280+
uint256 nonce
281+
) external pure returns (bytes16) {
282+
return _generateAgreementId(payer, dataService, serviceProvider, deadline, nonce);
283+
}
284+
264285
/**
265286
* @notice Decodes the collect data.
266287
* @param data The encoded collect parameters.
@@ -457,7 +478,6 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
457478
keccak256(
458479
abi.encode(
459480
EIP712_RCA_TYPEHASH,
460-
_rca.agreementId,
461481
_rca.deadline,
462482
_rca.endsAt,
463483
_rca.payer,
@@ -467,6 +487,7 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
467487
_rca.maxOngoingTokensPerSecond,
468488
_rca.minSecondsPerCollection,
469489
_rca.maxSecondsPerCollection,
490+
_rca.nonce,
470491
keccak256(_rca.metadata)
471492
)
472493
)
@@ -590,4 +611,23 @@ contract RecurringCollector is EIP712, GraphDirectory, Authorizable, IRecurringC
590611
function _agreementCollectionStartAt(AgreementData memory _agreement) private pure returns (uint256) {
591612
return _agreement.lastCollectionAt > 0 ? _agreement.lastCollectionAt : _agreement.acceptedAt;
592613
}
614+
615+
/**
616+
* @notice Internal function to generate deterministic agreement ID
617+
* @param _payer The address of the payer
618+
* @param _dataService The address of the data service
619+
* @param _serviceProvider The address of the service provider
620+
* @param _deadline The deadline for accepting the agreement
621+
* @param _nonce A unique nonce for preventing collisions
622+
* @return agreementId The deterministically generated agreement ID
623+
*/
624+
function _generateAgreementId(
625+
address _payer,
626+
address _dataService,
627+
address _serviceProvider,
628+
uint64 _deadline,
629+
uint256 _nonce
630+
) private pure returns (bytes16) {
631+
return bytes16(keccak256(abi.encode(_payer, _dataService, _serviceProvider, _deadline, _nonce)));
632+
}
593633
}

packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorHelper.t.sol

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,47 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder {
4545
return signedRCAU;
4646
}
4747

48-
function generateSignedRCAUWithCorrectNonce(
48+
function generateSignedRCAUForAgreement(
49+
bytes16 agreementId,
4950
IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau,
5051
uint256 signerPrivateKey
5152
) public view returns (IRecurringCollector.SignedRCAU memory) {
5253
// Automatically set the correct nonce based on current agreement state
53-
IRecurringCollector.AgreementData memory agreement = collector.getAgreement(rcau.agreementId);
54+
IRecurringCollector.AgreementData memory agreement = collector.getAgreement(agreementId);
5455
rcau.nonce = agreement.updateNonce + 1;
5556

5657
return generateSignedRCAU(rcau, signerPrivateKey);
5758
}
5859

60+
function generateSignedRCAUWithCorrectNonce(
61+
IRecurringCollector.RecurringCollectionAgreementUpdate memory rcau,
62+
uint256 signerPrivateKey
63+
) public view returns (IRecurringCollector.SignedRCAU memory) {
64+
// This is kept for backwards compatibility but should not be used with new interface
65+
// since we can't determine agreementId without it being passed separately
66+
return generateSignedRCAU(rcau, signerPrivateKey);
67+
}
68+
69+
function generateSignedRCAWithCalculatedId(
70+
IRecurringCollector.RecurringCollectionAgreement memory rca,
71+
uint256 signerPrivateKey
72+
) public view returns (IRecurringCollector.SignedRCA memory, bytes16) {
73+
// Ensure we have sensible values
74+
rca = sensibleRCA(rca);
75+
76+
// Calculate the agreement ID
77+
bytes16 agreementId = collector.generateAgreementId(
78+
rca.payer,
79+
rca.dataService,
80+
rca.serviceProvider,
81+
rca.deadline,
82+
rca.nonce
83+
);
84+
85+
IRecurringCollector.SignedRCA memory signedRCA = generateSignedRCA(rca, signerPrivateKey);
86+
return (signedRCA, agreementId);
87+
}
88+
5989
function withElapsedAcceptDeadline(
6090
IRecurringCollector.RecurringCollectionAgreement memory rca
6191
) public view returns (IRecurringCollector.RecurringCollectionAgreement memory) {
@@ -76,11 +106,15 @@ contract RecurringCollectorHelper is AuthorizableHelper, Bounder {
76106
function sensibleRCA(
77107
IRecurringCollector.RecurringCollectionAgreement memory rca
78108
) public view returns (IRecurringCollector.RecurringCollectionAgreement memory) {
79-
vm.assume(rca.agreementId != bytes16(0));
80109
vm.assume(rca.dataService != address(0));
81110
vm.assume(rca.payer != address(0));
82111
vm.assume(rca.serviceProvider != address(0));
83112

113+
// Ensure we have a nonce if it's zero
114+
if (rca.nonce == 0) {
115+
rca.nonce = 1;
116+
}
117+
84118
rca.minSecondsPerCollection = _sensibleMinSecondsPerCollection(rca.minSecondsPerCollection);
85119
rca.maxSecondsPerCollection = _sensibleMaxSecondsPerCollection(
86120
rca.maxSecondsPerCollection,

packages/horizon/test/unit/payments/recurring-collector/accept.t.sol

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ contract RecurringCollectorAcceptTest is RecurringCollectorSharedTest {
2020
IRecurringCollector.SignedRCA memory fuzzySignedRCA,
2121
uint256 unboundedSkip
2222
) public {
23-
vm.assume(fuzzySignedRCA.rca.agreementId != bytes16(0));
23+
// Generate deterministic agreement ID for validation
24+
bytes16 agreementId = _recurringCollector.generateAgreementId(
25+
fuzzySignedRCA.rca.payer,
26+
fuzzySignedRCA.rca.dataService,
27+
fuzzySignedRCA.rca.serviceProvider,
28+
fuzzySignedRCA.rca.deadline,
29+
fuzzySignedRCA.rca.nonce
30+
);
31+
vm.assume(agreementId != bytes16(0));
2432
skip(boundSkip(unboundedSkip, 1, type(uint64).max - block.timestamp));
2533
fuzzySignedRCA.rca = _recurringCollectorHelper.withElapsedAcceptDeadline(fuzzySignedRCA.rca);
2634

@@ -35,11 +43,13 @@ contract RecurringCollectorAcceptTest is RecurringCollectorSharedTest {
3543
}
3644

3745
function test_Accept_Revert_WhenAlreadyAccepted(FuzzyTestAccept calldata fuzzyTestAccept) public {
38-
(IRecurringCollector.SignedRCA memory accepted, ) = _sensibleAuthorizeAndAccept(fuzzyTestAccept);
46+
(IRecurringCollector.SignedRCA memory accepted, , bytes16 agreementId) = _sensibleAuthorizeAndAccept(
47+
fuzzyTestAccept
48+
);
3949

4050
bytes memory expectedErr = abi.encodeWithSelector(
4151
IRecurringCollector.RecurringCollectorAgreementIncorrectState.selector,
42-
accepted.rca.agreementId,
52+
agreementId,
4353
IRecurringCollector.AgreementState.Accepted
4454
);
4555
vm.expectRevert(expectedErr);

packages/horizon/test/unit/payments/recurring-collector/cancel.t.sol

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,34 @@ contract RecurringCollectorCancelTest is RecurringCollectorSharedTest {
1313
/* solhint-disable graph/func-name-mixedcase */
1414

1515
function test_Cancel(FuzzyTestAccept calldata fuzzyTestAccept, uint8 unboundedCanceler) public {
16-
_sensibleAuthorizeAndAccept(fuzzyTestAccept);
17-
_cancel(fuzzyTestAccept.rca, _fuzzyCancelAgreementBy(unboundedCanceler));
16+
(IRecurringCollector.SignedRCA memory accepted, , bytes16 agreementId) = _sensibleAuthorizeAndAccept(
17+
fuzzyTestAccept
18+
);
19+
20+
_cancel(accepted.rca, agreementId, _fuzzyCancelAgreementBy(unboundedCanceler));
1821
}
1922

2023
function test_Cancel_Revert_WhenNotAccepted(
2124
IRecurringCollector.RecurringCollectionAgreement memory fuzzyRCA,
2225
uint8 unboundedCanceler
2326
) public {
27+
// Generate deterministic agreement ID
28+
bytes16 agreementId = _recurringCollector.generateAgreementId(
29+
fuzzyRCA.payer,
30+
fuzzyRCA.dataService,
31+
fuzzyRCA.serviceProvider,
32+
fuzzyRCA.deadline,
33+
fuzzyRCA.nonce
34+
);
35+
2436
bytes memory expectedErr = abi.encodeWithSelector(
2537
IRecurringCollector.RecurringCollectorAgreementIncorrectState.selector,
26-
fuzzyRCA.agreementId,
38+
agreementId,
2739
IRecurringCollector.AgreementState.NotAccepted
2840
);
2941
vm.expectRevert(expectedErr);
3042
vm.prank(fuzzyRCA.dataService);
31-
_recurringCollector.cancel(fuzzyRCA.agreementId, _fuzzyCancelAgreementBy(unboundedCanceler));
43+
_recurringCollector.cancel(agreementId, _fuzzyCancelAgreementBy(unboundedCanceler));
3244
}
3345

3446
function test_Cancel_Revert_WhenNotDataService(
@@ -38,16 +50,16 @@ contract RecurringCollectorCancelTest is RecurringCollectorSharedTest {
3850
) public {
3951
vm.assume(fuzzyTestAccept.rca.dataService != notDataService);
4052

41-
_sensibleAuthorizeAndAccept(fuzzyTestAccept);
53+
(, , bytes16 agreementId) = _sensibleAuthorizeAndAccept(fuzzyTestAccept);
4254

4355
bytes memory expectedErr = abi.encodeWithSelector(
4456
IRecurringCollector.RecurringCollectorDataServiceNotAuthorized.selector,
45-
fuzzyTestAccept.rca.agreementId,
57+
agreementId,
4658
notDataService
4759
);
4860
vm.expectRevert(expectedErr);
4961
vm.prank(notDataService);
50-
_recurringCollector.cancel(fuzzyTestAccept.rca.agreementId, _fuzzyCancelAgreementBy(unboundedCanceler));
62+
_recurringCollector.cancel(agreementId, _fuzzyCancelAgreementBy(unboundedCanceler));
5163
}
5264
/* solhint-enable graph/func-name-mixedcase */
5365
}

0 commit comments

Comments
 (0)