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

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库来确保数值运算的安全性。通过标准代币接口和安全数学运算,开发者可以构建更可靠、更安全的去中心化应用。

相关文章:

  • go.work
  • (笔记)1.web3学习-区块链技术
  • web3方法详解
  • Oracle 逻辑结构与性能优化(上)
  • 【学习笔记】深入理解Java虚拟机学习笔记——第7章 虚拟机类加载机制
  • 基于 pysnmp 的实际业务场景应用案例:网络设备监控与配置系统
  • 【Linux手册】进程的状态:从创建到消亡的“生命百态”
  • 六月十五号Leetcode
  • React 实现砸金蛋游戏
  • Spring AI 项目实战(八):Spring Boot + AI + DeepSeek 打造企业级智能文档分类系统
  • Requests源码分析01:运行tests
  • 20年架构师视角:SpringAI如何重塑Java技术栈?
  • Windows下Docker一键部署Dify教程
  • ONLYOFFICE 协作空间 企业版使用秘籍-5.企业电子文件如何管理?便于查找、访问和协作,轻松提升效率
  • 【Python机器学习(一)】NumPy/Pandas手搓决策树+使用Graphviz可视化(以西瓜书数据集为例)
  • Java EE与Jakarta EE命名空间区别
  • OpenHarmony 5.0读取文件并写入到另一份文件(公共文件夹),并保持原先的格式以及编码类型
  • 案例:塔能科技智启某市“光网计划”——重构城市照明的数字底座与生态价值
  • AudioLab安卓版:音频处理,一应俱全
  • (LeetCode 动态规划(基础版)) 518. 零钱兑换 II (动态规划dp)
  • 公路建设新闻网站/引流推广效果好的app
  • 兰州新区疫情最新情况/太原自动seo
  • 中山网站建设华联在线/搜索引擎优化论文
  • 做网站就/app拉新推广赚佣金
  • 常州网站建设公司方案/互联网营销师考试内容
  • 淮南公司做网站/上海单个关键词优化