我最近在重新学solidity,巩固一下细节,也写一个“Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
欢迎关注我的推特:@0xAA_Science
WTF技术社群discord,内有加微信群方法:链接
所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): github.com/AmazingAng/WTFSolidity
不知不觉我已经完成了Solidity极简教程的前13讲(基础),内容包括:Helloworld.sol,变量类型,存储位置,函数,控制流,构造函数,修饰器,事件,继承,抽象合约,接口,库,异常。在进阶内容之前,我决定做一个ERC721
的专题,把之前的内容综合运用,帮助大家更好的复习基础知识,并且更深刻的理解ERC721
合约。希望在学习完这个专题之后,每个人都能发行自己的NFT
。
BAYC
超发,超过设定的供应量上限10,000枚。BAYC NFT
的reserveApes
函数没有检查是否超发,owner
地址可以随时铸造,不受供应量上限的限制。owner
私钥(有可能)。BAYC
以及复用其代码的其他NFT
项目方。BAYC
项目方放弃ownership
。BAYC
市值超过40亿美元,希望能引起项目方的重视。声明:本文只是技术探讨,没有FUD BAYC
。我很喜欢BAYC
,希望漏洞永远不被触发。
这是WTF
Solidity
极简入门ERC721
专题的第4讲,我们将介绍BAYC
合约及其漏洞。
无聊猿BAYC
(Bored Ape Yacht Club)是最顶级的NFT项目,2021年4月底以0.08 ETH
的价格发售,一共10000枚。目前地板价约130 ETH,涨了1000多倍,市值超过$40亿美元。
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,2, 3讲,Ownable
合约最重要的是实现了onlyOwner
修饰器,使得特定函数只能由合约的owner
地址调用,详见Solidity极简教程第8讲:构造函数和修饰器。
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 2saleIsActive
:是否开始公售。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
超发,超过设定的10,000枚。
用于证明图片没有被篡改、调换的BAYC_PROVENANCE
变量可以被合约owner
随意更改。攻击者(项目方或盗取私钥黑客)可以调换图片,利用setBaseURI
函数设定新的metadata
存放网址,然后算出新图片的BAYC_PROVENANCE
并更新。这样,人们没法通过BAYC_PROVENANCE
验证是否被篡改。
正确改写方法:将BAYC_PROVENANCE
变量设定为immutable
,并在构造函数中初始化,之后不能被更改。
用于给项目方预留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排序):
BAYC
合约及其漏洞。BAYC
超发,超过设定的供应量上限10,000枚。BAYC NFT
的reserveApes
函数没有检查是否超发,owner
地址可以随时铸造,不受供应量上限的限制。owner
私钥(有可能)。BAYC
以及复用其代码的其他NFT
项目方。BAYC
项目方放弃ownership
。声明:本文只是技术探讨,没有FUD BAYC
。我很喜欢BAYC
,希望漏洞永远不被触发。