Developers need to consider the risks of using safe functions and determine how external calls interact with the code they write.
Original Title: ” White Hat Hacker Samczsun: Attacks on NFT Assets Will Be More Frequent “
Written by: Samczsun, research partner of Paradigm, a blockchain investment institution, and a well-known encryption white hat hacker Compilation: Overnight Congee
The author of the original text is Samczsun, a white hat hacker known as the “audit god”, and he is also a research partner of Paradigm. He recently rescued USD 350 million in assets from the BitDAO MISO Dutch auction fund pool. In this article, He reminded about the potential security risks of the NFT token standard. He also predicted that as the ERC-721 and ERC-1155 token standards become more popular, attacks against NFTs are likely to become more frequent.
If you work in software engineering, chances are you have heard of at least one software engineering principle. Although I do not advocate strict observance of every principle, some are indeed worthy of attention.
What I want to talk about today is the principle of least astonishment. It has a peculiar name, but it is a very simple idea. What it says is that when presenting code that claims to do something, most users will assume how it did it. Therefore, as a developer, your job is to write code that meets these assumptions so that your users will not be surprised.
This is a good principle, because developers like to make assumptions about things. If you export a function called calculateScore(GameState), many people will assume that the function will only read from the game state. If you also change the game state, you will cause a lot of people to face a confused state, and they try to figure out why their game state is randomly destroyed. Even if you put it in the documentation, there is still no guarantee that people will see it, so it’s best to first make sure that your code won’t be surprising.
“Six hours of debugging work can save you 5 minutes of document reading time.”
The safer the better, right?
Back in early 2018, when the ERC-721 standard was drafted out, it was proposed the implementation of transfer security recommendations to ensure that tokens will not be stuck in the recipient does not contract for processing of tokens. To this end, the author of the proposal modified the behavior of the transfer function to check whether the recipient can support token transfer. They also introduced the unsafeTransfer function, which will bypass this check if the sender wishes.
However, due to concerns about backward compatibility, this function was renamed in subsequent commits. This makes the transfer function of ERC-20 and ERC-721 tokens behave the same. However, now the receiver check needs to be transferred elsewhere. Therefore, the standard author introduced safe functions: safeTransfer and safeTransferFrom.
This is a solution to the issue of legitimacy, because there are many examples of ERC-20 tokens being accidentally transferred to contracts that never expect to receive tokens (a particularly common mistake is to transfer tokens to a token contract To lock it permanently). When drafting the ERC-1155 standard, the author of the proposal drew inspiration from the ERC-721 standard, not only in the transfer, but also in the mint (mint) also included receiver inspection, this is not surprising.
In the next few years, most of these standards are dormant, and the ERC-20 token standard has maintained its popularity. The recent surge in gas costs and the increase in community interest in NFTs have naturally led to more and more developers. The more ERC-721 and ERC-1155 token standards are used. With these new interests, we should be thankful that these standards are designed with safety in mind, right?
The safer the better, really?
Ok, but what does security mean for money transfer and casting? Different parties have different interpretations of security. For developers, a security function may mean that it does not contain any bugs or introduce additional security issues. For users, this may mean that it contains additional guardrails to protect them from accidental shots in their feet.
It turns out that in this case, these functions are more of the latter and less of the former. This is particularly regrettable, because when choosing between the transfer and safeTransfer functions, why don’t you choose the safe function? The name is reflected!
Well, one of the reasons may be our old friend reentrancy (reentrancy), or I have been trying to rename it: unsafe external calls. Recall that if the receiver is controlled by an attacker, any external calls may be insecure, because the attacker may cause your contract to transition to an undefined state. By design, these “safe” functions perform external calls to the token receiver, usually controlled by the sender during minting or transfer. In other words, this is actually a textbook example of unsafe external calls.
However, you may ask yourself, if the recipient contract is allowed to reject transfers that they cannot handle, what is the worst consequence? Well, let me answer this question through two case studies.
Example 1: Hashmasks
Hashmasks is a NFT avatar project with limited supply. Users can purchase up to 20 mask NFTs per transaction (although they have been sold out for several months). The following is the function to purchase mask:
You might think this function looks very reasonable. However, as you might have expected, there is something sinister hidden in the _safeMint call. let’s see.
For security, this function performs a callback to the recipient of the token to check whether they are willing to accept the transfer. However, we are the recipient of the token, which means that we have just received a callback, at this point we can do whatever we want, including calling the mintNFT function again. If we do this, we will call the function again after only one mask has been cast, which means we can request another 19 masks to be cast. This resulted in the final casting of 39 mask NFTs, although the maximum number allowed by the rules was only 20.
Example 2: ENS domain name wrapper
Recently, Nick Johnson from ENS contacted me and he wanted to show me their work on the ENS domain name wrapper. This domain name wrapper allows users to tokenize their ENS domain names with the new ERC-1155 token, which provides support for fine-grained permissions and more consistent APIs.
In a nutshell, in order to encapsulate any ENS domain name (more specifically, all ENS domain names except 2LD.eth), you must first approve the domain name wrapper to access your ENS domain name. Then, you call wrap(bytes,address,uint96,address), which not only mints an ERC-1155 token for you, but also manages the underlying ENS domain name.
Here is the wrap function, which is quite simple. First, we call _wrap, which performs some logic and returns the hashed domain name. Then, we make sure that the sender of the transaction is indeed the owner of the ENS domain name, and then take over the domain name. Please note that if the sender does not own the underlying ENS domain name, the entire transaction should be reverted and any changes made in _wrap should be undone.
Below is the _wrap function itself, there is nothing special here.
Unfortunately, it is this _mint function that can bring terrible surprises to unsuspecting developers. The ERC-1155 specification stipulates that when casting a token, the recipient should be consulted whether it is willing to accept the token. After in-depth study of the library code (the code library was slightly modified based on OpenZeppelin’s foundation), we found that this is indeed the case.
But what is it good for us? Okay, once again we saw an insecure external call, which we can use to perform a reentrancy attack. Specifically, please note that during the callback period, we have the ERC-1155 token of the token ENS domain name, but the domain name wrapper has not verified that we own the basic ENS domain name itself. This allows us to operate on the ENS domain name without actually owning it. For example, we can ask the domain name wrapper to unlock our domain name, burn the token we just minted, and get the underlying ENS domain name.
Now that we have the underlying ENS domain name, we can use it to do whatever we want, such as registering a new subdomain name or setting up a resolver. When finished, we just need to exit the callback callback. The domain name wrapper will interact with the current owner of the underlying ENS domain name (ie us) and complete the transaction. Just like that, we have taken the temporary ownership of any ENS domain name for which the domain name wrapper is approved for use and made any changes to it.
in conclusion
Surprising code can destroy things in catastrophic ways. In the two cases in this article, the developers reasonably assumed that the safe function class can be used safely, but inadvertently increased their attack surface. As the ERC-721 and ERC-1155 token standards become more popular and widespread, such attacks are likely to become more frequent. Developers need to consider the risks of using safe functions and determine how external calls interact with the code they write.
Source link: www.8btc.com
Disclaimer: As a blockchain information platform, the articles published on this site only represent the author’s personal views, and have nothing to do with the position of ChainNews. The information, opinions, etc. in the article are for reference only, and are not intended as or regarded as actual investment advice.