Resonance Security reviewed Freename on 18 Mar 2023: 24 findings, 3 critical; 18 marked fixed, 6 open.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 3 |
| High | 0 | 0 | 3 |
| Medium | 2 | 0 | 4 |
| Low | 4 | 0 | 8 |
The FNSRegistry contract overloads three different versions of the mint() function. Two of these functions do not possess access control and can be called by any account. This effectively means that the minting process, which should be protected and accessible only by the _mintingManager, can be bypassed, and any token can be minted freely.This may result in denying legitimate users the ability to have their bought domains minted by the minting manager.
The function _transferOwnership() does not revoke the roles of the previous owner, i.e. the previous owner keeps the DEFAULT_ADMIN_ROLE. With this role, the previous owner may add himself back or other malicious users as minters and takeover the system completely.
The BaseForwarder smart contract and its child contracts provide a custom implementation of the ERC2771. Essentially, it allows an untrusted third-party address to pay for a transaction performed by a previously set trusted forwarder. This mechanism uses signature verification to validate that the meta transactions are indeed authorized.The custom verification implemented on the function _verify() requires the signed message to contain the following values:Request's data;Target contract of the transaction;Request's nonce;Blockchain ID.The vulnerability identified in this implementation is the fact that the provided nonce is not global and varies depending on the provided tokenId argument. Specifically, each time a transaction is performed via a trusted forwarder, the previous nonce for the tokenId provided as an input parameter is invalidated. This means that, at any specific time, several tokens can have the same current nonce. And since the tokenId is not part of the signed message, it can be freely modified by an attacker that attempts to use the same signature on a future transaction.Moreover, not every function call of the target contracts deals with tokens and requires a tokenId as an input parameter. In this case, this particular vulnerability becomes more apparent and dangerous. Examples of such functions are disableBlocklist(), pause(), sendToBridgeWithTokenOwnerSignature(), etc.As an example, the following proof of concept can be considered:A meta transaction is performed legitimately via a trusted forwarder. This transaction will have have a nonce equal to 0 and associated to the tokenId 1000.An attacker calls the function execute() with the same signature and the same arguments, except that it modifies the associated tokenId to 1337, which is provided as an input parameters. The current nonce for this tokenId is also 0.The verification of the signed message does not take into account the different tokenId, but it does verify that the nonce is valid. Since the current nonce of the tokenId 1337 is also 0, then the signature is validated, just as it was for the legitimate transaction with the tokenId 1000.
The state variables _mintingManager and _propertyManager on the FNSRegistry contract are initialized once and can never be modified again. This effectively means that if the accounts associated with the addresses stored on the state variables become compromised, the entire protocol may be at risk.
The functions initialize() on FNSRegistry and MintingManager possess input parameters that require the other smart contract to already be deployed, creating a dangerous loop on the input parameters, i.e:The initialize() function on MintingManager contains the input parameter fnsRegistry_ which should contain FNSRegistry's address.The initialize() function on FNSRegistry contains the input parameter mintingManager which should contain MintingManager's addressOn non-upgradeable smart contracts, these contracts could never be deployed since the constructor is executed during the deployment and the input parameters are not yet know during deployment.On upgradeable smart contracts where initializer functions are used, it is possible to first deploy the contracts and only initialize them afterwards with the necessary input parameters. However, this opens up the possibility of malicious users frontrunning the call to the initialize() function and completely take over the smart contracts. This is especially dangerous when the source code of the protocol is already know and it is being deployed on new blockchains.
The function reset() virtually clears all records of the token by changing its preset. This function does not implement access control and can be called by any account. This effectively means that any user can clear the records of other users's tokens.
An implementation contract should not be left uninitialized. An uninitialized implementation contract can be taken over by an attacker, which may impact the proxy.The contracts FNSRegistry and MintingManager implementations do not disable contract initialization with the usage of the function _disableInitializaers().
The Blocklist smart contract is used to provide a blocking feature to the minting of tokens. Once a token is minted and blocked, purposely or by mistake, it is impossible to unblock it since there is no unblock feature implemented.
The functions _setManyRecords(), _setManyRecordsByHash(), _setManyProperties(), and _setManyPropertiesByHash() do not validate the input arrays keys and values. It is possible for these functions to be called with mismatching input array lengths and allowing transactions to occur on only a subset of the input elements.
The system allows for the minting of the NFT tokenId 0. Throughout the code, there are no validations to account for the existence of such a tokenId. Due to this fact, several scenarios are enabled that do not reflect accurate information on correct possibilities. For example, when the tokenId 0 is minted, every user that has not yet set a default domain, will by default have this token as its default domain.
The ProxyReader contract inherits from upgradeable versions of OpenZeppelin's smart contracts. These versions have their constructor replaced with initializer functions that are not called automatically as constructors are, and as such, will not be initialized.Smart contracts that are meant to be upgraded should use OpenZeppelin's upgradeable versions of the contracts. On the contrary, smart contracts that are not meant to be upgraded should use the non-upgradeable version of the respective libraries and interfaces.
The ERC165 standard implementes the function supportsInterface() to verify whether the respective smart contract identifies and implements any given interface. Specifically, the standard requires that the implementing contract will have a supportsInterface() function that returns:true when interfaceID is 0x01ffc9a7 (EIP165 interface)false when interfaceID is 0xfffffffftrue for any other interfaceID this contract implementsfalse for any other interfaceIDOn this system, the function supportsInterface() does not validate all required interfaces inherited and implemented by the smart contract, therefore allowing the function to yield erroneous results for certain interfaces. Incorrect results of this function break the compatibility and consistency of the ERC165 standard across the blockchain.
The functions _init() and _init_unchained() are an implementation of OpenZeppelin's Upgradeability design standard and are used to serve as the constructor of upgradeable contracts. Despite being both used as initializers, they possess slightly different use cases to correctly implement the analog linearization of smart contract constructors.The function _init() should be used and implemented to embed the linearized calls to all parent initializers. The function _init_unchained() should be used to perform the initialization of the variables for the current contract only.As a consequence of this design, it is possible that, due to the lack of automatic linearization, the calling of _init() functions initializes the same contract multiple times. For these cases, the _init_unchained() function may be used manually to avoid double initialization, however it will break the upgradeability design and compatibility between smart contracts on the blockchain.Additionally, the inherited smart contracts should be initialized according to their inheritance order, from the most base-like to the most derived.The functions initialize() on FNSRegistry and MintingManager do not follow the Upgradeability design standard in several accounts:_init_unchained() functions are being called directly instead of their _init() counterparts;Initialization according to inheritance order is incorrectMissing initialization of certain contracts
The functions _setRecord() and _setProperty() do not validate the input parameters key and value. It is possible for these functions to be called with empty strings on the input parameters and allowing transactions to occur that simply waste gas.
The functions _setRecord(), _size(), _getAllKeysHashed(), _setProperty(), _propertiesSize(), and _getAllPropertyKeysHashed(), perform several calls to the same functions _presetOf() and _propertyPresetOf() yielding the same result on every call. This coding pattern requires additional gas, therefore making the users of the smart contract incur fees that can be avoided.
The function _beforeTokenTransfer() does not perform validation during token transfers, enabling the possibility of clearing the default domain even if it is not the one being transferred.
The following functions were found to be unused within the system:_getFullNameUri()Unused functions increase the complexity and readability of the smart contract's code and their inclusion should be discouraged whenever possible.
Sending Ether to other addresses is possible on EVM smart contracts through several methods. Three of these methods rely on the following function calls:transfer() - Sends Ether attached with 2300 gas and reverts on error.send() - Sends Ether attached with 2300 gas and returns false on error.call() - Sends Ether with a specified amount of gas and returns false on error.The function call() is the most flexible and currently it is the recommended way to transfer Ether since it does not put a limit on the gas used by the function. With the increase of gas costs over time, smart contracts should be built future-proof and should take that into consideration.The functions closeMinter() and rotateMinter() make use of the function transfer() to send ether to other addresses. This is not the recommended method of sending Ether.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (25)
18 projects reviewed · 25 reports · latest Aug 2025
See Resonance Security’s auditsSomething 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