Skip to content

Instantly share code, notes, and snippets.

@denniswon
Created March 16, 2026 04:39
Show Gist options
  • Select an option

  • Save denniswon/68a25027c2a737d1b8b18adec91da42d to your computer and use it in GitHub Desktop.

Select an option

Save denniswon/68a25027c2a737d1b8b18adec91da42d to your computer and use it in GitHub Desktop.
Denial of Service: Failing External Calls in Challenge Slashing Operations
Denial of Service: Failing External Calls in Challenge Slashing Operations
Executive Summary
The ChallengeVerifier contract's slashing mechanism can experience denial of service when processing challenges. The slashSigningOperators function iterates over all operators and strategies without any batching or gas limits, causing transactions to revert when these sets grow large. This prevents challenge finalization, attestation invalidation, and operator punishment.
Technical Analysis
Root Cause
The vulnerability stems from unbounded iteration in ChallengeLib.slashSigningOperators:
solidity
for (uint256 i; i < allOperatorInfo.length;) {
for (uint256 j; j < allOperatorInfo[i].length;) {
// ... per-operator processing
if (wasSigningOperator) {
_slashOperator(ctx, operatorAddress, uint8(quorumNumbers[i]));
}
unchecked { ++j; }
}
unchecked { ++i; }
}
For each operator, _slashOperator performs additional heavy operations:
solidity
function _slashOperator(
ChallengeContext memory ctx,
address operatorAddress,
uint8 quorumId
) private {
OperatorSet memory operatorset = OperatorSet({avs: ctx.serviceManager, id: quorumId});
IStrategy[] memory strategies =
IAllocationManager(ctx.allocationManager).getStrategiesInOperatorSet(operatorset);
uint256 strategyLength = strategies.length;
uint256[] memory wadsToSlash = new uint256[](strategyLength);
for (uint256 i; i < strategyLength;) {
wadsToSlash[i] = WADS_TO_SLASH;
unchecked { ++i; }
}
// External call to InstantSlasher
InstantSlasher(ctx.instantSlasher).fulfillSlashingRequest(slashingparams);
}
Key issues:
Fetches the full strategies list via external call for every operator
Allocates new wadsToSlash array for every operator
Makes an external fulfillSlashingRequest call for every operator
No upper bound on operators per quorum or strategies per operator set
Affected Functions
All challenge resolution paths are affected:
Function Location
raiseAndResolveChallenge ChallengeVerifier.sol:195
slashForCrossChainChallenge ChallengeVerifier.sol:260
challengeDirectlyVerifiedAttestation ChallengeVerifier.sol
challengeDirectlyVerifiedMismatch ChallengeVerifier.sol
Critical Ordering Problem
Each affected function only records success after slashing completes:
solidity
// In raiseAndResolveChallenge
ChallengeLib.slashSigningOperators(
ctx, task.quorumNumbers, task.taskCreatedBlock, addressOfNonSigningOperators
);
taskSuccesfullyChallenged[taskResponse.taskId] = true; // Only reached if slashing succeeds
return true;
If slashing reverts due to gas exhaustion:
taskSuccesfullyChallenged[taskId] remains false
The attestation is never invalidated
Operators escape punishment
The challenge can be retried but will continue to fail
Exploitation Scenarios
Scenario 1: Mismatch Challenge Blocked
Context: challengeDirectlyVerifiedMismatch is called when both direct and regular attestation paths have been executed for the same task, and their hashes differ.
Preconditions:
Source-chain deployment with challenges enabled
Both direct attestation flow and regular respondToTask flow completed for the same task
Hash mismatch exists between the two paths
Large operator set and/or strategy list in the quorum
Exploitation Flow:
Task is created and processed through both attestation paths
A challenger detects the mismatch and calls challengeDirectlyVerifiedMismatch
The function validates the mismatch condition:
solidity
bool taskHashMismatch = directTaskHash != bytes32(0) && directTaskHash != regularTaskHash;
bool responseHashMismatch = directResponseHash != bytes32(0) && directResponseHash != regularResponseHash;
require(taskHashMismatch || responseHashMismatch, ChallengeFailed());
Signature re-verification succeeds
slashSigningOperators begins iteration but exceeds block gas limit
Transaction reverts
taskSuccesfullyChallenged[taskId] remains false
AttestationValidator.invalidateAttestation(taskId) is never called
Operators avoid slashing
Impact: Malicious operators can provide conflicting attestations without consequence. The invalid attestation remains active in the system.
Scenario 2: Cross-Chain Challenge Blocked
Context: slashForCrossChainChallenge processes challenges proven on destination chains.
Preconditions:
Source-chain deployment with serviceManager set (non-zero)
Destination chain is registered in registeredDestinationChains
Valid ZK proof from destination chain proving response incorrectness
Large operator/strategy sets
Exploitation Flow:
A task response on a destination chain is proven incorrect via ZK proof
Challenger relays the challenge to source chain via slashForCrossChainChallenge
Policy and ZK verification succeed:
solidity
IRegoVerifier.RegoContext memory context =
RegoVerifier(regoVerifier).verifyRegoProof(challenge.data, challenge.proof);
bool challengeSuccess = keccak256(abi.encode(context.evaluation))
!= keccak256(abi.encode(taskResponse.evaluationResult));
require(challengeSuccess, ChallengeFailed());
slashSigningOperators is called but reverts due to gas exhaustion
crossChainChallenged[crossChainKey] is never set to true
Event CrossChainChallengeRelayed is never emitted
Operators on source chain avoid slashing for invalid destination chain responses
Impact: Cross-chain security guarantees are broken. Operators can misbehave on destination chains without consequence on the source chain where their stake is held.
Scenario 3: Local ZK Challenge Blocked
Context: raiseAndResolveChallenge processes local ZK proofs demonstrating incorrect task responses.
Preconditions:
Source-chain deployment with challenges enabled
Valid ZK proof demonstrating evaluation mismatch
Task is within challenge window
Large operator/strategy sets
Exploitation Flow:
Operators submit an incorrect taskResponse with wrong evaluationResult
A challenger generates a ZK proof showing the correct evaluation differs
Challenger calls raiseAndResolveChallenge
All validations pass:
solidity
bool challengeSuccess = keccak256(abi.encode(context.evaluation))
!= keccak256(abi.encode(taskResponse.evaluationResult));
require(challengeSuccess, ChallengeFailed());
slashSigningOperators reverts due to gas
taskSuccesfullyChallenged[taskResponse.taskId] remains false
The incorrect response remains accepted
Impact: Core challenge mechanism fails. Incorrect task responses cannot be challenged despite valid proofs.
Gas Consumption Analysis
Per-Operator Costs in _slashOperator:
Operation Estimated Gas
External call to getStrategiesInOperatorSet ~2,600 + storage reads
Memory allocation for wadsToSlash array ~200 + 3 per element
Loop to fill wadsToSlash ~50 per strategy
Struct construction for SlashingParams ~500
External call to fulfillSlashingRequest ~2,600 + internal processing
Scaling Factor:
Total gas ≈ O(operators × quorums) × (base_cost + O(strategies))
With:
100 operators per quorum
2 quorums
10 strategies per operator set
Approximate iterations: 200 × 10 = 2,000 individual operations, plus 200 external fulfillSlashingRequest calls. This can easily exceed the 30M gas block limit.
Remediation Approach
Recommended Fix: Batched Slashing with State Persistence
Step 1: Record challenge success before slashing
solidity
// Mark challenge as successful first
taskSuccesfullyChallenged[taskResponse.taskId] = true;
// Invalidate attestation
AttestationValidator(attestationValidator).invalidateAttestation(taskId);
// Then attempt slashing (can be retried if it fails)
if (serviceManager != address(0)) {
ChallengeLib.slashSigningOperators(...);
}
Step 2: Implement batched slashing
solidity
struct SlashingState {
bytes32 challengeId;
uint256 processedQuorumIndex;
uint256 processedOperatorIndex;
bool completed;
}
mapping(bytes32 => SlashingState) public slashingProgress;
function continueSlashing(bytes32 challengeId, uint256 maxOperators) external {
SlashingState storage state = slashingProgress[challengeId];
require(!state.completed, "Already completed");
// Process up to maxOperators, then save progress
// ...
}
Step 3: Use O(1) non-signer lookup
solidity
// Convert array to mapping for O(1) lookups
mapping(address => bool) memory isNonSigner;
for (uint256 i; i < addressOfNonSigningOperators.length; i++) {
isNonSigner[addressOfNonSigningOperators[i]] = true;
}
// Check in O(1) instead of O(n) loop
if (!isNonSigner[operatorAddress]) {
_slashOperator(...);
}
Step 4: Cache strategy data
solidity
// Fetch strategies once per quorum, not per operator
IStrategy[] memory strategies =
IAllocationManager(ctx.allocationManager).getStrategiesInOperatorSet(operatorset);
// Reuse for all operators in that quorum
Open Questions
Partial Slashing Fairness: If slashing is batched, what happens if only some operators are slashed before an interruption? Should slashing be atomic across all operators?
Challenge Window Extension: If slashing fails due to gas and needs retry, should the challenge window be extended to allow completion?
Maximum Operator/Strategy Limits: Should the protocol enforce hard limits on operators per quorum or strategies per operator set to guarantee slashing can complete in a single transaction?
Slashing Priority: If the challenge is recorded before slashing completes, but slashing repeatedly fails, what is the acceptable delay before operators are penalized?
External Contract Dependencies: The InstantSlasher.fulfillSlashingRequest call's gas consumption depends on the external contract's implementation. How should the protocol handle changes in that contract's gas requirements?
Conclusion
The unbounded iteration pattern in slashSigningOperators creates a denial-of-service vulnerability that blocks challenge finalization when operator or strategy sets grow large. The critical issue is the ordering of state updates—slashing must succeed before the challenge is recorded, creating an all-or-nothing situation that fails entirely under gas pressure.
The recommended fix separates challenge recording from slashing execution, allowing challenges to be finalized while slashing is processed in batches. This maintains the security guarantee that proven challenges cannot be ignored while accommodating the practical gas limits of blockchain execution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment