solidity(5)——ERC721标准和加密收藏品
第一章 以太坊上的代币
一个_代币_在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value)
和 balanceOf(address _owner)
.
在智能合约内部,通常有一个映射,mapping(address => uint256) balances
,用于追踪每个地址还有多少余额。
所以基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将他们的代币转移到其他地址的函数。
-
ERC20代币
由于所有 ERC20 代币共享具有相同名称的同一组函数,它们都可以以相同的方式进行交互。这意味着如果你构建的应用程序能够与一个 ERC20 代币进行交互,那么它就也能够与任何 ERC20 代币进行交互。 这样一来,将来你就可以轻松地将更多的代币添加到你的应用中,而无需进行自定义编码。
其中一个例子就是交易所。 当交易所添加一个新的 ERC20 代币时,实际上它只需要添加与之对话的另一个智能合约。用户可以让那个合约将代币发送到交易所的钱包地址,然后交易所可以让合约在用户要求取款时将代币发送回给他们。交易所只需要实现这种转移逻辑一次,然后当它想要添加一个新的 ERC20 代币时,只需将新的合约地址添加到它的数据库即可。 -
ERC721代币
ERC721 代币是不能互换的,因为每个代币都被认为是唯一且不可分割的。 你只能以整个单位交易它们,并且每个单位都有唯一的 ID。
第二章 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;
}
在实现一个代币合约的时候,我们首先要做的是将接口复制到它自己的 Solidity 文件并导入它,import "./erc721.sol";。 接着,让我们的合约继承它,然后我们用一个函数定义来重写每个方法。
第三章 balanceOf和ownerOf
- balanceOf
function balanceOf(address _owner) public view returns (uint256 _balance);
这个函数只需要一个传入 address 参数,然后返回这个 address 拥有多少代币。
- ownerOf
function ownerOf(uint256 _tokenId) public view returns (address _owner);
这个函数需要传入一个代币 ID 作为参数 (我们的情况就是一个僵尸 ID),然后返回该代币拥有者的 address。
第五章 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 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。
第九章 预防溢出
- 溢出
假设我们有一个uint8, 只能存储8bit数据。这意味着我们能存储的最大数字就是二进制 11111111 (或者说十进制的 2^8 - 1 = 255)。
uint8 number = 255;
number++;
//在这个例子中,我们导致了溢出 — 虽然我们加了1, 但是 number 出乎意料地等于 0了。 (如果你给二进制 11111111 加1, 它将被重置为 00000000,就像钟表从 23:59 走向 00:00)。
-
下溢
如果你从一个等于 0 的 uint8 减去 1, 它将变成 255 (因为 uint 是无符号的,其不能等于负数)。 -
使用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第二部分
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 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return 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
关键字 — 库(library)和 合约(contract)很相似,但是又有一些不同。 就我们的目的而言,库允许我们使用 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第三部分
如果我们用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。
第十三章 注释
- 单行注释:
//
- 多行注释:
/*...*/
natspec
格式注释:
// @title 一个简单的基础运算合约
/// @author H4XF13LD MORRIS 💯💯😎💯💯
/// @notice 现在,这个合约只添加一个乘法
contract Math {
/// @notice 两个数相乘
/// @param x 第一个 uint
/// @param y 第二个 uint
/// @return z (x * y) 的结果
/// @dev 现在这个方法不检查溢出
function multiply(uint x, uint y) returns (uint z) {
// 这只是个普通的注释,不会被 natspec 解释
z = x * y;
}
}
@title
(标题) 和 @author
(作者)很直接了.
@notice
(须知)向 用户 解释这个方法或者合约是做什么的。@dev
(开发者) 是向开发者解释更多的细节。
@param
(参数)和 @return
(返回)用来描述这个方法需要传入什么参数以及返回什么值。
注意你并不需要每次都用上所有的标签,它们都是可选的。不过最少,写下一个 @dev注释来解释每个方法是做什么的。