HCOW Info

HCOW is a GameFi ecosystem built around one idea: games should be provably fair, and the value they create should flow back to the people who play and hold.

HCOW Logo

Team and KYC Verification

The team has securely submitted their personal information to SolidProof.io for verification.

In the event of any fraudulent activities, this information will be promptly reported to the relevant authorities to ensure accountability and compliance.

TrustNet Score

The TrustNet Score evaluates crypto projects based on audit results, security, KYC verification, and social media presence. This score offers a quick, transparent view of a project's credibility, helping users make informed decisions in the Web3 space.

98.63
Poor Excellent

Real-Time Threat Detection

Real-time threat detection, powered by Cyvers.io, is currently not activated for this project.

This advanced feature provides continuous monitoring and instant alerts to safeguard your assets from potential security threats. Real-time detection enhances your project's security by proactively identifying and mitigating risks. For more information, click here.

Security Assessments

Select the audit
"Static Analysis Dynamic Analysis Symbolic Execution SWC Check Manual Review"
Contract address
N/A
Network N/A
License N/A
Compiler N/A
Type N/A
Language Solidity
Onboard date 2026/08/28
Revision date 2026/09/01

Summary and Final Words

No crucial issues found

The contract does not contain issues of high or medium criticality. This means that no known vulnerabilities were found in the source code.

Ownership is not renounced

The owner retains significant control, which could potentially be used to modify key contract parameters.

Contract is not upgradeable

The contract does not use proxy patterns or other mechanisms to allow future upgrades. Its behavior is locked in its current state.

Scope of Work

This audit encompasses the evaluation of the files listed below, each verified with a SHA-1 Hash. The team referenced above has provided the necessary files for assessment.

The auditing process consists of the following systematic steps:

  1. Specification Review: Analyze the provided specifications, source code, and instructions to fully understand the smart contract's size, scope, and functionality.
  2. Manual Code Examination: Conduct a thorough line-by-line review of the source code to identify potential vulnerabilities and areas for improvement.
  3. Specification Alignment: Ensure that the code accurately implements the provided specifications and intended functionalities.
  4. Test Coverage Assessment: Evaluate the extent and effectiveness of test cases in covering the codebase, identifying any gaps in testing.
  5. Symbolic Execution: Analyze the smart contract to determine how various inputs affect execution paths, identifying potential edge cases and vulnerabilities.
  6. Best Practices Evaluation: Assess the smart contracts against established industry and academic best practices to enhance efficiency, maintainability, and security.
  7. Actionable Recommendations: Provide detailed, specific, and actionable steps to secure and optimize the smart contracts.

A file with a different Hash has been intentionally or otherwise modified after the security review. A different Hash may indicate a changed condition or potential vulnerability that was not within the scope of this review.

Final Words

The following provides a concise summary of the audit report, accompanied by insightful comments from the auditor. This overview captures the key findings and observations, offering valuable context and clarity.


Smart Contract Analysis Statement

Re-audit Scope

This statement replaces the one issued on 28 August 2026 and covers the ledger contract as it stands at the pinned commit. The contract grew from three hundred and seventy lines to eight hundred and forty three, with five hundred and sixty three lines changed, which is the largest change in the project. It was reviewed as new code function by function. All seven findings from the first round were re-verified and four new observations are recorded. Within the scope agreed for this round, the reference Merkle and canonicalisation libraries were also reviewed, but strictly as a differential against the contract rather than as a general review of the off-chain layer.

Contract Analysis

Game rounds are recorded off chain, hashed into Merkle leaves, and only the root of each period is written here. Anyone holding a record and its proof can verify against the contract that the record is the one that was committed, without trusting the operator and without the operator revealing anything about other players. Live periods are strictly sequential with no cursor override, so the sequence can never contain a hole. Historical data that predates the contract is anchored through a separate function with a separate event, so nobody can read a backfill as live evidence.

Four structural changes account for most of the new code. Trees are now padded to a power of two with a distinct filler leaf rather than pairing an odd final node with itself, which removes a real collision: a tree over three records used to produce the same root as those three plus a duplicate of the third. The value anchored is no longer the bare Merkle root but a fold binding the root to a declared record count. Emptiness is now stated positively with a dedicated constant rather than encoded as a zero root, which is also what an unanchored period reads as. And the leaf hashing rule is now executable code on chain, so a verifier obtains the whole rule from the contract rather than from a document.

Ownership Privileges

Nothing the owner can do alters an anchored root. There is no admin function that rewrites history, not for the owner either. The owner can:

  • Grant and revoke the anchoring permission
  • Nominate a successor owner, who must accept, and withdraw that nomination
  • Renounce ownership permanently, but only once at least two permitted addresses have each actually anchored at least once

The renounce guard is the notable change. It previously counted configured entries, which a sentinel address nobody controls satisfied, so three individually valid owner calls could reach a ledger that could never advance again and had no owner to grant a replacement. It now requires evidence that a live key exists: an address counts only once it has written to the contract. An anchorer can also remove itself, which survives renouncement and is the only way a compromised key can be taken out of the permission set once there is no owner. That path refuses when it would leave no proven anchorer, so an ownerless ledger always keeps at least one live writer.

Security Features

  • An anchored root can never be changed or removed by anyone, including the owner
  • Live periods are strictly sequential, and a period may only be anchored once its hour has ended, so a stolen key cannot burn a year of future periods for pocket change
  • Replay is guarded on both paths, with separate maps, deliberately: writing the live map from the backfill path would let a stolen key make the next live root unusable and stop the ledger permanently
  • A backfill may not reach into time the live path covers, enforced against the genesis boundary rather than against the current time
  • Verification refuses a zero root, refuses the empty period constant, refuses the padding filler by name, and checks the proof length against the tree the root came from, so the root itself cannot be presented as a record with an empty proof
  • The leaf and node preimage spaces are separated by first byte unconditionally, which we verified: node preimages begin with one, the count fold with two, and every leaf preimage with the letter H
  • We drove two hundred and sixty four adversarial field values through both the contract and the reference library, covering real tabs, real newlines, literal backslashes, a literal backslash followed by t, every combination of those, values shaped to forge a field boundary, and multi-byte unicode. The two agree on every one

Findings Summary

Three of the seven first-round findings are fixed, one is fixed differently and better than recommended, and three are partially fixed. There were no medium or higher findings in either round, and none of the new observations reaches medium either. The anchored roots and every proof against them are sound; what the remaining items affect is a published metric and an indexer's ability to alert.

The uniqueness half of the record count finding is fully fixed and we confirmed the collision is gone. The count half is not fixed in the sense the finding described, and the response's account of the mechanism does not match the code: verification recomputes the fold from the figure held in storage, not from one the caller supplies. Because a tree of a given size and a tree padded up to the same power of two are byte-identical, and the required proof depth is identical across that whole bracket, the anchorer still chooses any count in the bracket at anchoring time and every genuine receipt still verifies. We measured three records anchored as four, and five hundred and thirteen records anchored as one thousand and twenty four, an inflation of just under two times, with receipts verifying in both cases. Understating remains self-punishing and breaks every receipt permanently, which is the honest-mistake mode the first round predicted.

A second route to the same double count was found. The two replay maps are separate for a correct reason, but neither sees the other, so the same anchored value can be submitted once on each path. We measured one thousand records reported as two thousand. This matters mainly because the source carries an explicit list of what the separation does not stop, written so a reader is not left to discover the gaps, and this route is not on it.

Two smaller items. No distinct event is emitted for an attested empty period, contrary to the response, so an indexer must match a root against a published constant rather than subscribe to a topic. And the change that moved the leaf rule on chain produced the project's only new high-impact static analysis result, on the packed encoding in the field join; the justification in the source is a two-part argument which we checked line by line and which holds, but the document that triages every other detector class does not mention it, and the committed raw report prints it first with no annotation.

On the two questions the response asks about this contract, our instructions are recorded in the findings: the forward-only rule for backfills should not be added, and the two-proven-anchorer form should be kept in preference to a recency window. In both cases the reasoning offered for departing from the first round's recommendation is better than the recommendation was.

Note - This Audit report consists of a security analysis of the HCOWLedger smart contract and, as a bounded differential only, of the reference Merkle and canonicalisation libraries. The anchoring worker, the browser verification page, the deployment scripts and the database schema were not reviewed and are outside the agreed scope. A contract that is correct while the worker feeding it is not produces receipts that are permanently unverifiable, so we recommend a separate review of that layer before mainnet.

Files and details

Findings and Audit result

low Issues | 6 findings

Pending

#1 low Issue
Historical backfill accepts unlimited replay and overlapping ranges
HCOWLedger.sol
L354-427
Description

The live anchoring path records each non-zero root and refuses a repeat, and a retry after a successful anchor is correctly rejected. The historical path never touches that mapping and never compares its time range against any stored batch, so the same root and the same range can be submitted without limit, each call minting a new identifier and adding the record count again. Testing confirmed a single real batch of fifty thousand records reported as five hundred thousand after ten submissions. The range is also bounded only by the current time rather than by the period the contract began at, so a backfill can claim to cover hours the live path has already anchored, and testing confirmed the same one thousand records reported as two thousand across the two counters. The most likely trigger is an ordinary retry after a dropped connection rather than malice, and the asymmetry with the replay-safe live path is the core of the issue. The published total record figure is therefore not a reliable number. Peer review is right that this affects an asserted metric rather than Merkle membership integrity, which is why it belongs at this severity: the anchored roots and every proof against them remain sound.

Resolved

#2 low Issue
Guard on renouncing ownership is satisfied by an address nobody controls
HCOWLedger.sol
L692-716
L726-750
L762-787
L823-842
Description

Renouncing ownership is refused when the anchorer count is zero, which is intended to stop the ledger being left with no way to advance. The check counts entries rather than proving any key is controlled, so a sentinel address satisfies it. Testing confirmed that three individually valid owner calls reach a ledger with no owner and one anchorer that nobody can spend: the ledger can never advance again and no one can grant a replacement. The direct ordering the guard was written for is correctly blocked, so this is a bypass of the guard rather than an absence of one. Separately, the single-step ownership transfer reaches an equivalent state through a mistyped address and carries no guard, which given the care taken over the renounce path looks unintended.

Pending

#3 low Issue
Record count is asserted rather than committed, and a wrong value disables verification
HCOWLedger.sol
L106-137
L469-520
Description

The record count is supplied by the anchorer and is not cryptographically tied to the tree. Verification uses it only to derive the expected proof depth, which pins down the tree's depth bracket rather than its leaf count, so the figure can be overstated by just under a factor of two with no effect on any genuine proof. Peer review supplied a sharper result which testing confirms: because an odd final node is paired with itself, the tree over three leaves produces exactly the same root as the tree over those three plus a duplicate of the third, and the required proof depth is identical for three and four leaves. An anchorer can therefore state four records for a tree containing three, and every genuine proof still verifies. That means neither the exact count nor the uniqueness of the records is established, not merely that the count is loosely bounded. The more likely problem remains an honest mistake, since a count outside the correct bracket makes every real proof fail permanently with no correction path, which testing also confirmed. Impact is contained because the raw root stays readable, so records can still be verified off-chain against it.

Pending

#4 low Issue
A non-empty period can be recorded as empty and is then indistinguishable from a quiet one
HCOWLedger.sol
L148-156
L308-347
Description

A period with no records is anchored with a zero root and a zero count, which the design correctly uses to ensure the sequence can never contain a gap. The contract accepts that combination for any period unconditionally, and once written the slot can never be revised because the cursor has moved past it. The result is that an observer cannot distinguish a genuinely quiet period from one whose records were erased. The accidental route matters more than the malicious one: a transient failure in the data store returns an empty result, and the worker has no reason to treat empty as exceptional because empty periods are a normal expected input.

Pending

#5 low Issue
The record count is still chosen freely at anchoring time, up to the next power of two
HCOWLedger.sol
L106-122
L434-450
L469-503
Description

The response states that verification recomputes the count fold from the caller-supplied figure, so a restated count cannot verify. Verification recomputes the fold from the figure held in storage, not from one the caller supplies; the caller passes only an epoch, a leaf and a proof. What the fold does achieve is real and worth keeping: the anchored thirty two bytes now commit to the pair of root and count rather than to the tree alone, so a count cannot be restated behind an already-anchored root, and combined with the padding change a duplicated final record is no longer indistinguishable from a distinct one. What it does not achieve is the property the finding was about. Because a tree of n records and a tree of n padded up to the same power of two are byte-identical, and the required proof depth is identical across that whole bracket, the anchorer still chooses any count in the bracket at anchoring time and every genuine receipt still verifies. Measured on the pinned commit: three records anchored as four, with all three receipts verifying; and 513 records anchored as 1024, an inflation of 1.996 times, again with receipts verifying. That is the just under a factor of two the original finding stated, unchanged. Understating remains self-punishing and was also confirmed: a count below the bracket moves the required proof depth and every genuine receipt fails permanently, which is the honest-mistake mode. Impact is contained in the same way as before, because the anchored value and the count are both readable and every proof against them is sound, so this affects a published metric rather than membership integrity.

Pending

#6 low Issue
The same tree can be counted twice across the live and historical paths
HCOWLedger.sol
L216-232
L308-347
L354-427
L684-686
Description

The two replay maps are deliberately separate, and the reason the source gives is correct: writing the live map from the backfill path would let a stolen anchorer key make the next live epoch's root unusable by front running it, which would stop the ledger permanently. The consequence not stated is that the same anchored value can be submitted once on each path, because neither map sees the other. Measured on the pinned commit: one thousand records anchored as a live epoch and then the identical anchored value submitted as a historical batch covering pre-genesis time, both accepted, with the record total reporting two thousand. This is the same double count across the two counters that the first round measured through the range overlap, reached by a different route, and the range guard that closed that route does not close this one. It matters mainly because the source contains an explicit list of what the separation does not stop, written precisely so a reader is not left to discover the gaps, and this is not on it. Severity is unchanged from the finding it descends from: the anchored roots and every proof against them remain sound, and what is affected is an asserted metric.

optimization Issues | 1 findings

Resolved

#1 optimization Issue
Unchecked block is a gas optimisation, not a correctness requirement
HCOWLedger.sol
L505-520
Description

The proof depth helper wraps its loop in an unchecked block and justifies it by describing an addition that does not appear anywhere in the function; the comment appears to describe an earlier implementation. A reader is led to believe the block is load-bearing for correctness when it is a pure optimisation. The function itself was verified to terminate for every input and never to overflow, since the halving expression is bounded above by its own input, and the zero case is unreachable because the caller returns early on it.

informational Issues | 4 findings

Resolved

#1 informational Issue
Leaf hashing rule lives entirely off-chain despite the published domain constants
HCOWLedger.sol
L48-146
L526-650
Description

The stated design goal is that an independent verifier can read every part of the hashing rule from the chain rather than from a document that could be edited. The internal node rule meets that: the prefix constant is published and is actually used. The leaf rule does not. The two leaf domain constants are declared and then referenced nowhere in the contract, so the field order, encoding and separator rules must still be taken on trust from documentation. No second-preimage or leaf-versus-node collision path was found given the stated encoding, and the fixed-width concatenation used for internal nodes is free of packing ambiguity, so this is a transparency gap rather than an exploitable weakness.

Resolved

#2 informational Issue
Period index is not bound on-chain to the time range it covers
HCOWLedger.sol
L158-189
L655-682
Description

The genesis period is derived from the deployment timestamp, which is the right choice and leaves no meaningful manipulation surface given the one hour granularity. A mismatch between the deployed genesis and the worker's expectation fails safely, with a clear sequencing error. The residual risk is in the recovery behaviour: unlike a backfill batch, a live anchor stores no time range, so nothing on-chain ties a period index to the data inside it. A worker whose error handling is to read the cursor and retry with that value will submit one period's data under the next period's label, and every subsequent period inherits the offset undetectably. The period the contract is deployed in is also a partial hour, so the operator must decide explicitly whether records from before deployment belong to it.

Pending

#3 informational Issue
No distinct event is emitted for an attested empty period
HCOWLedger.sol
L237-249
L308-347
Description

The response states that a distinct event is emitted for an attested empty period so an indexer can alert on an unexpected one. No such event exists. The contract declares six events and none of them is specific to an empty period; the ordinary anchoring event is emitted in both branches. Verified by recording logs across an empty period anchor: exactly one event fires and its topic is the ordinary anchoring event. An indexer can still distinguish the case by comparing the root carried in that event against the published constant, which is sufficient but is not what was recommended and not what the response describes. The recommendation was a distinct event precisely so the alerting path does not depend on the consumer knowing a constant and keeping in step with it.

Pending

#4 informational Issue
The project's highest impact static analysis result is absent from the triage table
HCOWLedger.sol
L612-616
Description

Moving the leaf rule on chain introduced a new Slither detector at High impact and High confidence, reporting the packed encoding in the join over two adjacent variable length operands. It is a true positive as a pattern and a false positive as a vulnerability, and the argument is set out at length in the source. That argument was checked line by line and it holds. The escape function replaces the backslash, the tab and the newline, and the replacement bytes introduce neither a raw tab nor a raw newline, so the eight tabs and one newline in the join really are the only field boundaries. The escape is a prefix free code with an explicit decoder, and a literal backslash followed by t maps to three bytes while a real tab maps to two, so the two do not collide. Both halves were confirmed executably as part of the differential. The two outer calls that Aderyn also flags pair a fixed length seven byte constant with the join, so the boundary is recoverable by length alone and the detector's precondition is not met. What is missing is the triage entry. The table in SECURITY.md enumerates the reentrancy family, strict equality, division before multiplication, timestamp use, the pragma and cyclomatic complexity, and does not mention this detector at all, while the committed raw report prints it first with no annotation. A reader working from the triage table finds the project's highest impact result unexplained in the one document written to explain them.