惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
量子位
A
Arctic Wolf
L
Lohrmann on Cybersecurity
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
WordPress大学
WordPress大学
V
Vulnerabilities – Threatpost
博客园 - Franky
C
Cyber Attacks, Cyber Crime and Cyber Security
The Cloudflare Blog
Last Week in AI
Last Week in AI
The Hacker News
The Hacker News
I
Intezer
J
Java Code Geeks
P
Privacy International News Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Secure Thoughts
Cisco Talos Blog
Cisco Talos Blog
阮一峰的网络日志
阮一峰的网络日志
S
Securelist
Security Latest
Security Latest
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
小众软件
小众软件
Jina AI
Jina AI
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
博客园_首页
酷 壳 – CoolShell
酷 壳 – CoolShell
T
The Exploit Database - CXSecurity.com
雷峰网
雷峰网
T
Tenable Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Privacy & Cybersecurity Law Blog
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 【当耐特】
T
Threat Research - Cisco Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
MongoDB | Blog
MongoDB | Blog
D
DataBreaches.Net
N
News | PayPal Newsroom
Google Online Security Blog
Google Online Security Blog
K
Kaspersky official blog
H
Help Net Security
宝玉的分享
宝玉的分享
罗磊的独立博客
Webroot Blog
Webroot Blog
月光博客
月光博客
B
Blog RSS Feed
Recorded Future
Recorded Future

追梦人物的博客

0x05:Merkle Tree & Patricia Trie 0x04:ECDSA - 以太坊设计与实现 LeetCode 105 从前序与中序遍历构造二叉树 迭代算法原理 + 完整证明 0x03:Address - 以太坊设计与实现 0x02:Secp256k1 - 以太坊设计与实现 0x00:专栏开篇 - 以太坊设计与实现 0x01:RLP 编码 - 以太坊设计与实现 Bellman-Ford 算法原理及其在 DeFi 套利中的应用 - 追梦人物的博客 迪杰斯特拉(Dijkstra)最短路径算法原理、实现与证明 - 追梦人物的博客 实战:CEX-DEX 稳定币套利监控程序开发 - 追梦人物的博客 CEX-DEX 稳定币套利模型 - 追梦人物的博客 Uniswap 手续费和协议费机制剖析 - 追梦人物的博客 Uniswap 流动性机制及相关数学原理分析 - 追梦人物的博客 uv 替代 pyenv + pipx + poetry 环境管理实践 Rust 项目从创建到发布 - 追梦人物的博客 VS Code 调试 Python - 追梦人物的博客 比特币跨市场套利的数学模型 - 追梦人物的博客 Django 老项目如何从 SQLite 迁到 PostgreSQL 自动生成接口文档 - HelloDjango - django REST framework 教程 单元测试 - HelloDjango - django REST framework 教程 限制接口访问频率 - HelloDjango - django REST framework 教程 API 版本管理 - HelloDjango - django REST framework 教程 如何在 Windows 下搭建高效的 django 开发环境 拓展Python Markdown - 追梦人物的博客 加缓存为接口提速 - HelloDjango - django REST framework 教程 基于 drf-haystack 实现文章搜索接口 - HelloDjango 评论接口 - HelloDjango - django REST framework 教程 实现分类、标签、归档日期接口 - HelloDjango - django REST framework 教程 在接口返回Markdown解析后的内容 - HelloDjango - django REST framework 教程 文章详情接口 - HelloDjango - django REST framework 教程 分页 - HelloDjango - django REST framework 教程 使用视图集简化代码 - HelloDjango - django REST framework 教程 用类视图实现首页 API - HelloDjango - django REST framework 教程 实现博客首页文章列表 API - HelloDjango - django REST framework 教程 初始化 RESTful API 风格的博客系统 - HelloDjango django-rest-framework 是什么鬼? - HelloDjango - django REST framework 教程 结束 or 开始? - HelloDjango Coverage.py 统计测试覆盖率 - HelloDjango - Django博客教程(第二版) 单元测试:测试评论应用 - HelloDjango - Django博客教程(第二版) 单元测试:测试 blog 应用 - HelloDjango Django Haystack 全文检索与关键词高亮 - HelloDjango Django 博客实现简单的全文搜索 - HelloDjango - Django博客教程(第二版) 开启 Django 博客的 RSS 功能 - HelloDjango 统计各个分类和标签下的文章数 - HelloDjango - Django博客教程(第二版) 稳定易用的 Django 分页库,完善分页功能 - HelloDjango 通过 Django Pagination 实现简单分页 - HelloDjango 在脚本中使用 ORM:Faker 批量生成测试数据 - HelloDjango Django 官方推荐的姿势:类视图 - HelloDjango - Django博客教程(第二版) Django 使用 union 合并不同模型(Model) 的查询集(QuerySet) 开发博客文章阅读量统计功能 - HelloDjango - Django博客教程(第二版) 使用 Docker 让部署 Django 项目更加轻松 - HelloDjango 使用 Certbot 向 Let's Encrypt 免费申请 HTTPS 证书 - HelloDjango 使用 Fabric 自动化部署 - HelloDjango 博客代码开源啦 - 追梦人物的博客 (赠书)推荐一本django书籍:Django企业开发实战 - 追梦人物的博客 Nginx+Gunicorn+Supervisor 部署 Django 博客应用 - HelloDjango 优化博客功能细节,提升使用体验 - HelloDjango - Django博客教程(第二版) 交流的桥梁:评论功能 - HelloDjango - Django博客教程(第二版) 分类、归档和标签页 - HelloDjango - Django博客教程(第二版) 页面侧边栏:使用自定义模板标签 - HelloDjango - Django博客教程(第二版) 自动生成文章摘要 - HelloDjango - Django博客教程(第二版) Markdown 文章自动生成目录,提升阅读体验 - HelloDjango - Django博客教程(第二版) 让博客支持 Markdown 语法和代码高亮 - HelloDjango 开发博客文章详情页 - HelloDjango - Django博客教程(第二版) 创作后台开启,请开始你的表演 - HelloDjango - Django博客教程(第二版) 博客从“裸奔”到“有皮肤” - HelloDjango - Django博客教程(第二版) Django 的接客之道 - HelloDjango - Django博客教程(第二版) Django 迁移、操作数据库 - HelloDjango - Django博客教程(第二版) 创建 Django 博客的数据库模型 - HelloDjango 开始进入 django 开发之旅 - HelloDjango "空空如也"的博客应用 - HelloDjango - Django博客教程(第二版) 一种自顶而下的Python装饰器设计方法 - 追梦人物的博客 Python提取支付宝和微信支付二维码 - 追梦人物的博客 2018 - 我的学生生涯最后一年回顾 - 追梦人物的博客 批量清除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 筛选练习参考答案 - Vue 2.x Todo 教程练习参考答案 删除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 编辑todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 添加todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 标为完成练习参考答案 - Vue 2.x Todo 教程练习参考答案 入门仪式_Hello_Vue练习参考答案 - Vue 2.x Todo 教程练习参考答案 组件化todo应用 - Vue 2.x Todo 教程 批量清除todo - Vue 2.x Todo 教程 本地存储 - Vue 2.x Todo 教程 筛选 - Vue 2.x Todo 教程 还剩多少todo未完成 - Vue 2.x Todo 教程 自定义指令实现自动聚焦 - Vue 2.x Todo 教程 全部标为完成 - Vue 2.x Todo 教程 删除todo - Vue 2.x Todo 教程 编辑todo - Vue 2.x Todo 教程 添加todo - Vue 2.x Todo 教程 标为完成 - Vue 2.x Todo 教程 UI - Vue 2.x Todo 教程 显示todo列表 - Vue 2.x Todo 教程 入门仪式:Hello Vue - Vue 2.x Todo 教程 在学习django-rest-framework时收集的学习资料推荐 - 追梦人物的博客 区块链理论与应用研究小组成员招募书 - 追梦人物的博客 Python界网红,豆瓣工程师董伟明加了我的QQ后 - 追梦人物的博客 招募Django学习小组项目组核心成员 - 追梦人物的博客
ERC-20 相关知识点总结 - 追梦人物的博客
2024-01-31 · via 追梦人物的博客

ERC-20 代币标准 由 V 神等人于 2015 年提出后,很快就被广泛采纳。此后为了不断完善以太坊生态的代币标准,又有很多基于 ERC-20 的兼容或不兼容的拓展提案被提出,有些方案被广泛接受,有些则仍在讨论当中。

尽管 ERC-20 本身是一个简单的代币标准,但随着多年的发展,其所涉及的知识点众多。因此这篇文章将对 ERC-20 相关的知识点做一个梳理和总结,以加深对 ERC-20 的认识和理解。

ERC-20 接口定义

以下是对 ERC-20 接口的定义,编程语言为 solidity。

// SPDX-License-Identifier: MIT
// WTF Solidity by 0xAA

pragma solidity ^0.8.20;

/**
 * @dev ERC20 接口合约.
 */
interface IERC20 {
    /**
     * @dev 释放条件:当 `value` 单位的货币从账户 (`from`) 转账到另一账户 (`to`)时.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev 释放条件:当 `value` 单位的货币从账户 (`owner`) 授权给另一账户 (`spender`)时.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev 返回代币总供给.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev 返回账户`account`所持有的代币数.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev 转账 `amount` 单位代币,从调用者账户到另一账户 `to`.
     *
     * 如果成功,返回 `true`.
     *
     * 释放 {Transfer} 事件.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev 返回`owner`账户授权给`spender`账户的额度,默认为0。
     *
     * 当{approve} 或 {transferFrom} 被调用时,`allowance`会改变.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev 调用者账户给`spender`账户授权 `amount`数量代币。
     *
     * 如果成功,返回 `true`.
     *
     * 释放 {Approval} 事件.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev 通过授权机制,从`from`账户向`to`账户转账`amount`数量代币。转账的部分会从调用者的`allowance`中扣除。
     *
     * 如果成功,返回 `true`.
     *
     * 释放 {Transfer} 事件.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
}

虽然下面这几个 function 在标准中没有强制规定,但绝大部分 ERC-20 的智能合约都会实现这些 function。

    /**
     * @dev 返回代币名称.
     */
    function name() external view returns (string memory);

    /**
     * @dev 返回代币名称的缩写.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev 返回代币使用的小数位数.
     */
    function decimals() external view returns (uint8);

ERC-20 简单实现

实现上,通常用一个 mapping 记录账户和余额(account address -> balance)。

再用一个 mappingmapping 记录 owner 账户给 spender 账户的授权额度(owner account address -> spender account address -> allowance)。

以下是一个 ERC-20 标准的简单实现,编程语言为 solidity。

// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

contract MyToken{
    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 public totalSupply;

    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    constructor(string memory name_, string memory symbol_, uint8 decimals_, uint256 totalSupply_) {
        name = name_;
        symbol = symbol_;
        decimals = decimals_;
        totalSupply = totalSupply_;
        _balances[msg.sender] = totalSupply_; // 将全部代币转入合约创建者的账户
    }

    function balanceOf(address account) external view returns (uint256) {
        return _balances[account];
    }

    function allowance(address owner, address spender) external view returns (uint256) {
        return _allowances[owner][spender];
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        _allowances[msg.sender][spender] = amount;

        emit Approval(msg.sender, spender, amount);

        return true;
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        _balances[msg.sender] -= amount;
        _balances[to] += amount;

        emit Transfer(msg.sender, to, amount);

        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        _allowances[from][msg.sender] -= amount;
        _balances[from] -= amount;
        _balances[to] += amount;

        emit Transfer(from, to, amount);

        return true;
    }
}

ERC-20 存在的问题

ERC-20 因为接口简单清晰而被广泛采用,但也存在设计不合理的地方,导致在安全性、用户体验等方面存在一些瑕疵。为了消除这些瑕疵,一些拓展提案被提了出来。这一节将对 ERC-20 存在的问题和解决方案做一个总结。

approve / transferFrom 存在安全瑕疵

approvetransferFrom 是一对孪生方法,可以说这是 ERC-20 标准的精髓所在。

通过 approve 方法,owner 账户可以授权 spender 账户一定的额度,授权后允许 spender 操作 owner 账户指定额度的代币。

通过 transferFrom 方法,spender 可操作 owner 账户指定额度的代币。

很多以太坊应用基于这个功能玩出了花。例如 UniSwap,当给 UniSwap 的池子添加流动性时,首先需要通过 approve 授权 UniSwap 智能合约操作用户的代币,授权后,智能合约就可以通过调用 transferFrom 将池子中的两种代币从用户账户划入合约。

看上去似乎没有问题,但当用户已经授权了某个 spender 一定额度后,又尝试修改其授权额度时,就可能出现被恶意攻击的地方。具体场景如下:

