diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol index 8ca7464d1..83f3558e9 100644 --- a/packages/horizon/contracts/interfaces/IGraphPayments.sol +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -31,6 +31,7 @@ interface IGraphPayments { * @param tokensDataService Amount of tokens for the data service * @param tokensDelegationPool Amount of tokens for delegators * @param tokensReceiver Amount of tokens for the receiver + * @param receiverDestination The address where the receiver's payment cut is sent. */ event GraphPaymentCollected( PaymentTypes indexed paymentType, @@ -41,7 +42,8 @@ interface IGraphPayments { uint256 tokensProtocol, uint256 tokensDataService, uint256 tokensDelegationPool, - uint256 tokensReceiver + uint256 tokensReceiver, + address receiverDestination ); /** @@ -63,19 +65,25 @@ interface IGraphPayments { /** * @notice Collects funds from a payer. - * It will pay cuts to all relevant parties and forward the rest to the receiver. + * It will pay cuts to all relevant parties and forward the rest to the receiver destination address. If the + * destination address is zero the funds are automatically staked to the receiver. Note that the receiver + * destination address can be set to the receiver address to collect funds on the receiver without re-staking. + * * Note that the collected amount can be zero. + * * @param paymentType The type of payment as defined in {IGraphPayments} * @param receiver The address of the receiver * @param tokens The amount of tokens being collected. * @param dataService The address of the data service * @param dataServiceCut The data service cut in PPM + * @param receiverDestination The address where the receiver's payment cut is sent. */ function collect( PaymentTypes paymentType, address receiver, uint256 tokens, address dataService, - uint256 dataServiceCut + uint256 dataServiceCut, + address receiverDestination ) external; } diff --git a/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol b/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol index 8a51c7adb..cb665bda5 100644 --- a/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol +++ b/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol @@ -107,7 +107,10 @@ interface IGraphTallyCollector is IPaymentsCollector { * - The amount of tokens to collect must be less than or equal to the total amount of tokens in the RAV minus * the tokens already collected. * @param paymentType The payment type to collect - * @param data Additional data required for the payment collection + * @param data Additional data required for the payment collection. Encoded as follows: + * - SignedRAV `signedRAV`: The signed RAV + * - uint256 `dataServiceCut`: The data service cut in PPM + * - address `receiverDestination`: The address where the receiver's payment should be sent. * @param tokensToCollect The amount of tokens to collect * @return The amount of tokens collected */ diff --git a/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol b/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol index f26030487..eb3b262e8 100644 --- a/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol +++ b/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol @@ -86,13 +86,15 @@ interface IPaymentsEscrow { * @param collector The address of the collector * @param receiver The address of the receiver * @param tokens The amount of tokens collected + * @param receiverDestination The address where the receiver's payment should be sent. */ event EscrowCollected( IGraphPayments.PaymentTypes indexed paymentType, address indexed payer, address indexed collector, address receiver, - uint256 tokens + uint256 tokens, + address receiverDestination ); // -- Errors -- @@ -221,6 +223,7 @@ interface IPaymentsEscrow { * @param tokens The amount of tokens to collect * @param dataService The address of the data service * @param dataServiceCut The data service cut in PPM that {GraphPayments} should send + * @param receiverDestination The address where the receiver's payment should be sent. */ function collect( IGraphPayments.PaymentTypes paymentType, @@ -228,7 +231,8 @@ interface IPaymentsEscrow { address receiver, uint256 tokens, address dataService, - uint256 dataServiceCut + uint256 dataServiceCut, + address receiverDestination ) external; /** diff --git a/packages/horizon/contracts/payments/GraphPayments.sol b/packages/horizon/contracts/payments/GraphPayments.sol index 5bfd993e9..e74e351cf 100644 --- a/packages/horizon/contracts/payments/GraphPayments.sol +++ b/packages/horizon/contracts/payments/GraphPayments.sol @@ -51,7 +51,8 @@ contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, I address receiver, uint256 tokens, address dataService, - uint256 dataServiceCut + uint256 dataServiceCut, + address receiverDestination ) external { require(PPMMath.isValidPPM(dataServiceCut), GraphPaymentsInvalidCut(dataServiceCut)); @@ -88,7 +89,14 @@ contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, I _graphStaking().addToDelegationPool(receiver, dataService, tokensDelegationPool); } - _graphToken().pushTokens(receiver, tokensRemaining); + if (tokensRemaining > 0) { + if (receiverDestination == address(0)) { + _graphToken().approve(address(_graphStaking()), tokensRemaining); + _graphStaking().stakeTo(receiver, tokensRemaining); + } else { + _graphToken().pushTokens(receiverDestination, tokensRemaining); + } + } emit GraphPaymentCollected( paymentType, @@ -99,7 +107,8 @@ contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, I tokensProtocol, tokensDataService, tokensDelegationPool, - tokensRemaining + tokensRemaining, + receiverDestination ); } } diff --git a/packages/horizon/contracts/payments/PaymentsEscrow.sol b/packages/horizon/contracts/payments/PaymentsEscrow.sol index c53a7a56e..d947921cd 100644 --- a/packages/horizon/contracts/payments/PaymentsEscrow.sol +++ b/packages/horizon/contracts/payments/PaymentsEscrow.sol @@ -125,7 +125,8 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, address receiver, uint256 tokens, address dataService, - uint256 dataServiceCut + uint256 dataServiceCut, + address receiverDestination ) external override notPaused { // Check if there are enough funds in the escrow account EscrowAccount storage account = escrowAccounts[payer][msg.sender][receiver]; @@ -137,7 +138,7 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, uint256 escrowBalanceBefore = _graphToken().balanceOf(address(this)); _graphToken().approve(address(_graphPayments()), tokens); - _graphPayments().collect(paymentType, receiver, tokens, dataService, dataServiceCut); + _graphPayments().collect(paymentType, receiver, tokens, dataService, dataServiceCut, receiverDestination); // Verify that the escrow balance is consistent with the collected tokens uint256 escrowBalanceAfter = _graphToken().balanceOf(address(this)); @@ -146,7 +147,7 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, PaymentsEscrowInconsistentCollection(escrowBalanceBefore, escrowBalanceAfter, tokens) ); - emit EscrowCollected(paymentType, payer, msg.sender, receiver, tokens); + emit EscrowCollected(paymentType, payer, msg.sender, receiver, tokens, receiverDestination); } /// @inheritdoc IPaymentsEscrow diff --git a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol index c3009f41a..671826169 100644 --- a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol +++ b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol @@ -58,6 +58,12 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall * - Signer of the RAV must be authorized to sign for the payer. * - Service provider must have an active provision with the data service to collect payments. * @notice REVERT: This function may revert if ECDSA.recover fails, check ECDSA library for details. + * @param paymentType The payment type to collect + * @param data Additional data required for the payment collection. Encoded as follows: + * - SignedRAV `signedRAV`: The signed RAV + * - uint256 `dataServiceCut`: The data service cut in PPM + * - address `receiverDestination`: The address where the receiver's payment should be sent. + * @return The amount of tokens collected */ /// @inheritdoc IPaymentsCollector function collect(IGraphPayments.PaymentTypes paymentType, bytes calldata data) external override returns (uint256) { @@ -96,7 +102,7 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall bytes calldata _data, uint256 _tokensToCollect ) private returns (uint256) { - (SignedRAV memory signedRAV, uint256 dataServiceCut) = abi.decode(_data, (SignedRAV, uint256)); + (SignedRAV memory signedRAV, uint256 dataServiceCut, address receiverDestination) = abi.decode(_data, (SignedRAV, uint256, address)); // Ensure caller is the RAV data service require( @@ -123,10 +129,9 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall } uint256 tokensToCollect = 0; - address payer = signedRAV.rav.payer; { uint256 tokensRAV = signedRAV.rav.valueAggregate; - uint256 tokensAlreadyCollected = tokensCollected[dataService][collectionId][receiver][payer]; + uint256 tokensAlreadyCollected = tokensCollected[dataService][collectionId][receiver][signedRAV.rav.payer]; require( tokensRAV > tokensAlreadyCollected, GraphTallyCollectorInconsistentRAVTokens(tokensRAV, tokensAlreadyCollected) @@ -147,16 +152,16 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall } if (tokensToCollect > 0) { - tokensCollected[dataService][collectionId][receiver][payer] += tokensToCollect; - _graphPaymentsEscrow().collect(_paymentType, payer, receiver, tokensToCollect, dataService, dataServiceCut); + tokensCollected[dataService][collectionId][receiver][signedRAV.rav.payer] += tokensToCollect; + _graphPaymentsEscrow().collect(_paymentType, signedRAV.rav.payer, receiver, tokensToCollect, dataService, dataServiceCut, receiverDestination); } - emit PaymentCollected(_paymentType, collectionId, payer, receiver, dataService, tokensToCollect); + emit PaymentCollected(_paymentType, collectionId, signedRAV.rav.payer, receiver, dataService, tokensToCollect); // This event is emitted to allow reconstructing RAV history with onchain data. emit RAVCollected( collectionId, - payer, + signedRAV.rav.payer, receiver, dataService, signedRAV.rav.timestampNs, diff --git a/packages/horizon/test/escrow/GraphEscrow.t.sol b/packages/horizon/test/escrow/GraphEscrow.t.sol index 4c6933adf..5d9dea53c 100644 --- a/packages/horizon/test/escrow/GraphEscrow.t.sol +++ b/packages/horizon/test/escrow/GraphEscrow.t.sol @@ -113,13 +113,21 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { uint256 payerEscrowBalance; } + struct CollectTokensData { + uint256 tokensProtocol; + uint256 tokensDataService; + uint256 tokensDelegation; + uint256 receiverExpectedPayment; + } + function _collectEscrow( IGraphPayments.PaymentTypes _paymentType, address _payer, address _receiver, uint256 _tokens, address _dataService, - uint256 _dataServiceCut + uint256 _dataServiceCut, + address _paymentsDestination ) internal { (, address _collector, ) = vm.readCallers(); @@ -132,6 +140,12 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { dataServiceBalance: token.balanceOf(_dataService), payerEscrowBalance: 0 }); + CollectTokensData memory collectTokensData = CollectTokensData({ + tokensProtocol: 0, + tokensDataService: 0, + tokensDelegation: 0, + receiverExpectedPayment: 0 + }); { (uint256 payerEscrowBalance, , ) = escrow.escrowAccounts(_payer, _collector, _receiver); @@ -139,24 +153,36 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { } vm.expectEmit(address(escrow)); - emit IPaymentsEscrow.EscrowCollected(_paymentType, _payer, _collector, _receiver, _tokens); - escrow.collect(_paymentType, _payer, _receiver, _tokens, _dataService, _dataServiceCut); + emit IPaymentsEscrow.EscrowCollected( + _paymentType, + _payer, + _collector, + _receiver, + _tokens, + _paymentsDestination + ); + escrow.collect(_paymentType, _payer, _receiver, _tokens, _dataService, _dataServiceCut, _paymentsDestination); // Calculate cuts // this is nasty but stack is indeed too deep - uint256 tokensDataService = (_tokens - _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT())).mulPPMRoundUp( + collectTokensData.tokensProtocol = _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()); + collectTokensData.tokensDataService = (_tokens - collectTokensData.tokensProtocol).mulPPMRoundUp( _dataServiceCut ); - uint256 tokensDelegation = 0; + IHorizonStakingTypes.DelegationPool memory pool = staking.getDelegationPool(_receiver, _dataService); if (pool.shares > 0) { - tokensDelegation = (_tokens - _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()) - tokensDataService) - .mulPPMRoundUp(staking.getDelegationFeeCut(_receiver, _dataService, _paymentType)); + collectTokensData.tokensDelegation = (_tokens - + collectTokensData.tokensProtocol - + collectTokensData.tokensDataService).mulPPMRoundUp( + staking.getDelegationFeeCut(_receiver, _dataService, _paymentType) + ); } - uint256 receiverExpectedPayment = _tokens - - _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()) - - tokensDataService - - tokensDelegation; + collectTokensData.receiverExpectedPayment = + _tokens - + collectTokensData.tokensProtocol - + collectTokensData.tokensDataService - + collectTokensData.tokensDelegation; // After balances CollectPaymentData memory afterBalances = CollectPaymentData({ @@ -173,11 +199,11 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { } // Check receiver balance after payment - assertEq(afterBalances.receiverBalance - previousBalances.receiverBalance, receiverExpectedPayment); + assertEq(afterBalances.receiverBalance - previousBalances.receiverBalance, collectTokensData.receiverExpectedPayment); assertEq(token.balanceOf(address(payments)), 0); // Check delegation pool balance after payment - assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, tokensDelegation); + assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, collectTokensData.tokensDelegation); // Check that the escrow account has been updated assertEq(previousBalances.escrowBalance, afterBalances.escrowBalance + _tokens); @@ -186,7 +212,7 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { assertEq(previousBalances.paymentsBalance, afterBalances.paymentsBalance); // Check data service balance after payment - assertEq(afterBalances.dataServiceBalance - previousBalances.dataServiceBalance, tokensDataService); + assertEq(afterBalances.dataServiceBalance - previousBalances.dataServiceBalance, collectTokensData.tokensDataService); // Check payers escrow balance after payment assertEq(previousBalances.payerEscrowBalance - _tokens, afterBalances.payerEscrowBalance); diff --git a/packages/horizon/test/escrow/collect.t.sol b/packages/horizon/test/escrow/collect.t.sol index f4357d213..09b7f7f6a 100644 --- a/packages/horizon/test/escrow/collect.t.sol +++ b/packages/horizon/test/escrow/collect.t.sol @@ -46,7 +46,8 @@ contract GraphEscrowCollectTest is GraphEscrowTest { users.indexer, tokensToCollect, subgraphDataServiceAddress, - dataServiceCut + dataServiceCut, + users.indexer ); } @@ -71,7 +72,8 @@ contract GraphEscrowCollectTest is GraphEscrowTest { users.indexer, tokens, subgraphDataServiceAddress, - dataServiceCut + dataServiceCut, + users.indexer ); } @@ -95,7 +97,8 @@ contract GraphEscrowCollectTest is GraphEscrowTest { users.indexer, amount, subgraphDataServiceAddress, - 0 + 0, + users.indexer ); vm.stopPrank(); } @@ -126,16 +129,8 @@ contract GraphEscrowCollectTest is GraphEscrowTest { users.indexer, firstCollect, subgraphDataServiceAddress, - 0 + 0, + users.indexer ); - - // _collectEscrow( - // IGraphPayments.PaymentTypes.QueryFee, - // users.gateway, - // users.indexer, - // secondCollect, - // subgraphDataServiceAddress, - // 0 - // ); } } diff --git a/packages/horizon/test/escrow/getters.t.sol b/packages/horizon/test/escrow/getters.t.sol index 6434e1b30..aad05996f 100644 --- a/packages/horizon/test/escrow/getters.t.sol +++ b/packages/horizon/test/escrow/getters.t.sol @@ -57,7 +57,8 @@ contract GraphEscrowGettersTest is GraphEscrowTest { users.indexer, amountCollected, subgraphDataServiceAddress, - 0 + 0, + users.indexer ); // balance should always be 0 since thawing funds > available funds diff --git a/packages/horizon/test/escrow/paused.t.sol b/packages/horizon/test/escrow/paused.t.sol index a75532ed6..d28e18702 100644 --- a/packages/horizon/test/escrow/paused.t.sol +++ b/packages/horizon/test/escrow/paused.t.sol @@ -66,7 +66,8 @@ contract GraphEscrowPausedTest is GraphEscrowTest { users.indexer, tokens, subgraphDataServiceAddress, - tokensDataService + tokensDataService, + users.indexer ); } } diff --git a/packages/horizon/test/escrow/withdraw.t.sol b/packages/horizon/test/escrow/withdraw.t.sol index ff4d98650..0974e8ef8 100644 --- a/packages/horizon/test/escrow/withdraw.t.sol +++ b/packages/horizon/test/escrow/withdraw.t.sol @@ -61,7 +61,8 @@ contract GraphEscrowWithdrawTest is GraphEscrowTest { users.indexer, amountCollected, subgraphDataServiceAddress, - 0 + 0, + users.indexer ); // Advance time to simulate the thawing period diff --git a/packages/horizon/test/payments/GraphPayments.t.sol b/packages/horizon/test/payments/GraphPayments.t.sol index 9ab5fae1d..c162ccfb5 100644 --- a/packages/horizon/test/payments/GraphPayments.t.sol +++ b/packages/horizon/test/payments/GraphPayments.t.sol @@ -27,8 +27,17 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { uint256 escrowBalance; uint256 paymentsBalance; uint256 receiverBalance; + uint256 receiverDestinationBalance; uint256 delegationPoolBalance; uint256 dataServiceBalance; + uint256 receiverStake; + } + + struct CollectTokensData { + uint256 tokensProtocol; + uint256 tokensDataService; + uint256 tokensDelegation; + uint256 receiverExpectedPayment; } function _collect( @@ -36,31 +45,48 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { address _receiver, uint256 _tokens, address _dataService, - uint256 _dataServiceCut + uint256 _dataServiceCut, + address _paymentsDestination ) private { // Previous balances CollectPaymentData memory previousBalances = CollectPaymentData({ escrowBalance: token.balanceOf(address(escrow)), paymentsBalance: token.balanceOf(address(payments)), receiverBalance: token.balanceOf(_receiver), + receiverDestinationBalance: token.balanceOf(_paymentsDestination), delegationPoolBalance: staking.getDelegatedTokensAvailable(_receiver, _dataService), - dataServiceBalance: token.balanceOf(_dataService) + dataServiceBalance: token.balanceOf(_dataService), + receiverStake: staking.getStake(_receiver) }); // Calculate cuts - uint256 tokensProtocol = _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()); - uint256 tokensDataService = (_tokens - tokensProtocol).mulPPMRoundUp(_dataServiceCut); - uint256 tokensDelegation = 0; + CollectTokensData memory collectTokensData = CollectTokensData({ + tokensProtocol: 0, + tokensDataService: 0, + tokensDelegation: 0, + receiverExpectedPayment: 0 + }); + collectTokensData.tokensProtocol = _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()); + collectTokensData.tokensDataService = (_tokens - collectTokensData.tokensProtocol).mulPPMRoundUp( + _dataServiceCut + ); + { IHorizonStakingTypes.DelegationPool memory pool = staking.getDelegationPool(_receiver, _dataService); if (pool.shares > 0) { - tokensDelegation = (_tokens - tokensProtocol - tokensDataService).mulPPMRoundUp( - staking.getDelegationFeeCut(_receiver, _dataService, _paymentType) - ); + collectTokensData.tokensDelegation = (_tokens - + collectTokensData.tokensProtocol - + collectTokensData.tokensDataService).mulPPMRoundUp( + staking.getDelegationFeeCut(_receiver, _dataService, _paymentType) + ); } } - uint256 receiverExpectedPayment = _tokens - tokensProtocol - tokensDataService - tokensDelegation; + collectTokensData.receiverExpectedPayment = + _tokens - + collectTokensData.tokensProtocol - + collectTokensData.tokensDataService - + collectTokensData.tokensDelegation; (, address msgSender, ) = vm.readCallers(); vm.expectEmit(address(payments)); @@ -70,28 +96,49 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { _receiver, _dataService, _tokens, - tokensProtocol, - tokensDataService, - tokensDelegation, - receiverExpectedPayment + collectTokensData.tokensProtocol, + collectTokensData.tokensDataService, + collectTokensData.tokensDelegation, + collectTokensData.receiverExpectedPayment, + _paymentsDestination ); - payments.collect(_paymentType, _receiver, _tokens, _dataService, _dataServiceCut); + payments.collect(_paymentType, _receiver, _tokens, _dataService, _dataServiceCut, _paymentsDestination); // After balances CollectPaymentData memory afterBalances = CollectPaymentData({ escrowBalance: token.balanceOf(address(escrow)), paymentsBalance: token.balanceOf(address(payments)), receiverBalance: token.balanceOf(_receiver), + receiverDestinationBalance: token.balanceOf(_paymentsDestination), delegationPoolBalance: staking.getDelegatedTokensAvailable(_receiver, _dataService), - dataServiceBalance: token.balanceOf(_dataService) + dataServiceBalance: token.balanceOf(_dataService), + receiverStake: staking.getStake(_receiver) }); // Check receiver balance after payment - assertEq(afterBalances.receiverBalance - previousBalances.receiverBalance, receiverExpectedPayment); + assertEq( + afterBalances.receiverBalance - previousBalances.receiverBalance, + _paymentsDestination == _receiver ? collectTokensData.receiverExpectedPayment : 0 + ); assertEq(token.balanceOf(address(payments)), 0); + // Check receiver destination balance after payment + assertEq( + afterBalances.receiverDestinationBalance - previousBalances.receiverDestinationBalance, + _paymentsDestination == address(0) ? 0 : collectTokensData.receiverExpectedPayment + ); + + // Check receiver stake after payment + assertEq( + afterBalances.receiverStake - previousBalances.receiverStake, + _paymentsDestination == address(0) ? collectTokensData.receiverExpectedPayment : 0 + ); + // Check delegation pool balance after payment - assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, tokensDelegation); + assertEq( + afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, + collectTokensData.tokensDelegation + ); // Check that the escrow account has been updated assertEq(previousBalances.escrowBalance, afterBalances.escrowBalance + _tokens); @@ -100,7 +147,10 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { assertEq(previousBalances.paymentsBalance, afterBalances.paymentsBalance); // Check data service balance after payment - assertEq(afterBalances.dataServiceBalance - previousBalances.dataServiceBalance, tokensDataService); + assertEq( + afterBalances.dataServiceBalance - previousBalances.dataServiceBalance, + collectTokensData.tokensDataService + ); } /* @@ -172,7 +222,94 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { users.indexer, amount, subgraphDataServiceAddress, - dataServiceCut + dataServiceCut, + users.indexer + ); + vm.stopPrank(); + } + + function testCollect_WithRestaking( + uint256 amount, + uint256 amountToCollect, + uint256 dataServiceCut, + uint256 tokensDelegate, + uint256 delegationFeeCut + ) public useIndexer useProvision(amount, 0, 0) { + amountToCollect = bound(amountToCollect, 1, MAX_STAKING_TOKENS); + dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); + tokensDelegate = bound(tokensDelegate, 1, MAX_STAKING_TOKENS); + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); // Covers zero, max, and everything in between + + // Set delegation fee cut + _setDelegationFeeCut( + users.indexer, + subgraphDataServiceAddress, + IGraphPayments.PaymentTypes.QueryFee, + delegationFeeCut + ); + + // Delegate tokens + tokensDelegate = bound(tokensDelegate, MIN_DELEGATION, MAX_STAKING_TOKENS); + vm.startPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, tokensDelegate, 0); + + // Add tokens in escrow + address escrowAddress = address(escrow); + mint(escrowAddress, amount); + vm.startPrank(escrowAddress); + approve(address(payments), amount); + + // Collect payments through GraphPayments + _collect( + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + dataServiceCut, + address(0) + ); + vm.stopPrank(); + } + + function testCollect_WithBeneficiary( + uint256 amount, + uint256 amountToCollect, + uint256 dataServiceCut, + uint256 tokensDelegate, + uint256 delegationFeeCut + ) public useIndexer useProvision(amount, 0, 0) { + amountToCollect = bound(amountToCollect, 1, MAX_STAKING_TOKENS); + dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); + tokensDelegate = bound(tokensDelegate, 1, MAX_STAKING_TOKENS); + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); // Covers zero, max, and everything in between + + // Set delegation fee cut + _setDelegationFeeCut( + users.indexer, + subgraphDataServiceAddress, + IGraphPayments.PaymentTypes.QueryFee, + delegationFeeCut + ); + + // Delegate tokens + tokensDelegate = bound(tokensDelegate, MIN_DELEGATION, MAX_STAKING_TOKENS); + vm.startPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, tokensDelegate, 0); + + // Add tokens in escrow + address escrowAddress = address(escrow); + mint(escrowAddress, amount); + vm.startPrank(escrowAddress); + approve(address(payments), amount); + + // Collect payments through GraphPayments + _collect( + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + dataServiceCut, + vm.addr(1) // use some random address as beneficiary ); vm.stopPrank(); } @@ -211,7 +348,8 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { users.indexer, amount, subgraphDataServiceAddress, - dataServiceCut + dataServiceCut, + users.indexer ); vm.stopPrank(); } @@ -238,12 +376,13 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { users.indexer, amount, subgraphDataServiceAddress, - dataServiceCut + dataServiceCut, + users.indexer ); } function testCollect_WithZeroAmount(uint256 amount) public useIndexer useProvision(amount, 0, 0) { - _collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, 0, subgraphDataServiceAddress, 0); + _collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, 0, subgraphDataServiceAddress, 0, users.indexer); } function testCollect_RevertWhen_UnauthorizedCaller(uint256 amount) public useIndexer useProvision(amount, 0, 0) { @@ -256,7 +395,14 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(payments), 0, amount) ); - payments.collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, amount, subgraphDataServiceAddress, 0); + payments.collect( + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + 0, + users.indexer + ); } function testCollect_WithNoDelegation( @@ -287,7 +433,8 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { users.indexer, amount, subgraphDataServiceAddress, - dataServiceCut + dataServiceCut, + users.indexer ); vm.stopPrank(); } @@ -307,7 +454,8 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { users.indexer, amount, subgraphDataServiceAddress, - 100_000 // 10% + 100_000, // 10% + users.indexer ); data[1] = abi.encodeWithSelector( payments.collect.selector, @@ -315,7 +463,8 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { users.indexer, amount, subgraphDataServiceAddress, - 200_000 // 20% + 200_000, // 20% + users.indexer ); payments.multicall(data); diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 7d700aafe..ec6634034 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -6,7 +6,6 @@ import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHo import { IDisputeManager } from "./interfaces/IDisputeManager.sol"; import { ISubgraphService } from "./interfaces/ISubgraphService.sol"; -import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; import { MathUtils } from "@graphprotocol/horizon/contracts/libraries/MathUtils.sol"; @@ -556,7 +555,7 @@ contract DisputeManager is /** * @notice Make the subgraph service contract slash the indexer and reward the fisherman. - * Give the fisherman a reward equal to the fishermanRewardPercentage of slashed amount + * Give the fisherman a reward equal to the fishermanRewardCut of slashed amount * @param _indexer Address of the indexer * @param _tokensSlash Amount of tokens to slash from the indexer * @param _tokensStakeSnapshot Snapshot of the indexer's stake at the time of the dispute creation @@ -627,8 +626,8 @@ contract DisputeManager is } /** - * @notice Set the percent reward that the fisherman gets when slashing occurs. - * @dev Update the reward percentage to `_percentage` + * @notice Set the reward cut that the fisherman gets when slashing occurs. + * @dev Update the reward cut to `_fishermanRewardCut` * @param _fishermanRewardCut The fisherman reward cut, in PPM */ function _setFishermanRewardCut(uint32 _fishermanRewardCut) private { @@ -641,8 +640,8 @@ contract DisputeManager is } /** - * @notice Set the maximum percentage that can be used for slashing indexers. - * @param _maxSlashingCut Max percentage slashing for disputes, in PPM + * @notice Set the maximum cut that can be used for slashing indexers. + * @param _maxSlashingCut Max slashing cut, in PPM */ function _setMaxSlashingCut(uint32 _maxSlashingCut) private { require(PPMMath.isValidPPM(_maxSlashingCut), DisputeManagerInvalidMaxSlashingCut(_maxSlashingCut)); diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index f6cee7d7e..00c997826 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -105,15 +105,15 @@ contract SubgraphService is * @param data Encoded registration data: * - address `url`: The URL of the indexer * - string `geohash`: The geohash of the indexer - * - address `rewardsDestination`: The address where the indexer wants to receive indexing rewards. - * Use zero address for automatic reprovisioning to the subgraph service. + * - address `paymentsDestination`: The address where the indexer wants to receive payments. + * Use zero address for automatically restaking payments. */ /// @inheritdoc IDataService function register( address indexer, bytes calldata data ) external override onlyAuthorizedForProvision(indexer) onlyValidProvision(indexer) whenNotPaused { - (string memory url, string memory geohash, address rewardsDestination_) = abi.decode( + (string memory url, string memory geohash, address paymentsDestination_) = abi.decode( data, (string, string, address) ); @@ -124,8 +124,8 @@ contract SubgraphService is // Register the indexer indexers[indexer] = Indexer({ registeredAt: block.timestamp, url: url, geoHash: geohash }); - if (rewardsDestination_ != address(0)) { - _setRewardsDestination(indexer, rewardsDestination_); + if (paymentsDestination_ != address(0)) { + _setPaymentsDestination(indexer, paymentsDestination_); } emit ServiceProviderRegistered(indexer, data); @@ -249,6 +249,13 @@ contract SubgraphService is * * @param indexer The address of the indexer * @param paymentType The type of payment to collect as defined in {IGraphPayments} + * @param data Encoded data: + * - For query fees: + * - IGraphTallyCollector.SignedRAV `signedRav`: The signed RAV + * - For indexing rewards: + * - address `allocationId`: The id of the allocation + * - bytes32 `poi`: The POI being presented + * - bytes `poiMetadata`: The metadata associated with the POI. See {AllocationManager-_collectIndexingRewards} for more details. */ /// @inheritdoc IDataService function collect( @@ -267,22 +274,9 @@ contract SubgraphService is uint256 paymentCollected = 0; if (paymentType == IGraphPayments.PaymentTypes.QueryFee) { - (IGraphTallyCollector.SignedRAV memory signedRav, uint256 tokensToCollect) = abi.decode( - data, - (IGraphTallyCollector.SignedRAV, uint256) - ); - require( - signedRav.rav.serviceProvider == indexer, - SubgraphServiceIndexerMismatch(signedRav.rav.serviceProvider, indexer) - ); - paymentCollected = _collectQueryFees(signedRav, tokensToCollect); + paymentCollected = _collectQueryFees(indexer, data); } else if (paymentType == IGraphPayments.PaymentTypes.IndexingRewards) { - (address allocationId, bytes32 poi) = abi.decode(data, (address, bytes32)); - require( - _allocations.get(allocationId).indexer == indexer, - SubgraphServiceAllocationNotAuthorized(indexer, allocationId) - ); - paymentCollected = _collectIndexingRewards(allocationId, poi, _delegationRatio); + paymentCollected = _collectIndexingRewards(indexer, data); } else { revert SubgraphServiceInvalidPaymentType(paymentType); } @@ -345,8 +339,8 @@ contract SubgraphService is } /// @inheritdoc ISubgraphService - function setRewardsDestination(address rewardsDestination_) external override { - _setRewardsDestination(msg.sender, rewardsDestination_); + function setPaymentsDestination(address paymentsDestination_) external override { + _setPaymentsDestination(msg.sender, paymentsDestination_); } /// @inheritdoc ISubgraphService @@ -476,24 +470,30 @@ contract SubgraphService is * Emits a {StakeClaimLocked} event. * Emits a {QueryFeesCollected} event. * - * @param _signedRav Signed RAV - * @param tokensToCollect The amount of tokens to collect. Allows partially collecting a RAV. If 0, the entire RAV will + * @param indexer The address of the indexer + * @param data Encoded data: + * - IGraphTallyCollector.SignedRAV `signedRav`: The signed RAV + * - uint256 `tokensToCollect`: The amount of tokens to collect. Allows partially collecting a RAV. If 0, the entire RAV will * be collected. * @return The amount of fees collected */ - function _collectQueryFees( - IGraphTallyCollector.SignedRAV memory _signedRav, - uint256 tokensToCollect - ) private returns (uint256) { - address indexer = _signedRav.rav.serviceProvider; + function _collectQueryFees(address indexer, bytes calldata data) private returns (uint256) { + (IGraphTallyCollector.SignedRAV memory signedRav, uint256 tokensToCollect) = abi.decode( + data, + (IGraphTallyCollector.SignedRAV, uint256) + ); + require( + signedRav.rav.serviceProvider == indexer, + SubgraphServiceIndexerMismatch(signedRav.rav.serviceProvider, indexer) + ); // Check that collectionId (256 bits) is a valid address (160 bits) // collectionId is expected to be a zero padded address so it's safe to cast to uint160 require( - uint256(_signedRav.rav.collectionId) <= type(uint160).max, - SubgraphServiceInvalidCollectionId(_signedRav.rav.collectionId) + uint256(signedRav.rav.collectionId) <= type(uint160).max, + SubgraphServiceInvalidCollectionId(signedRav.rav.collectionId) ); - address allocationId = address(uint160(uint256(_signedRav.rav.collectionId))); + address allocationId = address(uint160(uint256(signedRav.rav.collectionId))); Allocation.State memory allocation = _allocations.get(allocationId); // Check RAV is consistent - RAV indexer must match the allocation's indexer @@ -504,24 +504,29 @@ contract SubgraphService is _releaseStake(indexer, 0); // Collect from GraphPayments - only curators cut is sent back to the subgraph service - uint256 balanceBefore = _graphToken().balanceOf(address(this)); - - uint256 curationCut = _curation().isCurated(subgraphDeploymentId) ? curationFeesCut : 0; - uint256 tokensCollected = _graphTallyCollector().collect( - IGraphPayments.PaymentTypes.QueryFee, - abi.encode(_signedRav, curationCut), - tokensToCollect - ); + uint256 tokensCollected; + uint256 tokensCurators; + { + uint256 balanceBefore = _graphToken().balanceOf(address(this)); + + tokensCollected = _graphTallyCollector().collect( + IGraphPayments.PaymentTypes.QueryFee, + _encodeGraphTallyData(signedRav, _curation().isCurated(subgraphDeploymentId) ? curationFeesCut : 0), + tokensToCollect + ); - uint256 balanceAfter = _graphToken().balanceOf(address(this)); - require(balanceAfter >= balanceBefore, SubgraphServiceInconsistentCollection(balanceBefore, balanceAfter)); - uint256 tokensCurators = balanceAfter - balanceBefore; + uint256 balanceAfter = _graphToken().balanceOf(address(this)); + require(balanceAfter >= balanceBefore, SubgraphServiceInconsistentCollection(balanceBefore, balanceAfter)); + tokensCurators = balanceAfter - balanceBefore; + } if (tokensCollected > 0) { // lock stake as economic security for fees - uint256 tokensToLock = tokensCollected * stakeToFeesRatio; - uint256 unlockTimestamp = block.timestamp + _disputeManager().getDisputePeriod(); - _lockStake(indexer, tokensToLock, unlockTimestamp); + _lockStake( + indexer, + tokensCollected * stakeToFeesRatio, + block.timestamp + _disputeManager().getDisputePeriod() + ); if (tokensCurators > 0) { // curation collection changes subgraph signal so we take rewards snapshot @@ -535,7 +540,7 @@ contract SubgraphService is emit QueryFeesCollected( indexer, - _signedRav.rav.payer, + signedRav.rav.payer, allocationId, subgraphDeploymentId, tokensCollected, @@ -544,6 +549,35 @@ contract SubgraphService is return tokensCollected; } + /** + * @notice Collect indexing rewards + * @param indexer The address of the indexer + * @param data Encoded data: + * - address `allocationId`: The id of the allocation + * - bytes32 `poi`: The POI being presented + * - bytes `poiMetadata`: The metadata associated with the POI. See {AllocationManager-_presentPOI} for more details. + * @return The amount of indexing rewards collected + */ + function _collectIndexingRewards(address indexer, bytes calldata data) private returns (uint256) { + (address allocationId, bytes32 poi_, bytes memory poiMetadata_) = abi.decode(data, (address, bytes32, bytes)); + require( + _allocations.get(allocationId).indexer == indexer, + SubgraphServiceAllocationNotAuthorized(indexer, allocationId) + ); + return _presentPOI(allocationId, poi_, poiMetadata_, _delegationRatio, paymentsDestination[indexer]); + } + + /** + * @notice Sets the payments destination for an indexer to receive payments + * @dev Emits a {PaymentsDestinationSet} event + * @param _indexer The address of the indexer + * @param _paymentsDestination The address where payments should be sent + */ + function _setPaymentsDestination(address _indexer, address _paymentsDestination) internal { + paymentsDestination[_indexer] = _paymentsDestination; + emit PaymentsDestinationSet(_indexer, _paymentsDestination); + } + /** * @notice Set the stake to fees ratio. * @param _stakeToFeesRatio The stake to fees ratio @@ -553,4 +587,18 @@ contract SubgraphService is stakeToFeesRatio = _stakeToFeesRatio; emit StakeToFeesRatioSet(_stakeToFeesRatio); } + + /** + * @notice Encodes the data for the GraphTallyCollector + * @dev The purpose of this function is just to avoid stack too deep errors + * @param signedRav The signed RAV + * @param curationCut The curation cut + * @return The encoded data + */ + function _encodeGraphTallyData( + IGraphTallyCollector.SignedRAV memory signedRav, + uint256 curationCut + ) private view returns (bytes memory) { + return abi.encode(signedRav, curationCut, paymentsDestination[signedRav.rav.serviceProvider]); + } } diff --git a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol index 9e08d5505..06ada3a59 100644 --- a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol +++ b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol @@ -18,4 +18,7 @@ abstract contract SubgraphServiceV1Storage { /// @notice The cut curators take from query fee payments. In PPM. uint256 public curationFeesCut; + + /// @notice Destination of indexer payments + mapping(address indexer => address destination) public paymentsDestination; } diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol index af2a2a953..5c35296f2 100644 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol @@ -48,6 +48,13 @@ interface ISubgraphService is IDataServiceFees { uint256 tokensCurators ); + /** + * @notice Emitted when an indexer sets a new payments destination + * @param indexer The address of the indexer + * @param paymentsDestination The address where payments should be sent + */ + event PaymentsDestinationSet(address indexed indexer, address indexed paymentsDestination); + /** * @notice Emitted when the stake to fees ratio is set. * @param ratio The stake to fees ratio @@ -244,11 +251,11 @@ interface ISubgraphService is IDataServiceFees { function setCurationCut(uint256 curationCut) external; /** - * @notice Sets the rewards destination for an indexer to receive indexing rewards - * @dev Emits a {RewardsDestinationSet} event - * @param rewardsDestination The address where indexing rewards should be sent + * @notice Sets the payments destination for an indexer to receive payments + * @dev Emits a {PaymentsDestinationSet} event + * @param paymentsDestination The address where payments should be sent */ - function setRewardsDestination(address rewardsDestination) external; + function setPaymentsDestination(address paymentsDestination) external; /** * @notice Gets the details of an allocation diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index cd5ad6451..78e5fa190 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -62,6 +62,7 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca * @param tokensDelegationRewards The amount of tokens collected for delegators * @param poi The POI presented * @param currentEpoch The current epoch + * @param poiMetadata The metadata associated with the POI */ event IndexingRewardsCollected( address indexed indexer, @@ -71,6 +72,7 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca uint256 tokensIndexerRewards, uint256 tokensDelegationRewards, bytes32 poi, + bytes poiMetadata, uint256 currentEpoch ); @@ -118,13 +120,6 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca bytes32 indexed subgraphDeploymentId ); - /** - * @notice Emitted when an indexer sets a new indexing rewards destination - * @param indexer The address of the indexer - * @param rewardsDestination The address where indexing rewards should be sent - */ - event RewardsDestinationSet(address indexed indexer, address indexed rewardsDestination); - /** * @notice Emitted when the maximum POI staleness is updated * @param maxPOIStaleness The max POI staleness in seconds @@ -261,23 +256,25 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca * * @param _allocationId The id of the allocation to collect rewards for * @param _poi The POI being presented + * @param _poiMetadata The metadata associated with the POI. The data and encoding format is for off-chain components to define, this function will only emit the value in an event as-is. * @param _delegationRatio The delegation ratio to consider when locking tokens + * @param _paymentsDestination The address where indexing rewards should be sent * @return The amount of tokens collected */ - function _collectIndexingRewards( + function _presentPOI( address _allocationId, bytes32 _poi, - uint32 _delegationRatio + bytes memory _poiMetadata, + uint32 _delegationRatio, + address _paymentsDestination ) internal returns (uint256) { Allocation.State memory allocation = _allocations.get(_allocationId); require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId)); - uint256 currentEpoch = _graphEpochManager().currentEpoch(); - // Mint indexing rewards if all conditions are met uint256 tokensRewards = (!allocation.isStale(maxPOIStaleness) && !allocation.isAltruistic() && - _poi != bytes32(0)) && currentEpoch > allocation.createdAtEpoch + _poi != bytes32(0)) && _graphEpochManager().currentEpoch() > allocation.createdAtEpoch ? _graphRewardsManager().takeRewards(_allocationId) : 0; @@ -314,12 +311,11 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca // Distribute rewards to indexer tokensIndexerRewards = tokensRewards - tokensDelegationRewards; if (tokensIndexerRewards > 0) { - address rewardsDestination = rewardsDestination[allocation.indexer]; - if (rewardsDestination == address(0)) { + if (_paymentsDestination == address(0)) { _graphToken().approve(address(_graphStaking()), tokensIndexerRewards); _graphStaking().stakeToProvision(allocation.indexer, address(this), tokensIndexerRewards); } else { - _graphToken().pushTokens(rewardsDestination, tokensIndexerRewards); + _graphToken().pushTokens(_paymentsDestination, tokensIndexerRewards); } } } @@ -332,7 +328,8 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca tokensIndexerRewards, tokensDelegationRewards, _poi, - currentEpoch + _poiMetadata, + _graphEpochManager().currentEpoch() ); // Check if the indexer is over-allocated and force close the allocation if necessary @@ -437,17 +434,6 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca ); } - /** - * @notice Sets the rewards destination for an indexer to receive indexing rewards - * @dev Emits a {RewardsDestinationSet} event - * @param _indexer The address of the indexer - * @param _rewardsDestination The address where indexing rewards should be sent - */ - function _setRewardsDestination(address _indexer, address _rewardsDestination) internal { - rewardsDestination[_indexer] = _rewardsDestination; - emit RewardsDestinationSet(_indexer, _rewardsDestination); - } - /** * @notice Sets the maximum amount of time, in seconds, allowed between presenting POIs to qualify for indexing rewards * @dev Emits a {MaxPOIStalenessSet} event diff --git a/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol b/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol index e13ee1994..1c4f555d8 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol @@ -23,9 +23,6 @@ abstract contract AllocationManagerV1Storage { /// @notice Maximum amount of time, in seconds, allowed between presenting POIs to qualify for indexing rewards uint256 public maxPOIStaleness; - /// @notice Destination of accrued indexing rewards - mapping(address indexer => address destination) public rewardsDestination; - /// @notice Track total tokens allocated per subgraph deployment /// @dev Used to calculate indexing rewards mapping(bytes32 subgraphDeploymentId => uint256 tokens) internal _subgraphAllocatedTokens; diff --git a/packages/subgraph-service/hardhat.config.ts b/packages/subgraph-service/hardhat.config.ts index 9660e89b1..70334ba1e 100644 --- a/packages/subgraph-service/hardhat.config.ts +++ b/packages/subgraph-service/hardhat.config.ts @@ -23,7 +23,7 @@ const config: HardhatUserConfig = { settings: { optimizer: { enabled: true, - runs: 50, + runs: 10, }, }, }, diff --git a/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol b/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol index e2cf62e83..861e3b688 100644 --- a/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol +++ b/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol @@ -91,7 +91,7 @@ abstract contract SubgraphServiceSharedTest is HorizonStakingSharedTest { assertEq(indexer.geoHash, geohash); // Check rewards destination - assertEq(subgraphService.rewardsDestination(_indexer), rewardsDestination); + assertEq(subgraphService.paymentsDestination(_indexer), rewardsDestination); } function _startService(address _indexer, bytes memory _data) internal { diff --git a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol index 66117c32b..0d30baa2e 100644 --- a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol +++ b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol @@ -63,13 +63,13 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { (, address indexer, ) = vm.readCallers(); vm.expectEmit(address(subgraphService)); - emit AllocationManager.RewardsDestinationSet(indexer, _rewardsDestination); + emit ISubgraphService.PaymentsDestinationSet(indexer, _rewardsDestination); // Set rewards destination - subgraphService.setRewardsDestination(_rewardsDestination); + subgraphService.setPaymentsDestination(_rewardsDestination); // Check rewards destination - assertEq(subgraphService.rewardsDestination(indexer), _rewardsDestination); + assertEq(subgraphService.paymentsDestination(indexer), _rewardsDestination); } function _acceptProvision(address _indexer, bytes memory _data) internal { @@ -180,6 +180,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { struct IndexingRewardsData { bytes32 poi; + bytes poiMetadata; uint256 tokensIndexerRewards; uint256 tokensDelegationRewards; } @@ -196,6 +197,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { uint256 indexerBalance; uint256 curationBalance; uint256 lockedTokens; + uint256 indexerStake; } function _collect(address _indexer, IGraphPayments.PaymentTypes _paymentType, bytes memory _data) internal { @@ -239,9 +241,9 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { } function _collectPaymentDataBefore(address _indexer) private view returns (CollectPaymentData memory) { - address rewardsDestination = subgraphService.rewardsDestination(_indexer); + address paymentsDestination = subgraphService.paymentsDestination(_indexer); CollectPaymentData memory collectPaymentDataBefore; - collectPaymentDataBefore.rewardsDestinationBalance = token.balanceOf(rewardsDestination); + collectPaymentDataBefore.rewardsDestinationBalance = token.balanceOf(paymentsDestination); collectPaymentDataBefore.indexerProvisionBalance = staking.getProviderTokensAvailable( _indexer, address(subgraphService) @@ -253,13 +255,14 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { collectPaymentDataBefore.indexerBalance = token.balanceOf(_indexer); collectPaymentDataBefore.curationBalance = token.balanceOf(address(curation)); collectPaymentDataBefore.lockedTokens = subgraphService.feesProvisionTracker(_indexer); + collectPaymentDataBefore.indexerStake = staking.getStake(_indexer); return collectPaymentDataBefore; } function _collectPaymentDataAfter(address _indexer) private view returns (CollectPaymentData memory) { CollectPaymentData memory collectPaymentDataAfter; - address rewardsDestination = subgraphService.rewardsDestination(_indexer); - collectPaymentDataAfter.rewardsDestinationBalance = token.balanceOf(rewardsDestination); + address paymentsDestination = subgraphService.paymentsDestination(_indexer); + collectPaymentDataAfter.rewardsDestinationBalance = token.balanceOf(paymentsDestination); collectPaymentDataAfter.indexerProvisionBalance = staking.getProviderTokensAvailable( _indexer, address(subgraphService) @@ -271,6 +274,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { collectPaymentDataAfter.indexerBalance = token.balanceOf(_indexer); collectPaymentDataAfter.curationBalance = token.balanceOf(address(curation)); collectPaymentDataAfter.lockedTokens = subgraphService.feesProvisionTracker(_indexer); + collectPaymentDataAfter.indexerStake = staking.getStake(_indexer); return collectPaymentDataAfter; } @@ -326,7 +330,10 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { function _handleIndexingRewardsCollection( bytes memory _data ) private returns (uint256 paymentCollected, address allocationId, IndexingRewardsData memory indexingRewardsData) { - (allocationId, indexingRewardsData.poi) = abi.decode(_data, (address, bytes32)); + (allocationId, indexingRewardsData.poi, indexingRewardsData.poiMetadata) = abi.decode( + _data, + (address, bytes32, bytes) + ); Allocation.State memory allocation = subgraphService.getAllocation(allocationId); // Calculate accumulated tokens, this depends on the rewards manager which we have mocked @@ -360,6 +367,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { indexingRewardsData.tokensIndexerRewards, indexingRewardsData.tokensDelegationRewards, indexingRewardsData.poi, + indexingRewardsData.poiMetadata, epochManager.currentEpoch() ); @@ -386,8 +394,24 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { uint256 expectedIndexerTokensPayment = _paymentCollected - tokensProtocol - curationTokens; assertEq( - collectPaymentDataAfter.indexerBalance, - collectPaymentDataBefore.indexerBalance + expectedIndexerTokensPayment + collectPaymentDataAfter.indexerBalance - collectPaymentDataBefore.indexerBalance, + subgraphService.paymentsDestination(signedRav.rav.serviceProvider) == address(0) + ? 0 + : expectedIndexerTokensPayment + ); + + assertEq( + collectPaymentDataAfter.rewardsDestinationBalance - collectPaymentDataBefore.rewardsDestinationBalance, + subgraphService.paymentsDestination(signedRav.rav.serviceProvider) == address(0) + ? 0 + : expectedIndexerTokensPayment + ); + + assertEq( + collectPaymentDataAfter.indexerStake - collectPaymentDataBefore.indexerStake, + subgraphService.paymentsDestination(signedRav.rav.serviceProvider) == address(0) + ? expectedIndexerTokensPayment + : 0 ); assertEq(collectPaymentDataAfter.curationBalance, collectPaymentDataBefore.curationBalance + curationTokens); @@ -424,8 +448,8 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { assertEq(allocation.lastPOIPresentedAt, block.timestamp); // Check indexer got paid the correct amount - address rewardsDestination = subgraphService.rewardsDestination(_indexer); - if (rewardsDestination == address(0)) { + address paymentsDestination = subgraphService.paymentsDestination(_indexer); + if (paymentsDestination == address(0)) { // If rewards destination is address zero indexer should get paid to their provision balance assertEq( collectPaymentDataAfter.indexerProvisionBalance, @@ -514,4 +538,14 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { (uint256 tokens, uint256 createdAt, uint256 releasableAt, bytes32 nextClaim) = subgraphService.claims(_claimId); return IDataServiceFees.StakeClaim(tokens, createdAt, releasableAt, nextClaim); } + + // This doesn't matter for testing because the metadata is not decoded onchain but it's expected to be of the form: + // - uint256 blockNumber - the block number (indexed chain) the poi’s where computed at + // - bytes32 publicPOI - the public POI matching the presenting poi + // - uint8 indexingStatus - status (failed, syncing, etc). Mapping maintained by indexer agent. + // - uint8 errorCode - Again up to indexer agent, but seems sensible to use 0 if no error, and error codes for anything else. + // - uint256 errorBlockNumber - Block number (indexed chain) where the indexing error happens. 0 if no error. + function _getHardcodedPOIMetadata() internal view returns (bytes memory) { + return abi.encode(block.number, bytes32("PUBLIC_POI1"), uint8(0), uint8(0), uint256(0)); + } } diff --git a/packages/subgraph-service/test/subgraphService/allocation/forceClose.t.sol b/packages/subgraph-service/test/subgraphService/allocation/forceClose.t.sol index 9ade5bb2c..993b3f148 100644 --- a/packages/subgraph-service/test/subgraphService/allocation/forceClose.t.sol +++ b/packages/subgraph-service/test/subgraphService/allocation/forceClose.t.sol @@ -36,7 +36,7 @@ contract SubgraphServiceAllocationForceCloseTest is SubgraphServiceTest { // Skip forward skip(timeBetweenPOIs); - bytes memory data = abi.encode(allocationID, bytes32("POI1")); + bytes memory data = abi.encode(allocationID, bytes32("POI1"), _getHardcodedPOIMetadata()); _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); } @@ -61,7 +61,7 @@ contract SubgraphServiceAllocationForceCloseTest is SubgraphServiceTest { resetPrank(users.indexer); - bytes memory data = abi.encode(allocationID, bytes32("POI1")); + bytes memory data = abi.encode(allocationID, bytes32("POI1"), _getHardcodedPOIMetadata()); _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); resetPrank(permissionlessBob); diff --git a/packages/subgraph-service/test/subgraphService/allocation/resize.t.sol b/packages/subgraph-service/test/subgraphService/allocation/resize.t.sol index 9667309a1..6ff7add3f 100644 --- a/packages/subgraph-service/test/subgraphService/allocation/resize.t.sol +++ b/packages/subgraph-service/test/subgraphService/allocation/resize.t.sol @@ -39,7 +39,7 @@ contract SubgraphServiceAllocationResizeTest is SubgraphServiceTest { vm.roll(block.number + EPOCH_LENGTH); IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; - bytes memory data = abi.encode(allocationID, bytes32("POI1")); + bytes memory data = abi.encode(allocationID, bytes32("POI1"), _getHardcodedPOIMetadata()); _collect(users.indexer, paymentType, data); _addToProvision(users.indexer, resizeTokens); _resizeAllocation(users.indexer, allocationID, resizeTokens); diff --git a/packages/subgraph-service/test/subgraphService/collect/indexing/indexing.t.sol b/packages/subgraph-service/test/subgraphService/collect/indexing/indexing.t.sol index 4f0d6769d..580e5eb3c 100644 --- a/packages/subgraph-service/test/subgraphService/collect/indexing/indexing.t.sol +++ b/packages/subgraph-service/test/subgraphService/collect/indexing/indexing.t.sol @@ -15,7 +15,7 @@ contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { function test_SubgraphService_Collect_Indexing(uint256 tokens) public useIndexer useAllocation(tokens) { IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; - bytes memory data = abi.encode(allocationID, bytes32("POI")); + bytes memory data = abi.encode(allocationID, bytes32("POI"), _getHardcodedPOIMetadata()); // skip time to ensure allocation gets rewards vm.roll(block.number + EPOCH_LENGTH); @@ -40,7 +40,7 @@ contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { vm.roll(block.number + EPOCH_LENGTH); IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; - bytes memory data = abi.encode(allocationID, bytes32("POI")); + bytes memory data = abi.encode(allocationID, bytes32("POI"), _getHardcodedPOIMetadata()); _collect(users.indexer, paymentType, data); } @@ -65,7 +65,7 @@ contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { resetPrank(users.indexer); IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; - bytes memory data = abi.encode(allocationID, bytes32("POI")); + bytes memory data = abi.encode(allocationID, bytes32("POI"), _getHardcodedPOIMetadata()); _collect(users.indexer, paymentType, data); } @@ -76,7 +76,7 @@ contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { vm.roll(block.number + EPOCH_LENGTH); IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; - bytes memory data = abi.encode(allocationID, bytes32("POI")); + bytes memory data = abi.encode(allocationID, bytes32("POI"), _getHardcodedPOIMetadata()); _collect(users.indexer, paymentType, data); } @@ -92,7 +92,7 @@ contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { resetPrank(users.indexer); - bytes memory data = abi.encode(allocationID, bytes32("POI")); + bytes memory data = abi.encode(allocationID, bytes32("POI"), _getHardcodedPOIMetadata()); _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); } } @@ -118,7 +118,7 @@ contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { resetPrank(users.indexer); - bytes memory data = abi.encode(allocationID, bytes32("POI")); + bytes memory data = abi.encode(allocationID, bytes32("POI"), _getHardcodedPOIMetadata()); _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); } } @@ -145,7 +145,7 @@ contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { // this collection should close the allocation IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; - bytes memory collectData = abi.encode(allocationID, bytes32("POI")); + bytes memory collectData = abi.encode(allocationID, bytes32("POI"), _getHardcodedPOIMetadata()); _collect(users.indexer, paymentType, collectData); } @@ -156,7 +156,7 @@ contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { // Setup new indexer address newIndexer = makeAddr("newIndexer"); _createAndStartAllocation(newIndexer, tokens); - bytes memory data = abi.encode(allocationID, bytes32("POI")); + bytes memory data = abi.encode(allocationID, bytes32("POI"), _getHardcodedPOIMetadata()); // skip time to ensure allocation gets rewards vm.roll(block.number + EPOCH_LENGTH); 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 10068f745..d5913be51 100644 --- a/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol +++ b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol @@ -117,6 +117,26 @@ contract SubgraphServiceRegisterTest is SubgraphServiceTest { _collect(users.indexer, IGraphPayments.PaymentTypes.QueryFee, data); } + function testCollect_QueryFees_WithRewardsDestination( + uint256 tokensAllocated, + uint256 tokensPayment + ) public useIndexer useAllocation(tokensAllocated) { + vm.assume(tokensAllocated > minimumProvisionTokens * stakeToFeesRatio); + uint256 maxTokensPayment = tokensAllocated / stakeToFeesRatio > type(uint128).max + ? type(uint128).max + : tokensAllocated / stakeToFeesRatio; + tokensPayment = bound(tokensPayment, minimumProvisionTokens, maxTokensPayment); + + resetPrank(users.gateway); + _deposit(tokensPayment); + _authorizeSigner(); + + resetPrank(users.indexer); + subgraphService.setPaymentsDestination(users.indexer); + bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokensPayment), 0); + _collect(users.indexer, IGraphPayments.PaymentTypes.QueryFee, data); + } + function testCollect_MultipleQueryFees( uint256 tokensAllocated, uint8 numPayments