本文详细介绍了如何使用EIP-2612改进ERC-20代币的批准过程,实现更简化的用户体验和安全性。文章包括了EIP-2612的原理,环境搭建步骤,合约创建,部署及与合约交互的完整代码示例,适合有基础的开发者学习。
ERC-20 代币是以太坊区块链上流行的代币类型,但其使用中的一个问题是批准代币转移时需要多个交易。这就是 EIP-2612 的意义所在。EIP-2612 提出了一个标准化的方法,让用户能够一次性授予他人支出其代币的权限,从而简化批准代币转移的过程。在本指南中,我们将演示如何使用 EIP-2612 进行 ERC-20 授权批准。
你将需要的资源
你将要做的事情
EIP-2612 是一种以太坊改进提案,提出了一种新的 ERC-20 代币授权批准标准。它引入了“permit”函数的概念,允许用户在单个交易中授予他人支出其代币的权限。
EIP-2612 的主要好处包括:
你可以使用任何类型的非托管钱包(如 Phantom、MetaMask、Torus)进行本教程,只需确保你有两个私钥。
为了快速开发,我们将在本教程中使用 Torus 钱包。Torus 是一个支持多个链和网络的非托管钱包,包括以太坊、Polygon、Arbitrum、Avalanche 等。要开始,请访问 Torus 并按照说明生成私钥。
一旦你的两个钱包都设置完成,你需要在 Sepolia 上获取一些测试网络代币。你可以通过使用 QuickNode 多链水龙头 来轻松完成此操作。
只需连接你的钱包,或粘贴你的钱包地址并请求代币。你还可以分享推文以获得奖励!
在继续之前,确保你已创建两个帐户(即两个私钥),并用一些测试 ETH 在 Sepolia 上为这两个帐户提供资金。
你需要一个 API 端点与以太坊网络通信。你可以使用公共节点,也可以自行部署和管理基础设施;然而,如果你希望实现 8 倍的响应速度,可以让我们来处理繁重的工作。免费注册一个帐户 这里。
登录后,点击 创建端点,然后选择以太坊 Sepolia 测试网络链。
打开你的终端并使用以下命令创建一个名为 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 和私钥),并记得保存文件!
我们部署的 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);
}
}
记得保存文件!
让我们回顾一下合约。
现在我们已经创建了具有授权功能的 ERC20 代币,我们现在可以创建部署和交互脚本。为此,我们需要创建一个 scripts 文件夹,并包含 deploy.js 和 permit.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 批准。
现在是你期待已久的时刻。在这一部分,我们将向你展示如何允许用户无 gas 批准代币支出。
以下是这个过程的步骤:
话虽如此,让我们创建所需的文件并开始填写代码。
在你的 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 授权批准。查看其他 以太坊开发 和 智能合约开发 指南。
我们很想看看你正在构建的东西!通过 Discord 或 Twitter 与我们分享你的应用程序。
如果你对本指南有任何反馈,请 告诉我们!
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!