Team and KYC Verification
The KYC verification for this project is currently in progress.
The team has submitted their information and verification is pending.
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.
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
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:
- Specification Review: Analyze the provided specifications, source code, and instructions to fully understand the smart contract's size, scope, and functionality.
- Manual Code Examination: Conduct a thorough line-by-line review of the source code to identify potential vulnerabilities and areas for improvement.
- Specification Alignment: Ensure that the code accurately implements the provided specifications and intended functionalities.
- Test Coverage Assessment: Evaluate the extent and effectiveness of test cases in covering the codebase, identifying any gaps in testing.
- Symbolic Execution: Analyze the smart contract to determine how various inputs affect execution paths, identifying potential edge cases and vulnerabilities.
- Best Practices Evaluation: Assess the smart contracts against established industry and academic best practices to enhance efficiency, maintainability, and security.
- 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
deactivateEpochandrecoverFundsfor 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) beforedeactivateEpochbecomes callable, OR a timelock between deactivation and recovery, with a clearly-monitorable event. - Similarly, the owner can replace an epoch's Merkle root via
updateEpochRootas 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
updateEpochRootas 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
pqcTokendependency is set asimmutableat 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
claimandrecoverFunds, 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`
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`
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`
`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
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
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
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
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
`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
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
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
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
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
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
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
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
`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
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`
`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
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.