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 can mint

It is possible to mint new tokens.

Contract owner can blacklist addresses

It is 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 can be burned

There is a function to burn tokens in 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 PrimeQualityCredit contract implements a MiCA-compliant ERC20 token for a Permissioned Ecosystem on Rootstock, with KYC-gated transfers, role-based admin and rate-limited relayer minting, an admin-only burn path for OTC fiat redemption, and a global emergency pause. The concentrated admin authority is an intentional design choice documented in the project specification (the admin key represents the issuing entity in cold storage and is the on-chain anchor for off-chain 1:1 EUR backing and AML enforcement) and is acknowledged accordingly. The implementation is internally consistent and follows widely-used OpenZeppelin patterns. While the overall design is sound, a few areas need attention:

  • The global pause and the emergency admin burn share the same code path. When the contract is paused, even the admin cannot burn tokens from a compromised holder - which is exactly the moment that capability is needed most. Refactor the burn path so admin burn bypasses both the pause guard and the whitelist guard via a private helper that calls super._update(from, address(0), amount) directly.
  • The same code path drives a temporary whitelist mutation in adminBurn (whitelist[from] is flipped to true, then restored). It is safe today because the underlying ERC20 has no transfer hooks, but the pattern silently breaks the invariant that whitelist[user] == true implies the user passed KYC, and would become exploitable if a recipient hook (ERC777, compliance attestor) is ever added. The same private-helper refactor fixes both issues.
  • When the admin lowers the daily mint limit, the running counter mintedInWindow is not clamped to the new limit. The next mint is still correctly rejected, but the on-chain state remains visibly inconsistent until the 24-hour window rolls over, which complicates dashboards and monitoring.

Ownership Privileges

The ownership of the contract has been concentrated by design in the issuing entity, in line with the MiCA-compliant Permissioned Ecosystem architecture documented in the project specification. Authority is structured around four distinct roles managed through OpenZeppelin AccessControl. The deployer is auto-whitelisted so it can immediately receive minted tokens. The owner retains full privileges including:

  • DEFAULT_ADMIN_ROLE (cold storage, issuing entity) - full administrative control: grant and revoke any role, adjust the daily mint limit, mint without rate limit, and burn tokens from any address (intended for OTC fiat redemption).
  • WHITELISTER_ROLE - adds addresses to the KYC whitelist (single or batch); intended for the automated KYC webhook relayer.
  • PAUSER_ROLE - pauses and resumes all token movements globally for incident response or regulatory order.
  • MINTER_ROLE - mints to whitelisted addresses subject to the daily 50,000 PQC rate-limit; intended for the backend payment relayer that processes off-chain fiat settlement.
  • The owner cannot upgrade the contract - the bytecode is immutable, no proxy pattern is used.
  • The owner cannot impose transfer fees - no fee mechanism is implemented and _update forwards the original amount untouched.
  • The owner cannot mint to addresses that are not on the KYC whitelist - the recipient check in _update applies to admin and minter alike.
  • The owner cannot bypass the daily rate-limit when minting via MINTER_ROLE - only the DEFAULT_ADMIN_ROLE path is exempt by design.

Security Features

The contract implements several positive security features:

  • KYC whitelist enforcement on every mint, transfer, and burn through the overridden _update hook, an ERC-3643 inspired transfer-restriction pattern that prevents the token from circulating to non-verified or sanctioned addresses.
  • A configurable per-24-hour minting cap on the MINTER_ROLE that acts as a circuit breaker against a compromised relayer key, with the cap adjustable only by the cold-storage admin.
  • Role separation across four distinct AccessControl roles (admin, whitelister, pauser, minter), supporting hot-wallet versus cold-wallet operational separation and removing the single-key authority model at the operational layer.
  • Global pause-and-resume control gated by a dedicated PAUSER_ROLE, plus ReentrancyGuard on the mint path. The contract intentionally exposes no user-facing burn or redemption function, in line with the MiCA-compliance requirement that redemption flows through regulated off-chain channels only.

Recommended Operational Hardening

Although the centralized authority and discretionary administration are part of the intentional Permissioned Ecosystem design, two defense-in-depth measures are still strongly recommended for production: hold DEFAULT_ADMIN_ROLE exclusively through a multi-signature wallet (for example a 3-of-5 cold-storage Safe) backed by a Timelock contract, and introduce a hard cap on setDailyMintLimit so that even a compromised admin key cannot raise the rate-limit beyond a recoverable bound.

Note - This Audit report consists of a security analysis of the PrimeQualityCredit smart contract. This analysis did not include economic analysis of the contract's tokenomics. Moreover, we only audited the main contract for the PrimeQualityCredit 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

medium Issues | 1 findings

Resolved

#1 medium Issue
Global pause blocks even admin emergency burn
PrimeQualityCredit.sol
L171-180
L258-274
L309-318
Description

When the contract is paused via `PAUSER_ROLE`, the overridden `_update` reverts on every transfer and burn because it calls `_requireNotPaused()` first. `adminBurn` goes through `_burn` -> `_update` and therefore reverts too. The documented purpose of the pause is to freeze user activity during an exploit; in that exact scenario the admin may need to burn an attacker's balance, but the design prevents it without first unpausing - which would re-enable the exploit. This couples the emergency-response surface to the freeze surface in an unsafe way and is the same code path that motivates the temporary whitelist mutation in PQC-T-M-002.

low Issues | 4 findings

Resolved

#1 low Issue
Lowering daily limit does not clamp the running counter
PrimeQualityCredit.sol
L224-228
Description

The admin can call `setDailyMintLimit(newLimit)` with a value smaller than the current `mintedInWindow`. Subsequent `MINTER_ROLE` mint attempts will still revert correctly because the require check uses the new limit, but the storage value `mintedInWindow > dailyMintLimit` is observable until the next window rollover. This was confirmed by a stateful invariant run that produced a sequence (mint then setLimit) leaving the system in this state. There is no funds-at-risk, but the inconsistency complicates off-chain monitoring and is a code-smell.

Acknowledged

#2 low Issue
Single-step admin role with no backup grant in constructor
PrimeQualityCredit.sol
L109-118
Description

The constructor grants `DEFAULT_ADMIN_ROLE` only to the deployer (`msg.sender`). If that key is lost or revoked by mistake (DEFAULT_ADMIN_ROLE can revoke itself), the contract becomes unrecoverable - no further role changes, mint-limit adjustments, or admin burns will be possible. The pause/unpause and minter mints can continue from already-granted role holders, but the contract is effectively unmanageable.

Acknowledged

#3 low Issue
Block timestamp dependency for the rate-limit window
PrimeQualityCredit.sol
L235
L286
Description

`block.timestamp >= windowStart + WINDOW_DURATION` is used to decide when to roll the window. On Rootstock the block time is ~30s and the validator can drift the timestamp by a small amount, so the window can effectively shift by seconds. This is not exploitable for a 24h-scale limit but is worth flagging as a design consideration.

Resolved

#4 low Issue
Admin burn temporarily mutates whitelist state to bypass `_update` check
PrimeQualityCredit.sol
L258-274
Description

`adminBurn` flips `whitelist[from] = true`, calls `_burn`, then restores the original value. Today this is safe because the overridden `_update` does no external calls and the underlying ERC20 has no transfer hooks. The pattern is dangerous in design: any future change adding a recipient hook (for example switching to ERC777 or adding an external pre-burn callback) would expose a window where a de-listed user appears whitelisted from any other contract reading the mapping, and the invariant `whitelist[user] == true implies user passed KYC` is silently broken inside the call. The cleanest fix is shared with PQC-T-M-003.

optimization Issues | 7 findings

Resolved

#1 optimization Issue
Replace string-based requires with custom errors
PrimeQualityCredit.sol
L131
L132
L144
L158
L199
L202
L203
L259
L260
L290
L312
L315
Description

The contract uses long descriptive `require` strings for revert messages. Custom errors (Solidity 0.8.4+) are both cheaper at deploy time (no string in the runtime bytecode) and cheaper at revert time (4 bytes of selector instead of an ABI-encoded string). The semantic meaning is preserved and tooling support is now mature.

Resolved

#2 optimization Issue
Loop uses post-increment and checked arithmetic
PrimeQualityCredit.sol
L143-149
Description

The for-loop in `batchAddToWhitelist` uses `i++` (post-increment) and the default checked-arithmetic for the increment. Pre-increment with an unchecked block is the standard cheap-loop idiom in Solidity 0.8.x.

Resolved

#3 optimization Issue
Storage slot read twice per iteration
PrimeQualityCredit.sol
L143-149
Description

Inside `batchAddToWhitelist`, the expression `accounts[i]` is read three times per iteration (require, if-check, assignment). Caching the value in a local cuts two calldata loads per iteration.

Resolved

#4 optimization Issue
`mintedInWindow += amount` can be unchecked
PrimeQualityCredit.sol
L294
Description

The `mintedInWindow + amount` value is already bounded by the immediately-preceding require check. The Solidity 0.8 overflow check on the next line is redundant and can be elided with `unchecked`.

Resolved

#5 optimization Issue
`nonReentrant` on `adminMint` is unnecessary
PrimeQualityCredit.sol
L197
Description

ReentrancyGuard adds a storage read and write on every protected call. `adminMint` only calls `_mint` (internal), `_enforceDailyLimit` (internal), `hasRole` (internal storage read), and emits an event. There is no external call surface that could lead to reentrancy, so the guard is dead weight.

Resolved

#6 optimization Issue
Constants are private and not exposed via getter
PrimeQualityCredit.sol
L73
L76
Description

Both constants are `private` and therefore invisible to external callers. Public visibility costs nothing for constants and improves observability for explorers and dashboards.

Resolved

#7 optimization Issue
Repeated `whitelist[accounts[i]]` reads from storage
PrimeQualityCredit.sol
L143-149
Description

The mapping read `whitelist[accounts[i]]` happens twice per iteration (the if and the assignment side-effect). Caching the read saves one SLOAD per already-whitelisted entry.

informational Issues | 10 findings

Acknowledged

#1 informational Issue
Centralized admin authority - acknowledged Permissioned Ecosystem design
PrimeQualityCredit.sol
L197-212
L224-228
L258-274
Description

The DEFAULT_ADMIN_ROLE bypasses the daily mint rate limit (line 206), can set the limit to any value with no upper cap (line 224), and can burn tokens from any address (line 258). The project specification (Sections 3 and 5) documents this concentration of authority as an intentional design choice for the MiCA-compliant Permissioned Ecosystem on Rootstock - the admin key represents the issuing entity in cold storage and is the on-chain anchor for off-chain 1:1 EUR backing, OTC redemptions, and AML enforcement. The associated operational risk is acknowledged design rather than a code defect; the recommendation below is defense in depth.

Resolved

#2 informational Issue
Documentation calls the limit "rolling" but implementation is fixed-window
PrimeQualityCredit.sol
L31-32
L281-295
Description

Doc comments describe `dailyMintLimit` as a "rolling 24-hour window" rate limit. The implementation is actually a fixed-window counter that resets at most once per 24 hours - not a sliding window. The two semantics differ: a fixed window allows up to 2x the cap to be minted across a boundary (one full cap right before reset, one full cap right after). This is not a vulnerability but a wording clarity issue with operational implications.

Resolved

#3 informational Issue
Floating pragma `^0.8.24`
PrimeQualityCredit.sol
L2
Description

The pragma `^0.8.24` allows any patch version of Solidity 0.8.24 or newer. While Solidity 0.8.x guarantees backward compatibility, pinning to an exact version is the recommended hygiene for an audited production contract.

Pending

#4 informational Issue
Two different pragma constraints across the project
PrimeQualityCredit.sol
L37 (whole contract)
Description

OpenZeppelin contracts in `lib/openzeppelin-contracts/` declare `pragma solidity ^0.8.20`, while project files declare `pragma solidity ^0.8.24`. The compiler picks the most restrictive constraint and compiles correctly, but inconsistent pragmas across a codebase is a known source of confusion.

Resolved

#5 informational Issue
Custom events `AdminMinted`/`AdminBurned` index only the address
PrimeQualityCredit.sol
L89
L92
Description

Both events index only the address parameter. This is the conventional choice and is correct - `amount` does not benefit from being indexed. Noted for completeness.

Pending

#6 informational Issue
Off-chain monitoring should track on-chain rate-limit state directly
PrimeQualityCredit.sol
L234-242
L286-289
Description

The `dailyMintRemaining` view does not advance the window in storage; it only computes the visible remaining amount based on the current state. Off-chain consumers should be aware that what they see and what the next mint will compute may differ for a few seconds around the window boundary.

Pending

#7 informational Issue
No EIP-2612 `permit` support
PrimeQualityCredit.sol
L37 (whole contract)
Description

Users wanting to interact with DeFi or a reward distributor must perform a separate `approve` transaction, which costs gas and adds a UX step. EIP-2612 `permit` allows signed approvals batched into one transaction.

Pending

#8 informational Issue
Decimals defaults to 18 - confirm this matches the EUR ledger ratio
PrimeQualityCredit.sol
L108
Description

The contract does not override `decimals()`, so it inherits the ERC20 default of 18. The architecture comment claims a 1:1 EUR ratio. Make sure the relayer's payment-to-mint conversion uses the same denomination, otherwise rounding bugs may creep in.

Acknowledged

#9 informational Issue
Unbounded array length in `batchAddToWhitelist`
PrimeQualityCredit.sol
L142-150
Description

`batchAddToWhitelist` iterates over a calldata array of unrestricted length. A misconfigured automation that submits a too-large array exhausts the block gas limit and the transaction reverts. There is no economic incentive for an external attacker since the function is role-gated.

Acknowledged

#10 informational Issue
Internal `_enforceDailyLimit` does not validate `amount > 0`
PrimeQualityCredit.sol
L285-295
Description

The internal `_enforceDailyLimit` does not verify `amount > 0`. The public `adminMint` does check this, so today there is no exploit, but the helper is fragile to future refactors. Defensive coding suggests validating the precondition where the work is done.