diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 58c93a756..2854282f4 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -31,11 +31,9 @@ import { AttestationManager } from "./utilities/AttestationManager.sol"; * Indexers use the derived private key for an allocation to sign attestations. * * Indexing Disputes: - * Indexers present a Proof of Indexing (POI) when they close allocations to prove - * they were indexing a subgraph. The Staking contract emits that proof with the format - * keccak256(indexer.address, POI). - * Any fisherman can dispute the validity of a POI by submitting a dispute to this contract - * along with a deposit. + * Indexers periodically present a Proof of Indexing (POI) to prove they are indexing a subgraph. + * The Subgraph Service contract emits that proof which includes the POI. Any fisherman can dispute the + * validity of a POI by submitting a dispute to this contract along with a deposit. * * Arbitration: * Disputes can only be accepted, rejected or drawn by the arbitrator role that can be delegated @@ -59,6 +57,9 @@ contract DisputeManager is // Maximum value for fisherman reward cut in PPM uint32 public constant MAX_FISHERMAN_REWARD_CUT = 500000; + // Minimum value for dispute deposit + uint256 public constant MIN_DISPUTE_DEPOSIT = 1e18; // 1 GRT + // -- Modifiers -- /** @@ -140,7 +141,7 @@ contract DisputeManager is */ function createIndexingDispute(address allocationId, bytes32 poi) external override returns (bytes32) { // Get funds from fisherman - _pullFishermanDeposit(); + _graphToken().pullTokens(msg.sender, disputeDeposit); // Create a dispute return _createIndexingDisputeWithAllocation(msg.sender, disputeDeposit, allocationId, poi); @@ -158,7 +159,7 @@ contract DisputeManager is */ function createQueryDispute(bytes calldata attestationData) external override returns (bytes32) { // Get funds from fisherman - _pullFishermanDeposit(); + _graphToken().pullTokens(msg.sender, disputeDeposit); // Create a dispute return @@ -174,10 +175,14 @@ contract DisputeManager is * @notice Create query disputes for two conflicting attestations. * A conflicting attestation is a proof presented by two different indexers * where for the same request on a subgraph the response is different. - * For this type of dispute the fisherman is not required to present a deposit - * as one of the attestation is considered to be right. * Two linked disputes will be created and if the arbitrator resolve one, the other - * one will be automatically resolved. + * one will be automatically resolved. Note that: + * - it's not possible to reject a conflicting query dispute as by definition at least one + * of the attestations is incorrect. + * - if both attestations are proven to be incorrect, the arbitrator can slash the indexer twice. + * Requirements: + * - fisherman must have previously approved this contract to pull `disputeDeposit` amount + * of tokens from their balance. * @param attestationData1 First attestation data submitted * @param attestationData2 Second attestation data submitted * @return DisputeId1, DisputeId2 @@ -205,10 +210,23 @@ contract DisputeManager is ) ); + // Get funds from fisherman + _graphToken().pullTokens(msg.sender, disputeDeposit); + // Create the disputes // The deposit is zero for conflicting attestations - bytes32 dId1 = _createQueryDisputeWithAttestation(fisherman, 0, attestation1, attestationData1); - bytes32 dId2 = _createQueryDisputeWithAttestation(fisherman, 0, attestation2, attestationData2); + bytes32 dId1 = _createQueryDisputeWithAttestation( + fisherman, + disputeDeposit / 2, + attestation1, + attestationData1 + ); + bytes32 dId2 = _createQueryDisputeWithAttestation( + fisherman, + disputeDeposit / 2, + attestation2, + attestationData2 + ); // Store the linked disputes to be resolved disputes[dId1].relatedDisputeId = dId2; @@ -225,6 +243,8 @@ contract DisputeManager is * This function will revert if the indexer is not slashable, whether because it does not have * any stake available or the slashing percentage is configured to be zero. In those cases * a dispute must be resolved using drawDispute or rejectDispute. + * This function will also revert if the dispute is in conflict, to accept a conflicting dispute + * use acceptDisputeConflict. * @dev Accept a dispute with Id `disputeId` * @param disputeId Id of the dispute to be accepted * @param tokensSlash Amount of tokens to slash from the indexer @@ -233,49 +253,71 @@ contract DisputeManager is bytes32 disputeId, uint256 tokensSlash ) external override onlyArbitrator onlyPendingDispute(disputeId) { + require(!_isDisputeInConflict(disputes[disputeId]), DisputeManagerDisputeInConflict(disputeId)); Dispute storage dispute = disputes[disputeId]; + _acceptDispute(disputeId, dispute, tokensSlash); + } - // store the dispute status - dispute.status = IDisputeManager.DisputeStatus.Accepted; - - // Slash - uint256 tokensToReward = _slashIndexer(dispute.indexer, tokensSlash, dispute.stakeSnapshot); - - // Give the fisherman their reward and their deposit back - _graphToken().pushTokens(dispute.fisherman, tokensToReward + dispute.deposit); + /** + * @notice The arbitrator accepts a conflicting dispute as being valid. + * This function will revert if the indexer is not slashable, whether because it does not have + * any stake available or the slashing percentage is configured to be zero. In those cases + * a dispute must be resolved using drawDispute. + * @param disputeId Id of the dispute to be accepted + * @param tokensSlash Amount of tokens to slash from the indexer for the first dispute + * @param acceptDisputeInConflict Accept the conflicting dispute. Otherwise it will be drawn automatically + * @param tokensSlashRelated Amount of tokens to slash from the indexer for the related dispute in case + * acceptDisputeInConflict is true, otherwise it will be ignored + */ + function acceptDisputeConflict( + bytes32 disputeId, + uint256 tokensSlash, + bool acceptDisputeInConflict, + uint256 tokensSlashRelated + ) external override onlyArbitrator onlyPendingDispute(disputeId) { + require(_isDisputeInConflict(disputes[disputeId]), DisputeManagerDisputeNotInConflict(disputeId)); + Dispute storage dispute = disputes[disputeId]; + _acceptDispute(disputeId, dispute, tokensSlash); - if (_isDisputeInConflict(dispute)) { - rejectDispute(dispute.relatedDisputeId); + if (acceptDisputeInConflict) { + _acceptDispute(dispute.relatedDisputeId, disputes[dispute.relatedDisputeId], tokensSlashRelated); + } else { + _drawDispute(dispute.relatedDisputeId, disputes[dispute.relatedDisputeId]); } + } - emit DisputeAccepted(disputeId, dispute.indexer, dispute.fisherman, dispute.deposit + tokensToReward); + /** + * @notice The arbitrator rejects a dispute as being invalid. + * Note that conflicting query disputes cannot be rejected. + * @dev Reject a dispute with Id `disputeId` + * @param disputeId Id of the dispute to be rejected + */ + function rejectDispute(bytes32 disputeId) external override onlyArbitrator onlyPendingDispute(disputeId) { + Dispute storage dispute = disputes[disputeId]; + require(!_isDisputeInConflict(dispute), DisputeManagerDisputeInConflict(disputeId)); + _rejectDispute(disputeId, dispute); } /** * @notice The arbitrator draws dispute. + * Note that drawing a conflicting query dispute should not be possible however it is allowed + * to give arbitrators greater flexibility when resolving disputes. * @dev Ignore a dispute with Id `disputeId` * @param disputeId Id of the dispute to be disregarded */ function drawDispute(bytes32 disputeId) external override onlyArbitrator onlyPendingDispute(disputeId) { Dispute storage dispute = disputes[disputeId]; + _drawDispute(disputeId, dispute); - // Return deposit to the fisherman if any - if (dispute.deposit > 0) { - _graphToken().pushTokens(dispute.fisherman, dispute.deposit); + if (_isDisputeInConflict(dispute)) { + _drawDispute(dispute.relatedDisputeId, disputes[dispute.relatedDisputeId]); } - - // resolve related dispute if any - _drawDisputeInConflict(dispute); - - // store dispute status - dispute.status = IDisputeManager.DisputeStatus.Drawn; - - emit DisputeDrawn(disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); } /** * @notice Once the dispute period ends, if the dispute status remains Pending, * the fisherman can cancel the dispute and get back their initial deposit. + * Note that cancelling a conflicting query dispute will also cancel the related dispute. * @dev Cancel a dispute with Id `disputeId` * @param disputeId Id of the dispute to be cancelled */ @@ -284,19 +326,11 @@ contract DisputeManager is // Check if dispute period has finished require(dispute.createdAt + disputePeriod < block.timestamp, DisputeManagerDisputePeriodNotFinished()); + _cancelDispute(disputeId, dispute); - // Return deposit to the fisherman if any - if (dispute.deposit > 0) { - _graphToken().pushTokens(dispute.fisherman, dispute.deposit); + if (_isDisputeInConflict(dispute)) { + _cancelDispute(dispute.relatedDisputeId, disputes[dispute.relatedDisputeId]); } - - // resolve related dispute if any - _cancelDisputeInConflict(dispute); - - // store dispute status - dispute.status = IDisputeManager.DisputeStatus.Cancelled; - - emit DisputeCancelled(disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); } /** @@ -385,7 +419,10 @@ contract DisputeManager is * @param indexer The indexer address */ function getStakeSnapshot(address indexer) external view override returns (uint256) { - IHorizonStaking.Provision memory provision = _graphStaking().getProvision(indexer, address(subgraphService)); + IHorizonStaking.Provision memory provision = _graphStaking().getProvision( + indexer, + address(_getSubgraphService()) + ); return _getStakeSnapshot(indexer, provision.tokens); } @@ -401,29 +438,6 @@ contract DisputeManager is return Attestation.areConflicting(attestation1, attestation2); } - /** - * @notice The arbitrator rejects a dispute as being invalid. - * @dev Reject a dispute with Id `disputeId` - * @param disputeId Id of the dispute to be rejected - */ - function rejectDispute(bytes32 disputeId) public override onlyArbitrator onlyPendingDispute(disputeId) { - Dispute storage dispute = disputes[disputeId]; - - // store dispute status - dispute.status = IDisputeManager.DisputeStatus.Rejected; - - // For conflicting disputes, the related dispute must be accepted - require( - !_isDisputeInConflict(dispute), - DisputeManagerMustAcceptRelatedDispute(disputeId, dispute.relatedDisputeId) - ); - - // Burn the fisherman's deposit - _graphToken().burnTokens(dispute.deposit); - - emit DisputeRejected(disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); - } - /** * @notice Returns the indexer that signed an attestation. * @param attestation Attestation @@ -433,7 +447,7 @@ contract DisputeManager is // Get attestation signer. Indexers signs with the allocationId address allocationId = _recoverSigner(attestation); - Allocation.State memory alloc = subgraphService.getAllocation(allocationId); + Allocation.State memory alloc = _getSubgraphService().getAllocation(allocationId); require(alloc.indexer != address(0), DisputeManagerIndexerNotFound(allocationId)); require( alloc.subgraphDeploymentId == attestation.subgraphDeploymentId, @@ -472,7 +486,10 @@ contract DisputeManager is address indexer = getAttestationIndexer(_attestation); // The indexer is disputable - IHorizonStaking.Provision memory provision = _graphStaking().getProvision(indexer, address(subgraphService)); + IHorizonStaking.Provision memory provision = _graphStaking().getProvision( + indexer, + address(_getSubgraphService()) + ); require(provision.tokens != 0, DisputeManagerZeroTokens()); // Create a disputeId @@ -535,12 +552,13 @@ contract DisputeManager is require(!isDisputeCreated(disputeId), DisputeManagerDisputeAlreadyCreated(disputeId)); // Allocation must exist - Allocation.State memory alloc = subgraphService.getAllocation(_allocationId); + ISubgraphService subgraphService_ = _getSubgraphService(); + Allocation.State memory alloc = subgraphService_.getAllocation(_allocationId); address indexer = alloc.indexer; require(indexer != address(0), DisputeManagerIndexerNotFound(_allocationId)); // The indexer must be disputable - IHorizonStaking.Provision memory provision = _graphStaking().getProvision(indexer, address(subgraphService)); + IHorizonStaking.Provision memory provision = _graphStaking().getProvision(indexer, address(subgraphService_)); require(provision.tokens != 0, DisputeManagerZeroTokens()); // Store dispute @@ -561,42 +579,33 @@ contract DisputeManager is return disputeId; } - /** - * @notice Draw the conflicting dispute if there is any for the one passed to this function. - * @param _dispute Dispute - * @return True if resolved - */ - function _drawDisputeInConflict(Dispute memory _dispute) private returns (bool) { - if (_isDisputeInConflict(_dispute)) { - bytes32 relatedDisputeId = _dispute.relatedDisputeId; - Dispute storage relatedDispute = disputes[relatedDisputeId]; - relatedDispute.status = IDisputeManager.DisputeStatus.Drawn; - return true; - } - return false; + function _acceptDispute(bytes32 _disputeId, Dispute storage _dispute, uint256 _tokensSlashed) private { + uint256 tokensToReward = _slashIndexer(_dispute.indexer, _tokensSlashed, _dispute.stakeSnapshot); + _dispute.status = IDisputeManager.DisputeStatus.Accepted; + _graphToken().pushTokens(_dispute.fisherman, tokensToReward + _dispute.deposit); + + emit DisputeAccepted(_disputeId, _dispute.indexer, _dispute.fisherman, _dispute.deposit + tokensToReward); } - /** - * @notice Cancel the conflicting dispute if there is any for the one passed to this function. - * @param _dispute Dispute - * @return True if cancelled - */ - function _cancelDisputeInConflict(Dispute memory _dispute) private returns (bool) { - if (_isDisputeInConflict(_dispute)) { - bytes32 relatedDisputeId = _dispute.relatedDisputeId; - Dispute storage relatedDispute = disputes[relatedDisputeId]; - relatedDispute.status = IDisputeManager.DisputeStatus.Cancelled; - return true; - } - return false; + function _rejectDispute(bytes32 _disputeId, Dispute storage _dispute) private { + _dispute.status = IDisputeManager.DisputeStatus.Rejected; + _graphToken().burnTokens(_dispute.deposit); + + emit DisputeRejected(_disputeId, _dispute.indexer, _dispute.fisherman, _dispute.deposit); } - /** - * @notice Pull `disputeDeposit` from fisherman account. - */ - function _pullFishermanDeposit() private { - // Transfer tokens to deposit from fisherman to this contract - _graphToken().pullTokens(msg.sender, disputeDeposit); + function _drawDispute(bytes32 _disputeId, Dispute storage _dispute) private { + _dispute.status = IDisputeManager.DisputeStatus.Drawn; + _graphToken().pushTokens(_dispute.fisherman, _dispute.deposit); + + emit DisputeDrawn(_disputeId, _dispute.indexer, _dispute.fisherman, _dispute.deposit); + } + + function _cancelDispute(bytes32 _disputeId, Dispute storage _dispute) private { + _dispute.status = IDisputeManager.DisputeStatus.Cancelled; + _graphToken().pushTokens(_dispute.fisherman, _dispute.deposit); + + emit DisputeCancelled(_disputeId, _dispute.indexer, _dispute.fisherman, _dispute.deposit); } /** @@ -611,8 +620,10 @@ contract DisputeManager is uint256 _tokensSlash, uint256 _tokensStakeSnapshot ) private returns (uint256) { + ISubgraphService subgraphService_ = _getSubgraphService(); + // Get slashable amount for indexer - IHorizonStaking.Provision memory provision = _graphStaking().getProvision(_indexer, address(subgraphService)); + IHorizonStaking.Provision memory provision = _graphStaking().getProvision(_indexer, address(subgraphService_)); // Ensure slash amount is within the cap uint256 maxTokensSlash = _tokensStakeSnapshot.mulPPM(maxSlashingCut); @@ -626,7 +637,7 @@ contract DisputeManager is uint256 maxRewardableTokens = Math.min(_tokensSlash, provision.tokens); uint256 tokensRewards = uint256(fishermanRewardCut).mulPPM(maxRewardableTokens); - subgraphService.slash(_indexer, abi.encode(_tokensSlash, tokensRewards)); + subgraphService_.slash(_indexer, abi.encode(_tokensSlash, tokensRewards)); return tokensRewards; } @@ -658,7 +669,7 @@ contract DisputeManager is * @param _disputeDeposit The dispute deposit in Graph Tokens */ function _setDisputeDeposit(uint256 _disputeDeposit) private { - require(_disputeDeposit != 0, DisputeManagerInvalidDisputeDeposit(_disputeDeposit)); + require(_disputeDeposit >= MIN_DISPUTE_DEPOSIT, DisputeManagerInvalidDisputeDeposit(_disputeDeposit)); disputeDeposit = _disputeDeposit; emit DisputeDepositSet(_disputeDeposit); } @@ -699,15 +710,23 @@ contract DisputeManager is emit SubgraphServiceSet(_subgraphService); } + /** + * @notice Get the address of the subgraph service + * @dev Will revert if the subgraph service is not set + * @return The subgraph service address + */ + function _getSubgraphService() private view returns (ISubgraphService) { + require(address(subgraphService) != address(0), DisputeManagerSubgraphServiceNotSet()); + return subgraphService; + } + /** * @notice Returns whether the dispute is for a conflicting attestation or not. * @param _dispute Dispute * @return True conflicting attestation dispute */ - function _isDisputeInConflict(Dispute memory _dispute) private view returns (bool) { - bytes32 relatedId = _dispute.relatedDisputeId; - // this is so the check returns false when rejecting the related dispute. - return relatedId != 0 && disputes[relatedId].status == IDisputeManager.DisputeStatus.Pending; + function _isDisputeInConflict(Dispute storage _dispute) private view returns (bool) { + return _dispute.relatedDisputeId != bytes32(0); } /** @@ -722,8 +741,9 @@ contract DisputeManager is * @return Total stake snapshot */ function _getStakeSnapshot(address _indexer, uint256 _indexerStake) private view returns (uint256) { - uint256 delegatorsStake = _graphStaking().getDelegationPool(_indexer, address(subgraphService)).tokens; - uint256 delegatorsStakeMax = _indexerStake * uint256(subgraphService.getDelegationRatio()); + ISubgraphService subgraphService_ = _getSubgraphService(); + uint256 delegatorsStake = _graphStaking().getDelegationPool(_indexer, address(subgraphService_)).tokens; + uint256 delegatorsStakeMax = _indexerStake * uint256(subgraphService_.getDelegationRatio()); uint256 stakeSnapshot = _indexerStake + MathUtils.min(delegatorsStake, delegatorsStakeMax); return stakeSnapshot; } diff --git a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol index 95511ecc2..ee2e92ac2 100644 --- a/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol +++ b/packages/subgraph-service/contracts/interfaces/IDisputeManager.sol @@ -169,6 +169,8 @@ interface IDisputeManager { error DisputeManagerDisputeNotPending(IDisputeManager.DisputeStatus status); error DisputeManagerDisputeAlreadyCreated(bytes32 disputeId); error DisputeManagerDisputePeriodNotFinished(); + error DisputeManagerDisputeInConflict(bytes32 disputeId); + error DisputeManagerDisputeNotInConflict(bytes32 disputeId); error DisputeManagerMustAcceptRelatedDispute(bytes32 disputeId, bytes32 relatedDisputeId); error DisputeManagerIndexerNotFound(address allocationId); error DisputeManagerNonMatchingSubgraphDeployment(bytes32 subgraphDeploymentId1, bytes32 subgraphDeploymentId2); @@ -180,6 +182,7 @@ interface IDisputeManager { bytes32 responseCID2, bytes32 subgraphDeploymentId2 ); + error DisputeManagerSubgraphServiceNotSet(); function setDisputePeriod(uint64 disputePeriod) external; @@ -204,6 +207,13 @@ interface IDisputeManager { function acceptDispute(bytes32 disputeId, uint256 tokensSlash) external; + function acceptDisputeConflict( + bytes32 disputeId, + uint256 tokensSlash, + bool acceptDisputeInConflict, + uint256 tokensSlashRelated + ) external; + function rejectDispute(bytes32 disputeId) external; function drawDispute(bytes32 disputeId) external; diff --git a/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol b/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol index 0d99aec0b..0349fe7da 100644 --- a/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol +++ b/packages/subgraph-service/test/disputeManager/DisputeManager.t.sol @@ -3,8 +3,6 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"; import { Attestation } from "../../contracts/libraries/Attestation.sol"; @@ -26,7 +24,7 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { vm.stopPrank(); } - modifier useFisherman { + modifier useFisherman() { vm.startPrank(users.fisherman); _; vm.stopPrank(); @@ -64,6 +62,13 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { assertEq(disputeManager.disputeDeposit(), _disputeDeposit, "Dispute deposit should be set."); } + function _setSubgraphService(address _subgraphService) internal { + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.SubgraphServiceSet(_subgraphService); + disputeManager.setSubgraphService(_subgraphService); + assertEq(address(disputeManager.subgraphService()), _subgraphService, "Subgraph service should be set."); + } + function _createIndexingDispute(address _allocationId, bytes32 _poi) internal returns (bytes32) { (, address fisherman, ) = vm.readCallers(); bytes32 expectedDisputeId = keccak256(abi.encodePacked(_allocationId, _poi)); @@ -88,7 +93,7 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { // 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"); @@ -99,20 +104,32 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { 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( + 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"); + assertEq( + afterFishermanBalance, + beforeFishermanBalance - disputeDeposit, + "Fisherman should be charged the dispute deposit" + ); return _disputeId; } function _createQueryDispute(bytes memory _attestationData) internal returns (bytes32) { - (, address fisherman,) = vm.readCallers(); + (, address fisherman, ) = vm.readCallers(); Attestation.State memory attestation = Attestation.parse(_attestationData); address indexer = disputeManager.getAttestationIndexer(attestation); bytes32 expectedDisputeId = keccak256( @@ -130,7 +147,7 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { // Approve the dispute deposit token.approve(address(disputeManager), disputeDeposit); - + vm.expectEmit(address(disputeManager)); emit IDisputeManager.QueryDisputeCreated( expectedDisputeId, @@ -141,9 +158,9 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { _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"); @@ -154,15 +171,27 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { 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( + 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"); - + assertEq( + afterFishermanBalance, + beforeFishermanBalance - disputeDeposit, + "Fisherman should be charged the dispute deposit" + ); + return _disputeID; } @@ -174,11 +203,12 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { uint256 stakeSnapshot1; uint256 stakeSnapshot2; } + function _createQueryDisputeConflict( bytes memory attestationData1, bytes memory attestationData2 ) internal returns (bytes32, bytes32) { - (, address fisherman,) = vm.readCallers(); + (, address fisherman, ) = vm.readCallers(); BeforeValues_CreateQueryDisputeConflict memory beforeValues; beforeValues.attestation1 = Attestation.parse(attestationData1); @@ -188,6 +218,11 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { beforeValues.stakeSnapshot1 = disputeManager.getStakeSnapshot(beforeValues.indexer1); beforeValues.stakeSnapshot2 = disputeManager.getStakeSnapshot(beforeValues.indexer2); + uint256 beforeFishermanBalance = token.balanceOf(fisherman); + + // Approve the dispute deposit + token.approve(address(disputeManager), disputeDeposit); + bytes32 expectedDisputeId1 = keccak256( abi.encodePacked( beforeValues.attestation1.requestCID, @@ -213,7 +248,7 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { expectedDisputeId1, beforeValues.indexer1, fisherman, - 0, + disputeDeposit / 2, beforeValues.attestation1.subgraphDeploymentId, attestationData1, beforeValues.stakeSnapshot1 @@ -223,13 +258,16 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { expectedDisputeId2, beforeValues.indexer2, fisherman, - 0, + disputeDeposit / 2, beforeValues.attestation2.subgraphDeploymentId, attestationData2, beforeValues.stakeSnapshot2 ); - (bytes32 _disputeId1, bytes32 _disputeId2) = disputeManager.createQueryDisputeConflict(attestationData1, attestationData2); + (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."); @@ -241,23 +279,47 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { IDisputeManager.Dispute memory dispute1 = _getDispute(_disputeId1); assertEq(dispute1.indexer, beforeValues.indexer1, "Indexer 1 should match"); assertEq(dispute1.fisherman, fisherman, "Fisherman 1 should match"); - assertEq(dispute1.deposit, 0, "Deposit 1 should match"); + assertEq(dispute1.deposit, disputeDeposit / 2, "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( + 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, beforeValues.stakeSnapshot1, "Stake snapshot 1 should match"); IDisputeManager.Dispute memory dispute2 = _getDispute(_disputeId2); assertEq(dispute2.indexer, beforeValues.indexer2, "Indexer 2 should match"); assertEq(dispute2.fisherman, fisherman, "Fisherman 2 should match"); - assertEq(dispute2.deposit, 0, "Deposit 2 should match"); + assertEq(dispute2.deposit, disputeDeposit / 2, "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( + 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, beforeValues.stakeSnapshot2, "Stake snapshot 2 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" + ); + return (_disputeId1, _disputeId2); } @@ -271,14 +333,25 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { uint256 fishermanReward = _tokensSlash.mulPPM(fishermanRewardPercentage); vm.expectEmit(address(disputeManager)); - emit IDisputeManager.DisputeAccepted(_disputeId, dispute.indexer, dispute.fisherman, dispute.deposit + fishermanReward); + 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"); + 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; @@ -287,21 +360,157 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { } else { expectedIndexerTokensAvailable = indexerTokensAvailable - _tokensSlash; } - assertEq(staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), expectedIndexerTokensAvailable, "Indexer should be slashed by the correct amount"); + 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"); + 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"); + struct FishermanParams { + address fisherman; + uint256 previousBalance; + uint256 disputeDeposit; + uint256 relatedDisputeDeposit; + uint256 rewardPercentage; + uint256 rewardFirstDispute; + uint256 rewardRelatedDispute; + uint256 totalReward; + uint256 expectedBalance; + } + + function _acceptDisputeConflict(bytes32 _disputeId, uint256 _tokensSlash, bool _acceptRelatedDispute, uint256 _tokensRelatedSlash) internal { + IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + IDisputeManager.Dispute memory relatedDispute = _getDispute(dispute.relatedDisputeId); + uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); + uint256 relatedIndexerTokensAvailable = staking.getProviderTokensAvailable(relatedDispute.indexer, address(subgraphService)); + + FishermanParams memory params; + params.fisherman = dispute.fisherman; + params.previousBalance = token.balanceOf(params.fisherman); + params.disputeDeposit = dispute.deposit; + params.relatedDisputeDeposit = relatedDispute.deposit; + params.rewardPercentage = disputeManager.fishermanRewardCut(); + params.rewardFirstDispute = _tokensSlash.mulPPM(params.rewardPercentage); + params.rewardRelatedDispute = (_acceptRelatedDispute) ? _tokensRelatedSlash.mulPPM(params.rewardPercentage) : 0; + params.totalReward = params.rewardFirstDispute + params.rewardRelatedDispute; + + vm.expectEmit(address(disputeManager)); + emit IDisputeManager.DisputeAccepted( + _disputeId, + dispute.indexer, + params.fisherman, + params.disputeDeposit + params.rewardFirstDispute + ); + + if (_acceptRelatedDispute) { + emit IDisputeManager.DisputeAccepted( + dispute.relatedDisputeId, + relatedDispute.indexer, + relatedDispute.fisherman, + relatedDispute.deposit + params.rewardRelatedDispute + ); + } else { + emit IDisputeManager.DisputeDrawn( + dispute.relatedDisputeId, + relatedDispute.indexer, + relatedDispute.fisherman, + relatedDispute.deposit + ); } + + // Accept the dispute + disputeManager.acceptDisputeConflict(_disputeId, _tokensSlash, _acceptRelatedDispute, _tokensRelatedSlash); + + // Check fisherman's got their reward and their deposit back + params.expectedBalance = params.previousBalance + + params.totalReward + + params.disputeDeposit + + params.relatedDisputeDeposit; + assertEq( + token.balanceOf(params.fisherman), + params.expectedBalance, + "Fisherman should get their reward and deposit back" + ); + + // If both disputes are for the same indexer, check that the indexer was slashed by the correct amount + if (dispute.indexer == relatedDispute.indexer) { + uint256 tokensToSlash = (_acceptRelatedDispute) ? _tokensSlash + _tokensRelatedSlash : _tokensSlash; + uint256 expectedIndexerTokensAvailable; + if (tokensToSlash > indexerTokensAvailable) { + expectedIndexerTokensAvailable = 0; + } else { + expectedIndexerTokensAvailable = indexerTokensAvailable - tokensToSlash; + } + assertEq( + staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), + expectedIndexerTokensAvailable, + "Indexer should be slashed by the correct amount" + ); + } else { + // Check indexer for first dispute was slashed by the correct amount + uint256 expectedIndexerTokensAvailable; + uint256 tokensToSlash = (_acceptRelatedDispute) ? _tokensSlash : _tokensSlash; + if (tokensToSlash > indexerTokensAvailable) { + expectedIndexerTokensAvailable = 0; + } else { + expectedIndexerTokensAvailable = indexerTokensAvailable - tokensToSlash; + } + assertEq( + staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), + expectedIndexerTokensAvailable, + "Indexer should be slashed by the correct amount" + ); + + // Check indexer for related dispute was slashed by the correct amount if it was accepted + if (_acceptRelatedDispute) { + uint256 expectedRelatedIndexerTokensAvailable; + if (_tokensRelatedSlash > relatedIndexerTokensAvailable) { + expectedRelatedIndexerTokensAvailable = 0; + } else { + expectedRelatedIndexerTokensAvailable = relatedIndexerTokensAvailable - _tokensRelatedSlash; + } + assertEq( + staking.getProviderTokensAvailable(relatedDispute.indexer, address(subgraphService)), + expectedRelatedIndexerTokensAvailable, + "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 it + relatedDispute = _getDispute(dispute.relatedDisputeId); + assertEq( + uint8(relatedDispute.status), + _acceptRelatedDispute + ? uint8(IDisputeManager.DisputeStatus.Accepted) + : uint8(IDisputeManager.DisputeStatus.Drawn), + "Related dispute status should be drawn" + ); } function _drawDispute(bytes32 _disputeId) internal { IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); + bool isConflictingDispute = dispute.relatedDisputeId != bytes32(0); + IDisputeManager.Dispute memory relatedDispute; + if (isConflictingDispute) relatedDispute = _getDispute(dispute.relatedDisputeId); address fisherman = dispute.fisherman; uint256 fishermanPreviousBalance = token.balanceOf(fisherman); uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)); @@ -309,15 +518,29 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { vm.expectEmit(address(disputeManager)); emit IDisputeManager.DisputeDrawn(_disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); + if (isConflictingDispute) { + emit IDisputeManager.DisputeDrawn( + dispute.relatedDisputeId, + relatedDispute.indexer, + relatedDispute.fisherman, + relatedDispute.deposit + ); + } // Draw the dispute disputeManager.drawDispute(_disputeId); // Check that the fisherman got their deposit back - uint256 fishermanExpectedBalance = fishermanPreviousBalance + dispute.deposit; + uint256 fishermanExpectedBalance = fishermanPreviousBalance + + dispute.deposit + + (isConflictingDispute ? relatedDispute.deposit : 0); 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"); + assertEq( + staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService)), + indexerTokensAvailable, + "Indexer should not be slashed" + ); // Check dispute status dispute = _getDispute(_disputeId); @@ -325,8 +548,12 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { // 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"); + relatedDispute = _getDispute(dispute.relatedDisputeId); + assertEq( + uint8(relatedDispute.status), + uint8(IDisputeManager.DisputeStatus.Drawn), + "Related dispute status should be drawn" + ); } } @@ -346,17 +573,28 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { 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"); + 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"); + 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); + bool isDisputeInConflict = dispute.relatedDisputeId != bytes32(0); + IDisputeManager.Dispute memory relatedDispute; + if (isDisputeInConflict) relatedDispute = _getDispute(dispute.relatedDisputeId); address fisherman = dispute.fisherman; uint256 fishermanPreviousBalance = token.balanceOf(fisherman); uint256 disputePeriod = disputeManager.disputePeriod(); @@ -368,24 +606,50 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { vm.expectEmit(address(disputeManager)); emit IDisputeManager.DisputeCancelled(_disputeId, dispute.indexer, dispute.fisherman, dispute.deposit); + if (isDisputeInConflict) { + emit IDisputeManager.DisputeCancelled( + dispute.relatedDisputeId, + relatedDispute.indexer, + relatedDispute.fisherman, + relatedDispute.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."); + uint256 fishermanExpectedBalance = fishermanPreviousBalance + + dispute.deposit + + (isDisputeInConflict ? relatedDispute.deposit : 0); + 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"); + 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"); + 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"); + if (isDisputeInConflict) { + relatedDispute = _getDispute(dispute.relatedDisputeId); + assertEq( + uint8(relatedDispute.status), + uint8(IDisputeManager.DisputeStatus.Cancelled), + "Related dispute status should be cancelled" + ); } } @@ -398,11 +662,12 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { bytes32 responseCID, bytes32 subgraphDeploymentId ) internal pure returns (Attestation.Receipt memory receipt) { - return Attestation.Receipt({ - requestCID: requestCID, - responseCID: responseCID, - subgraphDeploymentId: subgraphDeploymentId - }); + return + Attestation.Receipt({ + requestCID: requestCID, + responseCID: responseCID, + subgraphDeploymentId: subgraphDeploymentId + }); } function _createConflictingAttestations( @@ -446,15 +711,20 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { 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 - }); + return + IDisputeManager.Dispute({ + indexer: indexer, + fisherman: fisherman, + deposit: deposit, + relatedDisputeId: relatedDisputeId, + disputeType: disputeType, + status: status, + createdAt: createdAt, + stakeSnapshot: stakeSnapshot + }); + } + + function _setStorage_SubgraphService(address _subgraphService) internal { + vm.store(address(disputeManager), bytes32(uint256(51)), bytes32(uint256(uint160(_subgraphService)))); } } diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/accept.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/accept.t.sol index 49bee9e26..f1d1dc24f 100644 --- a/packages/subgraph-service/test/disputeManager/disputes/indexing/accept.t.sol +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/accept.t.sol @@ -27,6 +27,36 @@ contract DisputeManagerIndexingAcceptDisputeTest is DisputeManagerTest { _acceptDispute(disputeID, tokensSlash); } + function test_Indexing_Accept_Dispute_RevertWhen_SubgraphServiceNotSet( + 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); + // clear subgraph service address from storage + _setStorage_SubgraphService(address(0)); + + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerSubgraphServiceNotSet.selector)); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Indexing_Accept_Dispute_OptParam( + 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 diff --git a/packages/subgraph-service/test/disputeManager/disputes/indexing/create.t.sol b/packages/subgraph-service/test/disputeManager/disputes/indexing/create.t.sol index 8d1b75c21..3b1df5011 100644 --- a/packages/subgraph-service/test/disputeManager/disputes/indexing/create.t.sol +++ b/packages/subgraph-service/test/disputeManager/disputes/indexing/create.t.sol @@ -7,16 +7,28 @@ import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManage import { DisputeManagerTest } from "../../DisputeManager.t.sol"; contract DisputeManagerIndexingCreateDisputeTest is DisputeManagerTest { - /* * TESTS */ - function test_Indexing_Create_Dispute( + function test_Indexing_Create_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + _createIndexingDispute(allocationID, bytes32("POI1")); + } + + function test_Indexing_Create_Dispute_RevertWhen_SubgraphServiceNotSet( uint256 tokens ) public useIndexer useAllocation(tokens) { resetPrank(users.fisherman); - _createIndexingDispute(allocationID, bytes32("POI1")); + + // clear subgraph service address from storage + _setStorage_SubgraphService(address(0)); + + // // Approve the dispute deposit + token.approve(address(disputeManager), disputeDeposit); + + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerSubgraphServiceNotSet.selector)); + disputeManager.createIndexingDispute(allocationID, bytes32("POI2")); } function test_Indexing_Create_MultipleDisputes() public { @@ -33,7 +45,12 @@ contract DisputeManagerIndexingCreateDisputeTest is DisputeManagerTest { _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); + bytes memory data = _createSubgraphAllocationData( + indexer, + subgraphDeployment, + allocationIDPrivateKey, + tokens + ); _startService(indexer, data); allocationIDPrivateKeys[i] = allocationIDPrivateKey; } @@ -48,7 +65,7 @@ contract DisputeManagerIndexingCreateDisputeTest is DisputeManagerTest { uint256 tokens ) public useIndexer useAllocation(tokens) { resetPrank(users.fisherman); - bytes32 disputeID =_createIndexingDispute(allocationID, bytes32("POI1")); + bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1")); // Create another dispute with different fisherman address otherFisherman = makeAddr("otherFisherman"); @@ -78,9 +95,7 @@ contract DisputeManagerIndexingCreateDisputeTest is DisputeManagerTest { vm.stopPrank(); } - function test_Indexing_Create_RevertIf_AllocationDoesNotExist( - uint256 tokens - ) public useFisherman { + 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( @@ -92,9 +107,7 @@ contract DisputeManagerIndexingCreateDisputeTest is DisputeManagerTest { vm.stopPrank(); } - function test_Indexing_Create_RevertIf_IndexerIsBelowStake( - uint256 tokens - ) public useIndexer useAllocation(tokens) { + function test_Indexing_Create_RevertIf_IndexerIsBelowStake(uint256 tokens) public useIndexer useAllocation(tokens) { // Close allocation bytes memory data = abi.encode(allocationID); _stopService(users.indexer, data); diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol index 7670d381e..7262dadb9 100644 --- a/packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol +++ b/packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol @@ -34,6 +34,39 @@ contract DisputeManagerQueryAcceptDisputeTest is DisputeManagerTest { _acceptDispute(disputeID, tokensSlash); } + function test_Query_Accept_Dispute_RevertWhen_SubgraphServiceNotSet( + 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); + // clear subgraph service address from storage + _setStorage_SubgraphService(address(0)); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerSubgraphServiceNotSet.selector)); + disputeManager.acceptDispute(disputeID, tokensSlash); + } + + function test_Query_Accept_Dispute_OptParam( + 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 @@ -72,4 +105,23 @@ contract DisputeManagerQueryAcceptDisputeTest is DisputeManagerTest { vm.expectRevert(expectedError); disputeManager.acceptDispute(disputeID, tokensSlash); } + + function test_Query_Accept_RevertWhen_UsingConflictAccept( + 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); + vm.expectRevert(abi.encodeWithSelector( + IDisputeManager.DisputeManagerDisputeNotInConflict.selector, + disputeID + )); + disputeManager.acceptDisputeConflict(disputeID, tokensSlash, true, 0); + } } diff --git a/packages/subgraph-service/test/disputeManager/disputes/query/create.t.sol b/packages/subgraph-service/test/disputeManager/disputes/query/create.t.sol index 4eba11744..3dd4f7bbf 100644 --- a/packages/subgraph-service/test/disputeManager/disputes/query/create.t.sol +++ b/packages/subgraph-service/test/disputeManager/disputes/query/create.t.sol @@ -17,13 +17,28 @@ contract DisputeManagerQueryCreateDisputeTest is DisputeManagerTest { * TESTS */ - function test_Query_Create_Dispute(uint256 tokens) public useIndexer useAllocation(tokens) { + function test_Query_Create_Dispute_Only(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_Dispute_RevertWhen_SubgraphServiceNotSet(uint256 tokens) public useIndexer useAllocation(tokens) { + resetPrank(users.fisherman); + Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId); + bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey); + + // clear subgraph service address from storage + _setStorage_SubgraphService(address(0)); + + // // Approve the dispute deposit + token.approve(address(disputeManager), disputeDeposit); + + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerSubgraphServiceNotSet.selector)); + disputeManager.createQueryDispute(attestationData); + } + function test_Query_Create_MultipleDisputes_DifferentFisherman( uint256 tokens ) public useIndexer useAllocation(tokens) { diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/accept.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/accept.t.sol index 46a6d4bdd..f145277af 100644 --- a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/accept.t.sol +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/accept.t.sol @@ -18,7 +18,7 @@ contract DisputeManagerQueryConflictAcceptDisputeTest is DisputeManagerTest { * TESTS */ - function test_Query_Conflict_Accept_Dispute( + function test_Query_Conflict_Accept_Dispute_Draw_Other( uint256 tokens, uint256 tokensSlash ) public useIndexer useAllocation(tokens) { @@ -33,11 +33,53 @@ contract DisputeManagerQueryConflictAcceptDisputeTest is DisputeManagerTest { allocationIDPrivateKey ); + uint256 fishermanBalanceBefore = token.balanceOf(users.fisherman); + resetPrank(users.fisherman); - (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); resetPrank(users.arbitrator); - _acceptDispute(disputeID1, tokensSlash); + _acceptDisputeConflict(disputeID1, tokensSlash, false, 0); + + uint256 fishermanRewardPercentage = disputeManager.fishermanRewardCut(); + uint256 fishermanReward = tokensSlash.mulPPM(fishermanRewardPercentage); + uint256 fishermanBalanceAfter = token.balanceOf(users.fisherman); + + assertEq(fishermanBalanceAfter, fishermanBalanceBefore + fishermanReward); + } + + function test_Query_Conflict_Accept_Dispute_Accept_Other( + uint256 tokens, + uint256 tokensSlash, + uint256 tokensSlashRelatedDispute + ) public useIndexer useAllocation(tokens) { + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + tokensSlashRelatedDispute = bound(tokensSlashRelatedDispute, 1, uint256(maxSlashingPercentage).mulPPM(tokens)); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + allocationIDPrivateKey + ); + + uint256 fishermanBalanceBefore = token.balanceOf(users.fisherman); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + _acceptDisputeConflict(disputeID1, tokensSlash, true, tokensSlashRelatedDispute); + + uint256 fishermanRewardPercentage = disputeManager.fishermanRewardCut(); + uint256 fishermanRewardFirstDispute = tokensSlash.mulPPM(fishermanRewardPercentage); + uint256 fishermanRewardRelatedDispute = tokensSlashRelatedDispute.mulPPM(fishermanRewardPercentage); + uint256 fishermanReward = fishermanRewardFirstDispute + fishermanRewardRelatedDispute; + uint256 fishermanBalanceAfter = token.balanceOf(users.fisherman); + + assertEq(fishermanBalanceAfter, fishermanBalanceBefore + fishermanReward); } function test_Query_Conflict_Accept_RevertIf_CallerIsNotArbitrator( @@ -56,12 +98,12 @@ contract DisputeManagerQueryConflictAcceptDisputeTest is DisputeManagerTest { ); resetPrank(users.fisherman); - (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + (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); + disputeManager.acceptDisputeConflict(disputeID1, tokensSlash, false, 0); } function test_Query_Conflict_Accept_RevertWhen_SlashingOverMaxSlashPercentage( @@ -80,17 +122,85 @@ contract DisputeManagerQueryConflictAcceptDisputeTest is DisputeManagerTest { ); resetPrank(users.fisherman); - (bytes32 disputeID1,) = _createQueryDisputeConflict(attestationData1, attestationData2); + (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, + IDisputeManager.DisputeManagerInvalidTokensSlash.selector, tokensSlash, maxTokensToSlash ); vm.expectRevert(expectedError); + disputeManager.acceptDisputeConflict(disputeID1, tokensSlash, false, 0); + } + + function test_Query_Conflict_Accept_AcceptRelated_DifferentIndexer( + uint256 tokensFirstIndexer, + uint256 tokensSecondIndexer, + uint256 tokensSlash, + uint256 tokensSlashRelatedDispute + ) public useIndexer useAllocation(tokensFirstIndexer) { + tokensSecondIndexer = bound(tokensSecondIndexer, minimumProvisionTokens, 10_000_000_000 ether); + tokensSlash = bound(tokensSlash, 1, uint256(maxSlashingPercentage).mulPPM(tokensFirstIndexer)); + + // Setup different indexer for related dispute + address differentIndexer = makeAddr("DifferentIndexer"); + mint(differentIndexer, tokensSecondIndexer); + uint256 differentIndexerAllocationIDPrivateKey = uint256(keccak256(abi.encodePacked(differentIndexer))); + resetPrank(differentIndexer); + _createProvision(differentIndexer, tokensSecondIndexer, maxSlashingPercentage, disputePeriod); + _register(differentIndexer, abi.encode("url", "geoHash", address(0))); + bytes memory data = _createSubgraphAllocationData( + differentIndexer, + subgraphDeployment, + differentIndexerAllocationIDPrivateKey, + tokensSecondIndexer + ); + _startService(differentIndexer, data); + tokensSlashRelatedDispute = bound( + tokensSlashRelatedDispute, + 1, + uint256(maxSlashingPercentage).mulPPM(tokensSecondIndexer) + ); + + (bytes memory attestationData1, bytes memory attestationData2) = _createConflictingAttestations( + requestCID, + subgraphDeployment, + responseCID1, + responseCID2, + allocationIDPrivateKey, + differentIndexerAllocationIDPrivateKey + ); + + resetPrank(users.fisherman); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); + + resetPrank(users.arbitrator); + _acceptDisputeConflict(disputeID1, tokensSlash, true, tokensSlashRelatedDispute); + } + + function test_Query_Conflict_Accept_RevertWhen_UsingSingleAccept( + 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); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputeInConflict.selector, disputeID1)); disputeManager.acceptDispute(disputeID1, tokensSlash); } } diff --git a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/reject.t.sol b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/reject.t.sol index ff347f7d2..3b56a05d8 100644 --- a/packages/subgraph-service/test/disputeManager/disputes/queryConflict/reject.t.sol +++ b/packages/subgraph-service/test/disputeManager/disputes/queryConflict/reject.t.sol @@ -7,14 +7,11 @@ import { IDisputeManager } from "../../../../contracts/interfaces/IDisputeManage import { DisputeManagerTest } from "../../DisputeManager.t.sol"; contract DisputeManagerQueryConflictRejectDisputeTest is DisputeManagerTest { - /* * TESTS */ - function test_Query_Conflict_Reject_Revert( - uint256 tokens - ) public useIndexer useAllocation(tokens) { + 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")); @@ -29,14 +26,10 @@ contract DisputeManagerQueryConflictRejectDisputeTest is DisputeManagerTest { ); resetPrank(users.fisherman); - (bytes32 disputeID1, bytes32 disputeID2) = _createQueryDisputeConflict(attestationData1, attestationData2); + (bytes32 disputeID1, ) = _createQueryDisputeConflict(attestationData1, attestationData2); resetPrank(users.arbitrator); - vm.expectRevert(abi.encodeWithSelector( - IDisputeManager.DisputeManagerMustAcceptRelatedDispute.selector, - disputeID1, - disputeID2 - )); + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputeInConflict.selector, disputeID1)); disputeManager.rejectDispute(disputeID1); } } diff --git a/packages/subgraph-service/test/disputeManager/governance/disputeDeposit.t.sol b/packages/subgraph-service/test/disputeManager/governance/disputeDeposit.t.sol index 1df13acb2..bfd97f731 100644 --- a/packages/subgraph-service/test/disputeManager/governance/disputeDeposit.t.sol +++ b/packages/subgraph-service/test/disputeManager/governance/disputeDeposit.t.sol @@ -14,12 +14,12 @@ contract DisputeManagerGovernanceDisputeDepositTest is DisputeManagerTest { */ function test_Governance_SetDisputeDeposit(uint256 disputeDeposit) public useGovernor { - vm.assume(disputeDeposit > 0); + vm.assume(disputeDeposit >= MIN_DISPUTE_DEPOSIT); _setDisputeDeposit(disputeDeposit); } - function test_Governance_RevertWhen_ZeroValue() public useGovernor { - uint256 disputeDeposit = 0; + function test_Governance_RevertWhen_DepositTooLow(uint256 disputeDeposit) public useGovernor { + vm.assume(disputeDeposit < MIN_DISPUTE_DEPOSIT); vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidDisputeDeposit.selector, disputeDeposit)); disputeManager.setDisputeDeposit(disputeDeposit); } diff --git a/packages/subgraph-service/test/disputeManager/governance/subgraphService.t.sol b/packages/subgraph-service/test/disputeManager/governance/subgraphService.t.sol new file mode 100644 index 000000000..8f39d48c3 --- /dev/null +++ b/packages/subgraph-service/test/disputeManager/governance/subgraphService.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IDisputeManager } from "../../../contracts/interfaces/IDisputeManager.sol"; +import { DisputeManagerTest } from "../DisputeManager.t.sol"; + +contract DisputeManagerGovernanceSubgraphService is DisputeManagerTest { + + /* + * TESTS + */ + + function test_Governance_SetSubgraphService(address subgraphService) public useGovernor { + vm.assume(subgraphService != address(0)); + _setSubgraphService(subgraphService); + } + + function test_Governance_SetSubgraphService_RevertWhenZero() public useGovernor { + vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerInvalidZeroAddress.selector)); + disputeManager.setSubgraphService(address(0)); + } +} diff --git a/packages/subgraph-service/test/mocks/MockGRTToken.sol b/packages/subgraph-service/test/mocks/MockGRTToken.sol index 7d21fd00a..c54f4e24c 100644 --- a/packages/subgraph-service/test/mocks/MockGRTToken.sol +++ b/packages/subgraph-service/test/mocks/MockGRTToken.sol @@ -7,7 +7,9 @@ import "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; contract MockGRTToken is ERC20, IGraphToken { constructor() ERC20("Graph Token", "GRT") {} - function burn(uint256 amount) external {} + function burn(uint256 amount) external { + _burn(msg.sender, amount); + } function burnFrom(address _from, uint256 amount) external { _burn(_from, amount); diff --git a/packages/subgraph-service/test/utils/Constants.sol b/packages/subgraph-service/test/utils/Constants.sol index 025396ea8..0c6419f3e 100644 --- a/packages/subgraph-service/test/utils/Constants.sol +++ b/packages/subgraph-service/test/utils/Constants.sol @@ -7,6 +7,7 @@ abstract contract Constants { uint256 internal constant EPOCH_LENGTH = 1; // Dispute Manager uint64 internal constant disputePeriod = 7 days; + uint256 internal constant MIN_DISPUTE_DEPOSIT = 1 ether; // 1 GRT uint256 internal constant disputeDeposit = 100 ether; // 100 GRT uint32 internal constant fishermanRewardPercentage = 500000; // 50% uint32 internal constant maxSlashingPercentage = 500000; // 50%