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;
}
多重继承
contract SatoshiNakamoto is NickSzabo, HalFinney {
// 啧啧啧,宇宙的奥秘泄露了
}
正如你所见,当使用多重继承的时候,你只需要用逗号 , 来隔开几个你想要继承的合约。在上面的例子中,我们的合约继承自NickSzabo和HalFinney。
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。
同样的,因为在我们的 DApp 里已经有一个 mapping (映射) 存储了这个信息,所以对我们来说这个实现非常直接清晰。我们可以只用一行 return 语句来实现这个函数。
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都将包含相同的转移逻辑,只是以相反的顺序。 (一种情况是代币的发送者调用函数;另一种情况是代币的接收者调用它)。
SafeMath预防溢出
合约安全增强: 溢出和下溢
我们将来学习你在编写智能合约的时候需要注意的一个主要的安全特性:防止溢出和下溢。
什么是溢出 (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),默认情况下可以防止这些问题。
比如,使用 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 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;
}
}
库允许我们使用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 方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。
6.注释
contract CryptoZombies {
/* 这是一个多行注释。我想对所有花时间来尝试这个编程课程的人说声谢谢。
它是免费的,并将永远免费。但是我们依然倾注了我们的心血来让它变得更好。
要知道这依然只是区块链开发的开始而已,虽然我们已经走了很远,
仍然有很多种方式来让我们的社区变得更好。
如果我们在哪个地方出了错,欢迎在我们的 github 提交 PR 或者 issue 来帮助我们改进:
https://github.com/loomnetwork/cryptozombie-lessons
或者,如果你有任何的想法、建议甚至仅仅想和我们打声招呼,欢迎来我们的电报群:
https://t.me/loomnetworkcn
*/
}
特别是,最好为你合约中每个方法添加注释来解释它的预期行为。这样其他开发者(或者你自己,在6个月以后再回到这个项目中)可以很快地理解你的代码而不需要逐行阅读所有代码。
Solidity 社区所使用的一个标准是使用一种被称作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 (返回) 用来描述这个方法需要传入什么参数以及返回什么值。