# Solidity极简入门 ERC721专题：4. BAYC合约严重漏洞

By [0xAA](https://paragraph.com/@wtfacademy) · 2022-04-26

---

我最近在重新学solidity，巩固一下细节，也写一个“Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。

欢迎关注我的推特：[@0xAA\_Science](https://twitter.com/0xAA_Science)

WTF技术社群discord，内有加微信群方法：[链接](https://discord.gg/5akcruXrsk)

所有代码和教程开源在github（1024个star发课程认证，2048个star发社群NFT）: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

不知不觉我已经完成了Solidity极简教程的前13讲（基础），内容包括：Helloworld.sol，变量类型，存储位置，函数，控制流，构造函数，修饰器，事件，继承，抽象合约，接口，库，异常。在进阶内容之前，我决定做一个`ERC721`的专题，把之前的内容综合运用，帮助大家更好的复习基础知识，并且更深刻的理解`ERC721`合约。希望在学习完这个专题之后，每个人都能发行自己的`NFT`。

* * *

TL;DR 太长不看
----------

*   在我查看BAYC合约的时候我发现了两个严重漏洞，其中一个可能会导致`BAYC`超发，超过设定的供应量上限10,000枚。
    
*   超发漏洞是由合约中用于项目方预留`BAYC NFT`的`reserveApes`函数没有检查是否超发，`owner`地址可以随时铸造，不受供应量上限的限制。
    
*   两种情况下漏洞会被触发：项目方做恶（几乎不可能）和黑客盗取`owner`私钥（有可能）。
    
*   受漏洞影响的包括`BAYC`以及复用其代码的其他`NFT`项目方。
    
*   最简单的解决办法就是`BAYC`项目方放弃`ownership`。
    
*   发现这两个漏洞并不难，我肯定不是第一个发现的（见这篇去年10月的[博客](https://medium.com/northwest-nfts/bored-ape-yacht-club-contract-review-80dce503308e)），但是这些漏洞并没有引起足够的重视。鉴于现在`BAYC`市值超过40亿美元，希望能引起项目方的重视。
    

**_声明：本文只是技术探讨，没有_**`FUD BAYC`**_。我很喜欢_**`BAYC`**_，希望漏洞永远不被触发。_**

BAYC
----

这是`WTF` `Solidity`极简入门`ERC721`专题的第4讲，我们将介绍`BAYC`合约及其漏洞。

无聊猿`BAYC`（Bored Ape Yacht Club）是最顶级的NFT项目，2021年4月底以0.08 `ETH`的价格发售，一共10000枚。目前地板价约130 ETH，涨了1000多倍，市值超过$40亿美元。

![BAYC](https://storage.googleapis.com/papyrus_images/8664cf77ff15c72b991601ac9bd4df88f589dd543b65e9723a5a51d8c551ecdf.webp)

BAYC

BAYC合约
------

`BAYC`的合约在[etherscan](https://etherscan.io/address/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d#code)上开源，所有人均可查看，今天我们就来仔细学习下它。

![BAYC的合约在etherscan上开源](https://storage.googleapis.com/papyrus_images/4685f52a1bfc319c7cbb9fb8a9bb4790e119c8494ae6bd8ba8cd78cb29a75e24.png)

BAYC的合约在etherscan上开源

`BAYC`的合约是`flat`形式的，一个文件把所有父合约都包括了，一共2021行。但其实前面1900行都是父合约的内容，只有最后的100行是主合约，也是我们将重点学习的。

### 继承

    pragma solidity ^0.7.0;
    contract BoredApeYachtClub is ERC721, Ownable {
    

`BAYC`合约的`solidity`版本是`0.7.0`，继承了两个合约，`ERC721`和`Ownable`。`ERC721`合约详见我的`ERC721`专题[1](https://mirror.xyz/ninjak.eth/PAsIFLAmEoMufZsXlX0NWsVF8DHpHz3OrYlooosy9Ho)，[2](https://mirror.xyz/ninjak.eth/4mPkMgHViRjx8OM7TAI-M-2oMfRle36ULzqlpC6S7IQ)， [3](https://mirror.xyz/ninjak.eth/-evZa3S--yw9vVcXfhn9I3UiNRaqWOTLG0eZFFgbcT0)讲，`Ownable`合约最重要的是实现了`onlyOwner`修饰器，使得特定函数只能由合约的`owner`地址调用，详见[Solidity极简教程第8讲：构造函数和修饰器](https://mirror.xyz/ninjak.eth/X8HHTaD8hqkfshhugHHp7ho3EaLjuviya_g1l3MsF_U)。

### 状态变量

    using SafeMath for uint256;
    
    string public BAYC_PROVENANCE = "";
    
    uint256 public startingIndexBlock;
    
    uint256 public startingIndex;
    
    uint256 public constant apePrice = 80000000000000000; //0.08 ETH
    
    uint public constant maxApePurchase = 20;
    
    uint256 public MAX_APES;
    
    bool public saleIsActive = false;
    
    uint256 public REVEAL_TIMESTAMP;
    

由于合约的`solidity`版本是`0.7.0`，尚未内置`SafeMath`，因此它用`using-for`声明了对`uint256`类型使用`SafeMath`库，防止溢出错误。

合约里的状态变量一共8个：

*   `BAYC_PROVENANCE`：把所有NFT图片的hash按一定顺序合并到一起。这个其实是个很巧妙的设计，可证明的把图片的内容和顺序确定下来，又不用在开图之前暴露图片信息，还可以解决NFT项目方偷把稀有度高的换给自己。**_Bug 1_**。但是BAYC项目方犯了个错误，他们加了一个`setProvenanceHash`函数，可以让owner无数次更改`BAYC_PROVENANCE`，与它的初衷相违背，并且留有做恶可能，比如偷换图片并更改`BAYC_PROVENANCE`。正确的改法是把`BAYC_PROVENANCE`设成`immutable`，在`constructor`里初始化后就不能再被修改：
    

    string public immutable BAYC_PROVENANCE = "";
    

*   `startingIndexBlock`：开始发售的区块高度。
    
*   `startingIndex`：看起来像是个多余的变量？
    
*   `apePrice：BAYC`发售价格，0.08 ETH。
    
*   `maxApePurchase`：每次`mint`的数量限制，最多一次铸造20只。一笔交易`mint`多个`NFT`，比每次只能`mint`一个要节省`gas`。
    
*   `MAX_APES`：`NFT`的最大供给，10,000个。**_Bug 2_**
    
*   `saleIsActive`：是否开始公售。
    
*   `REVEAL_TIMESTAMP`：开图的区块高度。
    

### 函数

BAYC主合约定义了10个函数：

*   `constuctor`：构造函数，初始化代币名称，代号，`MAX_APES`，`REVEAL_TIMESTAMP`。
    

        constructor(string memory name, string memory symbol, uint256 maxNftSupply, uint256 saleStart) ERC721(name, symbol) {
            MAX_APES = maxNftSupply;
            REVEAL_TIMESTAMP = saleStart + (86400 * 9);
        }
    

*   `withdraw`：取出销售`BAYC`得到的`ETH`。
    

        function withdraw() public onlyOwner {
            uint balance = address(this).balance;
            msg.sender.transfer(balance);
        }
    

*   `reserveApes`：\*\*重要！\*\*项目方给自己预留`BAYC`，每次调用给自己`mint`30个。只有合约的`owner`可以调用。**_Bug 2_**
    

        function reserveApes() public onlyOwner {        
            uint supply = totalSupply();
            uint i;
            for (i = 0; i < 30; i++) {
                _safeMint(msg.sender, supply + i);
            }
        }
    

*   `setRevealTimestamp`：更改`REVEAL_TIMESTAMP`。
    

        function setRevealTimestamp(uint256 revealTimeStamp) public onlyOwner {
            REVEAL_TIMESTAMP = revealTimeStamp;
        } 
    

*   `setProvenanceHash`：更改`BAYC_PROVENANCE`。**_Bug 1_**
    

        function setProvenanceHash(string memory provenanceHash) public onlyOwner {
            BAYC_PROVENANCE = provenanceHash;
        }
    

*   `setBaseURI`：设定`BAYC`的`BaseURI`（`ERC721`合约中的状态变量）。
    

        function setBaseURI(string memory baseURI) public onlyOwner {
            _setBaseURI(baseURI);
        }
    

*   `flipSaleState`：打开/暂停公售。
    

        function flipSaleState() public onlyOwner {
            saleIsActive = !saleIsActive;
        }
    

*   `mintApe`：\*\*重要！\*\*买家支付`ETH`并铸造`BAYC`。调用`ERC721`的`_safeMint`函数。条件：
    
    *   `saleIsActive`为`true`，即公售开始。
        
    *   `numberOfTokens` <= `maxApePurchase`，每次只能`mint`20个。
        
    *   `mint`后的流通量小于总供给（10,000枚）。
        
    *   支付的`ETH`要大于`0.08 * mint数量`。
        

        function mintApe(uint numberOfTokens) public payable {
            require(saleIsActive, "Sale must be active to mint Ape");
            require(numberOfTokens <= maxApePurchase, "Can only mint 20 tokens at a time");
            require(totalSupply().add(numberOfTokens) <= MAX_APES, "Purchase would exceed max supply of Apes");
            require(apePrice.mul(numberOfTokens) <= msg.value, "Ether value sent is not correct");
            
            for(uint i = 0; i < numberOfTokens; i++) {
                uint mintIndex = totalSupply();
                if (totalSupply() < MAX_APES) {
                    _safeMint(msg.sender, mintIndex);
                }
            }
    
            // If we haven't set the starting index and this is either 1) the last saleable token or 2) the first token to be sold after
            // the end of pre-sale, set the starting index block
            if (startingIndexBlock == 0 && (totalSupply() == MAX_APES || block.timestamp >= REVEAL_TIMESTAMP)) {
                startingIndexBlock = block.number;
            } 
        }
    

*   `setStartingIndex`：看起来像是个多余的函数？
    

        function setStartingIndex() public {
            require(startingIndex == 0, "Starting index is already set");
            require(startingIndexBlock != 0, "Starting index block must be set");
            
            startingIndex = uint(blockhash(startingIndexBlock)) % MAX_APES;
            // Just a sanity case in the worst case if this function is called late (EVM only stores last 256 block hashes)
            if (block.number.sub(startingIndexBlock) > 255) {
                startingIndex = uint(blockhash(block.number - 1)) % MAX_APES;
            }
            // Prevent default sequence
            if (startingIndex == 0) {
                startingIndex = startingIndex.add(1);
            }
        }
    

*   `emergencySetStartingIndexBlock`：在紧急情况下，开始公售。将`startingIndexBlock`设为当前区块高度。
    

        function emergencySetStartingIndexBlock() public onlyOwner {
            require(startingIndex == 0, "Starting index is already set");
            
            startingIndexBlock = block.number;
        }
    

BAYC合约的严重漏洞
-----------

`BAYC`合约一共有两个严重漏洞，一个可能导致图片被调换，另一个更严重，会使`BAYC`超发，超过设定的10,000枚。

### Bug 1：图片被换风险

用于证明图片没有被篡改、调换的`BAYC_PROVENANCE`变量可以被合约`owner`随意更改。攻击者（项目方或盗取私钥黑客）可以调换图片，利用`setBaseURI`函数设定新的`metadata`存放网址，然后算出新图片的`BAYC_PROVENANCE`并更新。这样，人们没法通过`BAYC_PROVENANCE`验证是否被篡改。

正确改写方法：将`BAYC_PROVENANCE`变量设定为`immutable`，并在构造函数中初始化，之后不能被更改。

### Bug 2：增发风险

用于给项目方预留`BAYC`的`reserveApes`函数没有像公售`mintApe`函数一样检查供应量，因此，即使最大供给量`MAX_APES`被设定为10,000，合约的`owner`（项目方或盗取私钥黑客）仍可以调用`reserveApes`铸造新的`BAYC`，使得`BAYC#10000`，`BAYC#10001`等等被`mint`出来，没有上限。

正确改写办法：在`reserveApes`函数中加入最大供应量检查：

        function reserveApes() public onlyOwner {        
            require(totalSupply().add(30) <= MAX_APES, "Mint would exceed max supply of Apes");    
        
            uint supply = totalSupply();
            uint i;
            for (i = 0; i < 30; i++) {
                _safeMint(msg.sender, supply + i);
            }
        }
    

### 在什么情况下会被攻击

前面讲的`BAYC`合约中两个严重漏洞，都需要合约`owner`去执行。一种情况就是项目方做恶，去攻击漏洞。但很显然`BAYC`非常成功，项目方不做恶会获得更大的收益，完全没有做恶的动机，因此这种情况几乎不会发生。第二种情况就是`owner`钱包私钥被黑客盗取，黑客做恶调用`reserveApes`超发`BAYC`出售。鉴于BAYC官方ins前几天刚被盗，这种情况发生不是绝无可能。目前BAYC合约的`owner`地址为：`0xaba7161a7fb69c88e16ed9f455ce62b791ee4d03`

让我们默默祈祷。

### 如何解决？

其实这两个漏洞的方法很简单，就是`BAYC`项目方调用`renounceOwnership`放弃合约`owner`。因为这两个漏洞的相关函数都需要`owner`去调用，放弃`owner`权限之后将没人可以攻击漏洞。

其他的解决办法大家也可以讨论。

彩蛋：项目方预留的BAYC给了谁？

BAYC项目方总共只调用过一次`reserveApes`函数，一共给自己预留了30个BAYC，#0到#29，并发送给了30个地址，大家可以挖掘一下他们都是谁（按tokenId排序）：

1.  [emperortomatoketchup.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xf7801b8115f3fe46ac55f8c0fdb5243726bdb66a)
    
2.  [0x46efbaedc92067e6d60e84ed6395099723252496](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x46efbaedc92067e6d60e84ed6395099723252496)
    
3.  [0xc5c7b46843014b1591e9af24de797156cde67f08](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xc5c7b46843014b1591e9af24de797156cde67f08)
    
4.  [garga.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x03546b22bc80b3e6b64c4445963d981250267a68)
    
5.  [0xwave.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x33569c101562e1faf5b24581057e5cee4c8288d7)
    
6.  [0xed7c0117d7d35850d71e2a3f390972406f8d5d46](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xed7c0117d7d35850d71e2a3f390972406f8d5d46)
    
7.  [0x898c4607809945b49d65ea51580101798931b241](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x898c4607809945b49d65ea51580101798931b241)
    
8.  [rdlwriter.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x9bd91abdf39a167712b9f6c24d1c9805d5fb9242)
    
9.  [bmouse.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x357931791284f40765b462aa7ad217ebf82920cb)
    
10.  [cryptorobo.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xf0fc2f35dabb294ce51e5be211d19c6de6cb44ae)
    
11.  [puzzle.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xb905576a1d9bff3b7f3a69764913037ea18f01da)
    
12.  [0xddb338bc464fde06b382d28f37e57cb3727c2e1b](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xddb338bc464fde06b382d28f37e57cb3727c2e1b)
    
13.  [chrishol.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x50ef16a9167661dc2500ddde8f83937c1ba4cd5f)
    
14.  [keltron.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x55907cf476998d2f58591c6d0a10ecbbe249a8eb)
    
15.  [yourstruly.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xba4c267fbccff4d8562f775f6bed556e779f28c5)
    
16.  [0x7225fd5032038bcf49c36deba23a16262521ede9](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x7225fd5032038bcf49c36deba23a16262521ede9)
    
17.  [dmnets.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x09b652cf4fc6da0095f0ed82cdc866a5f4b494c7)
    
18.  [nabito.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xba567d6ce93c021e46baa959ffc241fe35a10297)
    
19.  [tropofarmer.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xa442ddf27063320789b59a8fdca5b849cd2cdeac)
    
20.  [0xc3fbc3f485f0d9b0bd21b13a4aaa8340160156cb](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xc3fbc3f485f0d9b0bd21b13a4aaa8340160156cb)
    
21.  [nftfox.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xdff71a881a17737b6942fe1542f4b88128ea57d8)
    
22.  [d34thst4lker.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x12ccdf3513f8f09f4c0e6ad7821988a7a8ac0be1)
    
23.  [thephotographer.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x8615593f2b34626e69f476ac418596ae08178f5e)
    
24.  [0xed2d1254e79835bf5911aa8946e23bf508477da4](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xed2d1254e79835bf5911aa8946e23bf508477da4)
    
25.  [0xf9cb2a5944654b0c9b07d2311715728e30d3ee82](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xf9cb2a5944654b0c9b07d2311715728e30d3ee82)
    
26.  [billymcsmithers.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xe77aa37e6c61f2d526c0a4f3ebc0ac40e11bf490)
    
27.  [web3ireland.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x71a388b8870849d92b548d4e752e7b0e7301de3c)
    
28.  [yourstruly.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0xba4c267fbccff4d8562f775f6bed556e779f28c5)
    
29.  [0x822a16309a9ee40f15e196898f11a010ecb1c963](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x822a16309a9ee40f15e196898f11a010ecb1c963)
    
30.  [brulik.eth](https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d?a=0x6f8d29c94c7b196c5e53137ee97be32d3136b413)
    

总结
--

*   这是Solidity极简教学ERC721专题的第4讲，我们介绍`BAYC`合约及其漏洞。
    
*   在我查看BAYC合约的时候我发现了两个严重漏洞，其中一个可能会导致`BAYC`超发，超过设定的供应量上限10,000枚。
    
*   超发漏洞是由合约中用于项目方预留`BAYC NFT`的`reserveApes`函数没有检查是否超发，`owner`地址可以随时铸造，不受供应量上限的限制。
    
*   两种情况下漏洞会被触发：项目方做恶（几乎不可能）和黑客盗取`owner`私钥（有可能）。
    
*   受漏洞影响的包括`BAYC`以及复用其代码的其他`NFT`项目方。
    
*   最简单的解决办法就是`BAYC`项目方放弃`ownership`。
    
*   NFT项目方需要更严格的审计合约！
    

**_声明：本文只是技术探讨，没有_**`FUD BAYC`**_。我很喜欢_**`BAYC`**_，希望漏洞永远不被触发。_**

---

*Originally published on [0xAA](https://paragraph.com/@wtfacademy/solidity-erc721-4-bayc)*
