Prepared by:
HALBORN
Last Updated 09/23/2025
Date of Engagement: April 2nd, 2025 - May 13th, 2025
100% of all REPORTED Findings have been addressed
All findings
35
Critical
1
High
5
Medium
11
Low
6
Informational
12
Nomyx engaged Halborn to conduct a security assessment on their smart contracts beginning on April 2nd, 2025 and ending on May 13th, 2025. The security assessment was scoped to the smart contracts provided to Halborn. Commit hashes and further details can be found in the Scope section of this report.
The Nomyx codebase in scope mainly utilizes the Diamond proxy pattern, the main purpose of the project is to allow users to create and participate in trade deals, mint ERC721A tokens, and allow those NFTs to be sold in different ways.
Halborn was provided 30 days for the engagement and assigned 1 full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security expert with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols. The assessment also passed through Halborn's internal QA process, a multi-step review involving technical and leadership oversight, to help ensure clarity and consistency across findings. This process is designed to reinforce the quality of the assessment deliverable.
The objectives of this assessment were to:
Identify potential security issues within the smart contracts.
Ensure that smart contract functionality operates as intended.
In summary, Halborn identified several areas for improvement to reduce the likelihood and impact of risks, which were mostly addressed by the Nomyx team. The primary recommendations were:
Consider not allowing users to create a listing without depositing the NFT, or check whether the msg.sender owns the NFT for which he wants to create a listing.
Consider removing the requirement from the MultiSaleFacet.purchase() function to withdraw ERC20 tokens from the user. The MultiSaleLib.__purchase() function will dedcut them from the user.
Consider allowing the buyer to provide the maximum amount of tokens he is willing to pay as a parameter in the MultiSaleFacet.purchase() function.
Consider checking the length of the provided of the provided proof array in the MerkleProver.verify() function and revert the function if the length is 0.
In the TradeDealLib._depositUSDCToTradeDeal() function consider checking whether the funding for the trade deal has already been withdrawn, via the tradeDealFundingWithdrawn[tradeDealId] param, if it has revert the transaction and don't allow additional deposits.
Consider utilizing the address of the receiver or msg.sender in the merkle proof.
| Security analysis | Risk level | Remediation |
|---|---|---|
| Malicious user can steal NFTs from anybody | Critical | Solved - 05/21/2025 |
| MultiSaleFacet.purchase() function will take more tokens from the user, if ERC20 tokens are used | High | Solved - 05/21/2025 |
| Malicious users can frontrun non malicious buyers, and make them overpay | High | Solved - 06/25/2025 |
| Merkle proof verification can be bypassed | High | Solved - 05/21/2025 |
| Merkle proof can be used numerous times | High | Solved - 05/21/2025 |
| Trade deal participants can steal the APR from other participants | High | Solved - 05/21/2025 |
| Funders can't redeem their collateral | Medium | Solved - 05/21/2025 |
| Funders can't redeem their collateral for the first trade deal | Medium | Solved - 05/21/2025 |
| Incorrect tokenType checks | Medium | Solved - 05/21/2025 |
| MultiSaleFacet.purchase() doesn't work with ERC20 tokens | Medium | Solved - 06/25/2025 |
| MultiSaleFacet.purchase() function won't send ERC20 tokens to payee | Medium | Solved - 06/25/2025 |
| Purchase restrictions per account are not sufficiently enforced | Medium | Solved - 05/21/2025 |
| No restrictions on the amount of tokens that can be bought via MultiSaleFacet.purchaseProof() | Medium | Solved - 05/21/2025 |
| Lib functions are not implemented in the corresponding facet | Medium | Solved - 05/21/2025 |
| Incorrect function call order in _burn() | Medium | Solved - 05/21/2025 |
| Incorrect argument used in internal function call | Medium | Solved - 05/21/2025 |
| _burn() will panic due to underflow | Medium | Solved - 05/21/2025 |
| ETH may get stuck in the contract | Low | Solved - 06/11/2025 |
| Centralization risk | Low | Risk Accepted - 05/21/2025 |
| Use of transfer() | Low | Solved - 05/21/2025 |
| totalSupply() returns incorrect amount | Low | Solved - 05/21/2025 |
| removeIdentity() doesn't work as expected | Low | Solved - 06/11/2025 |
| Safe function for ERC20 interactions are not used | Low | Solved - 05/21/2025 |
| Function doesn't revert if the call to send ETH fails | Informational | Solved - 05/21/2025 |
| Custom errors should be used | Informational | Acknowledged - 05/06/2025 |
| Floating pragma | Informational | Acknowledged - 05/06/2025 |
| Consider Using Named Mappings | Informational | Acknowledged - 05/06/2025 |
| Insufficient test coverage | Informational | Future Release - 05/06/2025 |
| The length of arrays is not cached before loops | Informational | Solved - 05/21/2025 |
| Unresolved TO-DOs | Informational | Solved - 05/21/2025 |
| The project contains console.log | Informational | Solved - 05/21/2025 |
| Missing events | Informational | Acknowledged - 05/21/2025 |
| Unused modifer and repeated functions | Informational | Acknowledged - 05/21/2025 |
| Only one key can be added | Informational | Solved - 06/11/2025 |
| removeKey() doesn't clear all storage | Informational | Solved - 06/11/2025 |
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
Gemforce Contracts
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed