# 去年碰到的 NFT 漏洞

By [Transpot](https://paragraph.com/@sixer) · 2023-04-16

---

这篇文章咱水一篇去年碰到的 2 个 NFT 的漏洞，首先还是先上代码

    function mintAllow(uint256 _tokenAmount, string memory name, bytes memory signature) public payable {
      uint256 s = totalSupply();
      require(check(name, signature) == msg.sender, "Signature Invalid"); //server side signature
      require(OGStatus, "OG sale is not active");
      require(_tokenAmount > 0, "Mint more than 0");
      require(_tokenAmount <= maxMintPerTxOG, "Mint less");
      require(s + _tokenAmount <= maxSupply, "Mint less");
      require(msg.value >= priceOG * _tokenAmount, "ETH input is wrong");
      require(allowedMintCountOG(msg.sender) >= _tokenAmount, "You minted too many");
      require(tx.origin == msg.sender);
      for (uint256 i = 0; i < _tokenAmount; ++i) {
        _safeMint(msg.sender, s + i, "");
      }
      delete s;
      updateMintCountOG(msg.sender, _tokenAmount);
    }
    function check(string memory name, bytes memory signature) public view returns(address) {
      return _verify(name, signature);
    }
    function _verify(string memory name, bytes memory signature) internal view returns(address) {
      bytes32 digest = _hash(name);
      return ECDSA.recover(digest, signature);
    }
    function allowedMintCountOG(address minter) public view returns(uint256) {
      return maxMintPerWalletOG - mintCountMapOG[minter];
    }
    function updateMintCountOG(address minter, uint256 count) private {
      mintCountMapOG[minter] += count;
    }
    

项目就不说了，全部代码也不贴了，mintAllow 是 mint 入口，这是一个白名单机制的 mint，来看这个 mint 会有什么问题。

他的主要验证白名单的机制是 `require(check(name, signature) == msg.sender, "Signature Invalid");` 但是我们查看 check 函数后发现这个 signature 其实是 msg.sender 自己的签名，用户可以随便去签，完全没有限制作用。并且这里的签名还是用的 eth\_sign 这是个很危险的操作，用户不应该去签任何 eth\_sign 的签名。还有没有其它问题。其实是有的，updateMintCountOG 更新用户已经 mint 的数量放到了函数最后，这会导致可能的重入攻击，万幸是了 `require(tx.origin == msg.sender);` 禁止掉了使用合约进入 mint。

那么这么一段代码会有三个问题

1.链下不应该用 eth\_sign 签名，链下签名应该用 personal\_sign

2.ECDSA.recover 用法错误，正确用法是使用由开发者控制的私钥进行签名。

3.交换代码顺序，先更新变量，再交互 mint

这个项目最后结果是找到了项目方，关闭了 mint 通过其它方式来进行发售，结局很好。

    address private _signerAddress;
    function presaleBuy(bytes32 hash, bytes memory sig, uint256 qty, string memory nonce) external payable nonReentrant {
      require(presaleLive, "presale not live");
      require(matchAddresSigner(hash, sig), "no direct mint");
      require(qty <= 5, "no more than 5");
      require(hashTransaction(msg.sender, qty, nonce) == hash, "hash check failed");
      require(totalSupply() + qty <= maxPresale, "presale out of stock");
      require(pricePerToken * qty == msg.value, "exact amount needed");
      require(!_usedNonces[nonce], "nonce already used");
    
      _usedNonces[nonce] = true;
      for (uint256 i = 0; i < qty; i++) {
        _safeMint(msg.sender, totalSupply() + 1);
      }
    }
    function hashTransaction(address sender, uint256 qty, string memory nonce) private pure returns(bytes32) {
      bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(abi.encodePacked(sender, qty, nonce))));
      return hash;
    }
    function matchAddresSigner(bytes32 hash, bytes memory signature) private view returns(bool) {
      return _signerAddress == hash.recover(signature);
    }
    

同样具体项目也不说，入口为 presaleBuy，初年像是 ECDSA.recover 的正确使用方法，但是 ECDSA.recover 在这种场景不是单独使用的，需要配合相应的验证，不然会导致重放攻击，这个里面就没有进行相应的验证 hashTransaction 像是在进行验证，但是没有起作用，实际变成了浪费 gas 的代码。

这段代码有 2个问题

1.matchAddresSigner 没有检查 msg.sender

2.matchAddresSigner 没有相关的配合检查代码，导致了重放

这个项目的结局是 mint 的很快，有些很少数量被重放利用，损失不大。

mirror 的编辑器挺难用的，不能直接让编辑 markdown code 吗。

---

*Originally published on [Transpot](https://paragraph.com/@sixer/nft)*
