Skip to content

Instantly share code, notes, and snippets.

@denniswon
Created March 15, 2026 23:32
Show Gist options
  • Select an option

  • Save denniswon/3e4f2a88ba9a6c8836ea2bdf7acc7743 to your computer and use it in GitHub Desktop.

Select an option

Save denniswon/3e4f2a88ba9a6c8836ea2bdf7acc7743 to your computer and use it in GitHub Desktop.
Vulnerability Report: Flawed Challenge Logic in challengeDirectlyVerifiedMismatch Enables Illegitimate Operator Slashing
Vulnerability Report: Flawed Challenge Logic in challengeDirectlyVerifiedMismatch Enables Illegitimate Operator Slashing
Executive Summary
The challengeDirectlyVerifiedMismatch function in ChallengeVerifier.sol contains multiple critical flaws that enable illegitimate slashing of operators on source chains. The function compares incompatible hash formats that are guaranteed to mismatch, lacks time-bound restrictions, ignores the global isChallengeEnabled flag, and has no protection against repeated slashing of the same task.
Vulnerability Details
Root Cause Analysis
The vulnerability stems from four distinct but compounding issues:
1. Incompatible Hash Comparison
The function compares hashes that are computed differently:
Direct response hash (stored by AttestationValidator.validateAttestationDirect):
solidity
directTaskResponseHashes[taskId] = keccak256(abi.encode(taskResponse));
Regular response hash (stored by TaskManager):
solidity
keccak256(abi.encode(taskResponse, responseCertificate))
These encodings include different data structures, guaranteeing they will never match even for identical taskResponse values.
2. No Time-Bound Restrictions
Unlike raiseAndResolveChallenge, this function has no expiry checks:
solidity
function challengeDirectlyVerifiedMismatch(
INewtonProverTaskManager.Task calldata task,
INewtonProverTaskManager.TaskResponse calldata taskResponse,
bytes calldata signatureData,
address _taskResponseHandler
) external onlyTaskManager {
// No check for: block.number < responseCertificate.referenceBlock + taskChallengeWindowBlock
// No check for: block.number < responseCertificate.responseExpireBlock
Operators can be slashed indefinitely after task completion.
3. Global Challenge Flag Ignored
The function does not check isChallengeEnabled:
solidity
function challengeDirectlyVerifiedMismatch(...) external onlyTaskManager {
// Missing: require(isChallengeEnabled, ChallengeNotEnabled());
Even when challenges are globally disabled, this function remains exploitable.
4. No Repeated Slashing Protection
The function sets taskSuccesfullyChallenged[taskId] = true at the end but never checks it on entry:
solidity
// Step 8: Mark as challenged (happens at the end)
taskSuccesfullyChallenged[taskId] = true;
AttestationValidator(attestationValidator).invalidateAttestation(taskId);
Additionally, invalidateAttestation does not clear directlyVerifiedAttestations:
solidity
function invalidateAttestation(bytes32 taskId) external onlyChallengeVerifier {
delete attestations[taskId];
// directlyVerifiedAttestations[taskId] is NOT cleared
}
This allows isDirectlyVerified(taskId) to remain true, enabling repeated calls.
The Challenge Mismatch Logic
The vulnerable comparison logic:
solidity
// 2. Get direct hashes from AttestationValidator
bytes32 directTaskHash = AttestationValidator(attestationValidator).directTaskHashes(taskId);
bytes32 directResponseHash =
AttestationValidator(attestationValidator).directTaskResponseHashes(taskId);
// 3. Get regular hashes from TaskManager
bytes32 regularTaskHash = INewtonProverTaskManager(taskManager).taskHash(taskId);
bytes32 regularResponseHash = INewtonProverTaskManager(taskManager).taskResponseHash(taskId);
// 5. Challenge succeeds if EITHER task hash or response hash mismatches
bool taskHashMismatch = directTaskHash != bytes32(0) && directTaskHash != regularTaskHash;
bool responseHashMismatch =
directResponseHash != bytes32(0) && directResponseHash != regularResponseHash;
require(taskHashMismatch || responseHashMismatch, ChallengeFailed());
The responseHashMismatch condition will always be true when both direct and regular paths have been completed, due to the encoding difference.
Exploitation Scenarios
Scenario 1: Negative-Evaluation Task Exploitation
Context: A task with negative evaluation has no normal attestation created, making the optimistic direct verification path available at any time.
Attack Flow:
Task is created and responded to via respondToTask (stores regular hashes)
Task evaluation is negative (no normal attestation)
Attacker reads TaskResponded event to obtain signatureData
Attacker calls validateAttestationDirect with the same task/response/signatureData
Direct hashes are stored (with incompatible encoding)
Attacker calls challengeDirectlyVerifiedMismatch:
isDirectlyVerified(taskId) returns true
Regular hashes are non-zero
responseHashMismatch is guaranteed true
Signatures re-verify successfully
Operators are slashed
Attacker repeats step 6 to slash operators again
Preconditions:
Source chain deployment (blsApkRegistry != address(0))
respondToTask completed for the task
Negative evaluation (no normal attestation)
signatureData available from public TaskResponded event
Scenario 2: Positive-Evaluation Task with Delayed Slashing
Context: For positive-evaluation tasks, direct verification must be set before normal attestation expires or is spent.
Attack Flow:
Task created and responded to with positive evaluation
Normal attestation created with future expiry block
Before attestation expires, attacker calls validateAttestationDirect
Direct hashes stored
Time passes—attestation expires, challenge windows pass
Weeks/months later, attacker calls challengeDirectlyVerifiedMismatch
No time checks exist—operators slashed well after intended finality
Preconditions:
Source chain deployment
Positive evaluation with normal attestation
Attacker sets direct verification before attestation expires
No time limits on challenge
Scenario 3: Task Hash Manipulation
Context: The optimistic direct path does not bind the supplied task to the on-chain taskHash.
Attack Flow:
Task created and responded to
Attacker crafts modified task with different wasmArgs but same signature-relevant fields
Attacker calls validateAttestationDirect with modified task
Direct task hash differs from on-chain task hash
Attacker calls challengeDirectlyVerifiedMismatch:
taskHashMismatch is true
Slashing occurs even if response hash comparison were fixed
Preconditions:
Source chain deployment
Attacker can construct task variant that preserves signature validity
Optimistic direct path available (negative evaluation)
Impact Assessment
Impact Category Severity Description
Financial Loss Critical Direct slashing of operator stakes
Repeatability Critical Same task can trigger multiple slashing events
Time Exposure High No expiry allows indefinite exploitation window
Bypass Controls High isChallengeEnabled flag ineffective
Recommended Remediation
Fix 1: Add Global Challenge Enable Check
solidity
function challengeDirectlyVerifiedMismatch(
INewtonProverTaskManager.Task calldata task,
INewtonProverTaskManager.TaskResponse calldata taskResponse,
bytes calldata signatureData,
address _taskResponseHandler
) external onlyTaskManager {
require(isChallengeEnabled, ChallengeNotEnabled());
// ... rest of function
Fix 2: Add Challenge Window Time Bounds
solidity
// After verifying direct attestation exists
uint32 taskCreatedBlock = task.taskCreatedBlock;
require(
uint32(block.number) < taskCreatedBlock + taskChallengeWindowBlock,
ChallengePeriodExpired()
);
Fix 3: Prevent Repeated Slashing
solidity
bytes32 taskId = taskResponse.taskId;
// Check at function start
require(!taskSuccesfullyChallenged[taskId], "Already challenged");
Fix 4: Harmonize Hash Encoding
Option A: Store compatible hashes in AttestationValidator
Modify validateAttestationDirect to store the response hash in the same format:
solidity
// In AttestationValidator.validateAttestationDirect
directTaskResponseHashes[taskId] = keccak256(abi.encode(taskResponse, responseCertificate));
This requires passing responseCertificate to the direct validation path.
Option B: Compare only the taskResponse portion
In challengeDirectlyVerifiedMismatch, reconstruct the direct-compatible hash from the regular path:
solidity
bytes32 directStyleRegularResponseHash = keccak256(abi.encode(taskResponse));
bool responseHashMismatch =
directResponseHash != bytes32(0) && directResponseHash != directStyleRegularResponseHash;
Fix 5: Clear Direct Verification on Invalidation
solidity
// In AttestationValidator.invalidateAttestation
function invalidateAttestation(bytes32 taskId) external onlyChallengeVerifier {
delete attestations[taskId];
delete directlyVerifiedAttestations[taskId]; // Add this
delete directTaskHashes[taskId]; // Add this
delete directTaskResponseHashes[taskId]; // Add this
}
Fix 6: Bind Direct Path Task to On-Chain Hash
In validateAttestationDirect, verify the supplied task matches the stored task hash:
solidity
bytes32 expectedTaskHash = INewtonProverTaskManager(taskManager).taskHash(taskId);
require(TaskLib.taskHash(task) == expectedTaskHash, "Task mismatch");
Open Questions for Review
Design Intent Clarification: What is the intended purpose of challengeDirectlyVerifiedMismatch? The current implementation suggests it should detect discrepancies between direct and regular attestation paths, but the hash encoding difference makes this comparison meaningless.
Attestation Path Relationship: Should the direct attestation path be allowed for tasks that have already completed the regular respondToTask flow? If not, this would simplify the security model.
Repeated Challenge Semantics: The current code allows setting taskSuccesfullyChallenged[taskId] but doesn't check it on entry. Is this an oversight or intentional design for some edge case?
Cross-Function Consistency: challengeDirectlyVerifiedAttestation also lacks the isChallengeEnabled check but has different preconditions. Should both functions have consistent access controls?
Slashing Finality: Once operators are slashed, what recovery mechanisms exist if the slashing was illegitimate? This vulnerability could cause significant damage before detection.
Summary
The challengeDirectlyVerifiedMismatch function is fundamentally broken due to incompatible hash comparisons that guarantee mismatch conditions. Combined with missing time bounds, ignored global controls, and no repeated-slashing protection, this creates a critical vulnerability allowing unlimited illegitimate operator slashing on source chains. Immediate remediation is required across all identified issues.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment