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 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:
- 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 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 thatwhitelist[user] == trueimplies 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
mintedInWindowis 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
_updateforwards the original amount untouched. - The owner cannot mint to addresses that are not on the KYC whitelist - the recipient check in
_updateapplies 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
_updatehook, 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
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
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
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
`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
`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
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
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
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
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
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
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
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
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
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`
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
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
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
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
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
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`
`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`
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.