Skip to content

Commit c778b8f

Browse files
committed
Disputes with separate slashing percentages for queries and indexing (#458)
Adds a different slashing percentage for Indexing Disputes and Query Disputes - Add a type to the dispute to categorize it when someone creates it. - Create new storage var for a separate indexer and query slashing percentages. - Add setters for each new slashing governance variable. - Pick the right slashing percentage when the dispute is resolved. - Additional minor refactors.
1 parent 9e8a41f commit c778b8f

File tree

8 files changed

+309
-200
lines changed

8 files changed

+309
-200
lines changed

contracts/disputes/DisputeManager.sol

Lines changed: 158 additions & 115 deletions
Large diffs are not rendered by default.

contracts/disputes/DisputeManagerStorage.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ contract DisputeManagerV1Storage is Managed {
1717
// Minimum deposit required to create a Dispute
1818
uint256 public minimumDeposit;
1919

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

2425
// Percentage of indexer stake to slash on disputes
2526
// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%)
26-
uint32 public slashingPercentage;
27+
uint32 public qrySlashingPercentage;
28+
uint32 public idxSlashingPercentage;
2729

30+
// -- Slot 0x10
2831
// Disputes created : disputeID => Dispute
2932
// disputeID - check creation functions to see how disputeID is built
3033
mapping(bytes32 => IDisputeManager.Dispute) public disputes;

contracts/disputes/IDisputeManager.sol

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ pragma experimental ABIEncoderV2;
66
interface IDisputeManager {
77
// -- Dispute --
88

9+
enum DisputeType { Null, IndexingDispute, QueryDispute }
10+
911
// Disputes contain info necessary for the Arbitrator to verify and resolve
1012
struct Dispute {
1113
address indexer;
1214
address fisherman;
1315
uint256 deposit;
1416
bytes32 relatedDisputeID;
17+
DisputeType disputeType;
1518
}
1619

1720
// -- Attestation --
@@ -41,7 +44,7 @@ interface IDisputeManager {
4144

4245
function setFishermanRewardPercentage(uint32 _percentage) external;
4346

44-
function setSlashingPercentage(uint32 _percentage) external;
47+
function setSlashingPercentage(uint32 _qryPercentage, uint32 _idxPercentage) external;
4548

4649
// -- Getters --
4750

@@ -56,10 +59,6 @@ interface IDisputeManager {
5659

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

59-
function getTokensToReward(address _indexer) external view returns (uint256);
60-
61-
function getTokensToSlash(address _indexer) external view returns (uint256);
62-
6362
// -- Dispute --
6463

6564
function createQueryDispute(bytes calldata _attestationData, uint256 _deposit)

test/disputes/common.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { utils } from 'ethers'
2+
import { Attestation, Receipt } from '@graphprotocol/common-ts'
3+
4+
export const MAX_PPM = 1000000
5+
6+
const { defaultAbiCoder: abi, arrayify, concat, hexlify, solidityKeccak256, joinSignature } = utils
7+
8+
export interface Dispute {
9+
id: string
10+
attestation: Attestation
11+
encodedAttestation: string
12+
indexerAddress: string
13+
receipt: Receipt
14+
}
15+
16+
export function createQueryDisputeID(
17+
attestation: Attestation,
18+
indexerAddress: string,
19+
submitterAddress: string,
20+
): string {
21+
return solidityKeccak256(
22+
['bytes32', 'bytes32', 'bytes32', 'address', 'address'],
23+
[
24+
attestation.requestCID,
25+
attestation.responseCID,
26+
attestation.subgraphDeploymentID,
27+
indexerAddress,
28+
submitterAddress,
29+
],
30+
)
31+
}
32+
33+
export function encodeAttestation(attestation: Attestation): string {
34+
const data = arrayify(
35+
abi.encode(
36+
['bytes32', 'bytes32', 'bytes32'],
37+
[attestation.requestCID, attestation.responseCID, attestation.subgraphDeploymentID],
38+
),
39+
)
40+
const sig = joinSignature(attestation)
41+
return hexlify(concat([data, sig]))
42+
}

test/disputes/configuration.test.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,23 +103,30 @@ describe('DisputeManager:Config', () => {
103103

104104
describe('slashingPercentage', function () {
105105
it('should set `slashingPercentage`', async function () {
106-
const newValue = defaults.dispute.slashingPercentage
106+
const qryNewValue = defaults.dispute.qrySlashingPercentage
107+
const idxNewValue = defaults.dispute.idxSlashingPercentage
107108

108109
// Set right in the constructor
109-
expect(await disputeManager.slashingPercentage()).eq(newValue)
110+
expect(await disputeManager.qrySlashingPercentage()).eq(qryNewValue)
111+
expect(await disputeManager.idxSlashingPercentage()).eq(idxNewValue)
110112

111113
// Set new value
112-
await disputeManager.connect(governor.signer).setSlashingPercentage(0)
113-
await disputeManager.connect(governor.signer).setSlashingPercentage(newValue)
114+
await disputeManager.connect(governor.signer).setSlashingPercentage(0, 0)
115+
await disputeManager
116+
.connect(governor.signer)
117+
.setSlashingPercentage(qryNewValue, idxNewValue)
114118
})
115119

116120
it('reject set `slashingPercentage` if out of bounds', async function () {
117-
const tx = disputeManager.connect(governor.signer).setSlashingPercentage(MAX_PPM + 1)
118-
await expect(tx).revertedWith('Slashing percentage must be below or equal to MAX_PPM')
121+
const tx1 = disputeManager.connect(governor.signer).setSlashingPercentage(0, MAX_PPM + 1)
122+
await expect(tx1).revertedWith('Slashing percentage must be below or equal to MAX_PPM')
123+
124+
const tx2 = disputeManager.connect(governor.signer).setSlashingPercentage(MAX_PPM + 1, 0)
125+
await expect(tx2).revertedWith('Slashing percentage must be below or equal to MAX_PPM')
119126
})
120127

121128
it('reject set `slashingPercentage` if not allowed', async function () {
122-
const tx = disputeManager.connect(me.signer).setSlashingPercentage(50)
129+
const tx = disputeManager.connect(me.signer).setSlashingPercentage(50, 50)
123130
await expect(tx).revertedWith('Caller must be Controller governor')
124131
})
125132
})

test/disputes/poi.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
Account,
1919
} from '../lib/testHelpers'
2020

21+
import { MAX_PPM } from './common'
22+
2123
const { keccak256 } = utils
2224

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

53+
async function calculateSlashConditions(indexerAddress: string) {
54+
const idxSlashingPercentage = await disputeManager.idxSlashingPercentage()
55+
const fishermanRewardPercentage = await disputeManager.fishermanRewardPercentage()
56+
const stakeAmount = await staking.getIndexerStakedTokens(indexerAddress)
57+
const slashAmount = stakeAmount.mul(idxSlashingPercentage).div(toBN(MAX_PPM))
58+
const rewardsAmount = slashAmount.mul(fishermanRewardPercentage).div(toBN(MAX_PPM))
59+
60+
return { slashAmount, rewardsAmount }
61+
}
62+
5163
async function setupIndexers() {
5264
// Dispute manager is allowed to slash
5365
await staking.connect(governor.signer).setSlasher(disputeManager.address, true)
@@ -180,6 +192,8 @@ describe('DisputeManager:POI', async () => {
180192
})
181193

182194
context('> when dispute is created', function () {
195+
// NOTE: other dispute resolution paths are tested in query.test.ts
196+
183197
beforeEach(async function () {
184198
// Create dispute
185199
await disputeManager
@@ -193,6 +207,46 @@ describe('DisputeManager:POI', async () => {
193207
.createIndexingDispute(allocationID, fishermanDeposit)
194208
await expect(tx).revertedWith('Dispute already created')
195209
})
210+
211+
describe('accept a dispute', function () {
212+
it('should resolve dispute, slash indexer and reward the fisherman', async function () {
213+
const disputeID = keccak256(allocationID)
214+
215+
// Before state
216+
const beforeIndexerStake = await staking.getIndexerStakedTokens(indexer.address)
217+
const beforeFishermanBalance = await grt.balanceOf(fisherman.address)
218+
const beforeTotalSupply = await grt.totalSupply()
219+
220+
// Calculations
221+
const { slashAmount, rewardsAmount } = await calculateSlashConditions(indexer.address)
222+
223+
// Perform transaction (accept)
224+
const tx = disputeManager.connect(arbitrator.signer).acceptDispute(disputeID)
225+
await expect(tx)
226+
.emit(disputeManager, 'DisputeAccepted')
227+
.withArgs(
228+
disputeID,
229+
indexer.address,
230+
fisherman.address,
231+
fishermanDeposit.add(rewardsAmount),
232+
)
233+
234+
// After state
235+
const afterFishermanBalance = await grt.balanceOf(fisherman.address)
236+
const afterIndexerStake = await staking.getIndexerStakedTokens(indexer.address)
237+
const afterTotalSupply = await grt.totalSupply()
238+
239+
// Fisherman reward properly assigned + deposit returned
240+
expect(afterFishermanBalance).eq(
241+
beforeFishermanBalance.add(fishermanDeposit).add(rewardsAmount),
242+
)
243+
// Indexer slashed
244+
expect(afterIndexerStake).eq(beforeIndexerStake.sub(slashAmount))
245+
// Slashed funds burned
246+
const tokensToBurn = slashAmount.sub(rewardsAmount)
247+
expect(afterTotalSupply).eq(beforeTotalSupply.sub(tokensToBurn))
248+
})
249+
})
196250
})
197251
})
198252
})

0 commit comments

Comments
 (0)