指南:使用 Yul 编写 ERC20 合约

探讨如何使用 Yul 编写 ERC20 代币合约,实现一个高度Gas 优化,同时遵循 ERC20 标准的合约

在本文中,我们将探讨如何使用 Yul 编写 ERC20 代币合约,Yul 是一种提供与以太坊虚拟机(EVM)直接交互的中间语言。

img

这篇指南的需求源于优化智能合约以提高性能和安全性,同时遵循 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 是一个映射的映射,跟踪一个地址被允许代表另一个地址花费多少代币,对于 approvetransferFrom 函数至关重要。

  • _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 变量。

namesymbolbytes32 格式直接从输入参数存储,确保高效的存储和访问。存储名称和符号的长度以便在需要时进行字符串转换。

在部署时存储链 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())
        mstore(0x20, 0x00)
        let srcSlot := keccak256(0x00, 0x40)
        let srcBalance := sload(srcSlot)
        if lt(srcBalance, amount) {
            mstore(0x00, _INSUFFICIENT_BALANCE_SELECTOR)
            revert(0x00, 0x04)
        }
        sstore(srcSlot, sub(srcBalance, amount))
        // Update the destination's balance.
        mstore(0x00, dst)
        let dstSlot := keccak256(0x00, 0x40)
        sstore(dstSlot, add(sload(dstSlot), amount))
        // Log the Transfer event.
        mstore(0x00, amount)
        log3(0x00, 0x20, _TRANSFER_HASH, caller(), dst)
        // Indicate successful execution.
        success := 0x01
    }
}
  • 该函数首先检查目标地址 dst 是否不是零地址。将代币转移到零地址通常被视为销毁操作,在此实现中不允许,以防止意外丢失代币。
  • 它使用内联汇编从 _balances 映射中检索发送者 msg.sender 的余额,以提高效率和直接访问存储。如果发送者没有足够的代币(要转移的金额大于发送者的余额),则交易将以“余额不足”错误回滚。发送者的余额减少,接收者的余额增加了 amount
  • 触发一个带有发送者地址、接收者地址和转移金额的 Transfer 事件。
  • 该函数返回 truesuccess := 0x01)以指示成功执行。此返回模式遵循 ERC20 标准,为调用方返回布尔。

TransferFrom 函数

function transferFrom(
    address src,
    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)
        }
// Calculate allowance mapping storage slot and load the allowance value.
        mstore(0x00, src)
        mstore(0x20, 0x01)
        mstore(0x20, keccak256(0x00, 0x40))
        mstore(0x00, caller())
        let allowanceSlot := keccak256(0x00, 0x40)
        let allowanceVal := sload(allowanceSlot)
        // Check if allowance is sufficient and not infinite (_MAX).
        if lt(allowanceVal, _MAX) {
            if lt(allowanceVal, amount) {
                mstore(0x00, _INSUFFICIENT_ALLOWANCE_SELECTOR)
                revert(0x00, 0x04)
            }
            // Update the allowance if it's not set to _MAX.
            sstore(allowanceSlot, sub(allowanceVal, amount))
        }
        // Load the sender's balance, check for sufficient balance, and update it.
        mstore(0x00, src)
        mstore(0x20, 0x00)
        let srcSlot := keccak256(0x00, 0x40)
        let srcBalance := sload(srcSlot)
        if lt(srcBalance, amount) {
            mstore(0x00, _INSUFFICIENT_BALANCE_SELECTOR)
            revert(0x00, 0x04)
        }
        // Deduct the amount from the sender's balance.
        sstore(srcSlot, sub(srcBalance, amount))
        // Update the destination's balance.
        mstore(0x00, dst)
        let dstSlot := keccak256(0x00, 0x40)
        sstore(dstSlot, add(sload(dstSlot), amount))
        // Log the Transfer event.
        mstore(0x00, amount)
        log3(0x00, 0x20, _TRANSFER_HASH, src, dst)
        // Indicate successful execution.
        success := 0x01
    }
}
  • transfer 类似,它检查目标地址 dst 不为零,以防止意外烧毁代币。
  • 获取代币所有者(src)为调用者设置的授权额度,并确保其足够以满足请求的 amount。如果授权不足,函数将以“授权不足”错误回滚。如果授权不是特殊情况 _MAX,则从授权中扣除转移的 amount
  • 检查代币所有者 src 是否有足够的余额来覆盖正在转移的 amount。如果没有,则以“余额不足”错误回滚。从代币所有者的余额中扣除 amount 并将其添加到接收者 dst 的余额中。
  • 触发带有源地址、目标地址和转移金额的 Transfer 事件,
  • 成功执行后返回 truesuccess := 0x0),遵循 ERC20 标准的成功执行的约定。

Approve 函数

function approve(address dst, uint256 amount)
    public
    virtual
    returns (bool success)
{
    assembly {
        // _allowances[msg.sender][dst] = amount;
        mstore(0x00, caller())
        mstore(0x20, 0x01)
        mstore(0x20, keccak256(0x00, 0x40))
        mstore(0x00, dst)
        sstore(keccak256(0x00, 0x40), amount)

                // emit Approval(msg.sender, dst, amount);
        mstore(0x00, amount)
        log3(0x00, 0x20, _APPROVAL_HASH, caller(), dst)
        // return true;
        success := 0x01
    }
}
  • 该函数将 dst(支出者)的授权额度设置为 amount,允许 dst 代表 msg.sender(所有者)最多花费 amount 个代币。此操作使用内联汇编直接操作存储,用于计算基于代币所有者和支出者地址的授权存储槽。
  • 它触发一个带有所有者地址(msg.sender)、支出者地址 dst 和授权额度的 Approval 事件。
  • 该函数返回 true 以指示成功执行。返回布尔值在 ERC20 approve 函数中是标准行为,允许调用合约和交易验证授权是否成功。

Permit 函数

function permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) public virtual {
    assembly {
        // Check if the deadline has already passed.
        if gt(timestamp(), deadline) {
            mstore(0x00, _EXPIRED_SELECTOR)
            revert(0x00, 0x04)
        }
// Compute the domain separator and the digest for signing.
        let separator := DOMAIN_SEPARATOR()
        let nonce := sload(add(_nonces.slot, owner))
        let digest := keccak256(abi.encodePacked("\\x19\\x01", separator, keccak256(abi.encode(_PERMIT_HASH, owner, spender, value, nonce, deadline))))
        // Recover the signer from the signature.
        let recovered := ecrecover(digest, v, r, s)
        // Check if the recovered address is not zero and matches the owner.
        if or(iszero(recovered), iszero(eq(recovered, owner))) {
            mstore(0x00, _INVALID_SIG_SELECTOR)
            revert(0x00, 0x04)
        }
        // Increment the nonce for the owner to prevent replay attacks.
        sstore(add(_nonces.slot, owner), add(nonce, 1))
        // Approve the spender to spend the specified value.
        mstore(0x00, owner)
        mstore(0x20, spender)
        let allowanceSlot := keccak256(0x00, 0x40)
        sstore(allowanceSlot, value)
        // Emit an Approval event.
        log3(0x00, 0x20, _APPROVAL_HASH, owner, spender, value)
    }
}
  • 检查当前时间戳是否在指定的 deadline 之前。这确保了许可(permit)在过期后不会被使用。
  • 使用域分隔符、许可参数(ownerspendervalue)、所有者的当前 nonce 和截止日期计算 EIP-712 摘要。
  • 使用 ecrecover 从签名组件(vrs)和摘要中恢复地址。此恢复的地址必须与指定的 owner 地址匹配,以确保签名有效且确实来自所有者。
  • 增加所有者的 nonce,以确保每个许可只能使用一次。
  • spender 的授权额度设置为代表 owner 花费 value 个代币。这是 permit 的核心功能,相当于调用 approve 函数,但无需来自 owner 的交易。
  • 触发指示许可的新授权额度的 Approval 事件。

Allowance 函数

function allowance(address src, address dst)
    public
    view
    virtual
    returns (uint256 amount)
{
    assembly {
        // Calculate the storage slot for the allowance mapping using src and dst.
        mstore(0x00, src)
        mstore(0x20, 0x01) // The slot for _allowances mapping.
        mstore(0x20, keccak256(0x00, 0x40))
        mstore(0x00, dst)
        let allowanceSlot := keccak256(0x00, 0x40)
// Load the allowance amount from the calculated storage slot.
        amount := sload(allowanceSlot)
    }
}

