Skip to content
273 changes: 158 additions & 115 deletions contracts/disputes/DisputeManager.sol

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion contracts/disputes/DisputeManagerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ contract DisputeManagerV1Storage is Managed {
// Minimum deposit required to create a Dispute
uint256 public minimumDeposit;

// -- Slot 0xf
// Percentage of indexer slashed funds to assign as a reward to fisherman in successful dispute
// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%)
uint32 public fishermanRewardPercentage;

// Percentage of indexer stake to slash on disputes
// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%)
uint32 public slashingPercentage;
uint32 public qrySlashingPercentage;
uint32 public idxSlashingPercentage;

// -- Slot 0x10
// Disputes created : disputeID => Dispute
// disputeID - check creation functions to see how disputeID is built
mapping(bytes32 => IDisputeManager.Dispute) public disputes;
Expand Down
9 changes: 4 additions & 5 deletions contracts/disputes/IDisputeManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ pragma experimental ABIEncoderV2;
interface IDisputeManager {
// -- Dispute --

enum DisputeType { Null, IndexingDispute, QueryDispute }

// Disputes contain info necessary for the Arbitrator to verify and resolve
struct Dispute {
address indexer;
address fisherman;
uint256 deposit;
bytes32 relatedDisputeID;
DisputeType disputeType;
}

// -- Attestation --
Expand Down Expand Up @@ -41,7 +44,7 @@ interface IDisputeManager {

function setFishermanRewardPercentage(uint32 _percentage) external;

function setSlashingPercentage(uint32 _percentage) external;
function setSlashingPercentage(uint32 _qryPercentage, uint32 _idxPercentage) external;

// -- Getters --

Expand All @@ -56,10 +59,6 @@ interface IDisputeManager {

function getAttestationIndexer(Attestation memory _attestation) external view returns (address);

function getTokensToReward(address _indexer) external view returns (uint256);

function getTokensToSlash(address _indexer) external view returns (uint256);

// -- Dispute --

function createQueryDispute(bytes calldata _attestationData, uint256 _deposit)
Expand Down
42 changes: 42 additions & 0 deletions test/disputes/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { utils } from 'ethers'
import { Attestation, Receipt } from '@graphprotocol/common-ts'

export const MAX_PPM = 1000000

const { defaultAbiCoder: abi, arrayify, concat, hexlify, solidityKeccak256, joinSignature } = utils

export interface Dispute {
id: string
attestation: Attestation
encodedAttestation: string
indexerAddress: string
receipt: Receipt
}

export function createQueryDisputeID(
attestation: Attestation,
indexerAddress: string,
submitterAddress: string,
): string {
return solidityKeccak256(
['bytes32', 'bytes32', 'bytes32', 'address', 'address'],
[
attestation.requestCID,
attestation.responseCID,
attestation.subgraphDeploymentID,
indexerAddress,
submitterAddress,
],
)
}

export function encodeAttestation(attestation: Attestation): string {
const data = arrayify(
abi.encode(
['bytes32', 'bytes32', 'bytes32'],
[attestation.requestCID, attestation.responseCID, attestation.subgraphDeploymentID],
),
)
const sig = joinSignature(attestation)
return hexlify(concat([data, sig]))
}
21 changes: 14 additions & 7 deletions test/disputes/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,23 +103,30 @@ describe('DisputeManager:Config', () => {

describe('slashingPercentage', function () {
it('should set `slashingPercentage`', async function () {
const newValue = defaults.dispute.slashingPercentage
const qryNewValue = defaults.dispute.qrySlashingPercentage
const idxNewValue = defaults.dispute.idxSlashingPercentage

// Set right in the constructor
expect(await disputeManager.slashingPercentage()).eq(newValue)
expect(await disputeManager.qrySlashingPercentage()).eq(qryNewValue)
expect(await disputeManager.idxSlashingPercentage()).eq(idxNewValue)

// Set new value
await disputeManager.connect(governor.signer).setSlashingPercentage(0)
await disputeManager.connect(governor.signer).setSlashingPercentage(newValue)
await disputeManager.connect(governor.signer).setSlashingPercentage(0, 0)
await disputeManager
.connect(governor.signer)
.setSlashingPercentage(qryNewValue, idxNewValue)
})

it('reject set `slashingPercentage` if out of bounds', async function () {
const tx = disputeManager.connect(governor.signer).setSlashingPercentage(MAX_PPM + 1)
await expect(tx).revertedWith('Slashing percentage must be below or equal to MAX_PPM')
const tx1 = disputeManager.connect(governor.signer).setSlashingPercentage(0, MAX_PPM + 1)
await expect(tx1).revertedWith('Slashing percentage must be below or equal to MAX_PPM')

const tx2 = disputeManager.connect(governor.signer).setSlashingPercentage(MAX_PPM + 1, 0)
await expect(tx2).revertedWith('Slashing percentage must be below or equal to MAX_PPM')
})

it('reject set `slashingPercentage` if not allowed', async function () {
const tx = disputeManager.connect(me.signer).setSlashingPercentage(50)
const tx = disputeManager.connect(me.signer).setSlashingPercentage(50, 50)
await expect(tx).revertedWith('Caller must be Controller governor')
})
})
Expand Down
54 changes: 54 additions & 0 deletions test/disputes/poi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
Account,
} from '../lib/testHelpers'

import { MAX_PPM } from './common'

const { keccak256 } = utils

describe('DisputeManager:POI', async () => {
Expand Down Expand Up @@ -48,6 +50,16 @@ describe('DisputeManager:POI', async () => {
const metadata = randomHexBytes(32)
const poi = randomHexBytes(32) // proof of indexing

async function calculateSlashConditions(indexerAddress: string) {
const idxSlashingPercentage = await disputeManager.idxSlashingPercentage()
const fishermanRewardPercentage = await disputeManager.fishermanRewardPercentage()
const stakeAmount = await staking.getIndexerStakedTokens(indexerAddress)
const slashAmount = stakeAmount.mul(idxSlashingPercentage).div(toBN(MAX_PPM))
const rewardsAmount = slashAmount.mul(fishermanRewardPercentage).div(toBN(MAX_PPM))

return { slashAmount, rewardsAmount }
}

async function setupIndexers() {
// Dispute manager is allowed to slash
await staking.connect(governor.signer).setSlasher(disputeManager.address, true)
Expand Down Expand Up @@ -180,6 +192,8 @@ describe('DisputeManager:POI', async () => {
})

context('> when dispute is created', function () {
// NOTE: other dispute resolution paths are tested in query.test.ts

beforeEach(async function () {
// Create dispute
await disputeManager
Expand All @@ -193,6 +207,46 @@ describe('DisputeManager:POI', async () => {
.createIndexingDispute(allocationID, fishermanDeposit)
await expect(tx).revertedWith('Dispute already created')
})

describe('accept a dispute', function () {
it('should resolve dispute, slash indexer and reward the fisherman', async function () {
const disputeID = keccak256(allocationID)

// Before state
const beforeIndexerStake = await staking.getIndexerStakedTokens(indexer.address)
const beforeFishermanBalance = await grt.balanceOf(fisherman.address)
const beforeTotalSupply = await grt.totalSupply()

// Calculations
const { slashAmount, rewardsAmount } = await calculateSlashConditions(indexer.address)

// Perform transaction (accept)
const tx = disputeManager.connect(arbitrator.signer).acceptDispute(disputeID)
await expect(tx)
.emit(disputeManager, 'DisputeAccepted')
.withArgs(
disputeID,
indexer.address,
fisherman.address,
fishermanDeposit.add(rewardsAmount),
)

// After state
const afterFishermanBalance = await grt.balanceOf(fisherman.address)
const afterIndexerStake = await staking.getIndexerStakedTokens(indexer.address)
const afterTotalSupply = await grt.totalSupply()

// Fisherman reward properly assigned + deposit returned
expect(afterFishermanBalance).eq(
beforeFishermanBalance.add(fishermanDeposit).add(rewardsAmount),
)
// Indexer slashed
expect(afterIndexerStake).eq(beforeIndexerStake.sub(slashAmount))
// Slashed funds burned
const tokensToBurn = slashAmount.sub(rewardsAmount)
expect(afterTotalSupply).eq(beforeTotalSupply.sub(tokensToBurn))
})
})
})
})
})
Expand Down
Loading