Created
March 15, 2026 23:32
-
-
Save denniswon/3e4f2a88ba9a6c8836ea2bdf7acc7743 to your computer and use it in GitHub Desktop.
Vulnerability Report: Flawed Challenge Logic in challengeDirectlyVerifiedMismatch Enables Illegitimate Operator Slashing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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