Skip to content

Commit 0a7e416

Browse files
committed
fix: prevent indexer from possibly being exessively slashed (OZ M-04)
Signed-off-by: Tomás Migone <[email protected]>
1 parent 02cac66 commit 0a7e416

File tree

5 files changed

+222
-5
lines changed

5 files changed

+222
-5
lines changed

packages/subgraph-service/contracts/DisputeManager.sol

+10-4
Original file line numberDiff line numberDiff line change
@@ -667,15 +667,21 @@ contract DisputeManager is
667667
* - Thawing stake is not excluded from the snapshot.
668668
* - Delegators stake is capped at the delegation ratio to prevent delegators from inflating the snapshot
669669
* to increase the indexer slash amount.
670+
* - Delegator's stake is not considered if delegation slashing is disabled.
670671
* @param _indexer Indexer address
671672
* @param _indexerStake Indexer's stake
672673
* @return Total stake snapshot
673674
*/
674675
function _getStakeSnapshot(address _indexer, uint256 _indexerStake) private view returns (uint256) {
675676
ISubgraphService subgraphService_ = _getSubgraphService();
676-
uint256 delegatorsStake = _graphStaking().getDelegationPool(_indexer, address(subgraphService_)).tokens;
677-
uint256 delegatorsStakeMax = _indexerStake * uint256(subgraphService_.getDelegationRatio());
678-
uint256 stakeSnapshot = _indexerStake + MathUtils.min(delegatorsStake, delegatorsStakeMax);
679-
return stakeSnapshot;
677+
IHorizonStaking staking = _graphStaking();
678+
679+
if (staking.isDelegationSlashingEnabled()) {
680+
uint256 delegatorsStake = staking.getDelegationPool(_indexer, address(subgraphService_)).tokens;
681+
uint256 delegatorsStakeMax = _indexerStake * uint256(subgraphService_.getDelegationRatio());
682+
return _indexerStake + MathUtils.min(delegatorsStake, delegatorsStakeMax);
683+
} else {
684+
return _indexerStake;
685+
}
680686
}
681687
}

packages/subgraph-service/test/disputeManager/DisputeManager.t.sol

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol"
88
import { Attestation } from "../../contracts/libraries/Attestation.sol";
99
import { Allocation } from "../../contracts/libraries/Allocation.sol";
1010
import { IDisputeManager } from "../../contracts/interfaces/IDisputeManager.sol";
11+
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
1112

1213
import { SubgraphServiceSharedTest } from "../shared/SubgraphServiceShared.t.sol";
1314

@@ -403,9 +404,10 @@ contract DisputeManagerTest is SubgraphServiceSharedTest {
403404
address fisherman = dispute.fisherman;
404405
uint256 fishermanPreviousBalance = token.balanceOf(fisherman);
405406
uint256 indexerTokensAvailable = staking.getProviderTokensAvailable(dispute.indexer, address(subgraphService));
407+
uint256 provisionTokens = staking.getProvision(dispute.indexer, address(subgraphService)).tokens;
406408
uint256 disputeDeposit = dispute.deposit;
407409
uint256 fishermanRewardPercentage = disputeManager.fishermanRewardCut();
408-
uint256 fishermanReward = _tokensSlash.mulPPM(fishermanRewardPercentage);
410+
uint256 fishermanReward = Math.min(_tokensSlash, provisionTokens).mulPPM(fishermanRewardPercentage);
409411

410412
vm.expectEmit(address(disputeManager));
411413
emit IDisputeManager.DisputeAccepted(

packages/subgraph-service/test/disputeManager/disputes/indexing/accept.t.sol

+95
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,99 @@ contract DisputeManagerIndexingAcceptDisputeTest is DisputeManagerTest {
8888
vm.expectRevert(expectedError);
8989
disputeManager.acceptDispute(disputeID, tokensSlash);
9090
}
91+
92+
function test_Indexing_Accept_Dispute_WithDelegation(
93+
uint256 tokens,
94+
uint256 tokensDelegated,
95+
uint256 tokensSlash
96+
) public useIndexer useAllocation(tokens) useDelegation(tokensDelegated) {
97+
tokensSlash = bound(
98+
tokensSlash,
99+
1,
100+
uint256(maxSlashingPercentage).mulPPM(_calculateStakeSnapshot(tokens, tokensDelegated))
101+
);
102+
103+
// Initial dispute with delegation slashing disabled
104+
resetPrank(users.fisherman);
105+
bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI1"));
106+
107+
resetPrank(users.arbitrator);
108+
_acceptDispute(disputeID, tokensSlash);
109+
}
110+
111+
function test_Indexing_Accept_RevertWhen_SlashingOverMaxSlashPercentage_WithDelegation(
112+
uint256 tokens,
113+
uint256 tokensDelegated,
114+
uint256 tokensSlash
115+
) public useIndexer useAllocation(tokens) useDelegation(tokensDelegated) {
116+
uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(
117+
_calculateStakeSnapshot(tokens, tokensDelegated)
118+
);
119+
tokensSlash = bound(tokensSlash, maxTokensToSlash + 1, type(uint256).max);
120+
121+
resetPrank(users.fisherman);
122+
bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI101"));
123+
124+
// max slashing percentage is 50%
125+
resetPrank(users.arbitrator);
126+
bytes memory expectedError = abi.encodeWithSelector(
127+
IDisputeManager.DisputeManagerInvalidTokensSlash.selector,
128+
tokensSlash,
129+
maxTokensToSlash
130+
);
131+
vm.expectRevert(expectedError);
132+
disputeManager.acceptDispute(disputeID, tokensSlash);
133+
}
134+
135+
function test_Indexing_Accept_Dispute_WithDelegation_DelegationSlashing(
136+
uint256 tokens,
137+
uint256 tokensDelegated,
138+
uint256 tokensSlash
139+
) public useIndexer useAllocation(tokens) useDelegation(tokensDelegated) {
140+
// enable delegation slashing
141+
resetPrank(users.governor);
142+
staking.setDelegationSlashingEnabled();
143+
144+
tokensSlash = bound(
145+
tokensSlash,
146+
1,
147+
uint256(maxSlashingPercentage).mulPPM(_calculateStakeSnapshot(tokens, tokensDelegated))
148+
);
149+
150+
// Create a new dispute with delegation slashing enabled
151+
resetPrank(users.fisherman);
152+
bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI2"));
153+
154+
resetPrank(users.arbitrator);
155+
_acceptDispute(disputeID, tokensSlash);
156+
}
157+
158+
function test_Indexing_Accept_RevertWhen_SlashingOverMaxSlashPercentage_WithDelegation_DelegationSlashing(
159+
uint256 tokens,
160+
uint256 tokensDelegated,
161+
uint256 tokensSlash
162+
) public useIndexer useAllocation(tokens) useDelegation(tokensDelegated) {
163+
// enable delegation slashing
164+
resetPrank(users.governor);
165+
staking.setDelegationSlashingEnabled();
166+
167+
uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(
168+
_calculateStakeSnapshot(tokens, tokensDelegated)
169+
);
170+
tokensSlash = bound(tokensSlash, maxTokensToSlash + 1, type(uint256).max);
171+
172+
// Create a new dispute with delegation slashing enabled
173+
resetPrank(users.fisherman);
174+
bytes32 disputeID = _createIndexingDispute(allocationID, bytes32("POI101"));
175+
176+
// max slashing percentage is 50%
177+
resetPrank(users.arbitrator);
178+
bytes memory expectedError = abi.encodeWithSelector(
179+
IDisputeManager.DisputeManagerInvalidTokensSlash.selector,
180+
tokensSlash,
181+
maxTokensToSlash
182+
);
183+
vm.expectRevert(expectedError);
184+
disputeManager.acceptDispute(disputeID, tokensSlash);
185+
}
91186
}

packages/subgraph-service/test/disputeManager/disputes/query/accept.t.sol

+104
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,108 @@ contract DisputeManagerQueryAcceptDisputeTest is DisputeManagerTest {
118118
vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerDisputeNotInConflict.selector, disputeID));
119119
disputeManager.acceptDisputeConflict(disputeID, tokensSlash, true, 0);
120120
}
121+
122+
function test_Query_Accept_Dispute_WithDelegation(
123+
uint256 tokens,
124+
uint256 tokensDelegated,
125+
uint256 tokensSlash
126+
) public useIndexer useAllocation(tokens) useDelegation(tokensDelegated) {
127+
tokensSlash = bound(
128+
tokensSlash,
129+
1,
130+
uint256(maxSlashingPercentage).mulPPM(_calculateStakeSnapshot(tokens, tokensDelegated))
131+
);
132+
133+
// Initial dispute with delegation slashing disabled
134+
resetPrank(users.fisherman);
135+
Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId);
136+
bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey);
137+
bytes32 disputeID = _createQueryDispute(attestationData);
138+
139+
resetPrank(users.arbitrator);
140+
_acceptDispute(disputeID, tokensSlash);
141+
}
142+
143+
function test_Query_Accept_RevertWhen_SlashingOverMaxSlashPercentage_WithDelegation(
144+
uint256 tokens,
145+
uint256 tokensDelegated,
146+
uint256 tokensSlash
147+
) public useIndexer useAllocation(tokens) useDelegation(tokensDelegated) {
148+
uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(
149+
_calculateStakeSnapshot(tokens, tokensDelegated)
150+
);
151+
tokensSlash = bound(tokensSlash, maxTokensToSlash + 1, type(uint256).max);
152+
153+
resetPrank(users.fisherman);
154+
Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId);
155+
bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey);
156+
bytes32 disputeID = _createQueryDispute(attestationData);
157+
158+
// max slashing percentage is 50%
159+
resetPrank(users.arbitrator);
160+
bytes memory expectedError = abi.encodeWithSelector(
161+
IDisputeManager.DisputeManagerInvalidTokensSlash.selector,
162+
tokensSlash,
163+
maxTokensToSlash
164+
);
165+
vm.expectRevert(expectedError);
166+
disputeManager.acceptDispute(disputeID, tokensSlash);
167+
}
168+
169+
function test_Query_Accept_Dispute_WithDelegation_DelegationSlashing(
170+
uint256 tokens,
171+
uint256 tokensDelegated,
172+
uint256 tokensSlash
173+
) public useIndexer useAllocation(tokens) useDelegation(tokensDelegated) {
174+
// enable delegation slashing
175+
resetPrank(users.governor);
176+
staking.setDelegationSlashingEnabled();
177+
178+
tokensSlash = bound(
179+
tokensSlash,
180+
1,
181+
uint256(maxSlashingPercentage).mulPPM(_calculateStakeSnapshot(tokens, tokensDelegated))
182+
);
183+
184+
// Create a new dispute with delegation slashing enabled
185+
resetPrank(users.fisherman);
186+
Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId);
187+
bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey);
188+
bytes32 disputeID = _createQueryDispute(attestationData);
189+
190+
resetPrank(users.arbitrator);
191+
_acceptDispute(disputeID, tokensSlash);
192+
}
193+
194+
function test_Query_Accept_RevertWhen_SlashingOverMaxSlashPercentage_WithDelegation_DelegationSlashing(
195+
uint256 tokens,
196+
uint256 tokensDelegated,
197+
uint256 tokensSlash
198+
) public useIndexer useAllocation(tokens) useDelegation(tokensDelegated) {
199+
// enable delegation slashing
200+
resetPrank(users.governor);
201+
staking.setDelegationSlashingEnabled();
202+
203+
resetPrank(users.fisherman);
204+
uint256 maxTokensToSlash = uint256(maxSlashingPercentage).mulPPM(
205+
_calculateStakeSnapshot(tokens, tokensDelegated)
206+
);
207+
tokensSlash = bound(tokensSlash, maxTokensToSlash + 1, type(uint256).max);
208+
209+
// Create a new dispute with delegation slashing enabled
210+
resetPrank(users.fisherman);
211+
Attestation.Receipt memory receipt = _createAttestationReceipt(requestCID, responseCID, subgraphDeploymentId);
212+
bytes memory attestationData = _createAtestationData(receipt, allocationIDPrivateKey);
213+
bytes32 disputeID = _createQueryDispute(attestationData);
214+
215+
// max slashing percentage is 50%
216+
resetPrank(users.arbitrator);
217+
bytes memory expectedError = abi.encodeWithSelector(
218+
IDisputeManager.DisputeManagerInvalidTokensSlash.selector,
219+
tokensSlash,
220+
maxTokensToSlash
221+
);
222+
vm.expectRevert(expectedError);
223+
disputeManager.acceptDispute(disputeID, tokensSlash);
224+
}
121225
}

packages/subgraph-service/test/shared/SubgraphServiceShared.t.sol

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Allocation } from "../../contracts/libraries/Allocation.sol";
77
import { AllocationManager } from "../../contracts/utilities/AllocationManager.sol";
88
import { IDataService } from "@graphprotocol/horizon/contracts/data-service/interfaces/IDataService.sol";
99
import { ISubgraphService } from "../../contracts/interfaces/ISubgraphService.sol";
10+
import { MathUtils } from "@graphprotocol/horizon/contracts/libraries/MathUtils.sol";
1011

1112
import { HorizonStakingSharedTest } from "./HorizonStakingShared.t.sol";
1213

@@ -186,6 +187,15 @@ abstract contract SubgraphServiceSharedTest is HorizonStakingSharedTest {
186187
staking.delegate(users.indexer, address(subgraphService), tokens, 0);
187188
}
188189

190+
function _calculateStakeSnapshot(uint256 _tokens, uint256 _tokensDelegated) internal view returns (uint256) {
191+
bool delegationSlashingEnabled = staking.isDelegationSlashingEnabled();
192+
if (delegationSlashingEnabled) {
193+
return _tokens + MathUtils.min(_tokensDelegated, _tokens * subgraphService.getDelegationRatio());
194+
} else {
195+
return _tokens;
196+
}
197+
}
198+
189199
/*
190200
* PRIVATE FUNCTIONS
191201
*/

0 commit comments

Comments
 (0)