Web3-代币ERC20/ERC721以及合约安全溢出和下溢的研究
Web3-代币ERC20/ERC721以及合约安全溢出和下溢的研究
以太坊上的代币
如果你对以太坊的世界有一些了解,你很可能听人们聊过代币— ERC20代币
一个 代币 在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value)
和 balanceOf(address _owner)
。
在智能合约内部,通常有一个映射, mapping(address => uint256) balances
,用于追踪每个地址还有多少余额。
所以基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将他们的代币转移到其他地址的函数。
ERC20的重要性
由于所有 ERC20 代币共享具有相同名称的同一组函数,它们都可以以相同的方式进行交互。
这意味着如果你构建的应用程序能够与一个 ERC20 代币进行交互,那么它就也能够与任何 ERC20 代币进行交互。 这样一来,将来你就可以轻松地将更多的代币添加到你的应用中,而无需进行自定义编码。 你可以简单地插入新的代币合约地址,然后哗啦,你的应用程序有另一个它可以使用的代币了。
其中一个例子就是交易所。 当交易所添加一个新的 ERC20 代币时,实际上它只需要添加与之对话的另一个智能合约。 用户可以让那个合约将代币发送到交易所的钱包地址,然后交易所可以让合约在用户要求取款时将代币发送回给他们。
交易所只需要实现这种转移逻辑一次,然后当它想要添加一个新的 ERC20 代币时,只需将新的合约地址添加到它的数据库即可。
其他代币标准
对于像货币一样的代币来说,ERC20 代币非常酷。 但是要在我们僵尸游戏中代表僵尸就并不是特别有用。
首先,僵尸不像货币可以分割 —— 我可以发给你 0.237 以太,但是转移给你 0.237 的僵尸听起来就有些搞笑。
其次,并不是所有僵尸都是平等的。 你的2级僵尸"Steve"完全不能等同于我732级的僵尸"H4XF13LD MORRIS 💯💯😎💯💯"。(你差得远呢,Steve)。
有另一个代币标准更适合如 CryptoZombies 这样的加密收藏品——它们被称为*ERC721 代币.*
*ERC721 代币*是不能互换的,因为每个代币都被认为是唯一且不可分割的。 你只能以整个单位交易它们,并且每个单位都有唯一的 ID。 这些特性正好让我们的僵尸可以用来交易。
请注意,使用像 ERC721 这样的标准的优势就是,我们不必在我们的合约中实现拍卖或托管逻辑,这决定了玩家能够如何交易/出售我们的僵尸。 如果我们符合规范,其他人可以为加密可交易的 ERC721 资产搭建一个交易所平台,我们的 ERC721 僵尸将可以在该平台上使用。 所以使用代币标准相较于使用你自己的交易逻辑有明显的好处。
ERC721标准 多重继承
我们先看看ERC721标准
contract ERC721 {event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);function balanceOf(address _owner) public view returns (uint256 _balance);function ownerOf(uint256 _tokenId) public view returns (address _owner);function transfer(address _to, uint256 _tokenId) public;function approve(address _to, uint256 _tokenId) public;function takeOwnership(uint256 _tokenId) public;
}
我们在代码中应该如何使用ERC721,我们这边首先应用erc721,然后再继承他
pragma solidity ^0.4.19;import "./zombieattack.sol";
// 在这里引入文件
import "./erc721.sol";
// 在这里声明 ERC721 的继承
contract ZombieOwnership is ZombieAttack, ERC721 {
}
balanceOf和ownerOf
我们将实现两个方法balanceO
f和ownerOf
balanceOf:这个函数只需要一个传入 address
参数,然后返回这个 address
拥有多少代币。
function balanceOf(address _owner) public view returns (uint256 _balance);
ownerOf:这个函数需要传入一个代币 ID 作为参数 (我们的情况就是一个僵尸 ID),然后返回该代币拥有者的 address
。
function ownerOf(uint256 _tokenId) public view returns (address _owner);
ERC721转移标准
把所有权从一个人转移给另一个人来继续我们的 ERC721 规范的实现。
注意 ERC721 规范有两种不同的方法来转移代币:
function transfer(address _to, uint256 _tokenId) public;function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
- 第一种方法是代币的拥有者调用
transfer
方法,传入他想转移到的address
和他想转移的代币的_tokenId
。 - 第二种方法是代币拥有者首先调用
approve
,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个mapping (uint256 => address)
里。然后,当有人调用takeOwnership
时,合约会检查msg.sender
是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。
你注意到了吗,transfer
和 takeOwnership
都将包含相同的转移逻辑,只是以相反的顺序。 (一种情况是代币的发送者调用函数;另一种情况是代币的接收者调用它)。
所以我们把这个逻辑抽象成它自己的私有函数 _transfer
,然后由这两个函数来调用它。 这样我们就不用写重复的代码了。
ERC721 批准 approve
现在,让我们来实现 approve
。
记住,使用 approve
或者 takeOwnership
的时候,转移有2个步骤:
- 你,作为所有者,用新主人的
address
和你希望他获取的_tokenId
来调用approve
- 新主人用
_tokenId
来调用takeOwnership
,合约会检查确保他获得了批准,然后把代币转移给他。
因为这发生在2个函数的调用中,所以在函数调用之间,我们需要一个数据结构来存储什么人被批准获取什么。
合约安全增强:溢出和下溢
在编写智能合约的时候需要注意的一个主要的安全特性:防止溢出和下溢。
什么是 溢出 (*overflow*)?
假设我们有一个 uint8
, 只能存储8 bit数据。这意味着我们能存储的最大数字就是二进制 11111111
(或者说十进制的 2^8 - 1 = 255).
来看看下面的代码。最后 number
将会是什么值?
uint8 number = 255;
number++;
在这个例子中,我们导致了溢出 — 虽然我们加了1, 但是 number
出乎意料地等于 0
了。 (如果你给二进制 11111111
加1, 它将被重置为 00000000
,就像钟表从 23:59
走向 00:00
)。
下溢(underflow
)也类似,如果你从一个等于 0
的 uint8
减去 1
, 它将变成 255
(因为 uint
是无符号的,其不能等于负数)。
虽然我们在这里不使用 uint8
,而且每次给一个 uint256
加 1
也不太可能溢出 (2^256 真的是一个很大的数了),在我们的合约中添加一些保护机制依然是非常有必要的,以防我们的 DApp 以后出现什么异常情况。
使用SafeMath
为了防止这些情况,OpenZeppelin 建立了一个叫做 SafeMath 的 库(*library*),默认情况下可以防止这些问题。
不过在我们使用之前…… 什么叫做库?
一个**库** 是 Solidity 中一种特殊的合约。其中一个有用的功能是给原始数据类型增加一些方法。
比如,使用 SafeMath 库的时候,我们将使用 using SafeMath for uint256
这样的语法。 SafeMath 库有四个方法 — add
, sub
, mul
, 以及 div
。现在我们可以这样来让 uint256
调用这些方法:
using SafeMath for uint256;uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
SafeMath的部分核心代码
library SafeMath {function mul(uint256 a, uint256 b) internal pure returns (uint256) {if (a == 0) {return 0;}uint256 c = a * b;assert(c / a == b);return c;}function div(uint256 a, uint256 b) internal pure returns (uint256) {// assert(b > 0); // Solidity automatically throws when dividing by 0uint256 c = a / b;// assert(a == b * c + a % b); // There is no case in which this doesn't holdreturn c;}function sub(uint256 a, uint256 b) internal pure returns (uint256) {assert(b <= a);return a - b;}function add(uint256 a, uint256 b) internal pure returns (uint256) {uint256 c = a + b;assert(c >= a);return c;}
}
首先我们有了 library
关键字 — 库和 合约
很相似,但是又有一些不同。 就我们的目的而言,库允许我们使用 using
关键字,它可以自动把库的所有方法添加给一个数据类型:
using SafeMath for uint;
// 这下我们可以为任何 uint 调用这些方法了
uint test = 2;
test = test.mul(3); // test 等于 6 了
test = test.add(5); // test 等于 11 了
注意 mul
和 add
其实都需要两个参数。 在我们声明了 using SafeMath for uint
后,我们用来调用这些方法的 uint
就自动被作为第一个参数传递进去了(在此例中就是 test
)
我们来看看 add
的源代码看 SafeMath 做了什么:
function add(uint256 a, uint256 b) internal pure returns (uint256) {uint256 c = a + b;assert(c >= a);return c;
}
基本上 add
只是像 +
一样对两个 uint
相加, 但是它用一个 assert
语句来确保结果大于 a
。这样就防止了溢出。
assert
和 require
相似,若结果为否它就会抛出错误。 assert
和 require
区别在于,require
若失败则会返还给用户剩下的 gas, assert
则不会。所以大部分情况下,你写代码的时候会比较喜欢 require
,assert
只在代码可能出现严重错误的时候使用,比如 uint
溢出。
所以简而言之, SafeMath 的 add
, sub
, mul
, 和 div
方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。
通常情况下,总是使用 SafeMath 而不是普通数学运算是个好主意,也许在以后 Solidity 的新版本里这点会被默认实现,但是现在我们得自己在代码里实现这些额外的安全措施。
不过我们遇到个小问题 — winCount
和 lossCount
是 uint16
, 而 level
是 uint32
。 所以如果我们用这些作为参数传入 SafeMath 的 add
方法。 它实际上并不会防止溢出,因为它会把这些变量都转换成 uint256
:
function add(uint256 a, uint256 b) internal pure returns (uint256) {uint256 c = a + b;assert(c >= a);return c;
}// 如果我们在`uint8` 上调用 `.add`。它将会被转换成 `uint256`.
// 所以它不会在 2^8 时溢出,因为 256 是一个有效的 `uint256`.
这就意味着,我们需要再实现两个库来防止 uint16
和 uint32
溢出或下溢。我们可以将其命名为 SafeMath16
和 SafeMath32
。
代码将和 SafeMath 完全相同,除了所有的 uint256
实例都将被替换成 uint32
或 uint16
。
我们已经将这些代码帮你写好了,打开 safemath.sol
合约看看代码吧。
总结
本文研究了以太坊智能合约中代币标准ERC20/ERC721的实现及其安全问题。首先介绍了ERC20代币作为可互换资产的合约标准,分析了其balanceOf和transfer等核心功能。其次探讨了ERC721代币作为不可互换资产的特性,详细说明了其多重继承的实现方式。文章重点分析了ERC721的两种所有权转移机制:直接transfer和approve/takeOwnership组合。最后强调了智能合约安全中防范数据溢出和下溢的重要性,建议使用SafeMath库来确保数值运算的安全性。通过标准代币接口和安全数学运算,开发者可以构建更可靠、更安全的去中心化应用。