diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol index ed7e90804..eafce457a 100644 --- a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol +++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol @@ -402,6 +402,13 @@ interface IHorizonStakingMain { */ error HorizonStakingInvalidDelegationPoolState(address serviceProvider, address verifier); + /** + * @notice Thrown when attempting to operate with a delegation pool that does not exist. + * @param serviceProvider The service provider address + * @param verifier The verifier address + */ + error HorizonStakingInvalidDelegationPool(address serviceProvider, address verifier); + // -- Errors: thaw requests -- error HorizonStakingNothingThawing(); diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol index e36d33a73..c8709e639 100644 --- a/packages/horizon/contracts/staking/HorizonStaking.sol +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -279,7 +279,15 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { uint256 tokens ) external override notPaused { require(tokens != 0, HorizonStakingInvalidZeroTokens()); + + // Provision must exist before adding to delegation pool + Provision memory prov = _provisions[serviceProvider][verifier]; + require(prov.createdAt != 0, HorizonStakingInvalidProvision(serviceProvider, verifier)); + + // Delegation pool must exist before adding tokens DelegationPoolInternal storage pool = _getDelegationPool(serviceProvider, verifier); + require(pool.shares > 0, HorizonStakingInvalidDelegationPool(serviceProvider, verifier)); + pool.tokens = pool.tokens + tokens; _graphToken().pullTokens(msg.sender, tokens); emit TokensToDelegationPoolAdded(serviceProvider, verifier, tokens); diff --git a/packages/horizon/test/escrow/collect.t.sol b/packages/horizon/test/escrow/collect.t.sol index 67efcc6e7..cbf945515 100644 --- a/packages/horizon/test/escrow/collect.t.sol +++ b/packages/horizon/test/escrow/collect.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; +import { IHorizonStakingMain } from "../../contracts/interfaces/internal/IHorizonStakingMain.sol"; import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; import { IPaymentsEscrow } from "../../contracts/interfaces/IPaymentsEscrow.sol"; @@ -10,30 +11,107 @@ import { GraphEscrowTest } from "./GraphEscrow.t.sol"; contract GraphEscrowCollectTest is GraphEscrowTest { + struct CollectPaymentData { + uint256 escrowBalance; + uint256 paymentsBalance; + uint256 receiverBalance; + uint256 delegationPoolBalance; + uint256 dataServiceBalance; + } + + function _collect( + IGraphPayments.PaymentTypes _paymentType, + address _payer, + address _receiver, + uint256 _tokens, + address _dataService, + uint256 _tokensDataService + ) private { + // Previous balances + (uint256 previousPayerEscrowBalance,,) = escrow.escrowAccounts(_payer, _receiver); + CollectPaymentData memory previousBalances = CollectPaymentData({ + escrowBalance: token.balanceOf(address(escrow)), + paymentsBalance: token.balanceOf(address(payments)), + receiverBalance: token.balanceOf(_receiver), + delegationPoolBalance: staking.getDelegatedTokensAvailable( + _receiver, + _dataService + ), + dataServiceBalance: token.balanceOf(_dataService) + }); + + vm.expectEmit(address(escrow)); + emit IPaymentsEscrow.EscrowCollected(_payer, _receiver, _tokens); + escrow.collect(_paymentType, _payer, _receiver, _tokens, _dataService, _tokensDataService); + + // Calculate cuts + uint256 protocolPaymentCut = payments.PROTOCOL_PAYMENT_CUT(); + uint256 delegatorCut = staking.getDelegationFeeCut( + _receiver, + _dataService, + _paymentType + ); + uint256 tokensProtocol = _tokens * protocolPaymentCut / MAX_PPM; + uint256 tokensDelegation = _tokens * delegatorCut / MAX_PPM; + + // After balances + (uint256 afterPayerEscrowBalance,,) = escrow.escrowAccounts(_payer, _receiver); + CollectPaymentData memory afterBalances = CollectPaymentData({ + escrowBalance: token.balanceOf(address(escrow)), + paymentsBalance: token.balanceOf(address(payments)), + receiverBalance: token.balanceOf(_receiver), + delegationPoolBalance: staking.getDelegatedTokensAvailable( + _receiver, + _dataService + ), + dataServiceBalance: token.balanceOf(_dataService) + }); + + // Check receiver balance after payment + uint256 receiverExpectedPayment = _tokens - _tokensDataService - tokensProtocol - tokensDelegation; + assertEq(afterBalances.receiverBalance - previousBalances.receiverBalance, receiverExpectedPayment); + assertEq(token.balanceOf(address(payments)), 0); + + // Check delegation pool balance after payment + assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, tokensDelegation); + + // Check that the escrow account has been updated + assertEq(previousBalances.escrowBalance, afterBalances.escrowBalance + _tokens); + + // Check that payments balance didn't change + assertEq(previousBalances.paymentsBalance, afterBalances.paymentsBalance); + + // Check data service balance after payment + assertEq(afterBalances.dataServiceBalance - previousBalances.dataServiceBalance, _tokensDataService); + + // Check payers escrow balance after payment + assertEq(previousPayerEscrowBalance - _tokens, afterPayerEscrowBalance); + } + /* * TESTS */ function testCollect_Tokens( - uint256 amount, + uint256 tokens, + uint256 delegationTokens, uint256 tokensDataService - ) public useIndexer useProvision(amount, 0, 0) useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { - uint256 tokensProtocol = amount * protocolPaymentCut / MAX_PPM; - uint256 tokensDelegatoion = amount * delegationFeeCut / MAX_PPM; - vm.assume(tokensDataService < amount - tokensProtocol - tokensDelegatoion); - - vm.startPrank(users.gateway); - escrow.approveCollector(users.verifier, amount); - _depositTokens(amount); + ) public useIndexer useProvision(tokens, 0, 0) useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { + uint256 tokensProtocol = tokens * protocolPaymentCut / MAX_PPM; + uint256 tokensDelegatoion = tokens * delegationFeeCut / MAX_PPM; + vm.assume(tokensDataService < tokens - tokensProtocol - tokensDelegatoion); - uint256 indexerPreviousBalance = token.balanceOf(users.indexer); - vm.startPrank(users.verifier); - escrow.collect(IGraphPayments.PaymentTypes.QueryFee, users.gateway, users.indexer, amount, subgraphDataServiceAddress, tokensDataService); + vm.assume(delegationTokens > MIN_DELEGATION); + vm.assume(delegationTokens <= MAX_STAKING_TOKENS); + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); - uint256 indexerBalance = token.balanceOf(users.indexer); - uint256 indexerExpectedPayment = amount - tokensDataService - tokensProtocol - tokensDelegatoion; - assertEq(indexerBalance - indexerPreviousBalance, indexerExpectedPayment); - assertEq(token.balanceOf(address(payments)), 0); + resetPrank(users.gateway); + escrow.approveCollector(users.verifier, tokens); + _depositTokens(tokens); + + resetPrank(users.verifier); + _collect(IGraphPayments.PaymentTypes.QueryFee, users.gateway, users.indexer, tokens, subgraphDataServiceAddress, tokensDataService); } function testCollect_RevertWhen_CollectorNotAuthorized(uint256 amount) public { @@ -78,4 +156,41 @@ contract GraphEscrowCollectTest is GraphEscrowTest { escrow.collect(IGraphPayments.PaymentTypes.QueryFee, users.gateway, users.indexer, amount, subgraphDataServiceAddress, 0); vm.stopPrank(); } + + function testCollect_RevertWhen_InvalidPool( + uint256 amount + ) public useIndexer useProvision(amount, 0, 0) useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { + vm.assume(amount > 1 ether); + + resetPrank(users.gateway); + escrow.approveCollector(users.verifier, amount); + _depositTokens(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); + + resetPrank(users.gateway); + escrow.approveCollector(users.verifier, amount); + _depositTokens(amount); + + resetPrank(users.verifier); + vm.expectRevert(abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidProvision.selector, + users.indexer, + subgraphDataServiceAddress + )); + escrow.collect(IGraphPayments.PaymentTypes.QueryFee, users.gateway, users.indexer, amount, subgraphDataServiceAddress, 1); + } } \ 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 62e582c26..8b76678b4 100644 --- a/packages/horizon/test/payments/GraphPayments.t.sol +++ b/packages/horizon/test/payments/GraphPayments.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; +import { IHorizonStakingMain } from "../../contracts/interfaces/internal/IHorizonStakingMain.sol"; import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; import { GraphPayments } from "../../contracts/payments/GraphPayments.sol"; @@ -10,6 +11,87 @@ import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStaki contract GraphPaymentsTest is HorizonStakingSharedTest { + struct CollectPaymentData { + uint256 escrowBalance; + uint256 paymentsBalance; + uint256 receiverBalance; + uint256 delegationPoolBalance; + uint256 dataServiceBalance; + } + + function _collect( + IGraphPayments.PaymentTypes _paymentType, + address _receiver, + uint256 _tokens, + address _dataService, + uint256 _tokensDataService + ) private { + // Previous balances + CollectPaymentData memory previousBalances = CollectPaymentData({ + escrowBalance: token.balanceOf(address(escrow)), + paymentsBalance: token.balanceOf(address(payments)), + receiverBalance: token.balanceOf(_receiver), + delegationPoolBalance: staking.getDelegatedTokensAvailable( + _receiver, + _dataService + ), + dataServiceBalance: token.balanceOf(_dataService) + }); + + // Calculate cuts + uint256 protocolPaymentCut = payments.PROTOCOL_PAYMENT_CUT(); + uint256 delegatorCut = staking.getDelegationFeeCut( + _receiver, + _dataService, + _paymentType + ); + uint256 tokensProtocol = _tokens * protocolPaymentCut / MAX_PPM; + uint256 tokensDelegation = _tokens * delegatorCut / MAX_PPM; + + uint256 receiverExpectedPayment = _tokens - _tokensDataService - tokensProtocol - tokensDelegation; + + (,address msgSender, ) = vm.readCallers(); + vm.expectEmit(address(payments)); + emit IGraphPayments.PaymentCollected( + msgSender, + _receiver, + _dataService, + receiverExpectedPayment, + tokensDelegation, + _tokensDataService, + tokensProtocol + ); + payments.collect(_paymentType, _receiver, _tokens, _dataService, _tokensDataService); + + // After balances + CollectPaymentData memory afterBalances = CollectPaymentData({ + escrowBalance: token.balanceOf(address(escrow)), + paymentsBalance: token.balanceOf(address(payments)), + receiverBalance: token.balanceOf(_receiver), + delegationPoolBalance: staking.getDelegatedTokensAvailable( + _receiver, + _dataService + ), + dataServiceBalance: token.balanceOf(_dataService) + }); + + // Check receiver balance after payment + assertEq(afterBalances.receiverBalance - previousBalances.receiverBalance, receiverExpectedPayment); + assertEq(token.balanceOf(address(payments)), 0); + + // Check delegation pool balance after payment + assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, tokensDelegation); + + // Check that the escrow account has been updated + assertEq(previousBalances.escrowBalance, afterBalances.escrowBalance + _tokens); + + // Check that payments balance didn't change + assertEq(previousBalances.paymentsBalance, afterBalances.paymentsBalance); + + // Check data service balance after payment + assertEq(afterBalances.dataServiceBalance - previousBalances.dataServiceBalance, _tokensDataService); + } + /* * TESTS */ @@ -28,32 +110,28 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { function testCollect( uint256 amount, - uint256 tokensDataService + uint256 tokensDataService, + uint256 tokensDelegate ) public useIndexer useProvision(amount, 0, 0) useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { uint256 tokensProtocol = amount * protocolPaymentCut / MAX_PPM; - uint256 tokensDelegatoion = amount * delegationFeeCut / MAX_PPM; - vm.assume(tokensDataService < amount - tokensProtocol - tokensDelegatoion); + uint256 tokensDelegation = amount * delegationFeeCut / MAX_PPM; + vm.assume(tokensDataService < amount - tokensProtocol - tokensDelegation); address escrowAddress = address(escrow); + // Delegate tokens + vm.assume(tokensDelegate > MIN_DELEGATION); + vm.assume(tokensDelegate <= MAX_STAKING_TOKENS); + vm.startPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, tokensDelegate, 0); + // Add tokens in escrow mint(escrowAddress, amount); vm.startPrank(escrowAddress); approve(address(payments), amount); // Collect payments through GraphPayments - uint256 indexerPreviousBalance = token.balanceOf(users.indexer); - payments.collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, amount, subgraphDataServiceAddress, tokensDataService); + _collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, amount, subgraphDataServiceAddress, tokensDataService); vm.stopPrank(); - - uint256 indexerBalance = token.balanceOf(users.indexer); - uint256 expectedPayment = amount - tokensDataService - tokensProtocol - tokensDelegatoion; - assertEq(indexerBalance - indexerPreviousBalance, expectedPayment); - - uint256 dataServiceBalance = token.balanceOf(subgraphDataServiceAddress); - assertEq(dataServiceBalance, tokensDataService); - - uint256 delegatorBalance = staking.getDelegatedTokensAvailable(users.indexer, subgraphDataServiceAddress); - assertEq(delegatorBalance, tokensDelegatoion); } function testCollect_RevertWhen_InsufficientAmount( @@ -77,4 +155,45 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { vm.expectRevert(expectedError); payments.collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, amount, subgraphDataServiceAddress, tokensDataService); } + + 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); + + // Add tokens in escrow + mint(escrowAddress, amount); + vm.startPrank(escrowAddress); + approve(address(payments), amount); + + // Collect payments through GraphPayments + vm.expectRevert(abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidDelegationPool.selector, + users.indexer, + subgraphDataServiceAddress + )); + payments.collect(IGraphPayments.PaymentTypes.QueryFee, 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); + address escrowAddress = address(escrow); + + // Add tokens in 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 + )); + payments.collect(IGraphPayments.PaymentTypes.QueryFee, users.indexer, amount, subgraphDataServiceAddress, 1); + } } diff --git a/packages/horizon/test/staking/delegation/addToPool.t.sol b/packages/horizon/test/staking/delegation/addToPool.t.sol index e88becbbf..b9a583ee8 100644 --- a/packages/horizon/test/staking/delegation/addToPool.t.sol +++ b/packages/horizon/test/staking/delegation/addToPool.t.sol @@ -9,6 +9,12 @@ import { HorizonStakingTest } from "../HorizonStaking.t.sol"; contract HorizonStakingDelegationAddToPoolTest is HorizonStakingTest { modifier useValidDelegationAmount(uint256 tokens) { + vm.assume(tokens > MIN_DELEGATION); + vm.assume(tokens <= MAX_STAKING_TOKENS); + _; + } + + modifier useValidAddToPoolAmount(uint256 tokens) { vm.assume(tokens > 0); vm.assume(tokens <= MAX_STAKING_TOKENS); _; @@ -20,36 +26,31 @@ contract HorizonStakingDelegationAddToPoolTest is HorizonStakingTest { function test_Delegation_AddToPool_Verifier( uint256 amount, - uint256 delegationAmount - ) public useIndexer useProvision(amount, 0, 0) useValidDelegationAmount(delegationAmount) { - uint256 stakingPreviousBalance = token.balanceOf(address(staking)); - + uint256 delegationAmount, + uint256 addToPoolAmount + ) public useIndexer useProvision(amount, 0, 0) useValidDelegationAmount(delegationAmount) useValidAddToPoolAmount(addToPoolAmount) { + // Initialize delegation pool + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + resetPrank(subgraphDataServiceAddress); - mint(subgraphDataServiceAddress, delegationAmount); - token.approve(address(staking), delegationAmount); - _addToDelegationPool(users.indexer, subgraphDataServiceAddress, delegationAmount); - - uint256 delegatedTokens = staking.getDelegatedTokensAvailable(users.indexer, subgraphDataServiceAddress); - assertEq(delegatedTokens, delegationAmount); - assertEq(token.balanceOf(subgraphDataServiceAddress), 0); - assertEq(token.balanceOf(address(staking)), stakingPreviousBalance + delegationAmount); + mint(subgraphDataServiceAddress, addToPoolAmount); + token.approve(address(staking), addToPoolAmount); + _addToDelegationPool(users.indexer, subgraphDataServiceAddress, addToPoolAmount); } function test_Delegation_AddToPool_Payments( uint256 amount, uint256 delegationAmount - ) public useIndexer useProvision(amount, 0, 0) useValidDelegationAmount(delegationAmount) { - uint256 stakingPreviousBalance = token.balanceOf(address(staking)); + ) public useIndexer useProvision(amount, 0, 0) useValidDelegationAmount(delegationAmount) useValidAddToPoolAmount(delegationAmount) { + // Initialize delegation pool + resetPrank(users.delegator); + _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); resetPrank(address(payments)); mint(address(payments), delegationAmount); token.approve(address(staking), delegationAmount); _addToDelegationPool(users.indexer, subgraphDataServiceAddress, delegationAmount); - - uint256 delegatedTokens = staking.getDelegatedTokensAvailable(users.indexer, subgraphDataServiceAddress); - assertEq(delegatedTokens, delegationAmount); - assertEq(token.balanceOf(subgraphDataServiceAddress), 0); - assertEq(token.balanceOf(address(staking)), stakingPreviousBalance + delegationAmount); } function test_Delegation_AddToPool_RevertWhen_ZeroTokens( @@ -61,5 +62,29 @@ contract HorizonStakingDelegationAddToPoolTest is HorizonStakingTest { staking.addToDelegationPool(users.indexer, subgraphDataServiceAddress, 0); } + function test_Delegation_AddToPool_RevertWhen_PoolHasNoShares( + uint256 amount + ) public useIndexer useProvision(amount, 0, 0) { + vm.startPrank(subgraphDataServiceAddress); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidDelegationPool.selector, + users.indexer, + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + staking.addToDelegationPool(users.indexer, subgraphDataServiceAddress, 1); + } + + function test_Deletaion_AddToPool_RevertWhen_NoProvision() public { + vm.startPrank(subgraphDataServiceAddress); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidProvision.selector, + users.indexer, + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + staking.addToDelegationPool(users.indexer, subgraphDataServiceAddress, 1); + } + // TODO: test recovering an invalid delegation pool } \ No newline at end of file