Skip to content

Disputes with separate slashing percentages for queries and indexing #458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 4, 2021
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