该函数计算存储授权信息的确切存储槽。ERC20 授权信息保存在嵌套映射 mapping(owner => mapping(spender => uint256) 中,因此计算涉及 2 个步骤:

  • 首先,它找到与 _allowances 映射中的 owner 对应的存储槽。
  • 然后,它使用 keccak256 哈希函数在该所有者映射中计算 spender 的存储槽。这是 Solidity 中嵌套映射存储槽计算的标准方法。

确定正确的存储槽后,函数使用 sload 操作直接从 EVM 存储中读取授权额度。

返回授权额度,指示 spender 被允许代表 owner 转移多少代币。

balanceOf 函数

function balanceOf(address src)
    public
    view
    virtual
    returns (uint256 amount)
{
    assembly {
        // Calculate the storage slot for the balance mapping using src.
        mstore(0x00, src)
        mstore(0x20, 0x00) // The slot for _balances mapping.
        let balanceSlot := keccak256(0x00, 0x40)
// Load the balance amount from the calculated storage slot.
        amount := sload(balanceSlot)
    }
}

确定给定地址(src)的余额信息存储的确切存储槽。在 Solidity 中,每个账户的余额都保存在一个映射中(mapping(address => uint256) _balances),并且使用哈希函数计算此映射中每个地址的存储槽。

该函数首先将地址(src)存储在内存中,然后使用 keccak256 哈希函数,将地址与 _balances 映射的存储槽号结合起来,计算确切的存储槽。

确定存储槽后,函数使用 sload 指令直接从以太坊虚拟机(EVM)存储中读取代币余额。

返回查询的余额(amount),指示 src 地址持有多少代币。

nonces 函数

function nonces(address src) public view virtual returns (uint256 nonce) {
    assembly {
        // Calculate the storage slot for the nonce mapping using src.
        mstore(0x00, src)
        mstore(0x20, 0x03) // The slot for _nonces mapping.
        let nonceSlot := keccak256(0x00, 0x40)
// Load the nonce value from the calculated storage slot.
        nonce := sload(nonceSlot)
    }
}

该函数计算给定地址(src)的 nonce 存储的存储槽。Nonce 保存在一个映射中(mapping(address => uint256) _nonces),其中每个地址都有一个相关联的 nonce,随着每个成功的许可操作而递增。

它使用 keccak256 哈希函数计算 src 地址的确切存储槽的位置。此计算涉及将地址与 _nonces 映射的预定存储槽号结合起来。

使用 sload 指令直接从 EVM 存储中检索 nonce 值。此步骤有效地获取指定地址的当前 nonce,表示此地址发出的许可数量。

返回 nonce 值,这对于构建下一个有效的许可至关重要。

totalSupply 函数

function totalSupply() public view virtual returns (uint256 amount) {
    assembly {
        // Load the total supply value from its storage slot.
        amount := sload(0x02)
    }
}

该函数直接从以太坊虚拟机(EVM)存储中访问总代币供应量的值。总供应量存储在预定的存储槽(0x02)中,遵循合约的存储布局。

返回代币的总供应量(amount)。

name 函数

function name() public view virtual returns (string memory value) {
    bytes32 myName = _name;
    uint256 myNameLen = _nameLen;
    assembly {
        // Allocate memory for the return string.
        value := mload(0x40)
        // Set the length of the string.
        mstore(value, myNameLen)
        // Store the name bytes directly after the length prefix.
        mstore(add(value, 0x20), myName)
        // Update the free memory pointer to avoid overwriting this string.
        mstore(0x40, add(value, 0x40))
    }
}

该函数首先为返回值分配内存。它使用 mload(0x40) 找到当前的“空闲内存指针”,该指针指示可以安全存储新数据的位置。

将代币名称的长度(myNameLen)存储在分配的内存的开头。

代币名称的实际字节(myName)直接存储在长度前缀之后。由于名称存储为 bytes32 类型,因此可以在单个操作中高效地复制到内存中。

最后,通过将其前一个值加上 64 字节来更新“空闲内存指针”:32 字节用于长度前缀,32 字节用于实际名称数据。

symbol 函数

function symbol() public view virtual returns (string memory value) {
    bytes32 mySymbol = _symbol;
    uint256 mySymbolLen = _symbolLen;
    assembly {
        // Allocate memory for the return string.
        value := mload(0x40)
        // Set the length of the string.
        mstore(value, mySymbolLen)
        // Store the symbol bytes directly after the length prefix.
        mstore(add(value, 0x20), mySymbol)
        // Update the free memory pointer to avoid overwriting this string.
        mstore(0x40, add(value, 0x40))
    }
}

name 函数类似,它首先通过找到当前的空闲内存指针来确定在哪里安全分配内存以存储返回字符串。

它将代币符号的长度存储在分配的内存空间的开头。

代币符号的字节存储在长度前缀之后的内存中。通过相应地更新空闲内存指针,确保下一个分配的数据不会覆盖符号字符串。指针向前移动了分配字符串的大小加上其长度前缀。

DOMAIN_SEPARATOR 函数

// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
    return
        block.chainid == _initialChainId
            ? _initialDomainSeparator
            : _computeDomainSeparator(keccak256(abi.encode(_name)));
}

