- 一、payable
- 二、提现
- 三、ERC20代币
- 三、ERC721代币
- 四、合约的多重继承
- 五、balanceOf
- 六、ownerof
- 七、代币转移(所有权)
- transfer()
- approve()
- takeOwnership()
- 八、合约安全增强:溢出和下溢
- 什么是溢出、下溢
- SafeMath库
- library关键字
- 使用SafeMath库
- assert()函数
- uint16 / uint32防止溢出的问题
contract OnlineStore {
function buySomething() external payable {
// 检查以确定0.001以太发送出去来运行函数:
require(msg.value == 0.001 ether);
// 如果为真,一些用来向函数调用者发送数字内容的逻辑
transferThing(msg.sender);
}
}
在这里,msg.value 是一种可以查看向合约发送了多少以太的方法,另外 ether 是一个內建单元。
这里发生的事是,一些人会从 web3.js 调用这个函数 (从DApp的前端), 像这样 :
OnlineStore.buySomething().send(from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001))
注意这个 value 字段, JavaScript 调用来指定发送多少(0.001)以太。如果把事务想象成一个信封,你发送到函数的参数就是信的内容。 添加一个 value 很像在信封里面放钱 —— 信件内容和钱同时发送给了接收者。
二、提现注意: 如果一个函数没标记为payable, 而你尝试利用上面的方法发送以太,函数将拒绝你的事务。
在你发送以太之后,它将被存储进以合约的以太坊账户中, 并冻结在哪里 —— 除非你添加一个函数来从合约中把以太提现。
你可以写一个函数来从合约中提现以太,类似这样:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
注意我们使用 Ownable 合约中的 owner 和 onlyOwner,假定它已经被引入了。
你可以通过 transfer 函数向一个地址发送以太, 然后 this.balance 将返回当前合约存储了多少以太。 所以如果100个用户每人向我们支付1以太, this.balance 将是100以太。
你可以通过 transfer 向任何以太坊地址付钱。 比如,你可以有一个函数在 msg.sender 超额付款的时候给他们退钱:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
或者在一个有卖家和卖家的合约中, 你可以把卖家的地址存储起来, 当有人买了它的东西的时候,把买家支付的钱发送给它 seller.transfer(msg.value)。
有很多例子来展示什么让以太坊编程如此之酷 —— 你可以拥有一个不被任何人控制的去中心化市场。
一个 代币 在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value) 和 balanceOf(address _owner).在智能合约内部,通常有一个映射, mapping(address => uint256) balances,用于追踪每个地址还有多少余额。
所以基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将他们的代币转移到其他地址的函数。
由于所有 ERC20 代币共享具有相同名称的同一组函数,它们都可以以相同的方式进行交互。
这意味着如果你构建的应用程序能够与一个 ERC20 代币进行交互,那么它就也能够与任何 ERC20 代币进行交互。 这样一来,将来你就可以轻松地将更多的代币添加到你的应用中,而无需进行自定义编码。 你可以简单地插入新的代币合约地址,然后哗啦,你的应用程序有另一个它可以使用的代币了。
其中一个例子就是交易所。 当交易所添加一个新的 ERC20 代币时,实际上它只需要添加与之对话的另一个智能合约。 用户可以让那个合约将代币发送到交易所的钱包地址,然后交易所可以让合约在用户要求取款时将代币发送回给他们。
交易所只需要实现这种转移逻辑一次,然后当它想要添加一个新的 ERC20 代币时,只需将新的合约地址添加到它的数据库即可。
三、ERC721代币对于像货币一样的代币来说,ERC20 代币非常酷。 但是要在我们僵尸游戏中代表僵尸就并不是特别有用。
首先,僵尸不像货币可以分割 —— 我可以发给你 0.237 以太,但是转移给你 0.237 的僵尸听起来就有些搞笑。
其次,并不是所有僵尸都是平等的。 你的2级僵尸"Steve"完全不能等同于我732级的僵尸"H4XF13LD MORRIS "。(你差得远呢,Steve)。
有另一个代币标准更适合如 CryptoZombies 这样的加密收藏品——它们被称为ERC721 代币.
ERC721 代币是不能互换的,因为每个代币都被认为是唯一且不可分割的。 你只能以整个单位交易它们,并且每个单位都有唯一的 ID。 这些特性正好让我们的僵尸可以用来交易。
四、合约的多重继承请注意,使用像 ERC721 这样的标准的优势就是,我们不必在我们的合约中实现拍卖或托管逻辑,这决定了玩家能够如何交易/出售我们的僵尸。 如果我们符合规范,其他人可以为加密可交易的 ERC721 资产搭建一个交易所平台,我们的 ERC721 僵尸将可以在该平台上使用。 所以使用代币标准相较于使用你自己的交易逻辑有明显的好处。
在Solidity,你的合约可以继承自多个合约,参考如下:
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 语句来实现这个函数。
七、代币转移(所有权)注意:要记得, uint256 等同于uint。我们从课程的开始一直在代码中使用 uint,但从现在开始我们将在这里用
uint256,因为我们直接从规范中复制粘贴。
ERC721 规范有两种不同的方法来转移代币:
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
1.第一种方法是代币的拥有者调用transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId。
2.第二种方法是代币拥有者首先调用 approve,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个 mapping (uint256 => address) 里。然后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。
transfer 和 takeOwnership 都将包含相同的转移逻辑,只是以相反的顺序。 (一种情况是代币的发送者调用函数;另一种情况是代币的接收者调用它)。
所以我们把这个逻辑抽象成它自己的私有函数 _transfer,然后由这两个函数来调用它。 这样我们就不用写重复的代码了。
transfer()1. 确保只有代币所有者可以批准某人获取代币,所以添加onlyOwnerOf修饰符(此修饰符目前为自定义修饰符)。
2.定义一个_transfer函数
2.修改记录持有个数的映射,发送者--,接收者人++
3.改变代币持有者的映射,把该代币指向接收者
4.触发ERC721的Transfer事件
5.transfer调用_transfer函数
approve()
使用 approve 或者 takeOwnership 的时候,转移有2个步骤:
1.你,作为所有者,用新主人的 address 和你希望他获取的 _tokenId 来调用 approve
2.新主人用 _tokenId 来调用 takeOwnership,合约会检查确保他获得了批准,然后把代币转移给他。
因为这发生在2个函数的调用中,所以在函数调用之间,我们需要一个数据结构来存储什么人被批准获取什么。
1.定义批准对应关系映射,用来快速查找谁被批准获取哪个代币。
2.确保只有代币所有者可以批准某人获取代币,所以添加onlyOwnerOf修饰符(此修饰符目前为自定义修饰符)。
3.函数的正文部分,将 _tokenId 的 映射 设置为和 _to 相等。
4.最后,在 ERC721 规范里有一个 Approval 事件。所以我们应该在这个函数的最后触发这个事件。(参考 erc721.sol 来确认传入的参数,并确保 _owner 是 msg.sender)
takeOwnership()
确保msg.sender已经被批准提取这个代币,若确认就调用_transfer.
1.require批准映射表,判断是否是已批准的人调用的方法
2.为了调用_tranfer函数,查询当前代币的所有人(例如ownerOf(_tokenId))
3.调用 _transfer, 并传入所有必须的参数。
八、合约安全增强:溢出和下溢
什么是溢出、下溢
假设我们有一个 uint8, 只能存储8 bit数据。这意味着我们能存储的最大数字就是二进制 11111111 (或者说十进制的 2^8 - 1 = 255).
uint8 number = 255;
number++;
在这个例子中,导致了溢出——虽然我们加了1,但是number出乎意料的等于0了。
下溢也类似,如果你从等于0的uint8减去1,它将变成255(因为uint是无符号的,其不能等于负数)。
虽然我们在这里不使用uint8,而且每次给一个uint256加1也不太可能溢出(2^256真的是一个很大的数了),但在我们的合约中添加一些保护机制依然是非常必要的,以防我们的DApp以后出现什么异常情况。
为了防止上述溢出、下溢这些情况,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
library关键字
library 关键字 — 库和 合约很相似,但是又有一些不同。 就我们的目的而言,库允许我们使用 using 关键字,它可以自动把库的所有方法添加给一个数据类型:
using SafeMath for uint;
// 这下我们可以为任何 uint 调用这些方法了
uint test = 2;
test = test.mul(3); // test 等于 6 了
test = test.add(5); // test 等于 11 了
使用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;
}
}
使用using把库的所有方法添加给一个数据类型:
using SafeMath for uint;
注意 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。这样就防止了溢出。
所以简而言之, SafeMath 的 add, sub, mul, 和 div 方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。
assert 和 require 相似,若结果为否它就会抛出错误。 assert 和 require 区别在于,require 若失败则会返还给用户剩下的 gas, assert 则不会。所以大部分情况下,你写代码的时候会比较喜欢 require,assert 只在代码可能出现严重错误的时候使用,比如 uint 溢出。
uint16 / uint32防止溢出的问题(通常情况下,总是使用 SafeMath 而不是普通数学运算是个好主意,也许在以后 Solidity 的新版本里这点会被默认实现,但是现在我们得自己在代码里实现这些额外的安全措施)。
但是有一个问题,例如
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
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。例如uint32:
library SafeMath32 {
function mul(uint32 a, uint32 b) internal pure returns (uint32) {
if (a == 0) {
return 0;
}
uint32 c = a * b;
assert(c / a == b);
return c;
}
function div(uint32 a, uint32 b) internal pure returns (uint32) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint32 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint32 a, uint32 b) internal pure returns (uint32) {
assert(b <= a);
return a - b;
}
function add(uint32 a, uint32 b) internal pure returns (uint32) {
uint32 c = a + b;
assert(c >= a);
return c;
}
}
在使用时,前面加上这个就可以了。
using SafeMath32 for uint32;
using SafeMath16 for uint16;
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)