diff --git a/.gitignore b/.gitignore index 89efef0a5..5861633e5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,8 +34,10 @@ bin/ .vscode # Coverage and other reports +coverage/ reports/ coverage.json +lcov.info # Local test files addresses-local.json diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol index eaac08ae4..f62828868 100644 --- a/packages/horizon/contracts/interfaces/IGraphPayments.sol +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -20,6 +20,7 @@ interface IGraphPayments { /** * @notice Emitted when a payment is collected + * @param paymentType The type of payment as defined in {IGraphPayments} * @param payer The address of the payer * @param receiver The address of the receiver * @param dataService The address of the data service @@ -30,8 +31,9 @@ interface IGraphPayments { * @param tokensReceiver Amount of tokens for the receiver */ event GraphPaymentCollected( + PaymentTypes indexed paymentType, address indexed payer, - address indexed receiver, + address receiver, address indexed dataService, uint256 tokens, uint256 tokensProtocol, @@ -40,14 +42,6 @@ interface IGraphPayments { uint256 tokensReceiver ); - /** - * @notice Thrown when the calculated amount of tokens to be paid out to all parties is - * not the same as the amount of tokens being collected - * @param tokens The amount of tokens being collected - * @param tokensCalculated The sum of all the tokens to be paid out - */ - error GraphPaymentsBadAccounting(uint256 tokens, uint256 tokensCalculated); - /** * @notice Thrown when the protocol payment cut is invalid * @param protocolPaymentCut The protocol payment cut @@ -63,9 +57,10 @@ interface IGraphPayments { /** * @notice Collects funds from a payer. * It will pay cuts to all relevant parties and forward the rest to the receiver. + * 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 tokens The amount of tokens being collected. * @param dataService The address of the data service * @param dataServiceCut The data service cut in PPM */ diff --git a/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol b/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol index 760a086a7..da05d8c9f 100644 --- a/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol +++ b/packages/horizon/contracts/interfaces/IPaymentsEscrow.sol @@ -37,9 +37,18 @@ interface IPaymentsEscrow { /** * @notice Emitted when a payer cancels an escrow thawing * @param payer The address of the payer + * @param collector The address of the collector * @param receiver The address of the receiver + * @param tokensThawing The amount of tokens that were being thawed + * @param thawEndTimestamp The timestamp at which the thawing period was ending */ - event CancelThaw(address indexed payer, address indexed receiver); + event CancelThaw( + address indexed payer, + address indexed collector, + address indexed receiver, + uint256 tokensThawing, + uint256 thawEndTimestamp + ); /** * @notice Emitted when a payer thaws funds from the escrow for a payer-collector-receiver tuple @@ -68,12 +77,19 @@ interface IPaymentsEscrow { /** * @notice Emitted when a collector collects funds from the escrow for a payer-collector-receiver tuple + * @param paymentType The type of payment being collected as defined in the {IGraphPayments} interface * @param payer The address of the payer * @param collector The address of the collector * @param receiver The address of the receiver * @param tokens The amount of tokens collected */ - event EscrowCollected(address indexed payer, address indexed collector, address indexed receiver, uint256 tokens); + event EscrowCollected( + IGraphPayments.PaymentTypes indexed paymentType, + address indexed payer, + address indexed collector, + address receiver, + uint256 tokens + ); // -- Errors -- @@ -145,13 +161,12 @@ interface IPaymentsEscrow { /** * @notice Thaw a specific amount of escrow from a payer-collector-receiver's escrow account. * The payer is the transaction caller. - * If `tokens` is zero and funds were already thawing it will cancel the thawing. * Note that repeated calls to this function will overwrite the previous thawing amount * and reset the thawing period. * @dev Requirements: * - `tokens` must be less than or equal to the available balance * - * Emits a {Thaw} event. If `tokens` is zero it will emit a {CancelThaw} event. + * Emits a {Thaw} event. * * @param collector The address of the collector * @param receiver The address of the receiver @@ -159,6 +174,16 @@ interface IPaymentsEscrow { */ function thaw(address collector, address receiver, uint256 tokens) external; + /** + * @notice Cancels the thawing of escrow from a payer-collector-receiver's escrow account. + * @param collector The address of the collector + * @param receiver The address of the receiver + * @dev Requirements: + * - The payer must be thawing funds + * Emits a {CancelThaw} event. + */ + function cancelThaw(address collector, address receiver) external; + /** * @notice Withdraws all thawed escrow from a payer-collector-receiver's escrow account. * The payer is the transaction caller. diff --git a/packages/horizon/contracts/interfaces/ITAPCollector.sol b/packages/horizon/contracts/interfaces/ITAPCollector.sol index f9c482308..fd17d15c7 100644 --- a/packages/horizon/contracts/interfaces/ITAPCollector.sol +++ b/packages/horizon/contracts/interfaces/ITAPCollector.sol @@ -29,10 +29,10 @@ interface ITAPCollector is IPaymentsCollector { bytes32 collectionId; // The address of the payer the RAV was issued by address payer; - // The address of the data service the RAV was issued to - address dataService; // The address of the service provider the RAV was issued to address serviceProvider; + // The address of the data service the RAV was issued to + address dataService; // The RAV timestamp, indicating the latest TAP Receipt in the RAV uint64 timestampNs; // Total amount owed to the service provider since the beginning of the diff --git a/packages/horizon/contracts/payments/GraphPayments.sol b/packages/horizon/contracts/payments/GraphPayments.sol index 0dd06ef72..a32a8a523 100644 --- a/packages/horizon/contracts/payments/GraphPayments.sol +++ b/packages/horizon/contracts/payments/GraphPayments.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; +import { IHorizonStakingTypes } from "../interfaces/internal/IHorizonStakingTypes.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; @@ -22,6 +23,8 @@ import { GraphDirectory } from "../utilities/GraphDirectory.sol"; contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, IGraphPayments { using TokenUtils for IGraphToken; using PPMMath for uint256; + + /// @notice Protocol payment cut in PPM uint256 public immutable PROTOCOL_PAYMENT_CUT; /** @@ -70,14 +73,14 @@ contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, I uint256 tokensDataService = tokensRemaining.mulPPMRoundUp(dataServiceCut); tokensRemaining = tokensRemaining - tokensDataService; - uint256 tokensDelegationPool = tokensRemaining.mulPPMRoundUp( - _graphStaking().getDelegationFeeCut(receiver, dataService, paymentType) - ); - tokensRemaining = tokensRemaining - tokensDelegationPool; - - // Ensure accounting is correct - uint256 tokensTotal = tokensProtocol + tokensDataService + tokensDelegationPool + tokensRemaining; - require(tokens == tokensTotal, GraphPaymentsBadAccounting(tokens, tokensTotal)); + uint256 tokensDelegationPool = 0; + IHorizonStakingTypes.DelegationPool memory pool = _graphStaking().getDelegationPool(receiver, dataService); + if (pool.shares > 0) { + tokensDelegationPool = tokensRemaining.mulPPMRoundUp( + _graphStaking().getDelegationFeeCut(receiver, dataService, paymentType) + ); + tokensRemaining = tokensRemaining - tokensDelegationPool; + } // Pay all parties _graphToken().burnTokens(tokensProtocol); @@ -92,6 +95,7 @@ contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, I _graphToken().pushTokens(receiver, tokensRemaining); emit GraphPaymentCollected( + paymentType, msg.sender, receiver, dataService, diff --git a/packages/horizon/contracts/payments/PaymentsEscrow.sol b/packages/horizon/contracts/payments/PaymentsEscrow.sol index 6f4252873..c1de3bce1 100644 --- a/packages/horizon/contracts/payments/PaymentsEscrow.sol +++ b/packages/horizon/contracts/payments/PaymentsEscrow.sol @@ -23,10 +23,6 @@ import { GraphDirectory } from "../utilities/GraphDirectory.sol"; contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, IPaymentsEscrow { using TokenUtils for IGraphToken; - /// @notice Escrow account details for payer-collector-receiver tuples - mapping(address payer => mapping(address collector => mapping(address receiver => IPaymentsEscrow.EscrowAccount escrowAccount))) - public escrowAccounts; - /// @notice The maximum thawing period (in seconds) for both escrow withdrawal and collector revocation /// @dev This is a precautionary measure to avoid inadvertedly locking funds for too long uint256 public constant MAX_WAIT_PERIOD = 90 days; @@ -34,6 +30,14 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, /// @notice Thawing period in seconds for escrow funds withdrawal uint256 public immutable WITHDRAW_ESCROW_THAWING_PERIOD; + /// @notice Escrow account details for payer-collector-receiver tuples + mapping(address payer => mapping(address collector => mapping(address receiver => IPaymentsEscrow.EscrowAccount escrowAccount))) + public escrowAccounts; + + /** + * @notice Modifier to prevent function execution when contract is paused + * @dev Reverts if the controller indicates the contract is paused + */ modifier notPaused() { require(!_graphController().paused(), PaymentsEscrowIsPaused()); _; @@ -78,19 +82,9 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, * @notice See {IPaymentsEscrow-thaw} */ function thaw(address collector, address receiver, uint256 tokens) external override notPaused { - EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver]; - - // if amount thawing is zero and requested amount is zero this is an invalid request. - // otherwise if amount thawing is greater than zero and requested amount is zero this - // is a cancel thaw request. - if (tokens == 0) { - require(account.tokensThawing != 0, PaymentsEscrowNotThawing()); - account.tokensThawing = 0; - account.thawEndTimestamp = 0; - emit CancelThaw(msg.sender, receiver); - return; - } + require(tokens > 0, PaymentsEscrowInvalidZeroTokens()); + EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver]; require(account.balance >= tokens, PaymentsEscrowInsufficientBalance(account.balance, tokens)); account.tokensThawing = tokens; @@ -99,6 +93,21 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, emit Thaw(msg.sender, collector, receiver, tokens, account.thawEndTimestamp); } + /** + * @notice See {IPaymentsEscrow-cancelThaw} + */ + function cancelThaw(address collector, address receiver) external override notPaused { + EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver]; + require(account.tokensThawing != 0, PaymentsEscrowNotThawing()); + + uint256 tokensThawing = account.tokensThawing; + uint256 thawEndTimestamp = account.thawEndTimestamp; + account.tokensThawing = 0; + account.thawEndTimestamp = 0; + + emit CancelThaw(msg.sender, collector, receiver, tokensThawing, thawEndTimestamp); + } + /** * @notice See {IPaymentsEscrow-withdraw} */ @@ -138,18 +147,19 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, // Reduce amount from account balance account.balance -= tokens; - uint256 balanceBefore = _graphToken().balanceOf(address(this)); + uint256 escrowBalanceBefore = _graphToken().balanceOf(address(this)); _graphToken().approve(address(_graphPayments()), tokens); _graphPayments().collect(paymentType, receiver, tokens, dataService, dataServiceCut); - uint256 balanceAfter = _graphToken().balanceOf(address(this)); + // Verify that the escrow balance is consistent with the collected tokens + uint256 escrowBalanceAfter = _graphToken().balanceOf(address(this)); require( - balanceBefore == tokens + balanceAfter, - PaymentsEscrowInconsistentCollection(balanceBefore, balanceAfter, tokens) + escrowBalanceBefore == tokens + escrowBalanceAfter, + PaymentsEscrowInconsistentCollection(escrowBalanceBefore, escrowBalanceAfter, tokens) ); - emit EscrowCollected(payer, msg.sender, receiver, tokens); + emit EscrowCollected(paymentType, payer, msg.sender, receiver, tokens); } /** @@ -157,10 +167,7 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, */ function getBalance(address payer, address collector, address receiver) external view override returns (uint256) { EscrowAccount storage account = escrowAccounts[payer][collector][receiver]; - if (account.balance <= account.tokensThawing) { - return 0; - } - return account.balance - account.tokensThawing; + return account.balance > account.tokensThawing ? account.balance - account.tokensThawing : 0; } /** diff --git a/packages/horizon/contracts/payments/collectors/TAPCollector.sol b/packages/horizon/contracts/payments/collectors/TAPCollector.sol index 426f84a15..6a19dbe54 100644 --- a/packages/horizon/contracts/payments/collectors/TAPCollector.sol +++ b/packages/horizon/contracts/payments/collectors/TAPCollector.sol @@ -29,7 +29,7 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { /// @notice The EIP712 typehash for the ReceiptAggregateVoucher struct bytes32 private constant EIP712_RAV_TYPEHASH = keccak256( - "ReceiptAggregateVoucher(address payer,address dataService,address serviceProvider,uint64 timestampNs,uint128 valueAggregate,bytes metadata)" + "ReceiptAggregateVoucher(address payer,address serviceProvider,address dataService,uint64 timestampNs,uint128 valueAggregate,bytes metadata)" ); /// @notice Authorization details for payer-signer pairs @@ -166,8 +166,9 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { bytes memory _data, uint256 _tokensToCollect ) private returns (uint256) { - // Ensure caller is the RAV data service (SignedRAV memory signedRAV, uint256 dataServiceCut) = abi.decode(_data, (SignedRAV, uint256)); + + // Ensure caller is the RAV data service require( signedRAV.rav.dataService == msg.sender, TAPCollectorCallerNotDataService(msg.sender, signedRAV.rav.dataService) @@ -259,8 +260,8 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector { abi.encode( EIP712_RAV_TYPEHASH, _rav.payer, - _rav.dataService, _rav.serviceProvider, + _rav.dataService, _rav.timestampNs, _rav.valueAggregate, keccak256(_rav.metadata) diff --git a/packages/horizon/test/GraphBase.t.sol b/packages/horizon/test/GraphBase.t.sol index b2eef0dd9..a01fb2958 100644 --- a/packages/horizon/test/GraphBase.t.sol +++ b/packages/horizon/test/GraphBase.t.sol @@ -41,7 +41,7 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { RewardsManagerMock public rewardsManager; CurationMock public curation; TAPCollector tapCollector; - + HorizonStaking private stakingBase; HorizonStakingExtension private stakingExtension; @@ -103,22 +103,34 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { GraphProxy stakingProxy = new GraphProxy(address(0), address(proxyAdmin)); // GraphPayments predict address - bytes memory paymentsParameters = abi.encode(address(controller), protocolPaymentCut); - bytes memory paymentsBytecode = abi.encodePacked( + bytes memory paymentsImplementationParameters = abi.encode(address(controller), protocolPaymentCut); + bytes memory paymentsImplementationBytecode = abi.encodePacked( type(GraphPayments).creationCode, - paymentsParameters + paymentsImplementationParameters ); - address predictedPaymentsAddress = _computeAddress( + address predictedPaymentsImplementationAddress = _computeAddress( "GraphPayments", - paymentsBytecode, + paymentsImplementationBytecode, users.deployer ); - - // PaymentsEscrow - bytes memory escrowImplementationParameters = abi.encode( - address(controller), - withdrawEscrowThawingPeriod + + bytes memory paymentsProxyParameters = abi.encode( + predictedPaymentsImplementationAddress, + users.governor, + abi.encodeCall(GraphPayments.initialize, ()) + ); + bytes memory paymentsProxyBytecode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + paymentsProxyParameters + ); + address predictedPaymentsProxyAddress = _computeAddress( + "TransparentUpgradeableProxy", + paymentsProxyBytecode, + users.deployer ); + + // PaymentsEscrow + bytes memory escrowImplementationParameters = abi.encode(address(controller), withdrawEscrowThawingPeriod); bytes memory escrowImplementationBytecode = abi.encodePacked( type(PaymentsEscrow).creationCode, escrowImplementationParameters @@ -157,29 +169,32 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { resetPrank(users.governor); controller.setContractProxy(keccak256("GraphToken"), address(token)); controller.setContractProxy(keccak256("PaymentsEscrow"), predictedEscrowProxyAddress); - controller.setContractProxy(keccak256("GraphPayments"), predictedPaymentsAddress); + controller.setContractProxy(keccak256("GraphPayments"), predictedPaymentsProxyAddress); controller.setContractProxy(keccak256("Staking"), address(stakingProxy)); controller.setContractProxy(keccak256("EpochManager"), address(epochManager)); controller.setContractProxy(keccak256("RewardsManager"), address(rewardsManager)); controller.setContractProxy(keccak256("Curation"), address(curation)); controller.setContractProxy(keccak256("GraphTokenGateway"), graphTokenGatewayAddress); controller.setContractProxy(keccak256("GraphProxyAdmin"), address(proxyAdmin)); - - resetPrank(users.deployer); - address paymentsAddress = _deployContract("GraphPayments", paymentsBytecode); - assertEq(paymentsAddress, predictedPaymentsAddress); - payments = GraphPayments(paymentsAddress); - - address escrowImplementationAddress = _deployContract("PaymentsEscrow", escrowImplementationBytecode); - address escrowProxyAddress = _deployContract("TransparentUpgradeableProxy", escrowProxyBytecode); - assertEq(escrowImplementationAddress, predictedEscrowImplementationAddress); - assertEq(escrowProxyAddress, predictedEscrowProxyAddress); - escrow = PaymentsEscrow(escrowProxyAddress); - stakingExtension = new HorizonStakingExtension( - address(controller), - subgraphDataServiceLegacyAddress - ); + resetPrank(users.deployer); + { + address paymentsImplementationAddress = _deployContract("GraphPayments", paymentsImplementationBytecode); + address paymentsProxyAddress = _deployContract("TransparentUpgradeableProxy", paymentsProxyBytecode); + assertEq(paymentsImplementationAddress, predictedPaymentsImplementationAddress); + assertEq(paymentsProxyAddress, predictedPaymentsProxyAddress); + payments = GraphPayments(paymentsProxyAddress); + } + + { + address escrowImplementationAddress = _deployContract("PaymentsEscrow", escrowImplementationBytecode); + address escrowProxyAddress = _deployContract("TransparentUpgradeableProxy", escrowProxyBytecode); + assertEq(escrowImplementationAddress, predictedEscrowImplementationAddress); + assertEq(escrowProxyAddress, predictedEscrowProxyAddress); + escrow = PaymentsEscrow(escrowProxyAddress); + } + + stakingExtension = new HorizonStakingExtension(address(controller), subgraphDataServiceLegacyAddress); stakingBase = new HorizonStaking( address(controller), address(stakingExtension), @@ -229,7 +244,11 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { * PRIVATE */ - function _computeAddress(string memory contractName, bytes memory bytecode, address deployer) private pure returns (address) { + function _computeAddress( + string memory contractName, + bytes memory bytecode, + address deployer + ) private pure returns (address) { bytes32 salt = keccak256(abi.encodePacked(contractName, "Salt")); return Create2.computeAddress(salt, keccak256(bytecode), deployer); } @@ -238,4 +257,4 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { bytes32 salt = keccak256(abi.encodePacked(contractName, "Salt")); return Create2.deploy(0, salt, bytecode); } -} \ No newline at end of file +} diff --git a/packages/horizon/test/escrow/GraphEscrow.t.sol b/packages/horizon/test/escrow/GraphEscrow.t.sol index 120713c6c..ee41a7909 100644 --- a/packages/horizon/test/escrow/GraphEscrow.t.sol +++ b/packages/horizon/test/escrow/GraphEscrow.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; import { IPaymentsEscrow } from "../../contracts/interfaces/IPaymentsEscrow.sol"; import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; +import { IHorizonStakingTypes } from "../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStakingShared.t.sol"; import { PaymentsEscrowSharedTest } from "../shared/payments-escrow/PaymentsEscrowShared.t.sol"; @@ -29,7 +30,9 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { } modifier depositAndThawTokens(uint256 amount, uint256 thawAmount) { + vm.assume(amount > 0); vm.assume(thawAmount > 0); + vm.assume(amount <= MAX_STAKING_TOKENS); vm.assume(amount > thawAmount); _depositTokens(users.verifier, users.indexer, amount); escrow.thaw(users.verifier, users.indexer, thawAmount); @@ -56,6 +59,43 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { assertEq(thawEndTimestamp, expectedThawEndTimestamp); } + function _cancelThawEscrow(address collector, address receiver) internal { + (, address msgSender, ) = vm.readCallers(); + (, uint256 amountThawingBefore, uint256 thawEndTimestampBefore) = escrow.escrowAccounts(msgSender, collector, receiver); + + vm.expectEmit(address(escrow)); + emit IPaymentsEscrow.CancelThaw(msgSender, collector, receiver, amountThawingBefore, thawEndTimestampBefore); + escrow.cancelThaw(collector, receiver); + + (, uint256 amountThawing, uint256 thawEndTimestamp) = escrow.escrowAccounts(msgSender, collector, receiver); + assertEq(amountThawing, 0); + assertEq(thawEndTimestamp, 0); + } + + function _withdrawEscrow(address collector, address receiver) internal { + (, address msgSender, ) = vm.readCallers(); + + (uint256 balanceBefore, uint256 amountThawingBefore, ) = escrow.escrowAccounts(msgSender, collector, receiver); + uint256 tokenBalanceBeforeSender = token.balanceOf(msgSender); + uint256 tokenBalanceBeforeEscrow = token.balanceOf(address(escrow)); + + uint256 amountToWithdraw = amountThawingBefore > balanceBefore ? balanceBefore : amountThawingBefore; + vm.expectEmit(address(escrow)); + emit IPaymentsEscrow.Withdraw(msgSender, collector, receiver, amountToWithdraw); + escrow.withdraw(collector, receiver); + + (uint256 balanceAfter, uint256 tokensThawingAfter, uint256 thawEndTimestampAfter) = escrow.escrowAccounts(msgSender, collector, receiver); + uint256 tokenBalanceAfterSender = token.balanceOf(msgSender); + uint256 tokenBalanceAfterEscrow = token.balanceOf(address(escrow)); + + assertEq(balanceAfter, balanceBefore - amountToWithdraw); + assertEq(tokensThawingAfter, 0); + assertEq(thawEndTimestampAfter, 0); + + assertEq(tokenBalanceAfterSender, tokenBalanceBeforeSender + amountToWithdraw); + assertEq(tokenBalanceAfterEscrow, tokenBalanceBeforeEscrow - amountToWithdraw); + } + struct CollectPaymentData { uint256 escrowBalance; uint256 paymentsBalance; @@ -91,7 +131,7 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { } vm.expectEmit(address(escrow)); - emit IPaymentsEscrow.EscrowCollected(_payer, _collector, _receiver, _tokens); + emit IPaymentsEscrow.EscrowCollected(_paymentType, _payer, _collector, _receiver, _tokens); escrow.collect(_paymentType, _payer, _receiver, _tokens, _dataService, _dataServiceCut); // Calculate cuts @@ -99,9 +139,12 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { uint256 tokensDataService = (_tokens - _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT())).mulPPMRoundUp( _dataServiceCut ); - uint256 tokensDelegation = (_tokens - - _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()) - - tokensDataService).mulPPMRoundUp(staking.getDelegationFeeCut(_receiver, _dataService, _paymentType)); + 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)); + } uint256 receiverExpectedPayment = _tokens - _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()) - tokensDataService - diff --git a/packages/horizon/test/escrow/collect.t.sol b/packages/horizon/test/escrow/collect.t.sol index 55f378b3a..f4357d213 100644 --- a/packages/horizon/test/escrow/collect.t.sol +++ b/packages/horizon/test/escrow/collect.t.sol @@ -13,8 +13,10 @@ contract GraphEscrowCollectTest is GraphEscrowTest { * TESTS */ + // use users.verifier as collector function testCollect_Tokens( uint256 tokens, + uint256 tokensToCollect, uint256 delegationTokens, uint256 dataServiceCut ) @@ -25,13 +27,43 @@ contract GraphEscrowCollectTest is GraphEscrowTest { { dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); + tokensToCollect = bound(tokensToCollect, 1, MAX_STAKING_TOKENS); resetPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + resetPrank(users.gateway); + _depositTokens(users.verifier, users.indexer, tokensToCollect); + + // burn some tokens to prevent overflow + resetPrank(users.indexer); + token.burn(MAX_STAKING_TOKENS); + + resetPrank(users.verifier); + _collectEscrow( + IGraphPayments.PaymentTypes.QueryFee, + users.gateway, + users.indexer, + tokensToCollect, + subgraphDataServiceAddress, + dataServiceCut + ); + } + + function testCollect_Tokens_NoProvision( + uint256 tokens, + uint256 dataServiceCut + ) public useIndexer useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { + dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + resetPrank(users.gateway); _depositTokens(users.verifier, users.indexer, tokens); + // burn some tokens to prevent overflow + resetPrank(users.indexer); + token.burn(MAX_STAKING_TOKENS); + resetPrank(users.verifier); _collectEscrow( IGraphPayments.PaymentTypes.QueryFee, @@ -68,61 +100,42 @@ contract GraphEscrowCollectTest is GraphEscrowTest { vm.stopPrank(); } - function testCollect_RevertWhen_InvalidPool( - uint256 amount - ) - public - useIndexer - useProvision(amount, 0, 0) - useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) - { - vm.assume(amount > 1 ether); + function testCollect_MultipleCollections( + uint256 depositAmount, + uint256 firstCollect, + uint256 secondCollect + ) public useIndexer { + // Tests multiple collect operations from the same escrow account + vm.assume(firstCollect < MAX_STAKING_TOKENS); + vm.assume(secondCollect < MAX_STAKING_TOKENS); + vm.assume(depositAmount > 0); + vm.assume(firstCollect > 0 && firstCollect < depositAmount); + vm.assume(secondCollect > 0 && secondCollect <= depositAmount - firstCollect); resetPrank(users.gateway); - _depositTokens(users.verifier, users.indexer, amount); - - resetPrank(users.verifier); - vm.expectRevert( - abi.encodeWithSelector( - IHorizonStakingMain.HorizonStakingInvalidDelegationPool.selector, - users.indexer, - subgraphDataServiceAddress - ) - ); - escrow.collect( - IGraphPayments.PaymentTypes.QueryFee, - users.gateway, - users.indexer, - amount, - subgraphDataServiceAddress, - 1 - ); - } - - function testCollect_RevertWhen_InvalidProvision( - uint256 amount - ) public useIndexer useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { - vm.assume(amount > 1 ether); - vm.assume(amount <= MAX_STAKING_TOKENS); + _depositTokens(users.verifier, users.indexer, depositAmount); - resetPrank(users.gateway); - _depositTokens(users.verifier, users.indexer, amount); + // burn some tokens to prevent overflow + resetPrank(users.indexer); + token.burn(MAX_STAKING_TOKENS); resetPrank(users.verifier); - vm.expectRevert( - abi.encodeWithSelector( - IHorizonStakingMain.HorizonStakingInvalidProvision.selector, - users.indexer, - subgraphDataServiceAddress - ) - ); - escrow.collect( + _collectEscrow( IGraphPayments.PaymentTypes.QueryFee, users.gateway, users.indexer, - amount, + firstCollect, subgraphDataServiceAddress, - 1 + 0 ); + + // _collectEscrow( + // IGraphPayments.PaymentTypes.QueryFee, + // users.gateway, + // users.indexer, + // secondCollect, + // subgraphDataServiceAddress, + // 0 + // ); } } diff --git a/packages/horizon/test/escrow/deposit.t.sol b/packages/horizon/test/escrow/deposit.t.sol index 002d2b1a4..fba3b7c0d 100644 --- a/packages/horizon/test/escrow/deposit.t.sol +++ b/packages/horizon/test/escrow/deposit.t.sol @@ -6,13 +6,32 @@ import "forge-std/Test.sol"; import { GraphEscrowTest } from "./GraphEscrow.t.sol"; contract GraphEscrowDepositTest is GraphEscrowTest { - /* * TESTS */ function testDeposit_Tokens(uint256 amount) public useGateway useDeposit(amount) { - (uint256 indexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, users.verifier, users.indexer); + (uint256 indexerEscrowBalance, , ) = escrow.escrowAccounts(users.gateway, users.verifier, users.indexer); assertEq(indexerEscrowBalance, amount); } -} \ No newline at end of file + + function testDepositTo_Tokens(uint256 amount) public { + resetPrank(users.delegator); + token.approve(address(escrow), amount); + _depositToTokens(users.gateway, users.verifier, users.indexer, amount); + } + + // Tests multiple deposits accumulate correctly in the escrow account + function testDeposit_MultipleDeposits(uint256 amount1, uint256 amount2) public useGateway { + vm.assume(amount1 > 0); + vm.assume(amount2 > 0); + vm.assume(amount1 <= MAX_STAKING_TOKENS); + vm.assume(amount2 <= MAX_STAKING_TOKENS); + + _depositTokens(users.verifier, users.indexer, amount1); + _depositTokens(users.verifier, users.indexer, amount2); + + (uint256 balance,,) = escrow.escrowAccounts(users.gateway, users.verifier, users.indexer); + assertEq(balance, amount1 + amount2); + } +} diff --git a/packages/horizon/test/escrow/thaw.t.sol b/packages/horizon/test/escrow/thaw.t.sol index 017c3291f..f7c23371b 100644 --- a/packages/horizon/test/escrow/thaw.t.sol +++ b/packages/horizon/test/escrow/thaw.t.sol @@ -6,19 +6,48 @@ import "forge-std/Test.sol"; import { GraphEscrowTest } from "./GraphEscrow.t.sol"; contract GraphEscrowThawTest is GraphEscrowTest { - /* * TESTS */ - function testThaw_Tokens(uint256 amount) public useGateway useDeposit(amount) { + function testThaw_PartialBalanceThaw( + uint256 amountDeposited, + uint256 amountThawed + ) public useGateway useDeposit(amountDeposited) { + vm.assume(amountThawed > 0); + vm.assume(amountThawed <= amountDeposited); + _thawEscrow(users.verifier, users.indexer, amountThawed); + } + + function testThaw_FullBalanceThaw(uint256 amount) public useGateway useDeposit(amount) { + vm.assume(amount > 0); _thawEscrow(users.verifier, users.indexer, amount); + + uint256 availableBalance = escrow.getBalance(users.gateway, users.verifier, users.indexer); + assertEq(availableBalance, 0); } - function testThaw_RevertWhen_InsufficientThawAmount( - uint256 amount - ) public useGateway useDeposit(amount) { - bytes memory expectedError = abi.encodeWithSignature("PaymentsEscrowNotThawing()"); + function testThaw_Tokens_SuccesiveCalls(uint256 amount) public useGateway { + amount = bound(amount, 2, type(uint256).max - 10); + _depositTokens(users.verifier, users.indexer, amount); + + uint256 firstAmountToThaw = (amount + 2 - 1) / 2; + uint256 secondAmountToThaw = (amount + 10 - 1) / 10; + _thawEscrow(users.verifier, users.indexer, firstAmountToThaw); + _thawEscrow(users.verifier, users.indexer, secondAmountToThaw); + + (, address msgSender, ) = vm.readCallers(); + (, uint256 amountThawing, uint256 thawEndTimestamp) = escrow.escrowAccounts( + msgSender, + users.verifier, + users.indexer + ); + assertEq(amountThawing, secondAmountToThaw); + assertEq(thawEndTimestamp, block.timestamp + withdrawEscrowThawingPeriod); + } + + function testThaw_Tokens_RevertWhen_AmountIsZero() public useGateway { + bytes memory expectedError = abi.encodeWithSignature("PaymentsEscrowInvalidZeroTokens()"); vm.expectRevert(expectedError); escrow.thaw(users.verifier, users.indexer, 0); } @@ -28,17 +57,23 @@ contract GraphEscrowThawTest is GraphEscrowTest { uint256 overAmount ) public useGateway useDeposit(amount) { overAmount = bound(overAmount, amount + 1, type(uint256).max); - bytes memory expectedError = abi.encodeWithSignature("PaymentsEscrowInsufficientBalance(uint256,uint256)", amount, overAmount); + bytes memory expectedError = abi.encodeWithSignature( + "PaymentsEscrowInsufficientBalance(uint256,uint256)", + amount, + overAmount + ); vm.expectRevert(expectedError); escrow.thaw(users.verifier, users.indexer, overAmount); } function testThaw_CancelRequest(uint256 amount) public useGateway useDeposit(amount) { - escrow.thaw(users.verifier, users.indexer, amount); - escrow.thaw(users.verifier, users.indexer, 0); + _thawEscrow(users.verifier, users.indexer, amount); + _cancelThawEscrow(users.verifier, users.indexer); + } - (, uint256 amountThawing,uint256 thawEndTimestamp) = escrow.escrowAccounts(users.gateway, users.verifier, users.indexer); - assertEq(amountThawing, 0); - assertEq(thawEndTimestamp, 0); + function testThaw_CancelRequest_RevertWhen_NoThawing(uint256 amount) public useGateway useDeposit(amount) { + bytes memory expectedError = abi.encodeWithSignature("PaymentsEscrowNotThawing()"); + vm.expectRevert(expectedError); + escrow.cancelThaw(users.verifier, users.indexer); } -} \ No newline at end of file +} diff --git a/packages/horizon/test/escrow/withdraw.t.sol b/packages/horizon/test/escrow/withdraw.t.sol index a141d039b..db7001fd8 100644 --- a/packages/horizon/test/escrow/withdraw.t.sol +++ b/packages/horizon/test/escrow/withdraw.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; +import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; import { GraphEscrowTest } from "./GraphEscrow.t.sol"; contract GraphEscrowWithdrawTest is GraphEscrowTest { @@ -18,11 +19,8 @@ contract GraphEscrowWithdrawTest is GraphEscrowTest { // advance time skip(withdrawEscrowThawingPeriod + 1); - escrow.withdraw(users.verifier, users.indexer); + _withdrawEscrow(users.verifier, users.indexer); vm.stopPrank(); - - (uint256 indexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, users.verifier, users.indexer); - assertEq(indexerEscrowBalance, amount - thawAmount); } function testWithdraw_RevertWhen_NotThawing(uint256 amount) public useGateway useDeposit(amount) { @@ -39,4 +37,35 @@ contract GraphEscrowWithdrawTest is GraphEscrowTest { vm.expectRevert(expectedError); escrow.withdraw(users.verifier, users.indexer); } + + function testWithdraw_BalanceAfterCollect( + uint256 amountDeposited, + uint256 amountThawed, + uint256 amountCollected + ) public useGateway depositAndThawTokens(amountDeposited, amountThawed) { + vm.assume(amountCollected > 0); + vm.assume(amountCollected <= amountDeposited); + + // burn some tokens to prevent overflow + resetPrank(users.indexer); + token.burn(MAX_STAKING_TOKENS); + + // collect + resetPrank(users.verifier); + _collectEscrow( + IGraphPayments.PaymentTypes.QueryFee, + users.gateway, + users.indexer, + amountCollected, + subgraphDataServiceAddress, + 0 + ); + + // Advance time to simulate the thawing period + skip(withdrawEscrowThawingPeriod + 1); + + // withdraw the remaining thawed balance + resetPrank(users.gateway); + _withdrawEscrow(users.verifier, users.indexer); + } } \ No newline at end of file diff --git a/packages/horizon/test/payments/GraphPayments.t.sol b/packages/horizon/test/payments/GraphPayments.t.sol index 494a7912a..9ab5fae1d 100644 --- a/packages/horizon/test/payments/GraphPayments.t.sol +++ b/packages/horizon/test/payments/GraphPayments.t.sol @@ -4,11 +4,21 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; import { IHorizonStakingMain } from "../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { IHorizonStakingTypes } from "../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; import { GraphPayments } from "../../contracts/payments/GraphPayments.sol"; import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStakingShared.t.sol"; import { PPMMath } from "../../contracts/libraries/PPMMath.sol"; +import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; + +contract GraphPaymentsExtended is GraphPayments { + constructor(address controller, uint256 protocolPaymentCut) GraphPayments(controller, protocolPaymentCut) {} + + function readController() external view returns (address) { + return address(_graphController()); + } +} contract GraphPaymentsTest is HorizonStakingSharedTest { using PPMMath for uint256; @@ -40,15 +50,22 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { // Calculate cuts uint256 tokensProtocol = _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()); uint256 tokensDataService = (_tokens - tokensProtocol).mulPPMRoundUp(_dataServiceCut); - uint256 tokensDelegation = (_tokens - tokensProtocol - tokensDataService).mulPPMRoundUp( - staking.getDelegationFeeCut(_receiver, _dataService, _paymentType) - ); + uint256 tokensDelegation = 0; + { + IHorizonStakingTypes.DelegationPool memory pool = staking.getDelegationPool(_receiver, _dataService); + if (pool.shares > 0) { + tokensDelegation = (_tokens - tokensProtocol - tokensDataService).mulPPMRoundUp( + staking.getDelegationFeeCut(_receiver, _dataService, _paymentType) + ); + } + } uint256 receiverExpectedPayment = _tokens - tokensProtocol - tokensDataService - tokensDelegation; (, address msgSender, ) = vm.readCallers(); vm.expectEmit(address(payments)); emit IGraphPayments.GraphPaymentCollected( + _paymentType, msgSender, _receiver, _dataService, @@ -58,13 +75,7 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { tokensDelegation, receiverExpectedPayment ); - payments.collect( - _paymentType, - _receiver, - _tokens, - _dataService, - _dataServiceCut - ); + payments.collect(_paymentType, _receiver, _tokens, _dataService, _dataServiceCut); // After balances CollectPaymentData memory afterBalances = CollectPaymentData({ @@ -96,6 +107,13 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { * TESTS */ + function testConstructor() public { + uint256 protocolCut = 100_000; + GraphPaymentsExtended newPayments = new GraphPaymentsExtended(address(controller), protocolCut); + assertEq(address(newPayments.readController()), address(controller)); + assertEq(newPayments.PROTOCOL_PAYMENT_CUT(), protocolCut); + } + function testConstructor_RevertIf_InvalidProtocolPaymentCut(uint256 protocolPaymentCut) public { protocolPaymentCut = bound(protocolPaymentCut, MAX_PPM + 1, type(uint256).max); @@ -108,18 +126,34 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { new GraphPayments(address(controller), protocolPaymentCut); } + function testInitialize() public { + // Deploy new instance to test initialization + GraphPayments newPayments = new GraphPayments(address(controller), 100_000); + + // Should revert if not called by onlyInitializer + vm.expectRevert(); + newPayments.initialize(); + } + function testCollect( uint256 amount, + uint256 amountToCollect, uint256 dataServiceCut, - uint256 tokensDelegate - ) - public - useIndexer - useProvision(amount, 0, 0) - useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) - { + uint256 tokensDelegate, + uint256 delegationFeeCut + ) public useIndexer useProvision(amount, 0, 0) { + amountToCollect = bound(amountToCollect, 1, MAX_STAKING_TOKENS); dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); - address escrowAddress = address(escrow); + 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); @@ -127,6 +161,7 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { _delegate(users.indexer, subgraphDataServiceAddress, tokensDelegate, 0); // Add tokens in escrow + address escrowAddress = address(escrow); mint(escrowAddress, amount); vm.startPrank(escrowAddress); approve(address(payments), amount); @@ -142,6 +177,45 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { vm.stopPrank(); } + function testCollect_NoProvision( + uint256 amount, + uint256 dataServiceCut, + uint256 delegationFeeCut + ) public useIndexer { + amount = bound(amount, 1, MAX_STAKING_TOKENS); + dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); + 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 + ); + + // Add tokens in escrow + address escrowAddress = address(escrow); + mint(escrowAddress, amount); + vm.startPrank(escrowAddress); + approve(address(payments), amount); + + // burn some tokens to prevent overflow + resetPrank(users.indexer); + token.burn(MAX_STAKING_TOKENS); + + // Collect payments through GraphPayments + vm.startPrank(escrowAddress); + _collect( + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + dataServiceCut + ); + vm.stopPrank(); + } + function testCollect_RevertWhen_InvalidDataServiceCut( uint256 amount, uint256 dataServiceCut @@ -168,53 +242,82 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { ); } - function testCollect_RevertWhen_InvalidPool( - uint256 amount - ) - public - useIndexer - useProvision(amount, 0, 0) - useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) - { - vm.assume(amount > 1 ether); - address escrowAddress = address(escrow); + function testCollect_WithZeroAmount(uint256 amount) public useIndexer useProvision(amount, 0, 0) { + _collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, 0, subgraphDataServiceAddress, 0); + } - // Add tokens in escrow - mint(escrowAddress, amount); - vm.startPrank(escrowAddress); - approve(address(payments), amount); + function testCollect_RevertWhen_UnauthorizedCaller(uint256 amount) public useIndexer useProvision(amount, 0, 0) { + vm.assume(amount > 0 && amount <= MAX_STAKING_TOKENS); + + // Try to collect without being the escrow + resetPrank(users.indexer); - // Collect payments through GraphPayments vm.expectRevert( - abi.encodeWithSelector( - IHorizonStakingMain.HorizonStakingInvalidDelegationPool.selector, - users.indexer, - subgraphDataServiceAddress - ) + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(payments), 0, amount) ); - payments.collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, amount, subgraphDataServiceAddress, 1); + + payments.collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, amount, subgraphDataServiceAddress, 0); } - function testCollect_RevertWhen_InvalidProvision( - uint256 amount - ) public useIndexer useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { - vm.assume(amount > 1 ether); - vm.assume(amount <= MAX_STAKING_TOKENS); - address escrowAddress = address(escrow); + function testCollect_WithNoDelegation( + uint256 amount, + uint256 dataServiceCut, + uint256 delegationFeeCut + ) public useIndexer useProvision(amount, 0, 0) { + dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); + + // Set delegation fee cut + _setDelegationFeeCut( + users.indexer, + subgraphDataServiceAddress, + IGraphPayments.PaymentTypes.QueryFee, + delegationFeeCut + ); // Add tokens in escrow + address escrowAddress = address(escrow); mint(escrowAddress, amount); vm.startPrank(escrowAddress); approve(address(payments), amount); // Collect payments through GraphPayments - vm.expectRevert( - abi.encodeWithSelector( - IHorizonStakingMain.HorizonStakingInvalidProvision.selector, - users.indexer, - subgraphDataServiceAddress - ) + _collect( + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + dataServiceCut + ); + vm.stopPrank(); + } + + function testCollect_ViaMulticall(uint256 amount) public useIndexer { + amount = bound(amount, 1, MAX_STAKING_TOKENS / 2); // Divide by 2 as we'll make two calls + + address escrowAddress = address(escrow); + mint(escrowAddress, amount * 2); + vm.startPrank(escrowAddress); + approve(address(payments), amount * 2); + + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector( + payments.collect.selector, + IGraphPayments.PaymentTypes.QueryFee, + users.indexer, + amount, + subgraphDataServiceAddress, + 100_000 // 10% + ); + data[1] = abi.encodeWithSelector( + payments.collect.selector, + IGraphPayments.PaymentTypes.IndexingFee, + users.indexer, + amount, + subgraphDataServiceAddress, + 200_000 // 20% ); - payments.collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, amount, subgraphDataServiceAddress, 1); + + payments.multicall(data); } } diff --git a/packages/horizon/test/payments/tap-collector/collect/collect.t.sol b/packages/horizon/test/payments/tap-collector/collect/collect.t.sol index db1c994e4..366305b0f 100644 --- a/packages/horizon/test/payments/tap-collector/collect/collect.t.sol +++ b/packages/horizon/test/payments/tap-collector/collect/collect.t.sol @@ -43,14 +43,14 @@ contract TAPCollectorCollectTest is TAPCollectorTest { address _allocationId, address _payer, address _indexer, - address _collector, + address _dataService, uint128 _tokens ) private pure returns (ITAPCollector.ReceiptAggregateVoucher memory rav) { return ITAPCollector.ReceiptAggregateVoucher({ collectionId: bytes32(uint256(uint160(_allocationId))), payer: _payer, - dataService: _collector, + dataService: _dataService, serviceProvider: _indexer, timestampNs: 0, valueAggregate: _tokens, diff --git a/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol b/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol index c3714dfd6..28b0327ae 100644 --- a/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol +++ b/packages/horizon/test/shared/payments-escrow/PaymentsEscrowShared.t.sol @@ -34,4 +34,16 @@ abstract contract PaymentsEscrowSharedTest is GraphBaseTest { (uint256 escrowBalanceAfter,,) = escrow.escrowAccounts(msgSender, _collector, _receiver); assertEq(escrowBalanceAfter - _tokens, escrowBalanceBefore); } + + function _depositToTokens(address _payer, address _collector, address _receiver, uint256 _tokens) internal { + (uint256 escrowBalanceBefore,,) = escrow.escrowAccounts(_payer, _collector, _receiver); + token.approve(address(escrow), _tokens); + + vm.expectEmit(address(escrow)); + emit IPaymentsEscrow.Deposit(_payer, _collector, _receiver, _tokens); + escrow.depositTo(_payer, _collector, _receiver, _tokens); + + (uint256 escrowBalanceAfter,,) = escrow.escrowAccounts(_payer, _collector, _receiver); + assertEq(escrowBalanceAfter - _tokens, escrowBalanceBefore); + } } 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 d6271cd46..321f01a75 100644 --- a/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol +++ b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol @@ -49,8 +49,8 @@ contract SubgraphServiceRegisterTest is SubgraphServiceTest { ITAPCollector.ReceiptAggregateVoucher({ collectionId: collectionId, payer: users.gateway, - dataService: address(subgraphService), serviceProvider: indexer, + dataService: address(subgraphService), timestampNs: 0, valueAggregate: tokens, metadata: ""