该函数首先检查当前的 chainid 是否与部署合约时存储的 _initialChainId 匹配。这很重要,因为域分隔符包括链 ID,以确保签名不能在不同网络上重放。

如果链 ID 自部署以来没有更改,则函数返回 _initialDomainSeparator。此值在合约初始化期间计算并存储,以节省Gas,避免在每次调用中重新计算。

如果链 ID 发生变化(这可能发生在硬分叉之后或者合约部署在不同的网络上),该函数会使用_computeDomainSeparator计算一个新的域分隔符。该方法涉及将多个数据片段进行哈希运算,包括合约的名称(经过哈希处理)、当前的链 ID 以及 EIP-712 定义的其他常量,以形成一个用于签名上下文的唯一标识符。

decimals 函数

function decimals() public pure virtual returns (uint8) {
    return 18;
}

该函数简单地返回一个常量值,对于许多 ERC20 代币来说,惯例上将其设置为 18,这与以太坊本地货币以太的小数位数相一致。

_mint 函数

function _mint(address dst, uint256 amount) internal virtual {
    assembly {
        // Check if the destination address is not zero to prevent burning tokens.
        if iszero(dst) {
            mstore(0x00, _RECIPIENT_ZERO_SELECTOR)
            revert(0x00, 0x04)
        }
// Load the current total supply and add the amount to mint.
        let supply := sload(_supply.slot)
        let newSupply := add(supply, amount)
        // Check for overflow to ensure safe addition.
        if lt(newSupply, supply) {
            mstore(0x00, _OVERFLOW_SELECTOR)
            revert(0x00, 0x04)
        }
        // Update the total supply with the new value.
        sstore(_supply.slot, newSupply)
        // Update the balance of the destination address.
        mstore(0x00, dst)
        mstore(0x20, _balances.slot)
        let dstSlot := keccak256(0x00, 0x40)
        let dstBalance := sload(dstSlot)
        let newDstBalance := add(dstBalance, amount)
        // Check for overflow to ensure safe addition to the destination balance.
        if lt(newDstBalance, dstBalance) {
            mstore(0x00, _OVERFLOW_SELECTOR)
            revert(0x00, 0x04)
        }
        // Store the updated balance for the destination address.
        sstore(dstSlot, newDstBalance)
        // Emit a Transfer event from the zero address to indicate tokens were minted.
        mstore(0x00, amount)
        log3(0x00, 0x20, _TRANSFER_HASH, 0x00, dst, amount)
    }
}
  • 确保目标地址(dst)不是零地址,以防止在铸币过程中错误地销毁代币。
  • 检索代币的当前总供应量,将铸币数量添加到其中,并检查溢出情况,以确保操作的安全性。然后将新的总供应量存储回合约状态中。
  • 使用目标地址计算接收者余额的存储槽,并使用铸造数量更新其余额,再次检查溢出情况,以确保添加操作的安全性。
  • 触发一个带有零地址作为发送者的Transfer事件,表示代币已经被铸造,遵循 ERC20 标准中有关铸币事件的惯例。

_burn 函数

function _burn(address src, uint256 amount) internal virtual {
    assembly {
        // Check the balance of the source address to ensure it has enough tokens to burn.
        mstore(0x00, src)
        mstore(0x20, _balances.slot)
        let srcSlot := keccak256(0x00, 0x40)
        let srcBalance := sload(srcSlot)
if lt(srcBalance, amount) {
            mstore(0x00, _INSUFFICIENT_BALANCE_SELECTOR)
            revert(0x00, 0x04)
        }
        // Deduct the amount from the source address's balance.
        sstore(srcSlot, sub(srcBalance, amount))
        // Reduce the total supply by the amount burned.
        let supply := sload(_supply.slot)
        sstore(_supply.slot, sub(supply, amount))
        // Emit a Transfer event with the destination address as the zero address to indicate burning.
        mstore(0x00, amount)
        log3(0x00, 0x20, _TRANSFER_HASH, src, 0x00, amount)
    }
}

验证源地址(src)是否有足够的余额来销毁指定数量的代币。如果余额不足,操作将以“余额不足”错误回滚。

src地址的余额中扣除amount,确保代币被正确地从流通中移除。

减少相同amount的代币总供应量,反映了整体代币供应的减少。

触发一个带有目标地址设置为零地址的Transfer事件,表示代币已被销毁。

_computeDomainSeparator 函数

function _computeDomainSeparator(bytes32 nameHash)
    internal
    view
    virtual
    returns (bytes32 domainSeparator)
{
    assembly {
        let memptr := mload(0x40) // Load the free memory pointer.
        mstore(memptr, _EIP712_DOMAIN_PREFIX_HASH) // EIP-712 domain prefix hash.
        mstore(add(memptr, 0x20), nameHash) // Token name hash.
        mstore(add(memptr, 0x40), _VERSION_1_HASH) // Version hash ("1").
        mstore(add(memptr, 0x60), chainid()) // Current chain ID.
        mstore(add(memptr, 0x80), address()) // Contract address.
// Compute the EIP-712 domain separator.
        domainSeparator := keccak256(memptr, 0xA0)
    }
}

该函数首先分配内存以构造域分隔符的组件。它使用空闲内存指针来确保不覆盖现有数据。

顺序存储 EIP-712 域前缀哈希、代币名称的哈希、版本哈希(通常为“1”以表示版本)、当前的链 ID(以确保签名是特定于链的,并避免跨链的重放攻击)以及合约的地址。

在所有组件都存储在内存中后,该函数通过使用keccak256对连接的数据进行哈希运算来计算域分隔符。这个哈希操作是在内存中准备的整个数据块上执行的,从而为合约的域生成了一个唯一标识符。

智能合约的最终代码将类似于以下内容:

// SPDX-License-Identifier: MIT
// solhint-disable-next-line compiler-version
pragma solidity ^0.8.4;
/// @notice ERC20 (including EIP-2612 Permit) using max inline assembly.
contract ERC20 {
    // keccak256("Transfer(address,address,uint256)")
    bytes32 internal constant _TRANSFER_HASH =
        0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
    // keccak256("Approval(address,address,uint256)")
    bytes32 internal constant _APPROVAL_HASH =
        0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925;
    // first 4 bytes of keccak256("InsufficientBalance()") right padded with 0s
    bytes32 internal constant _INSUFFICIENT_BALANCE_SELECTOR =
        0xf4d678b800000000000000000000000000000000000000000000000000000000;
    // first 4 bytes of keccak256("InsufficientAllowance()") right padded with 0s
    bytes32 internal constant _INSUFFICIENT_ALLOWANCE_SELECTOR =
        0x13be252b00000000000000000000000000000000000000000000000000000000;
    // first 4 bytes of keccak256("InvalidRecipientZero()") right padded with 0s
    bytes32 internal constant _RECIPIENT_ZERO_SELECTOR =
        0x4c131ee600000000000000000000000000000000000000000000000000000000;
    // first 4 bytes of keccak256("InvalidSignature()") right padded with 0s
    bytes32 internal constant _INVALID_SIG_SELECTOR =
        0x8baa579f00000000000000000000000000000000000000000000000000000000;
    // first 4 bytes of keccak256("Expired()") right padded with 0s
    bytes32 internal constant _EXPIRED_SELECTOR =
        0x203d82d800000000000000000000000000000000000000000000000000000000;
    // first 4 bytes of keccak256("StringTooLong()") right padded with 0s
    bytes32 internal constant _STRING_TOO_LONG_SELECTOR =
        0xb11b2ad800000000000000000000000000000000000000000000000000000000;
    // first 4 bytes of keccak256("Overflow()") right padded with 0s
    bytes32 internal constant _OVERFLOW_SELECTOR =
        0x35278d1200000000000000000000000000000000000000000000000000000000;
    // solhint-disable-next-line max-line-length
    // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
    bytes32 internal constant _EIP712_DOMAIN_PREFIX_HASH =
        0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
    // solhint-disable-next-line max-line-length
    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
    bytes32 internal constant _PERMIT_HASH =
        0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
    // keccak256("1")
    bytes32 internal constant _VERSION_1_HASH =
        0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6;
    // max 256-bit integer, i.e. 2**256-1
    bytes32 internal constant _MAX =
        0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
    // token name, stored in an immutable bytes32 (constructor arg must be <=32 byte string)
    bytes32 internal immutable _name;
    // token symbol, stored in an immutable bytes32 (constructor arg must be <=32 byte string)
    bytes32 internal immutable _symbol;
    // token name string length
    uint256 internal immutable _nameLen;
    // token symbol string length
    uint256 internal immutable _symbolLen;
    // initial block.chainid, only changes in a future hardfork scenario
    uint256 internal immutable _initialChainId;
    // initial domain separator, only changes in a future hardfork scenario
    bytes32 internal immutable _initialDomainSeparator;
    // token balances mapping, storage slot 0x00
    mapping(address => uint256) internal _balances;
    // token allowances mapping (owner=>spender=>amount), storage slot 0x01
    mapping(address => mapping(address => uint256)) internal _allowances;
    // token total supply, storage slot 0x02
    uint256 internal _supply;
    // permit nonces, storage slot 0x03
    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);
    constructor(string memory name_, string memory symbol_) {
        /// @dev constructor in solidity bc cannot handle immutables with inline assembly
        /// also, constructor gas optimization not really important (one time cost)
        // 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;
    }
    function transfer(address dst, uint256 amount)
        public
        virtual
        returns (bool success)
    {
        assembly {
            // require(dst != address(0), "Address Zero");
            if iszero(dst) {
                mstore(0x00, _RECIPIENT_ZERO_SELECTOR)
                revert(0x00, 0x04)
            }
            // _balances[msg.sender] -= amount;
            mstore(0x00, caller())
            mstore(0x20, 0x00)
            let srcSlot := keccak256(0x00, 0x40)
            let srcBalance := sload(srcSlot)
            if lt(srcBalance, amount) {
                mstore(0x00, _INSUFFICIENT_BALANCE_SELECTOR)
                revert(0x00, 0x04)
            }
            sstore(srcSlot, sub(srcBalance, amount))
            // unchecked { _balances[dst] += amount; }
            mstore(0x00, dst)
            let dstSlot := keccak256(0x00, 0x40)
            sstore(dstSlot, add(sload(dstSlot), amount))
            // emit Transfer(msg.sender, dst, amount);
            mstore(0x00, amount)
            log3(0x00, 0x20, _TRANSFER_HASH, caller(), dst)
            // return true;
            success := 0x01
        }
    }
    // solhint-disable-next-line function-max-lines
    function transferFrom(
        address src,
        address dst,
        uint256 amount
    ) public virtual returns (bool success) {
        assembly {
            // require(dst != address(0), "Address Zero");
            if iszero(dst) {
                mstore(0x00, _RECIPIENT_ZERO_SELECTOR)
                revert(0x00, 0x04)
            }
            // uint256 allowanceVal = _allowances[msg.sender][dst];
            mstore(0x00, src)
            mstore(0x20, 0x01)
            mstore(0x20, keccak256(0x00, 0x40))
            mstore(0x00, caller())
            let allowanceSlot := keccak256(0x00, 0x40)
            let allowanceVal := sload(allowanceSlot)
            // if (allowanceVal != _MAX) _allowances[msg.sender][dst] = allowanceVal - amount;
            if lt(allowanceVal, _MAX) {
                if lt(allowanceVal, amount) {
                    mstore(0x00, _INSUFFICIENT_ALLOWANCE_SELECTOR)
                    revert(0x00, 0x04)
                }
                sstore(allowanceSlot, sub(allowanceVal, amount))
                /// @dev NOTE not logging Approval event here, OZ impl does
            }
            // _balances[src] -= amount;
            mstore(0x00, src)
            mstore(0x20, 0x00)
            let srcSlot := keccak256(0x00, 0x40)
            let srcBalance := sload(srcSlot)
            if lt(srcBalance, amount) {
                mstore(0x00, _INSUFFICIENT_BALANCE_SELECTOR)
                revert(0x00, 0x04)
            }
            sstore(srcSlot, sub(srcBalance, amount))
            // unchecked { _balances[dst] += amount; }
            mstore(0x00, dst)
            let dstSlot := keccak256(0x00, 0x40)
            sstore(dstSlot, add(sload(dstSlot), amount))
            // emit Transfer(src, dst, amount);
            mstore(0x00, amount)
            log3(0x00, 0x20, _TRANSFER_HASH, src, dst)
            // return true;
            success := 0x01
        }
    }
    function approve(address dst, uint256 amount)
        public
        virtual
        returns (bool success)
    {
        assembly {
            // _allowances[msg.sender][dst] = amount;
            mstore(0x00, caller())
            mstore(0x20, 0x01)
            mstore(0x20, keccak256(0x00, 0x40))
            mstore(0x00, dst)
            sstore(keccak256(0x00, 0x40), amount)
            // emit Approval(msg.sender, dst, amount);
            mstore(0x00, amount)
            log3(0x00, 0x20, _APPROVAL_HASH, caller(), dst)
            // return true;
            success := 0x01
        }
    }
    // solhint-disable-next-line function-max-lines
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
        assembly {
            // require(deadline >= block.timestamp, "Expired");
            if gt(timestamp(), deadline) {
                mstore(0x00, _EXPIRED_SELECTOR)
                revert(0x00, 0x04)
            }
        }
        bytes32 separator = DOMAIN_SEPARATOR();
        assembly {
            // uint256 nonce = _nonces[owner];
            mstore(0x00, owner)
            mstore(0x20, 0x03)
            let nonceSlot := keccak256(0x00, 0x40)
            let nonce := sload(nonceSlot)
            // bytes32 innerHash =
            //     keccak256(abi.encode(_PERMIT_HASH, owner, spender, value, nonce, deadline))
            let memptr := mload(0x40)
            mstore(memptr, _PERMIT_HASH)
            mstore(add(memptr, 0x20), owner)
            mstore(add(memptr, 0x40), spender)
            mstore(add(memptr, 0x60), value)
            mstore(add(memptr, 0x80), nonce)
            mstore(add(memptr, 0xA0), deadline)
            mstore(add(memptr, 0x22), keccak256(memptr, 0xC0))
            // bytes32 hash = keccak256(abi.encodePacked("\\x19\\x01", separator, innerHash))
            mstore8(memptr, 0x19)
            mstore8(add(memptr, 0x01), 0x01)
            mstore(add(memptr, 0x02), separator)
            mstore(memptr, keccak256(memptr, 0x42))
            // address recovered = ecrecover(hash, v, r, s)
            mstore(add(memptr, 0x20), v)
            mstore(add(memptr, 0x40), r)
            mstore(add(memptr, 0x60), s)
            if iszero(staticcall(_MAX, 0x01, memptr, 0x80, memptr, 0x20)) {
                revert(0x00, 0x00)
            }
            returndatacopy(memptr, 0x00, returndatasize())
            let recovered := mload(memptr)
            // require(recovered != address(0) && recovered == owner, "Invalid Signature");
            if iszero(and(eq(recovered, owner), gt(recovered, 0x00))) {
                mstore(0x00, _INVALID_SIG_SELECTOR)
                revert(0x00, 0x04)
            }
            // unchecked { _nonces[owner] += 1 }
            sstore(nonceSlot, add(nonce, 0x01))
            // _allowances[recovered][spender] = value;
            mstore(0x00, recovered)
            mstore(0x20, 0x01)
            mstore(0x20, keccak256(0x00, 0x40))
            mstore(0x00, spender)
            sstore(keccak256(0x00, 0x40), value)
            // emit Approval
            mstore(0x00, value)
            log3(0x00, 0x20, _APPROVAL_HASH, recovered, spender)
        }
    }
    function allowance(address src, address dst)
        public
        view
        virtual
        returns (uint256 amount)
    {
        assembly {
            // return _allowances[src][dst];
            mstore(0x00, src)
            mstore(0x20, 0x01)
            mstore(0x20, keccak256(0x00, 0x40))
            mstore(0x00, dst)
            amount := sload(keccak256(0x00, 0x40))
        }
    }
    function balanceOf(address src)
        public
        view
        virtual
        returns (uint256 amount)
    {
        assembly {
            // return _balances[src];
            mstore(0x00, src)
            mstore(0x20, 0x00)
            amount := sload(keccak256(0x00, 0x40))
        }
    }
    function nonces(address src) public view virtual returns (uint256 nonce) {
        assembly {
            // return nonces[src];
            mstore(0x00, src)
            mstore(0x20, 0x03)
            nonce := sload(keccak256(0x00, 0x40))
        }
    }
    function totalSupply() public view virtual returns (uint256 amount) {
        assembly {
            // return _supply;
            amount := sload(0x02)
        }
    }
    function name() public view virtual returns (string memory value) {
        bytes32 myName = _name;
        uint256 myNameLen = _nameLen;
        assembly {
            // return string(bytes(_name));
            value := mload(0x40)
            mstore(0x40, add(value, 0x40))
            mstore(value, myNameLen)
            mstore(add(value, 0x20), myName)
        }
    }
    function symbol() public view virtual returns (string memory value) {
        bytes32 mySymbol = _symbol;
        uint256 mySymbolLen = _symbolLen;
        assembly {
            // return string(bytes(_symbol));
            value := mload(0x40)
            mstore(0x40, add(value, 0x40))
            mstore(value, mySymbolLen)
            mstore(add(value, 0x20), mySymbol)
        }
    }
    // solhint-disable-next-line func-name-mixedcase
    function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
        return
            block.chainid == _initialChainId
                ? _initialDomainSeparator
                : _computeDomainSeparator(keccak256(abi.encode(_name)));
    }
    function decimals() public pure virtual returns (uint8 amount) {
        assembly {
            // return 18;
            amount := 0x12
        }
    }
    function _mint(address dst, uint256 amount) internal virtual {
        assembly {
            // require(dst != address(0), "Address Zero");
            if iszero(dst) {
                mstore(0x00, _RECIPIENT_ZERO_SELECTOR)
                revert(0x00, 0x04)
            }
            // _supply += amount;
            let newSupply := add(amount, sload(0x02))
            if lt(newSupply, amount) {
                mstore(0x00, _OVERFLOW_SELECTOR)
                revert(0x00, 0x04)
            }
            sstore(0x02, newSupply)
            // unchecked { _balances[dst] += amount; }
            mstore(0x00, dst)
            mstore(0x20, 0x00)
            let dstSlot := keccak256(0x00, 0x40)
            sstore(dstSlot, add(sload(dstSlot), amount))
            // emit Transfer(address(0), dst, amount);
            mstore(0x00, amount)
            log3(0x00, 0x20, _TRANSFER_HASH, 0x00, dst)
        }
    }
    function _burn(address src, uint256 amount) internal virtual {
        assembly {
            // _balances[src] -= amount;
            mstore(0x00, src)
            mstore(0x20, 0x00)
            let srcSlot := keccak256(0x00, 0x40)
            let srcBalance := sload(srcSlot)
            if lt(srcBalance, amount) {
                mstore(0x00, _INSUFFICIENT_BALANCE_SELECTOR)
                revert(0x00, 0x04)
            }
            sstore(srcSlot, sub(srcBalance, amount))
            // unchecked { _supply -= amount; }
            sstore(0x02, sub(sload(0x02), amount))
            // emit Transfer(src, address(0), amount);
            mstore(0x00, amount)
            log3(0x00, 0x20, _TRANSFER_HASH, src, 0x00)
        }
    }
    function _computeDomainSeparator(bytes32 nameHash)
        internal
        view
        virtual
        returns (bytes32 domainSeparator)
    {
        assembly {
            let memptr := mload(0x40)
            mstore(memptr, _EIP712_DOMAIN_PREFIX_HASH)
            mstore(add(memptr, 0x20), nameHash)
            mstore(add(memptr, 0x40), _VERSION_1_HASH)
            mstore(add(memptr, 0x60), chainid())
            mstore(add(memptr, 0x80), address())
            domainSeparator := keccak256(memptr, 0x100)
        }
    }
}

部署合约

要部署合约,请转到 remix deploy & run transactions,输入我们代币的名称和符号,然后单击部署按钮。

你可以测试所有函数,并查看一切是否按预期工作。

如有任何反馈或查询,请随时通过 TwitterLinkedin联系我们。


本翻译由 DeCert.me 协助支持, 在 DeCert.me 构建可信履历,为自己码一个未来。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO