feat(orc-537): Update hash consesnsus contract to prevent attack on _…#1580
feat(orc-537): Update hash consesnsus contract to prevent attack on _…#1580chasingrainbows wants to merge 7 commits intodevelopfrom
Conversation
…ng frame, and cant return reports with zero support
Hardhat Unit Tests Coverage SummaryDetailsDiff against masterResults for commit: b53f44f Minimum allowed coverage is ♻️ This comment has been updated with latest results |
…c-537-updade-hash-consensus-contract
There was a problem hiding this comment.
Pull request overview
This PR hardens HashConsensus against a gas/OOG griefing vector where a malicious member could spam unique report hashes to grow _reportVariants unboundedly, by switching to bounded per-member report tracking while keeping the existing external interface and consensus behavior.
Changes:
- Replace report-variant list tracking with
_memberReports(one hash per member) and_hashSupport(support count per hash) plus_currentFrameReportersfor bounded cleanup. - Update consensus bookkeeping to store the last consensus hash (
lastConsensusReport) instead of a variant index. - Adjust a members test to reflect that
getReportVariants()no longer returns zero-support variants after the only supporting member is removed.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| contracts/0.8.9/oracle/HashConsensus.sol | Reworks report tracking to bounded per-member/per-hash mappings and updates consensus bookkeeping accordingly. |
| test/0.8.9/oracle/hashConsensus.members.test.ts | Updates expectations for getReportVariants() when a removed member was the only supporter. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| address[] memory members = _memberAddresses; | ||
|
|
||
| for (uint256 i = 0; i < members.length; i++) { |
There was a problem hiding this comment.
_getConsensusReport copies the entire _memberAddresses storage array into memory (address[] memory members = _memberAddresses;), which adds an O(n) memory copy cost on every call (including from state-changing paths like _checkConsensus). Consider iterating over _memberAddresses in storage directly to avoid the extra allocation/copy and reduce gas.
| address[] memory members = _memberAddresses; | |
| for (uint256 i = 0; i < members.length; i++) { | |
| address[] storage members = _memberAddresses; | |
| uint256 length = members.length; | |
| for (uint256 i = 0; i < length; i++) { |
| /// @dev Clears member reports and hash support mappings for a new frame | ||
| function _clearMemberReports() internal { | ||
| address[] memory reporters = _currentFrameReporters; | ||
| uint256 reportersLength = reporters.length; | ||
| for (uint256 i = 0; i < reportersLength; i++) { | ||
| address reporter = reporters[i]; | ||
| bytes32 oldReport = _memberReports[reporter]; |
There was a problem hiding this comment.
_clearMemberReports copies _currentFrameReporters from storage into memory before iterating. Since this runs on the first report of each new frame, the extra memory copy is paid on-chain. Consider iterating _currentFrameReporters in storage (cache length first), then delete _currentFrameReporters after the loop, to reduce gas.
| // Add support to the new report | ||
| _memberReports[member] = report; | ||
| uint256 support = ++_hashSupport[report]; | ||
|
|
||
| if (varIndex < variantsLength) { | ||
| support = ++_reportVariants[varIndex].support; | ||
| } else { | ||
| support = 1; | ||
| _reportVariants[varIndex] = ReportVariant({hash: report, support: 1}); | ||
| _reportVariantsLength = ++variantsLength; | ||
| // Track this member as a reporter in current frame (avoid duplicates) |
There was a problem hiding this comment.
The new per-member reporting approach is intended to prevent unbounded growth from report-hash spamming, but there isn't a targeted regression test for this. Consider adding a test where one member submits many distinct hashes in the same frame and assert getReportVariants() remains bounded and support counters remain correct.
Reworked HashConsensus reporting to store one hash per member (_memberReports) plus per-hash support (_hashSupport), capping per-frame state and preventing unbounded variantsLength.
Problem
HashConsensus previously kept every distinct report in _reportVariants, so a malicious oracle could spam unique hashes, forcing unbounded iteration and potential OOG. We replaced that structure with per-member/per-hash mappings while preserving the historical interface (getReportVariants(), consensus events, fast-lane rules).
Solution
Replace the unbounded variant list with: