Yul 是一种提供与以太坊虚拟机(EVM)直接交互的中间语言。
使用 Yul 编写 ERC20 代币合约,实现一个高度Gas 优化,同时遵循 ERC20 标准的合约

这篇指南的需求源于优化智能合约以提高性能和安全性,同时遵循 ERC20 标准。Yul 通过实现对合约代码的更低级别控制,从而实现更高效和安全的智能合约部署。
我们将通过详细介绍以下过程来解决这些挑战:
在结束时,读者将全面了解 Yul 中的 ERC20 合约开发。在开始之前,请确保你已经阅读了关于 Yul 的全面指南。
想象一下,你想创建自己的一种数字货币,可以在以太坊网络上流畅地进行交易、共享,甚至在在线游戏和应用程序中使用。ERC20 本质上是一组规则,可以帮助你以一种在以太坊网络上流畅运行的方式创建这种数字货币。它就像一份食谱,确保你的数字货币可以轻松地被交换和他人使用。
它涵盖了以下内容:
Yul 就像是一种用于直接与 EVM 交流的秘密代码语言。当人们创建智能合约时,他们通常会用一种称为 Solidity 的语言来编写,这种语言更容易理解和使用。但有时,开发人员需要非常具体地告诉以太坊如何做事情,特别是如果他们想要在交易费用上节省 gas 或执行一些非常定制的操作。
这就是 Yul 的用武之地。把 Yul 想象成更接近机器语言,允许开发人员给出更精确和直接的指令。
Yul 让开发人员可以:
现在我们对 ERC20 和 Yul 有了基本的了解,让我们开始使用 Yul 创建我们的智能合约。
准备在 Yul 中编写 ERC20 合约非常简单。按照以下步骤进行设置:
contracts(用于合约文件)、scripts(用于部署脚本)和tests(用于测试文件)的文件夹组织你的工作。contracts文件夹,并创建一个名为ERC20Yul.sol的新文件。该文件将包含你的 ERC20 代币的 Yul 代码。ERC20Yul.sol文件后,你就可以开始使用 Yul 编写智能合约了。首先,我们将为我们的智能合约奠定基础,并设置我们将使用的所有变量。
// 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,以确保每个许可调用都是唯一的,并防止重放攻击。
声明这些事件是为了通知外部订阅者代币的转移和授权,这对于 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())