首页>>资讯>>产业

ERC-712 和 ERC-2612 协议详解

2024-12-24 15:45:18 2

一、前言


● ERC-712 是一种通用的结构化签名标准,为离线签名和链上验证提供了高效工具。

● ERC-2612 是基于 ERC-712 的扩展,专注于代币授权的优化,特别适用于 DeFi 和钱包应用场景。


二、ERC-712(EIP-712)—用于结构化数据的签名标准


1. 什么是ERC-712


ERC-712提供了一种对结构化数据进行离线签名的标准。它通过定义签名的格式和数据结构,确保签名的安全性和可验证性,并显著提高了用户交互的便利性。


2. 核心内容


2.1 数据结构


EIP-712允许开发者定义数据结构,并使用哈希算法将其转换为签名消息。


● 域分隔符(Domain Separator):用于区分不同的合约或网络环境,防止签名跨合约或跨链被滥用。


bytes32 DOMAIN_SEPARATOR = keccak256(

    abi.encode(

        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),

        keccak256(bytes("TokenName")), // 合约名称

        keccak256(bytes("1")),         // 合约版本

        chainId,                       // 链 ID

        address(this)                  // 合约地址

    )

);


● 消息结构:用户自定义的结构化数据,可以是交易信息、授权请求等。


struct Permit {

    address owner;

    address spender;

    uint256 value;

    uint256 nonce;

    uint256 deadline;

}


2.2 签名步骤


● 计算消息哈希:


bytes32 hashStruct = keccak256(

    abi.encode(

        keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),

        owner,

        spender,

        value,

        nonce,

        deadline

    )

);


● 计算完整签名的哈希值:


bytes32 digest = keccak256(

    abi.encodePacked(

        "\x19\x01",

        DOMAIN_SEPARATOR,

        hashStruct

    )

);



● 通过签名工具(如钱包)生成签名


2.3. 验证签名


链上验证签名是否由合法地址生成:


address signer = ecrecover(digest, v, r, s);

require(signer == owner, "Invalid signature");


3.应用场景


● 代币授权(与ERC-2612结合):实现无Gas授权,通过离线签名完成代币授权。

● 去中心化身份认证(DID):利用结构化签名验证用户身份。

● 多重签名钱包:简化多重签名中的签名和验证流程。


三、EIP-2612—基于ERC-712的代币无Gas授权标准


1. 什么是ERC-2612


ERC-2612是对ERC-20的扩展,利用ERC-712的结构化签名标准引入permit方法,使得用户无需调用approve方法即可离线授权代币转账。它简化了授权流程,节省了交易费用。


2. 核心内容


2.1 permit方法


permit方法是ERC-2612的核心功能,用于离线完成代币授权


function permit(

    address owner,

    address spender,

    uint256 value,

    uint256 deadline,

    uint8 v,

    bytes32 r,

    bytes32 s

) external;


● 参数说明:


   owner:授权代币的所有者

   spender:被授权的地址

   value:授权的代币数量

   deadline:签名的有效时间

   v,r,s:签名的分量。


● 功能:


   验证签名是否有效

   更新授权数据(触发Approval事件)


2.2 状态变量


● nonces:每个地址都有一个唯一的nonce,防止签名重放。


mapping(address => uint256) public nonces;


● DOMAIN_SEPARATOR:用于与EIP-712的签名格式对接,确保安全性。


2.3 ERC-2612的工作流程


● 用户构造一条授权数据,并离线签名(通过钱包工具)

● 用户将签名数据发送到链上合约,调用permit方法完成授权

● 合约验证签名的合法性,并记录授权信息


四、ERC-2612的实现代码示例


以下是一个符合ERC-2612的简单合约实现:


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20Permit is ERC20 {

    mapping(address => uint256) public nonces;

    bytes32 public DOMAIN_SEPARATOR;

    bytes32 public constant PERMIT_TYPEHASH =

        keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

    constructor(string memory name, string memory symbol) ERC20(name, symbol) {

        uint256 chainId;

        assembly {

            chainId := chainid()

        }

        DOMAIN_SEPARATOR = keccak256(

            abi.encode(

                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),

                keccak256(bytes(name)),

                keccak256(bytes("1")),

                chainId,

                address(this)

            )

        );

    }

    function permit(

        address owner,

        address spender,

        uint256 value,

        uint256 deadline,

        uint8 v,

        bytes32 r,

        bytes32 s

    ) external {

        require(block.timestamp <= deadline, "Permit: expired deadline");

        bytes32 digest = keccak256(

            abi.encodePacked(

                "\x19\x01",

                DOMAIN_SEPARATOR,

                keccak256(

                    abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)

                )

            )

        );

        address recoveredAddress = ecrecover(digest, v, r, s);

        require(recoveredAddress != address(0) && recoveredAddress == owner, "Permit: invalid signature");

        _approve(owner, spender, value);

    }

}


五、Permit2改进的ERC-20授权标准


Permit2是一种基于ERC-2612的改进协议,它扩展了ERC-20的授权模型,并为DeFi协议提供了更灵活和高效的授权机制。与ERC-2612类似,Permit2也利用了离线签名(基于EIP-712)来完成代币授权,但其功能更强大,支持批量授权、时间限制和转账功能。


1. Permit2的核心目标


●  增强授权灵活性


   支持批量授权操作

   支持时间范围限制,使授权更安全。


●  减少链上交互


   通过离线签名完成批量授权,降低交易成本。

   支持多次调用复用签名,提高效率。


● 改进安全性:限制授权的代币数量和时间范围,降低被滥用的风险。

● 适配多种代币:可以用于任何ERC-20代币,而无需代币原生支持。


2. 核心功能


2.1 授权代币(permit)


扩展的permit方法,允许通过签名授权代币使用,支持批量授权和时间限制。


function permit(

    address owner,

    PermitDetails[] calldata details,

    bytes calldata signature

) external;


● 参数解析


   owner:授权代币的持有人

   details:包含代币授权信息的数组,支持批量授权          

   signature:基于 EIP-712 的签名数据


PermitDetails 结构:


struct PermitDetails {

    address token;     // 授权的代币地址

    address spender;   // 被授权地址

    uint256 amount;    // 授权金额

    uint256 expiration; // 授权的过期时间

    uint256 nonce;      // 防止重放攻击的 nonce

}


2.2 限时授权(restricted transfer)


添加时间范围限制和授权金额限制,确保授权仅在特定时间内有效。


function transferFrom(

    address token,

    address from,

    address to,

    uint256 amount

) external;


● 功能


   验证token的授权信息

   确保调用发生在授权时间范围内

   执行代币转账


2.3 签名验证


Permit2使用EIP-712签名规范,验证离线签名的合法性。


● 计算消息哈希


bytes32 digest = keccak256(

    abi.encodePacked(

        "\x19\x01",

        DOMAIN_SEPARATOR,

        keccak256(

            abi.encode(

                keccak256("Permit(address owner,PermitDetails[] details)"),

                owner,

                details

            )

        )

    )

);


● 验证签名


address signer = ecrecover(digest, v, r, s);

require(signer == owner, "Invalid signature");


2.4 批量撤销授权


提供批量撤销授权的方法,提高安全性


function revoke(

    address[] calldata tokens,

    address[] calldata spenders

) external;


● 功能


   清除指定代币的授权记录

   确保敏感授权能够快速被撤销


3. 状态变量和安全机制


3.1 状态变量


● nonces:每个地址和授权记录都是唯一的nonce,防止签名被重复使用

● authorization:存储每个代币的授权记录,包括授权金额和有效时间


3.2 安全机制


● 签名验证:使用EIP-712验证签名,确保数据来源的可信性

● 时间限制:限制授权的时间范围,减少潜在的滥用风险

● 批量撤销:提供快速清除授权的方法,确保用户资产安全


4. permit2完整实例


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Permit2 {

    struct PermitDetails {

        address token;

        address spender;

        uint256 amount;

        uint256 expiration;

        uint256 nonce;

    }

    mapping(address => uint256) public nonces;

    mapping(address => mapping(address => uint256)) public allowances;

    bytes32 public DOMAIN_SEPARATOR;

    bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,PermitDetails[] details)");

    constructor(string memory name, uint256 chainId) {

        DOMAIN_SEPARATOR = keccak256(

            abi.encode(

                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),

                keccak256(bytes(name)),

                keccak256(bytes("1")),

                chainId,

                address(this)

            )

        );

    }

    function permit(

        address owner,

        PermitDetails[] calldata details,

        uint8 v,

        bytes32 r,

        bytes32 s

    ) external {

        bytes32 digest = keccak256(

            abi.encodePacked(

                "\x19\x01",

                DOMAIN_SEPARATOR,

                keccak256(abi.encode(PERMIT_TYPEHASH, owner, keccak256(abi.encode(details))))

            )

        );

        address signer = ecrecover(digest, v, r, s);

        require(signer == owner, "Invalid signature");

        for (uint256 i = 0; i < details.length; i++) {

            PermitDetails memory detail = details[i];

            require(block.timestamp <= detail.expiration, "Permit expired");

            allowances[detail.token][detail.spender] = detail.amount;

        }

    }

    function transferFrom(

        address token,

        address from,

        address to,

        uint256 amount

    ) external {

        require(allowances[token][msg.sender] >= amount, "Insufficient allowance");

        allowances[token][msg.sender] -= amount;

        IERC20(token).transferFrom(from, to, amount);

    }

    function revoke(address[] calldata tokens, address[] calldata spenders) external {

        require(tokens.length == spenders.length, "Mismatched input lengths");

        for (uint256 i = 0; i < tokens.length; i++) {

            allowances[tokens[i]][spenders[i]] = 0;

        }

    }

}


5.应用场景


● DeFi协议中的无Gas授权


用户通过离线签名授权协议操作代币,提升用户体验。


例如:Uniswap、SushiSwap


● 批量授权与撤销:支持一键批量授权和撤销操作,提高操作效率。

● 时间范围限制:设定授权时间范围,避免授权长期存在导致安全隐患。

● 多代币管理:在多资产场景中,一次性完成多个代币的授权和管理。


六、Permit和Permit2对比


Permit和Permit2是两种代币授权机制,旨在优化和增强ERC-20标准中的授权流程。它们通过离线签名(EIP-712)来减少链上交互,降低用户使用成本。以下是两者在功能、实现和应用场景方面的对比。


1. 基本概念

4.png

2. 功能对比

4.png

3. 方法对比


3.1 Permit(ERC-2612)


主要新增了permit方法,用于完成单一代币的授权操作


function permit(

    address owner,

    address spender,

    uint256 value,

    uint256 deadline,

    uint8 v,

    bytes32 r,

    bytes32 s

) external;


● 限制


   只能用于单一代币

   无法设定时间范围,授权有效期仅通过deadline控制


3.2 Permit2


在permit基础上进行了扩展,支持批量授权和时间范围限制


批量授权


function permit(

    address owner,

    PermitDetails[] calldata details,

    bytes calldata signature

) external;


● PermitDetails结构


struct PermitDetails {

    address token;       // 授权的代币地址

    address spender;     // 授权的账户

    uint256 amount;      // 授权金额

    uint256 expiration;  // 授权过期时间

    uint256 nonce;       // 防止重放攻击的 nonce

}


转账功能


function transferFrom(

    address token,

    address from,

    address to,

    uint256 amount

) external;


● 功能


  使用授权直接完成代币的转账操作

  验证转账是否在授权范围和时间范围内


批量撤销


function revoke(

    address[] calldata tokens,

    address[] calldata spenders

) external;


● 功能:批量清除多个代币的授权记录


4.数据管理对比

4.png

5.应用场景对比

4.png

6. 优势与劣势对比


