{"id":762,"date":"2025-08-04T16:13:25","date_gmt":"2025-08-04T14:13:25","guid":{"rendered":"https:\/\/ackee.xyz\/blog\/?p=762"},"modified":"2025-08-04T16:23:54","modified_gmt":"2025-08-04T14:23:54","slug":"reentrancy-attack-in-erc-721","status":"publish","type":"post","link":"https:\/\/ackee.xyz\/blog\/reentrancy-attack-in-erc-721\/","title":{"rendered":"Reentrancy Attack in ERC-721"},"content":{"rendered":"<p>ERC721 tokens have become the backbone of the NFT ecosystem, however their implementation contains subtle security risks that developers often overlook. The ERC721 standard includes a safety mechanism called the <code class=\"codehl\">ERC721Receiver<\/code> hook, designed to prevent tokens from being lost when sent to contracts. However, this same mechanism introduces an external call that can be exploited through reentrancy attacks.<\/p>\n<p>In this article, we&#8217;ll examine how attackers can manipulate the <code class=\"codehl\">_safeMint<\/code> function&#8217;s external call to bypass minting limits and drain NFT collections, even when developers believe they&#8217;ve followed secure coding practices.<\/p>\n<h2>Example Contract: Expected Usage<\/h2>\n<p>The <code class=\"codehl\">Masks<\/code> contract extends ERC721 and manages NFT minting with the following constraints:<\/p>\n<ul>\n<li>Users can call the <code class=\"codehl\">mintNFT<\/code> function to mint NFTs<\/li>\n<li>Maximum of 20 NFTs per transaction<\/li>\n<li>Total supply limited by <code class=\"codehl\">MAX_NFT_SUPPLY<\/code><\/li>\n<\/ul>\n<h2>Vulnerable Contract<\/h2>\n<p><script src=\"https:\/\/gist.github.com\/MeditationDuck\/ddf3a84a214b44352d6b1da59d79d103.js\"><\/script><\/p>\n<p>The vulnerability arises from the external call <code class=\"codehl\">IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data)<\/code> within the <code class=\"codehl\">_safeMint<\/code> function.<\/p>\n<p>The call flow follows this pattern:<\/p>\n<ul>\n<li><code class=\"codehl\">mintNFT<\/code> calls <code class=\"codehl\">_safeMint<\/code><\/li>\n<li><code class=\"codehl\">_safeMint<\/code> calls <code class=\"codehl\">_safeMint<\/code> to set arguments<\/li>\n<li><code class=\"codehl\">_safeMint<\/code> calls <code class=\"codehl\">_checkOnERC721Received<\/code><\/li>\n<li><code class=\"codehl\">_checkOnERC721Received<\/code> calls <code class=\"codehl\">IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data)<\/code><\/li>\n<\/ul>\n<p>The Masks contract checks the number of NFTs being minted at the function&#8217;s beginning. <code class=\"codehl\">totalSupply()<\/code> uses <code class=\"codehl\">_tokenOwners.length()<\/code>.<\/p>\n<p>This value is updated by <code class=\"codehl\">_tokenOwners.set(tokenId, to);<\/code> in the <code class=\"codehl\">_mint<\/code> function, and after that <code class=\"codehl\">_checkOnERC721Received<\/code> is called. So it initially seems that reentrancy does not occur.<\/p>\n<p>However, the condition in <code class=\"codehl\">mintNFT<\/code> compares the current <code class=\"codehl\">totalSupply<\/code> and <code class=\"codehl\">numberOfNft<\/code> with <code class=\"codehl\">MAX_NFT_SUPPLY<\/code>.<\/p>\n<p>If an attacker re-enters the for loop, the comparison uses an outdated <code class=\"codehl\">totalSupply<\/code> value, allowing excess NFT minting.<\/p>\n<h2>Attack Example<\/h2>\n<p>The attack proceeds through these steps:<\/p>\n<ul>\n<li>Attacker calls <code class=\"codehl\">mintNFT(20)<\/code><\/li>\n<li>Let&#8217;s say the value of <code class=\"codehl\">totalSupply()<\/code> is <code class=\"codehl\">N<\/code><\/li>\n<li>The <code class=\"codehl\">_mint()<\/code> function updates <code class=\"codehl\">_tokenOwners<\/code>, so now <code class=\"codehl\">totalSupply()<\/code> is <code class=\"codehl\">N+1<\/code><\/li>\n<li>The function <code class=\"codehl\">_checkOnERC721Received<\/code> calls <code class=\"codehl\">onERC721Received()<\/code> in the attacker contract:\n<ul>\n<li>Attacker calls <code class=\"codehl\">mintNFT(20)<\/code> again via reentrancy<\/li>\n<li>At this moment <code class=\"codehl\">totalSupply()<\/code> is <code class=\"codehl\">N+1<\/code> <strong>not <code class=\"codehl\">N+20<\/code> &#8211; this is the main point<\/strong><\/li>\n<li>So we can generate 18 additional NFTs from <code class=\"codehl\">totalSupply()<\/code> of <code class=\"codehl\">N+1<\/code><\/li>\n<li>It will check whether <code class=\"codehl\">N+1+18<\/code> is less than <code class=\"codehl\">MAX_NFT_SUPPLY<\/code>, but it should be checked with <code class=\"codehl\">N+20+18<\/code>, which should revert<\/li>\n<li>Repeat the process similarly<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>This is an attacker contract:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/MeditationDuck\/dce40a4df401240a33cdba5b45dc0645.js\"><\/script><\/p>\n<p>And this is the exploit:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/MeditationDuck\/9c5d6d9d2a04d84db5081f2b0103b170.js\"><\/script><\/p>\n<p>The attacker successfully mints 110 NFTs, which exceeds both the 20 NFT limit and the maximum mintable number in one transaction.<\/p>\n<h2>Prevention<\/h2>\n<p>Implement a reentrancy guard to prevent this vulnerability.<\/p>\n<h2>Conclusion<\/h2>\n<p>The vulnerability demonstrated here highlights a critical lesson for smart contract developers: even when attempting to follow established security patterns like checks-effects-interactions, the introduction of loops with external calls can create unexpected attack vectors. The ERC721 standard&#8217;s safety features, while well-intentioned, can become security liabilities without proper safeguards.<\/p>\n<p>This case underscores why comprehensive security audits and reentrancy guards are essential for any contract handling valuable assets. As the DeFi and NFT ecosystems continue to evolve, developers must remain vigilant about these subtle but devastating vulnerabilities that can bypass seemingly robust validation logic.<\/p>\n<p>We maintain a <a href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\">Reentrancy Examples Github Repository<\/a> that covers other types of reentrancy attacks and protocol-specific reentrancies.<\/p>\n<p>We have also written about type-specific reentrancy attacks:<\/p>\n<ul>\n<li><a href=\"https:\/\/ackee.xyz\/blog\/single-function-reentrancy-attack\/\">Single Function Reentrancy Attack<\/a><\/li>\n<li><a href=\"https:\/\/ackee.xyz\/blog\/cross-function-reentrancy-attack\/\">Cross Function Reentrancy Attack<\/a><\/li>\n<li><a href=\"https:\/\/ackee.xyz\/blog\/cross-contract-reentrancy-attack\/\">Cross Contract Reentrancy Attack<\/a><\/li>\n<li><a href=\"https:\/\/ackee.xyz\/blog\/read-only-reentrancy-attack\/\">Read Only Reentrancy Attack<\/a><\/li>\n<\/ul>\n<h3>Resource<\/h3>\n<p><a href=\"https:\/\/samczsun.com\/the-dangers-of-surprising-code\/\">https:\/\/samczsun.com\/the-dangers-of-surprising-code\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>ERC721 tokens have become the backbone of the NFT ecosystem, however their implementation contains subtle security risks that developers often overlook. The ERC721 standard includes a safety mechanism called the ERC721Receiver hook, designed to prevent tokens from being lost when sent to contracts. However, this same mechanism introduces an external call that can be exploited through reentrancy attacks. In this article, we&#8217;ll&hellip;<\/p>\n","protected":false},"author":24,"featured_media":1102,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[61,10,85,84,80,63,103],"tags":[96,14,86,138],"class_list":["post-762","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-education","category-ethereum","category-exploits","category-hacks","category-solidity","category-tutorial","category-wake","tag-educational","tag-exploit","tag-hack","tag-reentrancy-attack"],"aioseo_notices":[],"featured_image_src":"https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2025\/07\/Read-Only-Reentrancy-Attack-600x400.png","featured_image_src_square":"https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2025\/07\/Read-Only-Reentrancy-Attack-600x600.png","author_info":{"display_name":"Naoki Yoshida","author_link":"https:\/\/ackee.xyz\/blog\/author\/naoki-yoshida\/"},"_links":{"self":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts\/762","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/users\/24"}],"replies":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/comments?post=762"}],"version-history":[{"count":0,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts\/762\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/media\/1102"}],"wp:attachment":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/media?parent=762"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/categories?post=762"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/tags?post=762"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}