diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index bd36002a0..0045b970d 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -340,6 +340,29 @@ contract SubgraphService is _setDelegationRatio(delegationRatio); } + /** + * @notice See {ISubgraphService.setStakeToFeesRatio} + */ + function setStakeToFeesRatio(uint256 _stakeToFeesRatio) external override onlyOwner { + stakeToFeesRatio = _stakeToFeesRatio; + emit StakeToFeesRatioSet(_stakeToFeesRatio); + } + + /** + * @notice See {ISubgraphService.setMaxPOIStaleness} + */ + function setMaxPOIStaleness(uint256 maxPOIStaleness) external override onlyOwner { + _setMaxPOIStaleness(maxPOIStaleness); + } + + /** + * @notice See {ISubgraphService.setPaymentCuts} + */ + function setPaymentCuts(IGraphPayments.PaymentTypes paymentType, uint128 serviceCut, uint128 curationCut) external override onlyOwner { + paymentCuts[paymentType] = PaymentCuts(serviceCut, curationCut); + emit PaymentCutsSet(paymentType, serviceCut, curationCut); + } + /** * @notice See {ISubgraphService.getAllocation} */ diff --git a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol index 39fc7430d..2c378809e 100644 --- a/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol +++ b/packages/subgraph-service/contracts/interfaces/ISubgraphService.sol @@ -48,6 +48,20 @@ interface ISubgraphService is IDataServiceFees { uint256 tokensSubgraphService ); + /** + * @notice Emitted when the stake to fees ratio is set. + * @param ratio The stake to fees ratio + */ + event StakeToFeesRatioSet(uint256 ratio); + + /** + * @notice Emmited when payment cuts are set for a payment type + * @param paymentType The payment type + * @param serviceCut The service cut for the payment type + * @param curationCut The curation cut for the payment type + */ + event PaymentCutsSet(IGraphPayments.PaymentTypes paymentType, uint128 serviceCut, uint128 curationCut); + /** * @notice Thrown when an indexer tries to register with an empty URL */ @@ -133,6 +147,28 @@ interface ISubgraphService is IDataServiceFees { */ function setDelegationRatio(uint32 delegationRatio) external; + /** + * @notice Sets the stake to fees ratio + * @param stakeToFeesRatio The stake to fees ratio + */ + function setStakeToFeesRatio(uint256 stakeToFeesRatio) external; + + /** + * @notice Sets the max POI staleness + * See {AllocationManagerV1Storage-maxPOIStaleness} for more details. + * @param maxPOIStaleness The max POI staleness in seconds + */ + function setMaxPOIStaleness(uint256 maxPOIStaleness) external; + + /** + * @notice Sets the payment cuts for a payment type + * @dev Emits a {PaymentCutsSet} event + * @param paymentType The payment type + * @param serviceCut The service cut for the payment type + * @param curationCut The curation cut for the payment type + */ + function setPaymentCuts(IGraphPayments.PaymentTypes paymentType, uint128 serviceCut, uint128 curationCut) external; + /** * @notice Sets the rewards destination for an indexer to receive indexing rewards * @dev Emits a {RewardsDestinationSet} event diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 8d14da240..850b3fd13 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -114,6 +114,12 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca */ 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 + */ + event MaxPOIStalenessSet(uint256 maxPOIStaleness); + /** * @notice Thrown when an allocation proof is invalid * Both `signer` and `allocationId` should match for a valid proof. @@ -384,15 +390,15 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca function _closeAllocation(address _allocationId) internal returns (Allocation.State memory) { Allocation.State memory allocation = allocations.get(_allocationId); - allocations.close(_allocationId); - allocationProvisionTracker.release(allocation.indexer, allocation.tokens); - // Take rewards snapshot to prevent other allos from counting tokens from this allo allocations.snapshotRewards( _allocationId, _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) ); + allocations.close(_allocationId); + allocationProvisionTracker.release(allocation.indexer, allocation.tokens); + // Update total allocated tokens for the subgraph deployment subgraphAllocatedTokens[allocation.subgraphDeploymentId] = subgraphAllocatedTokens[allocation.subgraphDeploymentId] - @@ -412,6 +418,16 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca 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 + * @param _maxPOIStaleness The max POI staleness in seconds + */ + function _setMaxPOIStaleness(uint256 _maxPOIStaleness) internal { + maxPOIStaleness = _maxPOIStaleness; + emit MaxPOIStalenessSet(_maxPOIStaleness); + } + /** * @notice Gets the details of an allocation * @param _allocationId The id of the allocation diff --git a/packages/subgraph-service/foundry.toml b/packages/subgraph-service/foundry.toml index d4c30311f..235a58eb9 100644 --- a/packages/subgraph-service/foundry.toml +++ b/packages/subgraph-service/foundry.toml @@ -7,6 +7,7 @@ cache_path = 'cache_forge' optimizer = true optimizer-runs = 200 via_ir = true +fs_permissions = [{ access = "read", path = "./"}] [profile.lite] optimizer = false diff --git a/packages/subgraph-service/test/DisputeManager.t.sol b/packages/subgraph-service/test/DisputeManager.t.sol deleted file mode 100644 index a5375dc0d..000000000 --- a/packages/subgraph-service/test/DisputeManager.t.sol +++ /dev/null @@ -1,512 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import "forge-std/Test.sol"; - -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; -import { Controller } from "@graphprotocol/contracts/contracts/governance/Controller.sol"; -import { UnsafeUpgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; - -import { DisputeManager } from "../contracts/DisputeManager.sol"; -import { IDisputeManager } from "../contracts/interfaces/IDisputeManager.sol"; -import { Attestation } from "../contracts/libraries/Attestation.sol"; - -import { SubgraphService } from "../contracts/SubgraphService.sol"; - -// Mocks - -import "./mocks/MockGRTToken.sol"; -import "./mocks/MockHorizonStaking.sol"; -import "./mocks/MockRewardsManager.sol"; - -contract DisputeManagerTest is Test { - DisputeManager disputeManager; - - address governor; - address arbitrator; - - uint256 indexerPrivateKey; - address indexer; - - uint256 fishermanPrivateKey; - address fisherman; - - uint256 allocationIDPrivateKey; - address allocationID; - - uint64 disputePeriod = 300; // 5 minutes - uint256 minimumDeposit = 100 ether; // 100 GRT - uint32 fishermanRewardPercentage = 100000; // 10% - uint32 maxSlashingPercentage = 500000; // 50% - - Controller controller; - MockGRTToken graphToken; - SubgraphService subgraphService; - MockHorizonStaking staking; - MockRewardsManager rewardsManager; - - // Setup - - function setUp() public { - governor = address(0xA1); - arbitrator = address(0xA2); - - indexerPrivateKey = 0xB1; - indexer = vm.addr(indexerPrivateKey); - - fishermanPrivateKey = 0xC1; - fisherman = vm.addr(fishermanPrivateKey); - - allocationIDPrivateKey = 0xD1; - allocationID = vm.addr(allocationIDPrivateKey); - - graphToken = new MockGRTToken(); - staking = new MockHorizonStaking(address(graphToken)); - rewardsManager = new MockRewardsManager(); - - address tapVerifier = address(0xE3); - address curation = address(0xE4); - - vm.startPrank(governor); - controller = new Controller(); - controller.setContractProxy(keccak256("GraphToken"), address(graphToken)); - controller.setContractProxy(keccak256("Staking"), address(staking)); - controller.setContractProxy(keccak256("RewardsManager"), address(rewardsManager)); - controller.setContractProxy(keccak256("GraphPayments"), address(0x100)); - controller.setContractProxy(keccak256("PaymentsEscrow"), address(0x101)); - controller.setContractProxy(keccak256("EpochManager"), address(0x102)); - controller.setContractProxy(keccak256("GraphTokenGateway"), address(0x103)); - controller.setContractProxy(keccak256("GraphProxyAdmin"), address(0x104)); - controller.setContractProxy(keccak256("Curation"), address(0x105)); - vm.stopPrank(); - - address disputeManagerImplementation = address(new DisputeManager(address(controller))); - address disputeManagerProxy = UnsafeUpgrades.deployTransparentProxy( - disputeManagerImplementation, - governor, - abi.encodeCall( - DisputeManager.initialize, - (arbitrator, disputePeriod, minimumDeposit, fishermanRewardPercentage, maxSlashingPercentage) - ) - ); - disputeManager = DisputeManager(disputeManagerProxy); - - address subgraphServiceImplementation = address( - new SubgraphService(address(controller), address(disputeManager), tapVerifier, curation) - ); - address subgraphServiceProxy = UnsafeUpgrades.deployTransparentProxy( - subgraphServiceImplementation, - governor, - abi.encodeCall(SubgraphService.initialize, (1000 ether, 16)) - ); - subgraphService = SubgraphService(subgraphServiceProxy); - - disputeManager.setSubgraphService(address(subgraphService)); - } - - // Helper functions - - function createProvisionAndAllocate(address _allocationID, uint256 tokens) private { - vm.startPrank(indexer); - graphToken.mint(indexer, tokens); - staking.provision(tokens, address(subgraphService), 500000, 300); - bytes32 subgraphDeployment = keccak256(abi.encodePacked("Subgraph Deployment ID")); - bytes32 digest = subgraphService.encodeAllocationProof(indexer, _allocationID); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationIDPrivateKey, digest); - - subgraphService.register(indexer, abi.encode("url", "geoHash", address(0))); - - bytes memory data = abi.encode(subgraphDeployment, tokens, _allocationID, abi.encodePacked(r, s, v)); - subgraphService.startService(indexer, data); - vm.stopPrank(); - } - - function createIndexingDispute(address _allocationID, bytes32 _poi, uint256 tokens) private returns (bytes32 disputeID) { - vm.startPrank(fisherman); - graphToken.mint(fisherman, tokens); - graphToken.approve(address(disputeManager), tokens); - bytes32 _disputeID = disputeManager.createIndexingDispute(_allocationID, _poi, tokens); - vm.stopPrank(); - return _disputeID; - } - - function createQueryDispute(uint256 tokens) private returns (bytes32 disputeID) { - Attestation.Receipt memory receipt = Attestation.Receipt({ - requestCID: keccak256(abi.encodePacked("Request CID")), - responseCID: keccak256(abi.encodePacked("Response CID")), - subgraphDeploymentId: keccak256(abi.encodePacked("Subgraph Deployment ID")) - }); - bytes memory attestationData = createAtestationData(receipt, allocationIDPrivateKey); - - vm.startPrank(fisherman); - graphToken.mint(fisherman, tokens); - graphToken.approve(address(disputeManager), tokens); - bytes32 _disputeID = disputeManager.createQueryDispute(attestationData, tokens); - vm.stopPrank(); - return _disputeID; - } - - function createConflictingAttestations( - bytes32 responseCID1, - bytes32 subgraphDeploymentId1, - bytes32 responseCID2, - bytes32 subgraphDeploymentId2 - ) private view returns (bytes memory attestationData1, bytes memory attestationData2) { - bytes32 requestCID = keccak256(abi.encodePacked("Request CID")); - Attestation.Receipt memory receipt1 = Attestation.Receipt({ - requestCID: requestCID, - responseCID: responseCID1, - subgraphDeploymentId: subgraphDeploymentId1 - }); - - Attestation.Receipt memory receipt2 = Attestation.Receipt({ - requestCID: requestCID, - responseCID: responseCID2, - subgraphDeploymentId: subgraphDeploymentId2 - }); - - bytes memory _attestationData1 = createAtestationData(receipt1, allocationIDPrivateKey); - bytes memory _attestationData2 = createAtestationData(receipt2, allocationIDPrivateKey); - return (_attestationData1, _attestationData2); - } - - function createAtestationData( - Attestation.Receipt memory receipt, - uint256 signer - ) private view returns (bytes memory attestationData) { - bytes32 digest = disputeManager.encodeReceipt(receipt); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signer, digest); - - return abi.encodePacked(receipt.requestCID, receipt.responseCID, receipt.subgraphDeploymentId, r, s, v); - } - - // Tests - - // Create dispute - - function testCreateIndexingDispute() public { - createProvisionAndAllocate(allocationID, 10000 ether); - - bytes32 disputeID = createIndexingDispute(allocationID, bytes32("POI1"), 200 ether); - assertTrue(disputeManager.isDisputeCreated(disputeID), "Dispute should be created."); - } - - function testCreateQueryDispute() public { - createProvisionAndAllocate(allocationID, 10000 ether); - - bytes32 disputeID = createQueryDispute(200 ether); - assertTrue(disputeManager.isDisputeCreated(disputeID), "Dispute should be created."); - } - - function testCreateQueryDisputeConflict() public { - createProvisionAndAllocate(allocationID, 10000 ether); - - bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); - bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); - bytes32 subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); - - (bytes memory attestationData1, bytes memory attestationData2) = createConflictingAttestations( - responseCID1, - subgraphDeploymentId, - responseCID2, - subgraphDeploymentId - ); - - vm.prank(fisherman); - (bytes32 disputeID1, bytes32 disputeID2) = disputeManager.createQueryDisputeConflict( - attestationData1, - attestationData2 - ); - assertTrue(disputeManager.isDisputeCreated(disputeID1), "Dispute 1 should be created."); - assertTrue(disputeManager.isDisputeCreated(disputeID2), "Dispute 2 should be created."); - } - - function test_RevertWhen_DisputeAlreadyCreated() public { - createProvisionAndAllocate(allocationID, 10000 ether); - bytes32 disputeID = createIndexingDispute(allocationID, bytes32("POI1"), 200 ether); - - // Create another dispute with different fisherman - address otherFisherman = address(0x5); - uint256 tokens = 200 ether; - vm.startPrank(otherFisherman); - graphToken.mint(otherFisherman, tokens); - graphToken.approve(address(disputeManager), tokens); - bytes memory expectedError = abi.encodeWithSignature("DisputeManagerDisputeAlreadyCreated(bytes32)", disputeID); - vm.expectRevert(expectedError); - disputeManager.createIndexingDispute(allocationID, bytes32("POI1"), tokens); - vm.stopPrank(); - } - - function test_RevertIf_DepositUnderMinimum() public { - // minimum deposit is 100 ether - vm.startPrank(fisherman); - graphToken.mint(fisherman, 50 ether); - bytes memory expectedError = abi.encodeWithSignature( - "DisputeManagerInsufficientDeposit(uint256,uint256)", - 50 ether, - 100 ether - ); - vm.expectRevert(expectedError); - disputeManager.createIndexingDispute(allocationID, bytes32("POI3"), 50 ether); - vm.stopPrank(); - } - - function test_RevertIf_AllocationDoesNotExist() public { - // create dispute without an existing allocation - uint256 tokens = 200 ether; - vm.startPrank(fisherman); - graphToken.mint(fisherman, tokens); - graphToken.approve(address(disputeManager), tokens); - bytes memory expectedError = abi.encodeWithSignature("DisputeManagerIndexerNotFound(address)", allocationID); - vm.expectRevert(expectedError); - disputeManager.createIndexingDispute(allocationID, bytes32("POI4"), tokens); - vm.stopPrank(); - } - - function test_RevertIf_ConflictingAttestationsResponsesAreTheSame() public { - bytes32 requestCID = keccak256(abi.encodePacked("Request CID")); - bytes32 responseCID = keccak256(abi.encodePacked("Response CID")); - bytes32 subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); - - (bytes memory attestationData1, bytes memory attestationData2) = createConflictingAttestations( - responseCID, - subgraphDeploymentId, - responseCID, - subgraphDeploymentId - ); - - vm.prank(fisherman); - - bytes memory expectedError = abi.encodeWithSignature( - "DisputeManagerNonConflictingAttestations(bytes32,bytes32,bytes32,bytes32,bytes32,bytes32)", - requestCID, - responseCID, - subgraphDeploymentId, - requestCID, - responseCID, - subgraphDeploymentId - ); - vm.expectRevert(expectedError); - disputeManager.createQueryDisputeConflict(attestationData1, attestationData2); - } - - function test_RevertIf_ConflictingAttestationsHaveDifferentSubgraph() public { - bytes32 requestCID = keccak256(abi.encodePacked("Request CID")); - bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); - bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); - bytes32 subgraphDeploymentId1 = keccak256(abi.encodePacked("Subgraph Deployment ID 1")); - bytes32 subgraphDeploymentId2 = keccak256(abi.encodePacked("Subgraph Deployment ID 2")); - - (bytes memory attestationData1, bytes memory attestationData2) = createConflictingAttestations( - responseCID1, - subgraphDeploymentId1, - responseCID2, - subgraphDeploymentId2 - ); - - vm.prank(fisherman); - bytes memory expectedError = abi.encodeWithSignature( - "DisputeManagerNonConflictingAttestations(bytes32,bytes32,bytes32,bytes32,bytes32,bytes32)", - requestCID, - responseCID1, - subgraphDeploymentId1, - requestCID, - responseCID2, - subgraphDeploymentId2 - ); - vm.expectRevert(expectedError); - disputeManager.createQueryDisputeConflict(attestationData1, attestationData2); - } - - // Accept dispute - - function testAcceptIndexingDispute() public { - createProvisionAndAllocate(allocationID, 10000 ether); - bytes32 disputeID = createIndexingDispute(allocationID, bytes32("POI1"), 200 ether); - - vm.prank(arbitrator); - disputeManager.acceptDispute(disputeID, 5000 ether); - - assertEq(graphToken.balanceOf(fisherman), 700 ether, "Fisherman should receive 50% of slashed tokens."); - assertEq(graphToken.balanceOf(indexer), 5000 ether, "Service provider should have 5000 GRT slashed."); - } - - function testAcceptQueryDispute() public { - createProvisionAndAllocate(allocationID, 10000 ether); - bytes32 disputeID = createQueryDispute(200 ether); - - vm.prank(arbitrator); - disputeManager.acceptDispute(disputeID, 5000 ether); - - assertEq(graphToken.balanceOf(fisherman), 700 ether, "Fisherman should receive 50% of slashed tokens."); - assertEq(graphToken.balanceOf(indexer), 5000 ether, "Service provider should have 5000 GRT slashed."); - } - - function testAcceptQueryDisputeConflicting() public { - createProvisionAndAllocate(allocationID, 10000 ether); - - bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); - bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); - bytes32 subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); - - (bytes memory attestationData1, bytes memory attestationData2) = createConflictingAttestations( - responseCID1, - subgraphDeploymentId, - responseCID2, - subgraphDeploymentId - ); - - vm.prank(fisherman); - (bytes32 disputeID1, bytes32 disputeID2) = disputeManager.createQueryDisputeConflict( - attestationData1, - attestationData2 - ); - - vm.prank(arbitrator); - disputeManager.acceptDispute(disputeID1, 5000 ether); - - assertEq(graphToken.balanceOf(fisherman), 500 ether, "Fisherman should receive 50% of slashed tokens."); - assertEq(graphToken.balanceOf(indexer), 5000 ether, "Service provider should have 5000 GRT slashed."); - - (, , , , , IDisputeManager.DisputeStatus status1, ) = disputeManager.disputes(disputeID1); - (, , , , , IDisputeManager.DisputeStatus status2, ) = disputeManager.disputes(disputeID2); - assertTrue(status1 == IDisputeManager.DisputeStatus.Accepted, "Dispute 1 should be accepted."); - assertTrue(status2 == IDisputeManager.DisputeStatus.Rejected, "Dispute 2 should be rejected."); - } - - function test_RevertIf_CallerIsNotArbitrator_AcceptDispute() public { - createProvisionAndAllocate(allocationID, 10000 ether); - - bytes32 disputeID = createIndexingDispute(allocationID, bytes32("POI1"), 200 ether); - - // attempt to accept dispute as fisherman - vm.prank(fisherman); - vm.expectRevert(bytes4(keccak256("DisputeManagerNotArbitrator()"))); - disputeManager.acceptDispute(disputeID, 5000 ether); - } - - function test_RevertIf_SlashingOverMaxSlashPercentage() public { - createProvisionAndAllocate(allocationID, 10000 ether); - bytes32 disputeID = createIndexingDispute(allocationID, bytes32("POI101"), 200 ether); - - // max slashing percentage is 50% - vm.prank(arbitrator); - bytes memory expectedError = abi.encodeWithSignature("DisputeManagerInvalidTokensSlash(uint256)", 6000 ether); - vm.expectRevert(expectedError); - disputeManager.acceptDispute(disputeID, 6000 ether); - } - - // Cancel dispute - - function testCancelDispute() public { - createProvisionAndAllocate(allocationID, 10000 ether); - bytes32 disputeID = createIndexingDispute(allocationID, bytes32("POI1"), 200 ether); - - // skip to end of dispute period - skip(disputePeriod + 1); - - vm.prank(fisherman); - disputeManager.cancelDispute(disputeID); - - assertEq(graphToken.balanceOf(fisherman), 200 ether, "Fisherman should receive their deposit back."); - assertEq(graphToken.balanceOf(indexer), 10000 ether, "There's no slashing to the indexer."); - } - - function testCancelQueryDisputeConflicting() public { - createProvisionAndAllocate(allocationID, 10000 ether); - - bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); - bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); - bytes32 subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); - - (bytes memory attestationData1, bytes memory attestationData2) = createConflictingAttestations( - responseCID1, - subgraphDeploymentId, - responseCID2, - subgraphDeploymentId - ); - - vm.prank(fisherman); - (bytes32 disputeID1, bytes32 disputeID2) = disputeManager.createQueryDisputeConflict( - attestationData1, - attestationData2 - ); - - // skip to end of dispute period - skip(disputePeriod + 1); - - vm.prank(fisherman); - disputeManager.cancelDispute(disputeID1); - - assertEq(graphToken.balanceOf(indexer), 10000 ether, "There's no slashing to the indexer."); - - (, , , , , IDisputeManager.DisputeStatus status1, ) = disputeManager.disputes(disputeID1); - (, , , , , IDisputeManager.DisputeStatus status2, ) = disputeManager.disputes(disputeID2); - assertTrue(status1 == IDisputeManager.DisputeStatus.Cancelled, "Dispute 1 should be cancelled."); - assertTrue(status2 == IDisputeManager.DisputeStatus.Cancelled, "Dispute 2 should be cancelled."); - } - - function test_RevertIf_CallerIsNotFisherman_CancelDispute() public { - createProvisionAndAllocate(allocationID, 10000 ether); - bytes32 disputeID = createIndexingDispute(allocationID, bytes32("POI1"), 200 ether); - - vm.prank(arbitrator); - vm.expectRevert(bytes4(keccak256("DisputeManagerNotFisherman()"))); - disputeManager.cancelDispute(disputeID); - } - - // Draw dispute - - function testDrawDispute() public { - createProvisionAndAllocate(allocationID, 10000 ether); - bytes32 disputeID = createIndexingDispute(allocationID, bytes32("POI32"), 200 ether); - - vm.prank(arbitrator); - disputeManager.drawDispute(disputeID); - - assertEq(graphToken.balanceOf(fisherman), 200 ether, "Fisherman should receive their deposit back."); - assertEq(graphToken.balanceOf(indexer), 10000 ether, "There's no slashing to the indexer."); - } - - function testDrawQueryDisputeConflicting() public { - createProvisionAndAllocate(allocationID, 10000 ether); - - bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); - bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); - bytes32 subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); - - (bytes memory attestationData1, bytes memory attestationData2) = createConflictingAttestations( - responseCID1, - subgraphDeploymentId, - responseCID2, - subgraphDeploymentId - ); - - vm.prank(fisherman); - (bytes32 disputeID1, bytes32 disputeID2) = disputeManager.createQueryDisputeConflict( - attestationData1, - attestationData2 - ); - - vm.prank(arbitrator); - disputeManager.drawDispute(disputeID1); - - assertEq(graphToken.balanceOf(indexer), 10000 ether, "There's no slashing to the indexer."); - - (, , , , , IDisputeManager.DisputeStatus status1, ) = disputeManager.disputes(disputeID1); - (, , , , , IDisputeManager.DisputeStatus status2, ) = disputeManager.disputes(disputeID2); - assertTrue(status1 == IDisputeManager.DisputeStatus.Drawn, "Dispute 1 should be drawn."); - assertTrue(status2 == IDisputeManager.DisputeStatus.Drawn, "Dispute 2 should be drawn."); - } - - function test_RevertIf_CallerIsNotArbitrator_DrawDispute() public { - createProvisionAndAllocate(allocationID, 10000 ether); - bytes32 disputeID = createIndexingDispute(allocationID,bytes32("POI1"), 200 ether); - - // attempt to draw dispute as fisherman - vm.prank(fisherman); - vm.expectRevert(bytes4(keccak256("DisputeManagerNotArbitrator()"))); - disputeManager.drawDispute(disputeID); - } -} diff --git a/packages/subgraph-service/test/SubgraphBaseTest.t.sol b/packages/subgraph-service/test/SubgraphBaseTest.t.sol new file mode 100644 index 000000000..8344875bf --- /dev/null +++ b/packages/subgraph-service/test/SubgraphBaseTest.t.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { Controller } from "@graphprotocol/contracts/contracts/governance/Controller.sol"; +import { GraphPayments } from "@graphprotocol/horizon/contracts/payments/GraphPayments.sol"; +import { GraphProxy } from "@graphprotocol/contracts/contracts/upgrades/GraphProxy.sol"; +import { GraphProxyAdmin } from "@graphprotocol/contracts/contracts/upgrades/GraphProxyAdmin.sol"; +import { HorizonStaking } from "@graphprotocol/horizon/contracts/staking/HorizonStaking.sol"; +import { HorizonStakingExtension } from "@graphprotocol/horizon/contracts/staking/HorizonStakingExtension.sol"; +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; +import { IPaymentsEscrow } from "@graphprotocol/horizon/contracts/interfaces/IPaymentsEscrow.sol"; +import { ITAPCollector } from "@graphprotocol/horizon/contracts/interfaces/ITAPCollector.sol"; +import { TAPCollector } from "@graphprotocol/horizon/contracts/payments/collectors/TAPCollector.sol"; +import { PaymentsEscrow } from "@graphprotocol/horizon/contracts/payments/PaymentsEscrow.sol"; +import { UnsafeUpgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +import { Constants } from "./utils/Constants.sol"; +import { DisputeManager } from "../contracts/DisputeManager.sol"; +import { SubgraphService } from "../contracts/SubgraphService.sol"; +import { Users } from "./utils/Users.sol"; +import { Utils } from "./utils/Utils.sol"; + +import { MockCuration } from "./mocks/MockCuration.sol"; +import { MockGRTToken } from "./mocks/MockGRTToken.sol"; +import { MockRewardsManager } from "./mocks/MockRewardsManager.sol"; + +abstract contract SubgraphBaseTest is Utils, Constants { + + /* + * VARIABLES + */ + + /* Contracts */ + + GraphProxyAdmin proxyAdmin; + Controller controller; + SubgraphService subgraphService; + DisputeManager disputeManager; + IHorizonStaking staking; + IGraphPayments graphPayments; + IPaymentsEscrow escrow; + ITAPCollector tapCollector; + + HorizonStaking private stakingBase; + HorizonStakingExtension private stakingExtension; + + MockCuration curation; + MockGRTToken token; + MockRewardsManager rewardsManager; + + /* Users */ + + Users internal users; + + /* + * SET UP + */ + + function setUp() public virtual { + token = new MockGRTToken(); + + // Setup Users + users = Users({ + governor: createUser("governor"), + deployer: createUser("deployer"), + indexer: createUser("indexer"), + operator: createUser("operator"), + gateway: createUser("gateway"), + verifier: createUser("verifier"), + delegator: createUser("delegator"), + arbitrator: createUser("arbitrator"), + fisherman: createUser("fisherman"), + rewardsDestination: createUser("rewardsDestination") + }); + + deployProtocolContracts(); + setupProtocol(); + unpauseProtocol(); + vm.stopPrank(); + } + + function deployProtocolContracts() private { + resetPrank(users.governor); + proxyAdmin = new GraphProxyAdmin(); + controller = new Controller(); + + resetPrank(users.deployer); + GraphProxy stakingProxy = new GraphProxy(address(0), address(proxyAdmin)); + rewardsManager = new MockRewardsManager(token, rewardsPerSignal); + curation = new MockCuration(); + + // GraphPayments predict address + bytes32 saltGraphPayments = keccak256("GraphPaymentsSalt"); + bytes32 paymentsHash = keccak256(bytes.concat( + vm.getCode("GraphPayments.sol:GraphPayments"), + abi.encode(address(controller), protocolPaymentCut) + )); + address predictedGraphPaymentsAddress = vm.computeCreate2Address( + saltGraphPayments, + paymentsHash, + users.deployer + ); + + // GraphEscrow predict address + bytes32 saltEscrow = keccak256("GraphEscrowSalt"); + bytes32 escrowHash = keccak256(bytes.concat( + vm.getCode("PaymentsEscrow.sol:PaymentsEscrow"), + abi.encode( + address(controller), + revokeCollectorThawingPeriod, + withdrawEscrowThawingPeriod + ) + )); + address predictedEscrowAddress = vm.computeCreate2Address( + saltEscrow, + escrowHash, + users.deployer + ); + + resetPrank(users.governor); + controller.setContractProxy(keccak256("GraphToken"), address(token)); + controller.setContractProxy(keccak256("Staking"), address(stakingProxy)); + controller.setContractProxy(keccak256("RewardsManager"), address(rewardsManager)); + controller.setContractProxy(keccak256("GraphPayments"), predictedGraphPaymentsAddress); + controller.setContractProxy(keccak256("PaymentsEscrow"), predictedEscrowAddress); + controller.setContractProxy(keccak256("EpochManager"), makeAddr("EpochManager")); + controller.setContractProxy(keccak256("GraphTokenGateway"), makeAddr("GraphTokenGateway")); + controller.setContractProxy(keccak256("GraphProxyAdmin"), makeAddr("GraphProxyAdmin")); + controller.setContractProxy(keccak256("Curation"), address(curation)); + + resetPrank(users.deployer); + address disputeManagerImplementation = address(new DisputeManager(address(controller))); + address disputeManagerProxy = UnsafeUpgrades.deployTransparentProxy( + disputeManagerImplementation, + users.governor, + abi.encodeCall( + DisputeManager.initialize, + (users.arbitrator, disputePeriod, minimumDeposit, fishermanRewardPercentage, maxSlashingPercentage) + ) + ); + disputeManager = DisputeManager(disputeManagerProxy); + + tapCollector = new TAPCollector("TAPCollector", "1", address(controller)); + address subgraphServiceImplementation = address( + new SubgraphService(address(controller), address(disputeManager), address(tapCollector), address(curation)) + ); + address subgraphServiceProxy = UnsafeUpgrades.deployTransparentProxy( + subgraphServiceImplementation, + users.governor, + abi.encodeCall(SubgraphService.initialize, (minimumProvisionTokens, delegationRatio)) + ); + subgraphService = SubgraphService(subgraphServiceProxy); + + disputeManager.setSubgraphService(address(subgraphService)); + + stakingExtension = new HorizonStakingExtension( + address(controller), + address(subgraphService) + ); + stakingBase = new HorizonStaking( + address(controller), + address(stakingExtension), + address(subgraphService) + ); + + graphPayments = new GraphPayments{salt: saltGraphPayments}( + address(controller), + protocolPaymentCut + ); + escrow = new PaymentsEscrow{salt: saltEscrow}( + address(controller), + revokeCollectorThawingPeriod, + withdrawEscrowThawingPeriod + ); + + resetPrank(users.governor); + proxyAdmin.upgrade(stakingProxy, address(stakingBase)); + proxyAdmin.acceptProxy(stakingBase, stakingProxy); + staking = IHorizonStaking(address(stakingProxy)); + } + + function setupProtocol() private { + resetPrank(users.governor); + staking.setMaxThawingPeriod(MAX_THAWING_PERIOD); + resetPrank(users.deployer); + subgraphService.setStakeToFeesRatio(stakeToFeesRatio); + subgraphService.setMaxPOIStaleness(maxPOIStaleness); + subgraphService.setPaymentCuts(IGraphPayments.PaymentTypes.QueryFee, serviceCut, curationCut); + } + + function unpauseProtocol() private { + resetPrank(users.governor); + controller.setPaused(false); + } + + function createUser(string memory name) private returns (address) { + address user = makeAddr(name); + vm.deal({ account: user, newBalance: 100 ether }); + deal({ token: address(token), to: user, give: 10_000_000_000 ether }); + vm.label({ account: user, newLabel: name }); + return user; + } + + function mint(address _address, uint256 amount) internal { + deal({ token: address(token), to: _address, give: amount }); + } + + function burn(address _from, uint256 amount) internal { + token.burnFrom(_from, amount); + } +} \ No newline at end of file diff --git a/packages/subgraph-service/test/disputes/DisputeManager.t.sol b/packages/subgraph-service/test/disputes/DisputeManager.t.sol new file mode 100644 index 000000000..8b6f95b12 --- /dev/null +++ b/packages/subgraph-service/test/disputes/DisputeManager.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"; +import { Attestation } from "../../contracts/libraries/Attestation.sol"; + +import { SubgraphServiceSharedTest } from "../shared/SubgraphServiceShared.t.sol"; + +contract DisputeManagerTest is SubgraphServiceSharedTest { + using PPMMath for uint256; + + /* + * MODIFIERS + */ + + modifier useFisherman { + vm.startPrank(users.fisherman); + _; + vm.stopPrank(); + } + + /* + * HELPERS + */ + + function _createIndexingDispute(address _allocationID, bytes32 _poi, uint256 tokens) internal returns (bytes32 disputeID) { + address msgSender; + (, msgSender,) = vm.readCallers(); + resetPrank(users.fisherman); + token.approve(address(disputeManager), tokens); + bytes32 _disputeID = disputeManager.createIndexingDispute(_allocationID, _poi, tokens); + resetPrank(msgSender); + return _disputeID; + } + + function _createQueryDispute(uint256 tokens) internal returns (bytes32 disputeID) { + address msgSender; + (, msgSender,) = vm.readCallers(); + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = Attestation.Receipt({ + requestCID: keccak256(abi.encodePacked("Request CID")), + responseCID: keccak256(abi.encodePacked("Response CID")), + subgraphDeploymentId: keccak256(abi.encodePacked("Subgraph Deployment ID")) + }); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + + token.approve(address(disputeManager), tokens); + bytes32 _disputeID = disputeManager.createQueryDispute(attestationData, tokens); + resetPrank(msgSender); + return _disputeID; + } + + function _createConflictingAttestations( + bytes32 responseCID1, + bytes32 subgraphDeploymentId1, + bytes32 responseCID2, + bytes32 subgraphDeploymentId2 + ) internal view returns (bytes memory attestationData1, bytes memory attestationData2) { + bytes32 requestCID = keccak256(abi.encodePacked("Request CID")); + Attestation.Receipt memory receipt1 = Attestation.Receipt({ + requestCID: requestCID, + responseCID: responseCID1, + subgraphDeploymentId: subgraphDeploymentId1 + }); + + Attestation.Receipt memory receipt2 = Attestation.Receipt({ + requestCID: requestCID, + responseCID: responseCID2, + subgraphDeploymentId: subgraphDeploymentId2 + }); + + bytes memory _attestationData1 = _createAtestationData(receipt1, allocationIDPrivateKey); + bytes memory _attestationData2 = _createAtestationData(receipt2, allocationIDPrivateKey); + return (_attestationData1, _attestationData2); + } + + function _createAtestationData( + Attestation.Receipt memory receipt, + uint256 signer + ) private view returns (bytes memory attestationData) { + bytes32 digest = disputeManager.encodeReceipt(receipt); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signer, digest); + + return abi.encodePacked(receipt.requestCID, receipt.responseCID, receipt.subgraphDeploymentId, r, s, v); + } +} diff --git a/packages/subgraph-service/test/disputes/accept.t.sol b/packages/subgraph-service/test/disputes/accept.t.sol new file mode 100644 index 000000000..6292e65a4 --- /dev/null +++ b/packages/subgraph-service/test/disputes/accept.t.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "./DisputeManager.t.sol"; + +contract DisputeManagerAcceptDisputeTest is DisputeManagerTest { + using PPMMath for uint256; + + /* + * TESTS + */ + + function testAccept_IndexingDispute( + uint256 tokens, + uint256 tokensDispute, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + + uint256 fishermanPreviousBalance = token.balanceOf(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1"), tokensDispute); + + resetPrank(users.arbitrator); + disputeManager.acceptDispute(disputeID, tokensSlash); + + uint256 fishermanReward = tokensSlash.mulPPM(fishermanRewardPercentage); + uint256 fishermanExpectedBalance = fishermanPreviousBalance + fishermanReward; + assertEq(token.balanceOf(users.fisherman), fishermanExpectedBalance, "Fisherman should receive 50% of slashed tokens."); + } + + function testAccept_QueryDispute( + uint256 tokens, + uint256 tokensDispute, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + + uint256 fishermanPreviousBalance = token.balanceOf(users.fisherman); + bytes32 disputeID = _createQueryDispute(tokensDispute); + + resetPrank(users.arbitrator); + disputeManager.acceptDispute(disputeID, tokensSlash); + + uint256 fishermanReward = tokensSlash.mulPPM(fishermanRewardPercentage); + uint256 fishermanExpectedBalance = fishermanPreviousBalance + fishermanReward; + assertEq(token.balanceOf(users.fisherman), fishermanExpectedBalance, "Fisherman should receive 50% of slashed tokens."); + } + + function testAccept_QueryDisputeConflicting( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + bytes32 subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + uint256 fishermanPreviousBalance = token.balanceOf(users.fisherman); + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + responseCID1, + subgraphDeploymentId, + responseCID2, + subgraphDeploymentId + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, bytes32 disputeID2) = disputeManager.createQueryDisputeConflict( + attestationData1, + attestationData2 + ); + + resetPrank(users.arbitrator); + disputeManager.acceptDispute(disputeID1, tokensSlash); + + uint256 fishermanReward = tokensSlash.mulPPM(fishermanRewardPercentage); + uint256 fishermanExpectedBalance = fishermanPreviousBalance + fishermanReward; + assertEq(token.balanceOf(users.fisherman), fishermanExpectedBalance, "Fisherman should receive 50% of slashed tokens."); + + (, , , , , IDisputeManager.DisputeStatus status1, ) = disputeManager.disputes(disputeID1); + (, , , , , IDisputeManager.DisputeStatus status2, ) = disputeManager.disputes(disputeID2); + assertTrue(status1 == IDisputeManager.DisputeStatus.Accepted, "Dispute 1 should be accepted."); + assertTrue(status2 == IDisputeManager.DisputeStatus.Rejected, "Dispute 2 should be rejected."); + } + + function testAccept_RevertIf_CallerIsNotArbitrator( + uint256 tokens, + uint256 tokensDispute, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1"), tokensDispute); + + // attempt to accept dispute as fisherman + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function testAccept_RevertWhen_SlashingOverMaxSlashPercentage( + uint256 tokens, + uint256 tokensDispute, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, uint256(maxSlashingPercentage).mulPPM(tokens) + 1, type(uint256).max); + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI101"), tokensDispute); + + // max slashing percentage is 50% + resetPrank(users.arbitrator); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidTokensSlash.selector, + tokensSlash + ); + vm.expectRevert(expectedError); + disputeManager.acceptDispute(disputeID, tokensSlash); + } +} diff --git a/packages/subgraph-service/test/disputes/cancel.t.sol b/packages/subgraph-service/test/disputes/cancel.t.sol new file mode 100644 index 000000000..741feda16 --- /dev/null +++ b/packages/subgraph-service/test/disputes/cancel.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "./DisputeManager.t.sol"; + +contract DisputeManagerCancelDisputeTest is DisputeManagerTest { + + /* + * TESTS + */ + + function testCancel_Dispute( + uint256 tokens, + uint256 tokensDispute + ) public useIndexer useAllocation(tokens) { + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + uint256 fishermanPreviousBalance = token.balanceOf(users.fisherman); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1"), tokensDispute); + + // skip to end of dispute period + skip(disputePeriod + 1); + + resetPrank(users.fisherman); + disputeManager.cancelDispute(disputeID); + + assertEq(token.balanceOf(users.fisherman), fishermanPreviousBalance, "Fisherman should receive their deposit back."); + } + + function testCancel_QueryDisputeConflicting( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + bytes32 subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + responseCID1, + subgraphDeploymentId, + responseCID2, + subgraphDeploymentId + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, bytes32 disputeID2) = disputeManager.createQueryDisputeConflict( + attestationData1, + attestationData2 + ); + + // skip to end of dispute period + skip(disputePeriod + 1); + + disputeManager.cancelDispute(disputeID1); + + (, , , , , IDisputeManager.DisputeStatus status1, ) = disputeManager.disputes(disputeID1); + (, , , , , IDisputeManager.DisputeStatus status2, ) = disputeManager.disputes(disputeID2); + assertTrue(status1 == IDisputeManager.DisputeStatus.Cancelled, "Dispute 1 should be cancelled."); + assertTrue(status2 == IDisputeManager.DisputeStatus.Cancelled, "Dispute 2 should be cancelled."); + } + + function testCancel_RevertIf_CallerIsNotFisherman( + uint256 tokens, + uint256 tokensDispute + ) public useIndexer useAllocation(tokens) { + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1"), tokensDispute); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotFisherman.selector)); + disputeManager.cancelDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputes/create.t.sol b/packages/subgraph-service/test/disputes/create.t.sol new file mode 100644 index 000000000..5112aea0a --- /dev/null +++ b/packages/subgraph-service/test/disputes/create.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "./DisputeManager.t.sol"; + +contract DisputeManagerCreateDisputeTest is DisputeManagerTest { + + /* + * TESTS + */ + + function testCreate_IndexingDispute( + uint256 tokens, + uint256 tokensDispute + ) public useIndexer useAllocation(tokens) { + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1"), tokensDispute); + assertTrue(disputeManager.isDisputeCreated(disputeID), "Dispute should be created."); + } + + function testCreate_QueryDispute( + uint256 tokens, + uint256 tokensDispute + ) public useIndexer useAllocation(tokens) { + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + bytes32 disputeID = _createQueryDispute(tokensDispute); + assertTrue(disputeManager.isDisputeCreated(disputeID), "Dispute should be created."); + } + + function testCreate_QueryDisputeConflict( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + bytes32 subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + responseCID1, + subgraphDeploymentId, + responseCID2, + subgraphDeploymentId + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, bytes32 disputeID2) = disputeManager.createQueryDisputeConflict( + attestationData1, + attestationData2 + ); + assertTrue(disputeManager.isDisputeCreated(disputeID1), "Dispute 1 should be created."); + assertTrue(disputeManager.isDisputeCreated(disputeID2), "Dispute 2 should be created."); + } + + function testCreate_RevertWhen_DisputeAlreadyCreated( + uint256 tokens, + uint256 tokensDispute + ) public useIndexer useAllocation(tokens) { + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1"), tokensDispute); + + // Create another dispute with different fisherman + address otherFisherman = makeAddr("otherFisherman"); + resetPrank(otherFisherman); + mint(otherFisherman, tokensDispute); + token.approve(address(disputeManager), tokensDispute); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerDisputeAlreadyCreated.selector, + disputeID + ); + vm.expectRevert(expectedError); + disputeManager.createIndexingDispute(allocationID, bytes32("POI1"), tokensDispute); + vm.stopPrank(); + } + + function testCreate_RevertIf_DepositUnderMinimum( + uint256 tokensDispute + ) public useFisherman { + tokensDispute = bound(tokensDispute, 1, minimumDeposit - 1); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInsufficientDeposit.selector, + tokensDispute, + minimumDeposit + ); + vm.expectRevert(expectedError); + disputeManager.createIndexingDispute(allocationID, bytes32("POI3"), tokensDispute); + vm.stopPrank(); + } + + function testCreate_RevertIf_AllocationDoesNotExist( + uint256 tokens + ) public useFisherman { + tokens = bound(tokens, minimumDeposit, 10_000_000_000 ether); + token.approve(address(disputeManager), tokens); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerIndexerNotFound.selector, + allocationID + ); + vm.expectRevert(expectedError); + disputeManager.createIndexingDispute(allocationID, bytes32("POI4"), tokens); + vm.stopPrank(); + } + + function testCreate_RevertIf_ConflictingAttestationsResponsesAreTheSame() public useFisherman { + bytes32 requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + responseCID, + subgraphDeploymentId, + responseCID, + subgraphDeploymentId + ); + + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerNonConflictingAttestations.selector, + requestCID, + responseCID, + subgraphDeploymentId, + requestCID, + responseCID, + subgraphDeploymentId + ); + vm.expectRevert(expectedError); + disputeManager.createQueryDisputeConflict(attestationData1, attestationData2); + } + + function testCreate_RevertIf_ConflictingAttestationsHaveDifferentSubgraph() public { + bytes32 requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + bytes32 subgraphDeploymentId1 = keccak256(abi.encodePacked("Subgraph Deployment ID 1")); + bytes32 subgraphDeploymentId2 = keccak256(abi.encodePacked("Subgraph Deployment ID 2")); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + responseCID1, + subgraphDeploymentId1, + responseCID2, + subgraphDeploymentId2 + ); + + vm.prank(users.fisherman); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerNonConflictingAttestations.selector, + requestCID, + responseCID1, + subgraphDeploymentId1, + requestCID, + responseCID2, + subgraphDeploymentId2 + ); + vm.expectRevert(expectedError); + disputeManager.createQueryDisputeConflict(attestationData1, attestationData2); + } +} diff --git a/packages/subgraph-service/test/disputes/draw.t.sol b/packages/subgraph-service/test/disputes/draw.t.sol new file mode 100644 index 000000000..746d25ebd --- /dev/null +++ b/packages/subgraph-service/test/disputes/draw.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "./DisputeManager.t.sol"; + +contract DisputeManagerDrawDisputeTest is DisputeManagerTest { + + /* + * TESTS + */ + + function testDraw_Dispute( + uint256 tokens, + uint256 tokensDispute + ) public useIndexer useAllocation(tokens) { + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + uint256 fishermanPreviousBalance = token.balanceOf(users.fisherman); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI32"), tokensDispute); + + resetPrank(users.arbitrator); + disputeManager.drawDispute(disputeID); + + assertEq(token.balanceOf(users.fisherman), fishermanPreviousBalance, "Fisherman should receive their deposit back."); + } + + function testDraw_QueryDisputeConflicting( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + bytes32 subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + responseCID1, + subgraphDeploymentId, + responseCID2, + subgraphDeploymentId + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, bytes32 disputeID2) = disputeManager.createQueryDisputeConflict( + attestationData1, + attestationData2 + ); + + resetPrank(users.arbitrator); + disputeManager.drawDispute(disputeID1); + + (, , , , , IDisputeManager.DisputeStatus status1, ) = disputeManager.disputes(disputeID1); + (, , , , , IDisputeManager.DisputeStatus status2, ) = disputeManager.disputes(disputeID2); + assertTrue(status1 == IDisputeManager.DisputeStatus.Drawn, "Dispute 1 should be drawn."); + assertTrue(status2 == IDisputeManager.DisputeStatus.Drawn, "Dispute 2 should be drawn."); + } + + function testDraw_RevertIf_CallerIsNotArbitrator( + uint256 tokens, + uint256 tokensDispute + ) public useIndexer useAllocation(tokens) { + tokensDispute = bound(tokensDispute, minimumDeposit, tokens); + bytes32 disputeID =_createIndexingDispute(allocationID,bytes32("POI1"), tokens); + + // attempt to draw dispute as fisherman + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.drawDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/mocks/MockCuration.sol b/packages/subgraph-service/test/mocks/MockCuration.sol new file mode 100644 index 000000000..68b4a8ce9 --- /dev/null +++ b/packages/subgraph-service/test/mocks/MockCuration.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.26; + +contract MockCuration { + function isCurated(bytes32) public pure returns (bool) { + return true; + } + + function collect(bytes32, uint256) external {} +} \ No newline at end of file diff --git a/packages/subgraph-service/test/mocks/MockHorizonStaking.sol b/packages/subgraph-service/test/mocks/MockHorizonStaking.sol deleted file mode 100644 index d5f1d48d0..000000000 --- a/packages/subgraph-service/test/mocks/MockHorizonStaking.sol +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import "forge-std/Test.sol"; - -import { IHorizonStaking } from "@graphprotocol/horizon/contracts/interfaces/IHorizonStaking.sol"; -import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; -import { MockGRTToken } from "./MockGRTToken.sol"; - -contract MockHorizonStaking { - mapping (address verifier => mapping (address serviceProvider => IHorizonStaking.Provision provision)) public _provisions; - MockGRTToken public grtToken; - - constructor(address _grtTokenAddress) { - grtToken = MockGRTToken(_grtTokenAddress); - } - - // whitelist/deny a verifier - function allowVerifier(address verifier, bool allow) external {} - - // deposit stake - function stake(uint256 tokens) external {} - - // create a provision - function provision(uint256 tokens, address verifier, uint32 maxVerifierCut, uint64 thawingPeriod) external { - IHorizonStaking.Provision memory newProvision = IHorizonStakingTypes.Provision({ - tokens: tokens, - tokensThawing: 0, - sharesThawing: 0, - maxVerifierCut: maxVerifierCut, - thawingPeriod: thawingPeriod, - createdAt: uint64(block.timestamp), - maxVerifierCutPending: maxVerifierCut, - thawingPeriodPending: thawingPeriod - }); - _provisions[verifier][msg.sender] = newProvision; - } - - function acceptProvision(address serviceProvider) external {} - - // initiate a thawing to remove tokens from a provision - function thaw(bytes32 provisionId, uint256 tokens) external returns (bytes32 thawRequestId) {} - - // moves thawed stake from a provision back into the provider's available stake - function deprovision(bytes32 thawRequestId) external {} - - // moves thawed stake from one provision into another provision - function reprovision(bytes32 thawRequestId, bytes32 provisionId) external {} - - // moves thawed stake back to the owner's account - stake is removed from the protocol - function withdraw(bytes32 thawRequestId) external {} - - // delegate tokens to a provider - function delegate(address serviceProvider, uint256 tokens) external {} - - // undelegate tokens - function undelegate( - address serviceProvider, - uint256 tokens, - bytes32[] calldata provisions - ) external returns (bytes32 thawRequestId) {} - - // slash a service provider - function slash(address serviceProvider, uint256 tokens, uint256 reward, address rewardsDestination) external { - grtToken.mint(rewardsDestination, reward); - grtToken.burnFrom(serviceProvider, tokens); - } - - // set the Service Provider's preferred provisions to be force thawed - function setForceThawProvisions(bytes32[] calldata provisions) external {} - - // total staked tokens to the provider - // `ServiceProvider.tokensStaked + DelegationPool.serviceProvider.tokens` - function getStake(address serviceProvider) external view returns (uint256 tokens) {} - - // staked tokens that are currently not provisioned, aka idle stake - // `getStake(serviceProvider) - ServiceProvider.tokensProvisioned` - function getIdleStake(address serviceProvider) external view returns (uint256 tokens) {} - - // staked tokens the provider can provision before hitting the delegation cap - // `ServiceProvider.tokensStaked * Staking.delegationRatio - Provision.tokensProvisioned` - function getCapacity(address serviceProvider) external view returns (uint256 tokens) {} - - // provisioned tokens that are not being used - // `Provision.tokens - Provision.tokensThawing` - function getTokensAvailable(address serviceProvider, address verifier, uint32 delegationRatio) external view returns (uint256 tokens) { - return _provisions[verifier][serviceProvider].tokens; - } - - function getServiceProvider(address serviceProvider) external view returns (IHorizonStaking.ServiceProvider memory) {} - - function getProvision(address serviceProvider, address verifier) external view returns (IHorizonStaking.Provision memory) { - return _provisions[verifier][serviceProvider]; - } - - function isAuthorized(address, address, address) external pure returns (bool) { - return true; - } - - function getDelegationPool(address serviceProvider, address verifier) external view returns (IHorizonStakingTypes.DelegationPool memory) { - return IHorizonStakingTypes.DelegationPool({ - tokens: 0, - shares: 0, - tokensThawing: 0, - sharesThawing: 0 - }); - } - function getDelegationCut(address serviceProvider, uint8 paymentType) external view returns (uint256 delegationCut) {} - function addToDelegationPool(address serviceProvider, uint256 tokens) external {} - function stakeToProvision(address _serviceProvider, address _verifier, uint256 _tokens) external {} -} \ No newline at end of file diff --git a/packages/subgraph-service/test/mocks/MockRewardsManager.sol b/packages/subgraph-service/test/mocks/MockRewardsManager.sol index 158feef50..dad236c7a 100644 --- a/packages/subgraph-service/test/mocks/MockRewardsManager.sol +++ b/packages/subgraph-service/test/mocks/MockRewardsManager.sol @@ -4,8 +4,32 @@ pragma solidity 0.8.26; import "forge-std/Test.sol"; import { IRewardsManager } from "@graphprotocol/contracts/contracts/rewards/IRewardsManager.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; + +import { MockGRTToken } from "./MockGRTToken.sol"; + +interface IRewardsIssuer { + function getAllocationData( + address allocationId + ) + external + view + returns (address indexer, bytes32 subgraphDeploymentId, uint256 tokens, uint256 accRewardsPerAllocatedToken); +} contract MockRewardsManager is IRewardsManager { + using PPMMath for uint256; + + MockGRTToken public token; + uint256 public rewardsPerSignal; + + uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; + + constructor(MockGRTToken _token, uint256 _rewardsPerSignal) { + token = _token; + rewardsPerSignal = _rewardsPerSignal; + } + // -- Config -- function setIssuancePerBlock(uint256) external {} @@ -40,7 +64,20 @@ contract MockRewardsManager is IRewardsManager { function updateAccRewardsPerSignal() external returns (uint256) {} - function takeRewards(address) external returns (uint256) {} + function takeRewards(address _allocationID) external returns (uint256) { + address rewardsIssuer = msg.sender; + ( + , + , + uint256 tokens, + uint256 accRewardsPerAllocatedToken + ) = IRewardsIssuer(rewardsIssuer).getAllocationData(_allocationID); + + uint256 accRewardsPerTokens = tokens.mulPPM(rewardsPerSignal); + uint256 rewards = accRewardsPerTokens - accRewardsPerAllocatedToken; + token.mint(rewardsIssuer, rewards); + return rewards; + } // -- Hooks -- diff --git a/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol b/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol new file mode 100644 index 000000000..3db0509ec --- /dev/null +++ b/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { SubgraphBaseTest } from "../SubgraphBaseTest.t.sol"; + +abstract contract SubgraphServiceSharedTest is SubgraphBaseTest { + + /* + * VARIABLES + */ + + uint256 allocationIDPrivateKey; + address allocationID; + bytes32 subgraphDeployment; + + /* + * MODIFIERS + */ + + modifier useIndexer { + vm.startPrank(users.indexer); + _; + vm.stopPrank(); + } + + modifier useAllocation(uint256 tokens) { + vm.assume(tokens > minimumProvisionTokens); + vm.assume(tokens < 10_000_000_000 ether); + _createProvision(tokens); + _registerIndexer(address(0)); + _startService(tokens); + _; + } + + /* + * SET UP + */ + + function setUp() public virtual override { + super.setUp(); + (allocationID, allocationIDPrivateKey) = makeAddrAndKey("allocationId"); + subgraphDeployment = keccak256(abi.encodePacked("Subgraph Deployment ID")); + } + + /* + * HELPERS + */ + + function _createProvision(uint256 tokens) internal { + token.approve(address(staking), tokens); + staking.stakeTo(users.indexer, tokens); + staking.provision(users.indexer, address(subgraphService), tokens, maxSlashingPercentage, disputePeriod); + } + + function _registerIndexer(address rewardsDestination) internal { + subgraphService.register(users.indexer, abi.encode("url", "geoHash", rewardsDestination)); + } + + function _startService(uint256 tokens) internal { + bytes32 digest = subgraphService.encodeAllocationProof(users.indexer, allocationID); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationIDPrivateKey, digest); + + bytes memory data = abi.encode(subgraphDeployment, tokens, allocationID, abi.encodePacked(r, s, v)); + subgraphService.startService(users.indexer, data); + } +} diff --git a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol new file mode 100644 index 000000000..65498e485 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { SubgraphServiceSharedTest } from "../shared/SubgraphServiceShared.t.sol"; + +contract SubgraphServiceTest is SubgraphServiceSharedTest { + + /* + * VARIABLES + */ + + /* + * MODIFIERS + */ + + modifier useOperator { + vm.startPrank(users.operator); + _; + vm.stopPrank(); + } + + /* + * SET UP + */ + + function setUp() public virtual override { + super.setUp(); + } + + /* + * HELPERS + */ + +} diff --git a/packages/subgraph-service/test/subgraphService/allocate/start.t.sol b/packages/subgraph-service/test/subgraphService/allocate/start.t.sol new file mode 100644 index 000000000..65e4ec35b --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/allocate/start.t.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; + +import { Allocation } from "../../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../../contracts/utilities/AllocationManager.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { LegacyAllocation } from "../../../contracts/libraries/LegacyAllocation.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceAllocateStartTest is SubgraphServiceTest { + + /* + * Helpers + */ + + function _generateData(uint256 tokens) private view returns(bytes memory) { + bytes32 digest = subgraphService.encodeAllocationProof(users.indexer, allocationID); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationIDPrivateKey, digest); + return abi.encode(subgraphDeployment, tokens, allocationID, abi.encodePacked(r, s, v)); + } + + /* + * TESTS + */ + + function testStart_Allocation(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(tokens); + _registerIndexer(address(0)); + + bytes memory data = _generateData(tokens); + vm.expectEmit(address(subgraphService)); + emit IDataService.ServiceStarted(users.indexer, data); + subgraphService.startService(users.indexer, data); + + Allocation.State memory allocation = subgraphService.getAllocation(allocationID); + assertEq(allocation.tokens, tokens); + assertEq(allocation.indexer, users.indexer); + assertEq(allocation.subgraphDeploymentId, subgraphDeployment); + assertEq(allocation.createdAt, block.timestamp); + assertEq(allocation.closedAt, 0); + assertEq(allocation.lastPOIPresentedAt, 0); + assertEq(allocation.accRewardsPerAllocatedToken, 0); + assertEq(allocation.accRewardsPending, 0); + + uint256 subgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeployment); + assertEq(subgraphAllocatedTokens, tokens); + } + + function testStart_RevertWhen_NotAuthorized(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(tokens); + _registerIndexer(address(0)); + + resetPrank(users.operator); + bytes memory data = _generateData(tokens); + vm.expectRevert(abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + users.operator, + users.indexer + )); + subgraphService.startService(users.indexer, data); + } + + function testStart_RevertWhen_NoValidProvision(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + bytes memory data = _generateData(tokens); + vm.expectRevert(abi.encodeWithSelector( + ProvisionManager.ProvisionManagerProvisionNotFound.selector, + users.indexer + )); + subgraphService.startService(users.indexer, data); + } + + function testStart_RevertWhen_NotRegistered(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(tokens); + + bytes memory data = _generateData(tokens); + vm.expectRevert(abi.encodeWithSelector( + ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, + users.indexer + )); + subgraphService.startService(users.indexer, data); + } + + function testStart_RevertWhen_ZeroAllocationId(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(tokens); + _registerIndexer(address(0)); + + bytes32 digest = subgraphService.encodeAllocationProof(users.indexer, address(0)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationIDPrivateKey, digest); + bytes memory data = abi.encode(subgraphDeployment, tokens, address(0), abi.encodePacked(r, s, v)); + vm.expectRevert(abi.encodeWithSelector( + AllocationManager.AllocationManagerInvalidZeroAllocationId.selector + )); + subgraphService.startService(users.indexer, data); + } + + function testStart_RevertWhen_InvalidSignature(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(tokens); + _registerIndexer(address(0)); + + (address signer, uint256 signerPrivateKey) = makeAddrAndKey("invalidSigner"); + bytes32 digest = subgraphService.encodeAllocationProof(users.indexer, allocationID); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, digest); + bytes memory data = abi.encode(subgraphDeployment, tokens, allocationID, abi.encodePacked(r, s, v)); + vm.expectRevert(abi.encodeWithSelector( + AllocationManager.AllocationManagerInvalidAllocationProof.selector, + signer, + allocationID + )); + subgraphService.startService(users.indexer, data); + } + + function testStart_RevertWhen_ArealdyExists(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(tokens); + _registerIndexer(address(0)); + + bytes32 slot = keccak256(abi.encode(allocationID, uint256(158))); + vm.store(address(subgraphService), slot, bytes32(uint256(uint160(users.indexer)))); + vm.store(address(subgraphService), bytes32(uint256(slot) + 1), subgraphDeployment); + + bytes memory data = _generateData(tokens); + vm.expectRevert(abi.encodeWithSelector( + LegacyAllocation.LegacyAllocationExists.selector, + allocationID + )); + subgraphService.startService(users.indexer, data); + } + + function testStart_RevertWhen_NotEnoughTokens( + uint256 tokens, + uint256 lockTokens + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS - 1); + lockTokens = bound(lockTokens, tokens + 1, MAX_TOKENS); + + _createProvision(tokens); + _registerIndexer(address(0)); + + bytes memory data = _generateData(lockTokens); + vm.expectRevert(abi.encodeWithSelector( + ProvisionTracker.ProvisionTrackerInsufficientTokens.selector, + tokens, + lockTokens + )); + subgraphService.startService(users.indexer, data); + } +} diff --git a/packages/subgraph-service/test/subgraphService/allocate/stop.t.sol b/packages/subgraph-service/test/subgraphService/allocate/stop.t.sol new file mode 100644 index 000000000..06a2019ec --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/allocate/stop.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol"; + +import { Allocation } from "../../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../../contracts/utilities/AllocationManager.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { LegacyAllocation } from "../../../contracts/libraries/LegacyAllocation.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceAllocateStopTest is SubgraphServiceTest { + + /* + * Helpers + */ + + /* + * TESTS + */ + + function testStop_Allocation(uint256 tokens) public useIndexer useAllocation(tokens) { + assertTrue(subgraphService.isActiveAllocation(allocationID)); + bytes memory data = abi.encode(allocationID); + vm.expectEmit(address(subgraphService)); + emit IDataService.ServiceStopped(users.indexer, data); + subgraphService.stopService(users.indexer, data); + + uint256 subgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeployment); + assertEq(subgraphAllocatedTokens, 0); + } + + function testStop_RevertWhen_NotAuthorized(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.operator); + bytes memory data = abi.encode(allocationID); + vm.expectRevert(abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + users.operator, + users.indexer + )); + subgraphService.stopService(users.indexer, data); + } + + function testStop_RevertWhen_NotRegistered() public useIndexer { + bytes memory data = abi.encode(allocationID); + vm.expectRevert(abi.encodeWithSelector( + ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, + users.indexer + )); + subgraphService.stopService(users.indexer, data); + } + + function testStop_RevertWhen_NotOpen(uint256 tokens) public useIndexer useAllocation(tokens) { + bytes memory data = abi.encode(allocationID); + subgraphService.stopService(users.indexer, data); + vm.expectRevert(abi.encodeWithSelector( + Allocation.AllocationClosed.selector, + allocationID, + block.timestamp + )); + subgraphService.stopService(users.indexer, data); + } +} diff --git a/packages/subgraph-service/test/subgraphService/collect/collect.t.sol b/packages/subgraph-service/test/subgraphService/collect/collect.t.sol new file mode 100644 index 000000000..0683c4712 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/collect/collect.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { ITAPCollector } from "@graphprotocol/horizon/contracts/interfaces/ITAPCollector.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; + +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceRegisterTest is SubgraphServiceTest { + using PPMMath for uint128; + using PPMMath for uint256; + + address signer; + uint256 signerPrivateKey; + + /* + * HELPERS + */ + + function _getQueryFeeEncodedData( + uint128 tokens + ) private view returns (bytes memory) { + ITAPCollector.ReceiptAggregateVoucher memory rav = _getRAV(tokens); + bytes32 messageHash = tapCollector.encodeRAV(rav); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + ITAPCollector.SignedRAV memory signedRAV = ITAPCollector.SignedRAV(rav, signature); + return abi.encode(signedRAV); + } + + function _getRAV(uint128 tokens) private view returns (ITAPCollector.ReceiptAggregateVoucher memory rav) { + return ITAPCollector.ReceiptAggregateVoucher({ + dataService: address(subgraphService), + serviceProvider: users.indexer, + timestampNs: 0, + valueAggregate: tokens, + metadata: abi.encode(allocationID) + }); + } + + function _approveCollector(uint128 tokens) private { + address msgSender; + (, msgSender,) = vm.readCallers(); + resetPrank(signer); + mint(signer, tokens); + escrow.approveCollector(address(tapCollector), tokens); + token.approve(address(escrow), tokens); + escrow.deposit(users.indexer, tokens); + resetPrank(msgSender); + } + + /* + * SET UP + */ + + function setUp() public virtual override { + super.setUp(); + (signer, signerPrivateKey) = makeAddrAndKey("signer"); + vm.label({ account: signer, newLabel: "signer" }); + } + + /* + * TESTS + */ + + function testCollect_QueryFees( + uint256 tokens, + uint256 tokensPayment + ) public useIndexer useAllocation(tokens) { + vm.assume(tokens > minimumProvisionTokens * stakeToFeesRatio); + uint256 maxTokensPayment = tokens / stakeToFeesRatio > type(uint128).max ? type(uint128).max : tokens / stakeToFeesRatio; + tokensPayment = bound(tokensPayment, minimumProvisionTokens, maxTokensPayment); + uint128 tokensPayment128 = uint128(tokensPayment); + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.QueryFee; + bytes memory data = _getQueryFeeEncodedData(tokensPayment128); + + uint256 indexerPreviousBalance = token.balanceOf(users.indexer); + + _approveCollector(tokensPayment128); + subgraphService.collect(users.indexer, paymentType, data); + + uint256 indexerBalance = token.balanceOf(users.indexer); + uint256 tokensProtocol = tokensPayment128.mulPPM(protocolPaymentCut); + uint256 curationTokens = tokensPayment128.mulPPMRoundUp(curationCut); + uint256 dataServiceTokens = tokensPayment128.mulPPM(serviceCut + curationCut) - curationTokens; + + uint256 expectedIndexerTokensPayment = tokensPayment128 - tokensProtocol - dataServiceTokens - curationTokens; + assertEq(indexerBalance, indexerPreviousBalance + expectedIndexerTokensPayment); + } + + function testCollect_IndexingFees(uint256 tokens) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory data = abi.encode(allocationID, bytes32("POI1")); + + uint256 indexerPreviousProvisionBalance = staking.getProviderTokensAvailable(users.indexer, address(subgraphService)); + subgraphService.collect(users.indexer, paymentType, data); + + uint256 indexerProvisionBalance = staking.getProviderTokensAvailable(users.indexer, address(subgraphService)); + assertEq(indexerProvisionBalance, indexerPreviousProvisionBalance + tokens.mulPPM(rewardsPerSignal)); + } + + function testCollect_RevertWhen_InvalidPayment( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes invalidPaymentType = IGraphPayments.PaymentTypes.IndexingFee; + vm.expectRevert(abi.encodeWithSelector( + ISubgraphService.SubgraphServiceInvalidPaymentType.selector, + invalidPaymentType + )); + subgraphService.collect(users.indexer, invalidPaymentType, ""); + } +} diff --git a/packages/subgraph-service/test/subgraphService/provider/register.t.sol b/packages/subgraph-service/test/subgraphService/provider/register.t.sol new file mode 100644 index 000000000..2605d9e00 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/provider/register.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceRegisterTest is SubgraphServiceTest { + + /* + * TESTS + */ + + function testRegister_Indexer(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + _createProvision(tokens); + bytes memory data = abi.encode("url", "geoHash", users.rewardsDestination); + vm.expectEmit(address(subgraphService)); + emit IDataService.ServiceProviderRegistered( + users.indexer, + data + ); + subgraphService.register(users.indexer, data); + + uint256 registeredAt; + string memory url; + string memory geoHash; + (registeredAt, url, geoHash) = subgraphService.indexers(users.indexer); + assertEq(registeredAt, block.timestamp); + assertEq(url, "url"); + assertEq(geoHash, "geoHash"); + } + + function testRegister_RevertIf_AlreadyRegistered( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + vm.expectRevert(abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerAlreadyRegistered.selector)); + _registerIndexer(users.rewardsDestination); + } + + function testRegister_RevertWhen_InvalidProvision() public useIndexer { + vm.expectRevert(abi.encodeWithSelector( + ProvisionManager.ProvisionManagerProvisionNotFound.selector, + users.indexer + )); + _registerIndexer(users.rewardsDestination); + } + + function testRegister_RevertWhen_NotAuthorized() public { + resetPrank(users.operator); + vm.expectRevert(abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + users.operator, + users.indexer + )); + _registerIndexer(users.rewardsDestination); + } + + function testRegister_RevertWhen_InvalidProvisionValues(uint256 tokens) public useIndexer { + tokens = bound(tokens, 1, minimumProvisionTokens - 1); + _createProvision(tokens); + + vm.expectRevert(abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "tokens", + tokens, + minimumProvisionTokens, + maximumProvisionTokens + )); + _registerIndexer(address(0)); + } +} diff --git a/packages/subgraph-service/test/subgraphService/provision/accept.t.sol b/packages/subgraph-service/test/subgraphService/provision/accept.t.sol new file mode 100644 index 000000000..8e98078c6 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/provision/accept.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { ProvisionManager } from "@graphprotocol/horizon/contracts/data-service/utilities/ProvisionManager.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; + +contract SubgraphServiceProvisionAcceptTest is SubgraphServiceTest { + + /* + * TESTS + */ + + function testAccept_Provision(uint256 tokens) public useIndexer useAllocation(tokens) { + vm.expectEmit(address(subgraphService)); + emit IDataService.ProvisionAccepted(users.indexer); + subgraphService.acceptProvision(users.indexer, ""); + } + + function testAccept_RevertWhen_NotRegistered() public useIndexer { + vm.expectRevert(abi.encodeWithSelector( + ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, + users.indexer + )); + subgraphService.acceptProvision(users.indexer, ""); + } + + function testAccept_RevertWhen_NotAuthorized() public { + resetPrank(users.operator); + vm.expectRevert(abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + users.operator, + users.indexer + )); + subgraphService.acceptProvision(users.indexer, ""); + } +} diff --git a/packages/subgraph-service/test/utils/Constants.sol b/packages/subgraph-service/test/utils/Constants.sol new file mode 100644 index 000000000..75ff738a8 --- /dev/null +++ b/packages/subgraph-service/test/utils/Constants.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +abstract contract Constants { + uint256 internal constant MAX_TOKENS = 10_000_000_000 ether; + // Dispute Manager + uint64 internal constant disputePeriod = 300; // 5 minutes + uint256 internal constant minimumDeposit = 100 ether; // 100 GRT + uint32 internal constant fishermanRewardPercentage = 100000; // 10% + uint32 internal constant maxSlashingPercentage = 500000; // 50% + // Subgraph Service + uint256 internal constant minimumProvisionTokens = 1000 ether; + uint256 internal constant maximumProvisionTokens = type(uint256).max; + uint32 internal constant delegationRatio = 16; + uint256 public constant stakeToFeesRatio = 2; + uint256 public constant maxPOIStaleness = 28 days; + uint128 public constant serviceCut = 10000; + uint128 public constant curationCut = 10000; + // Staking + uint64 internal constant MAX_THAWING_PERIOD = 28 days; + // GraphEscrow parameters + uint256 internal constant withdrawEscrowThawingPeriod = 60; + uint256 internal constant revokeCollectorThawingPeriod = 60; + // GraphPayments parameters + uint256 internal constant protocolPaymentCut = 10000; + // RewardsMananger parameters + uint256 public constant rewardsPerSignal = 10000; +} \ No newline at end of file diff --git a/packages/subgraph-service/test/utils/Users.sol b/packages/subgraph-service/test/utils/Users.sol new file mode 100644 index 000000000..b2976f98a --- /dev/null +++ b/packages/subgraph-service/test/utils/Users.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +struct Users { + address governor; + address deployer; + address indexer; + address operator; + address gateway; + address verifier; + address delegator; + address arbitrator; + address fisherman; + address rewardsDestination; +} \ No newline at end of file diff --git a/packages/subgraph-service/test/utils/Utils.sol b/packages/subgraph-service/test/utils/Utils.sol new file mode 100644 index 000000000..47e4e68df --- /dev/null +++ b/packages/subgraph-service/test/utils/Utils.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +abstract contract Utils is Test { + /// @dev Stops the active prank and sets a new one. + function resetPrank(address msgSender) internal { + vm.stopPrank(); + vm.startPrank(msgSender); + } +} \ No newline at end of file