● Permit(ERC-2612)


   优势


   实现简单,适合单一代币的授权需求

   使用离线签名,降低用户Gas成本


   劣势


   无法批量授权,适配性较弱

   授权时间不可灵活控制

   需要代币原生支持,限制较多


● Permit2


   优势


   支持批量授权和撤销操作,提高效率

   引入时间范围限制,增强安全性

   适配任何ERC-20代币,无需代币本身支持。


   劣势


   状态管理更复杂,可能增加存储和逻辑开销

   实现成本更高,签名数据结构更加复杂


7. 总结

4.png

● 选择Permit(ERC-2612):适用于单一代币的简单授权场景,如普通钱包授权或单代币DeFi协议。

● 选择Permit2:适用于复杂DeFi场景,需要多代币管理、批量授权和撤销操作。


七、ERC-712和ERC-2612项目实战


以下是一个完整的基于 Foundry 的项目示例,包含 Permit(ERC-2612) 和 Permit2 的实现、部署脚本和测试脚本。该项目演示了如何同时实现和测试两种授权机制。


1. 项目目录结构


permit-foundry/

├── src/

│   ├── ERC20Permit.sol       # ERC-2612 实现

│   ├── Permit2.sol           # Permit2 实现

├── script/

│   ├── Deploy.s.sol          # 部署脚本

├── test/

│   ├── ERC20Permit.t.sol     # ERC-2612 测试脚本

│   ├── Permit2.t.sol         # Permit2 测试脚本

├── foundry.toml              # Foundry 配置文件


2.ERC-2612和Permit2合约实现


● 文件:src/ERC20Permit.sol


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20Permit is ERC20 {

    mapping(address => uint256) public nonces;

    bytes32 public DOMAIN_SEPARATOR;

    bytes32 public constant PERMIT_TYPEHASH =

        keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

    constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {

        uint256 chainId;

        assembly {

            chainId := chainid()

        }

        DOMAIN_SEPARATOR = keccak256(

            abi.encode(

                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),

                keccak256(bytes(name)),

                keccak256(bytes("1")),

                chainId,

                address(this)

            )

        );

        _mint(msg.sender, initialSupply);

    }

    function permit(

        address owner,

        address spender,

        uint256 value,

        uint256 deadline,

        uint8 v,

        bytes32 r,

        bytes32 s

    ) external {

        require(block.timestamp <= deadline, "Permit: expired deadline");

        bytes32 digest = keccak256(

            abi.encodePacked(

                "\x19\x01",

                DOMAIN_SEPARATOR,

                keccak256(

                    abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)

                )

            )

        );

        address recoveredAddress = ecrecover(digest, v, r, s);

        require(recoveredAddress != address(0) && recoveredAddress == owner, "Permit: invalid signature");

        _approve(owner, spender, value);

    }

}


● 文件:src/Permit2.sol


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Permit2 {

    struct PermitDetails {

        address token;

        address spender;

        uint256 amount;

        uint256 expiration;

        uint256 nonce;

    }

    mapping(address => uint256) public nonces;

    mapping(address => mapping(address => uint256)) public allowances;

    bytes32 public DOMAIN_SEPARATOR;

    bytes32 public constant PERMIT_TYPEHASH =

        keccak256("Permit(address owner,PermitDetails[] details)");

    constructor(string memory name, uint256 chainId) {

        DOMAIN_SEPARATOR = keccak256(

            abi.encode(

                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),

                keccak256(bytes(name)),

                keccak256(bytes("1")),

                chainId,

                address(this)

            )

        );

    }

    function permit(

        address owner,

        PermitDetails[] calldata details,

        uint8 v,

        bytes32 r,

        bytes32 s

    ) external {

        bytes32 digest = keccak256(

            abi.encodePacked(

                "\x19\x01",

                DOMAIN_SEPARATOR,

                keccak256(abi.encode(PERMIT_TYPEHASH, owner, keccak256(abi.encode(details))))

            )

        );

        address signer = ecrecover(digest, v, r, s);

        require(signer == owner, "Invalid signature");

        for (uint256 i = 0; i < details.length; i++) {

            PermitDetails memory detail = details[i];

            require(block.timestamp <= detail.expiration, "Permit expired");

            allowances[detail.token][detail.spender] = detail.amount;

        }

    }

    function transferFrom(

        address token,

        address from,

        address to,

        uint256 amount

    ) external {

        require(allowances[token][msg.sender] >= amount, "Insufficient allowance");

        allowances[token][msg.sender] -= amount;

        IERC20(token).transferFrom(from, to, amount);

    }

}


3.部署脚本


● 文件:script/Deploy.s.sol


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "forge-std/Script.sol";

import "../src/ERC20Permit.sol";

import "../src/Permit2.sol";

contract Deploy is Script {

    function run() external {

        vm.startBroadcast();

        // 部署 ERC20Permit

        ERC20Permit token = new ERC20Permit("MyToken", "MTK", 1000 * 10 ** 18);

        console.log("ERC20Permit deployed at:", address(token));

        // 部署 Permit2

        Permit2 permit2 = new Permit2("Permit2", block.chainid);

        console.log("Permit2 deployed at:", address(permit2));

        vm.stopBroadcast();

    }

}


4.测试脚本


● 文件:test/ERC20Permit.t.sol


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "forge-std/Test.sol";

import "../src/ERC20Permit.sol";

contract ERC20PermitTest is Test {

    ERC20Permit public token;

    address public owner;

    address public spender;

    function setUp() public {

        token = new ERC20Permit("MyToken", "MTK", 1000 * 10 ** 18);

        owner = address(1);

        spender = address(2);

        vm.prank(address(this));

        token.transfer(owner, 500 * 10 ** 18);

    }

    function testPermit() public {

        uint256 amount = 100 * 10 ** 18;

        uint256 deadline = block.timestamp + 1 days;

        uint256 nonce = token.nonces(owner);

        bytes32 digest = keccak256(

            abi.encodePacked(

                "\x19\x01",

                token.DOMAIN_SEPARATOR(),

                keccak256(

                    abi.encode(

                        token.PERMIT_TYPEHASH(),

                        owner,

                        spender,

                        amount,

                        nonce,

                        deadline

                    )

                )

            )

        );

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, digest);

        vm.prank(spender);

        token.permit(owner, spender, amount, deadline, v, r, s);

        assertEq(token.allowance(owner, spender), amount);

    }

}


● 文件:test/Permit2.t.sol


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "forge-std/Test.sol";

import "../src/Permit2.sol";

contract Permit2Test is Test {

    Permit2 public permit2;

    address public owner;

    address public spender;

    function setUp() public {

        permit2 = new Permit2("Permit2", block.chainid);

        owner = address(1);

        spender = address(2);

    }

    function testPermit2() public {

        Permit2.PermitDetails;

        details[0] = Permit2.PermitDetails({

            token: address(this),

            spender: spender,

            amount: 100,

            expiration: block.timestamp + 1 days,

            nonce: 0

        });

        bytes32 digest = keccak256(

            abi.encodePacked(

                "\x19\x01",

                permit2.DOMAIN_SEPARATOR(),

                keccak256(abi.encode(permit2.PERMIT_TYPEHASH(), owner, keccak256(abi.encode(details))))

            )

        );

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, digest);

        vm.prank(spender);

        permit2.permit(owner, details, v, r, s);

        assertEq(permit2.allowances(address(this), spender), 100);

    }

}


5. 测试与部署


● 安装依赖

forge install OpenZeppelin/openzeppelin-contracts


● 编译

forge build


● 运行测试

forge test


● 部署合约

forge script script/Deploy.s.sol --broadcast --rpc-url <YOUR_RPC_URL>


6. 总结


该项目实现了 Permit 和 Permit2 的完整逻辑,并展示了如何使用 Foundry 进行测试和部署。Permit 提供单代币授权,而 Permit2 支持批量操作。

声明:本网站所有相关资料如有侵权请联系站长删除,资料仅供用户学习及研究之用,不构成任何投资建议!