Skip to content

Commit 0d6413a

Browse files
committed
chore: added integration tests for disputes
1 parent 959dde7 commit 0d6413a

File tree

5 files changed

+939
-0
lines changed

5 files changed

+939
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ethers, HDNodeWallet, Wallet } from "ethers"
2+
3+
import { IDisputeManager } from "@graphprotocol/subgraph-service"
4+
5+
/**
6+
* Creates an attestation data for a given request and response CIDs.
7+
* @param disputeManager The dispute manager contract instance.
8+
* @param signer The allocation ID that will be signing the attestation.
9+
* @param requestCID The request CID.
10+
* @param responseCID The response CID.
11+
* @param subgraphDeploymentId The subgraph deployment ID.
12+
* @returns The attestation data.
13+
*/
14+
export async function createAttestationData(
15+
disputeManager: IDisputeManager,
16+
signer: Wallet | HDNodeWallet,
17+
requestCID: string,
18+
responseCID: string,
19+
subgraphDeploymentId: string
20+
): Promise<string> {
21+
// Create receipt struct
22+
const receipt = {
23+
requestCID,
24+
responseCID,
25+
subgraphDeploymentId
26+
}
27+
28+
// Encode the receipt using the dispute manager
29+
const receiptHash = await disputeManager.encodeReceipt(receipt)
30+
31+
// Sign the receipt hash with the allocation private key
32+
const signature = signer.signingKey.sign(ethers.getBytes(receiptHash))
33+
const sig = ethers.Signature.from(signature)
34+
35+
// Concatenate the bytes directly
36+
return ethers.concat([
37+
ethers.getBytes(requestCID),
38+
ethers.getBytes(responseCID),
39+
ethers.getBytes(subgraphDeploymentId),
40+
ethers.getBytes(sig.r),
41+
ethers.getBytes(sig.s),
42+
new Uint8Array([sig.v])
43+
])
44+
}

packages/hardhat-graph-protocol/src/sdk/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { createAttestationData } from './deployments/subgraph-service/utils/attestation'
12
import { generateAllocationProof } from './deployments/subgraph-service/utils/allocation'
23
import { getSignedRAVCalldata, getSignerProof } from './deployments/subgraph-service/utils/collection'
34
import { SubgraphServiceActions } from './deployments/subgraph-service/actions/subgraphService'
45

56
export {
7+
createAttestationData,
68
generateAllocationProof,
79
getSignedRAVCalldata,
810
getSignerProof,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { ethers } from 'hardhat'
2+
import { expect } from 'chai'
3+
import hre from 'hardhat'
4+
import { EventLog } from 'ethers'
5+
6+
import { DisputeManager, IGraphToken, IHorizonStaking, SubgraphService } from '../../../../typechain-types'
7+
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
8+
9+
import { indexers } from '../../../../tasks/test/fixtures/indexers'
10+
11+
describe('Indexing Disputes', () => {
12+
let disputeManager: DisputeManager
13+
let graphToken: IGraphToken
14+
let staking: IHorizonStaking
15+
let subgraphService: SubgraphService
16+
17+
let snapshotId: string
18+
19+
// Test addresses
20+
let fisherman: SignerWithAddress
21+
let arbitrator: SignerWithAddress
22+
let indexer: SignerWithAddress
23+
24+
let allocationId: string
25+
26+
// Dispute manager variables
27+
let disputeDeposit: bigint
28+
let fishermanRewardCut: bigint
29+
let disputePeriod: bigint
30+
31+
before(async () => {
32+
// Get contracts
33+
const graph = hre.graph()
34+
disputeManager = graph.subgraphService!.contracts.DisputeManager as unknown as DisputeManager
35+
graphToken = graph.horizon!.contracts.GraphToken as unknown as IGraphToken
36+
staking = graph.horizon!.contracts.HorizonStaking as unknown as IHorizonStaking
37+
subgraphService = graph.subgraphService!.contracts.SubgraphService as unknown as SubgraphService
38+
39+
// Get signers
40+
const signers = await ethers.getSigners()
41+
fisherman = signers[0]
42+
arbitrator = signers[2]
43+
44+
// Get indexer
45+
const indexerFixture = indexers[0]
46+
indexer = await ethers.getSigner(indexerFixture.address)
47+
48+
// Get allocation
49+
const allocation = indexerFixture.allocations[0]
50+
allocationId = allocation.allocationID
51+
52+
// Dispute manager variables
53+
disputeDeposit = await disputeManager.disputeDeposit()
54+
fishermanRewardCut = await disputeManager.fishermanRewardCut()
55+
disputePeriod = await disputeManager.disputePeriod()
56+
})
57+
58+
beforeEach(async () => {
59+
// Take a snapshot before each test
60+
snapshotId = await ethers.provider.send('evm_snapshot', [])
61+
})
62+
63+
afterEach(async () => {
64+
// Revert to the snapshot after each test
65+
await ethers.provider.send('evm_revert', [snapshotId])
66+
})
67+
68+
describe('Fisherman', () => {
69+
it('should allow fisherman to create an indexing dispute', async () => {
70+
// Create dispute
71+
const poi = ethers.keccak256(ethers.toUtf8Bytes('test-poi'))
72+
73+
// Approve dispute manager for dispute deposit
74+
await graphToken.connect(fisherman).approve(disputeManager.target, disputeDeposit)
75+
76+
// Create dispute
77+
const tx = await disputeManager.connect(fisherman).createIndexingDispute(allocationId, poi)
78+
const receipt = await tx.wait()
79+
80+
// Get dispute ID from event
81+
const disputeCreatedEvent = receipt?.logs.find(
82+
log => log instanceof EventLog && log.fragment?.name === 'IndexingDisputeCreated',
83+
) as EventLog
84+
const disputeId = disputeCreatedEvent?.args[0]
85+
86+
// Verify dispute was created
87+
const dispute = await disputeManager.disputes(disputeId)
88+
expect(dispute.indexer).to.equal(indexer.address, 'Indexer address mismatch')
89+
expect(dispute.fisherman).to.equal(fisherman.address, 'Fisherman address mismatch')
90+
expect(dispute.disputeType).to.equal(1, 'Dispute type should be indexing')
91+
expect(dispute.status).to.equal(4, 'Dispute status should be pending')
92+
})
93+
94+
it('should allow fisherman to cancel an indexing dispute', async () => {
95+
// Create dispute
96+
const poi = ethers.keccak256(ethers.toUtf8Bytes('test-poi'))
97+
98+
// Approve dispute manager for dispute deposit
99+
await graphToken.connect(fisherman).approve(disputeManager.target, disputeDeposit)
100+
101+
// Create dispute
102+
const tx = await disputeManager.connect(fisherman).createIndexingDispute(allocationId, poi)
103+
const receipt = await tx.wait()
104+
105+
// Get dispute ID from event
106+
const disputeCreatedEvent = receipt?.logs.find(
107+
log => log instanceof EventLog && log.fragment?.name === 'IndexingDisputeCreated',
108+
) as EventLog
109+
const disputeId = disputeCreatedEvent?.args[0]
110+
111+
// Get fisherman's balance before canceling dispute
112+
const fishermanBalanceBefore = await graphToken.balanceOf(fisherman.address)
113+
114+
// Pass dispute period
115+
await ethers.provider.send('evm_increaseTime', [Number(disputePeriod) + 1])
116+
await ethers.provider.send('evm_mine', [])
117+
118+
// Cancel dispute
119+
await disputeManager.connect(fisherman).cancelDispute(disputeId)
120+
121+
// Verify dispute was canceled
122+
const updatedDispute = await disputeManager.disputes(disputeId)
123+
expect(updatedDispute.status).to.equal(5, 'Dispute status should be canceled')
124+
125+
// Verify fisherman got the deposit back
126+
const fishermanBalance = await graphToken.balanceOf(fisherman.address)
127+
expect(fishermanBalance).to.equal(fishermanBalanceBefore + disputeDeposit, 'Fisherman should receive the deposit back')
128+
})
129+
})
130+
131+
describe('Arbitrating Indexing Disputes', () => {
132+
let disputeId: string
133+
134+
beforeEach(async () => {
135+
// Approve dispute manager for dispute deposit
136+
await graphToken.connect(fisherman).approve(disputeManager.target, disputeDeposit)
137+
138+
// Create dispute
139+
const poi = ethers.keccak256(ethers.toUtf8Bytes('test-poi'))
140+
const tx = await disputeManager.connect(fisherman).createIndexingDispute(allocationId, poi)
141+
const receipt = await tx.wait()
142+
143+
// Get dispute ID from event
144+
const disputeCreatedEvent = receipt?.logs.find(
145+
log => log instanceof EventLog && log.fragment?.name === 'IndexingDisputeCreated',
146+
) as EventLog
147+
disputeId = disputeCreatedEvent?.args[0]
148+
})
149+
150+
it('should allow arbitrator to accept an indexing dispute', async () => {
151+
// Get fisherman's balance before accepting dispute
152+
const fishermanBalanceBefore = await graphToken.balanceOf(fisherman.address)
153+
154+
// Get indexer's provision before accepting dispute
155+
const provision = await staking.getProviderTokensAvailable(indexer.address, await subgraphService.getAddress())
156+
157+
// Get indexer stake snapshot
158+
const dispute = await disputeManager.disputes(disputeId)
159+
const tokensToSlash = dispute.stakeSnapshot / 10n
160+
161+
// Accept dispute
162+
await disputeManager.connect(arbitrator).acceptDispute(disputeId, tokensToSlash)
163+
164+
// Verify dispute status
165+
const updatedDispute = await disputeManager.disputes(disputeId)
166+
expect(updatedDispute.status).to.equal(1, 'Dispute status should be accepted')
167+
168+
// Verify indexer's stake was slashed
169+
const updatedProvision = await staking.getProviderTokensAvailable(indexer.address, await subgraphService.getAddress())
170+
expect(updatedProvision).to.equal(provision - tokensToSlash, 'Indexer stake should be slashed')
171+
172+
// Verify fisherman got the deposit plus the reward
173+
const fishermanBalance = await graphToken.balanceOf(fisherman.address)
174+
const fishermanReward = (tokensToSlash * fishermanRewardCut) / 1000000n
175+
const fishermanTotal = fishermanBalanceBefore + fishermanReward + disputeDeposit
176+
expect(fishermanBalance).to.equal(fishermanTotal, 'Fisherman balance should be increased by the reward and deposit')
177+
})
178+
179+
it('should allow arbitrator to draw an indexing dispute', async () => {
180+
// Get fisherman's balance before drawing dispute
181+
const fishermanBalanceBefore = await graphToken.balanceOf(fisherman.address)
182+
183+
// Get indexer's provision before drawing dispute
184+
const provision = await staking.getProviderTokensAvailable(indexer.address, await subgraphService.getAddress())
185+
186+
// Draw dispute
187+
await disputeManager.connect(arbitrator).drawDispute(disputeId)
188+
189+
// Verify dispute status
190+
const updatedDispute = await disputeManager.disputes(disputeId)
191+
expect(updatedDispute.status).to.equal(3, 'Dispute status should be drawn')
192+
193+
// Verify indexer's provision was not affected
194+
const updatedProvision = await staking.getProviderTokensAvailable(indexer.address, await subgraphService.getAddress())
195+
expect(updatedProvision).to.equal(provision, 'Indexer stake should not be affected')
196+
197+
// Verify fisherman got the deposit back
198+
const fishermanBalance = await graphToken.balanceOf(fisherman.address)
199+
expect(fishermanBalance).to.equal(fishermanBalanceBefore + disputeDeposit, 'Fisherman should receive the deposit back')
200+
})
201+
202+
it('should allow arbitrator to reject an indexing dispute', async () => {
203+
// Get fisherman's balance before rejecting dispute
204+
const fishermanBalanceBefore = await graphToken.balanceOf(fisherman.address)
205+
206+
// Get indexer's provision before rejecting dispute
207+
const provision = await staking.getProviderTokensAvailable(indexer.address, await subgraphService.getAddress())
208+
209+
// Reject dispute
210+
await disputeManager.connect(arbitrator).rejectDispute(disputeId)
211+
212+
// Verify dispute status
213+
const updatedDispute = await disputeManager.disputes(disputeId)
214+
expect(updatedDispute.status).to.equal(2, 'Dispute status should be rejected')
215+
216+
// Verify indexer's provision was not affected
217+
const updatedProvision = await staking.getProviderTokensAvailable(indexer.address, await subgraphService.getAddress())
218+
expect(updatedProvision).to.equal(provision, 'Indexer stake should not be affected')
219+
220+
// Verify fisherman did not receive the deposit
221+
const fishermanBalance = await graphToken.balanceOf(fisherman.address)
222+
expect(fishermanBalance).to.equal(fishermanBalanceBefore, 'Fisherman balance should not receive the deposit back')
223+
})
224+
225+
it('should not allow non-arbitrator to accept an indexing dispute', async () => {
226+
// Get indexer stake snapshot
227+
const dispute = await disputeManager.disputes(disputeId)
228+
const tokensToSlash = dispute.stakeSnapshot / 10n
229+
230+
// Attempt to accept dispute as fisherman
231+
await expect(
232+
disputeManager.connect(fisherman).acceptDispute(disputeId, tokensToSlash),
233+
).to.be.revertedWithCustomError(disputeManager, 'DisputeManagerNotArbitrator')
234+
})
235+
236+
it('should not allow non-arbitrator to draw an indexing dispute', async () => {
237+
// Attempt to draw dispute as fisherman
238+
await expect(
239+
disputeManager.connect(fisherman).drawDispute(disputeId),
240+
).to.be.revertedWithCustomError(disputeManager, 'DisputeManagerNotArbitrator')
241+
})
242+
243+
it('should not allow non-arbitrator to reject an indexing dispute', async () => {
244+
// Attempt to reject dispute as fisherman
245+
await expect(
246+
disputeManager.connect(fisherman).rejectDispute(disputeId),
247+
).to.be.revertedWithCustomError(disputeManager, 'DisputeManagerNotArbitrator')
248+
})
249+
})
250+
})

0 commit comments

Comments
 (0)