实现一个ERC20标准代币的增强版合约之ERC20Permit合约

  • 木西
  • 发布于 11小时前
  • 阅读 20

前言ERC20Permit是对扩展了ERC20标准的扩展,添加了一个permit函数,允许用户通过EIP-712签名修改授权,而不是通过msg.sender;ERC20Permit定义:ERC20Permit是对扩展了ERC20标准的扩展,添加了一个permit函数,

前言

ERC20Permit是对扩展了 ERC20 标准的扩展,添加了一个 permit 函数,允许用户通过 EIP-712 签名修改授权,而不是通过 msg.sender

ERC20Permit

定义:ERC20Permit是对扩展了 ERC20 标准的扩展,添加了一个 permit 函数,允许用户通过 EIP-712 签名修改授权

特点、优势

降低交易成本

  • 减少交易数量:在标准的ERC20代币中,用户需要先执行approve交易授权一定数量的代币给接收者,然后再由接收者执行transferFrom交易来转移代币,这需要两次上链交易。而使用ERC20Permit,可以将这两个步骤合并为一个交易,从而节省了gas费用。例如,标准ERC20流程中,approve交易,transferFrom交易;而使用ERC20Permit的transferWithPermit交易,包括permit和transferFrom操作在内。
  • 支持批量处理:接收者可以将多个permit和transferFrom操作批量在一个交易中执行,进一步降低了gas消耗,这对于需要频繁进行代币授权和转移的场景非常有利,能够有效减少用户的交易成本和区块链网络的负载。

    提升用户体验

  • 简化操作流程:用户无需再进行单独的approve交易来授权代币转移,只需生成一个签名(这一操作是在链下进行的,不产生gas费用),然后由接收者调用transferWithPermit函数即可完成代币的转移,简化了用户的操作步骤,使代币交互过程更加顺畅。
  • 避免重复授权:在一些应用场景中,用户可能需要频繁地授权不同数量的代币给不同的接收者。使用ERC20Permit,用户只需生成一次签名,就可以授权多个不同的转移操作,无需像传统方式那样每次都需要进行一次approve交易,避免了重复授权的繁琐操作,提升了用户体验。

增强安全性

  • 防止重放攻击:ERC20Permit通过使用nonce(随机数)机制来防止重放攻击。每个账户都有一个与之关联的nonce值,每次使用签名进行授权时,nonce值都会增加。这样,即使攻击者截获了用户的签名,也无法再次使用该签名来重复执行相同的授权操作,因为nonce值已经改变,从而有效保障了用户资金的安全。
  • 降低授权风险:在传统的ERC20授权方式中,用户需要先将代币授权给一个合约或地址,然后再由该合约或地址执行转移操作。如果合约存在漏洞或恶意代码,可能会导致用户授权的代币被恶意转移。而ERC20Permit允许用户通过签名来精确控制每次代币转移的数量、接收者和截止时间等信息,用户无需将大量代币长期授权给合约,降低了因合约漏洞或恶意行为导致的资金损失风险

合约开发

说明:借助openzeppelin库实现;

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

import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "hardhat/console.sol";

contract MyTokenPermit is ERC20Permit {
    constructor() ERC20("MyTokenPermit", "MTKP") ERC20Permit("MyTokenPermit") {
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }
}
# 编译指令
# npx hardhat compile

测试合约

测试说明:主要测试 允许并批准使用签名、截止日期过期、 签名无效 、过期随机数场景

const { ethers } = require("hardhat");
const { expect } = require("chai");

