diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 4a40b3590..a9afcff28 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -128,18 +128,15 @@ contract DisputeManager is * The disputes are created in reference to an allocationId and specifically * a POI for that allocation. * This function is called by a challenger and it will pull `disputeDeposit` GRT tokens. - * + * * Requirements: - * - Challenger must have previously approved this contract to pull `disputeDeposit` amount + * - Challenger must have previously approved this contract to pull `disputeDeposit` amount * of tokens from their balance. - * + * * @param allocationId The allocation to dispute * @param poi The Proof of Indexing (POI) being disputed */ - function createIndexingDispute( - address allocationId, - bytes32 poi - ) external override returns (bytes32) { + function createIndexingDispute(address allocationId, bytes32 poi) external override returns (bytes32) { // Get funds from submitter _pullSubmitterDeposit(); @@ -150,11 +147,11 @@ contract DisputeManager is /** * @notice Create a query dispute for the arbitrator to resolve. * This function is called by a challenger and it will pull `disputeDeposit` GRT tokens. - * + * * * Requirements: - * - Challenger must have previously approved this contract to pull `disputeDeposit` amount + * - Challenger must have previously approved this contract to pull `disputeDeposit` amount * of tokens from their balance. - * + * * @param attestationData Attestation bytes submitted by the challenger */ function createQueryDispute(bytes calldata attestationData) external override returns (bytes32) { @@ -296,6 +293,8 @@ contract DisputeManager is // store dispute status dispute.status = IDisputeManager.DisputeStatus.Cancelled; + + emit DisputeCancelled(disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); } /** @@ -668,7 +667,10 @@ contract DisputeManager is * @param _fishermanRewardCut Reward as a percentage of indexer stake */ function _setFishermanRewardCut(uint32 _fishermanRewardCut) private { - require(_fishermanRewardCut <= MAX_FISHERMAN_REWARD_CUT, DisputeManagerInvalidFishermanReward(_fishermanRewardCut)); + require( + _fishermanRewardCut <= MAX_FISHERMAN_REWARD_CUT, + DisputeManagerInvalidFishermanReward(_fishermanRewardCut) + ); fishermanRewardCut = _fishermanRewardCut; emit FishermanRewardCutSet(_fishermanRewardCut); } diff --git a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol index 088133e05..73f7e56d6 100644 --- a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol +++ b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol @@ -143,6 +143,17 @@ interface IDisputeManager { */ event DisputeLinked(bytes32 indexed disputeId1, bytes32 indexed disputeId2); + /** + * @dev Emitted when a dispute is cancelled by the fisherman. + * The event emits the amount `tokens` returned to the fisherman. + */ + event DisputeCancelled( + bytes32 indexed disputeId, + address indexed indexer, + address indexed fisherman, + uint256 tokens + ); + // -- Errors -- error DisputeManagerNotArbitrator(); @@ -213,10 +224,10 @@ interface IDisputeManager { function getAttestationIndexer(Attestation.State memory attestation) external view returns (address); + function getStakeSnapshot(address indexer) external view returns (uint256); + function areConflictingAttestations( Attestation.State memory attestation1, Attestation.State memory attestation2 ) external pure returns (bool); - - function getStakeSnapshot(address indexer) external view returns (uint256); } diff --git a/packages/subgraph-service/contracts/libraries/Attestation.sol b/packages/subgraph-service/contracts/libraries/Attestation.sol index 587fadca7..0211db674 100644 --- a/packages/subgraph-service/contracts/libraries/Attestation.sol +++ b/packages/subgraph-service/contracts/libraries/Attestation.sol @@ -33,7 +33,7 @@ library Attestation { uint256 private constant SIG_S_OFFSET = RECEIPT_SIZE_BYTES + SIG_R_LENGTH; uint256 private constant SIG_V_OFFSET = RECEIPT_SIZE_BYTES + SIG_R_LENGTH + SIG_S_LENGTH; uint256 private constant SIG_SIZE_BYTES = SIG_R_LENGTH + SIG_S_LENGTH + SIG_V_LENGTH; - + uint256 private constant ATTESTATION_SIZE_BYTES = RECEIPT_SIZE_BYTES + SIG_SIZE_BYTES; uint256 private constant UINT8_BYTE_LENGTH = 1; diff --git a/packages/subgraph-service/test/SubgraphBaseTest.t.sol b/packages/subgraph-service/test/SubgraphBaseTest.t.sol index 9991ab8a5..2fe5ccf64 100644 --- a/packages/subgraph-service/test/SubgraphBaseTest.t.sol +++ b/packages/subgraph-service/test/SubgraphBaseTest.t.sol @@ -40,9 +40,9 @@ abstract contract SubgraphBaseTest is Utils, Constants { SubgraphService subgraphService; DisputeManager disputeManager; IHorizonStaking staking; - IGraphPayments graphPayments; + GraphPayments graphPayments; IPaymentsEscrow escrow; - ITAPCollector tapCollector; + TAPCollector tapCollector; HorizonStaking private stakingBase; HorizonStakingExtension private stakingExtension; @@ -196,7 +196,7 @@ abstract contract SubgraphBaseTest is Utils, Constants { controller.setPaused(false); } - function createUser(string memory name) private returns (address) { + function createUser(string memory name) internal 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 }); diff --git a/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol b/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol index 9f80512b4..1077c1435 100644 --- a/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol +++ b/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol @@ -8,6 +8,8 @@ import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils. import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"; import { Attestation } from "../../contracts/libraries/Attestation.sol"; +import { Allocation } from "../../contracts/libraries/Allocation.sol"; +import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"; import { SubgraphServiceSharedTest } from "../shared/SubgraphServiceShared.t.sol"; @@ -31,73 +33,415 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { } /* - * HELPERS + * ACTIONS */ - function _createIndexingDispute(address _allocationID, bytes32 _poi) internal returns (bytes32 disputeID) { - address msgSender; - (, msgSender,) = vm.readCallers(); - resetPrank(users.fisherman); - uint256 beforeFishermanBalance = token.balanceOf(users.fisherman); + function _setArbitrator(address _arbitrator) internal { + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.ArbitratorSet(_arbitrator); + disputeManager.setArbitrator(_arbitrator); + assertEq(disputeManager.arbitrator(), _arbitrator, "Arbitrator should be set."); + } + + function _setFishermanRewardCut(uint32 _fishermanRewardCut) internal { + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.FishermanRewardCutSet(_fishermanRewardCut); + disputeManager.setFishermanRewardCut(_fishermanRewardCut); + assertEq(disputeManager.fishermanRewardCut(), _fishermanRewardCut, "Fisherman reward cut should be set."); + } + + function _setMaxSlashingCut(uint32 _maxSlashingCut) internal { + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.MaxSlashingCutSet(_maxSlashingCut); + disputeManager.setMaxSlashingCut(_maxSlashingCut); + assertEq(disputeManager.maxSlashingCut(), _maxSlashingCut, "Max slashing cut should be set."); + } + + function _setDisputeDeposit(uint256 _disputeDeposit) internal { + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeDepositSet(_disputeDeposit); + disputeManager.setDisputeDeposit(_disputeDeposit); + assertEq(disputeManager.disputeDeposit(), _disputeDeposit, "Dispute deposit should be set."); + } + + function _createIndexingDispute(address _allocationId, bytes32 _poi) internal returns (bytes32) { + (, address fisherman, ) = vm.readCallers(); + bytes32 expectedDisputeId = keccak256(abi.encodePacked(_allocationId, _poi)); + uint256 disputeDeposit = disputeManager.disputeDeposit(); + uint256 beforeFishermanBalance = token.balanceOf(fisherman); + Allocation.State memory alloc = subgraphService.getAllocation(_allocationId); + uint256 stakeSnapshot = disputeManager.getStakeSnapshot(alloc.indexer); + + // Approve the dispute deposit token.approve(address(disputeManager), disputeDeposit); - bytes32 _disputeID = disputeManager.createIndexingDispute(_allocationID, _poi); - uint256 afterFishermanBalance = token.balanceOf(users.fisherman); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.IndexingDisputeCreated( + expectedDisputeId, + alloc.indexer, + fisherman, + disputeDeposit, + _allocationId, + _poi, + stakeSnapshot + ); + + // Create the indexing dispute + bytes32 _disputeId = disputeManager.createIndexingDispute(_allocationId, _poi); + + // Check that the dispute was created and that it has the correct ID + assertTrue(disputeManager.isDisputeCreated(_disputeId), "Dispute should be created."); + assertEq(expectedDisputeId, _disputeId, "Dispute ID should match"); + + // Check dispute values + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + assertEq(dispute.indexer, alloc.indexer, "Indexer should match"); + assertEq(dispute.fisherman, fisherman, "Fisherman should match"); + assertEq(dispute.deposit, disputeDeposit, "Deposit should match"); + assertEq(dispute.relatedDisputeId, bytes32(0), "Related dispute ID should be empty"); + assertEq(uint8(dispute.disputeType), uint8(IDisputeManager.DisputeType.IndexingDispute), "Dispute type should be indexing"); + assertEq(uint8(dispute.status), uint8(IDisputeManager.DisputeStatus.Pending), "Dispute status should be pending"); + assertEq(dispute.createdAt, block.timestamp, "Created at should match"); + assertEq(dispute.stakeSnapshot, stakeSnapshot, "Stake snapshot should match"); + + // Check that the fisherman was charged the dispute deposit + uint256 afterFishermanBalance = token.balanceOf(fisherman); assertEq(afterFishermanBalance, beforeFishermanBalance - disputeDeposit, "Fisherman should be charged the dispute deposit"); - resetPrank(msgSender); - return _disputeID; + + return _disputeId; } - function _createQueryDispute() 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); + function _createQueryDispute(bytes memory _attestationData) internal returns (bytes32) { + (, address fisherman,) = vm.readCallers(); + Attestation.State memory attestation = Attestation.parse(_attestationData); + address indexer = disputeManager.getAttestationIndexer(attestation); + bytes32 expectedDisputeId = keccak256( + abi.encodePacked( + attestation.requestCID, + attestation.responseCID, + attestation.subgraphDeploymentId, + indexer, + fisherman + ) + ); + uint256 disputeDeposit = disputeManager.disputeDeposit(); + uint256 beforeFishermanBalance = token.balanceOf(fisherman); + uint256 stakeSnapshot = disputeManager.getStakeSnapshot(indexer); - uint256 beforeFishermanBalance = token.balanceOf(users.fisherman); + // Approve the dispute deposit token.approve(address(disputeManager), disputeDeposit); - bytes32 _disputeID = disputeManager.createQueryDispute(attestationData); - uint256 afterFishermanBalance = token.balanceOf(users.fisherman); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.QueryDisputeCreated( + expectedDisputeId, + indexer, + fisherman, + disputeDeposit, + attestation.subgraphDeploymentId, + _attestationData, + stakeSnapshot + ); + + bytes32 _disputeID = disputeManager.createQueryDispute(_attestationData); + + // Check that the dispute was created and that it has the correct ID + assertTrue(disputeManager.isDisputeCreated(_disputeID), "Dispute should be created."); + assertEq(expectedDisputeId, _disputeID, "Dispute ID should match"); + + // Check dispute values + IDisputeManager.Dispute memory dispute = _getDispute(_disputeID); + assertEq(dispute.indexer, indexer, "Indexer should match"); + assertEq(dispute.fisherman, fisherman, "Fisherman should match"); + assertEq(dispute.deposit, disputeDeposit, "Deposit should match"); + assertEq(dispute.relatedDisputeId, bytes32(0), "Related dispute ID should be empty"); + assertEq(uint8(dispute.disputeType), uint8(IDisputeManager.DisputeType.QueryDispute), "Dispute type should be query"); + assertEq(uint8(dispute.status), uint8(IDisputeManager.DisputeStatus.Pending), "Dispute status should be pending"); + assertEq(dispute.createdAt, block.timestamp, "Created at should match"); + assertEq(dispute.stakeSnapshot, stakeSnapshot, "Stake snapshot should match"); + + // Check that the fisherman was charged the dispute deposit + uint256 afterFishermanBalance = token.balanceOf(fisherman); assertEq(afterFishermanBalance, beforeFishermanBalance - disputeDeposit, "Fisherman should be charged the dispute deposit"); - resetPrank(msgSender); + return _disputeID; } + function _createQueryDisputeConflict( + bytes memory attestationData1, + bytes memory attestationData2 + ) internal returns (bytes32, bytes32) { + (, address fisherman,) = vm.readCallers(); + Attestation.State memory attestation1 = Attestation.parse(attestationData1); + Attestation.State memory attestation2 = Attestation.parse(attestationData2); + address indexer1 = disputeManager.getAttestationIndexer(attestation1); + address indexer2 = disputeManager.getAttestationIndexer(attestation2); + bytes32 expectedDisputeId1 = keccak256( + abi.encodePacked( + attestation1.requestCID, + attestation1.responseCID, + attestation1.subgraphDeploymentId, + indexer1, + fisherman + ) + ); + bytes32 expectedDisputeId2 = keccak256( + abi.encodePacked( + attestation2.requestCID, + attestation2.responseCID, + attestation2.subgraphDeploymentId, + indexer2, + fisherman + ) + ); + uint256 stakeSnapshot1 = disputeManager.getStakeSnapshot(indexer1); + uint256 stakeSnapshot2 = disputeManager.getStakeSnapshot(indexer2); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.QueryDisputeCreated( + expectedDisputeId1, + indexer1, + fisherman, + 0, + attestation1.subgraphDeploymentId, + attestationData1, + stakeSnapshot1 + ); + emit IDisputeManager.QueryDisputeCreated( + expectedDisputeId2, + indexer2, + fisherman, + 0, + attestation2.subgraphDeploymentId, + attestationData2, + stakeSnapshot2 + ); + + (bytes32 _disputeId1, bytes32 _disputeId2) = disputeManager.createQueryDisputeConflict(attestationData1, attestationData2); + + // Check that the disputes were created and that they have the correct IDs + assertTrue(disputeManager.isDisputeCreated(_disputeId1), "Dispute 1 should be created."); + assertTrue(disputeManager.isDisputeCreated(_disputeId2), "Dispute 2 should be created."); + assertEq(expectedDisputeId1, _disputeId1, "Dispute 1 ID should match"); + assertEq(expectedDisputeId2, _disputeId2, "Dispute 2 ID should match"); + + // Check dispute values + IDisputeManager.Dispute memory dispute1 = _getDispute(_disputeId1); + assertEq(dispute1.indexer, indexer1, "Indexer 1 should match"); + assertEq(dispute1.fisherman, fisherman, "Fisherman 1 should match"); + assertEq(dispute1.deposit, 0, "Deposit 1 should match"); + assertEq(dispute1.relatedDisputeId, _disputeId2, "Related dispute ID 1 should be the id of the other dispute"); + assertEq(uint8(dispute1.disputeType), uint8(IDisputeManager.DisputeType.QueryDispute), "Dispute type 1 should be query"); + assertEq(uint8(dispute1.status), uint8(IDisputeManager.DisputeStatus.Pending), "Dispute status 1 should be pending"); + assertEq(dispute1.createdAt, block.timestamp, "Created at 1 should match"); + assertEq(dispute1.stakeSnapshot, stakeSnapshot1, "Stake snapshot 1 should match"); + + IDisputeManager.Dispute memory dispute2 = _getDispute(_disputeId2); + assertEq(dispute2.indexer, indexer2, "Indexer 2 should match"); + assertEq(dispute2.fisherman, fisherman, "Fisherman 2 should match"); + assertEq(dispute2.deposit, 0, "Deposit 2 should match"); + assertEq(dispute2.relatedDisputeId, _disputeId1, "Related dispute ID 2 should be the id of the other dispute"); + assertEq(uint8(dispute2.disputeType), uint8(IDisputeManager.DisputeType.QueryDispute), "Dispute type 2 should be query"); + assertEq(uint8(dispute2.status), uint8(IDisputeManager.DisputeStatus.Pending), "Dispute status 2 should be pending"); + assertEq(dispute2.createdAt, block.timestamp, "Created at 2 should match"); + assertEq(dispute2.stakeSnapshot, stakeSnapshot2, "Stake snapshot 2 should match"); + + return (_disputeId1, _disputeId2); + } + + function _acceptDispute(bytes32 _disputeId, uint256 _tokensSlash) internal { + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + address fisherman = dispute.fisherman; + uint256 fishermanPreviousBalance = token.balanceOf(fisherman); + uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); + uint256 disputeDeposit = dispute.deposit; + uint256 fishermanRewardPercentage = disputeManager.fishermanRewardCut(); + uint256 fishermanReward = _tokensSlash.mulPPM(fishermanRewardPercentage); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeAccepted(_disputeId, dispute.indexer, dispute.fisherman, dispute.deposit + fishermanReward); + + // Accept the dispute + disputeManager.acceptDispute(_disputeId, _tokensSlash); + + // Check fisherman's got their reward and their deposit (if any) back + uint256 fishermanExpectedBalance = fishermanPreviousBalance + fishermanReward + disputeDeposit; + assertEq(token.balanceOf(fisherman), fishermanExpectedBalance, "Fisherman should get their reward and deposit back"); + + // Check indexer was slashed by the correct amount + uint256 expectedIndexerTokensAvailable; + if (_tokensSlash > indexerTokensAvailable) { + expectedIndexerTokensAvailable = 0; + } else { + expectedIndexerTokensAvailable = indexerTokensAvailable - _tokensSlash; + } + assertEq(staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), expectedIndexerTokensAvailable, "Indexer should be slashed by the correct amount"); + + // Check dispute status + dispute = _getDispute(_disputeId); + assertEq(uint8(dispute.status), uint8(IDisputeManager.DisputeStatus.Accepted), "Dispute status should be accepted"); + + // If there's a related dispute, check that it was rejected + if (dispute.relatedDisputeId != bytes32(0)) { + IDisputeManager.Dispute memory relatedDispute = _getDispute(dispute.relatedDisputeId); + assertEq(uint8(relatedDispute.status), uint8(IDisputeManager.DisputeStatus.Rejected), "Related dispute status should be rejected"); + } + } + + function _drawDispute(bytes32 _disputeId) internal { + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + address fisherman = dispute.fisherman; + uint256 fishermanPreviousBalance = token.balanceOf(fisherman); + uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeDrawn(_disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); + + // Draw the dispute + disputeManager.drawDispute(_disputeId); + + // Check that the fisherman got their deposit back + uint256 fishermanExpectedBalance = fishermanPreviousBalance + dispute.deposit; + assertEq(token.balanceOf(fisherman), fishermanExpectedBalance, "Fisherman should receive their deposit back."); + + // Check that indexer was not slashed + assertEq(staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), indexerTokensAvailable, "Indexer should not be slashed"); + + // Check dispute status + dispute = _getDispute(_disputeId); + assertEq(uint8(dispute.status), uint8(IDisputeManager.DisputeStatus.Drawn), "Dispute status should be drawn"); + + // If there's a related dispute, check that it was drawn too + if (dispute.relatedDisputeId != bytes32(0)) { + IDisputeManager.Dispute memory relatedDispute = _getDispute(dispute.relatedDisputeId); + assertEq(uint8(relatedDispute.status), uint8(IDisputeManager.DisputeStatus.Drawn), "Related dispute status should be drawn"); + } + } + + function _rejectDispute(bytes32 _disputeId) internal { + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + address fisherman = dispute.fisherman; + uint256 fishermanPreviousBalance = token.balanceOf(fisherman); + uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeRejected(_disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); + + // Reject the dispute + disputeManager.rejectDispute(_disputeId); + + // Check that the fisherman didn't get their deposit back + assertEq(token.balanceOf(users.fisherman), fishermanPreviousBalance, "Fisherman should lose the deposit."); + + // Check that indexer was not slashed + assertEq(staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), indexerTokensAvailable, "Indexer should not be slashed"); + + // Check dispute status + dispute = _getDispute(_disputeId); + assertEq(uint8(dispute.status), uint8(IDisputeManager.DisputeStatus.Rejected), "Dispute status should be rejected"); + // Checl related id is empty + assertEq(dispute.relatedDisputeId, bytes32(0), "Related dispute ID should be empty"); + } + + function _cancelDispute(bytes32 _disputeId) internal { + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + address fisherman = dispute.fisherman; + uint256 fishermanPreviousBalance = token.balanceOf(fisherman); + uint256 disputePeriod = disputeManager.disputePeriod(); + uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); + + // skip to end of dispute period + skip(disputePeriod + 1); + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeCancelled(_disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); + + // Cancel the dispute + disputeManager.cancelDispute(_disputeId); + + // Check that the fisherman got their deposit back + uint256 fishermanExpectedBalance = fishermanPreviousBalance + dispute.deposit; + assertEq(token.balanceOf(users.fisherman), fishermanExpectedBalance, "Fisherman should receive their deposit back."); + + // Check that indexer was not slashed + assertEq(staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), indexerTokensAvailable, "Indexer should not be slashed"); + + // Check dispute status + dispute = _getDispute(_disputeId); + assertEq(uint8(dispute.status), uint8(IDisputeManager.DisputeStatus.Cancelled), "Dispute status should be cancelled"); + + // If there's a related dispute, check that it was cancelled too + if (dispute.relatedDisputeId != bytes32(0)) { + IDisputeManager.Dispute memory relatedDispute = _getDispute(dispute.relatedDisputeId); + assertEq(uint8(relatedDispute.status), uint8(IDisputeManager.DisputeStatus.Cancelled), "Related dispute status should be cancelled"); + } + } + + /* + * HELPERS + */ + + function _createAttestationReceipt( + bytes32 requestCID, + bytes32 responseCID, + bytes32 subgraphDeploymentId + ) internal pure returns (Attestation.Receipt memory receipt) { + return Attestation.Receipt({ + requestCID: requestCID, + responseCID: responseCID, + subgraphDeploymentId: subgraphDeploymentId + }); + } + function _createConflictingAttestations( + bytes32 requestCID, + bytes32 subgraphDeploymentId, bytes32 responseCID1, - bytes32 subgraphDeploymentId1, bytes32 responseCID2, - bytes32 subgraphDeploymentId2 + uint256 signer1, + uint256 signer2 ) 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 receipt1 = _createAttestationReceipt(requestCID, responseCID1, subgraphDeploymentId); + Attestation.Receipt memory receipt2 = _createAttestationReceipt(requestCID, responseCID2, subgraphDeploymentId); - Attestation.Receipt memory receipt2 = Attestation.Receipt({ - requestCID: requestCID, - responseCID: responseCID2, - subgraphDeploymentId: subgraphDeploymentId2 - }); - - bytes memory _attestationData1 = _createAtestationData(receipt1, allocationIDPrivateKey); - bytes memory _attestationData2 = _createAtestationData(receipt2, allocationIDPrivateKey); + bytes memory _attestationData1 = _createAtestationData(receipt1, signer1); + bytes memory _attestationData2 = _createAtestationData(receipt2, signer2); return (_attestationData1, _attestationData2); } function _createAtestationData( Attestation.Receipt memory receipt, uint256 signer - ) private view returns (bytes memory attestationData) { + ) internal 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); } + + /* + * PRIVATE FUNCTIONS + */ + + function _getDispute(bytes32 _disputeId) internal view returns (IDisputeManager.Dispute memory) { + ( + address indexer, + address fisherman, + uint256 deposit, + bytes32 relatedDisputeId, + IDisputeManager.DisputeType disputeType, + IDisputeManager.DisputeStatus status, + uint256 createdAt, + uint256 stakeSnapshot + ) = disputeManager.disputes(_disputeId); + return IDisputeManager.Dispute({ + indexer: indexer, + fisherman: fisherman, + deposit: deposit, + relatedDisputeId: relatedDisputeId, + disputeType: disputeType, + status: status, + createdAt: createdAt, + stakeSnapshot: stakeSnapshot + }); + } } diff --git a/packages/subgraph-service/test/disputeManager/constructor/constructor.t.sol b/packages/subgraph-service/test/disputeManager/constructor/constructor.t.sol index d4f100f68..76f90481a 100644 --- a/packages/subgraph-service/test/disputeManager/constructor/constructor.t.sol +++ b/packages/subgraph-service/test/disputeManager/constructor/constructor.t.sol @@ -49,10 +49,12 @@ contract DisputeManagerConstructorTest is DisputeManagerTest { * TESTS */ - function test_Constructor( - uint32 fishermanRewardPercentage + function test_DisputeManager_Constructor( + uint32 fishermanRewardPercentage, + uint32 maxSlashingPercentage ) public useDeployer { vm.assume(fishermanRewardPercentage <= disputeManager.MAX_FISHERMAN_REWARD_CUT()); + vm.assume(maxSlashingPercentage <= PPMMath.MAX_PPM); address disputeManagerImplementation = address(new DisputeManager(address(controller))); address proxy = _initializeDisputeManager( disputeManagerImplementation, @@ -70,7 +72,7 @@ contract DisputeManagerConstructorTest is DisputeManagerTest { assertEq(disputeManager.fishermanRewardCut(), fishermanRewardPercentage); } - function test_Constructor_RevertIf_ControllerAddressIsZero() public useDeployer { + function test_DisputeManager_Constructor_RevertIf_ControllerAddressIsZero() public useDeployer { bytes memory expectedError = abi.encodeWithSelector( GraphDirectory.GraphDirectoryInvalidZeroAddress.selector, "Controller" @@ -79,7 +81,7 @@ contract DisputeManagerConstructorTest is DisputeManagerTest { new DisputeManager(address(0)); } - function test_Constructor_RevertIf_ArbitratorAddressIsZero() public useDeployer { + function test_DisputeManager_Constructor_RevertIf_ArbitratorAddressIsZero() public useDeployer { address disputeManagerImplementation = address(new DisputeManager(address(controller))); bytes memory expectedError = abi.encodeWithSelector( IDisputeManager.DisputeManagerInvalidZeroAddress.selector @@ -95,7 +97,7 @@ contract DisputeManagerConstructorTest is DisputeManagerTest { ); } - function test_Constructor_RevertIf_InvalidDisputePeriod() public useDeployer { + function test_DisputeManager_Constructor_RevertIf_InvalidDisputePeriod() public useDeployer { address disputeManagerImplementation = address(new DisputeManager(address(controller))); bytes memory expectedError = abi.encodeWithSelector( IDisputeManager.DisputeManagerDisputePeriodZero.selector @@ -111,7 +113,7 @@ contract DisputeManagerConstructorTest is DisputeManagerTest { ); } - function test_Constructor_RevertIf_InvalidDisputeDeposit() public useDeployer { + function test_DisputeManager_Constructor_RevertIf_InvalidDisputeDeposit() public useDeployer { address disputeManagerImplementation = address(new DisputeManager(address(controller))); bytes memory expectedError = abi.encodeWithSelector( IDisputeManager.DisputeManagerInvalidDisputeDeposit.selector, @@ -128,7 +130,7 @@ contract DisputeManagerConstructorTest is DisputeManagerTest { ); } - function test_Constructor_RevertIf_InvalidFishermanRewardPercentage(uint32 _fishermanRewardPercentage) public useDeployer { + function test_DisputeManager_Constructor_RevertIf_InvalidFishermanRewardPercentage(uint32 _fishermanRewardPercentage) public useDeployer { vm.assume(_fishermanRewardPercentage > disputeManager.MAX_FISHERMAN_REWARD_CUT()); address disputeManagerImplementation = address(new DisputeManager(address(controller))); bytes memory expectedError = abi.encodeWithSelector( @@ -146,7 +148,7 @@ contract DisputeManagerConstructorTest is DisputeManagerTest { ); } - function test_Constructor_RevertIf_InvalidMaxSlashingPercentage(uint32 _maxSlashingPercentage) public useDeployer { + function test_DisputeManager_Constructor_RevertIf_InvalidMaxSlashingPercentage(uint32 _maxSlashingPercentage) public useDeployer { vm.assume(_maxSlashingPercentage > PPMMath.MAX_PPM); address disputeManagerImplementation = address(new DisputeManager(address(controller))); bytes memory expectedError = abi.encodeWithSelector( @@ -163,4 +165,4 @@ contract DisputeManagerConstructorTest is DisputeManagerTest { _maxSlashingPercentage ); } -} \ No newline at end of file +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/accept.t.sol b/packages/subgraph-service/test/disputeManager/disputes/accept.t.sol deleted file mode 100644 index 29e3bb6f5..000000000 --- a/packages/subgraph-service/test/disputeManager/disputes/accept.t.sol +++ /dev/null @@ -1,223 +0,0 @@ -// 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"; -import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; - -contract DisputeManagerAcceptDisputeTest is DisputeManagerTest { - using PPMMath for uint256; - - /* - * TESTS - */ - - function testAccept_IndexingDispute( - uint256 tokens, - uint256 tokensSlash, - uint256 delegationAmount - ) public useIndexer useAllocation(tokens) { - delegationAmount = bound(delegationAmount, 1 ether, 10_000_000_000 ether); - - resetPrank(users.delegator); - _delegate(delegationAmount); - - uint256 stakeSnapshot = disputeManager.getStakeSnapshot(users.indexer); - uint256 tokensSlashCap = stakeSnapshot.mulPPM(maxSlashingPercentage); - tokensSlash = bound(tokensSlash, 1, tokensSlashCap); - - uint256 fishermanPreviousBalance = token.balanceOf(users.fisherman); - bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); - - resetPrank(users.arbitrator); - disputeManager.acceptDispute(disputeID, tokensSlash); - - uint256 fishermanReward = Math.min(tokensSlash, tokens).mulPPM(fishermanRewardPercentage); - uint256 fishermanExpectedBalance = fishermanPreviousBalance + fishermanReward; - assertEq(token.balanceOf(users.fisherman), fishermanExpectedBalance); - } - - function testAccept_QueryDispute( - uint256 tokens, - uint256 tokensSlash, - uint256 delegationAmount - ) public useIndexer useAllocation(tokens) { - delegationAmount = bound(delegationAmount, 1 ether, 10_000_000_000 ether); - - resetPrank(users.delegator); - _delegate(delegationAmount); - - uint256 stakeSnapshot = disputeManager.getStakeSnapshot(users.indexer); - uint256 tokensSlashCap = stakeSnapshot.mulPPM(maxSlashingPercentage); - tokensSlash = bound(tokensSlash, 1, tokensSlashCap); - - uint256 fishermanPreviousBalance = token.balanceOf(users.fisherman); - bytes32 disputeID = _createQueryDispute(); - - resetPrank(users.arbitrator); - disputeManager.acceptDispute(disputeID, tokensSlash); - - uint256 fishermanReward = Math.min(tokensSlash, tokens).mulPPM(fishermanRewardPercentage); - uint256 fishermanExpectedBalance = fishermanPreviousBalance + fishermanReward; - assertEq(token.balanceOf(users.fisherman), fishermanExpectedBalance); - } - - function testAccept_QueryDisputeConflicting( - uint256 tokens, - uint256 tokensSlash, - uint256 delegationAmount - ) public useIndexer useAllocation(tokens) { - delegationAmount = bound(delegationAmount, 1 ether, 10_000_000_000 ether); - - resetPrank(users.delegator); - _delegate(delegationAmount); - - uint256 stakeSnapshot = disputeManager.getStakeSnapshot(users.indexer); - uint256 tokensSlashCap = stakeSnapshot.mulPPM(maxSlashingPercentage); - tokensSlash = bound(tokensSlash, 1, tokensSlashCap); - - 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 = Math.min(tokensSlash, tokens).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_IndexingDispute_RevertIf_SlashAmountTooHigh( - uint256 tokens, - uint256 tokensSlash, - uint256 delegationAmount - ) public useIndexer useAllocation(tokens) { - delegationAmount = bound(delegationAmount, 1 ether, 10_000_000_000 ether); - - resetPrank(users.delegator); - _delegate(delegationAmount); - - uint256 stakeSnapshot = disputeManager.getStakeSnapshot(users.indexer); - uint256 tokensSlashCap = stakeSnapshot.mulPPM(maxSlashingPercentage); - tokensSlash = bound(tokensSlash, tokensSlashCap + 1 wei, type(uint256).max); - - bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); - - resetPrank(users.arbitrator); - bytes memory expectedError = abi.encodeWithSelector( - IDisputeManager.DisputeManagerInvalidTokensSlash.selector, - tokensSlash, - tokensSlashCap - ); - vm.expectRevert(expectedError); - disputeManager.acceptDispute(disputeID, tokensSlash); - } - - function testAccept_QueryDispute_RevertIf_SlashAmountTooHigh( - uint256 tokens, - uint256 tokensSlash, - uint256 delegationAmount - ) public useIndexer useAllocation(tokens) { - delegationAmount = bound(delegationAmount, 1 ether, 10_000_000_000 ether); - - resetPrank(users.delegator); - _delegate(delegationAmount); - - uint256 stakeSnapshot = disputeManager.getStakeSnapshot(users.indexer); - uint256 tokensSlashCap = stakeSnapshot.mulPPM(maxSlashingPercentage); - tokensSlash = bound(tokensSlash, tokensSlashCap + 1 wei, type(uint256).max); - - bytes32 disputeID = _createQueryDispute(); - - resetPrank(users.arbitrator); - bytes memory expectedError = abi.encodeWithSelector( - IDisputeManager.DisputeManagerInvalidTokensSlash.selector, - tokensSlash, - tokensSlashCap - ); - vm.expectRevert(expectedError); - disputeManager.acceptDispute(disputeID, tokensSlash); - } - - function testAccept_ConflictingQueryDispute_RevertIf_SlashAmountTooHigh( - uint256 tokens, - uint256 tokensSlash, - uint256 delegationAmount - ) public useIndexer useAllocation(tokens) { - delegationAmount = bound(delegationAmount, 1 ether, 10_000_000_000 ether); - - resetPrank(users.delegator); - _delegate(delegationAmount); - - uint256 stakeSnapshot = disputeManager.getStakeSnapshot(users.indexer); - uint256 tokensSlashCap = stakeSnapshot.mulPPM(maxSlashingPercentage); - tokensSlash = bound(tokensSlash, tokensSlashCap + 1 wei, type(uint256).max); - - 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); - bytes memory expectedError = abi.encodeWithSelector( - IDisputeManager.DisputeManagerInvalidTokensSlash.selector, - tokensSlash, - tokensSlashCap - ); - vm.expectRevert(expectedError); - disputeManager.acceptDispute(disputeID1, tokensSlash); - } - - function testAccept_RevertIf_CallerIsNotArbitrator( - uint256 tokens, - uint256 tokensSlash - ) public useIndexer useAllocation(tokens) { - tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); - - bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); - - // attempt to accept dispute as fisherman - resetPrank(users.fisherman); - vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); - disputeManager.acceptDispute(disputeID, tokensSlash); - } -} diff --git a/packages/subgraph-service/test/disputeManager/disputes/cancel.t.sol b/packages/subgraph-service/test/disputeManager/disputes/cancel.t.sol deleted file mode 100644 index abd55e051..000000000 --- a/packages/subgraph-service/test/disputeManager/disputes/cancel.t.sol +++ /dev/null @@ -1,70 +0,0 @@ -// 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 - ) public useIndexer useAllocation(tokens) { - uint256 fishermanPreviousBalance = token.balanceOf(users.fisherman); - bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); - - // 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 - ) public useIndexer useAllocation(tokens) { - bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); - - resetPrank(users.arbitrator); - vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotFisherman.selector)); - disputeManager.cancelDispute(disputeID); - } -} diff --git a/packages/subgraph-service/test/disputeManager/disputes/create.t.sol b/packages/subgraph-service/test/disputeManager/disputes/create.t.sol deleted file mode 100644 index 05147ec73..000000000 --- a/packages/subgraph-service/test/disputeManager/disputes/create.t.sol +++ /dev/null @@ -1,146 +0,0 @@ -// 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 - ) public useIndexer useAllocation(tokens) { - bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); - assertTrue(disputeManager.isDisputeCreated(disputeID), "Dispute should be created."); - } - - function testCreate_QueryDispute( - uint256 tokens - ) public useIndexer useAllocation(tokens) { - bytes32 disputeID = _createQueryDispute(); - 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 - ) public useIndexer useAllocation(tokens) { - bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); - - // Create another dispute with different fisherman - address otherFisherman = makeAddr("otherFisherman"); - resetPrank(otherFisherman); - mint(otherFisherman, disputeDeposit); - token.approve(address(disputeManager), disputeDeposit); - bytes memory expectedError = abi.encodeWithSelector( - IDisputeManager.DisputeManagerDisputeAlreadyCreated.selector, - disputeID - ); - vm.expectRevert(expectedError); - disputeManager.createIndexingDispute(allocationID, bytes32("POI1")); - vm.stopPrank(); - } - - function testCreate_RevertIf_TokensNotApproved() public useFisherman { - bytes memory expectedError = abi.encodeWithSignature( - "ERC20InsufficientAllowance(address,uint256,uint256)", - address(disputeManager), - 0, - disputeDeposit - ); - vm.expectRevert(expectedError); - disputeManager.createIndexingDispute(allocationID, bytes32("POI3")); - vm.stopPrank(); - } - - function testCreate_RevertIf_AllocationDoesNotExist() public useFisherman { - token.approve(address(disputeManager), disputeDeposit); - bytes memory expectedError = abi.encodeWithSelector( - IDisputeManager.DisputeManagerIndexerNotFound.selector, - allocationID - ); - vm.expectRevert(expectedError); - disputeManager.createIndexingDispute(allocationID, bytes32("POI4")); - 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/disputeManager/disputes/disputes.t.sol b/packages/subgraph-service/test/disputeManager/disputes/disputes.t.sol new file mode 100644 index 000000000..cd229dbc4 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/disputes.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 DisputeManagerDisputeTest is DisputeManagerTest { + using PPMMath for uint256; + + /* + * TESTS + */ + + function test_Dispute_Accept_RevertIf_DisputeDoesNotExist( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + bytes32 disputeID = bytes32("0x0"); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidDispute.selector, + disputeID + )); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Dispute_Accept_RevertIf_SlashZeroTokens( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI101")); + + // attempt to accept dispute with 0 tokens slashed + resetPrank(users.arbitrator); + uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(tokens); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidTokensSlash.selector, 0, maxTokensToSlash)); + disputeManager.acceptDispute(disputeID, 0); + } + + function test_Dispute_Reject_RevertIf_DisputeDoesNotExist( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + bytes32 disputeID = bytes32("0x0"); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidDispute.selector, + disputeID + )); + disputeManager.rejectDispute(disputeID); + } + + function test_Dispute_Draw_RevertIf_DisputeDoesNotExist( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + bytes32 disputeID = bytes32("0x0"); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidDispute.selector, + disputeID + )); + disputeManager.drawDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/draw.t.sol b/packages/subgraph-service/test/disputeManager/disputes/draw.t.sol deleted file mode 100644 index 75dabaa2f..000000000 --- a/packages/subgraph-service/test/disputeManager/disputes/draw.t.sol +++ /dev/null @@ -1,67 +0,0 @@ -// 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 - ) public useIndexer useAllocation(tokens) { - uint256 fishermanPreviousBalance = token.balanceOf(users.fisherman); - bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI32")); - - 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 - ) public useIndexer useAllocation(tokens) { - bytes32 disputeID = _createIndexingDispute(allocationID,bytes32("POI1")); - - // 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/disputeManager/disputes/indexing/accept.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/accept.t.sol new file mode 100644 index 000000000..5e0d36132 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/accept.t.sol @@ -0,0 +1,64 @@ +// 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 DisputeManagerIndexingAcceptDisputeTest is DisputeManagerTest { + using PPMMath for uint256; + + /* + * TESTS + */ + + function test_Indexing_Accept_Dispute( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + resetPrank(users.arbitrator); + _acceptDispute(disputeID, tokensSlash); + } + + function test_Indexing_Accept_RevertIf_CallerIsNotArbitrator( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + // attempt to accept dispute as fisherman + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Indexing_Accept_RevertWhen_SlashingOverMaxSlashPercentage( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + tokensSlash = bound(tokensSlash, uint256(maxSlashingPercentage).mulPPM(tokens) + 1, type(uint256).max); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI101")); + + // max slashing percentage is 50% + resetPrank(users.arbitrator); + uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(tokens); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidTokensSlash.selector, + tokensSlash, + maxTokensToSlash + ); + vm.expectRevert(expectedError); + disputeManager.acceptDispute(disputeID, tokensSlash); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/cancel.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/cancel.t.sol new file mode 100644 index 000000000..0a09ad7f4 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/cancel.t.sol @@ -0,0 +1,42 @@ +// 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 DisputeManagerIndexingCancelDisputeTest is DisputeManagerTest { + + /* + * TESTS + */ + + function test_Indexing_Cancel_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1")); + + _cancelDispute(disputeID); + } + + function test_Indexing_Cancel_RevertIf_CallerIsNotFisherman( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1")); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotFisherman.selector)); + disputeManager.cancelDispute(disputeID); + } + + function test_Indexing_Cancel_RevertIf_DisputePeriodNotOver( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1")); + + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputePeriodNotFinished.selector)); + disputeManager.cancelDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/create.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/create.t.sol new file mode 100644 index 000000000..0174315a2 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/create.t.sol @@ -0,0 +1,111 @@ +// 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 DisputeManagerIndexingCreateDisputeTest is DisputeManagerTest { + + /* + * TESTS + */ + + function test_Indexing_Create_Dispute( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + _createIndexingDispute(allocationID, bytes32("POI1")); + } + + function test_Indexing_Create_MultipleDisputes() public { + uint256 tokens = 10000 ether; + uint8 numIndexers = 10; + uint256[] memory allocationIDPrivateKeys = new uint256[](numIndexers); + for (uint i = 0; i < numIndexers; i++) { + string memory indexerName = string(abi.encodePacked("Indexer ", i)); + address indexer = createUser(indexerName); + vm.assume(indexer != address(0)); + + resetPrank(indexer); + mint(indexer, tokens); + _createProvision(indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(indexer, abi.encode("url", "geoHash", address(0))); + uint256 allocationIDPrivateKey = uint256(keccak256(abi.encodePacked(i))); + bytes memory data = _createSubgraphAllocationData(indexer, subgraphDeployment, allocationIDPrivateKey, tokens); + _startService(indexer, data); + allocationIDPrivateKeys[i] = allocationIDPrivateKey; + } + + resetPrank(users.fisherman); + for (uint i = 0; i < allocationIDPrivateKeys.length; i++) { + _createIndexingDispute(vm.addr(allocationIDPrivateKeys[i]), bytes32("POI1")); + } + } + + function test_Indexing_Create_RevertWhen_DisputeAlreadyCreated( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1")); + + // Create another dispute with different fisherman + address otherFisherman = makeAddr("otherFisherman"); + resetPrank(otherFisherman); + mint(otherFisherman, disputeDeposit); + token.approve(address(disputeManager), disputeDeposit); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerDisputeAlreadyCreated.selector, + disputeID + ); + vm.expectRevert(expectedError); + disputeManager.createIndexingDispute(allocationID, bytes32("POI1")); + vm.stopPrank(); + } + + function test_Indexing_Create_RevertIf_DepositUnderMinimum(uint256 tokensDeposit) public useFisherman { + tokensDeposit = bound(tokensDeposit, 0, disputeDeposit - 1); + token.approve(address(disputeManager), tokensDeposit); + bytes memory expectedError = abi.encodeWithSignature( + "ERC20InsufficientAllowance(address,uint256,uint256)", + address(disputeManager), + tokensDeposit, + disputeDeposit + ); + vm.expectRevert(expectedError); + disputeManager.createIndexingDispute(allocationID, bytes32("POI3")); + vm.stopPrank(); + } + + function test_Indexing_Create_RevertIf_AllocationDoesNotExist( + uint256 tokens + ) public useFisherman { + tokens = bound(tokens, disputeDeposit, 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")); + vm.stopPrank(); + } + + function test_Indexing_Create_RevertIf_IndexerIsBelowStake( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Close allocation + bytes memory data = abi.encode(allocationID); + _stopService(users.indexer, data); + // Thaw, deprovision and unstake + address subgraphDataServiceAddress = address(subgraphService); + _thawDeprovisionAndUnstake(users.indexer, subgraphDataServiceAddress, tokens); + + // Attempt to create dispute + resetPrank(users.fisherman); + token.approve(address(disputeManager), tokens); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerZeroTokens.selector)); + disputeManager.createIndexingDispute(allocationID, bytes32("POI1")); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/draw.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/draw.t.sol new file mode 100644 index 000000000..3df590f74 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/draw.t.sol @@ -0,0 +1,34 @@ +// 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 DisputeManagerIndexingDrawDisputeTest is DisputeManagerTest { + + /* + * TESTS + */ + + function test_Indexing_Draw_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI32")); + + resetPrank(users.arbitrator); + _drawDispute(disputeID); + } + + function test_Indexing_Draw_RevertIf_CallerIsNotArbitrator( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID,bytes32("POI1")); + + // attempt to draw dispute as fisherman + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.drawDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/reject.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/reject.t.sol new file mode 100644 index 000000000..5401c0afe --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/reject.t.sol @@ -0,0 +1,34 @@ +// 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 DisputeManagerIndexingRejectDisputeTest is DisputeManagerTest { + + /* + * TESTS + */ + + function test_Indexing_Reject_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); + + resetPrank(users.arbitrator); + _rejectDispute(disputeID); + } + + function test_Indexing_Reject_RevertIf_CallerIsNotArbitrator( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1")); + + // attempt to accept dispute as fisherman + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.rejectDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol new file mode 100644 index 000000000..d0f00234d --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryAcceptDisputeTest is DisputeManagerTest { + using PPMMath for uint256; + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + /* + * TESTS + */ + + function test_Query_Accept_Dispute( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + _acceptDispute(disputeID, tokensSlash); + } + + function test_Query_Accept_RevertIf_CallerIsNotArbitrator( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + // attempt to accept dispute as fisherman + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Query_Accept_RevertWhen_SlashingOverMaxSlashPercentage( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, uint256(maxSlashingPercentage).mulPPM(tokens) + 1, type(uint256).max); + + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + // max slashing percentage is 50% + resetPrank(users.arbitrator); + uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(tokens); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidTokensSlash.selector, + tokensSlash, + maxTokensToSlash + ); + vm.expectRevert(expectedError); + disputeManager.acceptDispute(disputeID, tokensSlash); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/cancel.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/cancel.t.sol new file mode 100644 index 000000000..a61db6ad9 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/query/cancel.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryCancelDisputeTest is DisputeManagerTest { + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + /* + * TESTS + */ + + function test_Query_Cancel_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + _cancelDispute(disputeID); + } + + function test_Query_Cancel_RevertIf_CallerIsNotFisherman( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotFisherman.selector)); + disputeManager.cancelDispute(disputeID); + } + + function test_Query_Cancel_RevertIf_DisputePeriodNotOver( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputePeriodNotFinished.selector)); + disputeManager.cancelDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/create.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/create.t.sol new file mode 100644 index 000000000..01f27cf66 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/query/create.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryCreateDisputeTest is DisputeManagerTest { + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + /* + * TESTS + */ + + function test_Query_Create_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + _createQueryDispute(attestationData); + } + + function test_Query_Create_MultipleDisputes_DifferentFisherman( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + _createQueryDispute(attestationData); + + // Create another dispute with different fisherman + address otherFisherman = makeAddr("otherFisherman"); + resetPrank(otherFisherman); + mint(otherFisherman, MAX_TOKENS); + Attestation.Receipt memory otherFishermanReceipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory otherFishermanAttestationData = _createAtestationData(otherFishermanReceipt, allocationIDPrivateKey); + _createQueryDispute(otherFishermanAttestationData); + } + + function test_Query_Create_MultipleDisputes_DifferentIndexer( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Create first dispute for indexer + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + _createQueryDispute(attestationData); + + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + uint256 newAllocationIDKey = uint256(keccak256(abi.encodePacked("newAllocationID"))); + mint(newIndexer, tokens); + resetPrank(newIndexer); + _createProvision(newIndexer, tokens, maxSlashingPercentage, disputePeriod); + _register(newIndexer, abi.encode("url", "geoHash", 0x0)); + bytes memory data = _createSubgraphAllocationData(newIndexer, subgraphDeployment, newAllocationIDKey, tokens); + _startService(newIndexer, data); + + // Create another dispute with same receipt but different indexer + resetPrank(users.fisherman); + bytes memory attestationData2 = _createAtestationData(receipt, newAllocationIDKey); + _createQueryDispute(attestationData2); + } + + function test_Query_Create_RevertIf_Duplicate(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + Attestation.Receipt memory newReceipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory newAttestationData = _createAtestationData(newReceipt, allocationIDPrivateKey); + token.approve(address(disputeManager), disputeDeposit); + vm.expectRevert(abi.encodeWithSelector( + IDisputeManager.DisputeManagerDisputeAlreadyCreated.selector, + disputeID + )); + disputeManager.createQueryDispute(newAttestationData); + } + + function test_Query_Create_RevertIf_DepositUnderMinimum(uint256 tokensDispute) public useFisherman { + tokensDispute = bound(tokensDispute, 0, disputeDeposit - 1); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + + token.approve(address(disputeManager), tokensDispute); + bytes memory expectedError = abi.encodeWithSignature( + "ERC20InsufficientAllowance(address,uint256,uint256)", + address(disputeManager), + tokensDispute, + disputeDeposit + ); + vm.expectRevert(expectedError); + disputeManager.createQueryDispute(attestationData); + } + + function test_Query_Create_RevertIf_AllocationDoesNotExist( + uint256 tokens + ) public useFisherman { + tokens = bound(tokens, disputeDeposit, 10_000_000_000 ether); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + token.approve(address(disputeManager), tokens); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerIndexerNotFound.selector, + allocationID + ); + vm.expectRevert(expectedError); + disputeManager.createQueryDispute(attestationData); + vm.stopPrank(); + } + + function test_Query_Create_RevertIf_IndexerIsBelowStake( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Close allocation + bytes memory data = abi.encode(allocationID); + _stopService(users.indexer, data); + + // Thaw, deprovision and unstake + address subgraphDataServiceAddress = address(subgraphService); + _thawDeprovisionAndUnstake(users.indexer, subgraphDataServiceAddress, tokens); + + // Atempt to create dispute + resetPrank(users.fisherman); + token.approve(address(disputeManager), tokens); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerZeroTokens.selector)); + disputeManager.createQueryDispute(attestationData); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/draw.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/draw.t.sol new file mode 100644 index 000000000..1b9c96ad2 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/query/draw.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryDrawDisputeTest is DisputeManagerTest { + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + /* + * TESTS + */ + + function test_Query_Draw_Dispute( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + _drawDispute(disputeID); + } + + function test_Query_Draw_RevertIf_CallerIsNotArbitrator( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + // attempt to draw dispute as fisherman + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.drawDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/reject.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/reject.t.sol new file mode 100644 index 000000000..07c738a90 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/query/reject.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryRejectDisputeTest is DisputeManagerTest { + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); + bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); + + /* + * TESTS + */ + + function test_Query_Reject_Dispute( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + resetPrank(users.arbitrator); + _rejectDispute(disputeID); + } + + function test_Query_Reject_RevertIf_CallerIsNotArbitrator( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + bytes32 disputeID = _createQueryDispute(attestationData); + + // attempt to accept dispute as fisherman + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.rejectDispute(disputeID); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/accept.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/accept.t.sol new file mode 100644 index 000000000..1cfde0a8c --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/accept.t.sol @@ -0,0 +1,96 @@ +// 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 DisputeManagerQueryConflictAcceptDisputeTest is DisputeManagerTest { + using PPMMath for uint256; + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 private responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + + /* + * TESTS + */ + + function test_Query_Conflict_Accept_Dispute( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + _acceptDispute(disputeID1, tokensSlash); + } + + function test_Query_Conflict_Accept_RevertIf_CallerIsNotArbitrator( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + + // attempt to accept dispute as fisherman + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.acceptDispute(disputeID1, tokensSlash); + } + + function test_Query_Conflict_Accept_RevertWhen_SlashingOverMaxSlashPercentage( + uint256 tokens, + uint256 tokensSlash + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, uint256(maxSlashingPercentage).mulPPM(tokens) + 1, type(uint256).max); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + + // max slashing percentage is 50% + resetPrank(users.arbitrator); + uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(tokens); + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerInvalidTokensSlash.selector, + tokensSlash, + maxTokensToSlash + ); + vm.expectRevert(expectedError); + disputeManager.acceptDispute(disputeID1, tokensSlash); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/cancel.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/cancel.t.sol new file mode 100644 index 000000000..4de2b3155 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/cancel.t.sol @@ -0,0 +1,76 @@ +// 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 DisputeManagerQueryConflictCancelDisputeTest is DisputeManagerTest { + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 private responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + + /* + * TESTS + */ + + function test_Query_Conflict_Cancel_Dispute( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + + _cancelDispute(disputeID1); + } + + function test_Query_Conflict_Cancel_RevertIf_CallerIsNotFisherman( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.indexer); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotFisherman.selector)); + disputeManager.cancelDispute(disputeID1); + } + + function test_Query_Conflict_Cancel_RevertIf_DisputePeriodNotOver( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputePeriodNotFinished.selector)); + disputeManager.cancelDispute(disputeID1); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/create.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/create.t.sol new file mode 100644 index 000000000..2f54983ae --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/create.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManager.sol"; +import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; +import { DisputeManagerTest } from "../../DisputeManager.t.sol"; + +contract DisputeManagerQueryConflictCreateDisputeTest is DisputeManagerTest { + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 private responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + + /* + * TESTS + */ + + function test_Query_Conflict_Create_DisputeAttestation( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + _createQueryDisputeConflict(attestationData1, attestationData2); + } + + function test_Query_Conflict_Create_DisputeAttestationDifferentIndexers( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + uint256 newAllocationIDKey = uint256(keccak256(abi.encodePacked("newAllocationID"))); + mint(newIndexer, tokens); + resetPrank(newIndexer); + _createProvision(newIndexer, tokens, maxSlashingPercentage, disputePeriod); + _register(newIndexer, abi.encode("url", "geoHash", 0x0)); + bytes memory data = _createSubgraphAllocationData(newIndexer, subgraphDeployment, newAllocationIDKey, tokens); + _startService(newIndexer, data); + + // Create query conflict dispute + resetPrank(users.fisherman); + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + newAllocationIDKey + ); + + _createQueryDisputeConflict(attestationData1, attestationData2); + } + + function test_Query_Conflict_Create_RevertIf_AttestationsResponsesAreTheSame() public useFisherman { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID1, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerNonConflictingAttestations.selector, + requestCID, + responseCID1, + subgraphDeployment, + requestCID, + responseCID1, + subgraphDeployment + ); + vm.expectRevert(expectedError); + disputeManager.createQueryDisputeConflict(attestationData1, attestationData2); + } + + function test_Query_Conflict_Create_RevertIf_AttestationsHaveDifferentSubgraph() public useFisherman { + bytes32 subgraphDeploymentId2 = keccak256(abi.encodePacked("Subgraph Deployment ID 2")); + + Attestation.Receipt memory receipt1 = _createAttestationReceipt(requestCID, responseCID1, subgraphDeployment); + bytes memory attestationData1 = _createAtestationData(receipt1, allocationIDPrivateKey); + + Attestation.Receipt memory receipt2 = _createAttestationReceipt(requestCID, responseCID2, subgraphDeploymentId2); + bytes memory attestationData2 = _createAtestationData(receipt2, allocationIDPrivateKey); + + bytes memory expectedError = abi.encodeWithSelector( + IDisputeManager.DisputeManagerNonConflictingAttestations.selector, + requestCID, + responseCID1, + subgraphDeployment, + requestCID, + responseCID2, + subgraphDeploymentId2 + ); + vm.expectRevert(expectedError); + disputeManager.createQueryDisputeConflict(attestationData1, attestationData2); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/draw.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/draw.t.sol new file mode 100644 index 000000000..5127b56a5 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/draw.t.sol @@ -0,0 +1,59 @@ +// 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 DisputeManagerQueryConflictDrawDisputeTest is DisputeManagerTest { + + bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 private responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 private responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + + /* + * TESTS + */ + + function test_Query_Conflict_Draw_Dispute( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + _drawDispute(disputeID1); + } + + function test_Query_Conflict_Draw_RevertIf_CallerIsNotArbitrator( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + + // attempt to draw dispute as fisherman + resetPrank(users.fisherman); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); + disputeManager.drawDispute(disputeID1); + } +} diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/reject.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/reject.t.sol new file mode 100644 index 000000000..078969267 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/reject.t.sol @@ -0,0 +1,42 @@ +// 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 DisputeManagerQueryConflictRejectDisputeTest is DisputeManagerTest { + + /* + * TESTS + */ + + function test_Query_Conflict_Reject_Revert( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + bytes32 requestCID = keccak256(abi.encodePacked("Request CID")); + bytes32 responseCID1 = keccak256(abi.encodePacked("Response CID 1")); + bytes32 responseCID2 = keccak256(abi.encodePacked("Response CID 2")); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, bytes32 disputeID2) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + vm.expectRevert(abi.encodeWithSelector( + IDisputeManager.DisputeManagerMustAcceptRelatedDispute.selector, + disputeID1, + disputeID2 + )); + disputeManager.rejectDispute(disputeID1); + } +} diff --git a/packages/subgraph-service/test/disputeManager/governance/arbitrator.t.sol b/packages/subgraph-service/test/disputeManager/governance/arbitrator.t.sol new file mode 100644 index 000000000..53e36a497 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/governance/arbitrator.t.sol @@ -0,0 +1,32 @@ +// 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"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract DisputeManagerGovernanceArbitratorTest is DisputeManagerTest { + + /* + * TESTS + */ + + function test_Governance_SetArbitrator() public useGovernor { + address arbitrator = makeAddr("newArbitrator"); + _setArbitrator(arbitrator); + } + + function test_Governance_RevertWhen_ZeroAddress() public useGovernor { + address arbitrator = address(0); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidZeroAddress.selector)); + disputeManager.setArbitrator(arbitrator); + } + + function test_Governance_RevertWhen_NotGovernor() public useFisherman { + address arbitrator = makeAddr("newArbitrator"); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.fisherman)); + disputeManager.setArbitrator(arbitrator); + } +} diff --git a/packages/subgraph-service/test/disputeManager/governance/disputeDeposit.t.sol b/packages/subgraph-service/test/disputeManager/governance/disputeDeposit.t.sol new file mode 100644 index 000000000..163157886 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/governance/disputeDeposit.t.sol @@ -0,0 +1,32 @@ +// 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"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract DisputeManagerGovernanceDisputeDepositTest is DisputeManagerTest { + + /* + * TESTS + */ + + function test_Governance_SetDisputeDeposit(uint256 disputeDeposit) public useGovernor { + vm.assume(disputeDeposit > 0); + _setDisputeDeposit(disputeDeposit); + } + + function test_Governance_RevertWhen_ZeroValue() public useGovernor { + uint256 disputeDeposit = 0; + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidDisputeDeposit.selector, disputeDeposit)); + disputeManager.setDisputeDeposit(disputeDeposit); + } + + function test_Governance_RevertWhen_NotGovernor() public useFisherman { + uint256 disputeDeposit = 100 ether; + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.fisherman)); + disputeManager.setDisputeDeposit(disputeDeposit); + } +} diff --git a/packages/subgraph-service/test/disputeManager/governance/fishermanRewardCut.t.sol b/packages/subgraph-service/test/disputeManager/governance/fishermanRewardCut.t.sol index 0c821e2a2..5a09bdd03 100644 --- a/packages/subgraph-service/test/disputeManager/governance/fishermanRewardCut.t.sol +++ b/packages/subgraph-service/test/disputeManager/governance/fishermanRewardCut.t.sol @@ -13,10 +13,9 @@ contract DisputeManagerGovernanceFishermanRewardCutTest is DisputeManagerTest { * TESTS */ - function test_Governance_SetFishermanRewardCut() public useGovernor { - uint32 fishermanRewardCut = 1000; - disputeManager.setFishermanRewardCut(fishermanRewardCut); - assertEq(disputeManager.fishermanRewardCut(), fishermanRewardCut, "Fisherman reward cut should be set."); + function test_Governance_SetFishermanRewardCut(uint32 fishermanRewardCut) public useGovernor { + vm.assume(fishermanRewardCut <= disputeManager.MAX_FISHERMAN_REWARD_CUT()); + _setFishermanRewardCut(fishermanRewardCut); } function test_Governance_RevertWhen_OverMaximumValue(uint32 fishermanRewardCut) public useGovernor { diff --git a/packages/subgraph-service/test/disputeManager/governance/maxSlashingCut.t.sol b/packages/subgraph-service/test/disputeManager/governance/maxSlashingCut.t.sol new file mode 100644 index 000000000..343e514bc --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/governance/maxSlashingCut.t.sol @@ -0,0 +1,32 @@ +// 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"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract DisputeManagerGovernanceMaxSlashingCutTest is DisputeManagerTest { + + /* + * TESTS + */ + + function test_Governance_SetMaxSlashingCut(uint32 maxSlashingCut) public useGovernor { + vm.assume(maxSlashingCut <= MAX_PPM); + _setMaxSlashingCut(maxSlashingCut); + } + + function test_Governance_RevertWhen_NotPPM(uint32 maxSlashingCut) public useGovernor { + vm.assume(maxSlashingCut > MAX_PPM); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidMaxSlashingCut.selector, maxSlashingCut)); + disputeManager.setMaxSlashingCut(maxSlashingCut); + } + + function test_Governance_RevertWhen_NotGovernor() public useFisherman { + uint32 maxSlashingCut = 1000; + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.fisherman)); + disputeManager.setMaxSlashingCut(maxSlashingCut); + } +} diff --git a/packages/subgraph-service/test/shared/HorizonStakingShared.t.sol b/packages/subgraph-service/test/shared/HorizonStakingShared.t.sol new file mode 100644 index 000000000..28a22e1a2 --- /dev/null +++ b/packages/subgraph-service/test/shared/HorizonStakingShared.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; +import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; + +import { SubgraphBaseTest } from "../SubgraphBaseTest.t.sol"; + +abstract contract HorizonStakingSharedTest is SubgraphBaseTest { + + /* + * HELPERS + */ + + function _createProvision( + address _indexer, + uint256 _tokens, + uint32 _maxSlashingPercentage, + uint64 _disputePeriod + ) internal { + _stakeTo(_indexer, _tokens); + staking.provision(_indexer, address(subgraphService), _tokens, _maxSlashingPercentage, _disputePeriod); + } + + function _addToProvision(address _indexer, uint256 _tokens) internal { + _stakeTo(_indexer, _tokens); + staking.addToProvision(_indexer, address(subgraphService), _tokens); + } + + function _delegate(address _indexer, address _verifier, uint256 _tokens, uint256 _minSharesOut) internal { + staking.delegate(_indexer, _verifier, _tokens, _minSharesOut); + } + + function _setDelegationFeeCut( + address _indexer, + address _verifier, + IGraphPayments.PaymentTypes _paymentType, + uint256 _cut + ) internal { + staking.setDelegationFeeCut(_indexer, _verifier, _paymentType, _cut); + } + + function _thawDeprovisionAndUnstake(address _indexer, address _verifier, uint256 _tokens) internal { + // Initiate thaw request + staking.thaw(_indexer, _verifier, _tokens); + + // Skip thawing period + IHorizonStakingTypes.Provision memory provision = staking.getProvision(_indexer, _verifier); + skip(provision.thawingPeriod + 1); + + // Deprovision and unstake + staking.deprovision(_indexer, _verifier, 0); + staking.unstake(_tokens); + } + + function _setProvisionParameters( + address _indexer, + address _verifier, + uint32 _maxVerifierCut, + uint64 _thawingPeriod + ) internal { + staking.setProvisionParameters(_indexer, _verifier, _maxVerifierCut, _thawingPeriod); + } + + /* + * PRIVATE + */ + + function _stakeTo(address _indexer, uint256 _tokens) private { + token.approve(address(staking), _tokens); + staking.stakeTo(_indexer, _tokens); + } +} diff --git a/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol b/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol index 8c0b9d0fe..1067acf14 100644 --- a/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol +++ b/packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol @@ -3,9 +3,14 @@ pragma solidity 0.8.26; import "forge-std/Test.sol"; -import { SubgraphBaseTest } from "../SubgraphBaseTest.t.sol"; +import { Allocation } from "../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../contracts/utilities/AllocationManager.sol"; +import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol"; +import { ISubgraphService } from "../../contracts/interfaces/ISubgraphService.sol"; -abstract contract SubgraphServiceSharedTest is SubgraphBaseTest { +import { HorizonStakingSharedTest } from "./HorizonStakingShared.t.sol"; + +abstract contract SubgraphServiceSharedTest is HorizonStakingSharedTest { /* * VARIABLES @@ -28,9 +33,22 @@ abstract contract SubgraphServiceSharedTest is SubgraphBaseTest { modifier useAllocation(uint256 tokens) { vm.assume(tokens > minimumProvisionTokens); vm.assume(tokens < 10_000_000_000 ether); - _createProvision(tokens); - _registerIndexer(address(0)); - _startService(tokens); + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + bytes memory data = _createSubgraphAllocationData(users.indexer, subgraphDeployment, allocationIDPrivateKey, tokens); + _startService(users.indexer, data); + _; + } + + modifier useDelegation(uint256 tokens) { + // 1e18 is the minimum delegation amount until L2 transfers are removed + vm.assume(tokens > 1e18); + vm.assume(tokens < 10_000_000_000 ether); + (, address msgSender,) = vm.readCallers(); + resetPrank(users.delegator); + token.approve(address(staking), tokens); + _delegate(users.indexer, address(subgraphService), tokens, 0); + resetPrank(msgSender); _; } @@ -45,29 +63,109 @@ abstract contract SubgraphServiceSharedTest is SubgraphBaseTest { } /* - * HELPERS + * ACTIONS */ - function _createProvision(uint256 tokens) internal { - _stakeTo(users.indexer, tokens); - staking.provision(users.indexer, address(subgraphService), tokens, maxSlashingPercentage, disputePeriod); + function _register(address _indexer, bytes memory _data) internal { + (string memory url, string memory geohash, address rewardsDestination) = abi.decode( + _data, + (string, string, address) + ); + + vm.expectEmit(address(subgraphService)); + emit IDataService.ServiceProviderRegistered( + _indexer, + _data + ); + + // Register indexer + subgraphService.register(_indexer, _data); + + // Check registered indexer data + ISubgraphService.Indexer memory indexer = _getIndexer(_indexer); + assertEq(indexer.registeredAt, block.timestamp); + assertEq(indexer.url, url); + assertEq(indexer.geoHash, geohash); + + // Check rewards destination + assertEq(subgraphService.rewardsDestination(_indexer), rewardsDestination); } - function _addToProvision(address _indexer, uint256 _tokens) internal { - _stakeTo(_indexer, _tokens); - staking.addToProvision(_indexer, address(subgraphService), _tokens); + function _startService(address _indexer, bytes memory _data) internal { + (bytes32 subgraphDeploymentId, uint256 tokens, address allocationId,) = abi.decode( + _data, + (bytes32, uint256, address, bytes) + ); + uint256 previousSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeploymentId); + + vm.expectEmit(address(subgraphService)); + emit IDataService.ServiceStarted(_indexer, _data); + + // TODO: improve this + uint256 accRewardsPerAllocatedToken = 0; + if (rewardsManager.subgraphs(subgraphDeploymentId)) { + accRewardsPerAllocatedToken = rewardsManager.rewardsPerSubgraphAllocationUpdate(); + } + + // Start service + subgraphService.startService(_indexer, _data); + + // Check allocation data + Allocation.State memory allocation = subgraphService.getAllocation(allocationId); + assertEq(allocation.tokens, tokens); + assertEq(allocation.indexer, _indexer); + assertEq(allocation.subgraphDeploymentId, subgraphDeploymentId); + assertEq(allocation.createdAt, block.timestamp); + assertEq(allocation.closedAt, 0); + assertEq(allocation.lastPOIPresentedAt, 0); + assertEq(allocation.accRewardsPerAllocatedToken, accRewardsPerAllocatedToken); + assertEq(allocation.accRewardsPending, 0); + + // Check subgraph deployment allocated tokens + uint256 subgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeploymentId); + assertEq(subgraphAllocatedTokens, previousSubgraphAllocatedTokens + tokens); } - function _registerIndexer(address rewardsDestination) internal { - subgraphService.register(users.indexer, abi.encode("url", "geoHash", rewardsDestination)); + function _stopService(address _indexer, bytes memory _data) internal { + address allocationId = abi.decode(_data, (address)); + assertTrue(subgraphService.isActiveAllocation(allocationId)); + + Allocation.State memory allocation = subgraphService.getAllocation(allocationId); + uint256 previousSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(allocation.subgraphDeploymentId); + + vm.expectEmit(address(subgraphService)); + emit AllocationManager.AllocationClosed(_indexer, allocationId, allocation.subgraphDeploymentId, allocation.tokens); + emit IDataService.ServiceStopped(_indexer, _data); + + // stop allocation + subgraphService.stopService(_indexer, _data); + + // update allocation + allocation = subgraphService.getAllocation(allocationId); + + // check allocation + assertEq(allocation.closedAt, block.timestamp); + + // check subgraph deployment allocated tokens + uint256 subgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeployment); + assertEq(subgraphAllocatedTokens, previousSubgraphAllocatedTokens - allocation.tokens); } - function _startService(uint256 tokens) internal { - bytes32 digest = subgraphService.encodeAllocationProof(users.indexer, allocationID); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(allocationIDPrivateKey, digest); + /* + * HELPERS + */ - bytes memory data = abi.encode(subgraphDeployment, tokens, allocationID, abi.encodePacked(r, s, v)); - subgraphService.startService(users.indexer, data); + function _createSubgraphAllocationData( + address _indexer, + bytes32 _subgraphDeployment, + uint256 _allocationIdPrivateKey, + uint256 _tokens + ) internal view returns (bytes memory) { + address allocationId = vm.addr(_allocationIdPrivateKey); + bytes32 digest = subgraphService.encodeAllocationProof(_indexer, allocationId); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_allocationIdPrivateKey, digest); + + return abi.encode(_subgraphDeployment, _tokens, allocationId, abi.encodePacked(r, s, v)); } function _delegate(uint256 tokens) internal { @@ -76,11 +174,15 @@ abstract contract SubgraphServiceSharedTest is SubgraphBaseTest { } /* - * PRIVATE + * PRIVATE FUNCTIONS */ - function _stakeTo(address _indexer, uint256 _tokens) internal { - token.approve(address(staking), _tokens); - staking.stakeTo(_indexer, _tokens); + function _getIndexer(address _indexer) private view returns (ISubgraphService.Indexer memory) { + (uint256 registeredAt, string memory url, string memory geoHash) = subgraphService.indexers(_indexer); + return ISubgraphService.Indexer({ + registeredAt: registeredAt, + url: url, + geoHash: geoHash + }); } } diff --git a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol index 89b613344..12d20581f 100644 --- a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol +++ b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol @@ -6,11 +6,21 @@ 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 { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; +import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { ITAPCollector } from "@graphprotocol/horizon/contracts/interfaces/ITAPCollector.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { LinkedList } from "@graphprotocol/horizon/contracts/libraries/LinkedList.sol"; +import { IDataServiceFees } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataServiceFees.sol"; +import { Allocation } from "../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../contracts/utilities/AllocationManager.sol"; +import { ISubgraphService } from "../../contracts/interfaces/ISubgraphService.sol"; import { SubgraphServiceSharedTest } from "../shared/SubgraphServiceShared.t.sol"; contract SubgraphServiceTest is SubgraphServiceSharedTest { using PPMMath for uint256; + using Allocation for Allocation.State; + using LinkedList for LinkedList.List; /* * VARIABLES @@ -21,11 +31,18 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { */ modifier useOperator { - vm.startPrank(users.operator); + resetPrank(users.indexer); + staking.setOperator(users.operator, address(subgraphService), true); + resetPrank(users.operator); _; vm.stopPrank(); } + modifier useRewardsDestination { + _setRewardsDestination(users.rewardsDestination); + _; + } + /* * SET UP */ @@ -34,6 +51,285 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { super.setUp(); } + /* + * ACTIONS + */ + + function _setRewardsDestination(address _rewardsDestination) internal { + (, address indexer, ) = vm.readCallers(); + + vm.expectEmit(address(subgraphService)); + emit AllocationManager.RewardsDestinationSet(indexer, _rewardsDestination); + + // Set rewards destination + subgraphService.setRewardsDestination(_rewardsDestination); + + // Check rewards destination + assertEq(subgraphService.rewardsDestination(indexer), _rewardsDestination); + } + + function _acceptProvision(address _indexer, bytes memory _data) internal { + IHorizonStakingTypes.Provision memory provision = staking.getProvision(_indexer, address(subgraphService)); + uint32 maxVerifierCut = provision.maxVerifierCut; + uint64 thawingPeriod = provision.thawingPeriod; + uint32 maxVerifierCutPending = provision.maxVerifierCutPending; + uint64 thawingPeriodPending = provision.thawingPeriodPending; + + vm.expectEmit(address(subgraphService)); + emit IDataService.ProvisionAccepted(_indexer); + + // Accept provision + subgraphService.acceptProvision(_indexer, _data); + + // Update provision after acceptance + provision = staking.getProvision(_indexer, address(subgraphService)); + + // Check that max verifier cut updated to pending value if needed + if (maxVerifierCut != maxVerifierCutPending) { + assertEq(provision.maxVerifierCut, maxVerifierCutPending); + } + + // Check that thawing period updated to pending value if needed + if (thawingPeriod != thawingPeriodPending) { + assertEq(provision.thawingPeriod, thawingPeriodPending); + } + } + + function _resizeAllocation(address _indexer, address _allocationId, uint256 _tokens) internal { + // before state + Allocation.State memory beforeAllocation = subgraphService.getAllocation(_allocationId); + bytes32 subgraphDeploymentId = beforeAllocation.subgraphDeploymentId; + uint256 beforeSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeploymentId); + uint256 beforeAllocatedTokens = subgraphService.allocationProvisionTracker(_indexer); + + uint256 allocatedTokensDelta; + if (_tokens > beforeAllocation.tokens) { + allocatedTokensDelta = _tokens - beforeAllocation.tokens; + } else { + allocatedTokensDelta = beforeAllocation.tokens - _tokens; + } + + vm.expectEmit(address(subgraphService)); + emit AllocationManager.AllocationResized(_indexer, _allocationId, subgraphDeploymentId, _tokens, beforeSubgraphAllocatedTokens); + + // resize allocation + subgraphService.resizeAllocation(_indexer, _allocationId, _tokens); + + // after state + uint256 afterSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeploymentId); + uint256 afterAllocatedTokens = subgraphService.allocationProvisionTracker(_indexer); + Allocation.State memory afterAllocation = subgraphService.getAllocation(_allocationId); + uint256 accRewardsPerAllocatedTokenDelta = afterAllocation.accRewardsPerAllocatedToken - beforeAllocation.accRewardsPerAllocatedToken; + uint256 afterAccRewardsPending = rewardsManager.calcRewards(beforeAllocation.tokens, accRewardsPerAllocatedTokenDelta); + + // check state + if (_tokens > beforeAllocation.tokens) { + assertEq(afterAllocatedTokens, beforeAllocatedTokens + allocatedTokensDelta); + } else { + assertEq(afterAllocatedTokens, beforeAllocatedTokens - allocatedTokensDelta); + } + assertEq(afterAllocation.tokens, _tokens); + assertEq(afterAllocation.accRewardsPerAllocatedToken, rewardsPerSubgraphAllocationUpdate); + assertEq(afterAllocation.accRewardsPending, afterAccRewardsPending); + assertEq(afterSubgraphAllocatedTokens, _tokens); + } + + function _closeStaleAllocation(address _allocationId) internal { + assertTrue(subgraphService.isActiveAllocation(_allocationId)); + + Allocation.State memory allocation = subgraphService.getAllocation(_allocationId); + uint256 previousSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(allocation.subgraphDeploymentId); + + vm.expectEmit(address(subgraphService)); + emit AllocationManager.AllocationClosed(allocation.indexer, _allocationId, allocation.subgraphDeploymentId, allocation.tokens); + + // close stale allocation + subgraphService.closeStaleAllocation(_allocationId); + + // update allocation + allocation = subgraphService.getAllocation(_allocationId); + + // check allocation + assertEq(allocation.closedAt, block.timestamp); + + // check subgraph deployment allocated tokens + uint256 subgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeployment); + assertEq(subgraphAllocatedTokens, previousSubgraphAllocatedTokens - allocation.tokens); + } + + struct IndexingRewardsData { + bytes32 poi; + uint256 tokensIndexerRewards; + uint256 tokensDelegationRewards; + } + + struct QueryFeeData { + uint256 curationCut; + uint256 protocolPaymentCut; + } + + struct CollectPaymentData { + uint256 rewardsDestinationBalance; + uint256 indexerProvisionBalance; + uint256 delegationPoolBalance; + uint256 indexerBalance; + uint256 curationBalance; + uint256 lockedTokens; + } + + function _collect(address _indexer, IGraphPayments.PaymentTypes _paymentType, bytes memory _data) internal { + address allocationId; + uint256 paymentCollected = 0; + Allocation.State memory allocation; + CollectPaymentData memory collectPaymentDataBefore; + + // PaymentType.IndexingRewards variables + IndexingRewardsData memory indexingRewardsData; + uint32 delegationRatio = subgraphService.delegationRatio(); + address rewardsDestination = subgraphService.rewardsDestination(_indexer); + collectPaymentDataBefore.rewardsDestinationBalance = token.balanceOf(rewardsDestination); + collectPaymentDataBefore.indexerProvisionBalance = staking.getProviderTokensAvailable(_indexer, address(subgraphService)); + collectPaymentDataBefore.delegationPoolBalance = staking.getDelegatedTokensAvailable(_indexer, address(subgraphService)); + + // PaymentType.QueryFee variables + QueryFeeData memory queryFeeData; + queryFeeData.protocolPaymentCut = graphPayments.PROTOCOL_PAYMENT_CUT(); + collectPaymentDataBefore.indexerBalance = token.balanceOf(_indexer); + collectPaymentDataBefore.curationBalance = token.balanceOf(address(curation)); + collectPaymentDataBefore.lockedTokens = subgraphService.feesProvisionTracker(_indexer); + + if (_paymentType == IGraphPayments.PaymentTypes.QueryFee) { + // Recover RAV data + ITAPCollector.SignedRAV memory signedRav = abi.decode(_data, (ITAPCollector.SignedRAV)); + allocationId = abi.decode(signedRav.rav.metadata, (address)); + allocation = subgraphService.getAllocation(allocationId); + address payer = _recoverRAVSigner(signedRav); + + // Total amount of tokens collected for indexer + uint256 tokensCollected = tapCollector.tokensCollected(address(subgraphService), _indexer, payer); + // Find out how much of the payment was collected via this RAV + paymentCollected = signedRav.rav.valueAggregate - tokensCollected; + + // Calculate curation cut + uint256 curationFeesCut = subgraphService.curationFeesCut(); + queryFeeData.curationCut = curation.isCurated(allocation.subgraphDeploymentId) ? curationFeesCut : 0; + uint256 tokensCurators = paymentCollected.mulPPM(queryFeeData.curationCut); + + vm.expectEmit(address(subgraphService)); + emit ISubgraphService.QueryFeesCollected(_indexer, paymentCollected, tokensCurators); + } else if (_paymentType == IGraphPayments.PaymentTypes.IndexingRewards) { + // Recover IndexingRewards data + (allocationId, indexingRewardsData.poi) = abi.decode(_data, (address, bytes32)); + allocation = subgraphService.getAllocation(allocationId); + + // Calculate accumulated tokens, this depends on the rewards manager which we have mocked + uint256 accRewardsPerTokens = allocation.tokens.mulPPM(rewardsManager.rewardsPerSignal()); + // Calculate the payment collected by the indexer for this transaction + paymentCollected = accRewardsPerTokens - allocation.accRewardsPerAllocatedToken; + + uint256 delegatorCut = staking.getDelegationFeeCut( + allocation.indexer, + address(subgraphService), + // TODO: this should be fixed in AllocationManager, it should be IndexingRewards instead + IGraphPayments.PaymentTypes.IndexingFee + ); + indexingRewardsData.tokensDelegationRewards = paymentCollected.mulPPM(delegatorCut); + indexingRewardsData.tokensIndexerRewards = paymentCollected - indexingRewardsData.tokensDelegationRewards; + + vm.expectEmit(address(subgraphService)); + emit AllocationManager.IndexingRewardsCollected( + allocation.indexer, + allocationId, + allocation.subgraphDeploymentId, + paymentCollected, + indexingRewardsData.tokensIndexerRewards, + indexingRewardsData.tokensDelegationRewards, + indexingRewardsData.poi + ); + } + + vm.expectEmit(address(subgraphService)); + emit IDataService.ServicePaymentCollected(_indexer, _paymentType, paymentCollected); + + // collect rewards + subgraphService.collect(_indexer, _paymentType, _data); + + // Collect payment data after + CollectPaymentData memory collectPaymentDataAfter; + collectPaymentDataAfter.rewardsDestinationBalance = token.balanceOf(rewardsDestination); + collectPaymentDataAfter.indexerProvisionBalance = staking.getProviderTokensAvailable(_indexer, address(subgraphService)); + collectPaymentDataAfter.delegationPoolBalance = staking.getDelegatedTokensAvailable(_indexer, address(subgraphService)); + collectPaymentDataAfter.indexerBalance = token.balanceOf(_indexer); + collectPaymentDataAfter.curationBalance = token.balanceOf(address(curation)); + collectPaymentDataAfter.lockedTokens = subgraphService.feesProvisionTracker(_indexer); + + if (_paymentType == IGraphPayments.PaymentTypes.QueryFee) { + // Check indexer got paid the correct amount + uint256 tokensProtocol = paymentCollected.mulPPM(protocolPaymentCut); + uint256 curationTokens = paymentCollected.mulPPM(queryFeeData.curationCut); + uint256 expectedIndexerTokensPayment = paymentCollected - tokensProtocol - curationTokens; + assertEq(collectPaymentDataAfter.indexerBalance, collectPaymentDataBefore.indexerBalance + expectedIndexerTokensPayment); + + // Check curation got paid the correct amount + assertEq(collectPaymentDataAfter.curationBalance, collectPaymentDataBefore.curationBalance + curationTokens); + + // Check locked tokens + uint256 stakeToFeesRatio = subgraphService.stakeToFeesRatio(); + uint256 tokensToLock = paymentCollected * stakeToFeesRatio; + assertEq(collectPaymentDataAfter.lockedTokens, collectPaymentDataBefore.lockedTokens + tokensToLock); + + // Check the stake claim + LinkedList.List memory claimsList = _getClaimList(_indexer); + bytes32 claimId = _buildStakeClaimId(_indexer, claimsList.nonce - 1); + IDataServiceFees.StakeClaim memory stakeClaim = _getStakeClaim(claimId); + uint64 disputePeriod = disputeManager.getDisputePeriod(); + assertEq(stakeClaim.tokens, tokensToLock); + assertEq(stakeClaim.createdAt, block.timestamp); + assertEq(stakeClaim.releaseAt, block.timestamp + disputePeriod); + assertEq(stakeClaim.nextClaim, bytes32(0)); + } else { + // Update allocation after collecting rewards + allocation = subgraphService.getAllocation(allocationId); + + // Check allocation state + assertEq(allocation.accRewardsPending, 0); + uint256 accRewardsPerAllocatedToken = rewardsManager.onSubgraphAllocationUpdate(allocation.subgraphDeploymentId); + assertEq(allocation.accRewardsPerAllocatedToken, accRewardsPerAllocatedToken); + assertEq(allocation.lastPOIPresentedAt, block.timestamp); + + // Check indexer got paid the correct amount + if (rewardsDestination == address(0)) { + // If rewards destination is address zero indexer should get paid to their provision balance + assertEq( + collectPaymentDataAfter.indexerProvisionBalance, + collectPaymentDataBefore.indexerProvisionBalance + indexingRewardsData.tokensIndexerRewards + ); + } else { + // If rewards destination is set indexer should get paid to the rewards destination address + assertEq( + collectPaymentDataAfter.rewardsDestinationBalance, + collectPaymentDataBefore.rewardsDestinationBalance + indexingRewardsData.tokensIndexerRewards + ); + } + + // Check delegation pool got paid the correct amount + assertEq( + collectPaymentDataAfter.delegationPoolBalance, + collectPaymentDataBefore.delegationPoolBalance + indexingRewardsData.tokensDelegationRewards + ); + + // If after collecting indexing rewards the indexer is over allocated the allcation should close + uint256 tokensAvailable = staking.getTokensAvailable(_indexer, address(subgraphService), delegationRatio); + if (allocation.tokens <= tokensAvailable) { + // Indexer isn't over allocated so allocation should still be open + assertTrue(allocation.isOpen()); + } else { + // Indexer is over allocated so allocation should be closed + assertFalse(allocation.isOpen()); + } + } + } + /* * HELPERS */ @@ -45,37 +341,36 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { token.approve(address(staking), _tokens); staking.stakeTo(_indexer, _tokens); staking.provision(_indexer, address(subgraphService), _tokens, maxSlashingPercentage, disputePeriod); - subgraphService.register(_indexer, abi.encode("url", "geoHash", address(0))); + _register(_indexer, abi.encode("url", "geoHash", address(0))); (address newIndexerAllocationId, uint256 newIndexerAllocationKey) = makeAddrAndKey("newIndexerAllocationId"); bytes32 digest = subgraphService.encodeAllocationProof(_indexer, newIndexerAllocationId); (uint8 v, bytes32 r, bytes32 s) = vm.sign(newIndexerAllocationKey, digest); bytes memory data = abi.encode(subgraphDeployment, _tokens, newIndexerAllocationId, abi.encodePacked(r, s, v)); - subgraphService.startService(_indexer, data); + _startService(_indexer, data); } - function _stopAllocation(address _indexer, address _allocationID) internal { - resetPrank(_indexer); - assertTrue(subgraphService.isActiveAllocation(_allocationID)); - bytes memory data = abi.encode(_allocationID); - vm.expectEmit(address(subgraphService)); - emit IDataService.ServiceStopped(_indexer, data); - subgraphService.stopService(_indexer, data); + /* + * PRIVATE FUNCTIONS + */ - uint256 subgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(subgraphDeployment); - assertEq(subgraphAllocatedTokens, 0); + function _recoverRAVSigner(ITAPCollector.SignedRAV memory _signedRAV) private view returns (address) { + bytes32 messageHash = tapCollector.encodeRAV(_signedRAV.rav); + return ECDSA.recover(messageHash, _signedRAV.signature); } - function _collectIndexingRewards(address _indexer, address _allocationID, uint256 _tokens) internal { - resetPrank(_indexer); - IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; - bytes memory data = abi.encode(_allocationID, bytes32("POI1")); + function _getClaimList(address _indexer) private view returns (LinkedList.List memory) { + (bytes32 head, bytes32 tail, uint256 nonce, uint256 count) = subgraphService.claimsLists(_indexer); + return LinkedList.List(head, tail, nonce, count); + } - uint256 indexerPreviousProvisionBalance = staking.getProviderTokensAvailable(_indexer, address(subgraphService)); - subgraphService.collect(_indexer, paymentType, data); + function _buildStakeClaimId(address _indexer, uint256 _nonce) private view returns (bytes32) { + return keccak256(abi.encodePacked(address(subgraphService), _indexer, _nonce)); + } - uint256 indexerProvisionBalance = staking.getProviderTokensAvailable(_indexer, address(subgraphService)); - assertEq(indexerProvisionBalance, indexerPreviousProvisionBalance + _tokens.mulPPM(rewardsPerSignal)); + function _getStakeClaim(bytes32 _claimId) private view returns (IDataServiceFees.StakeClaim memory) { + (uint256 tokens, uint256 createdAt, uint256 releaseAt, bytes32 nextClaim) = subgraphService.claims(_claimId); + return IDataServiceFees.StakeClaim(tokens, createdAt, releaseAt, nextClaim); } } diff --git a/packages/subgraph-service/test/subgraphService/allocate/resize.t.sol b/packages/subgraph-service/test/subgraphService/allocate/resize.t.sol deleted file mode 100644 index a2d8ef4a7..000000000 --- a/packages/subgraph-service/test/subgraphService/allocate/resize.t.sol +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import "forge-std/Test.sol"; - -import { Allocation } from "../../../contracts/libraries/Allocation.sol"; -import { AllocationManager } from "../../../contracts/utilities/AllocationManager.sol"; -import { SubgraphServiceTest } from "../SubgraphService.t.sol"; -import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; - -contract SubgraphServiceAllocateResizeTest is SubgraphServiceTest { - - /* - * Helpers - */ - - function _setupResize(address _indexer, uint256 _tokens) private { - token.approve(address(staking), _tokens); - staking.stakeTo(_indexer, _tokens); - staking.addToProvision(_indexer, address(subgraphService), _tokens); - } - - function _resizeAllocation(address _indexer, address _allocationID, bytes32 _subgraphDeployment, uint256 _tokens) private { - // before - uint256 beforeSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(_subgraphDeployment); - uint256 beforeAllocatedTokens = subgraphService.allocationProvisionTracker(_indexer); - Allocation.State memory beforeAllocation = subgraphService.getAllocation(_allocationID); - - uint256 allocatedTokensDelta; - if (_tokens > beforeAllocation.tokens) { - allocatedTokensDelta = _tokens - beforeAllocation.tokens; - } else { - allocatedTokensDelta = beforeAllocation.tokens - _tokens; - } - - // resize - vm.expectEmit(address(subgraphService)); - emit AllocationManager.AllocationResized(_indexer, _allocationID, _subgraphDeployment, _tokens, beforeSubgraphAllocatedTokens); - subgraphService.resizeAllocation(_indexer, _allocationID, _tokens); - - // after - uint256 afterSubgraphAllocatedTokens = subgraphService.getSubgraphAllocatedTokens(_subgraphDeployment); - uint256 afterAllocatedTokens = subgraphService.allocationProvisionTracker(_indexer); - Allocation.State memory afterAllocation = subgraphService.getAllocation(_allocationID); - uint256 accRewardsPerAllocatedTokenDelta = afterAllocation.accRewardsPerAllocatedToken - beforeAllocation.accRewardsPerAllocatedToken; - uint256 afterAccRewardsPending = rewardsManager.calcRewards(beforeAllocation.tokens, accRewardsPerAllocatedTokenDelta); - - //assert - if (_tokens > beforeAllocation.tokens) { - assertEq(afterAllocatedTokens, beforeAllocatedTokens + allocatedTokensDelta); - } else { - assertEq(afterAllocatedTokens, beforeAllocatedTokens - allocatedTokensDelta); - } - assertEq(afterAllocation.tokens, _tokens); - assertEq(afterAllocation.accRewardsPerAllocatedToken, rewardsPerSubgraphAllocationUpdate); - assertEq(afterAllocation.accRewardsPending, afterAccRewardsPending); - assertEq(afterSubgraphAllocatedTokens, _tokens); - } - - /* - * TESTS - */ - - function testResize_Allocation(uint256 tokens, uint256 resizeTokens) public useIndexer useAllocation(tokens) { - resizeTokens = bound(resizeTokens, 1, MAX_TOKENS); - vm.assume(resizeTokens != tokens); - - mint(users.indexer, resizeTokens); - _addToProvision(users.indexer, resizeTokens); - _resizeAllocation(users.indexer, allocationID, subgraphDeployment, resizeTokens); - } - - function testResize_Allocation_AfterCollectingIndexingRewards(uint256 tokens, uint256 resizeTokens) public useIndexer useAllocation(tokens) { - resizeTokens = bound(resizeTokens, 1, MAX_TOKENS); - vm.assume(resizeTokens != tokens); - - mint(users.indexer, resizeTokens); - _collectIndexingRewards(users.indexer, allocationID, tokens); - _addToProvision(users.indexer, resizeTokens); - _resizeAllocation(users.indexer, allocationID, subgraphDeployment, resizeTokens); - } - - function testResize_Allocation_SecondTime(uint256 tokens, uint256 firstResizeTokens, uint256 secondResizeTokens) public useIndexer useAllocation(tokens) { - firstResizeTokens = bound(firstResizeTokens, 1, MAX_TOKENS); - secondResizeTokens = bound(secondResizeTokens, 1, MAX_TOKENS); - vm.assume(firstResizeTokens != tokens); - vm.assume(secondResizeTokens != firstResizeTokens); - - mint(users.indexer, firstResizeTokens); - _addToProvision(users.indexer, firstResizeTokens); - _resizeAllocation(users.indexer, allocationID, subgraphDeployment, firstResizeTokens); - - mint(users.indexer, secondResizeTokens); - _addToProvision(users.indexer, secondResizeTokens); - _resizeAllocation(users.indexer, allocationID, subgraphDeployment, secondResizeTokens); - } - - function testResize_RevertWhen_NotAuthorized(uint256 tokens, uint256 resizeTokens) public useIndexer useAllocation(tokens) { - resizeTokens = bound(resizeTokens, tokens + 1, MAX_TOKENS); - - address newIndexer = makeAddr("newIndexer"); - _createAndStartAllocation(newIndexer, tokens); - vm.expectRevert(abi.encodeWithSelector( - ISubgraphService.SubgraphServiceAllocationNotAuthorized.selector, - newIndexer, - allocationID - )); - subgraphService.resizeAllocation(newIndexer, allocationID, resizeTokens); - } - - function testResize_RevertWhen_SameSize(uint256 tokens) public useIndexer useAllocation(tokens) { - vm.expectRevert(abi.encodeWithSelector( - AllocationManager.AllocationManagerAllocationSameSize.selector, - allocationID, - tokens - )); - subgraphService.resizeAllocation(users.indexer, allocationID, tokens); - } - - function testResize_RevertIf_AllocationIsClosed(uint256 tokens, uint256 resizeTokens) public useIndexer useAllocation(tokens) { - resizeTokens = bound(resizeTokens, tokens + 1, MAX_TOKENS); - _stopAllocation(users.indexer, allocationID); - vm.expectRevert(abi.encodeWithSelector( - AllocationManager.AllocationManagerAllocationClosed.selector, - allocationID - )); - subgraphService.resizeAllocation(users.indexer, allocationID, resizeTokens); - } -} diff --git a/packages/subgraph-service/test/subgraphService/allocate/start.t.sol b/packages/subgraph-service/test/subgraphService/allocate/start.t.sol deleted file mode 100644 index 65e4ec35b..000000000 --- a/packages/subgraph-service/test/subgraphService/allocate/start.t.sol +++ /dev/null @@ -1,166 +0,0 @@ -// 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/allocation/closeStale.t.sol b/packages/subgraph-service/test/subgraphService/allocation/closeStale.t.sol new file mode 100644 index 000000000..3e6672dd7 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/allocation/closeStale.t.sol @@ -0,0 +1,108 @@ +// 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 { 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 SubgraphServiceAllocationCloseStaleTest is SubgraphServiceTest { + + address private permissionlessBob = makeAddr("permissionlessBob"); + + /* + * TESTS + */ + + function test_SubgraphService_Allocation_CloseStale( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Skip forward + skip(maxPOIStaleness + 1); + + resetPrank(permissionlessBob); + _closeStaleAllocation(allocationID); + } + + function test_SubgraphService_Allocation_CloseStale_AfterCollecting( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Simulate POIs being submitted + uint8 numberOfPOIs = 5; + uint256 timeBetweenPOIs = 5 days; + + for (uint8 i = 0; i < numberOfPOIs; i++) { + // Skip forward + skip(timeBetweenPOIs); + + bytes memory data = abi.encode(allocationID, bytes32("POI1")); + _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); + } + + // Skip forward so that the allocation is stale + skip(maxPOIStaleness + 1); + + // Close the stale allocation + resetPrank(permissionlessBob); + _closeStaleAllocation(allocationID); + } + + function test_SubgraphService_Allocation_CloseStale_RevertIf_NotStale( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Simulate POIs being submitted + uint8 numberOfPOIs = 20; + uint256 timeBetweenPOIs = (maxPOIStaleness - 1) / numberOfPOIs; + + for (uint8 i = 0; i < numberOfPOIs; i++) { + // Skip forward + skip(timeBetweenPOIs); + + resetPrank(users.indexer); + + bytes memory data = abi.encode(allocationID, bytes32("POI1")); + _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); + + resetPrank(permissionlessBob); + vm.expectRevert( + abi.encodeWithSelector( + ISubgraphService.SubgraphServiceAllocationNotStale.selector, + allocationID, + block.timestamp + ) + ); + subgraphService.closeStaleAllocation(allocationID); + } + } + + function test_SubgraphService_Allocation_CloseStale_RevertIf_Altruistic( + uint256 tokens + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _createSubgraphAllocationData(users.indexer, subgraphDeployment, allocationIDPrivateKey, 0); + _startService(users.indexer, data); + + skip(maxPOIStaleness + 1); + + resetPrank(permissionlessBob); + vm.expectRevert( + abi.encodeWithSelector( + ISubgraphService.SubgraphServiceAllocationIsAltruistic.selector, + allocationID + ) + ); + subgraphService.closeStaleAllocation(allocationID); + } +} diff --git a/packages/subgraph-service/test/subgraphService/allocation/resize.t.sol b/packages/subgraph-service/test/subgraphService/allocation/resize.t.sol new file mode 100644 index 000000000..3da7c5a0c --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/allocation/resize.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { Allocation } from "../../../contracts/libraries/Allocation.sol"; +import { AllocationManager } from "../../../contracts/utilities/AllocationManager.sol"; +import { SubgraphServiceTest } from "../SubgraphService.t.sol"; +import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService.sol"; +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; + +contract SubgraphServiceAllocationResizeTest is SubgraphServiceTest { + + /* + * TESTS + */ + + function test_SubgraphService_Allocation_Resize(uint256 tokens, uint256 resizeTokens) public useIndexer useAllocation(tokens) { + resizeTokens = bound(resizeTokens, 1, MAX_TOKENS); + vm.assume(resizeTokens != tokens); + + mint(users.indexer, resizeTokens); + _addToProvision(users.indexer, resizeTokens); + _resizeAllocation(users.indexer, allocationID, resizeTokens); + } + + function test_SubgraphService_Allocation_Resize_AfterCollectingIndexingRewards(uint256 tokens, uint256 resizeTokens) public useIndexer useAllocation(tokens) { + resizeTokens = bound(resizeTokens, 1, MAX_TOKENS); + vm.assume(resizeTokens != tokens); + + mint(users.indexer, resizeTokens); + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory data = abi.encode(allocationID, bytes32("POI1")); + _collect(users.indexer, paymentType, data); + _addToProvision(users.indexer, resizeTokens); + _resizeAllocation(users.indexer, allocationID, resizeTokens); + } + + function test_SubgraphService_Allocation_Resize_SecondTime(uint256 tokens, uint256 firstResizeTokens, uint256 secondResizeTokens) public useIndexer useAllocation(tokens) { + firstResizeTokens = bound(firstResizeTokens, 1, MAX_TOKENS); + secondResizeTokens = bound(secondResizeTokens, 1, MAX_TOKENS); + vm.assume(firstResizeTokens != tokens); + vm.assume(secondResizeTokens != firstResizeTokens); + + mint(users.indexer, firstResizeTokens); + _addToProvision(users.indexer, firstResizeTokens); + _resizeAllocation(users.indexer, allocationID, firstResizeTokens); + + mint(users.indexer, secondResizeTokens); + _addToProvision(users.indexer, secondResizeTokens); + _resizeAllocation(users.indexer, allocationID, secondResizeTokens); + } + + function test_SubgraphService_Allocation_Resize_RevertWhen_NotAuthorized(uint256 tokens, uint256 resizeTokens) public useIndexer useAllocation(tokens) { + resizeTokens = bound(resizeTokens, tokens + 1, MAX_TOKENS); + + address newIndexer = makeAddr("newIndexer"); + _createAndStartAllocation(newIndexer, tokens); + vm.expectRevert(abi.encodeWithSelector( + ISubgraphService.SubgraphServiceAllocationNotAuthorized.selector, + newIndexer, + allocationID + )); + subgraphService.resizeAllocation(newIndexer, allocationID, resizeTokens); + } + + function test_SubgraphService_Allocation_Resize_RevertWhen_SameSize(uint256 tokens) public useIndexer useAllocation(tokens) { + vm.expectRevert(abi.encodeWithSelector( + AllocationManager.AllocationManagerAllocationSameSize.selector, + allocationID, + tokens + )); + subgraphService.resizeAllocation(users.indexer, allocationID, tokens); + } + + function test_SubgraphService_Allocation_Resize_RevertIf_AllocationIsClosed(uint256 tokens, uint256 resizeTokens) public useIndexer useAllocation(tokens) { + resizeTokens = bound(resizeTokens, tokens + 1, MAX_TOKENS); + bytes memory data = abi.encode(allocationID); + _stopService(users.indexer, data); + vm.expectRevert(abi.encodeWithSelector( + AllocationManager.AllocationManagerAllocationClosed.selector, + allocationID + )); + subgraphService.resizeAllocation(users.indexer, allocationID, resizeTokens); + } +} diff --git a/packages/subgraph-service/test/subgraphService/allocation/start.t.sol b/packages/subgraph-service/test/subgraphService/allocation/start.t.sol new file mode 100644 index 000000000..6db5e73db --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/allocation/start.t.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.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 SubgraphServiceAllocationStartTest is SubgraphServiceTest { + + /* + * TESTS + */ + + function test_SubgraphService_Allocation_Start(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _generateData(tokens); + _startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_AllowsZeroTokens(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _generateData(0); + _startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_ByOperator(uint256 tokens) public useOperator { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _generateData(tokens); + _startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_NotAuthorized(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", 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 test_SubgraphService_Allocation_Start_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 test_SubgraphService_Allocation_Start_RevertWhen_NotRegistered(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + + bytes memory data = _generateData(tokens); + vm.expectRevert(abi.encodeWithSelector( + ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, + users.indexer + )); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_ZeroAllocationId(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", 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 test_SubgraphService_Allocation_Start_RevertWhen_InvalidSignature(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", 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 test_SubgraphService_Allocation_Start_RevertWhen_InvalidData(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = abi.encode(subgraphDeployment, tokens, allocationID, _generateRandomHexBytes(32)); + vm.expectRevert(abi.encodeWithSelector( + ECDSA.ECDSAInvalidSignatureLength.selector, + 32 + )); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_ArealdyExists(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", 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 test_SubgraphService_Allocation_Start_RevertWhen_ReusingAllocationId(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _generateData(tokens); + _startService(users.indexer, data); + + vm.expectRevert(abi.encodeWithSelector( + Allocation.AllocationAlreadyExists.selector, + allocationID + )); + subgraphService.startService(users.indexer, data); + } + + function test_SubgraphService_Allocation_Start_RevertWhen_NotEnoughTokens( + uint256 tokens, + uint256 lockTokens + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS - 1); + lockTokens = bound(lockTokens, tokens + 1, MAX_TOKENS); + + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + bytes memory data = _generateData(lockTokens); + vm.expectRevert(abi.encodeWithSelector( + ProvisionTracker.ProvisionTrackerInsufficientTokens.selector, + tokens, + lockTokens + )); + subgraphService.startService(users.indexer, data); + } + + /* + * PRIVATE FUNCTIONS + */ + + function _generateData(uint256 tokens) private view returns(bytes memory) { + return _createSubgraphAllocationData(users.indexer, subgraphDeployment, allocationIDPrivateKey, tokens); + } + + function _generateRandomHexBytes(uint256 length) private view returns (bytes memory) { + bytes memory randomBytes = new bytes(length); + for (uint256 i = 0; i < length; i++) { + randomBytes[i] = bytes1(uint8(uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, i))) % 256)); + } + return randomBytes; + } +} diff --git a/packages/subgraph-service/test/subgraphService/allocate/stop.t.sol b/packages/subgraph-service/test/subgraphService/allocation/stop.t.sol similarity index 70% rename from packages/subgraph-service/test/subgraphService/allocate/stop.t.sol rename to packages/subgraph-service/test/subgraphService/allocation/stop.t.sol index fff5c457f..84b9da27e 100644 --- a/packages/subgraph-service/test/subgraphService/allocate/stop.t.sol +++ b/packages/subgraph-service/test/subgraphService/allocation/stop.t.sol @@ -13,20 +13,18 @@ import { ISubgraphService } from "../../../contracts/interfaces/ISubgraphService import { LegacyAllocation } from "../../../contracts/libraries/LegacyAllocation.sol"; import { SubgraphServiceTest } from "../SubgraphService.t.sol"; -contract SubgraphServiceAllocateStopTest is SubgraphServiceTest { - /* - * Helpers - */ +contract SubgraphServiceAllocationStopTest is SubgraphServiceTest { /* * TESTS */ - function testStop_Allocation(uint256 tokens) public useIndexer useAllocation(tokens) { - _stopAllocation(users.indexer, allocationID); + function test_SubgraphService_Allocation_Stop(uint256 tokens) public useIndexer useAllocation(tokens) { + bytes memory data = abi.encode(allocationID); + _stopService(users.indexer, data); } - function testStop_RevertWhen_IndexerIsNotTheAllocationOwner( + function test_SubgraphService_Allocation_Stop_RevertWhen_IndexerIsNotTheAllocationOwner( uint256 tokens ) public useIndexer useAllocation(tokens) { // Setup new indexer @@ -45,7 +43,7 @@ contract SubgraphServiceAllocateStopTest is SubgraphServiceTest { subgraphService.stopService(newIndexer, data); } - function testStop_RevertWhen_NotAuthorized(uint256 tokens) public useIndexer useAllocation(tokens) { + function test_SubgraphService_Allocation_Stop_RevertWhen_NotAuthorized(uint256 tokens) public useIndexer useAllocation(tokens) { resetPrank(users.operator); bytes memory data = abi.encode(allocationID); vm.expectRevert( @@ -58,7 +56,7 @@ contract SubgraphServiceAllocateStopTest is SubgraphServiceTest { subgraphService.stopService(users.indexer, data); } - function testStop_RevertWhen_NotRegistered() public useIndexer { + function test_SubgraphService_Allocation_Stop_RevertWhen_NotRegistered() public useIndexer { bytes memory data = abi.encode(allocationID); vm.expectRevert( abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, users.indexer) @@ -66,21 +64,10 @@ contract SubgraphServiceAllocateStopTest is SubgraphServiceTest { subgraphService.stopService(users.indexer, data); } - function testStop_RevertWhen_NotOpen(uint256 tokens) public useIndexer useAllocation(tokens) { + function test_SubgraphService_Allocation_Stop_RevertWhen_NotOpen(uint256 tokens) public useIndexer useAllocation(tokens) { bytes memory data = abi.encode(allocationID); - subgraphService.stopService(users.indexer, data); + _stopService(users.indexer, data); vm.expectRevert(abi.encodeWithSelector(Allocation.AllocationClosed.selector, allocationID, block.timestamp)); subgraphService.stopService(users.indexer, data); } - - function testCloseStaleAllocation( - uint256 tokens - ) public useIndexer useAllocation(tokens) { - address permissionlessBob = makeAddr("permissionlessBob"); - - skip(maxPOIStaleness + 1); - - resetPrank(permissionlessBob); - subgraphService.closeStaleAllocation(allocationID); - } } diff --git a/packages/subgraph-service/test/subgraphService/collect/collect.t.sol b/packages/subgraph-service/test/subgraphService/collect/collect.t.sol index a04b248a3..4891a4a75 100644 --- a/packages/subgraph-service/test/subgraphService/collect/collect.t.sol +++ b/packages/subgraph-service/test/subgraphService/collect/collect.t.sol @@ -3,228 +3,22 @@ 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 { Allocation } from "../../../contracts/libraries/Allocation.sol"; -import { AllocationManager } from "../../../contracts/utilities/AllocationManager.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; - using Allocation for Allocation.State; - - address signer; - uint256 signerPrivateKey; - - /* - * HELPERS - */ - - function _getQueryFeeEncodedData(address indexer, uint128 tokens) private view returns (bytes memory) { - ITAPCollector.ReceiptAggregateVoucher memory rav = _getRAV(indexer, tokens); - bytes32 messageHash = tapCollector.encodeRAV(rav); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); - bytes memory signature = abi.encodePacked(r, s, v); - ITAPCollector.SignedRAV memory signedRAV = ITAPCollector.SignedRAV(rav, signature); - return abi.encode(signedRAV); - } - - function _getRAV( - address indexer, - uint128 tokens - ) private view returns (ITAPCollector.ReceiptAggregateVoucher memory rav) { - return - ITAPCollector.ReceiptAggregateVoucher({ - dataService: address(subgraphService), - serviceProvider: indexer, - timestampNs: 0, - valueAggregate: tokens, - metadata: abi.encode(allocationID) - }); - } - - function _approveCollector(uint256 tokens) private { - address msgSender; - (, msgSender, ) = vm.readCallers(); - resetPrank(signer); - mint(signer, tokens); - escrow.approveCollector(address(tapCollector), tokens); - token.approve(address(escrow), tokens); - escrow.deposit(users.indexer, tokens); - resetPrank(msgSender); - } - - /* - * SET UP - */ - - function setUp() public virtual override { - super.setUp(); - (signer, signerPrivateKey) = makeAddrAndKey("signer"); - vm.label({ account: signer, newLabel: "signer" }); - } +contract SubgraphServiceCollectTest is SubgraphServiceTest { /* * TESTS */ - function testCollect_QueryFees( - uint256 tokensAllocated, - uint256 tokensPayment - ) public useIndexer useAllocation(tokensAllocated) { - vm.assume(tokensAllocated > minimumProvisionTokens * stakeToFeesRatio); - uint256 maxTokensPayment = tokensAllocated / stakeToFeesRatio > type(uint128).max - ? type(uint128).max - : tokensAllocated / stakeToFeesRatio; - tokensPayment = bound(tokensPayment, minimumProvisionTokens, maxTokensPayment); - - _approveCollector(tokensPayment); - - uint256 indexerPreviousBalance = token.balanceOf(users.indexer); - - bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokensPayment)); - uint256 tokensCollected = subgraphService.collect(users.indexer, IGraphPayments.PaymentTypes.QueryFee, data); - - uint256 indexerBalance = token.balanceOf(users.indexer); - uint256 tokensProtocol = tokensCollected.mulPPM(protocolPaymentCut); - uint256 curationTokens = tokensCollected.mulPPM(curationCut); - - uint256 expectedIndexerTokensPayment = tokensCollected - tokensProtocol - curationTokens; - assertEq(indexerBalance, indexerPreviousBalance + expectedIndexerTokensPayment); - } - - function testCollect_MultipleQueryFees( - uint256 tokensAllocated, - uint256 numPayments - ) public useIndexer useAllocation(tokensAllocated) { - vm.assume(tokensAllocated > minimumProvisionTokens * stakeToFeesRatio); - numPayments = bound(numPayments, 1, 10); - uint256 tokensPayment = tokensAllocated / stakeToFeesRatio / numPayments; - - _approveCollector(tokensAllocated); - - uint256 accTokensPayment = 0; - for (uint i = 0; i < numPayments; i++) { - accTokensPayment = accTokensPayment + tokensPayment; - uint256 indexerPreviousBalance = token.balanceOf(users.indexer); - - bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(accTokensPayment)); - uint256 tokensCollected = subgraphService.collect( - users.indexer, - IGraphPayments.PaymentTypes.QueryFee, - data - ); - - uint256 indexerBalance = token.balanceOf(users.indexer); - uint256 tokensProtocol = tokensCollected.mulPPM(protocolPaymentCut); - uint256 curationTokens = tokensCollected.mulPPM(curationCut); - - uint256 expectedIndexerTokensPayment = tokensCollected - tokensProtocol - curationTokens; - assertEq(indexerBalance, indexerPreviousBalance + expectedIndexerTokensPayment); - } - } - - function testCollect_IndexingRewards(uint256 tokens) public useIndexer useAllocation(tokens) { - _collectIndexingRewards(users.indexer, allocationID, tokens); - - Allocation.State memory afterAllocation = subgraphService.getAllocation(allocationID); - assertEq(afterAllocation.isOpen(), true); - } - - function testCollect_IndexingRewards_When_OverAllocated(uint256 tokens) public useIndexer { - tokens = bound(tokens, minimumProvisionTokens * 2, 10_000_000_000 ether); - - // setup allocation - _createProvision(tokens); - _registerIndexer(address(0)); - _startService(tokens); - - // thaw some tokens to become over allocated - staking.thaw(users.indexer, address(subgraphService), tokens / 2); - - // this collection should close the allocation - _collectIndexingRewards(users.indexer, allocationID, tokens); - - Allocation.State memory afterAllocation = subgraphService.getAllocation(allocationID); - assertEq(afterAllocation.isOpen(), false); - } - - function testCollect_RevertWhen_InvalidPayment(uint256 tokens) public useIndexer useAllocation(tokens) { + function test_SubgraphService_Collect_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, ""); } - - function testCollect_RevertWhen_NotAuthorized(uint256 tokens) public useIndexer useAllocation(tokens) { - IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.QueryFee; - bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokens)); - resetPrank(users.operator); - vm.expectRevert( - abi.encodeWithSelector( - ProvisionManager.ProvisionManagerNotAuthorized.selector, - users.operator, - users.indexer - ) - ); - subgraphService.collect(users.indexer, paymentType, data); - } - - function testCollect_QueryFees_RevertWhen_IndexerIsNotAllocationOwner( - uint256 tokens - ) public useIndexer useAllocation(tokens) { - IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.QueryFee; - // Setup new indexer - address newIndexer = makeAddr("newIndexer"); - _createAndStartAllocation(newIndexer, tokens); - - // This data is for user.indexer allocationId - bytes memory data = _getQueryFeeEncodedData(newIndexer, uint128(tokens)); - - resetPrank(newIndexer); - vm.expectRevert( - abi.encodeWithSelector( - ISubgraphService.SubgraphServiceInvalidRAV.selector, - newIndexer, - users.indexer - ) - ); - subgraphService.collect(newIndexer, paymentType, data); - } - - function testCollect_QueryFees_RevertWhen_CollectingOtherIndexersFees( - uint256 tokens - ) public useIndexer useAllocation(tokens) { - IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.QueryFee; - // Setup new indexer - address newIndexer = makeAddr("newIndexer"); - _createAndStartAllocation(newIndexer, tokens); - bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokens)); - vm.expectRevert( - abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerMismatch.selector, users.indexer, newIndexer) - ); - subgraphService.collect(newIndexer, paymentType, data); - } - - function testCollect_IndexingFees_RevertWhen_IndexerIsNotAllocationOwner( - uint256 tokens - ) public useIndexer useAllocation(tokens) { - IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; - // Setup new indexer - address newIndexer = makeAddr("newIndexer"); - _createAndStartAllocation(newIndexer, tokens); - bytes memory data = abi.encode(allocationID, bytes32("POI1")); - // Attempt to collect from other indexer's allocation - vm.expectRevert( - abi.encodeWithSelector(ISubgraphService.SubgraphServiceAllocationNotAuthorized.selector, newIndexer, allocationID) - ); - subgraphService.collect(newIndexer, paymentType, data); - } } diff --git a/packages/subgraph-service/test/subgraphService/collect/indexing/indexing.t.sol b/packages/subgraph-service/test/subgraphService/collect/indexing/indexing.t.sol new file mode 100644 index 000000000..d51f9ea4b --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/collect/indexing/indexing.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol"; + +import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphService.sol"; +import { SubgraphServiceTest } from "../../SubgraphService.t.sol"; + +contract SubgraphServiceCollectIndexingTest is SubgraphServiceTest { + + /* + * TESTS + */ + + function test_SubgraphService_Collect_Indexing( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory data = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, paymentType, data); + } + + function test_SubgraphService_Collect_Indexing_WithDelegation( + uint256 tokens, + uint256 delegationTokens, + uint256 delegationFeeCut + ) public useIndexer useAllocation(tokens) useDelegation(delegationTokens) { + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); + _setDelegationFeeCut( + users.indexer, + address(subgraphService), + // TODO: this should be fixed in AllocationManager, it should be IndexingRewards instead + IGraphPayments.PaymentTypes.IndexingFee, + 100_000 + ); + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory data = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, paymentType, data); + } + + function test_SubgraphService_Collect_Indexing_RewardsDestination( + uint256 tokens + ) public useIndexer useAllocation(tokens) useRewardsDestination { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory data = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, paymentType, data); + } + + function test_subgraphService_Collect_Indexing_MultipleOverTime(uint256 tokens) public useIndexer useAllocation(tokens) { + uint8 numberOfPOIs = 20; + uint256 timeBetweenPOIs = 5 days; + + for (uint8 i = 0; i < numberOfPOIs; i++) { + // Skip forward + skip(timeBetweenPOIs); + + resetPrank(users.indexer); + + bytes memory data = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); + } + } + + function test_subgraphService_Collect_Indexing_MultipleOverTime_WithDelegation( + uint256 tokens, + uint256 delegationTokens, + uint256 delegationFeeCut + ) public useIndexer useAllocation(tokens) useDelegation(delegationTokens) { + delegationFeeCut = bound(delegationFeeCut, 0, MAX_PPM); + _setDelegationFeeCut( + users.indexer, + address(subgraphService), + // TODO: this should be fixed in AllocationManager, it should be IndexingRewards instead + IGraphPayments.PaymentTypes.IndexingFee, + 100_000 + ); + + uint8 numberOfPOIs = 20; + uint256 timeBetweenPOIs = 5 days; + for (uint8 i = 0; i < numberOfPOIs; i++) { + // Skip forward + skip(timeBetweenPOIs); + + resetPrank(users.indexer); + + bytes memory data = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, IGraphPayments.PaymentTypes.IndexingRewards, data); + } + } + + function test_SubgraphService_Collect_Indexing_RevertWhen_OverAllocated(uint256 tokens) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens * 2, 10_000_000_000 ether); + + // setup allocation + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + bytes memory data = _createSubgraphAllocationData(users.indexer, subgraphDeployment, allocationIDPrivateKey, tokens); + _startService(users.indexer, data); + + // thaw some tokens to become over allocated + staking.thaw(users.indexer, address(subgraphService), tokens / 2); + + // this collection should close the allocation + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + bytes memory collectData = abi.encode(allocationID, bytes32("POI")); + _collect(users.indexer, paymentType, collectData); + } + + function test_SubgraphService_Collect_Indexing_RevertWhen_IndexerIsNotAllocationOwner( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.IndexingRewards; + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + _createAndStartAllocation(newIndexer, tokens); + bytes memory data = abi.encode(allocationID, bytes32("POI")); + // Attempt to collect from other indexer's allocation + vm.expectRevert( + abi.encodeWithSelector(ISubgraphService.SubgraphServiceAllocationNotAuthorized.selector, newIndexer, allocationID) + ); + subgraphService.collect(newIndexer, paymentType, data); + } +} diff --git a/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol new file mode 100644 index 000000000..9e20ad352 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol @@ -0,0 +1,157 @@ +// 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(address indexer, uint128 tokens) private view returns (bytes memory) { + ITAPCollector.ReceiptAggregateVoucher memory rav = _getRAV(indexer, tokens); + bytes32 messageHash = tapCollector.encodeRAV(rav); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, messageHash); + bytes memory signature = abi.encodePacked(r, s, v); + ITAPCollector.SignedRAV memory signedRAV = ITAPCollector.SignedRAV(rav, signature); + return abi.encode(signedRAV); + } + + function _getRAV( + address indexer, + uint128 tokens + ) private view returns (ITAPCollector.ReceiptAggregateVoucher memory rav) { + return + ITAPCollector.ReceiptAggregateVoucher({ + dataService: address(subgraphService), + serviceProvider: indexer, + timestampNs: 0, + valueAggregate: tokens, + metadata: abi.encode(allocationID) + }); + } + + function _approveCollector(uint256 tokens) private { + address msgSender; + (, msgSender, ) = vm.readCallers(); + resetPrank(signer); + mint(signer, tokens); + escrow.approveCollector(address(tapCollector), tokens); + token.approve(address(escrow), tokens); + escrow.deposit(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_ONLY_THIS( + uint256 tokensAllocated, + uint256 tokensPayment + ) public useIndexer useAllocation(tokensAllocated) { + vm.assume(tokensAllocated > minimumProvisionTokens * stakeToFeesRatio); + uint256 maxTokensPayment = tokensAllocated / stakeToFeesRatio > type(uint128).max + ? type(uint128).max + : tokensAllocated / stakeToFeesRatio; + tokensPayment = bound(tokensPayment, minimumProvisionTokens, maxTokensPayment); + + _approveCollector(tokensPayment); + + bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokensPayment)); + _collect(users.indexer, IGraphPayments.PaymentTypes.QueryFee, data); + } + + function testCollect_MultipleQueryFees( + uint256 tokensAllocated, + uint256 numPayments + ) public useIndexer useAllocation(tokensAllocated) { + vm.assume(tokensAllocated > minimumProvisionTokens * stakeToFeesRatio); + numPayments = bound(numPayments, 1, 10); + uint256 tokensPayment = tokensAllocated / stakeToFeesRatio / numPayments; + + _approveCollector(tokensAllocated); + + uint256 accTokensPayment = 0; + for (uint i = 0; i < numPayments; i++) { + accTokensPayment = accTokensPayment + tokensPayment; + bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(accTokensPayment)); + _collect(users.indexer, IGraphPayments.PaymentTypes.QueryFee, data); + } + } + + function testCollect_RevertWhen_NotAuthorized(uint256 tokens) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.QueryFee; + bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokens)); + resetPrank(users.operator); + vm.expectRevert( + abi.encodeWithSelector( + ProvisionManager.ProvisionManagerNotAuthorized.selector, + users.operator, + users.indexer + ) + ); + subgraphService.collect(users.indexer, paymentType, data); + } + + function testCollect_QueryFees_RevertWhen_IndexerIsNotAllocationOwner( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.QueryFee; + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + _createAndStartAllocation(newIndexer, tokens); + + // This data is for user.indexer allocationId + bytes memory data = _getQueryFeeEncodedData(newIndexer, uint128(tokens)); + + resetPrank(newIndexer); + vm.expectRevert( + abi.encodeWithSelector( + ISubgraphService.SubgraphServiceInvalidRAV.selector, + newIndexer, + users.indexer + ) + ); + subgraphService.collect(newIndexer, paymentType, data); + } + + function testCollect_QueryFees_RevertWhen_CollectingOtherIndexersFees( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + IGraphPayments.PaymentTypes paymentType = IGraphPayments.PaymentTypes.QueryFee; + // Setup new indexer + address newIndexer = makeAddr("newIndexer"); + _createAndStartAllocation(newIndexer, tokens); + bytes memory data = _getQueryFeeEncodedData(users.indexer, uint128(tokens)); + vm.expectRevert( + abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerMismatch.selector, users.indexer, newIndexer) + ); + subgraphService.collect(newIndexer, paymentType, data); + } +} diff --git a/packages/subgraph-service/test/subgraphService/provider/register.t.sol b/packages/subgraph-service/test/subgraphService/provider/register.t.sol index e42e152f7..8e3d0577e 100644 --- a/packages/subgraph-service/test/subgraphService/provider/register.t.sol +++ b/packages/subgraph-service/test/subgraphService/provider/register.t.sol @@ -3,65 +3,54 @@ 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 { +contract SubgraphServiceProviderRegisterTest is SubgraphServiceTest { /* * TESTS */ - function testRegister_Indexer(uint256 tokens) public useIndexer { + function test_SubgraphService_Provider_Register(uint256 tokens) public useIndexer { tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); - _createProvision(tokens); + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); 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"); + _register(users.indexer, data); } - function testRegister_RevertIf_AlreadyRegistered( + function test_SubgraphService_Provider_Register_RevertIf_AlreadyRegistered( uint256 tokens ) public useIndexer useAllocation(tokens) { vm.expectRevert(abi.encodeWithSelector(ISubgraphService.SubgraphServiceIndexerAlreadyRegistered.selector)); - _registerIndexer(users.rewardsDestination); + bytes memory data = abi.encode("url", "geoHash", users.rewardsDestination); + subgraphService.register(users.indexer, data); } - function testRegister_RevertWhen_InvalidProvision() public useIndexer { + function test_SubgraphService_Provider_Register_RevertWhen_InvalidProvision() public useIndexer { vm.expectRevert(abi.encodeWithSelector( ProvisionManager.ProvisionManagerProvisionNotFound.selector, users.indexer )); - _registerIndexer(users.rewardsDestination); + bytes memory data = abi.encode("url", "geoHash", users.rewardsDestination); + subgraphService.register(users.indexer, data); } - function testRegister_RevertWhen_NotAuthorized() public { + function test_SubgraphService_Provider_Register_RevertWhen_NotAuthorized() public { resetPrank(users.operator); vm.expectRevert(abi.encodeWithSelector( ProvisionManager.ProvisionManagerNotAuthorized.selector, users.operator, users.indexer )); - _registerIndexer(users.rewardsDestination); + bytes memory data = abi.encode("url", "geoHash", users.rewardsDestination); + subgraphService.register(users.indexer, data); } - function testRegister_RevertWhen_InvalidProvisionValues(uint256 tokens) public useIndexer { + function test_SubgraphService_Provider_Register_RevertWhen_InvalidProvisionValues(uint256 tokens) public useIndexer { tokens = bound(tokens, 1, minimumProvisionTokens - 1); - _createProvision(tokens); + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); vm.expectRevert(abi.encodeWithSelector( ProvisionManager.ProvisionManagerInvalidValue.selector, @@ -70,20 +59,20 @@ contract SubgraphServiceRegisterTest is SubgraphServiceTest { minimumProvisionTokens, maximumProvisionTokens )); - _registerIndexer(address(0)); + subgraphService.register(users.indexer, abi.encode("url", "geoHash", address(0))); } - function testRegister_RevertIf_EmptyUrl(uint256 tokens) public useIndexer { + function test_SubgraphService_Provider_Register_RevertIf_EmptyUrl(uint256 tokens) public useIndexer { tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); - _createProvision(tokens); + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); bytes memory data = abi.encode("", "geoHash", users.rewardsDestination); vm.expectRevert(abi.encodeWithSelector(ISubgraphService.SubgraphServiceEmptyUrl.selector)); subgraphService.register(users.indexer, data); } - function testRegister_RevertIf_EmptyGeohash(uint256 tokens) public useIndexer { + function test_SubgraphService_Provider_Register_RevertIf_EmptyGeohash(uint256 tokens) public useIndexer { tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); - _createProvision(tokens); + _createProvision(users.indexer, tokens, maxSlashingPercentage, disputePeriod); bytes memory data = abi.encode("url", "", users.rewardsDestination); vm.expectRevert(abi.encodeWithSelector(ISubgraphService.SubgraphServiceEmptyGeohash.selector)); subgraphService.register(users.indexer, data); diff --git a/packages/subgraph-service/test/subgraphService/provider/rewardsDestination.t.sol b/packages/subgraph-service/test/subgraphService/provider/rewardsDestination.t.sol new file mode 100644 index 000000000..3926bc362 --- /dev/null +++ b/packages/subgraph-service/test/subgraphService/provider/rewardsDestination.t.sol @@ -0,0 +1,26 @@ +// 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 SubgraphServiceProviderRewardsDestinationTest is SubgraphServiceTest { + + /* + * TESTS + */ + + function test_SubgraphService_Provider_RewardsDestination_Set( + uint256 tokens + ) public useIndexer useAllocation(tokens) { + // Should be able to use new address + _setRewardsDestination(users.rewardsDestination); + + // Should be able to set back to address zero + _setRewardsDestination(address(0)); + } +} diff --git a/packages/subgraph-service/test/subgraphService/provision/accept.t.sol b/packages/subgraph-service/test/subgraphService/provision/accept.t.sol index 8e98078c6..9fdb0b76f 100644 --- a/packages/subgraph-service/test/subgraphService/provision/accept.t.sol +++ b/packages/subgraph-service/test/subgraphService/provision/accept.t.sol @@ -14,13 +14,28 @@ 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 test_SubgraphService_Provision_Accept( + uint256 tokens, + uint32 newVerifierCut, + uint64 newDisputePeriod + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + vm.assume(newVerifierCut >= fishermanRewardPercentage); + vm.assume(newVerifierCut <= MAX_PPM); + vm.assume(newDisputePeriod >= disputePeriod); + + // Setup indexer + _createProvision(users.indexer, tokens, fishermanRewardPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + // Update parameters with new values + _setProvisionParameters(users.indexer, address(subgraphService), newVerifierCut, newDisputePeriod); + + // Accept provision and check parameters + _acceptProvision(users.indexer, ""); } - function testAccept_RevertWhen_NotRegistered() public useIndexer { + function test_SubgraphService_Provision_Accept_RevertWhen_NotRegistered() public useIndexer { vm.expectRevert(abi.encodeWithSelector( ISubgraphService.SubgraphServiceIndexerNotRegistered.selector, users.indexer @@ -28,7 +43,7 @@ contract SubgraphServiceProvisionAcceptTest is SubgraphServiceTest { subgraphService.acceptProvision(users.indexer, ""); } - function testAccept_RevertWhen_NotAuthorized() public { + function test_SubgraphService_Provision_Accept_RevertWhen_NotAuthorized() public { resetPrank(users.operator); vm.expectRevert(abi.encodeWithSelector( ProvisionManager.ProvisionManagerNotAuthorized.selector, @@ -37,4 +52,54 @@ contract SubgraphServiceProvisionAcceptTest is SubgraphServiceTest { )); subgraphService.acceptProvision(users.indexer, ""); } + + function test_SubgraphService_Provision_Accept_RevertIf_InvalidVerifierCut( + uint256 tokens, + uint32 newVerifierCut + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + vm.assume(newVerifierCut > MAX_PPM); + + // Setup indexer + _createProvision(users.indexer, tokens, fishermanRewardPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + // Update parameters with new values + _setProvisionParameters(users.indexer, address(subgraphService), newVerifierCut, disputePeriod); + + // Should revert since newVerifierCut is invalid + vm.expectRevert(abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "maxVerifierCut", + newVerifierCut, + fishermanRewardPercentage, + MAX_PPM + )); + subgraphService.acceptProvision(users.indexer, ""); + } + + function test_SubgraphService_Provision_Accept_RevertIf_InvalidDisputePeriod( + uint256 tokens, + uint64 newDisputePeriod + ) public useIndexer { + tokens = bound(tokens, minimumProvisionTokens, MAX_TOKENS); + vm.assume(newDisputePeriod < disputePeriod); + + // Setup indexer + _createProvision(users.indexer, tokens, fishermanRewardPercentage, disputePeriod); + _register(users.indexer, abi.encode("url", "geoHash", address(0))); + + // Update parameters with new values + _setProvisionParameters(users.indexer, address(subgraphService), fishermanRewardPercentage, newDisputePeriod); + + // Should revert since newDisputePeriod is invalid + vm.expectRevert(abi.encodeWithSelector( + ProvisionManager.ProvisionManagerInvalidValue.selector, + "thawingPeriod", + newDisputePeriod, + disputePeriod, + type(uint64).max + )); + subgraphService.acceptProvision(users.indexer, ""); + } } diff --git a/packages/subgraph-service/test/utils/Constants.sol b/packages/subgraph-service/test/utils/Constants.sol index b28362653..e20d1fc72 100644 --- a/packages/subgraph-service/test/utils/Constants.sol +++ b/packages/subgraph-service/test/utils/Constants.sol @@ -3,10 +3,11 @@ pragma solidity 0.8.26; abstract contract Constants { uint256 internal constant MAX_TOKENS = 10_000_000_000 ether; + uint256 internal constant MAX_PPM = 1_000_000; // Dispute Manager uint64 internal constant disputePeriod = 300; // 5 minutes uint256 internal constant disputeDeposit = 100 ether; // 100 GRT - uint32 internal constant fishermanRewardPercentage = 100000; // 10% + uint32 internal constant fishermanRewardPercentage = 500000; // 50% uint32 internal constant maxSlashingPercentage = 500000; // 50% // Subgraph Service uint256 internal constant minimumProvisionTokens = 1000 ether;