As of 24 Oct 2026, ZNS Token Upgrade has 2 audit reports by 1 security firm on Trustblock; the latest is by Quantstamp, dated 17 Nov 2023. Those reports list no open critical findings.
Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Zero Name Service (ZNS) | Security firm: Quantstamp | Date: | Scope: Repository at commit 46f632e | No open critical findings | Open · certificate.quantstamp.com |
| ZNS Token Upgrade | Security firm: Quantstamp | Date: | Scope: Files named in the report | No open critical findings | Open · certificate.quantstamp.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 1 |
| Medium | 0 | 3 | 3 |
| Low | 1 | 6 | 10 |
The `ZNSRootRegistrar.registerRootDomain()` function does not validate if the provided name contains a dot. Consequently, if a user has registered a domain like `0://wilder`, another user could register `0://wilder.fundme`, which might mislead users into believing it is a legitimate subdomain of the original domain. This impersonation risk arises because the genuine subdomain registration logic resides solely in the `ZNSSubRegistrar.sol` contract. Furthermore, this oversight in validation extends to the `ZNSSubRegistrar.registerSubdomain()` function. An attacker can register multi-level subdomains all at once. As an example, given the registered domain `0://wilder`, an attacker could directly register a third-level subdomain like `0://wilder.2ld.3ld.4ld` without sequentially registering `0://wilder.2ld` and `0://wilder.2ld.3ld`.
Open the reportThe `getPrice()` function within the ZNSCurvePricer contract calculates the domain's price based on the `CurvePriceConfig`. If no pricing configuration is set for a domain, the system may inadvertently return a price of zero due to the default behavior of Solidity which initializes struct values to zero. When a user attempts to register a subdomain, the `ZNSSubRegistrar.registerSubdomain()` function, present in an external contract, invokes the `getPrice()` function. If the domain pricing configuration is missing or not correctly defined, the domain may be registered for free.
Open the reportThe `ZNSRootRegistrar.registerRootDomain()` function and `ZNSSubRegistrar.registerSubdomain()` subdomain registration function does not account for domain name string variations, especially concerning character casing. This lack of validation means that domain names, which appear identical after normalization, are recognized as distinct entities on-chain due to their differing hash representations. For instance, both `wilder` and `wIlder` can be registered as separate domains. However, when client-side normalization interprets both names as `wilder`, both domains could resolve to the same address, creating potential impersonation risks.
Open the reportThe `ZNSCurvePricer._getPrice()` function is designed to determine the price of domain labels based on their lengths, following an asymptotic decline in price as length increases. However, there is a flaw in the function `_validateConfig`. While the function is intended to return a price no less than the `minPrice`, an edge case exists when the domain length is exactly `maxLength`. This issue emerges because the validation function `_validateConfig` only checks the price at `maxLength - 1`, assuming the price at `maxLength` would always be above `minPrice`. Therefore, the returned price might be less than the specified `minPrice` and the protocol might unintentionally charge users less than the intended `minPrice` when they acquire domains of exact length `maxLength`. This can lead to potential revenue loss for the domain owner.
Open the reportThe `ZNSSubRegistrar` contract provides functionality for a domain owner/operator to update the resolver contract for a specific domain hash via the `ZNSRegistry.updateDomainResolver()` or `ZNSRegistry.updateDomainRecord()` function. If manipulated by a malicious owner or operator, they could redirect requests to a malevolent resolver that hijacks the intended flow of external contracts trying to resolve a ZNS domain address. Although the integrity of the protocol is not directly compromised, any third-party systems relying on the external resolver are susceptible to unexpected and potentially malicious behaviors. If users adhere to the `IZNSAddressResolver` interface, which mandates that `getAddress()` is a view function, the adverse potential of this issue is limited but still possible. The designation of `getAddress()` as a view function means the contract resolving the domain will use the `STATICCALL` opcode, which ensures that the state cannot be altered during the call. However, external protocols are not guaranteed to use `IZNSAddressResolver`.
Open the reportThe protocol permits domain owners to designate a beneficiary address for payments associated with a specific domain, identified by its `domainHash` in the `ZNSTreasury` through functions `setPaymentConfig()` and `setBeneficiary()`. However, if a beneficiary is not set for a `domainHash`, funds sent to purchase a subdomain under this parent `domainHash` will be directed to a zero address. While OpenZeppelin's ERC20 implementation includes checks against transfers to a zero address, the treasury is designed to accept any ERC20 tokens, which might not have such safeguards.
Open the reportAll privileged permissions should be clearly documented for users. The privileged permissions of the protocol contract are documented below. <br> The following are cases of `GOVERNOR_ROLE` privileges: - The `UUPS` function `upgradeToAndCall()` allows governors to update the implementation used and invoke a call in upgradeable contracts. - The `UUPS` function `upgradeTo()` allows governors to update the implementation used in upgradeable contracts. - The governors can grant `GOVERNOR_ROLE` to addresses. - The governors can grant `ADMIN_ROLE` to addresses. - The governors can grant any role to any address through the function `ZNSAccessController.setRoleAdmin()`. <br> The following are cases of `ADMIN_ROLE` privileges: - The function `setRegistry()` allows admins to update the `registry` address for contracts inheriting `ARegistryWired`: - `ZNSCurvePricer` - `ZNSRootRegistrar` - `ZNSSubRegistrar` - `ZNSAddressResolver` - `ZNSDomainToken` - `ZNSTreasury` - The function `ZNSRootRegister.setRootPricer()` allows admins to update the `pricer` contract used to determine pricing for root domains. - The function `ZNSRootRegister.setTreasury()` allows admins to update the `ZNSTreasury` contract used to store protocol fees and staked funds. - The function `ZNSRootRegister.setDomainToken()` allows admins to update the domain token contract used to validate domain ownership. - The function `ZNSRootRegister.setSubRegistrar()` allows admins to update the subdomain registrar contract. - The function `ZNSRootRegister.setAddressResolver()` allows admins to update the root domain resolver. - The admins can grant `REGISTRAR_ROLE` to addresses. - (Fix Review) The function `ZNSRegistry.addResolverType()` allows admins to add new resolver types to the mapping of resolver types and addresses. - (Fix Review) The function `ZNSRegistry.deleteResolverType()` allows admins to delete resolver types to the mapping of resolver types and addresses. <br> The following are cases of `REGISTRAR_ROLE` privileges: - The function `ZNSRootRegister.coreRegister()` allows registrars to register domains. - The function `ZNSRegistry.createDomainRecord()` allows registrars to register domain records which track ownership and address solver. - The function `ZNSDomainToken.register()` allows registrars to mint tokens which are used to validate domain ownership. - The function `ZNSDomainToken.revoke()` allows registrars to burn tokens to revoke domain ownership. - The function `ZNSTreasury.stakeForDomain()` allows registrars to process registration fee to beneficiaries and stake domain funds in the treasury. The staked funds are returned to the domain owner when the domain is revoked. - The function `ZNSTreasury.unstakeForDomain()` allows registrars to unstake domain registration funds in the treasury during the domain revocation process. - The function `ZNSTreasury.processDirectPayment()` allows registrars to process registration fees to beneficiaries directly. Based on documentation and discussion with the client, the `REGISTRAR_ROLE` is reserved for contracts `ZNSRootRegistrar` and `ZNSSubRegistrar` only. <br> The role `EXECUTOR_ROLE` does not have any privileges. Based on the code documentation, this role may be used for future implementations.
Open the reportThe implementation contracts may not consistently ensure their initializer is disabled. This vulnerability allows malicious actors to call the `initialize()` function on the implementation contracts. Though the `initialize()` function is guarded by the "initializer" modifier to ensure it is invoked only once, this protection is valid only from the proxy contract's storage layout. The logic contract remains uninitialized from its direct perspective because `initialize()` is not directly activated within it. Consequently, unauthorized entities can call the `initialize()` function and alter state variables of the implementation contract.
Open the reportWhen a domain is revoked via `ZNSRootRegistrar.revokeDomain()`, several actions occur: the access type is set to `AccessType.LOCKED`, its token is burnt, its registry record is removed, and staked funds, if any, are returned to the domain beneficiary. However, if a new owner purchases this domain and sets the `AccessType` to `MINTLIST`, the protocol will default to the previous owner's mintlist configuration, which is not cleared during revocation. The new domain owner, unaware, can be constrained by the previous owner's access settings until they use `updateMintlistForDomain()` to clear the previous allowance.
Open the reportEvery Solidity file specifies in the header a version number of the format `pragma solidity (^)0.8.*`. The caret (`^`) before the version number implies an unlocked pragma, meaning that the compiler will use the specified version *and above*, hence the term "unlocked".
Open the reportAlthough the new implementation contract has removed snapshot functionalities during token transfers, it is still possible to access `ERC20SnapshotUpgradeable` public functions `balanceOfAt()` and `totalSupplyAt()`.
Open the reportIt is crucial to validate inputs, even if the inputs come from trusted addresses, to avoid human error. A lack of robust input validation can only increase the likelihood and impact in the event of mistakes. Following is the list of places that can potentially benefit from stricter input validation: 1. qs-badge-fixed `MeowToken.sol#37`: the `amount` of the `transferBulk()` should be greater than zero. 2. qs-badge-fixed `MeowToken.sol#56`: the `amount` of the `transferFromBulk()` should be greater than zero.
Open the reportThe new implementation contains an initialize function that mints tokens to the initializer address. If minting is not needed and is used for testing only, the minting function should be removed from the initialize function. Otherwise, anyone can invoke the `initialize()` function of the implementation contract and mint arbitrary tokens. In general, leaving an implementation contract uninitialized can pose a security risk. To mitigate this risk, it is recommended to invoke the `_disableInitializers()` function in the constructor during deployment (see: [OpenZeppelin doc](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#initializing_the_implementation_contract)). This action will automatically lock the implementation contract, preventing any unauthorized usage.
Open the reportEvery Solidity file specifies in the header a version number of the format `pragma solidity (^)0.8.*`. The caret (`^`) before the version number implies an unlocked pragma, meaning that the compiler will use the specified version *and above*, hence the term "unlocked".
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="zns-token-upgrade"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us