Executive Summary
During our recent security review of the DN404 🥜 codebase, we discovered two high-severity issues that could lead to NFT theft. Additionally, we identified several intriguing protocol value extraction and integration issues unique to the dual nature of the tokens.
The high severity issues that we identified were present in the codebase from version 0.0.8 up to and including version 0.0.16, from Feb 19, 2024 to May 11, 2024.
Since the codebase of DN404 was public at the time of the engagement, the high severity findings prompted an on-chain search to identify if any integrating project was vulnerable.
Luckily, the two largest projects implementing DN404 at the time, Asterixlabs and Oracle Red Bull Racing ’s Velocity Pass were safe. Asterix’s Anomalies collection uses a version without the high-severity issues, and Red Bull’s Velocity Pass did not use the vulnerable functions. Furthermore, no other deployed contracts were found to be vulnerable.
The Guardian security engagement ran from the 23rd of April to the 3rd of May. A team of five auditors reviewed the source code of the DN404 and DN404Mirror contracts. After addressing the issues, Vectorized, the main developer of the project, committed the fixes in the main public repository as version 0.0.17 . The full Guardian report for the engagement and disclosure is here.
About Dual Nature Token Pairs ― ERC-7631
In the early part of this year, a new way of combining ERC20 and ERC721 tokens appeared in the wild. This method involved merging features of fungible (ERC20) and non-fungible (ERC721) tokens to create what was later coined as semi-fungible tokens. With this innovation, users could own fractions of an NFT by holding the underlying ERC20 base tokens.
The first semi-fungible variation to gain significant popularity was named ERC404, created by Acme and Calder. The ERC abbreviation (Ethereum Request for Comment) is usually used to denote a standard approved by the Ethereum community to be used and published on the Ethereum Improvement Proposals platform. However, in the case of the ERC404 implementation, it was just part of the project name.
A key difference between the popular ERC404 and the DN404 implementation is that ERC404 combines both ERC20 and ERC721 standards into one contract but is not fully compatible with either standard. In contrast, DN404 uses two separate contracts, one for each token type, interlinked and each fully compatible with their respective standard.
The official ERC draft of the distinct dual pairing was published on the 21st of February, 2024, as ERC-7631. At the time of writing this article, ERC-7631 is still in a draft state and may undergo breaking changes. ERC-7631, in short, describes a way in which a fungible ERC20 token contract and a non-fungible ERC721 token contract can be interlinked, allowing actions performed on one contract to be reflected on the other. To put it simply, moving enough ERC20 base tokens will also move interlinked ERC721 mirror tokens.
Standardized or not, ERC404 tokens, as they are popularly referred to, have already reached a market cap of almost $60 million and appear to have solidified their place in the Real World Assets (RWA) narrative.
The DN404 implementation, audited by Guardian, was developed by the authors of the ERC-7631 themselves, namely Vectorized, Thomas, Quit, Michael Amadi, Cygaar, and Harrison, and then further optimized (excruciatingly) by Vectorized.
DN404 Architecture
The DN404 project architecture is intentionally kept as simple as possible, consisting of only two contracts that rely on each other.
The DN404 contract represents the base ERC20 token, while the DN404Mirror contract provides an interface for the mirrored ERC721 tokens.
src
├─ DN404 — "ERC20 contract for DN404"
└─ DN404Mirror — "ERC721 contract for DN404"
ERC-7631 does not include implementation details such as where to implement the synchronization logic when transferring either ERC721 or ERC20 tokens. The DN404 implementation places this logic in the base ERC20 contract, the DN404 contract itself.
In the DN404 contract, ERC20 transfers are directly noted internally. Conversely, when ERC721 transfers occur on the DN404Mirror contract, the calldata is passed to the DN404 contract for internal recording. In essence, all relevant ERC721 and ERC20 state storage resides in the main DN404 contract, which the DN404Mirror contract queries as needed.
All ERC721 operations are available on the DN404Mirror contract and forwarded to the ERC20 base contract for execution. In contrast, the base ERC20 contract, (DN404) directly calls the mirror contract (DN404Mirror) only at initialization to interlink the contracts and when passing ERC721 Transfer event data. Event data is passed to and emitted from the mirror contract whenever sufficient base ERC20 tokens are transferred, burned, or minted to result in ERC721 token transfers.
Implementing the passing of event data and emitting them on the ERC721 side ensures compliance with ERC721 standards.
In terms of naming convention, the amount of base ERC20 tokens required to gain one mirrored ERC721 token is referred to as a “ unit” .
Communication from the DN404Mirror contract with the DN404 base contract is facilitated via the dn404Fallback modifier, which efficiently handles the most used function selectors at a higher priority for greater efficiency. In reverse, when the DN404 contract directly calls the DN404Mirror contract, it is done so through the dn404NFTFallback modifier.
[Resolved] Vulnerability #1: "_mintNext Allows NFTs In The Burn Pool To Be Stolen"
As mentioned in the system architecture overview, minting base ERC20 tokens can trigger the minting of mirrored ERC721 token if the receiver reaches the minimum unit amount of tokens required.
Within the DN404 contract, there are two methods for minting base ERC20 tokens. Of particular interest is the _mintNext function, which mints base ERC20 tokens under the assumption that if ERC721 minting also occurs, the token ID for the newly minted NFT will always be outside of the existing ID pool. Specifically the function was added with presale scenarios in mind.
However, an underlying issue arises with the way the NFTs are minted using the _mintNext function in conjunction with the utilization of the burnedPool feature of the contract. The burnedPool was introduced to avoid expensive lookups under high load factors, since for each newly minted NFT, an ID must be selected and checked to ensure it is not already owned.
Refer to the following image and subsequent explanations for further clarification.
When minting an ERC721 due to transferring base ERC20 tokens from one user to another, we can see in the blue square that if the burned pool is used and there are burned IDs, then the soon-to-be minted NFT will use one of these IDs. If the burned pool is not used, an expensive lookup for an available ID will be done, as depicted in the red square.
Another crucial observation is that, in the blue square, the burned pool head value is incremented. This incrementation marks the ID as used and removes it from the burn pool. Also to note is that, the option of using the burnedPool is disabled by default.
For the issue to be triggered, it requires both the burnedPool feature to be active in conjunction with the use of the _mintNext function. In this scenario, after burning enough base tokens to also burn a mirror NFT, then re-minting base tokens using the _mintNext function, the token IDs in the burnedPool may be used for the mint, but they are not marked as removed from the burn pool. Therefore these NFTs can be re-minted by the mint function!
In the image below we see the implementation of the _mintNext function with the red square highlighting the NFT ID selection logic and the pink square the checks being preformed, or lack thereof.
The ID of the new NFT to be minted is directly calculated and used as totalSupply / unit() + 1. Unlike the logic in the _transfer function shown previously, there are no checks to verify if the ID is from the burn pool.
Minting succeeds, and the new owner gets an NFT with the next available ID, which errantly is from the burn pool.
In this Proof of Concept (PoC), an ID appears in the burn pool when we burn ERC20s and reduce the amount of circulating NFTs that can exist. In that case, the burned IDs can be those outside of the totalSupply / _unit() + 1 value.
Now, on the first subsequent ERC721 mint, resulting from base token operations, an ID from the burn pool is minted directly without an ownership check. This ID is already owned, and by "re-minting" it, the NFT is unintentionally transferred from the original owner to the receiver of the mint.
The second high-severity issue highlights a different method to achieve the same malicious effect by causing the _mintNext function to wrap into the lower IDs, which may also exist in the burn pool. This issue is documented as H-02 in the Guardian report.
[Documented] Vulnerability #2: "NFT Marketplace Bidding Bait-And-Switch"
Between the base ERC20 token and ERC721 token, a synchronization logic exists that determines which NFTs are transferred or burned first. The ERC7631 standard does not specify any particular implementation logic. It is left up to the implementer to define.
In the case of DN404, the last NFTs to enter a user’s wallet, whether through direct NFT transfer, base ERC20 transfer, or base ERC20 minting, are the first to be sent out. In other words, the implementation is designed so that the most recently acquired NFTs are the first to be transferred out. In Computer Science this behavior is referred to as the LIFO (Last in, first out) method.
For example, suppose a user receives a unit of base ERC20 tokens and mints ID 10. Then, they purchase ID 12 from an NFT marketplace. Afterwards the user mints additional base tokens, resulting in the user receiving an NFT with the ID 13. If the user transfers enough base tokens to equate to two NFTs, then the last two NFTs he acquired — ID 13 and ID 12, in that order — are the ones transferred out.
This design, as simple as it is, leaves enough room that in the right conditions, a user may end up losing funds. This scenario affects NFT traders particularly and is as such:
From an economical view, the victim unwillingly sells a rare NFT, which he bid and paid for above the floor price, for the price of the base ERC20 equivalent value, which is even less than the NFT floor price. Meanwhile, the attacker profits from this difference while keeping his rare NFT.
[Resolved] Vulnerability #3: "Pools Created After First Interaction Not Excluded From ERC721 Minting"
The ERC7631 standard also defines a feature for skipping NFT minting. This option allows accounts to avoid automatic minting of ERC-721 tokens whenever there is an ERC-20 transfer to them.
This is useful for instance, when transferring large amounts of ERC-20 tokens in or out of a liquidity pool, or when loading vesting contracts with substantial amounts ERC-20 tokens to be vested.
DN404 implemented this mechanism by automatically marking smart contracts as skipped. It also implemented initialization logic such that once an account had been interacted with, a skip status is set, and from now on only the account itself may change it.
A subtle issue with this mechanism is that, it presumes the address to which tokens are sent cannot change from an Externally Owned Account (EOA) to a smart contract. In other words, it presumes that if an address does not currently have bytecode at the time of initialization, it never will. However this presumption is incorrect.
A malicious actor can transfer a small amount of base tokens to an address that will hold a smart contract in the future. By doing so, the address will be considered initialized and will not be marked with the skip NFT minting flag since it does not hold any bytecode at this point. We will refer to this process as staining.
For example, staining the address of a Uniswap liquidity pool before its deployment results in NFTs being errantly minted to and burned from the pool address upon liquidity modification and swaps. The address of a Uniswap liquidity pool can also be easily predicted, making this attack especially relevant for ERC20 trading.
This creates a pool of NFTs that are not owned by end users but are instead locked up in a swap pool where a significant amount of them may not be able to move due to locked liquidity.
Depending on the stained smart contract or arbitrary integration, these ERC721 tokens could be unintentionally locked for a significant amount of time.
Additionally, this significantly increases the gas usage necessary for larger swaps, which will become costly as more and more NFTs are minted to the Uniswap pool in the swap transaction.
Finding Remediations
H-01
The issue with the _mintNext function inadvertently using an NFT ID from the burn pool was resolved elegantly and in a simple manner by invalidating the burned pool whenever an NFT is minted with the _mintNext function.
This solution is elegant and simple because it adds just two lines of extra code to Empty the burn pool. Emptying the pool ensures that new IDs used for minting NFTs are recycled in a loop rather than sourced from the burn pool. Thus, the only drawback is a slight increase in gas cost for the next regular mint.
M-01
The NFT marketplace bidding bait-and-switch issue, where a user with a bid on a rare NFT initiated a sell swap of base tokens, was mitigated by explicitly advising users to refrain from doing simultaneous ERC20 and ERC721 operations within the same wallet. Similar issues may arise due to the nature of co-paired tokens, regardless of the chosen synchronization logic. Therefore, the simplest approach is to avoid such situations.
M-03
Staining the address of a smart contract with dust amounts of base tokens before deployment, to circumvent checks and prevent the address from skipping NFT minting, was resolved by always checking if the intended target is a smart contract to skip, unless the address had been explicitly initialized with a skipNft value.
All other issues uncovered were resolved to the extent that was possible.
Conclusion
The DN404 codebase is the first ERC7631 implementation that boasts full ERC20 and ERC721 compatibility for interlinked tokens — and doing so with extreme gas efficiency.
Our audit of the codebase uncovered both severe issues and intriguing value extraction scenarios, all of which were addressed by the DN404 team.
As the technology continues to mature and adoption increases, we are eagerly anticipating the evolution of Dual Natured tokens within the ever-changing Web3 landscape.
We are proud to have audited such a groundbreaking and thoughtful codebase and, in the words of the original authors:
Authored By: abarbatei