如何在不花费气体费用的情况下批准代币转移,使用EIP-2612

  • QuickNode
  • 发布于 2024-12-20 20:57
  • 阅读 31

本文详细介绍了如何使用EIP-2612改进ERC-20代币的批准过程,实现更简化的用户体验和安全性。文章包括了EIP-2612的原理,环境搭建步骤,合约创建,部署及与合约交互的完整代码示例,适合有基础的开发者学习。

概述

ERC-20 代币是以太坊区块链上流行的代币类型,但其使用中的一个问题是批准代币转移时需要多个交易。这就是 EIP-2612 的意义所在。EIP-2612 提出了一个标准化的方法,让用户能够一次性授予他人支出其代币的权限,从而简化批准代币转移的过程。在本指南中,我们将演示如何使用 EIP-2612 进行 ERC-20 授权批准。

你将需要的资源

  • 智能合约 和编程概念的基本理解
  • 可以访问以太坊端点(你可以免费获得一个 这里!
  • 安装 Ethers.js(版本 5.7)

你将要做的事情

  • 讨论 EIP-2612 是什么及其好处
  • 使用 Hardhat 设置开发环境
  • 部署具有 Permit 功能的 ERC-20 合约
  • 在 ERC-20 合约上调用 permit 函数以无 gas 批准代币支出

什么是 EIP-2612

EIP-2612 是一种以太坊改进提案,提出了一种新的 ERC-20 代币授权批准标准。它引入了“permit”函数的概念,允许用户在单个交易中授予他人支出其代币的权限。

EIP-2612 的好处

EIP-2612 的主要好处包括:

  • 简化用户体验:用户只需批准一次代币转移,而不是为每次转移批准。
  • 安全性提高:授权批准比传统批准更安全,因为它们包含到期时间和唯一的 nonce。
  • 减少 gas 成本:授权批准所需的交易更少,从而降低用户的 gas 成本。

设置你的开发环境

设置 Web3 钱包并获取测试网络代币

你可以使用任何类型的非托管钱包(如 PhantomMetaMaskTorus)进行本教程,只需确保你有两个私钥。

为了快速开发,我们将在本教程中使用 Torus 钱包。Torus 是一个支持多个链和网络的非托管钱包,包括以太坊、Polygon、Arbitrum、Avalanche 等。要开始,请访问 Torus 并按照说明生成私钥。

Torus 钱包

一旦你的两个钱包都设置完成,你需要在 Sepolia 上获取一些测试网络代币。你可以通过使用 QuickNode 多链水龙头 来轻松完成此操作。

只需连接你的钱包,或粘贴你的钱包地址并请求代币。你还可以分享推文以获得奖励!

QuickNode 多链水龙头

在继续之前,确保你已创建两个帐户(即两个私钥),并用一些测试 ETH 在 Sepolia 上为这两个帐户提供资金。

创建 QuickNode 端点

你需要一个 API 端点与以太坊网络通信。你可以使用公共节点,也可以自行部署和管理基础设施;然而,如果你希望实现 8 倍的响应速度,可以让我们来处理繁重的工作。免费注册一个帐户 这里

登录后,点击 创建端点,然后选择以太坊 Sepolia 测试网络链。

QuickNode 端点

创建项目并安装依赖项

打开你的终端并使用以下命令创建一个名为 erc20permit 的项目文件夹:

mkdir erc20permit

使用以下命令进入该文件夹,然后初始化一个默认的 NPM 项目:

cd erc20permit && npm init -y

要安装 Hardhat,请在你的项目目录中运行以下命令:

npm install --save-dev hardhat

然后安装其他所需依赖项,例如 Ethers.js(确保版本为 5.7):

npm install --save ethers@5.7 dotenv @nomicfoundation/hardhat-toolbox @openzeppelin/contracts

最后,让我们使用以下命令初始化一个 Hardhat 模板项目:

npx hardhat

当系统提示你选择要创建的项目时,选择最后一个选项,“创建一个空的 hardhat.config.js”。

你的项目结构现在应如下所示:

项目目录

项目目录设置正确后,我们可以更新 hardhat.config.js 文件,包含以下代码:

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  networks: {
    sepolia: {
      url: process.env.RPC_URL,
      accounts: [process.env.PRIVATE_KEY_DEPLOYER]
    }
  },
  solidity: {
    version: "0.8.9",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  }
}

记得在继续之前保存文件。现在,让我们回顾一下这段代码。

hardhat.config.js 文件是配置文件,让我们可以设置 Solidity 版本、优化器设置、部署设置等。

在本教程中,我们将使用 Sepolia 网络,所以我们将设置一个 sepolia 对象,它将包含我们的 RPC 提供程序 URL(例如,QuickNode 端点)和将部署合约与调用 permit 函数的帐户私钥。我们还将定义 Solidity 版本并启用优化器,以便我们的代码得到优化,并为部署和交互节省一些 gas。

此外,出于本教程的目的,我们将使用一个 .env 文件,以免将任何私密凭据上传到 GitHub。因此,现在我们用以下命令创建 .env 文件:

echo > .env

然后,打开文件并更新以包括以下环境变量:

RPC_URL=
PRIVATE_KEY_DEPLOYER=
PRIVATE_KEY_ACCOUNT_2=

花一点时间填写上面的变量(即,你在上一部分中获得的 QuickNode HTTP 提供程序 URL 和私钥),并记得保存文件!

创建具有 ERC20Permit 的 ERC-20 智能合约

我们部署的 ERC-20 智能合约将使用 OpenZeppelin 标准,可以在 这里 找到。

在你的 erc20permit 的根目录中,创建一个名为 contracts 的文件夹,并创建一个名为 MyToken.sol 的文件:

mkdir contracts && cd contracts && echo > MyToken.sol

然后,打开文件并包括以下代码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";

contract MyToken is ERC20, Ownable, ERC20Permit {
    constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {
        _mint(msg.sender, 1000 * 10 ** decimals());
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

记得保存文件!

让我们回顾一下合约。

  • 文件顶部指定了 Solidity 版本(即,0.8.9 或更高)
  • 合约中使用的导入来自 OpenZeppelin 库,包括 ERC20.sol、Ownable.sol 和 draft-ERC20Permit.sol。
  • 合约声明为“ MyToken”,符号为“ MTK”。
  • 合约继承了三个其他合约:ERC20、Ownable 和 ERC20Permit。因此,我们的合约将是一个由单个所有者(由 Ownable 指定)拥有的 ERC20 代币,并将使用 ERC20Permit 添加授权功能。
  • 合约允许所有者铸造新代币并将其添加到代币供应中。
  • 合约在部署期间向合约的发送者铸造 1000 个代币。
  • 铸造新代币的函数仅限于合约的所有者。

创建部署和交互脚本

现在我们已经创建了具有授权功能的 ERC20 代币,我们现在可以创建部署和交互脚本。为此,我们需要创建一个 scripts 文件夹,并包含 deploy.jspermit.js 文件。

返回你的 erc20permit 的根目录,运行以下终端命令:

mkdir scripts && cd scripts && echo > deploy.js && echo > permit.js

现在,让我们填充代码。打开 deploy.js 文件并包括以下代码:

const hre = require("hardhat");

async function deploy() {
    // 部署合约
    const MyToken = await hre.ethers.getContractFactory("MyToken");
    const myToken = await MyToken.deploy();

    // 记录部署的合约地址
    console.log("ERC20 Permit 合约部署在:", myToken.address);
}

deploy()
    .then(() => console.log("部署完成"))
    .catch((error) => console.error("部署合约时出错:", error));

上述部署脚本仅仅是部署 MyToken 合约,并输出地址。花几分钟时间阅读代码注释,以便更好地理解代码。

在下一部分中,我们将部署合约。

合约编译与部署

是时候将智能合约编译并部署到 Sepolia 测试网了。因此现在让我们返回到 erc20permit 根目录。

保存了所有文件后,运行以下命令以编译合约:

npx hardhat compile

你应该看到类似如下的消息:

(node:85523) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
成功编译 13 个 Solidity 文件

然后,运行命令部署合约:

npx hardhat run --network sepolia scripts/deploy.js

你将看到类似如下的输出:

ERC20 Permit 合约部署在: 0x0906781EA63813BCCF8FBBd8f11EE2170F5bB5Fb
部署完成

你可以在 Etherscan 粘贴合约地址,查看更多详情。在下一部分中,我们将与已部署的合约交互并调用 Permit 函数,以允许无 gas 批准。

调用 Permit 函数

现在是你期待已久的时刻。在这一部分,我们将向你展示如何允许用户无 gas 批准代币支出。

以下是这个过程的步骤:

  1. 获取 ERC-20 代币的合约地址
  2. 使用 Ethers.js 创建 ERC-20 合约的实例
  3. 使用 tokenOwner 私钥和相关字段对授权数据进行签名
  4. 从代币接收者调用 ERC-20 合约上的 permit 函数,传入必要的参数
  5. 在无需支付批准交易费用的情况下转移代币

话虽如此,让我们创建所需的文件并开始填写代码。

在你的 scripts 文件夹中,打开 permit.js 文件并包含以下代码:

const { ethers } = require("hardhat");
const { abi } = require("../artifacts/contracts/MyToken.sol/MyToken.json")
require('dotenv').config()

  function getTimestampInSeconds() {
    // 返回当前的时间戳(以秒为单位)
    return Math.floor(Date.now() / 1000);
  }

  async function main() {

    // 获取提供程序实例
    const provider = new ethers.providers.StaticJsonRpcProvider(process.env.RPC_URL)

    // 获取网络链 ID
    const chainId = (await provider.getNetwork()).chainId;

    // 创建一个与代币所有者的签名实例
    const tokenOwner = await new ethers.Wallet(process.env.PRIVATE_KEY_DEPLOYER, provider)

    // 创建一个与代币接收者的签名实例
    const tokenReceiver = await new ethers.Wallet(process.env.PRIVATE_KEY_ACCOUNT_2, provider)

    // 获取 MyToken 合约工厂并部署合约的新实例
    const myToken = new ethers.Contract("YOUR_DEPLOYED_CONTRACT_ADDRESS", abi, provider)

    // 检查账户余额
    let tokenOwnerBalance = (await myToken.balanceOf(tokenOwner.address)).toString()
    let tokenReceiverBalance = (await myToken.balanceOf(tokenReceiver.address)).toString()

    console.log(`代币所有者初始余额: ${tokenOwnerBalance}`);
    console.log(`代币接收者初始余额: ${tokenReceiverBalance}`);

    // 设置代币数量和截止时间
    const value = ethers.utils.parseEther("1");
    const deadline = getTimestampInSeconds() + 4200;

    // 获取部署者地址的当前 nonce
    const nonces = await myToken.nonces(tokenOwner.address);

    // 设置域参数
    const domain = {
      name: await myToken.name(),
      version: "1",
      chainId: chainId,
      verifyingContract: myToken.address
    };

    // 设置 Permit 类型参数
    const types = {
      Permit: [{
          name: "owner",
          type: "address"
        },
        {
          name: "spender",
          type: "address"
        },
        {
          name: "value",
          type: "uint256"
        },
        {
          name: "nonce",
          type: "uint256"
        },
        {
          name: "deadline",
          type: "uint256"
        },
      ],
    };

    // 设置 Permit 类型值
    const values = {
      owner: tokenOwner.address,
      spender: tokenReceiver.address,
      value: value,
      nonce: nonces,
      deadline: deadline,
    };

    // 使用部署者的私钥签署 Permit 类型数据
    const signature = await tokenOwner._signTypedData(domain, types, values);

    // 将签名分解为其组件
    const sig = ethers.utils.splitSignature(signature);

    // 使用签名验证 Permit 类型数据
    const recovered = ethers.utils.verifyTypedData(
      domain,
      types,
      values,
      sig
    );

    // 获取网络 gas 价格
    gasPrice = await provider.getGasPrice()

    // 允许 tokenReceiver 地址花费代币所有者的代币
    let tx = await myToken.connect(tokenReceiver).permit(
      tokenOwner.address,
      tokenReceiver.address,
      value,
      deadline,
      sig.v,
      sig.r,
      sig.s, {
        gasPrice: gasPrice,
        gasLimit: 80000 // 硬编码的 gas 限制;如有需要可更改
      }
    );

    await tx.wait(2) // 等待 tx 确认后 2 个区块

    // 检查 tokenReceiver 地址是否现在可以代表 tokenOwner 花费代币
    console.log(`检查 tokenReceiver 的授权: ${await myToken.allowance(tokenOwner.address, tokenReceiver.address)}`);

    // 从 tokenOwner 转移代币到 tokenReceiver 地址
    tx = await myToken.connect(tokenReceiver).transferFrom(
      tokenOwner.address,
      tokenReceiver.address,
      value, {
        gasPrice: gasPrice,
        gasLimit: 80000 // 硬编码的 gas 限制;如有需要可更改
      }
    );

    await tx.wait(2) // 等待 tx 确认后 2 个区块

    // 获取结束余额
    tokenOwnerBalance = (await myToken.balanceOf(tokenOwner.address)).toString()
    tokenReceiverBalance = (await myToken.balanceOf(tokenReceiver.address)).toString()

    console.log(`结束时代币所有者余额: ${tokenOwnerBalance}`);
    console.log(`结束时代币接收者余额: ${tokenReceiverBalance}`);
  }

  main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
  });

YOUR_DEPLOYED_CONTRACT_ADDRESS 替换为你实际部署的合约地址,并记得保存文件!

花一点时间审阅代码注释,以更好地熟悉代码,当你准备好时,返回到 erc20permit 根文件夹并运行以下命令执行 permit 脚本:

npx hardhat run --network sepolia scripts/permit.js

你将看到如下输出:

代币所有者初始余额: 1000000000000000000000
代币接收者初始余额: 0
检查 tokenReceiver 的授权: 1000000000000000000
结束时代币所有者余额: 999000000000000000000
结束时代币接收者余额: 1000000000000000000

注意到 tokenOwner(即合约的部署者)有余额,而 tokenReceiver 的初始余额为零。在调用 permit 函数后(来自 tokenReceiver),tokenReceiver 的授权为 1,000,000,000,000,000,000(相当于 1 ETH)。然后,我们调用 transferFrom 函数,从 tokenReceiver 账户代表 tokenOwner 转移代币。最后,通过调用 balanceOf 函数检查余额并看到代币已被转移。我们可以通过在 Etherscan 双重检查事务来确认这一点。

最终想法

就这样!你已了解如何实现 ERC-20 授权批准。查看其他 以太坊开发智能合约开发 指南。

我们很想看看你正在构建的东西!通过 DiscordTwitter 与我们分享你的应用程序。

我们 ❤️ 反馈!

如果你对本指南有任何反馈,请 告诉我们!

  • 原文链接: quicknode.com/guides/eth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。