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..dd557de53 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 payer The address of the payer authorizing the signer + * @param authorizedSigner The address of the authorized signer + */ + event SignerAuthorized(address indexed payer, address indexed authorizedSigner); + + /** + * @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 SignerThawing(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 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 SignerRevoked(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 authorizingPayer The address of the payer authorizing the signer + * @param signer The address of the signer + */ + error TAPCollectorSignerAlreadyAuthorized(address authorizingPayer, address signer); + + /** + * 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 payer The address of the payer + * @param signer The address of the signer + */ + error TAPCollectorSignerNotAuthorizedByPayer(address payer, address signer); + + /** + * 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 {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) + */ + 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 {SignerThawing} 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 {SignerThawCanceled} 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 {SignerRevoked} 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..57588a042 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 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 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) { + REVOKE_SIGNER_THAWING_PERIOD = revokeSignerThawingPeriod; + } + + /** + * See {ITAPCollector.authorizeSigner}. + */ + function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external override { + require( + authorizedSigners[signer].payer == address(0), + TAPCollectorSignerAlreadyAuthorized(authorizedSigners[signer].payer, signer) + ); + + _verifyAuthorizedSignerProof(proof, proofDeadline, signer); + + authorizedSigners[signer].payer = msg.sender; + authorizedSigners[signer].thawEndTimestamp = 0; + emit SignerAuthorized(msg.sender, signer); + } + + /** + * See {ITAPCollector.thawSigner}. + */ + function thawSigner(address signer) external override { + PayerAuthorization storage authorization = authorizedSigners[signer]; + + require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(msg.sender, signer)); + + authorization.thawEndTimestamp = block.timestamp + REVOKE_SIGNER_THAWING_PERIOD; + emit SignerThawing(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(msg.sender, signer)); + require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer)); + + authorization.thawEndTimestamp = 0; + emit SignerThawCanceled(msg.sender, signer, 0); + } + + /** + * See {ITAPCollector.revokeAuthorizedSigner}. + */ + function revokeAuthorizedSigner(address signer) external override { + PayerAuthorization storage authorization = authorizedSigners[signer]; + + require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(msg.sender, signer)); + require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer)); + require( + authorization.thawEndTimestamp <= block.timestamp, + TAPCollectorSignerStillThawing(block.timestamp, authorization.thawEndTimestamp) + ); + + delete authorizedSigners[signer]; + emit SignerRevoked(msg.sender, signer); + } /** * @notice Initiate a payment collection through the payments protocol @@ -58,59 +127,73 @@ 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()); + + return _collect(paymentType, authorizedSigners[signer].payer, signedRAV, dataServiceCut); + } + + /** + * @notice See {ITAPCollector.recoverRAVSigner} + */ + function recoverRAVSigner(SignedRAV calldata signedRAV) external view override returns (address) { + return _recoverRAVSigner(signedRAV); + } + + /** + * @notice See {ITAPCollector.encodeRAV} + */ + function encodeRAV(ReceiptAggregateVoucher calldata rav) external view returns (bytes32) { + return _encodeRAV(rav); + } - uint256 tokensRAV = signedRAV.rav.valueAggregate; - uint256 tokensAlreadyCollected = tokensCollected[dataService][receiver][payer]; + /** + * @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); + uint256 tokensDataService = tokensToCollect.mulPPM(_dataServiceCut); if (tokensToCollect > 0) { + tokensCollected[dataService][receiver][_payer] = tokensRAV; _graphPaymentsEscrow().collect( - paymentType, - payer, + _paymentType, + _payer, receiver, tokensToCollect, dataService, tokensDataService ); - tokensCollected[dataService][receiver][payer] = tokensRAV; } - emit PaymentCollected(paymentType, payer, receiver, tokensToCollect, dataService, tokensDataService); + emit PaymentCollected(_paymentType, _payer, receiver, tokensToCollect, dataService, tokensDataService); emit RAVCollected( - payer, + _payer, dataService, receiver, - signedRAV.rav.timestampNs, - signedRAV.rav.valueAggregate, - signedRAV.rav.metadata, - signedRAV.signature + _signedRAV.rav.timestampNs, + _signedRAV.rav.valueAggregate, + _signedRAV.rav.metadata, + _signedRAV.signature ); return tokensToCollect; } - /** - * @notice See {ITAPCollector.recoverRAVSigner} - */ - function recoverRAVSigner(SignedRAV calldata signedRAV) external view override returns (address) { - return _recoverRAVSigner(signedRAV); - } - - /** - * @notice See {ITAPCollector.encodeRAV} - */ - function encodeRAV(ReceiptAggregateVoucher calldata rav) external view returns (bytes32) { - return _encodeRAV(rav); - } - /** * @notice See {ITAPCollector.recoverRAVSigner} */ @@ -137,4 +220,27 @@ 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()); + } } 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 deleted file mode 100644 index e1c177282..000000000 --- a/packages/horizon/test/payments/TAPCollector.t.sol +++ /dev/null @@ -1,167 +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 { 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"; - -contract TAPCollectorTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { - using PPMMath for uint256; - - address payer; - uint256 payerPrivateKey; - - /* - * HELPERS - */ - - 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); - (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 - ) private pure returns (ITAPCollector.ReceiptAggregateVoucher memory rav) { - return - ITAPCollector.ReceiptAggregateVoucher({ - dataService: collector, - serviceProvider: indexer, - timestampNs: 0, - valueAggregate: tokens, - metadata: abi.encode("") - }); - } - - 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); - uint256 tokensAlreadyCollected = tapCollector.tokensCollected(signedRAV.rav.dataService, signedRAV.rav.serviceProvider, _payer); - uint256 tokensToCollect = signedRAV.rav.valueAggregate - tokensAlreadyCollected; - uint256 tokensDataService = tokensToCollect.mulPPM(dataServiceCut); - - vm.expectEmit(address(tapCollector)); - emit IPaymentsCollector.PaymentCollected( - _paymentType, - _payer, - signedRAV.rav.serviceProvider, - tokensToCollect, - signedRAV.rav.dataService, - tokensDataService - ); - emit ITAPCollector.RAVCollected( - _payer, - signedRAV.rav.dataService, - signedRAV.rav.serviceProvider, - signedRAV.rav.timestampNs, - signedRAV.rav.valueAggregate, - signedRAV.rav.metadata, - signedRAV.signature - ); - - uint256 tokensCollected = tapCollector.collect(_paymentType, _data); - assertEq(tokensCollected, tokensToCollect); - - 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" }); - deal({ token: address(token), to: payer, give: type(uint256).max }); - } - - /* - * TESTS - */ - - function testCollect(uint256 tokens) public { - 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 { - 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); - - 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 { - 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.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 { - 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); - - // 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/TAPCollector.t.sol b/packages/horizon/test/payments/tap-collector/TAPCollector.t.sol new file mode 100644 index 000000000..25b4c901c --- /dev/null +++ b/packages/horizon/test/payments/tap-collector/TAPCollector.t.sol @@ -0,0 +1,148 @@ +// 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 { 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; + + address signer; + uint256 signerPrivateKey; + + /* + * MODIFIERS + */ + + modifier useSigner() { + uint256 proofDeadline = block.timestamp + 1; + 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 + */ + + 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); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signer, proofToDigest); + return abi.encodePacked(r, s, v); + } + + /* + * ACTIONS + */ + + function _authorizeSigner(address _signer, uint256 _proofDeadline, bytes memory _proof) internal { + (, address msgSender, ) = vm.readCallers(); + + vm.expectEmit(address(tapCollector)); + emit ITAPCollector.SignerAuthorized(msgSender, _signer); + + tapCollector.authorizeSigner(_signer, _proofDeadline, _proof); + + (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); + 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); + + vm.expectEmit(address(tapCollector)); + emit IPaymentsCollector.PaymentCollected( + _paymentType, + _payer, + signedRAV.rav.serviceProvider, + tokensToCollect, + signedRAV.rav.dataService, + tokensDataService + ); + emit ITAPCollector.RAVCollected( + _payer, + signedRAV.rav.dataService, + signedRAV.rav.serviceProvider, + signedRAV.rav.timestampNs, + signedRAV.rav.valueAggregate, + signedRAV.rav.metadata, + signedRAV.signature + ); + + uint256 tokensCollected = tapCollector.collect(_paymentType, _data); + assertEq(tokensCollected, tokensToCollect); + + uint256 tokensCollectedAfter = tapCollector.tokensCollected(signedRAV.rav.dataService, signedRAV.rav.serviceProvider, _payer); + assertEq(tokensCollectedAfter, signedRAV.rav.valueAggregate); + } +} 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); + } +} 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/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 99bfab1ca..93972679f 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,22 +90,28 @@ 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); } 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); _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; }