  1. Alice 授权 Bob 操作某个代币,额度为 N。
  2. Alice 想将授权额度修改为 M,于是发起了一笔交易,此交易正在等待矿工确认。
  3. Bob 发现了这个交易,于是他立即发起一个交易,将 N 额度的代币从 Alice 的账户转走,由于 Bob 给的 gas 费比较高,这个交易先于 Alice 的交易被确认(即所谓抢跑交易)。
  4. 随后 Alice 的交易被确认,将授权 Bob 的额度修改为 M。
  5. Bob 趁 Alice 不注意,再次发起一笔交易,将 M 额度的代币从 Alice 的账户转走。

上述场景中,Alice 本意只允许 Bob 操作 M 额度的代币,但实际 Bob 坑走了 Alice N+M 的代币。

要防止出现这种问题,一种方案是将修改额度的操作分为 2 步。第一步发起一笔交易将授权额度改为 0,然后确认 spender 没有恶意花费代币后,再发起一笔交易将授权额度改为新的值。但缺点是需要付出双倍 gas 费,且操作很麻烦。

另一种是 OpenZeppelin 的方案,其实现的 ERC-20 合约中新增了increaseAllowncedecreaseAllowance 方法。如果要增加授权额度,调用 increaseAllownce 方法;减少授权额度则调用 decreaseAllowance 方法。这两个方法都接收 2 个参数,一个是被授权账户的地址,另外一个是需要增加或减少的授权额度。

function increaseAllowance(address spender, uint256 addedValue) public returns (bool);

function decreaseAllowance(address spender, uint256 requestedDecrease) public returns (bool)

increaseAllownce 不存在上述抢跑交易的问题,因为无论 spender 有没有通过抢跑交易花费代币,授权额度增加的总是预期的增量。

decreaseAllowance 仍然存在抢跑风险,如果 spender 抢在 decreaseAllowance 的交易被确认前用掉一定或者全部的额度,使得剩余的额度低于需减少的额度,将导致额度扣减失败。

ERC20 API: An Attack Vector on the Approve/TransferFrom Methods 中还提到一种方案可完全解决这个问题,但需要修改 approve 方法的参数:

function approve(
  address _spender,
  uint256 _currentValue,
  uint256 _value)
returns (bool success)

授权者在发起新的 approve 交易修改授权额度为 _value 时,需要将当前授权额度 _currentValue 传递给 approve 方法。只有 approve 方法在执行时发现查询到的授权额度和授权者传递的 _currentValue 相等时,说明在这个过程中被授权者没有通过抢跑交易使用授权额度,才执行更新操作。

这个方案的缺点是需要修改 approve 方法的参数,会导致不兼容改动,因此并未被采纳。

虽然存在这样一个安全上的小瑕疵,但通常来说,用户只会给一些知名的智能合约授权,这些合约中不一定存在可以执行以上攻击的代码逻辑,所以总体来说不算一个大的安全漏洞。

approve / transferFrom 用户体验不佳

假如要让智能合约操作用户账户中的代币(例如向 UniSwap 池子中添加流动性或者移除流动性),用户需要执行 2 步操作:

  1. 发起一笔交易,调用 approve 授权智能合约操作用户账户中的代币。
  2. 授权完成后,再发起一笔交易调用智能合约的某个方法,这个方法会调用 transferFrom 来划转用户账户中已授权的代币。

2 笔交易就要支付 2 次 gas 费,且操作麻烦。

为了优化上述问题,ERC-2612 提案在 ERC-20 标准的基础上新增了一个 permit 方法,该方法也可以用来执行授权操作。通过这个方法执行授权的方式就像签署一张银行支票一样,用户签署一张支票,拥有此支票的人(包括用户自己)就可以执行授权操作。

有了 permit 方法,智能合约要操作用户账户中的代币,就可以将之前的 2 个交易合并为 1 个交易。

  1. 用户线下签署授权消息(不涉及链上交易)。
  2. 再发起一笔交易调用智能合约的某个方法,将签署的授权消息作为参数传递,合约调用 permittransferFrom 同时完成授权和代币划转的操作。

可能有人会疑惑,既然这样,只需要智能合约的实现中,同时调用 approvetransferFrom 就行了,为什么还要 permit 方法呢?

我们可以仔细看一下 approve 接口的定义:

function approve(address spender, uint256 amount) external returns (bool);

这里 approve 的参数只有被授权账户 spender 和授权额度 amount,那么授权人是谁呢?答案是授权交易的发起人。因此,为了完成授权操作,只能由授权人亲自发起交易才行。

通过 permit,则可以将授权人和授权的交易分离。授权人只需要负责签署授权消息,后续他可以亲自发起交易,也可以将签署的消息交给第三方,由第三方来代为发起交易,节省了 gas 的同时提高了灵活性。而且为后续解决“账户中需要 ETH 才能操作代币”的问题奠定了基础。

账户中需要 ETH 才能操作代币

ERC-20 代币还有一个体验不佳的地方,为了操作代币,账户中必须要有 ETH 用来支付 gas 费用。例如钱包账户中有 USDT,但没有 ETH,那么用户就无法转账 USDT,必须向钱包转入一定的 ETH 后,才能发起转账交易。

为了解决这个问题,人们提出了 gasless transaction,或者叫 meta transaction 的概念。其基本思想是,用户线下签署某个授权转账的消息,将其发给第三方,第三方收到这个消息后,代替用户支付 gas 费,发起交易执行转账操作。同时消息中也会授权第三方扣除用户账户中一定的代币数量,以补偿第三方支付的 gas 费用。这样在用户端看来,他只需要支付代币,而不需要支付 ETH 即可完成转账交易。

其中这个第三方可以是中心化的服务商,也可以是去中心化的服务商。例如 OpenGSN 就是提供此类服务去中心化解决方案的项目。OpenZeppelin Defender 也提供了此类服务中心化的解决方案。

总结

ERC-20 代币标准 被提出后得到了广泛的采纳,但该标准也存在一些安全瑕疵和用户体验不佳的地方。为了消除这些瑕疵和优化用户体验,一些拓展方案被提了出来,其中一些已经得到了广泛的支持和应用。虽然 ERC-20 接口简单,但以其为基础建立的应用生态非常繁荣,涉及的知识点也非常的多,需要花精力了解和学习。

参考文章

  1. ERC-20: Token Standard
  2. ERC20 API: An Attack Vector on the Approve/TransferFrom Methods
  3. ERC-2612: Permit Extension for EIP-20 Signed Approvals

-- EOF --