describe("MyTokenPermit", function () {
    let MyTokenPermit;
    let myTokenPermit;
    let owner;
    let spender;
    let addr2;

    beforeEach(async function () {
        await deployments.fixture(["MyTokenPermit"]);
        [owner, spender, addr2] = await ethers.getSigners();
        [owner,addr1,addr2]=await ethers.getSigners();
        const MyTokenPermitDeployment = await deployments.get("MyTokenPermit");
        myTokenPermit = await ethers.getContractAt("MyTokenPermit",MyTokenPermitDeployment.address);//已经部署的合约交互
        //同上代码
        // const MyTokenPermitFactory = await ethers.getContractFactory("MyTokenPermit");
        // myTokenPermit = await MyTokenPermitFactory.deploy();
        // await myTokenPermit.waitForDeployment();
    });

    describe("Basic Token Operations", function () {
        it("查看代币的 name 和 symbol", async function () {
            expect(await myTokenPermit.name()).to.equal("MyTokenPermit");
            expect(await myTokenPermit.symbol()).to.equal("MTKP");
        });

        it("查看代币的总量", async function () {
            const ownerBalance = await myTokenPermit.balanceOf(owner.address);
            expect(await myTokenPermit.totalSupply()).to.equal(ownerBalance);
        });
    });

    describe("Permit", function () {
        it("允许并批准使用签名", async function () {
            const chainId = (await ethers.provider.getNetwork()).chainId;

            // 构建域分隔符
            const domain = {
                name: "MyTokenPermit",
                version: "1",
                chainId: chainId,
                verifyingContract: await myTokenPermit.getAddress()
            };

            // EIP2612 标准的 Permit 类型
            const types = {
                Permit: [
                    { name: "owner", type: "address" },
                    { name: "spender", type: "address" },
                    { name: "value", type: "uint256" },
                    { name: "nonce", type: "uint256" },
                    { name: "deadline", type: "uint256" }
                ]
            };

            const value = ethers.parseEther("100");
            const deadline = Math.floor(Date.now() / 1000) + 60 * 60; // 1 hour from now
            const nonce = await myTokenPermit.nonces(owner.address);

            // 准备签名数据
            const message = {
                owner: owner.address,
                spender: spender.address,
                value: value,
                nonce: nonce,
                deadline: deadline
            };

            // 签名
            const signature = await owner.signTypedData(domain, types, message);
            const { v, r, s } = ethers.Signature.from(signature);

            // 使用 permit
            await myTokenPermit.permit(
                owner.address,
                spender.address,
                value,
                deadline,
                v,
                r,
                s
            );

            // 验证授权额度
            expect(await myTokenPermit.allowance(owner.address, spender.address)).to.equal(value);

            // 测试转账
            await myTokenPermit.connect(spender).transferFrom(owner.address, addr2.address, value);
            expect(await myTokenPermit.balanceOf(addr2.address)).to.equal(value);
            console.log("转账成功")
        });

        it("截止日期过期 失败", async function () {
            const chainId = (await ethers.provider.getNetwork()).chainId;

            const domain = {
                name: "MyTokenPermit",
                version: "1",
                chainId: chainId,
                verifyingContract: await myTokenPermit.getAddress()
            };

            const types = {
                Permit: [
                    { name: "owner", type: "address" },
                    { name: "spender", type: "address" },
                    { name: "value", type: "uint256" },
                    { name: "nonce", type: "uint256" },
                    { name: "deadline", type: "uint256" }
                ]
            };

            const value = ethers.parseEther("100");
            const deadline = Math.floor(Date.now() / 1000) - 60; // 1 minute ago
            const nonce = await myTokenPermit.nonces(owner.address);

            const message = {
                owner: owner.address,
                spender: spender.address,
                value: value,
                nonce: nonce,
                deadline: deadline
            };

            const signature = await owner.signTypedData(domain, types, message);
            const { v, r, s } = ethers.Signature.from(signature);

            await expect(
                myTokenPermit.permit(
                    owner.address,
                    spender.address,
                    value,
                    deadline,
                    v,
                    r,
                    s
                )
            ).to.be.revertedWith("ERC20Permit: expired deadline");
        });

        it("签名无效 失败", async function () {
            const chainId = (await ethers.provider.getNetwork()).chainId;

            const domain = {
                name: "MyTokenPermit",
                version: "1",
                chainId: chainId,
                verifyingContract: await myTokenPermit.getAddress()
            };

            const types = {
                Permit: [
                    { name: "owner", type: "address" },
                    { name: "spender", type: "address" },
                    { name: "value", type: "uint256" },
                    { name: "nonce", type: "uint256" },
                    { name: "deadline", type: "uint256" }
                ]
            };

            const value = ethers.parseEther("100");
            const deadline = Math.floor(Date.now() / 1000) + 60 * 60;
            const nonce = await myTokenPermit.nonces(owner.address);

            const message = {
                owner: owner.address,
                spender: spender.address,
                value: value,
                nonce: nonce,
                deadline: deadline
            };

            // 使用错误的签名者
            const signature = await spender.signTypedData(domain, types, message);
            const { v, r, s } = ethers.Signature.from(signature);

            await expect(
                myTokenPermit.permit(
                    owner.address,
                    spender.address,
                    value,
                    deadline,
                    v,
                    r,
                    s
                )
            ).to.be.revertedWith("ERC20Permit: invalid signature");
        });

        it("过期随机数 失败", async function () {
            const chainId = (await ethers.provider.getNetwork()).chainId;

            const domain = {
                name: "MyTokenPermit",
                version: "1",
                chainId: chainId,
                verifyingContract: await myTokenPermit.getAddress()
            };

            const types = {
                Permit: [
                    { name: "owner", type: "address" },
                    { name: "spender", type: "address" },
                    { name: "value", type: "uint256" },
                    { name: "nonce", type: "uint256" },
                    { name: "deadline", type: "uint256" }
                ]
            };

            const value = ethers.parseEther("100");
            const deadline = Math.floor(Date.now() / 1000) + 60 * 60;
            const nonce = await myTokenPermit.nonces(owner.address);

            const message = {
                owner: owner.address,
                spender: spender.address,
                value: value,
                nonce: nonce,
                deadline: deadline
            };

            const signature = await owner.signTypedData(domain, types, message);
            const { v, r, s } = ethers.Signature.from(signature);

            // 第一次使用permit应该成功
            await myTokenPermit.permit(
                owner.address,
                spender.address,
                value,
                deadline,
                v,
                r,
                s
            );

            // 使用相同的签名再次调用permit应该失败
            await expect(
                myTokenPermit.permit(
                    owner.address,
                    spender.address,
                    value,
                    deadline,
                    v,
                    r,
                    s
                )
            ).to.be.revertedWith("ERC20Permit: invalid signature");
        });
    });
});
# 测试指令
# npx hardhat test ./test/xxx.js

合约部署

module.exports = async function ({getNamedAccounts,deployments}) {
    const  firstAccount= (await getNamedAccounts()).firstAccount;
    const  secondAccount= (await getNamedAccounts()).secondAccount;
    const {deploy,log}=deployments
    const MyTokenPermit=await deploy("MyTokenPermit",{
        from:firstAccount,
        args: [],//参数 代币名字,代币符号
        log: true,
    })
    console.log('MyTokenPermit合约地址',MyTokenPermit.address)
}
module.exports.tags = ["all", "MyTokenPermit"];
# 部署指令
# npx hardhat deploy

总结

以上就是基于EIP-712 签名修改授权标准,实现一个ERC20代币的增强版的合约,涵盖了开发、测试、部署全部流程以及对该标准使用场景和优点等方面的相关介绍。

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

0 条评论

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