Skip to content

Commit 504fff1

Browse files
authored
chore(Horizon): add signers to TAPCollector (#1060)
* chore(Horizon): add signers to TAPCollector * fix: collect multiple queries test * fix: rename events and change parameter order to make it consistent * fix: lint issues * chore: add tap collector signer unit tests
1 parent 971a5c5 commit 504fff1

File tree

18 files changed

+827
-215
lines changed

18 files changed

+827
-215
lines changed

packages/horizon/contracts/interfaces/IPaymentsCollector.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ interface IPaymentsCollector {
3636
* @notice Initiate a payment collection through the payments protocol
3737
* @dev This function should require the caller to present some form of evidence of the payer's debt to
3838
* the receiver. The collector should validate this evidence and, if valid, collect the payment.
39+
* Requirements:
40+
* - The caller must be the data service the RAV was issued to
41+
* - The signer of the RAV must be authorized to sign for the payer
3942
*
4043
* Emits a {PaymentCollected} event
4144
*

packages/horizon/contracts/interfaces/ITAPCollector.sol

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ import { IPaymentsCollector } from "./IPaymentsCollector.sol";
1111
* payments using a TAP RAV (Receipt Aggregate Voucher).
1212
*/
1313
interface ITAPCollector is IPaymentsCollector {
14+
/// @notice Details for a payer-signer pair
15+
/// @dev Signers can be removed only after a thawing period
16+
struct PayerAuthorization {
17+
// Payer the signer is authorized to sign for
18+
address payer;
19+
// Timestamp at which thawing period ends (zero if not thawing)
20+
uint256 thawEndTimestamp;
21+
}
22+
1423
/// @notice The Receipt Aggregate Voucher (RAV) struct
1524
struct ReceiptAggregateVoucher {
1625
// The address of the data service the RAV was issued to
@@ -34,6 +43,36 @@ interface ITAPCollector is IPaymentsCollector {
3443
bytes signature;
3544
}
3645

46+
/**
47+
* @notice Emitted when a signer is authorized to sign RAVs for a payer
48+
* @param payer The address of the payer authorizing the signer
49+
* @param authorizedSigner The address of the authorized signer
50+
*/
51+
event SignerAuthorized(address indexed payer, address indexed authorizedSigner);
52+
53+
/**
54+
* @notice Emitted when a signer is thawed to be removed from the authorized signers list
55+
* @param payer The address of the payer thawing the signer
56+
* @param authorizedSigner The address of the signer to thaw
57+
* @param thawEndTimestamp The timestamp at which the thawing period ends
58+
*/
59+
event SignerThawing(address indexed payer, address indexed authorizedSigner, uint256 thawEndTimestamp);
60+
61+
/**
62+
* @dev Emitted when the thawing of a signer is cancelled
63+
* @param payer The address of the payer cancelling the thawing
64+
* @param authorizedSigner The address of the authorized signer
65+
* @param thawEndTimestamp The timestamp at which the thawing period ends
66+
*/
67+
event SignerThawCanceled(address indexed payer, address indexed authorizedSigner, uint256 thawEndTimestamp);
68+
69+
/**
70+
* @dev Emitted when a authorized signer has been revoked
71+
* @param payer The address of the payer revoking the signer
72+
* @param authorizedSigner The address of the authorized signer
73+
*/
74+
event SignerRevoked(address indexed payer, address indexed authorizedSigner);
75+
3776
/**
3877
* @notice Emitted when a RAV is collected
3978
* @param payer The address of the payer
@@ -54,6 +93,50 @@ interface ITAPCollector is IPaymentsCollector {
5493
bytes signature
5594
);
5695

96+
/**
97+
* Thrown when the signer is already authorized
98+
* @param authorizingPayer The address of the payer authorizing the signer
99+
* @param signer The address of the signer
100+
*/
101+
error TAPCollectorSignerAlreadyAuthorized(address authorizingPayer, address signer);
102+
103+
/**
104+
* Thrown when the signer proof deadline is invalid
105+
* @param proofDeadline The deadline for the proof provided by the signer
106+
* @param currentTimestamp The current timestamp
107+
*/
108+
error TAPCollectorInvalidSignerProofDeadline(uint256 proofDeadline, uint256 currentTimestamp);
109+
110+
/**
111+
* Thrown when the signer proof is invalid
112+
*/
113+
error TAPCollectorInvalidSignerProof();
114+
115+
/**
116+
* Thrown when the signer is not authorized by the payer
117+
* @param payer The address of the payer
118+
* @param signer The address of the signer
119+
*/
120+
error TAPCollectorSignerNotAuthorizedByPayer(address payer, address signer);
121+
122+
/**
123+
* Thrown when the signer is not thawing
124+
* @param signer The address of the signer
125+
*/
126+
error TAPCollectorSignerNotThawing(address signer);
127+
128+
/**
129+
* Thrown when the signer is still thawing
130+
* @param currentTimestamp The current timestamp
131+
* @param thawEndTimestamp The timestamp at which the thawing period ends
132+
*/
133+
error TAPCollectorSignerStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp);
134+
135+
/**
136+
* Thrown when the RAV signer is invalid
137+
*/
138+
error TAPCollectorInvalidRAVSigner();
139+
57140
/**
58141
* Thrown when the caller is not the data service the RAV was issued to
59142
* @param caller The address of the caller
@@ -69,6 +152,54 @@ interface ITAPCollector is IPaymentsCollector {
69152
*/
70153
error TAPCollectorInconsistentRAVTokens(uint256 tokens, uint256 tokensCollected);
71154

155+
/**
156+
* @notice Authorize a signer to sign on behalf of the payer
157+
* @dev Requirements:
158+
* - `signer` must not be already authorized
159+
* - `proofDeadline` must be greater than the current timestamp
160+
* - `proof` must be a valid signature from the signer being authorized
161+
*
162+
* Emits an {SignerAuthorized} event
163+
* @param signer The addres of the authorized signer
164+
* @param proofDeadline The deadline for the proof provided by the signer
165+
* @param proof The proof provided by the signer to be authorized by the payer, consists of (chainID, proof deadline, sender address)
166+
*/
167+
function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external;
168+
169+
/**
170+
* @notice Starts thawing a signer to be removed from the authorized signers list
171+
* @dev Thawing a signer alerts receivers that signatures from that signer will soon be deemed invalid.
172+
* Receivers without existing signed receipts or RAVs from this signer should treat them as unauthorized.
173+
* Those with existing signed documents from this signer should work towards settling their engagements.
174+
* Once a signer is thawed, they should be viewed as revoked regardless of their revocation status.
175+
* Requirements:
176+
* - `signer` must be authorized by the payer calling this function
177+
*
178+
* Emits a {SignerThawing} event
179+
* @param signer The address of the signer to thaw
180+
*/
181+
function thawSigner(address signer) external;
182+
183+
/**
184+
* @notice Stops thawing a signer.
185+
* @dev Requirements:
186+
* - `signer` must be thawing and authorized by the payer calling this function
187+
*
188+
* Emits a {SignerThawCanceled} event
189+
* @param signer The address of the signer to cancel thawing
190+
*/
191+
function cancelThawSigner(address signer) external;
192+
193+
/**
194+
* @notice Revokes a signer from the authorized signers list if thawed.
195+
* @dev Requirements:
196+
* - `signer` must be thawed and authorized by the payer calling this function
197+
*
198+
* Emits a {SignerRevoked} event
199+
* @param signer The address of the signer
200+
*/
201+
function revokeAuthorizedSigner(address signer) external;
202+
72203
/**
73204
* @dev Recovers the signer address of a signed ReceiptAggregateVoucher (RAV).
74205
* @param signedRAV The SignedRAV containing the RAV and its signature.

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

Lines changed: 137 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { PPMMath } from "../../libraries/PPMMath.sol";
99

1010
import { GraphDirectory } from "../../utilities/GraphDirectory.sol";
1111
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
12+
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
1213

1314
/**
1415
* @title TAPCollector contract
@@ -29,21 +30,89 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector {
2930
"ReceiptAggregateVoucher(address dataService,address serviceProvider,uint64 timestampNs,uint128 valueAggregate,bytes metadata)"
3031
);
3132

33+
/// @notice Authorization details for payer-signer pairs
34+
mapping(address signer => PayerAuthorization authorizedSigner) public authorizedSigners;
35+
3236
/// @notice Tracks the amount of tokens already collected by a data service from a payer to a receiver
3337
mapping(address dataService => mapping(address receiver => mapping(address payer => uint256 tokens)))
3438
public tokensCollected;
3539

40+
/// @notice The duration (in seconds) in which a signer is thawing before they can be revoked
41+
uint256 public immutable REVOKE_SIGNER_THAWING_PERIOD;
42+
3643
/**
3744
* @notice Constructs a new instance of the TAPVerifier contract.
3845
* @param eip712Name The name of the EIP712 domain.
3946
* @param eip712Version The version of the EIP712 domain.
4047
* @param controller The address of the Graph controller.
48+
* @param revokeSignerThawingPeriod The duration (in seconds) in which a signer is thawing before they can be revoked.
4149
*/
4250
constructor(
4351
string memory eip712Name,
4452
string memory eip712Version,
45-
address controller
46-
) EIP712(eip712Name, eip712Version) GraphDirectory(controller) {}
53+
address controller,
54+
uint256 revokeSignerThawingPeriod
55+
) EIP712(eip712Name, eip712Version) GraphDirectory(controller) {
56+
REVOKE_SIGNER_THAWING_PERIOD = revokeSignerThawingPeriod;
57+
}
58+
59+
/**
60+
* See {ITAPCollector.authorizeSigner}.
61+
*/
62+
function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external override {
63+
require(
64+
authorizedSigners[signer].payer == address(0),
65+
TAPCollectorSignerAlreadyAuthorized(authorizedSigners[signer].payer, signer)
66+
);
67+
68+
_verifyAuthorizedSignerProof(proof, proofDeadline, signer);
69+
70+
authorizedSigners[signer].payer = msg.sender;
71+
authorizedSigners[signer].thawEndTimestamp = 0;
72+
emit SignerAuthorized(msg.sender, signer);
73+
}
74+
75+
/**
76+
* See {ITAPCollector.thawSigner}.
77+
*/
78+
function thawSigner(address signer) external override {
79+
PayerAuthorization storage authorization = authorizedSigners[signer];
80+
81+
require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(msg.sender, signer));
82+
83+
authorization.thawEndTimestamp = block.timestamp + REVOKE_SIGNER_THAWING_PERIOD;
84+
emit SignerThawing(msg.sender, signer, authorization.thawEndTimestamp);
85+
}
86+
87+
/**
88+
* See {ITAPCollector.cancelThawSigner}.
89+
*/
90+
function cancelThawSigner(address signer) external override {
91+
PayerAuthorization storage authorization = authorizedSigners[signer];
92+
93+
require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(msg.sender, signer));
94+
require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer));
95+
96+
authorization.thawEndTimestamp = 0;
97+
emit SignerThawCanceled(msg.sender, signer, 0);
98+
}
99+
100+
/**
101+
* See {ITAPCollector.revokeAuthorizedSigner}.
102+
*/
103+
function revokeAuthorizedSigner(address signer) external override {
104+
PayerAuthorization storage authorization = authorizedSigners[signer];
105+
106+
require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(msg.sender, signer));
107+
require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer));
108+
require(
109+
authorization.thawEndTimestamp <= block.timestamp,
110+
TAPCollectorSignerStillThawing(block.timestamp, authorization.thawEndTimestamp)
111+
);
112+
113+
delete authorizedSigners[signer];
114+
emit SignerRevoked(msg.sender, signer);
115+
}
47116

48117
/**
49118
* @notice Initiate a payment collection through the payments protocol
@@ -58,59 +127,73 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector {
58127
TAPCollectorCallerNotDataService(msg.sender, signedRAV.rav.dataService)
59128
);
60129

61-
address dataService = signedRAV.rav.dataService;
62-
address payer = _recoverRAVSigner(signedRAV);
63-
address receiver = signedRAV.rav.serviceProvider;
130+
address signer = _recoverRAVSigner(signedRAV);
131+
require(authorizedSigners[signer].payer != address(0), TAPCollectorInvalidRAVSigner());
132+
133+
return _collect(paymentType, authorizedSigners[signer].payer, signedRAV, dataServiceCut);
134+
}
135+
136+
/**
137+
* @notice See {ITAPCollector.recoverRAVSigner}
138+
*/
139+
function recoverRAVSigner(SignedRAV calldata signedRAV) external view override returns (address) {
140+
return _recoverRAVSigner(signedRAV);
141+
}
142+
143+
/**
144+
* @notice See {ITAPCollector.encodeRAV}
145+
*/
146+
function encodeRAV(ReceiptAggregateVoucher calldata rav) external view returns (bytes32) {
147+
return _encodeRAV(rav);
148+
}
64149

65-
uint256 tokensRAV = signedRAV.rav.valueAggregate;
66-
uint256 tokensAlreadyCollected = tokensCollected[dataService][receiver][payer];
150+
/**
151+
* @notice See {ITAPCollector.collect}
152+
*/
153+
function _collect(
154+
IGraphPayments.PaymentTypes _paymentType,
155+
address _payer,
156+
SignedRAV memory _signedRAV,
157+
uint256 _dataServiceCut
158+
) private returns (uint256) {
159+
address dataService = _signedRAV.rav.dataService;
160+
address receiver = _signedRAV.rav.serviceProvider;
161+
162+
uint256 tokensRAV = _signedRAV.rav.valueAggregate;
163+
uint256 tokensAlreadyCollected = tokensCollected[dataService][receiver][_payer];
67164
require(
68165
tokensRAV > tokensAlreadyCollected,
69166
TAPCollectorInconsistentRAVTokens(tokensRAV, tokensAlreadyCollected)
70167
);
71168

72169
uint256 tokensToCollect = tokensRAV - tokensAlreadyCollected;
73-
uint256 tokensDataService = tokensToCollect.mulPPM(dataServiceCut);
170+
uint256 tokensDataService = tokensToCollect.mulPPM(_dataServiceCut);
74171

75172
if (tokensToCollect > 0) {
173+
tokensCollected[dataService][receiver][_payer] = tokensRAV;
76174
_graphPaymentsEscrow().collect(
77-
paymentType,
78-
payer,
175+
_paymentType,
176+
_payer,
79177
receiver,
80178
tokensToCollect,
81179
dataService,
82180
tokensDataService
83181
);
84-
tokensCollected[dataService][receiver][payer] = tokensRAV;
85182
}
86183

87-
emit PaymentCollected(paymentType, payer, receiver, tokensToCollect, dataService, tokensDataService);
184+
emit PaymentCollected(_paymentType, _payer, receiver, tokensToCollect, dataService, tokensDataService);
88185
emit RAVCollected(
89-
payer,
186+
_payer,
90187
dataService,
91188
receiver,
92-
signedRAV.rav.timestampNs,
93-
signedRAV.rav.valueAggregate,
94-
signedRAV.rav.metadata,
95-
signedRAV.signature
189+
_signedRAV.rav.timestampNs,
190+
_signedRAV.rav.valueAggregate,
191+
_signedRAV.rav.metadata,
192+
_signedRAV.signature
96193
);
97194
return tokensToCollect;
98195
}
99196

100-
/**
101-
* @notice See {ITAPCollector.recoverRAVSigner}
102-
*/
103-
function recoverRAVSigner(SignedRAV calldata signedRAV) external view override returns (address) {
104-
return _recoverRAVSigner(signedRAV);
105-
}
106-
107-
/**
108-
* @notice See {ITAPCollector.encodeRAV}
109-
*/
110-
function encodeRAV(ReceiptAggregateVoucher calldata rav) external view returns (bytes32) {
111-
return _encodeRAV(rav);
112-
}
113-
114197
/**
115198
* @notice See {ITAPCollector.recoverRAVSigner}
116199
*/
@@ -137,4 +220,27 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector {
137220
)
138221
);
139222
}
223+
224+
/**
225+
* @notice Verify the proof provided by the payer authorizing the signer
226+
* @param _proof The proof provided by the payer authorizing the signer
227+
* @param _proofDeadline The deadline by which the proof must be verified
228+
* @param _signer The signer to be authorized
229+
*/
230+
function _verifyAuthorizedSignerProof(bytes calldata _proof, uint256 _proofDeadline, address _signer) private view {
231+
// Verify that the proofDeadline has not passed
232+
require(
233+
_proofDeadline > block.timestamp,
234+
TAPCollectorInvalidSignerProofDeadline(_proofDeadline, block.timestamp)
235+
);
236+
237+
// Generate the hash of the payer's address
238+
bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, _proofDeadline, msg.sender));
239+
240+
// Generate the digest to be signed by the signer
241+
bytes32 digest = MessageHashUtils.toEthSignedMessageHash(messageHash);
242+
243+
// Verify that the recovered signer matches the expected signer
244+
require(ECDSA.recover(digest, _proof) == _signer, TAPCollectorInvalidSignerProof());
245+
}
140246
}

0 commit comments

Comments
 (0)