背景知识\定义
NFT
- 是数字或物理资产所有权的区块链表示。
- 不仅限于数字图片,视频和画作等艺术品也可以转化为 NFT 进行交易。
- 近年来受到广泛关注,2021 年 NFT 交易额达到约 410 亿美元。
智能合约
- 是在区块链上运行的图灵完备程序。
- 支持各种去中心化应用 (DApp) 的部署。
- 是 NFT 项目的底层技术。
- 开发人员可以使用智能合约编码 NFT 交易规则。
- 允许用户在交易市场上铸造和转移 NFT。
场景:
假设 Alice 想要购买一个 NFT,而 Bob 想要出售自己的 NFT。
-
传统方式:
Alice 和 Bob 需要找到一个中介机构,例如拍卖行或交易平台。
他们需要签订一份合同,并支付手续费。
中介机构负责验证交易并确保双方履行合同。
交易完成后,中介机构将 NFT 转移给 Alice,并将资金转移给 Bob。 -
智能合约方式:
Alice 和 Bob 可以使用智能合约来执行交易。
他们将 NFT 和资金存入智能合约。
智能合约会自动验证交易条件,例如价格和数量。
如果条件满足,智能合约会将 NFT 转移给 Alice,并将资金转移给 Bob。
如果条件不满足,智能合约会自动退还 NFT 和资金。
EVM
EVM(以太坊虚拟机)是运行在以太坊区块链上的虚拟机,它负责执行智能合约代码。
-
EVM 的执行机制:
EVM 将智能合约的字节码拆分成操作码 (opcode)。
EVM 遵循操作码的指令执行相应的操作,例如读取和写入状态变量、调用其他合约等。
EVM 的执行过程是顺序执行的,但也可以通过跳转指令进行分支执行。 -
EVM 字节码的特点:
跳转位置无法静态确定:EVM 的跳转指令 (例如 jump, jumpi) 的目标地址需要在运行时动态确定,这增加了智能合约代码的复杂性和安全性。
没有返回指令:EVM 没有类似于函数调用的返回指令,而是通过状态转换完成函数调用和返回。 -
智能合约中的数据存储:
存储 (Storage): 用于存储永久数据,例如 NFT 的所有权信息、DeFi 合约的余额等。
内存 (Memory): 用于存储临时数据,例如函数调用的参数和返回值、循环变量等。
calldata: 用于存储函数调用的输入数据,例如交易数据、NFT 的 ID 等。 -
智能合约中的状态变量:
每个可变状态变量在编译时都会被分配一个 slot ID,指示其在存储空间中的位置。
slot ID 帮助 EVM 在执行时确定状态变量的存储位置。
对于复杂的数据类型,例如映射 (mapping) 和动态数组,需要结合 slot ID 和哈希计算来确定存储位置。
ERC-721
用于在智能合约中跟踪NFTs的一套规则和接口。
-
ERC-721 标准:
- 由以太坊改进提案(EIPs)定义,用于在智能合约中实现 NFT 的标准 API。
- ERC-721 标准适用于非同质化、不可分割且独特的代币,这些代币代表特定数字或物理资产的所有权。
- 与 ERC-20 标准不同,后者适用于可互换的同质化代币。
-
ERC-721 的关键功能:
- approve:允许代币所有者授权另一个地址(_approved)管理特定的代币(_tokenId)。
- setApprovalForAll:允许代币所有者授权一个操作者(_operator)管理他们所有的代币。
- transferFrom:允许代币所有者、授权操作者或特定代币操作者转移代币所有权。
- safeTransferFrom:与 transferFrom 类似,但增加了安全检查,确保接收方能够处理代币。
-
ERC-721 的接口:
- ERC-721 标准定义了强制性和可选性的接口。
- 开发者必须遵循 ERC-721 提出的开发注释来实现每个接口。
- 每个 ERC-721 兼容的智能合约都应该实现 ERC-721 和 ERC-165 接口。
-
安全性和元数据:
- safeTransferFrom 函数会调用 onERC721Received 接口,确保接收方能够处理代币。
- 钱包或代币接收者必须实现 onERC721Received 接口以支持代币转移。
- ERC721Metadata 扩展允许代币所有者在铸造新代币时设置代币 URI,用户可以通过此接口查询代币代表的资产详情
- ERC721Enumerable 接口允许 NFT 智能合约发布其完整的 NFT 列表,并使其可被发现。
存在的问题
the high value of NFTs also makes them a target for attackers. The defects in NFT smart contracts could be exploited by attackers to harm the security and reliability of the NFT ecosystem
NFT 的高价值也使其成为攻击者的目标。NFT 智能合约中的缺陷可能被攻击者利用,从而损害 NFT 生态系统的安全性和可靠性。
In addition, due to the immutability of smart contracts, it is critical to ensure that the NFT smart contract is bugfree before it is deployed on the blockchain.
智能合约的不可变性意味着一旦部署,就无法修改。因此,在将 NFT 智能合约部署到区块链之前,必须确保其没有缺陷。
Although a set of smart contract defects have been reported
in previous work [21], many scenarios cannot be covered due to
the increasing complexity and security requirements of smart contracts, e.g., NFT smart contracts
尽管之前的工作已经报告了一些智能合约缺陷,但无法涵盖 NFT 智能合约等复杂场景下的所有情况。
创新点
提出5种缺陷
- 数据收集:通过收集StackOverflow 帖子(使用“NFT”和“ERC721”标签进行过滤,我们获得了 672 个与 NFT 相关的 StackOverflow 帖子)和安全分析报告(例如 Medium、Twitter 以及知名区块链安全团队如 SlowMist 和 PeckShield 的官方网站,88份)
- 数据分析:
1. Risky Mutable Proxy(风险可变代理):
- 背景:
* OpenSea 是 NFT 生态系统中最大、最受欢迎的交易市场。
* OpenSea 使用 Wyvern 协议来促进 NFT 的去中心化交易。
* 当卖家首次在 OpenSea 上列出他们的 NFT 时,一个代理注册合约会创建一个智能合约,称为 OwnableDelegateProxy。
* 这个合约存储了卖家的地址,代理注册合约可以使用这个新合约代表卖家采取行动并调用其他合约的方法。
* 当卖家在他们 NFT 智能合约中列出任何项目时,他们会授权代理注册合约转移他们的代币。
* 因此,用户不需要为每个 NFT 支付额外的 gas 费用以获取额外批准,使得交易变得简单。 - 示例:
* 如果代理注册合约的地址可以被修改,那么所有用户的代币都可能被转移到攻击者手中。
* 攻击者可以通过代理设置功能更改代理注册地址,而无需获得权限。
缺陷代码:
-
setProxyRegistryAddress 函数
function setProxyRegistryAddress(address proxyAddress) external onlyOwner { proxyRegistryAddress = proxyAddress; }
- 这个函数允许合约的所有者设置一个新的代理合约地址。这是通过简单地将传入的
proxyAddress
赋值给状态变量proxyRegistryAddress
来实现的。 - 问题在于,如果合约的所有者是恶意的,或者所有者的私钥被泄露,那么攻击者可以利用这个函数将代理合约地址更改为一个他们控制的地址。
- 这个函数允许合约的所有者设置一个新的代理合约地址。这是通过简单地将传入的
-
isApprovedForAll 函数
function isApprovedForAll(address owner, address operator) override public view returns (bool) { ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); if (address(proxyRegistry.proxies(owner)) == operator) { return true; } return super.isApprovedForAll(owner, operator); }
- 这个函数检查一个操作者是否有权代表所有者进行操作。它首先尝试通过代理合约来验证操作者是否被授权。
- 如果
proxyRegistry.proxies(owner)
返回的地址与operator
相匹配,那么函数返回true
,表示操作者被授权。 - 如果不匹配,它会调用父合约的
isApprovedForAll
函数来检查是否有其他形式的授权。
安全风险
- 代理地址可修改性: 由于
setProxyRegistryAddress
函数允许修改代理合约地址,这可能被恶意行为者利用。如果代理合约地址被更改为攻击者控制的合约,那么攻击者可以通过这个代理合约来控制所有通过它授权的NFT。 - 权限管理漏洞:
isApprovedForAll
函数的逻辑可能会导致未经授权的操作者获得对NFT的控制权,尤其是当代理合约的地址被恶意更改后。
2. ERC-721 Reentrancy(ERC-721可重入):
- 定义: 在外部调用后修改状态变量。
- 示例: 描述了一种情况,其中智能合约在调用外部合约(如在
safeTransferFrom
函数中)后,没有正确地锁定状态变量,导致可以重新进入(reentering)并修改状态,这可能会破坏合约的逻辑。
缺陷代码
function mintNFT(uint256 _numOfTokens, bytes memory _signature) public payable {
// 预检查地址是否已经铸造过NFT
(bool success, string memory reason) = canMint(msg.sender, _signature);
require(success, reason);
for (uint i = 0; i < _numOfTokens; i++) {
_safeMint(msg.sender, totalSupply() + 1);
}
addressMinted[msg.sender] = true;
}
-
函数定义:
mintNFT
函数接受两个参数:_numOfTokens
表示要铸造的NFT数量,_signature
表示一些验证信息。- 该函数是
payable
的,意味着它可以接收以太币。
-
预检查:
canMint
函数被调用来检查msg.sender
(调用者地址)是否有权限铸造NFT。这个检查基于传入的签名。- 如果
canMint
返回success
为false
,则函数会停止执行并显示错误信息reason
。
-
铸造NFT:
- 通过一个循环,为每个要铸造的NFT调用
_safeMint
函数。 _safeMint
函数是安全铸造NFT的标准实践,它调用接收合约的onERC721Received
钩子函数,以确保接收方合约能够接受NFT。
- 通过一个循环,为每个要铸造的NFT调用
-
标记已铸造:
- 循环结束后,将
addressMinted[msg.sender]
设置为true
,表示该地址已经铸造过NFT。
- 循环结束后,将
安全风险:
- 可重入调用: 在
_safeMint
调用期间,如果接收NFT的合约(可能是恶意的)实现了onERC721Received
函数,它可以在_safeMint
调用期间再次调用mintNFT
函数。 - 状态竞争条件: 因为
addressMinted[msg.sender]
在循环结束后才被设置为true
,如果在_safeMint
调用期间mintNFT
被重新调用,那么canMint
的检查可能会被绕过,允许用户铸造超过限制的NFT。大多数情况下,这种重入调用会导致铸造的 NFT 数量超过稀有度阈值,损害其他买家的利益。
3. Unlimited Minting(无限铸造):
- 定义: 在铸造NFT时不检查NFT的最大供应量。
- 示例: 合约中没有适当的检查来限制铸造的NFT数量,可能导致超过预定数量的NFT被铸造,影响NFT的稀缺性和价值。
缺陷代码解析
function reserveApes() public onlyOwner {
uint supply = totalSupply();
uint i;
for (i = 0; i < 30; i++) {
_safeMint(msg.sender, supply + i);
}
}
-
函数定义:
reserveApes
函数没有参数,并且只能由合约的所有者(onlyOwner
)调用。
-
获取当前供应量:
- 调用
totalSupply()
函数获取当前已铸造的NFT总量,并将其存储在变量supply
中。
- 调用
-
铸造NFT:
- 通过一个循环,每次循环调用
_safeMint
函数来铸造一个新的NFT。 _safeMint
函数接受两个参数:接收者地址(在这里是msg.sender
,即合约所有者)和要铸造的NFT的ID(在这里是supply + i
)。
- 通过一个循环,每次循环调用
安全风险:
- 无限制铸造: 这个函数没有检查每次铸造后是否超出了项目的预定最大供应量。由于在循环中连续调用
_safeMint
,每次铸造的NFT ID都是基于当前供应量(supply
),这可能导致铸造的NFT数量超出了项目的预定限制。 - 潜在的经济影响: 如果攻击者或合约所有者滥用这个功能,他们可以无限制地铸造新的NFT,这将破坏NFT的稀缺性,从而对项目的货币价值和市场信任造成重大损害。
4. Missing Requirements(缺少要求):
- 定义: 未遵循ERC-721标准接口的开发注释。
- 示例: 开发者在实现合约时没有遵循ERC-721标准的要求,例如在
approve
函数中没有进行必要的权限检查,这可能导致安全问题。
缺陷代码解析
/* ERC-721 annotations on approve function */
// Throws unless msg.sender is the current NFT owner, or an authorized operator of the current owner.
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721.ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
/* missing requirement of checking msg.sender */
_approve(to, tokenId);
}
-
函数注释:
- 注释说明了
approve
函数的预期行为:除非msg.sender
是当前NFT的所有者或被授权的操作者,否则应抛出异常。
- 注释说明了
-
获取NFT所有者:
owner
变量通过调用ERC721.ownerOf(tokenId)
获取指定NFT的所有者地址。
-
检查接收者:
- 使用
require
函数检查被授权的地址 (to
) 是否不等于NFT的所有者 (owner
)。如果是,将抛出异常,因为逻辑上不应该将NFT授权给其所有者。
- 使用
-
缺少调用者检查:
- 注释中提到缺少对
msg.sender
的检查,这是ERC-721标准中approve
函数的一个重要要求。msg.sender
应该是NFT的所有者或被授权的操作者。
- 注释中提到缺少对
-
实际授权:
- 调用
ERC721
合约的内部函数_approve
来实际设置授权,允许地址to
转移或操作指定的tokenId
。
- 调用
安全风险:
- 授权给非所有者或非授权操作者: 由于缺少对
msg.sender
的检查,任何人都可以调用此approve
函数尝试授权自己为特定NFT的操作者。如果这个缺陷被利用,攻击者可以授权自己操作他人的NFT,然后转移或以其他方式滥用这些NFT。
5. Public Burn(公开销毁):
- 定义: 在销毁NFT的操作中未检查调用者权限。
- 示例: 合约中的
burn
函数是公开的,没有适当的权限检查,任何用户都可以调用它来销毁他人的NFT,这显然违背了NFT所有权的基本原则。
缺陷代码解析
function burn(uint256 tokenId) public {
_burn(tokenId);
}
function _burn(uint256 tokenId) internal virtual {
address owner = ERC721.ownerOf(tokenId);
// Clear approvals
_approve(address(0), tokenId);
_balances[owner] -= 1;
delete _owners[tokenId];
}
-
burn 函数:
burn
函数是公开的,意味着任何外部调用者都可以触发这个函数。- 它接受一个
tokenId
参数,代表要销毁的NFT的ID。 - 函数内部直接调用了一个内部函数
_burn
,传递了相同的tokenId
。
-
_burn 函数:
_burn
函数是内部的(internal
),意味着它只能在合约内部或继承合约中被调用。- 它首先获取NFT的所有者地址。
- 然后调用
_approve
函数,将NFT的授权地址设置为0,这表示撤销所有对该NFT的授权。 - 接下来,减少所有者账户的余额计数。
- 最后,使用
delete
语句从_owners
映射中移除该NFT,实际上销毁了这个NFT。
安全风险:
- 公开销毁权限:
burn
函数是公开的,没有对调用者进行检查,这意味着任何外部调用者都可以销毁任何NFT,而不仅仅是他们自己的。这显然是一个严重的安全漏洞,因为它允许任何人销毁他人的资产。 - 所有权检查缺失: 在
_burn
函数中,尽管获取了NFT的所有者地址,但没有检查调用burn
函数的地址是否与NFT的所有者地址相同。这导致了上述的安全风险。
设计NFTGuard 工具
NFTGuard工具概述
-
主要组件:
- Inputter: 负责接收和处理输入的Solidity源代码。
- Feature Detector: 用于检测合约中的关键特征,如映射存储、删除操作和外部调用。
- CFG Builder: 基于符号执行构建控制流图(CFG),用于分析合约的执行路径。
- Defect Identifier: 根据预定义的规则和模式识别和报告检测到的缺陷。
-
工作流程:
- 用户输入Solidity源代码,该代码被编译成EVM字节码和抽象语法树(AST)以供进一步分析。
- Inputter组件从AST中提取源映射信息,并使用槽映射来存储变量与它们槽ID之间的映射关系。
- 通过Geth API将合约字节码反汇编成操作码(opcodes),然后动态构建CFG。
- 在符号执行过程中,Feature Detector检测关键的操作特征,如映射存储、删除操作和外部调用。
- Defect Identifier根据预定义的模式和规则报告检测到的缺陷。
-
结合源代码和字节码信息:
- NFTGuard利用从源代码和字节码中提取的关键信息来提高检测的准确性和覆盖率。
- 通过分析AST,NFTGuard能够获取状态变量的槽ID和数据类型,这些信息在符号执行期间用于监控特定变量的操作。
-
设计动机:
- 使用源代码信息的目的是为了在执行特定操作码时定位缺陷代码,这有助于更有效地检测复杂的NFT智能合约。
-
扩展性:
- NFTGuard被设计为一个可扩展的框架,支持最新的Solidity编译器版本(例如v0.8+),并允许开发者添加新的检测模式来识别更多类型的缺陷。
实验
数据集
Smart Contract Sanctuary 是一个专为在 Etherscan 上验证过的 Ethereum 智能合约提供存储的仓库。
作者通过关键词“NFT”或“ERC721”过滤,提取了 NFT 智能合约。
由于这篇论文撰写时最新的 Solidity 编译器版本是 0.8.16,作者选择了这个版本,并移除了无法编译的合约。
最终,作者获得了 16,527 个智能合约,并进行了大规模的实验。
评估效果
- 合约缺陷(Contract Defect):这列列出了NFTGuard检测到的缺陷类型。
- # Defects:这列显示了数据集中每个缺陷类型的数量。
- Per(%):这列显示了数据集中每个缺陷类型的百分比。
- # Samples:这列显示了为评估每个缺陷类型而随机抽取的样本数量。
- # TP:这列显示了每个缺陷类型中,被正确检测为缺陷的样本数量(真阳性)。
- # FP:这列显示了每个缺陷类型中,被错误检测为缺陷的样本数量(假阳性)。
- Prec(%):这列显示了每个缺陷类型的精确率,即真阳性与真阳性加假阳性总数之比。
关于随机抽取
作者通过以下步骤来评估 NFTGuard 的性能:
- 随机抽样:从每个缺陷的检测结果中随机抽取一定数量的合约。这些合约是 NFTGuard 报告为阳性的合约。
- 样本大小确定:为了确定每个缺陷的样本大小,作者采用了基于置信区间的抽样方法。这种方法旨在从总体中推断出特定缺陷的缺陷数量。
- 置信区间和置信水平:作者设定了 10% 的置信区间和 95% 的置信水平,并计算了需要收集的样本数量。
- 样本数量计算:根据计算结果,作者为五个缺陷分别计算了所需的样本数量,分别为 13、81、86、44 和 30。
- 数据集抽样和手动标注:根据计算结果,作者对数据集进行了抽样,并由两位作者仔细手动标注了这些样本。
- 真阳性和假阳性分离:在标注过程中,作者将样本分为真阳性和假阳性,以便分析 NFTGuard 的性能。
- 相关工作的采用:这种评估方法也被其他相关研究采用,以评估智能合约缺陷检测工具的性能。
通过这种方法,作者能够评估 NFTGuard 在检测 NFT 智能合约缺陷方面的性能,包括其精确率和误报情况,从而回答 RQ2。