ERC-712 和 ERC-2612 协议详解

  • Dapplink
  • 发布于 2024-12-24 20:51
  • 阅读 29

ERC-712是一种通用的结构化签名标准,为离线签名和链上验证提供了高效工具。ERC-2612是基于ERC-712的扩展,专注于代币授权的优化,特别适用于DeFi和钱包应用场景。

一、前言

  • 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):利用结构化签名验证用户身份。
  • 多重签名钱包:简化多重签名中的签名和验证流程。

<!--StartFragment-->

三、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 &lt;= 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 &lt; details.length; i++) {   
       PermitDetails memory detail = details[i];            
       require(block.timestamp &lt;= 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 &lt; tokens.length; i++) {            
     allowances[tokens[i]][spenders[i]] = 0;       
     }    
  }
}

5.应用场景

  • DeFi协议中的无Gas授权用户通过离线签名授权协议操作代币,提升用户体验。例如:Uniswap、SushiSwap
  • 批量授权与撤销:支持一键批量授权和撤销操作,提高操作效率。
  • 时间范围限制:设定授权时间范围,避免授权长期存在导致安全隐患。
  • 多代币管理:在多资产场景中,一次性完成多个代币的授权和管理。

六、Permit和Permit2

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

500b2b576de99896d5b5b14030125c08.png

2. 功能对比

3e565177e18408ba3733dd381c9a641f.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.数据管理对比

fd30a67b551c13a409fc55b06139bd09.png

5.应用场景对比

26fd82976f0b9f082e69c9da50d8ae87.png

6. 优势与劣势对比

● Permit(ERC-2612)

   优势

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

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

   劣势

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

   授权时间不可灵活控制

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

● Permit2

   优势

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

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

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

   劣势

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

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

7. 总结

119c66f9c6b6d54b75f1efb247de3f63.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 &lt;= 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 &lt; details.length; i++) {            
                PermitDetails memory detail = details[i];           
                require(block.timestamp &lt;= 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);    
         }
   }
  1. 测试与部署
    • 安装依赖 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 支持批量操作。

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

0 条评论

请先 登录 后评论
Dapplink
Dapplink
0xBdcb...f214
首个模块化、可组合的Layer3协议。