From 41403b7eab0ad576556119e65896f0bda48c00b7 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Thu, 10 Oct 2024 18:43:47 -0300 Subject: [PATCH 1/5] chore(Horizon): add signers to TAPCollector --- .../interfaces/IPaymentsCollector.sol | 3 + .../contracts/interfaces/ITAPCollector.sol | 131 +++++++++++++ .../payments/collectors/TAPCollector.sol | 182 ++++++++++++++---- packages/horizon/test/GraphBase.t.sol | 2 +- .../horizon/test/escrow/GraphEscrow.t.sol | 6 - .../horizon/test/payments/TAPCollector.t.sol | 56 +++++- .../PaymentsEscrowShared.t.sol | 10 + packages/horizon/test/utils/Constants.sol | 2 + .../test/SubgraphBaseTest.t.sol | 2 +- .../subgraphService/collect/query/query.t.sol | 27 ++- .../subgraph-service/test/utils/Constants.sol | 2 + 11 files changed, 361 insertions(+), 62 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IPaymentsCollector.sol b/packages/horizon/contracts/interfaces/IPaymentsCollector.sol index bcd67df0a..85d09d59f 100644 --- a/packages/horizon/contracts/interfaces/IPaymentsCollector.sol +++ b/packages/horizon/contracts/interfaces/IPaymentsCollector.sol @@ -36,6 +36,9 @@ interface IPaymentsCollector { * @notice Initiate a payment collection through the payments protocol * @dev This function should require the caller to present some form of evidence of the payer's debt to * the receiver. The collector should validate this evidence and, if valid, collect the payment. + * Requirements: + * - The caller must be the data service the RAV was issued to + * - The signer of the RAV must be authorized to sign for the payer * * Emits a {PaymentCollected} event * diff --git a/packages/horizon/contracts/interfaces/ITAPCollector.sol b/packages/horizon/contracts/interfaces/ITAPCollector.sol index 72a8dc0bc..6d92a8e5c 100644 --- a/packages/horizon/contracts/interfaces/ITAPCollector.sol +++ b/packages/horizon/contracts/interfaces/ITAPCollector.sol @@ -11,6 +11,15 @@ import { IPaymentsCollector } from "./IPaymentsCollector.sol"; * payments using a TAP RAV (Receipt Aggregate Voucher). */ interface ITAPCollector is IPaymentsCollector { + /// @notice Details for a payer-signer pair + /// @dev Signers can be removed only after a thawing period + struct PayerAuthorization { + // Payer the signer is authorized to sign for + address payer; + // Timestamp at which thawing period ends (zero if not thawing) + uint256 thawEndTimestamp; + } + /// @notice The Receipt Aggregate Voucher (RAV) struct struct ReceiptAggregateVoucher { // The address of the data service the RAV was issued to @@ -34,6 +43,36 @@ interface ITAPCollector is IPaymentsCollector { bytes signature; } + /** + * @notice Emitted when a signer is authorized to sign RAVs for a payer + * @param authorizedSigner The address of the authorized signer + * @param payer The address of the payer authorizing the signer + */ + event AuthorizeSigner(address indexed authorizedSigner, address indexed payer); + + /** + * @notice Emitted when a signer is thawed to be removed from the authorized signers list + * @param payer The address of the payer thawing the signer + * @param authorizedSigner The address of the signer to thaw + * @param thawEndTimestamp The timestamp at which the thawing period ends + */ + event ThawSigner(address indexed payer, address indexed authorizedSigner, uint256 thawEndTimestamp); + + /** + * @dev Emitted when the thawing of a signer is cancelled + * @param payer The address of the payer cancelling the thawing + * @param authorizedSigner The address of the authorized signer + * @param thawEndTimestamp The timestamp at which the thawing period ends + */ + event CancelThawSigner(address indexed payer, address indexed authorizedSigner, uint256 thawEndTimestamp); + + /** + * @dev Emitted when a authorized signer has been revoked + * @param payer The address of the payer revoking the signer + * @param authorizedSigner The address of the authorized signer + */ + event RevokeAuthorizedSigner(address indexed payer, address indexed authorizedSigner); + /** * @notice Emitted when a RAV is collected * @param payer The address of the payer @@ -54,6 +93,50 @@ interface ITAPCollector is IPaymentsCollector { bytes signature ); + /** + * Thrown when the signer is already authorized + * @param signer The address of the signer + * @param authorizingPayer The address of the payer authorizing the signer + */ + error TAPCollectorSignerAlreadyAuthorized(address signer, address authorizingPayer); + + /** + * Thrown when the signer proof deadline is invalid + * @param proofDeadline The deadline for the proof provided by the signer + * @param currentTimestamp The current timestamp + */ + error TAPCollectorInvalidSignerProofDeadline(uint256 proofDeadline, uint256 currentTimestamp); + + /** + * Thrown when the signer proof is invalid + */ + error TAPCollectorInvalidSignerProof(); + + /** + * Thrown when the signer is not authorized by the payer + * @param signer The address of the signer + * @param payer The address of the payer + */ + error TAPCollectorSignerNotAuthorizedByPayer(address signer, address payer); + + /** + * Thrown when the signer is not thawing + * @param signer The address of the signer + */ + error TAPCollectorSignerNotThawing(address signer); + + /** + * Thrown when the signer is still thawing + * @param currentTimestamp The current timestamp + * @param thawEndTimestamp The timestamp at which the thawing period ends + */ + error TAPCollectorSignerStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp); + + /** + * Thrown when the RAV signer is invalid + */ + error TAPCollectorInvalidRAVSigner(); + /** * Thrown when the caller is not the data service the RAV was issued to * @param caller The address of the caller @@ -69,6 +152,54 @@ interface ITAPCollector is IPaymentsCollector { */ error TAPCollectorInconsistentRAVTokens(uint256 tokens, uint256 tokensCollected); + /** + * @notice Authorize a signer to sign on behalf of the payer + * @dev Requirements: + * - `signer` must not be already authorized + * - `proofDeadline` must be greater than the current timestamp + * - `proof` must be a valid signature from the signer being authorized + * + * Emits an {AuthorizedSigner} event + * @param signer The addres of the authorized signer + * @param proofDeadline The deadline for the proof provided by the signer + * @param proof The proof provided by the signer to be authorized by the payer, consists of (chainID, proof deadline, sender address) + */ + function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external; + + /** + * @notice Starts thawing a signer to be removed from the authorized signers list + * @dev Thawing a signer alerts receivers that signatures from that signer will soon be deemed invalid. + * Receivers without existing signed receipts or RAVs from this signer should treat them as unauthorized. + * Those with existing signed documents from this signer should work towards settling their engagements. + * Once a signer is thawed, they should be viewed as revoked regardless of their revocation status. + * Requirements: + * - `signer` must be authorized by the payer calling this function + * + * Emits a {ThawSigner} event + * @param signer The address of the signer to thaw + */ + function thawSigner(address signer) external; + + /** + * @notice Stops thawing a signer. + * @dev Requirements: + * - `signer` must be thawing and authorized by the payer calling this function + * + * Emits a {CancelThawSigner} event + * @param signer The address of the signer to cancel thawing + */ + function cancelThawSigner(address signer) external; + + /** + * @notice Revokes a signer from the authorized signers list if thawed. + * @dev Requirements: + * - `signer` must be thawed and authorized by the payer calling this function + * + * Emits a {RevokeAuthorizedSigner} event + * @param signer The address of the signer + */ + function revokeAuthorizedSigner(address signer) external; + /** * @dev Recovers the signer address of a signed ReceiptAggregateVoucher (RAV). * @param signedRAV The SignedRAV containing the RAV and its signature. diff --git a/packages/horizon/contracts/payments/collectors/TAPCollector.sol b/packages/horizon/contracts/payments/collectors/TAPCollector.sol index 91293af09..0c9457d86 100644 --- a/packages/horizon/contracts/payments/collectors/TAPCollector.sol +++ b/packages/horizon/contracts/payments/collectors/TAPCollector.sol @@ -9,6 +9,7 @@ import { PPMMath } from "../../libraries/PPMMath.sol"; import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; /** * @title TAPCollector contract @@ -29,21 +30,89 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { "ReceiptAggregateVoucher(address dataService,address serviceProvider,uint64 timestampNs,uint128 valueAggregate,bytes metadata)" ); + /// @notice Authorization details for payer-signer pairs + mapping(address signer => PayerAuthorization authorizedSigner) public authorizedSigners; + /// @notice Tracks the amount of tokens already collected by a data service from a payer to a receiver mapping(address dataService => mapping(address receiver => mapping(address payer => uint256 tokens))) public tokensCollected; + /// @notice The duration (in seconds) in which a signer is thawing before they can be revoked + uint256 public immutable revokeSignerThawingPeriod; + /** * @notice Constructs a new instance of the TAPVerifier 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 - ) EIP712(eip712Name, eip712Version) GraphDirectory(controller) {} + address controller, + uint256 _revokeSignerThawingPeriod + ) EIP712(eip712Name, eip712Version) GraphDirectory(controller) { + revokeSignerThawingPeriod = _revokeSignerThawingPeriod; + } + + /** + * See {ITAPCollector.authorizeSigner}. + */ + function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external override { + require( + authorizedSigners[signer].payer == address(0), + TAPCollectorSignerAlreadyAuthorized(signer, authorizedSigners[signer].payer) + ); + + verifyAuthorizedSignerProof(proof, proofDeadline, signer); + + authorizedSigners[signer].payer = msg.sender; + authorizedSigners[signer].thawEndTimestamp = 0; + emit AuthorizeSigner(signer, msg.sender); + } + + /** + * See {ITAPCollector.thawSigner}. + */ + function thawSigner(address signer) external override { + PayerAuthorization storage authorization = authorizedSigners[signer]; + + require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(signer, msg.sender)); + + authorization.thawEndTimestamp = block.timestamp + revokeSignerThawingPeriod; + emit ThawSigner(msg.sender, signer, authorization.thawEndTimestamp); + } + + /** + * See {ITAPCollector.cancelThawSigner}. + */ + function cancelThawSigner(address signer) external override { + PayerAuthorization storage authorization = authorizedSigners[signer]; + + require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(signer, msg.sender)); + require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer)); + + authorization.thawEndTimestamp = 0; + emit CancelThawSigner(msg.sender, signer, 0); + } + + /** + * See {ITAPCollector.revokeAuthorizedSigner}. + */ + function revokeAuthorizedSigner(address signer) external override { + PayerAuthorization storage authorization = authorizedSigners[signer]; + + require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(signer, msg.sender)); + require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer)); + require( + authorization.thawEndTimestamp <= block.timestamp, + TAPCollectorSignerStillThawing(block.timestamp, authorization.thawEndTimestamp) + ); + + delete authorizedSigners[signer]; + emit RevokeAuthorizedSigner(msg.sender, signer); + } /** * @notice Initiate a payment collection through the payments protocol @@ -58,43 +127,10 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { TAPCollectorCallerNotDataService(msg.sender, signedRAV.rav.dataService) ); - address dataService = signedRAV.rav.dataService; - address payer = _recoverRAVSigner(signedRAV); - address receiver = signedRAV.rav.serviceProvider; + address signer = _recoverRAVSigner(signedRAV); + require(authorizedSigners[signer].payer != address(0), TAPCollectorInvalidRAVSigner()); - uint256 tokensRAV = signedRAV.rav.valueAggregate; - uint256 tokensAlreadyCollected = tokensCollected[dataService][receiver][payer]; - require( - tokensRAV > tokensAlreadyCollected, - TAPCollectorInconsistentRAVTokens(tokensRAV, tokensAlreadyCollected) - ); - - uint256 tokensToCollect = tokensRAV - tokensAlreadyCollected; - uint256 tokensDataService = tokensToCollect.mulPPM(dataServiceCut); - - if (tokensToCollect > 0) { - _graphPaymentsEscrow().collect( - paymentType, - payer, - receiver, - tokensToCollect, - dataService, - tokensDataService - ); - tokensCollected[dataService][receiver][payer] = tokensRAV; - } - - emit PaymentCollected(paymentType, payer, receiver, tokensToCollect, dataService, tokensDataService); - emit RAVCollected( - payer, - dataService, - receiver, - signedRAV.rav.timestampNs, - signedRAV.rav.valueAggregate, - signedRAV.rav.metadata, - signedRAV.signature - ); - return tokensToCollect; + return _collect(paymentType, authorizedSigners[signer].payer, signedRAV, dataServiceCut); } /** @@ -137,4 +173,74 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { ) ); } + + /** + * @notice Verify the proof provided by the payer authorizing the signer + * @param proof The proof provided by the payer authorizing the signer + * @param proofDeadline The deadline by which the proof must be verified + * @param signer The signer to be authorized + */ + function verifyAuthorizedSignerProof(bytes calldata proof, uint256 proofDeadline, address signer) private view { + // Verify that the proofDeadline has not passed + require( + proofDeadline > block.timestamp, + TAPCollectorInvalidSignerProofDeadline(proofDeadline, block.timestamp) + ); + + // Generate the hash of the payer's address + bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, proofDeadline, msg.sender)); + + // Generate the digest to be signed by the signer + bytes32 digest = MessageHashUtils.toEthSignedMessageHash(messageHash); + + // Verify that the recovered signer matches the expected signer + require(ECDSA.recover(digest, proof) == signer, TAPCollectorInvalidSignerProof()); + } + + /** + * @notice See {ITAPCollector.collect} + */ + function _collect( + IGraphPayments.PaymentTypes paymentType, + address payer, + SignedRAV memory signedRAV, + uint256 dataServiceCut + ) private returns (uint256) { + address dataService = signedRAV.rav.dataService; + address receiver = signedRAV.rav.serviceProvider; + + uint256 tokensRAV = signedRAV.rav.valueAggregate; + uint256 tokensAlreadyCollected = tokensCollected[dataService][receiver][payer]; + require( + tokensRAV > tokensAlreadyCollected, + TAPCollectorInconsistentRAVTokens(tokensRAV, tokensAlreadyCollected) + ); + + uint256 tokensToCollect = tokensRAV - tokensAlreadyCollected; + uint256 tokensDataService = tokensToCollect.mulPPM(dataServiceCut); + + if (tokensToCollect > 0) { + _graphPaymentsEscrow().collect( + paymentType, + payer, + receiver, + tokensToCollect, + dataService, + tokensDataService + ); + tokensCollected[dataService][receiver][payer] = tokensRAV; + } + + emit PaymentCollected(paymentType, payer, receiver, tokensToCollect, dataService, tokensDataService); + emit RAVCollected( + payer, + dataService, + receiver, + signedRAV.rav.timestampNs, + signedRAV.rav.valueAggregate, + signedRAV.rav.metadata, + signedRAV.signature + ); + return tokensToCollect; + } } diff --git a/packages/horizon/test/GraphBase.t.sol b/packages/horizon/test/GraphBase.t.sol index 16c89e5c5..7aa44d6f5 100644 --- a/packages/horizon/test/GraphBase.t.sol +++ b/packages/horizon/test/GraphBase.t.sol @@ -185,7 +185,7 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { subgraphDataServiceLegacyAddress ); - tapCollector = new TAPCollector("TAPCollector", "1", address(controller)); + tapCollector = new TAPCollector("TAPCollector", "1", address(controller), revokeSignerThawingPeriod); resetPrank(users.governor); proxyAdmin.upgrade(stakingProxy, address(stakingBase)); diff --git a/packages/horizon/test/escrow/GraphEscrow.t.sol b/packages/horizon/test/escrow/GraphEscrow.t.sol index a472162e9..d3ffd21da 100644 --- a/packages/horizon/test/escrow/GraphEscrow.t.sol +++ b/packages/horizon/test/escrow/GraphEscrow.t.sol @@ -12,12 +12,6 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { * MODIFIERS */ - modifier useGateway() { - vm.startPrank(users.gateway); - _; - vm.stopPrank(); - } - modifier approveEscrow(uint256 tokens) { _approveEscrow(tokens); _; diff --git a/packages/horizon/test/payments/TAPCollector.t.sol b/packages/horizon/test/payments/TAPCollector.t.sol index e1c177282..0771d5f33 100644 --- a/packages/horizon/test/payments/TAPCollector.t.sol +++ b/packages/horizon/test/payments/TAPCollector.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { IHorizonStakingMain } from "../../contracts/interfaces/internal/IHorizonStakingMain.sol"; import { ITAPCollector } from "../../contracts/interfaces/ITAPCollector.sol"; import { IPaymentsCollector } from "../../contracts/interfaces/IPaymentsCollector.sol"; @@ -20,10 +21,29 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest address payer; uint256 payerPrivateKey; + /* + * MODIFIERS + */ + + modifier usePayerSigner() { + uint256 proofDeadline = block.timestamp + 1; + bytes memory signerProof = _getSignerProof(proofDeadline, payerPrivateKey); + _authorizeSigner(payer, proofDeadline, signerProof); + _; + } + /* * HELPERS */ + function _getSignerProof(uint256 _proofDeadline, uint256 _signer) private returns (bytes memory) { + (, address msgSender, ) = vm.readCallers(); + bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, _proofDeadline, msgSender)); + bytes32 proofToDigest = MessageHashUtils.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signer, proofToDigest); + return abi.encodePacked(r, s, v); + } + function _getQueryFeeEncodedData(address indexer, address collector, uint128 tokens) private view returns (bytes memory) { ITAPCollector.ReceiptAggregateVoucher memory rav = _getRAV(indexer, collector, tokens); bytes32 messageHash = tapCollector.encodeRAV(rav); @@ -48,10 +68,28 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest }); } + /* + * ACTIONS + */ + + function _authorizeSigner(address signer, uint256 proofDeadline, bytes memory proof) private { + (, address msgSender, ) = vm.readCallers(); + + vm.expectEmit(address(tapCollector)); + emit ITAPCollector.AuthorizeSigner(signer, msgSender); + + tapCollector.authorizeSigner(signer, proofDeadline, proof); + + (address _payer, uint256 thawEndTimestamp) = tapCollector.authorizedSigners(signer); + assertEq(_payer, msgSender); + assertEq(thawEndTimestamp, 0); + } + function _collect(IGraphPayments.PaymentTypes _paymentType, bytes memory _data) private { (ITAPCollector.SignedRAV memory signedRAV, uint256 dataServiceCut) = abi.decode(_data, (ITAPCollector.SignedRAV, uint256)); bytes32 messageHash = tapCollector.encodeRAV(signedRAV.rav); - address _payer = ECDSA.recover(messageHash, signedRAV.signature); + address _signer = ECDSA.recover(messageHash, signedRAV.signature); + (address _payer, ) = tapCollector.authorizedSigners(_signer); uint256 tokensAlreadyCollected = tapCollector.tokensCollected(signedRAV.rav.dataService, signedRAV.rav.serviceProvider, _payer); uint256 tokensToCollect = signedRAV.rav.valueAggregate - tokensAlreadyCollected; uint256 tokensDataService = tokensToCollect.mulPPM(dataServiceCut); @@ -90,30 +128,28 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest super.setUp(); (payer, payerPrivateKey) = makeAddrAndKey("payer"); vm.label({ account: payer, newLabel: "payer" }); - deal({ token: address(token), to: payer, give: type(uint256).max }); } /* * TESTS */ - function testCollect(uint256 tokens) public { + function testCollect(uint256 tokens) public useGateway usePayerSigner { tokens = bound(tokens, 1, type(uint128).max); - resetPrank(payer); _approveCollector(address(tapCollector), tokens); _depositTokens(address(tapCollector), users.indexer, tokens); + bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); resetPrank(users.verifier); _collect(IGraphPayments.PaymentTypes.QueryFee, data); } - function testCollect_Multiple(uint256 tokens, uint8 steps) public { + function testCollect_Multiple(uint256 tokens, uint8 steps) public useGateway usePayerSigner { steps = uint8(bound(steps, 1, 100)); tokens = bound(tokens, steps, type(uint128).max); - resetPrank(payer); _approveCollector(address(tapCollector), tokens); _depositTokens(address(tapCollector), users.indexer, tokens); @@ -127,12 +163,13 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest } } - function testCollect_RevertWhen_CallerNotDataService(uint256 tokens) public { + function testCollect_RevertWhen_CallerNotDataService(uint256 tokens) public useGateway usePayerSigner { tokens = bound(tokens, 1, type(uint128).max); - resetPrank(payer); + resetPrank(users.gateway); _approveCollector(address(tapCollector), tokens); _depositTokens(address(tapCollector), users.indexer, tokens); + bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); resetPrank(users.indexer); @@ -145,10 +182,9 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); } - function testCollect_RevertWhen_InconsistentRAVTokens(uint256 tokens) public { + function testCollect_RevertWhen_InconsistentRAVTokens(uint256 tokens) public useGateway usePayerSigner { tokens = bound(tokens, 1, type(uint128).max); - resetPrank(payer); _approveCollector(address(tapCollector), tokens); _depositTokens(address(tapCollector), users.indexer, tokens); bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); diff --git a/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol b/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol index b7ee76839..2bc435f7a 100644 --- a/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol +++ b/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol @@ -8,6 +8,16 @@ import { GraphBaseTest } from "../../GraphBase.t.sol"; abstract contract PaymentsEscrowSharedTest is GraphBaseTest { + /* + * MODIFIERS + */ + + modifier useGateway() { + vm.startPrank(users.gateway); + _; + vm.stopPrank(); + } + /* * HELPERS */ diff --git a/packages/horizon/test/utils/Constants.sol b/packages/horizon/test/utils/Constants.sol index 5b1449ea7..cd5cc2bfb 100644 --- a/packages/horizon/test/utils/Constants.sol +++ b/packages/horizon/test/utils/Constants.sol @@ -18,4 +18,6 @@ abstract contract Constants { uint256 internal constant EPOCH_LENGTH = 1; // Rewards manager uint256 internal constant ALLOCATIONS_REWARD_CUT = 100 ether; + // TAPCollector + uint256 internal constant revokeSignerThawingPeriod = 7 days; } \ No newline at end of file diff --git a/packages/subgraph-service/test/SubgraphBaseTest.t.sol b/packages/subgraph-service/test/SubgraphBaseTest.t.sol index 43065add6..099164473 100644 --- a/packages/subgraph-service/test/SubgraphBaseTest.t.sol +++ b/packages/subgraph-service/test/SubgraphBaseTest.t.sol @@ -147,7 +147,7 @@ abstract contract SubgraphBaseTest is Utils, Constants { disputeManager = DisputeManager(disputeManagerProxy); disputeManager.transferOwnership(users.governor); - tapCollector = new TAPCollector("TAPCollector", "1", address(controller)); + tapCollector = new TAPCollector("TAPCollector", "1", address(controller), revokeSignerThawingPeriod); address subgraphServiceImplementation = address( new SubgraphService(address(controller), address(disputeManager), address(tapCollector), address(curation)) ); diff --git a/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol index 99bfab1ca..73a0a5aca 100644 --- a/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol +++ b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; -// import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; import { ITAPCollector } from "@graphprotocol/horizon/contracts/interfaces/ITAPCollector.sol"; import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; import { SubgraphServiceTest } from "../../SubgraphService.t.sol"; @@ -23,6 +23,14 @@ contract SubgraphServiceRegisterTest is SubgraphServiceTest { * HELPERS */ + function _getSignerProof(uint256 _proofDeadline, uint256 _signer) private returns (bytes memory) { + (, address msgSender, ) = vm.readCallers(); + bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, _proofDeadline, msgSender)); + bytes32 proofToDigest = MessageHashUtils.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signer, proofToDigest); + return abi.encodePacked(r, s, v); + } + function _getQueryFeeEncodedData(address indexer, uint128 tokens) private view returns (bytes memory) { ITAPCollector.ReceiptAggregateVoucher memory rav = _getRAV(indexer, tokens); bytes32 messageHash = tapCollector.encodeRAV(rav); @@ -47,14 +55,15 @@ contract SubgraphServiceRegisterTest is SubgraphServiceTest { } function _approveCollector(uint256 tokens) private { - address msgSender; - (, msgSender, ) = vm.readCallers(); - resetPrank(signer); - mint(signer, tokens); escrow.approveCollector(address(tapCollector), tokens); token.approve(address(escrow), tokens); escrow.deposit(address(tapCollector), users.indexer, tokens); - resetPrank(msgSender); + } + + function _authorizeSigner() private { + uint256 proofDeadline = block.timestamp + 1; + bytes memory proof = _getSignerProof(proofDeadline, signerPrivateKey); + tapCollector.authorizeSigner(signer, proofDeadline, proof); } /* @@ -81,8 +90,11 @@ contract SubgraphServiceRegisterTest is SubgraphServiceTest { : tokensAllocated / stakeToFeesRatio; tokensPayment = bound(tokensPayment, minimumProvisionTokens, maxTokensPayment); + resetPrank(users.gateway); _approveCollector(tokensPayment); + _authorizeSigner(); + resetPrank(users.indexer); bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokensPayment)); _collect(users.indexer, IGraphPayments.PaymentTypes.QueryFee, data); } @@ -95,8 +107,11 @@ contract SubgraphServiceRegisterTest is SubgraphServiceTest { numPayments = bound(numPayments, 1, 10); uint256 tokensPayment = tokensAllocated / stakeToFeesRatio / numPayments; + resetPrank(users.gateway); _approveCollector(tokensAllocated); + _authorizeSigner(); + resetPrank(users.indexer); uint256 accTokensPayment = 0; for (uint i = 0; i < numPayments; i++) { accTokensPayment = accTokensPayment + tokensPayment; diff --git a/packages/subgraph-service/test/utils/Constants.sol b/packages/subgraph-service/test/utils/Constants.sol index f1aac3d16..76f864da1 100644 --- a/packages/subgraph-service/test/utils/Constants.sol +++ b/packages/subgraph-service/test/utils/Constants.sol @@ -27,4 +27,6 @@ abstract contract Constants { // RewardsMananger parameters uint256 public constant rewardsPerSignal = 10000; uint256 public constant rewardsPerSubgraphAllocationUpdate = 1000; + // TAPCollector parameters + uint256 public constant revokeSignerThawingPeriod = 7 days; } From e035eacc217c86a190d8eed4a91d1b09dfafe158 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Thu, 10 Oct 2024 19:30:43 -0300 Subject: [PATCH 2/5] fix: collect multiple queries test --- .../test/subgraphService/SubgraphService.t.sol | 2 +- .../test/subgraphService/collect/query/query.t.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol index 013f869a8..b417f30bf 100644 --- a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol +++ b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol @@ -232,7 +232,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { ITAPCollector.SignedRAV memory signedRav = abi.decode(_data, (ITAPCollector.SignedRAV)); allocationId = abi.decode(signedRav.rav.metadata, (address)); allocation = subgraphService.getAllocation(allocationId); - address payer = _recoverRAVSigner(signedRav); + (address payer, ) = tapCollector.authorizedSigners(_recoverRAVSigner(signedRav)); // Total amount of tokens collected for indexer uint256 tokensCollected = tapCollector.tokensCollected(address(subgraphService), _indexer, payer); diff --git a/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol index 73a0a5aca..93972679f 100644 --- a/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol +++ b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol @@ -101,10 +101,10 @@ contract SubgraphServiceRegisterTest is SubgraphServiceTest { function testCollect_MultipleQueryFees( uint256 tokensAllocated, - uint256 numPayments + uint8 numPayments ) public useIndexer useAllocation(tokensAllocated) { vm.assume(tokensAllocated > minimumProvisionTokens * stakeToFeesRatio); - numPayments = bound(numPayments, 1, 10); + numPayments = uint8(bound(numPayments, 2, 10)); uint256 tokensPayment = tokensAllocated / stakeToFeesRatio / numPayments; resetPrank(users.gateway); From 060400f3b8d733659c32be83f5066801945b648b Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Thu, 10 Oct 2024 20:50:12 -0300 Subject: [PATCH 3/5] fix: rename events and change parameter order to make it consistent --- .../contracts/interfaces/ITAPCollector.sol | 26 ++--- .../payments/collectors/TAPCollector.sol | 28 +++--- .../{ => tap-collector}/TAPCollector.t.sol | 99 +++---------------- .../test/payments/tap-collector/collect.t.sol | 90 +++++++++++++++++ 4 files changed, 131 insertions(+), 112 deletions(-) rename packages/horizon/test/payments/{ => tap-collector}/TAPCollector.t.sol (53%) create mode 100644 packages/horizon/test/payments/tap-collector/collect.t.sol diff --git a/packages/horizon/contracts/interfaces/ITAPCollector.sol b/packages/horizon/contracts/interfaces/ITAPCollector.sol index 6d92a8e5c..dd557de53 100644 --- a/packages/horizon/contracts/interfaces/ITAPCollector.sol +++ b/packages/horizon/contracts/interfaces/ITAPCollector.sol @@ -45,10 +45,10 @@ interface ITAPCollector is IPaymentsCollector { /** * @notice Emitted when a signer is authorized to sign RAVs for a payer - * @param authorizedSigner The address of the authorized signer * @param payer The address of the payer authorizing the signer + * @param authorizedSigner The address of the authorized signer */ - event AuthorizeSigner(address indexed authorizedSigner, address indexed payer); + event SignerAuthorized(address indexed payer, address indexed authorizedSigner); /** * @notice Emitted when a signer is thawed to be removed from the authorized signers list @@ -56,7 +56,7 @@ interface ITAPCollector is IPaymentsCollector { * @param authorizedSigner The address of the signer to thaw * @param thawEndTimestamp The timestamp at which the thawing period ends */ - event ThawSigner(address indexed payer, address indexed authorizedSigner, uint256 thawEndTimestamp); + event SignerThawing(address indexed payer, address indexed authorizedSigner, uint256 thawEndTimestamp); /** * @dev Emitted when the thawing of a signer is cancelled @@ -64,14 +64,14 @@ interface ITAPCollector is IPaymentsCollector { * @param authorizedSigner The address of the authorized signer * @param thawEndTimestamp The timestamp at which the thawing period ends */ - event CancelThawSigner(address indexed payer, address indexed authorizedSigner, uint256 thawEndTimestamp); + event SignerThawCanceled(address indexed payer, address indexed authorizedSigner, uint256 thawEndTimestamp); /** * @dev Emitted when a authorized signer has been revoked * @param payer The address of the payer revoking the signer * @param authorizedSigner The address of the authorized signer */ - event RevokeAuthorizedSigner(address indexed payer, address indexed authorizedSigner); + event SignerRevoked(address indexed payer, address indexed authorizedSigner); /** * @notice Emitted when a RAV is collected @@ -95,10 +95,10 @@ interface ITAPCollector is IPaymentsCollector { /** * Thrown when the signer is already authorized - * @param signer The address of the signer * @param authorizingPayer The address of the payer authorizing the signer + * @param signer The address of the signer */ - error TAPCollectorSignerAlreadyAuthorized(address signer, address authorizingPayer); + error TAPCollectorSignerAlreadyAuthorized(address authorizingPayer, address signer); /** * Thrown when the signer proof deadline is invalid @@ -114,10 +114,10 @@ interface ITAPCollector is IPaymentsCollector { /** * Thrown when the signer is not authorized by the payer - * @param signer The address of the signer * @param payer The address of the payer + * @param signer The address of the signer */ - error TAPCollectorSignerNotAuthorizedByPayer(address signer, address payer); + error TAPCollectorSignerNotAuthorizedByPayer(address payer, address signer); /** * Thrown when the signer is not thawing @@ -159,7 +159,7 @@ interface ITAPCollector is IPaymentsCollector { * - `proofDeadline` must be greater than the current timestamp * - `proof` must be a valid signature from the signer being authorized * - * Emits an {AuthorizedSigner} event + * Emits an {SignerAuthorized} event * @param signer The addres of the authorized signer * @param proofDeadline The deadline for the proof provided by the signer * @param proof The proof provided by the signer to be authorized by the payer, consists of (chainID, proof deadline, sender address) @@ -175,7 +175,7 @@ interface ITAPCollector is IPaymentsCollector { * Requirements: * - `signer` must be authorized by the payer calling this function * - * Emits a {ThawSigner} event + * Emits a {SignerThawing} event * @param signer The address of the signer to thaw */ function thawSigner(address signer) external; @@ -185,7 +185,7 @@ interface ITAPCollector is IPaymentsCollector { * @dev Requirements: * - `signer` must be thawing and authorized by the payer calling this function * - * Emits a {CancelThawSigner} event + * Emits a {SignerThawCanceled} event * @param signer The address of the signer to cancel thawing */ function cancelThawSigner(address signer) external; @@ -195,7 +195,7 @@ interface ITAPCollector is IPaymentsCollector { * @dev Requirements: * - `signer` must be thawed and authorized by the payer calling this function * - * Emits a {RevokeAuthorizedSigner} event + * Emits a {SignerRevoked} event * @param signer The address of the signer */ function revokeAuthorizedSigner(address signer) external; diff --git a/packages/horizon/contracts/payments/collectors/TAPCollector.sol b/packages/horizon/contracts/payments/collectors/TAPCollector.sol index 0c9457d86..78fc3a743 100644 --- a/packages/horizon/contracts/payments/collectors/TAPCollector.sol +++ b/packages/horizon/contracts/payments/collectors/TAPCollector.sol @@ -45,15 +45,15 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { * @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. + * @param signerRevokeWaitingPeriod 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 + uint256 signerRevokeWaitingPeriod ) EIP712(eip712Name, eip712Version) GraphDirectory(controller) { - revokeSignerThawingPeriod = _revokeSignerThawingPeriod; + revokeSignerThawingPeriod = signerRevokeWaitingPeriod; } /** @@ -62,14 +62,14 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external override { require( authorizedSigners[signer].payer == address(0), - TAPCollectorSignerAlreadyAuthorized(signer, authorizedSigners[signer].payer) + TAPCollectorSignerAlreadyAuthorized(authorizedSigners[signer].payer, signer) ); - verifyAuthorizedSignerProof(proof, proofDeadline, signer); + _verifyAuthorizedSignerProof(proof, proofDeadline, signer); authorizedSigners[signer].payer = msg.sender; authorizedSigners[signer].thawEndTimestamp = 0; - emit AuthorizeSigner(signer, msg.sender); + emit SignerAuthorized(msg.sender, signer); } /** @@ -78,10 +78,10 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { function thawSigner(address signer) external override { PayerAuthorization storage authorization = authorizedSigners[signer]; - require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(signer, msg.sender)); + require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(msg.sender, signer)); authorization.thawEndTimestamp = block.timestamp + revokeSignerThawingPeriod; - emit ThawSigner(msg.sender, signer, authorization.thawEndTimestamp); + emit SignerThawing(msg.sender, signer, authorization.thawEndTimestamp); } /** @@ -90,11 +90,11 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { function cancelThawSigner(address signer) external override { PayerAuthorization storage authorization = authorizedSigners[signer]; - require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(signer, msg.sender)); + require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(msg.sender, signer)); require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer)); authorization.thawEndTimestamp = 0; - emit CancelThawSigner(msg.sender, signer, 0); + emit SignerThawCanceled(msg.sender, signer, 0); } /** @@ -103,7 +103,7 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { function revokeAuthorizedSigner(address signer) external override { PayerAuthorization storage authorization = authorizedSigners[signer]; - require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(signer, msg.sender)); + require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(msg.sender, signer)); require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer)); require( authorization.thawEndTimestamp <= block.timestamp, @@ -111,7 +111,7 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { ); delete authorizedSigners[signer]; - emit RevokeAuthorizedSigner(msg.sender, signer); + emit SignerRevoked(msg.sender, signer); } /** @@ -180,7 +180,7 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { * @param proofDeadline The deadline by which the proof must be verified * @param signer The signer to be authorized */ - function verifyAuthorizedSignerProof(bytes calldata proof, uint256 proofDeadline, address signer) private view { + function _verifyAuthorizedSignerProof(bytes calldata proof, uint256 proofDeadline, address signer) private view { // Verify that the proofDeadline has not passed require( proofDeadline > block.timestamp, @@ -220,6 +220,7 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { uint256 tokensDataService = tokensToCollect.mulPPM(dataServiceCut); if (tokensToCollect > 0) { + tokensCollected[dataService][receiver][payer] = tokensRAV; _graphPaymentsEscrow().collect( paymentType, payer, @@ -228,7 +229,6 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { dataService, tokensDataService ); - tokensCollected[dataService][receiver][payer] = tokensRAV; } emit PaymentCollected(paymentType, payer, receiver, tokensToCollect, dataService, tokensDataService); diff --git a/packages/horizon/test/payments/TAPCollector.t.sol b/packages/horizon/test/payments/tap-collector/TAPCollector.t.sol similarity index 53% rename from packages/horizon/test/payments/TAPCollector.t.sol rename to packages/horizon/test/payments/tap-collector/TAPCollector.t.sol index 0771d5f33..35a5f1643 100644 --- a/packages/horizon/test/payments/TAPCollector.t.sol +++ b/packages/horizon/test/payments/tap-collector/TAPCollector.t.sol @@ -5,15 +5,15 @@ import "forge-std/Test.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import { IHorizonStakingMain } from "../../contracts/interfaces/internal/IHorizonStakingMain.sol"; -import { ITAPCollector } from "../../contracts/interfaces/ITAPCollector.sol"; -import { IPaymentsCollector } from "../../contracts/interfaces/IPaymentsCollector.sol"; -import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; -import { TAPCollector } from "../../contracts/payments/collectors/TAPCollector.sol"; -import { PPMMath } from "../../contracts/libraries/PPMMath.sol"; +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { ITAPCollector } from "../../../contracts/interfaces/ITAPCollector.sol"; +import { IPaymentsCollector } from "../../../contracts/interfaces/IPaymentsCollector.sol"; +import { IGraphPayments } from "../../../contracts/interfaces/IGraphPayments.sol"; +import { TAPCollector } from "../../../contracts/payments/collectors/TAPCollector.sol"; +import { PPMMath } from "../../../contracts/libraries/PPMMath.sol"; -import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStakingShared.t.sol"; -import { PaymentsEscrowSharedTest } from "../shared/payments-escrow/PaymentsEscrowShared.t.sol"; +import { HorizonStakingSharedTest } from "../../shared/horizon-staking/HorizonStakingShared.t.sol"; +import { PaymentsEscrowSharedTest } from "../../shared/payments-escrow/PaymentsEscrowShared.t.sol"; contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { using PPMMath for uint256; @@ -36,7 +36,7 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest * HELPERS */ - function _getSignerProof(uint256 _proofDeadline, uint256 _signer) private returns (bytes memory) { + function _getSignerProof(uint256 _proofDeadline, uint256 _signer) internal returns (bytes memory) { (, address msgSender, ) = vm.readCallers(); bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, _proofDeadline, msgSender)); bytes32 proofToDigest = MessageHashUtils.toEthSignedMessageHash(messageHash); @@ -44,7 +44,7 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest return abi.encodePacked(r, s, v); } - function _getQueryFeeEncodedData(address indexer, address collector, uint128 tokens) private view returns (bytes memory) { + function _getQueryFeeEncodedData(address indexer, address collector, uint128 tokens) internal view returns (bytes memory) { ITAPCollector.ReceiptAggregateVoucher memory rav = _getRAV(indexer, collector, tokens); bytes32 messageHash = tapCollector.encodeRAV(rav); (uint8 v, bytes32 r, bytes32 s) = vm.sign(payerPrivateKey, messageHash); @@ -57,7 +57,7 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest address indexer, address collector, uint128 tokens - ) private pure returns (ITAPCollector.ReceiptAggregateVoucher memory rav) { + ) internal pure returns (ITAPCollector.ReceiptAggregateVoucher memory rav) { return ITAPCollector.ReceiptAggregateVoucher({ dataService: collector, @@ -72,11 +72,11 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest * ACTIONS */ - function _authorizeSigner(address signer, uint256 proofDeadline, bytes memory proof) private { + function _authorizeSigner(address signer, uint256 proofDeadline, bytes memory proof) internal { (, address msgSender, ) = vm.readCallers(); vm.expectEmit(address(tapCollector)); - emit ITAPCollector.AuthorizeSigner(signer, msgSender); + emit ITAPCollector.SignerAuthorized(msgSender, signer); tapCollector.authorizeSigner(signer, proofDeadline, proof); @@ -85,7 +85,7 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest assertEq(thawEndTimestamp, 0); } - function _collect(IGraphPayments.PaymentTypes _paymentType, bytes memory _data) private { + function _collect(IGraphPayments.PaymentTypes _paymentType, bytes memory _data) internal { (ITAPCollector.SignedRAV memory signedRAV, uint256 dataServiceCut) = abi.decode(_data, (ITAPCollector.SignedRAV, uint256)); bytes32 messageHash = tapCollector.encodeRAV(signedRAV.rav); address _signer = ECDSA.recover(messageHash, signedRAV.signature); @@ -129,75 +129,4 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest (payer, payerPrivateKey) = makeAddrAndKey("payer"); vm.label({ account: payer, newLabel: "payer" }); } - - /* - * TESTS - */ - - function testCollect(uint256 tokens) public useGateway usePayerSigner { - tokens = bound(tokens, 1, type(uint128).max); - - _approveCollector(address(tapCollector), tokens); - _depositTokens(address(tapCollector), users.indexer, tokens); - - bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); - - resetPrank(users.verifier); - _collect(IGraphPayments.PaymentTypes.QueryFee, data); - } - - function testCollect_Multiple(uint256 tokens, uint8 steps) public useGateway usePayerSigner { - steps = uint8(bound(steps, 1, 100)); - tokens = bound(tokens, steps, type(uint128).max); - - _approveCollector(address(tapCollector), tokens); - _depositTokens(address(tapCollector), users.indexer, tokens); - - resetPrank(users.verifier); - uint256 payed = 0; - uint256 tokensPerStep = tokens / steps; - for (uint256 i = 0; i < steps; i++) { - bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(payed + tokensPerStep)); - _collect(IGraphPayments.PaymentTypes.QueryFee, data); - payed += tokensPerStep; - } - } - - function testCollect_RevertWhen_CallerNotDataService(uint256 tokens) public useGateway usePayerSigner { - tokens = bound(tokens, 1, type(uint128).max); - - resetPrank(users.gateway); - _approveCollector(address(tapCollector), tokens); - _depositTokens(address(tapCollector), users.indexer, tokens); - - bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); - - resetPrank(users.indexer); - bytes memory expectedError = abi.encodeWithSelector( - ITAPCollector.TAPCollectorCallerNotDataService.selector, - users.indexer, - users.verifier - ); - vm.expectRevert(expectedError); - tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); - } - - function testCollect_RevertWhen_InconsistentRAVTokens(uint256 tokens) public useGateway usePayerSigner { - tokens = bound(tokens, 1, type(uint128).max); - - _approveCollector(address(tapCollector), tokens); - _depositTokens(address(tapCollector), users.indexer, tokens); - bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); - - resetPrank(users.verifier); - _collect(IGraphPayments.PaymentTypes.QueryFee, data); - - // Attempt to collect again - vm.expectRevert(abi.encodeWithSelector( - ITAPCollector.TAPCollectorInconsistentRAVTokens.selector, - tokens, - tokens - )); - tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); - } } diff --git a/packages/horizon/test/payments/tap-collector/collect.t.sol b/packages/horizon/test/payments/tap-collector/collect.t.sol new file mode 100644 index 000000000..e9e89cf6b --- /dev/null +++ b/packages/horizon/test/payments/tap-collector/collect.t.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { ITAPCollector } from "../../../contracts/interfaces/ITAPCollector.sol"; +import { IPaymentsCollector } from "../../../contracts/interfaces/IPaymentsCollector.sol"; +import { IGraphPayments } from "../../../contracts/interfaces/IGraphPayments.sol"; +import { TAPCollector } from "../../../contracts/payments/collectors/TAPCollector.sol"; +import { PPMMath } from "../../../contracts/libraries/PPMMath.sol"; + +import { TAPCollectorTest } from "./TAPCollector.t.sol"; + +contract TAPCollectorCollectTest is TAPCollectorTest { + using PPMMath for uint256; + + /* + * TESTS + */ + + function testCollect(uint256 tokens) public useGateway usePayerSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + + bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testCollect_Multiple(uint256 tokens, uint8 steps) public useGateway usePayerSigner { + steps = uint8(bound(steps, 1, 100)); + tokens = bound(tokens, steps, type(uint128).max); + + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + + resetPrank(users.verifier); + uint256 payed = 0; + uint256 tokensPerStep = tokens / steps; + for (uint256 i = 0; i < steps; i++) { + bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(payed + tokensPerStep)); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + payed += tokensPerStep; + } + } + + function testCollect_RevertWhen_CallerNotDataService(uint256 tokens) public useGateway usePayerSigner { + tokens = bound(tokens, 1, type(uint128).max); + + resetPrank(users.gateway); + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + + bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); + + resetPrank(users.indexer); + bytes memory expectedError = abi.encodeWithSelector( + ITAPCollector.TAPCollectorCallerNotDataService.selector, + users.indexer, + users.verifier + ); + vm.expectRevert(expectedError); + tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testCollect_RevertWhen_InconsistentRAVTokens(uint256 tokens) public useGateway usePayerSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + + // Attempt to collect again + vm.expectRevert(abi.encodeWithSelector( + ITAPCollector.TAPCollectorInconsistentRAVTokens.selector, + tokens, + tokens + )); + tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } +} From 55b4486835cd76fa68244045f309e123b032a10d Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Thu, 10 Oct 2024 22:16:00 -0300 Subject: [PATCH 4/5] fix: lint issues --- .../payments/collectors/TAPCollector.sol | 120 +++++++++--------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/packages/horizon/contracts/payments/collectors/TAPCollector.sol b/packages/horizon/contracts/payments/collectors/TAPCollector.sol index 78fc3a743..57588a042 100644 --- a/packages/horizon/contracts/payments/collectors/TAPCollector.sol +++ b/packages/horizon/contracts/payments/collectors/TAPCollector.sol @@ -38,22 +38,22 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { public tokensCollected; /// @notice The duration (in seconds) in which a signer is thawing before they can be revoked - uint256 public immutable revokeSignerThawingPeriod; + uint256 public immutable REVOKE_SIGNER_THAWING_PERIOD; /** * @notice Constructs a new instance of the TAPVerifier 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 signerRevokeWaitingPeriod The duration (in seconds) in which a signer is thawing before they can be revoked. + * @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 signerRevokeWaitingPeriod + uint256 revokeSignerThawingPeriod ) EIP712(eip712Name, eip712Version) GraphDirectory(controller) { - revokeSignerThawingPeriod = signerRevokeWaitingPeriod; + REVOKE_SIGNER_THAWING_PERIOD = revokeSignerThawingPeriod; } /** @@ -80,7 +80,7 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(msg.sender, signer)); - authorization.thawEndTimestamp = block.timestamp + revokeSignerThawingPeriod; + authorization.thawEndTimestamp = block.timestamp + REVOKE_SIGNER_THAWING_PERIOD; emit SignerThawing(msg.sender, signer, authorization.thawEndTimestamp); } @@ -147,6 +147,53 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { return _encodeRAV(rav); } + /** + * @notice See {ITAPCollector.collect} + */ + function _collect( + IGraphPayments.PaymentTypes _paymentType, + address _payer, + SignedRAV memory _signedRAV, + uint256 _dataServiceCut + ) private returns (uint256) { + address dataService = _signedRAV.rav.dataService; + address receiver = _signedRAV.rav.serviceProvider; + + uint256 tokensRAV = _signedRAV.rav.valueAggregate; + uint256 tokensAlreadyCollected = tokensCollected[dataService][receiver][_payer]; + require( + tokensRAV > tokensAlreadyCollected, + TAPCollectorInconsistentRAVTokens(tokensRAV, tokensAlreadyCollected) + ); + + uint256 tokensToCollect = tokensRAV - tokensAlreadyCollected; + uint256 tokensDataService = tokensToCollect.mulPPM(_dataServiceCut); + + if (tokensToCollect > 0) { + tokensCollected[dataService][receiver][_payer] = tokensRAV; + _graphPaymentsEscrow().collect( + _paymentType, + _payer, + receiver, + tokensToCollect, + dataService, + tokensDataService + ); + } + + emit PaymentCollected(_paymentType, _payer, receiver, tokensToCollect, dataService, tokensDataService); + emit RAVCollected( + _payer, + dataService, + receiver, + _signedRAV.rav.timestampNs, + _signedRAV.rav.valueAggregate, + _signedRAV.rav.metadata, + _signedRAV.signature + ); + return tokensToCollect; + } + /** * @notice See {ITAPCollector.recoverRAVSigner} */ @@ -176,71 +223,24 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { /** * @notice Verify the proof provided by the payer authorizing the signer - * @param proof The proof provided by the payer authorizing the signer - * @param proofDeadline The deadline by which the proof must be verified - * @param signer The signer to be authorized + * @param _proof The proof provided by the payer authorizing the signer + * @param _proofDeadline The deadline by which the proof must be verified + * @param _signer The signer to be authorized */ - function _verifyAuthorizedSignerProof(bytes calldata proof, uint256 proofDeadline, address signer) private view { + function _verifyAuthorizedSignerProof(bytes calldata _proof, uint256 _proofDeadline, address _signer) private view { // Verify that the proofDeadline has not passed require( - proofDeadline > block.timestamp, - TAPCollectorInvalidSignerProofDeadline(proofDeadline, block.timestamp) + _proofDeadline > block.timestamp, + TAPCollectorInvalidSignerProofDeadline(_proofDeadline, block.timestamp) ); // Generate the hash of the payer's address - bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, proofDeadline, msg.sender)); + bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, _proofDeadline, msg.sender)); // Generate the digest to be signed by the signer bytes32 digest = MessageHashUtils.toEthSignedMessageHash(messageHash); // Verify that the recovered signer matches the expected signer - require(ECDSA.recover(digest, proof) == signer, TAPCollectorInvalidSignerProof()); - } - - /** - * @notice See {ITAPCollector.collect} - */ - function _collect( - IGraphPayments.PaymentTypes paymentType, - address payer, - SignedRAV memory signedRAV, - uint256 dataServiceCut - ) private returns (uint256) { - address dataService = signedRAV.rav.dataService; - address receiver = signedRAV.rav.serviceProvider; - - uint256 tokensRAV = signedRAV.rav.valueAggregate; - uint256 tokensAlreadyCollected = tokensCollected[dataService][receiver][payer]; - require( - tokensRAV > tokensAlreadyCollected, - TAPCollectorInconsistentRAVTokens(tokensRAV, tokensAlreadyCollected) - ); - - uint256 tokensToCollect = tokensRAV - tokensAlreadyCollected; - uint256 tokensDataService = tokensToCollect.mulPPM(dataServiceCut); - - if (tokensToCollect > 0) { - tokensCollected[dataService][receiver][payer] = tokensRAV; - _graphPaymentsEscrow().collect( - paymentType, - payer, - receiver, - tokensToCollect, - dataService, - tokensDataService - ); - } - - emit PaymentCollected(paymentType, payer, receiver, tokensToCollect, dataService, tokensDataService); - emit RAVCollected( - payer, - dataService, - receiver, - signedRAV.rav.timestampNs, - signedRAV.rav.valueAggregate, - signedRAV.rav.metadata, - signedRAV.signature - ); - return tokensToCollect; + require(ECDSA.recover(digest, _proof) == _signer, TAPCollectorInvalidSignerProof()); } } From a712e319a1f21f5603de618ecbd41244a88e7a08 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Thu, 10 Oct 2024 22:18:33 -0300 Subject: [PATCH 5/5] chore: add tap collector signer unit tests --- .../payments/tap-collector/TAPCollector.t.sol | 102 +++++----- .../test/payments/tap-collector/collect.t.sol | 90 --------- .../tap-collector/collect/collect.t.sol | 180 ++++++++++++++++++ .../signer/authorizeSigner.t.sol | 66 +++++++ .../signer/cancelThawSigner.t.sol | 39 ++++ .../tap-collector/signer/revokeSigner.t.sol | 54 ++++++ .../tap-collector/signer/thawSigner.t.sol | 29 +++ 7 files changed, 427 insertions(+), 133 deletions(-) delete mode 100644 packages/horizon/test/payments/tap-collector/collect.t.sol create mode 100644 packages/horizon/test/payments/tap-collector/collect/collect.t.sol create mode 100644 packages/horizon/test/payments/tap-collector/signer/authorizeSigner.t.sol create mode 100644 packages/horizon/test/payments/tap-collector/signer/cancelThawSigner.t.sol create mode 100644 packages/horizon/test/payments/tap-collector/signer/revokeSigner.t.sol create mode 100644 packages/horizon/test/payments/tap-collector/signer/thawSigner.t.sol diff --git a/packages/horizon/test/payments/tap-collector/TAPCollector.t.sol b/packages/horizon/test/payments/tap-collector/TAPCollector.t.sol index 35a5f1643..25b4c901c 100644 --- a/packages/horizon/test/payments/tap-collector/TAPCollector.t.sol +++ b/packages/horizon/test/payments/tap-collector/TAPCollector.t.sol @@ -18,20 +18,30 @@ import { PaymentsEscrowSharedTest } from "../../shared/payments-escrow/PaymentsE contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { using PPMMath for uint256; - address payer; - uint256 payerPrivateKey; + address signer; + uint256 signerPrivateKey; /* * MODIFIERS */ - modifier usePayerSigner() { + modifier useSigner() { uint256 proofDeadline = block.timestamp + 1; - bytes memory signerProof = _getSignerProof(proofDeadline, payerPrivateKey); - _authorizeSigner(payer, proofDeadline, signerProof); + bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); + _authorizeSigner(signer, proofDeadline, signerProof); _; } + /* + * SET UP + */ + + function setUp() public virtual override { + super.setUp(); + (signer, signerPrivateKey) = makeAddrAndKey("signer"); + vm.label({ account: signer, newLabel: "signer" }); + } + /* * HELPERS */ @@ -44,47 +54,63 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest return abi.encodePacked(r, s, v); } - function _getQueryFeeEncodedData(address indexer, address collector, uint128 tokens) internal view returns (bytes memory) { - ITAPCollector.ReceiptAggregateVoucher memory rav = _getRAV(indexer, collector, tokens); - bytes32 messageHash = tapCollector.encodeRAV(rav); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(payerPrivateKey, messageHash); - bytes memory signature = abi.encodePacked(r, s, v); - ITAPCollector.SignedRAV memory signedRAV = ITAPCollector.SignedRAV(rav, signature); - return abi.encode(signedRAV); - } - - function _getRAV( - address indexer, - address collector, - uint128 tokens - ) internal pure returns (ITAPCollector.ReceiptAggregateVoucher memory rav) { - return - ITAPCollector.ReceiptAggregateVoucher({ - dataService: collector, - serviceProvider: indexer, - timestampNs: 0, - valueAggregate: tokens, - metadata: abi.encode("") - }); - } - /* * ACTIONS */ - function _authorizeSigner(address signer, uint256 proofDeadline, bytes memory proof) internal { + function _authorizeSigner(address _signer, uint256 _proofDeadline, bytes memory _proof) internal { (, address msgSender, ) = vm.readCallers(); vm.expectEmit(address(tapCollector)); - emit ITAPCollector.SignerAuthorized(msgSender, signer); + emit ITAPCollector.SignerAuthorized(msgSender, _signer); - tapCollector.authorizeSigner(signer, proofDeadline, proof); + tapCollector.authorizeSigner(_signer, _proofDeadline, _proof); - (address _payer, uint256 thawEndTimestamp) = tapCollector.authorizedSigners(signer); + (address _payer, uint256 thawEndTimestamp) = tapCollector.authorizedSigners(_signer); assertEq(_payer, msgSender); assertEq(thawEndTimestamp, 0); } + function _thawSigner(address _signer) internal { + (, address msgSender, ) = vm.readCallers(); + uint256 expectedThawEndTimestamp = block.timestamp + revokeSignerThawingPeriod; + + vm.expectEmit(address(tapCollector)); + emit ITAPCollector.SignerThawing(msgSender, _signer, expectedThawEndTimestamp); + + tapCollector.thawSigner(_signer); + + (address _payer, uint256 thawEndTimestamp) = tapCollector.authorizedSigners(_signer); + assertEq(_payer, msgSender); + assertEq(thawEndTimestamp, expectedThawEndTimestamp); + } + + function _cancelThawSigner(address _signer) internal { + (, address msgSender, ) = vm.readCallers(); + + vm.expectEmit(address(tapCollector)); + emit ITAPCollector.SignerThawCanceled(msgSender, _signer, 0); + + tapCollector.cancelThawSigner(_signer); + + (address _payer, uint256 thawEndTimestamp) = tapCollector.authorizedSigners(_signer); + assertEq(_payer, msgSender); + assertEq(thawEndTimestamp, 0); + } + + function _revokeAuthorizedSigner(address _signer) internal { + (, address msgSender, ) = vm.readCallers(); + + vm.expectEmit(address(tapCollector)); + emit ITAPCollector.SignerRevoked(msgSender, _signer); + + tapCollector.revokeAuthorizedSigner(_signer); + + (address _payer, uint256 thawEndTimestamp) = tapCollector.authorizedSigners(_signer); + assertEq(_payer, address(0)); + assertEq(thawEndTimestamp, 0); + } + function _collect(IGraphPayments.PaymentTypes _paymentType, bytes memory _data) internal { (ITAPCollector.SignedRAV memory signedRAV, uint256 dataServiceCut) = abi.decode(_data, (ITAPCollector.SignedRAV, uint256)); bytes32 messageHash = tapCollector.encodeRAV(signedRAV.rav); @@ -119,14 +145,4 @@ contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest uint256 tokensCollectedAfter = tapCollector.tokensCollected(signedRAV.rav.dataService, signedRAV.rav.serviceProvider, _payer); assertEq(tokensCollectedAfter, signedRAV.rav.valueAggregate); } - - /* - * SET UP - */ - - function setUp() public virtual override { - super.setUp(); - (payer, payerPrivateKey) = makeAddrAndKey("payer"); - vm.label({ account: payer, newLabel: "payer" }); - } } diff --git a/packages/horizon/test/payments/tap-collector/collect.t.sol b/packages/horizon/test/payments/tap-collector/collect.t.sol deleted file mode 100644 index e9e89cf6b..000000000 --- a/packages/horizon/test/payments/tap-collector/collect.t.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import "forge-std/Test.sol"; - -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; -import { ITAPCollector } from "../../../contracts/interfaces/ITAPCollector.sol"; -import { IPaymentsCollector } from "../../../contracts/interfaces/IPaymentsCollector.sol"; -import { IGraphPayments } from "../../../contracts/interfaces/IGraphPayments.sol"; -import { TAPCollector } from "../../../contracts/payments/collectors/TAPCollector.sol"; -import { PPMMath } from "../../../contracts/libraries/PPMMath.sol"; - -import { TAPCollectorTest } from "./TAPCollector.t.sol"; - -contract TAPCollectorCollectTest is TAPCollectorTest { - using PPMMath for uint256; - - /* - * TESTS - */ - - function testCollect(uint256 tokens) public useGateway usePayerSigner { - tokens = bound(tokens, 1, type(uint128).max); - - _approveCollector(address(tapCollector), tokens); - _depositTokens(address(tapCollector), users.indexer, tokens); - - bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); - - resetPrank(users.verifier); - _collect(IGraphPayments.PaymentTypes.QueryFee, data); - } - - function testCollect_Multiple(uint256 tokens, uint8 steps) public useGateway usePayerSigner { - steps = uint8(bound(steps, 1, 100)); - tokens = bound(tokens, steps, type(uint128).max); - - _approveCollector(address(tapCollector), tokens); - _depositTokens(address(tapCollector), users.indexer, tokens); - - resetPrank(users.verifier); - uint256 payed = 0; - uint256 tokensPerStep = tokens / steps; - for (uint256 i = 0; i < steps; i++) { - bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(payed + tokensPerStep)); - _collect(IGraphPayments.PaymentTypes.QueryFee, data); - payed += tokensPerStep; - } - } - - function testCollect_RevertWhen_CallerNotDataService(uint256 tokens) public useGateway usePayerSigner { - tokens = bound(tokens, 1, type(uint128).max); - - resetPrank(users.gateway); - _approveCollector(address(tapCollector), tokens); - _depositTokens(address(tapCollector), users.indexer, tokens); - - bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); - - resetPrank(users.indexer); - bytes memory expectedError = abi.encodeWithSelector( - ITAPCollector.TAPCollectorCallerNotDataService.selector, - users.indexer, - users.verifier - ); - vm.expectRevert(expectedError); - tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); - } - - function testCollect_RevertWhen_InconsistentRAVTokens(uint256 tokens) public useGateway usePayerSigner { - tokens = bound(tokens, 1, type(uint128).max); - - _approveCollector(address(tapCollector), tokens); - _depositTokens(address(tapCollector), users.indexer, tokens); - bytes memory data = _getQueryFeeEncodedData(users.indexer, users.verifier, uint128(tokens)); - - resetPrank(users.verifier); - _collect(IGraphPayments.PaymentTypes.QueryFee, data); - - // Attempt to collect again - vm.expectRevert(abi.encodeWithSelector( - ITAPCollector.TAPCollectorInconsistentRAVTokens.selector, - tokens, - tokens - )); - tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); - } -} diff --git a/packages/horizon/test/payments/tap-collector/collect/collect.t.sol b/packages/horizon/test/payments/tap-collector/collect/collect.t.sol new file mode 100644 index 000000000..06b0e027a --- /dev/null +++ b/packages/horizon/test/payments/tap-collector/collect/collect.t.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { ITAPCollector } from "../../../../contracts/interfaces/ITAPCollector.sol"; +import { IGraphPayments } from "../../../../contracts/interfaces/IGraphPayments.sol"; + +import { TAPCollectorTest } from "../TAPCollector.t.sol"; + +contract TAPCollectorCollectTest is TAPCollectorTest { + + /* + * HELPERS + */ + + function _getQueryFeeEncodedData( + uint256 _signerPrivateKey, + address _indexer, + address _collector, + uint128 _tokens + ) private view returns (bytes memory) { + ITAPCollector.ReceiptAggregateVoucher memory rav = _getRAV(_indexer, _collector, _tokens); + bytes32 messageHash = tapCollector.encodeRAV(rav); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPrivateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + ITAPCollector.SignedRAV memory signedRAV = ITAPCollector.SignedRAV(rav, signature); + return abi.encode(signedRAV); + } + + function _getRAV( + address _indexer, + address _collector, + uint128 _tokens + ) private pure returns (ITAPCollector.ReceiptAggregateVoucher memory rav) { + return + ITAPCollector.ReceiptAggregateVoucher({ + dataService: _collector, + serviceProvider: _indexer, + timestampNs: 0, + valueAggregate: _tokens, + metadata: abi.encode("") + }); + } + + /* + * TESTS + */ + + function testTAPCollector_Collect(uint256 tokens) public useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, users.indexer, users.verifier, uint128(tokens)); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testTAPCollector_Collect_Multiple(uint256 tokens, uint8 steps) public useGateway useSigner { + steps = uint8(bound(steps, 1, 100)); + tokens = bound(tokens, steps, type(uint128).max); + + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + + resetPrank(users.verifier); + uint256 payed = 0; + uint256 tokensPerStep = tokens / steps; + for (uint256 i = 0; i < steps; i++) { + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, users.indexer, users.verifier, uint128(payed + tokensPerStep)); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + payed += tokensPerStep; + } + } + + function testTAPCollector_Collect_RevertWhen_CallerNotDataService(uint256 tokens) public useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + resetPrank(users.gateway); + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, users.indexer, users.verifier, uint128(tokens)); + + resetPrank(users.indexer); + bytes memory expectedError = abi.encodeWithSelector( + ITAPCollector.TAPCollectorCallerNotDataService.selector, + users.indexer, + users.verifier + ); + vm.expectRevert(expectedError); + tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testTAPCollector_Collect_RevertWhen_InconsistentRAVTokens(uint256 tokens) public useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, users.indexer, users.verifier, uint128(tokens)); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + + // Attempt to collect again + vm.expectRevert(abi.encodeWithSelector( + ITAPCollector.TAPCollectorInconsistentRAVTokens.selector, + tokens, + tokens + )); + tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testTAPCollector_Collect_RevertWhen_SignerNotAuthorized(uint256 tokens) public useGateway { + tokens = bound(tokens, 1, type(uint128).max); + + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, users.indexer, users.verifier, uint128(tokens)); + + resetPrank(users.verifier); + vm.expectRevert(abi.encodeWithSelector(ITAPCollector.TAPCollectorInvalidRAVSigner.selector)); + tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testTAPCollector_Collect_ThawingSigner(uint256 tokens) public useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + + // Start thawing signer + _thawSigner(signer); + skip(revokeSignerThawingPeriod + 1); + + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, users.indexer, users.verifier, uint128(tokens)); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testTAPCollector_Collect_RevertIf_SignerWasRevoked(uint256 tokens) public useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + + // Start thawing signer + _thawSigner(signer); + skip(revokeSignerThawingPeriod + 1); + _revokeAuthorizedSigner(signer); + + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, users.indexer, users.verifier, uint128(tokens)); + + resetPrank(users.verifier); + vm.expectRevert(abi.encodeWithSelector(ITAPCollector.TAPCollectorInvalidRAVSigner.selector)); + tapCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testTAPCollector_Collect_ThawingSignerCanceled(uint256 tokens) public useGateway useSigner { + tokens = bound(tokens, 1, type(uint128).max); + + _approveCollector(address(tapCollector), tokens); + _depositTokens(address(tapCollector), users.indexer, tokens); + + // Start thawing signer + _thawSigner(signer); + skip(revokeSignerThawingPeriod + 1); + _cancelThawSigner(signer); + + bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, users.indexer, users.verifier, uint128(tokens)); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, data); + } +} diff --git a/packages/horizon/test/payments/tap-collector/signer/authorizeSigner.t.sol b/packages/horizon/test/payments/tap-collector/signer/authorizeSigner.t.sol new file mode 100644 index 000000000..b337c48c7 --- /dev/null +++ b/packages/horizon/test/payments/tap-collector/signer/authorizeSigner.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { ITAPCollector } from "../../../../contracts/interfaces/ITAPCollector.sol"; + +import { TAPCollectorTest } from "../TAPCollector.t.sol"; + +contract TAPCollectorAuthorizeSignerTest is TAPCollectorTest { + + uint256 constant SECP256K1_CURVE_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; + + /* + * TESTS + */ + + function testTAPCollector_AuthorizeSigner(uint256 signerKey) public useGateway { + signerKey = bound(signerKey, 1, SECP256K1_CURVE_ORDER - 1); + uint256 proofDeadline = block.timestamp + 1; + bytes memory signerProof = _getSignerProof(proofDeadline, signerKey); + _authorizeSigner(vm.addr(signerKey), proofDeadline, signerProof); + } + + function testTAPCollector_AuthorizeSigner_RevertWhen_Invalid() public useGateway { + // Sign proof with payer + uint256 proofDeadline = block.timestamp + 1; + bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); + + // Attempt to authorize delegator with payer's proof + bytes memory expectedError = abi.encodeWithSelector(ITAPCollector.TAPCollectorInvalidSignerProof.selector); + vm.expectRevert(expectedError); + tapCollector.authorizeSigner(users.delegator, proofDeadline, signerProof); + } + + function testTAPCollector_AuthorizeSigner_RevertWhen_AlreadyAuthroized() public useGateway { + // Authorize signer + uint256 proofDeadline = block.timestamp + 1; + bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); + _authorizeSigner(signer, proofDeadline, signerProof); + + // Attempt to authorize signer again + bytes memory expectedError = abi.encodeWithSelector( + ITAPCollector.TAPCollectorSignerAlreadyAuthorized.selector, + users.gateway, + signer + ); + vm.expectRevert(expectedError); + tapCollector.authorizeSigner(signer, proofDeadline, signerProof); + } + + function testTAPCollector_AuthorizeSigner_RevertWhen_ProofExpired() public useGateway { + // Sign proof with payer + uint256 proofDeadline = block.timestamp - 1; + bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); + + // Attempt to authorize delegator with expired proof + bytes memory expectedError = abi.encodeWithSelector( + ITAPCollector.TAPCollectorInvalidSignerProofDeadline.selector, + proofDeadline, + block.timestamp + ); + vm.expectRevert(expectedError); + tapCollector.authorizeSigner(users.delegator, proofDeadline, signerProof); + } +} diff --git a/packages/horizon/test/payments/tap-collector/signer/cancelThawSigner.t.sol b/packages/horizon/test/payments/tap-collector/signer/cancelThawSigner.t.sol new file mode 100644 index 000000000..dc25da8cf --- /dev/null +++ b/packages/horizon/test/payments/tap-collector/signer/cancelThawSigner.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { ITAPCollector } from "../../../../contracts/interfaces/ITAPCollector.sol"; + +import { TAPCollectorTest } from "../TAPCollector.t.sol"; + +contract TAPCollectorCancelThawSignerTest is TAPCollectorTest { + + /* + * TESTS + */ + + function testTAPCollector_CancelThawSigner() public useGateway useSigner { + _thawSigner(signer); + _cancelThawSigner(signer); + } + + function testTAPCollector_CancelThawSigner_RevertWhen_NotAuthorized() public useGateway { + bytes memory expectedError = abi.encodeWithSelector( + ITAPCollector.TAPCollectorSignerNotAuthorizedByPayer.selector, + users.gateway, + signer + ); + vm.expectRevert(expectedError); + tapCollector.thawSigner(signer); + } + + function testTAPCollector_CancelThawSigner_RevertWhen_NotThawing() public useGateway useSigner { + bytes memory expectedError = abi.encodeWithSelector( + ITAPCollector.TAPCollectorSignerNotThawing.selector, + signer + ); + vm.expectRevert(expectedError); + tapCollector.cancelThawSigner(signer); + } +} diff --git a/packages/horizon/test/payments/tap-collector/signer/revokeSigner.t.sol b/packages/horizon/test/payments/tap-collector/signer/revokeSigner.t.sol new file mode 100644 index 000000000..8c03245f8 --- /dev/null +++ b/packages/horizon/test/payments/tap-collector/signer/revokeSigner.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { ITAPCollector } from "../../../../contracts/interfaces/ITAPCollector.sol"; + +import { TAPCollectorTest } from "../TAPCollector.t.sol"; + +contract TAPCollectorRevokeAuthorizedSignerTest is TAPCollectorTest { + + /* + * TESTS + */ + + function testTAPCollector_RevokeAuthorizedSigner() public useGateway useSigner { + _thawSigner(signer); + + // Advance time to thaw signer + skip(revokeSignerThawingPeriod + 1); + + _revokeAuthorizedSigner(signer); + } + + function testTAPCollector_RevokeAuthorizedSigner_RevertWhen_NotAuthorized() public useGateway { + bytes memory expectedError = abi.encodeWithSelector( + ITAPCollector.TAPCollectorSignerNotAuthorizedByPayer.selector, + users.gateway, + signer + ); + vm.expectRevert(expectedError); + tapCollector.revokeAuthorizedSigner(signer); + } + + function testTAPCollector_RevokeAuthorizedSigner_RevertWhen_NotThawing() public useGateway useSigner { + bytes memory expectedError = abi.encodeWithSelector( + ITAPCollector.TAPCollectorSignerNotThawing.selector, + signer + ); + vm.expectRevert(expectedError); + tapCollector.revokeAuthorizedSigner(signer); + } + + function testTAPCollector_RevokeAuthorizedSigner_RevertWhen_StillThawing() public useGateway useSigner { + _thawSigner(signer); + bytes memory expectedError = abi.encodeWithSelector( + ITAPCollector.TAPCollectorSignerStillThawing.selector, + block.timestamp, + block.timestamp + revokeSignerThawingPeriod + ); + vm.expectRevert(expectedError); + tapCollector.revokeAuthorizedSigner(signer); + } +} diff --git a/packages/horizon/test/payments/tap-collector/signer/thawSigner.t.sol b/packages/horizon/test/payments/tap-collector/signer/thawSigner.t.sol new file mode 100644 index 000000000..fb47c37fb --- /dev/null +++ b/packages/horizon/test/payments/tap-collector/signer/thawSigner.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { ITAPCollector } from "../../../../contracts/interfaces/ITAPCollector.sol"; + +import { TAPCollectorTest } from "../TAPCollector.t.sol"; + +contract TAPCollectorThawSignerTest is TAPCollectorTest { + + /* + * TESTS + */ + + function testTAPCollector_ThawSigner() public useGateway useSigner { + _thawSigner(signer); + } + + function testTAPCollector_ThawSigner_RevertWhen_NotAuthorized() public useGateway { + bytes memory expectedError = abi.encodeWithSelector( + ITAPCollector.TAPCollectorSignerNotAuthorizedByPayer.selector, + users.gateway, + signer + ); + vm.expectRevert(expectedError); + tapCollector.thawSigner(signer); + } +}