Created
March 16, 2026 04:38
-
-
Save denniswon/e3ed1d29b8e110637351a9e08bf93bb7 to your computer and use it in GitHub Desktop.
Vulnerability Report: Missing taskCreatedBlock in Task Hash Enables Time Manipulation
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: Missing taskCreatedBlock in Task Hash Enables Time Manipulation | |
| Executive Summary | |
| A critical vulnerability exists in the TaskLib.taskHash() function where the taskCreatedBlock field is not included in the computed hash. This allows attackers to manipulate time-sensitive security checks by supplying arbitrary taskCreatedBlock values that still pass task identity verification. The vulnerability enables two distinct attack vectors: premature slashing of operators on source chains and acceptance of late task responses. | |
| Vulnerability Details | |
| Location | |
| File: contracts/src/libraries/TaskLib.sol | |
| Function: taskHash() (Lines 223-241) | |
| Vulnerable Code | |
| solidity | |
| function taskHash( | |
| INewtonProverTaskManager.Task calldata task | |
| ) external pure returns (bytes32) { | |
| return keccak256( | |
| abi.encode( | |
| task.taskId, | |
| task.intent, | |
| task.intentSignature, | |
| task.policyClient, | |
| task.wasmArgs, | |
| task.quorumNumbers, | |
| task.quorumThresholdPercentage | |
| ) | |
| ); | |
| } | |
| Critical Observation: The task.taskCreatedBlock field is conspicuously absent from the hash computation. | |
| Fixed Code | |
| solidity | |
| function taskHash( | |
| INewtonProverTaskManager.Task calldata task | |
| ) external pure returns (bytes32) { | |
| return keccak256( | |
| abi.encode( | |
| task.taskId, | |
| task.taskCreatedBlock, // <-- Added | |
| task.intent, | |
| task.intentSignature, | |
| task.policyClient, | |
| task.wasmArgs, | |
| task.quorumNumbers, | |
| task.quorumThresholdPercentage | |
| ) | |
| ); | |
| } | |
| Technical Analysis | |
| How Task Identity Works | |
| Task Creation: When a task is created, the system stores TaskLib.taskHash(task) in allTaskHashes. | |
| Task Verification: Later operations (responding, challenging) verify task identity by: | |
| Receiving a caller-supplied Task struct | |
| Computing TaskLib.taskHash(task) on the supplied struct | |
| Comparing against the stored hash | |
| The Gap: Since taskCreatedBlock is not part of the hash, a caller can supply a Task with a modified taskCreatedBlock and still pass the identity check. | |
| Downstream Dependencies on taskCreatedBlock | |
| The taskCreatedBlock field controls several critical security mechanisms: | |
| 1. Response Deadline Enforcement | |
| solidity | |
| // TaskLib.sanityCheckTaskResponse() | |
| require( | |
| blockNumber < task.taskCreatedBlock + responseWindowBlock, | |
| TaskResponseTooLate(blockNumber, task.taskCreatedBlock, responseWindowBlock) | |
| ); | |
| A manipulated taskCreatedBlock can shift the deadline window forward or backward. | |
| 2. Challenge Window Verification | |
| solidity | |
| // ChallengeVerifier.sol (Lines 340-349) | |
| require( | |
| block.number > task.taskCreatedBlock + taskResponseWindowBlock, | |
| TaskResponseWindowNotPassed(...) | |
| ); | |
| An attacker can supply an older taskCreatedBlock to make this check pass prematurely. | |
| 3. Operator Set Reconstruction | |
| solidity | |
| // OperatorVerifierLib.sol (Lines 60-61) | |
| // BLS signature verification reconstructs operator stakes at task.taskCreatedBlock | |
| This determines which operators are considered valid signers for the task. | |
| Exploitation Scenarios | |
| Scenario 1: Premature Operator Slashing (Source Chain) | |
| Attack Vector: Public challengeDirectlyVerifiedAttestation() function | |
| Attack Flow: | |
| A legitimate task is created at block N with taskCreatedBlock = N. | |
| Operators produce a valid aggregated BLS signature for this task. | |
| The task is validated via validateAttestationDirect(), setting isDirectlyVerified = true. | |
| The challenge window is W blocks. Legitimately, challenges should only be possible after block N + W. | |
| Attack: At block N + W/2 (before the window passes): | |
| Attacker calls challengeDirectlyVerifiedAttestation() | |
| Supplies a Task with taskCreatedBlock = N - W/2 | |
| The hash check passes (taskCreatedBlock not in hash) | |
| The "window passed" check evaluates: N + W/2 > (N - W/2) + W → N + W/2 > N + W/2 → passes | |
| BLS signature re-verification at block N - W/2 passes if operator membership remained stable. | |
| Result: Operators are unjustly slashed before the legitimate challenge window elapsed. | |
| Preconditions: | |
| Source chain deployment with slashing capabilities enabled | |
| validateAttestationDirect was used (not respondToTask) | |
| Operator membership stable between attacker-chosen block and actual signing block | |
| Attacker can access the signatureData from the direct validation event/storage | |
| Scenario 2: Late Response Acceptance (Trusted Role) | |
| Attack Vector: respondToTask() function (requires onlyTaskGenerator role) | |
| Attack Flow: | |
| A task is created at block N with response window W. | |
| The legitimate response deadline is block N + W. | |
| At block N + W + 100 (well past the deadline): | |
| Trusted generator calls respondToTask() | |
| Supplies a Task with taskCreatedBlock = N + 100 | |
| Hash check passes (taskCreatedBlock not in hash) | |
| Deadline check evaluates: N + W + 100 < (N + 100) + W → N + W + 100 < N + W + 100 → fails by 1 | |
| With taskCreatedBlock = N + 101: N + W + 100 < N + W + 101 → passes | |
| Result: A late response is accepted, minting an attestation despite the original window having elapsed. | |
| Preconditions: | |
| Requires onlyTaskGenerator role (trusted role) | |
| Task must exist in allTaskHashes | |
| Impact Assessment | |
| Severity: HIGH | |
| Factor Assessment | |
| Impact High - Direct financial loss through unjust operator slashing | |
| Likelihood Medium - Requires specific operational conditions but is publicly exploitable | |
| Attack Complexity Low - Simple parameter manipulation, no special tools required | |
| Privileges Required None for Scenario 1; Trusted role for Scenario 2 | |
| Specific Impacts | |
| Financial Loss: Operators lose staked funds through unjust slashing. | |
| Trust Degradation: Operators may lose confidence in the system's fairness. | |
| Protocol Integrity: Time-based security guarantees become meaningless. | |
| Operator Set Manipulation: Attacker can choose operator snapshots from blocks with favorable membership. | |
| Remediation | |
| Primary Fix | |
| Include taskCreatedBlock in the taskHash() computation: | |
| solidity | |
| function taskHash( | |
| INewtonProverTaskManager.Task calldata task | |
| ) external pure returns (bytes32) { | |
| return keccak256( | |
| abi.encode( | |
| task.taskId, | |
| task.taskCreatedBlock, // ADDED: Bind taskCreatedBlock to task identity | |
| task.intent, | |
| task.intentSignature, | |
| task.policyClient, | |
| task.wasmArgs, | |
| task.quorumNumbers, | |
| task.quorumThresholdPercentage | |
| ) | |
| ); | |
| } | |
| Migration Considerations | |
| Existing Tasks: After deploying the fix, existing tasks in allTaskHashes will have hashes computed without taskCreatedBlock. These tasks will fail verification under the new hash formula. | |
| Migration Options: | |
| Hard Cutover: Accept that pending tasks at upgrade time become invalid. Only viable if task lifecycles are short. | |
| Versioned Hashing: Introduce a version field or separate mapping for new tasks. | |
| Dual Verification: Temporarily accept both old and new hash formats during a transition period (adds complexity and temporary risk). | |
| Additional Hardening (Defense in Depth) | |
| Consider adding explicit taskCreatedBlock validation in challenge paths: | |
| solidity | |
| // In challenge functions | |
| require( | |
| task.taskCreatedBlock == storedTask.taskCreatedBlock, | |
| "Task creation block mismatch" | |
| ); | |
| This requires storing taskCreatedBlock separately or using the fixed hash approach. | |
| Open Questions for Review | |
| Migration Strategy: What is the expected lifecycle of tasks? Can a hard cutover be tolerated? | |
| Backward Compatibility: Are there external systems or indexers that depend on the current hash format? | |
| Additional Fields: Should any other Task fields be reviewed for inclusion in the hash? The current implementation excludes taskCreatedBlock but includes all other fields. | |
| Event/Storage Impact: If taskCreatedBlock is included in the hash, do any events or off-chain systems need updates to track this change? | |
| Trusted Role Risk: Scenario 2 requires a trusted role. What is the threat model for compromised or malicious task generators? | |
| Verification Steps | |
| After applying the fix: | |
| Unit Test: Create a task, attempt to respond/challenge with modified taskCreatedBlock → should revert with hash mismatch. | |
| Integration Test: Full flow from task creation through response and challenge with consistent taskCreatedBlock. | |
| Fuzzing: Verify that any modification to Task fields (including taskCreatedBlock) produces a different hash. | |
| References | |
| TaskLib.sol Lines 223-241: taskHash() function | |
| TaskLib.sol Lines 191-199: Deadline enforcement in sanityCheckTaskResponse() | |
| ChallengeVerifier.sol Lines 340-349: Challenge window verification | |
| OperatorVerifierLib.sol Lines 60-61: Operator set reconstruction | |
| NewtonProverTaskManagerShared.sol Lines 89-93: respondToTask() entry point |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment