PQC Info

A utility token built by a Bitcoiner in the Alps. Powered by Bitcoin. For the world.

PQC Logo

Team and KYC Verification

The KYC verification for this project is currently in progress.

The team has submitted their information and verification is pending.

KYC Badge

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.

68.51
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/05/04
Revision date 2026/05/05

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.

Contract owner cannot mint

It is not possible to mint new tokens.

Contract owner cannot blacklist addresses.

It is not possible to lock user funds by blacklisting addresses.

Contract owner cannot set high fees

The fees, if applicable, can be a maximum of 25% or lower. The contract can therefore not be locked. Please take a look in the comment section for more details.

Token transfer can be locked

Owner can lock user funds with owner functions.

Token cannot be burned

There is no burning within the contract without any allowances

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

Contract Analysis

The PQCRewardClaim contract implements an epoch-based Merkle-proof reward distributor that gates claims behind the KYC whitelist of the PrimeQualityCredit token. Each epoch is independently funded and tracked, claims are paid by the user (the user pays their own gas), and the admin can deactivate epochs and recover residual funds. The project specification documents reward distribution as discretionary ecosystem grants and explicitly authorizes the admin to recover unclaimed funds; the accounting and proof-verification logic is correct and was confirmed by symbolic execution and stateful invariant fuzzing. While the overall design is sound, a few areas need attention:

  • Although discretionary recovery is part of the intentional design, an owner can call deactivateEpoch and recoverFunds for the same epoch in a single transaction, which means users who fetch a Merkle proof off-chain do not see the on-chain timing decisions until the epoch is gone. As defense in depth, consider a minimum claim window (for example 30 days) before deactivateEpoch becomes callable, OR a timelock between deactivation and recovery, with a clearly-monitorable event.
  • Similarly, the owner can replace an epoch's Merkle root via updateEpochRoot as long as no claims have happened yet. Within the discretionary model this is acknowledged; pair it with an off-chain notification policy or a timelock so users have a published correction window.
  • The contract is documented as token-agnostic by design, but it does not check that a candidate reward token has standard ERC20 transfer semantics. Fee-on-transfer or rebasing tokens will break the bookkeeping invariant that funded equals the amount actually transferred in. Either maintain an admin-curated allowlist of accepted token addresses, OR fund the epoch by the actual post-transfer balance delta, OR explicitly document the supported token semantics in the operator runbook.

Ownership Privileges

The ownership of the contract has been assigned to the issuing entity through the single-step OpenZeppelin Ownable pattern. There is no two-step ownership handover and no built-in multi-signature requirement at the contract level (operational multi-sig is recommended). The owner retains full privileges including:

  • Creating new reward epochs by uploading a Merkle root and depositing the funding amount via createEpoch.
  • Updating an epoch's Merkle root via updateEpochRoot as long as no user has yet claimed against that epoch (acknowledged discretionary capability).
  • Deactivating any active epoch via deactivateEpoch, which immediately stops further claims for that epoch (acknowledged discretionary capability).
  • Recovering remaining funds from a deactivated epoch to any chosen address via recoverFunds (acknowledged discretionary capability for unclaimed grants).
  • The owner cannot mint or burn tokens - the contract holds and moves only.
  • The owner cannot directly blacklist a claimant - that capability lives on the PrimeQualityCredit token via removeFromWhitelist; the distributor only inherits the exclusion through its on-chain whitelist read.
  • The owner cannot bypass the per-user-per-epoch claim flag - a user that has claimed cannot be made to claim again and cannot be reimbursed via this contract.
  • The owner cannot upgrade the contract - the bytecode is immutable and the pqcToken dependency is set as immutable at construction.

Security Features

The contract implements several positive security features:

  • Merkle proof verification with double-hashed leaves (keccak256(keccak256(abi.encode(addr, amount)))), the recommended defense against second pre-image attacks on Merkle trees.
  • ReentrancyGuard on both claim and recoverFunds, blocking single-call reentry through ERC20 transfer callbacks even if a non-standard reward token is ever introduced.
  • A persistent per-user-per-epoch claim flag (hasClaimed), which makes double-claiming structurally impossible for the lifetime of the contract.
  • A pre-transfer accounting check (epoch.totalClaimed + amount <= epoch.totalFunded) on every claim, plus a per-epoch conservation invariant that was independently verified by stateful invariant testing across thousands of randomized sequences (no overclaim, no insolvency).

Recommended Operational Hardening

The discretionary nature of the program is acknowledged, but two defense-in-depth measures are still strongly recommended: switch from Ownable to Ownable2Step so an ownership transfer requires the recipient to confirm (preventing accidental lockout), and apply the same multi-signature plus timelock recommendation as for the token's DEFAULT_ADMIN_ROLE so that no single key compromise translates to unilateral epoch teardown. Aligning the on-chain admin actions with a published off-chain notice policy further strengthens user trust without changing the discretionary nature.

Note - This Audit report consists of a security analysis of the PQCRewardClaim smart contract. This analysis did not include economic analysis of the contract's tokenomics. Moreover, we only audited the main contract for the PQCRewardClaim team. Other contracts associated with the project were not audited by our team. We recommend investors do their own research before investing.

Files and details

Findings and Audit result

low Issues | 6 findings

Acknowledged

#1 low Issue
Single-step `Ownable` instead of `Ownable2Step`
PQCRewardClaim.sol
L39
Description

The contract uses `Ownable`, which transfers ownership in a single transaction. A typo in the new owner address permanently locks out the admin functions, and there is no way to recover. `Ownable2Step` requires a two-call handshake that catches address mistakes before they take effect.

Acknowledged

#2 low Issue
`nonReentrant` is not the first modifier on `recoverFunds`
PQCRewardClaim.sol
L180
Description

Modifiers run in the order they are declared. `recoverFunds` declares `onlyOwner nonReentrant`. Today this is fine, but the convention - and what most static analyzers expect - is `nonReentrant` first to bound reentrant cost as early as possible.

Acknowledged

#3 low Issue
`recoverFunds` overwrites `totalClaimed` to `totalFunded`
PQCRewardClaim.sol
L188
Description

`recoverFunds` sets `epoch.totalClaimed = epoch.totalFunded` to make `remainingBalance` return zero after recovery. This conflates user claims with admin recovery in the same field, so dashboards and audit trails cannot tell the two apart by reading on-chain state alone.

Acknowledged

#4 low Issue
Token-agnostic design has no compatibility check for non-standard ERC20 semantics
PQCRewardClaim.sol
L126-148
L233
Description

The project specification (Section 6) documents the Merkle distributor as token-agnostic by design - the broad acceptance of any ERC20 address itself is correct. However, no compatibility check is performed on the token's transfer semantics. For non-standard ERC20s (fee-on-transfer, rebasing, deflationary) the bookkeeping invariant `funded == amount actually transferred in` does not hold: `safeTransferFrom(msg.sender, address(this), 1000)` may deliver only 950 (fee) or vary over time (rebase), while the epoch records 1000 funded. The first claim succeeds, then later claims revert at the underlying ERC20 with an unclear error. Operator typos that point at a non-ERC20 contract or an EOA also revert at the `safeTransferFrom` step with no early sanity check.

Acknowledged

#5 low Issue
Owner can deactivate epoch and immediately recover unclaimed funds - acknowledged discretionary design
PQCRewardClaim.sol
L169-192
Description

An owner can call `deactivateEpoch(epochId)` and `recoverFunds(epochId, ownerAddress)` in the same transaction, transferring all unclaimed rewards out before any user has claimed. The project specification (Section 6) documents reward distribution as discretionary ecosystem grants and explicitly authorizes the admin to recover unclaimed funds from deactivated epochs. Within that documented model, no user has a guaranteed claim on a published Merkle leaf, so this is acknowledged as design rather than a misappropriation of user funds. The remaining concern is purely UX and trust: users who fetch a proof off-chain do not see the on-chain timing decisions until the epoch is gone, so the program looks more arbitrary than it has to.

Acknowledged

#6 low Issue
Owner can swap epoch Merkle root before any claim - acknowledged discretionary design
PQCRewardClaim.sol
L156-162
Description

The owner can replace an epoch's Merkle root via `updateEpochRoot` as long as no claims have happened yet. The project specification documents reward distribution as discretionary ecosystem grants, so the admin's ability to redirect or correct an unclaimed allocation is consistent with the documented discretion - acknowledged as design. Users who fetch a proof off-chain do not see when the root changes, which can erode trust if the change is not pre-announced.

optimization Issues | 5 findings

Acknowledged

#1 optimization Issue
Replace string-based requires with custom errors
PQCRewardClaim.sol
L111
L131
L132
L133
L157
L158
L159
L170
L182
L183
L186
L213
L214
L215
L216
L222
L226
Description

The contract is full of string-based revert messages. Custom errors reduce deployment size, are cheaper at runtime, and produce a structured 4-byte selector that frontends can match without parsing strings.

Acknowledged

#2 optimization Issue
Storage struct fields read multiple times
PQCRewardClaim.sol
L180-192
L255-258
Description

`recoverFunds` reads `epoch.totalFunded` and `epoch.totalClaimed` on line 185 and again on line 188; `claim` reads `epoch.totalClaimed`, `epoch.totalFunded`, and `epoch.rewardToken` two or three times each. Caching reduces gas by one SLOAD per repeated access.

Acknowledged

#3 optimization Issue
Re-cast `IERC20(epoch.rewardToken)` per call
PQCRewardClaim.sol
L189
L233
Description

Both `recoverFunds` and `claim` perform `IERC20(epoch.rewardToken).safeTransfer(...)`. The cast is a no-op but the read of `epoch.rewardToken` is a separate SLOAD that can be cached.

Acknowledged

#4 optimization Issue
Pack `Epoch` struct fields more efficiently
PQCRewardClaim.sol
L207-236
Description

The Epoch struct uses four storage slots. With reasonable bounds on funded amounts, the two `uint256` accountings can be packed into a single slot using `uint128`. The trade-off is a maximum per-epoch funding of about 2^128 - 1 wei (more than enough for any realistic distribution).

Acknowledged

#5 optimization Issue
`epoch.totalClaimed = epoch.totalFunded` is two SLOADs and one SSTORE
PQCRewardClaim.sol
L188
Description

Reading `epoch.totalFunded` to assign it back to `epoch.totalClaimed` issues two SLOADs. With the local-variable caching from O-002 this becomes one SSTORE.

informational Issues | 8 findings

Acknowledged

#1 informational Issue
Cross-contract read to `pqcToken.whitelist()` before state changes
PQCRewardClaim.sol
L207-236
Description

Static analysis flags the external call `pqcToken.whitelist(msg.sender)` followed by state changes as a reentrancy pattern. The call goes to a trusted, immutable token contract over a `view` function and the function is reentrancy-guarded, so there is no exploit surface today. The flag is structural and is mitigated by the trust assumption.

Resolved

#2 informational Issue
Double-hashed Merkle leaf for second pre-image resistance
PQCRewardClaim.sol
L219
L274
Description

The leaf is computed as `keccak256(bytes.concat(keccak256(abi.encode(addr, amount))))`. This double-hash is the recommended defense against second pre-image attacks where an attacker tries to forge a leaf by constructing a 64-byte string that collides with an internal node hash.

Acknowledged

#3 informational Issue
PUSH0 opcode emitted by Solidity 0.8.20+ - confirm RSK target supports it
PQCRewardClaim.sol
L2
Description

Solidity 0.8.20 and later emit the PUSH0 opcode (EIP-3855) by default unless the EVM version is explicitly downgraded. Some EVM-compatible chains (older Rootstock forks, certain L2s, BNB pre-fork) historically rejected PUSH0. Verify the target chain is current before relying on the default compiler settings.

Acknowledged

#4 informational Issue
No EIP-712 / meta-transaction support for claims
PQCRewardClaim.sol
L207-236 (whole function)
Description

Each user pays their own gas to claim. For an ecosystem reward, you may want a sponsored claim path. Adding EIP-2771 trusted forwarder support or an EIP-712-signed claim function would enable this.

Acknowledged

#5 informational Issue
Epoch IDs start at 1, not 0
PQCRewardClaim.sol
L76
L135
Description

`currentEpoch` starts at 0 and the first call to `createEpoch` produces epoch 1 via `++currentEpoch`. Off-chain consumers that index from 0 will read an empty struct for epoch 0.

Acknowledged

#6 informational Issue
`verifyProof` uses `epochs[epochId].merkleRoot` without validating epoch existence
PQCRewardClaim.sol
L268-276
Description

If `epochId` does not exist, `epochs[epochId].merkleRoot` is `bytes32(0)`. `MerkleProof.verify` then returns false for any proof, so the function is not unsafe. A clearer error would help frontend developers debug.

Acknowledged

#7 informational Issue
Operational dependency: distributor must be whitelisted in PrimeQualityCredit before `createEpoch`
PQCRewardClaim.sol
L129-148
Description

`createEpoch` deposits the reward token into the distributor via `safeTransferFrom`. Because PrimeQualityCredit's `_update` enforces whitelist on the recipient, the distributor itself must be added to the whitelist before any epoch is funded. This is purely operational, not a code defect.

Acknowledged

#8 informational Issue
No emergency pause mechanism on the distributor
PQCRewardClaim.sol
L39 (whole contract)
Description

There is no native pause on the distributor. If a Merkle proof problem is discovered mid-claim cycle, the only mitigation is per-epoch `deactivateEpoch`. The token-level pause already covers the catastrophic case (claim transfers will revert), so this is a defense-in-depth nice-to-have rather than a defect.