Yul 是一种提供与以太坊虚拟机(EVM)直接交互的中间语言。
使用 Yul 编写 ERC20 代币合约,实现一个高度Gas 优化,同时遵循 ERC20 标准的合约
这篇指南的需求源于优化智能合约以提高性能和安全性,同时遵循 ERC20 标准。Yul 通过实现对合约代码的更低级别控制,从而实现更高效和安全的智能合约部署。
我们将通过详细介绍以下过程来解决这些挑战:
- 在 Yul 中构建 ERC20 函数以实现有效的代币管理
- 实施针对漏洞的安全措施
- 优化 gas 使用以最小化交易成本
在结束时,读者将全面了解 Yul 中的 ERC20 合约开发。在开始之前,请确保你已经阅读了关于 Yul 的全面指南。
ERC20 和 Yul 简介
什么是 ERC20?
想象一下,你想创建自己的一种数字货币,可以在以太坊网络上流畅地进行交易、共享,甚至在在线游戏和应用程序中使用。ERC20 本质上是一组规则,可以帮助你以一种在以太坊网络上流畅运行的方式创建这种数字货币。它就像一份食谱,确保你的数字货币可以轻松地被交换和他人使用。
它涵盖了以下内容:
- 创建和跟踪代币: 它告诉你如何创建新的代币/硬币,可以创建多少代币,并跟踪谁拥有多少代币。
- 发送代币: 它向你展示如何安全地将代币从一个人转移到另一个人。
- 使用带有权限的代币: 它允许代币所有者让其他人花费其代币的一定数量,对于自动化服务或交易非常有用。
什么是 Yul?
Yul 就像是一种用于直接与 EVM 交流的秘密代码语言。当人们创建智能合约时,他们通常会用一种称为 Solidity 的语言来编写,这种语言更容易理解和使用。但有时,开发人员需要非常具体地告诉以太坊如何做事情,特别是如果他们想要在交易费用上节省 gas 或执行一些非常定制的操作。
这就是 Yul 的用武之地。把 Yul 想象成更接近机器语言,允许开发人员给出更精确和直接的指令。
Yul 让开发人员可以:
- 控制细节: 他们可以管理合约工作的细节,这在 Solidity 中很难做到
- 节省 Gas: 通过更直接的方式,可以使他们的合约使用更少的 gas
- 执行高级技巧: 对于非常专业的任务,Yul 允许开发人员编写更灵活和强大的代码。
现在我们对 ERC20 和 Yul 有了基本的了解,让我们开始使用 Yul 创建我们的智能合约。
设置你的开发环境
准备在 Yul 中编写 ERC20 合约非常简单。按照以下步骤进行设置:
- 打开你的网络浏览器,转到 Remix IDE 网站。Remix IDE 是一个在线工具,用于编写、测试和部署以太坊合约。
- 进入 Remix 后,你将开始在默认工作区。该区域允许你使用类似
contracts
(用于合约文件)、scripts
(用于部署脚本)和tests
(用于测试文件)的文件夹组织你的工作。 - 转到
contracts
文件夹,并创建一个名为ERC20Yul.sol
的新文件。该文件将包含你的 ERC20 代币的 Yul 代码。 - 创建了你的
ERC20Yul.sol
文件后,你就可以开始使用 Yul 编写智能合约了。
在 Yul 中编写 ERC20 合约
首先,我们将为我们的智能合约奠定基础,并设置我们将使用的所有变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract ERC20Yul { }
设置变量和常量
bytes32 internal constant _TRANSFER_HASH =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
bytes32 internal constant _APPROVAL_HASH =
0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925;
bytes32 internal constant _INSUFFICIENT_BALANCE_SELECTOR =
0xf4d678b800000000000000000000000000000000000000000000000000000000;
bytes32 internal constant _INSUFFICIENT_ALLOWANCE_SELECTOR =
0x13be252b00000000000000000000000000000000000000000000000000000000;
bytes32 internal constant _RECIPIENT_ZERO_SELECTOR =
0x4c131ee600000000000000000000000000000000000000000000000000000000;
bytes32 internal constant _INVALID_SIG_SELECTOR =
0x8baa579f00000000000000000000000000000000000000000000000000000000;
bytes32 internal constant _EXPIRED_SELECTOR =
0x203d82d800000000000000000000000000000000000000000000000000000000;
bytes32 internal constant _STRING_TOO_LONG_SELECTOR =
0xb11b2ad800000000000000000000000000000000000000000000000000000000;
bytes32 internal constant _OVERFLOW_SELECTOR =
0x35278d1200000000000000000000000000000000000000000000000000000000;
bytes32 internal constant _EIP712_DOMAIN_PREFIX_HASH =
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
bytes32 internal constant _PERMIT_HASH =
0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
bytes32 internal constant _VERSION_1_HASH =
0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6;
bytes32 internal constant _MAX =
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
bytes32 internal immutable _name;
bytes32 internal immutable _symbol;
uint256 internal immutable _nameLen;
uint256 internal immutable _symbolLen;
uint256 internal immutable _initialChainId;
bytes32 internal immutable _initialDomainSeparator;
mapping(address => uint256) internal _balances;
mapping(address => mapping(address => uint256)) internal _allowances;
uint256 internal _supply;
mapping(address => uint256) internal _nonces;
event Transfer(address indexed src, address indexed dst, uint256 amount);
event Approval(address indexed src, address indexed dst, uint256 amount);
_TRANSFER_HASH
、_APPROVAL_HASH
和类似的常量:
这些常量是特定字符串的预计算哈希,通常是事件签名或函数选择器。它们在内联汇编块中使用,以通过避免运行时计算这些哈希来优化 gas 使用。
_name
和_symbol
:
这些不可变变量以固定大小的 bytes32
格式存储代币的名称和符号。它们在合约部署期间设置,并且旨在存储和访问这些属性,而无需动态字符串存储。
_nameLen
和_symbolLen
:
这些不可变变量捕获了代币名称和符号的长度。这是必要的,因为名称和符号存储为 bytes32
,并且在需要时正确将它们转换回字符串。
-
_initialChainId
和_initialDomainSeparator
: -
_initialChainId
存储合约部署时的链 ID,用于 EIP-2612 的域分离符,以防止在不同链上的重放攻击。 -
_initialDomainSeparator
是基于初始链 ID 预先计算的 EIP-712 域分隔符,同样在 EIP-2612 的上下文中使用。 -
_balances
和_allowances
: -
_balances
是一个映射,跟踪每个地址的代币余额,这是任何 ERC20 代币的基本部分 -
_allowances
是一个映射的映射,跟踪一个地址被允许代表另一个地址花费多少代币,对于approve
和transferFrom
函数至关重要。 -
_supply
此变量跟踪代币的总供应量,在铸造或销毁代币时进行更新。
_nonces
:
用于 EIP-2612 permit 功能,此映射跟踪每个地址的 nonce,以确保每个许可调用都是唯一的,并防止重放攻击。
- 事件声明(Transfer 和 Approval):
声明这些事件是为了通知外部订阅者代币的转移和授权,这对于 ERC20 代币的可用性至关重要。
实现构造函数
constructor(string memory name_, string memory symbol_) {
// get string lengths
bytes memory nameB = bytes(name_);
bytes memory symbolB = bytes(symbol_);
uint256 nameLen = nameB.length;
uint256 symbolLen = symbolB.length;
// check strings are <=32 bytes
assembly {
if or(lt(0x20, nameLen), lt(0x20, symbolLen)) {
mstore(0x00, _STRING_TOO_LONG_SELECTOR)
revert(0x00, 0x04)
}
}
// compute domain separator
bytes32 initialDomainSeparator = _computeDomainSeparator(
keccak256(nameB)
);
// set immutables
_name = bytes32(nameB);
_symbol = bytes32(symbolB);
_nameLen = nameLen;
_symbolLen = symbolLen;
_initialChainId = block.chainid;
_initialDomainSeparator = initialDomainSeparator;
}
将 name_ 和 symbol_ 参数从字符串转换为字节,以获取它们的长度。
验证名称和符号是否都在 32 字节的限制内。这个限制是由于将这些参数存储在 bytes32
变量中,通过避免动态存储来优化 gas 成本。如果任一参数超过此限制,合约将使用自定义错误回滚。
- 使用
name_
参数的哈希调用_computeDomainSeparator
。此函数计算 EIP-712 域分隔符,对于安全实现 EIP-2612 的permit功能至关重要。域分隔符有助于确保为许可功能而签名的消息是特定于此合约和链的,以防止重放攻击。 - 初始化
_name
、_symbol
、_nameLen
、_symbolLen
、_initialChainId
和_initialDomainSeparator
变量。
name
和 symbol
以 bytes32
格式直接从输入参数存储,确保高效的存储和访问。存储名称和符号的长度以便在需要时进行字符串转换。
在部署时存储链 ID 到 _initialChainId
,以支持域分隔符的特定链。
使用预先计算的值设置 _initialDomainSeparator
,以供 permit
函数使用。
Transfer
函数
function transfer(address dst, uint256 amount)
public
virtual
returns (bool success)
{
assembly {
// Check if the destination address is not zero.
if iszero(dst) {
mstore(0x00, _RECIPIENT_ZERO_SELECTOR)
revert(0x00, 0x04)
}
// Load the sender's balance, check for sufficient balance, and update it.
mstore(0x00, caller