当前位置: 首页 > backend >正文

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

我们将实现两个方法balanceOf和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;
  1. 第一种方法是代币的拥有者调用transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId
  2. 第二种方法是代币拥有者首先调用 approve,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个 mapping (uint256 => address) 里。然后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。

你注意到了吗,transfertakeOwnership 都将包含相同的转移逻辑,只是以相反的顺序。 (一种情况是代币的发送者调用函数;另一种情况是代币的接收者调用它)。

所以我们把这个逻辑抽象成它自己的私有函数 _transfer,然后由这两个函数来调用它。 这样我们就不用写重复的代码了。

ERC721 批准 approve

现在,让我们来实现 approve

记住,使用 approve 或者 takeOwnership 的时候,转移有2个步骤:

  1. 你,作为所有者,用新主人的 address 和你希望他获取的 _tokenId 来调用 approve
  2. 新主人用 _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)也类似,如果你从一个等于 0uint8 减去 1, 它将变成 255 (因为 uint 是无符号的,其不能等于负数)。

虽然我们在这里不使用 uint8,而且每次给一个 uint2561 也不太可能溢出 (2^256 真的是一个很大的数了),在我们的合约中添加一些保护机制依然是非常有必要的,以防我们的 DApp 以后出现什么异常情况。

使用SafeMath

为了防止这些情况,OpenZeppelin 建立了一个叫做 SafeMath 的 (*library*),默认情况下可以防止这些问题。

不过在我们使用之前…… 什么叫做库?

一个**** 是 Solidity 中一种特殊的合约。其中一个有用的功能是给原始数据类型增加一些方法。

比如,使用 SafeMath 库的时候,我们将使用 using SafeMath for uint256 这样的语法。 SafeMath 库有四个方法 — addsubmul, 以及 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 了

注意 muladd 其实都需要两个参数。 在我们声明了 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。这样就防止了溢出。

assertrequire 相似,若结果为否它就会抛出错误。 assertrequire 区别在于,require 若失败则会返还给用户剩下的 gas, assert 则不会。所以大部分情况下,你写代码的时候会比较喜欢 requireassert 只在代码可能出现严重错误的时候使用,比如 uint 溢出。

所以简而言之, SafeMath 的 addsubmul, 和 div 方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。

通常情况下,总是使用 SafeMath 而不是普通数学运算是个好主意,也许在以后 Solidity 的新版本里这点会被默认实现,但是现在我们得自己在代码里实现这些额外的安全措施。

不过我们遇到个小问题 — winCountlossCountuint16, 而 leveluint32。 所以如果我们用这些作为参数传入 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`.

这就意味着,我们需要再实现两个库来防止 uint16uint32 溢出或下溢。我们可以将其命名为 SafeMath16SafeMath32

代码将和 SafeMath 完全相同,除了所有的 uint256 实例都将被替换成 uint32uint16

我们已经将这些代码帮你写好了,打开 safemath.sol 合约看看代码吧。

总结

本文研究了以太坊智能合约中代币标准ERC20/ERC721的实现及其安全问题。首先介绍了ERC20代币作为可互换资产的合约标准,分析了其balanceOf和transfer等核心功能。其次探讨了ERC721代币作为不可互换资产的特性,详细说明了其多重继承的实现方式。文章重点分析了ERC721的两种所有权转移机制:直接transfer和approve/takeOwnership组合。最后强调了智能合约安全中防范数据溢出和下溢的重要性,建议使用SafeMath库来确保数值运算的安全性。通过标准代币接口和安全数学运算,开发者可以构建更可靠、更安全的去中心化应用。

http://www.xdnf.cn/news/14393.html

相关文章:

  • EXCEL破解VBA密码 ( 仅供学习研究使用)
  • [VSCode] VSCode 设置 python 的编译器
  • 40-Oracle 23 ai Bigfile~Smallfile-Basicfile~Securefile矩阵对比
  • NodeJS里经常用到require,require的模块加载机制是什么
  • lua版的Frpc
  • go.work
  • 车载通信架构 --- IP ECU 在连接被拒绝后的重连机制
  • Spring Cloud Gateway 全面学习指南
  • 论文略读:MLPs Learn In-Context on Regression and Classification Tasks
  • CM工作室发展史 下
  • Python装饰器:优雅增强函数行为的艺术
  • AI+预测3D新模型百十个定位预测+胆码预测+去和尾2025年6月14日第108弹
  • Win10安装DockerDesktop踩坑记
  • Java学习_‘+’作连接符的情况
  • Go语言底层(五): 深入浅出Go语言的ants协程池
  • ASR语音转写技术全景解析:从原理到实战
  • shell三剑客
  • FileBrowser Quantum更丝滑的文件网盘共享FileBrowser的平替
  • Python命名空间与作用域:深入解析名称查找的艺术
  • halcon开发之我与阿莲的故事1
  • Web自动化测试详细流程和步骤
  • Vue框架详解与Element
  • Python Day51 学习(日志Day20复习)
  • Atcoder Beginner Contest 410 题解报告
  • 来自麻省理工和AI制药公司 Recursion 的结构与结合亲和力预测模型Boltz-2,解决小分子药物发现的关键问题
  • 高频计网面试题(附模板答案)
  • 电子计数跳绳加长改造
  • 多线程5(Thread)
  • wpa_supplicant:无线网络连接的“智能管家”
  • 龟兔赛跑算法(Floyd‘s Cycle-Finding Algorithm)寻找重复数