什么是EIP712

EIP712是一种特殊的类型化数据签名,与普通签名不同,EIP712的签名数据是结构化的。使用支持EIP712的Dapp进行签名时,Dapp会展示签名消息的结构化详细数据,用户可以对数据进行验证,确认后再进行签名。

实现逻辑

EIP712分为链下签名和链上校验两部分,链下的签名结构定义需要与链上的验证合约保持一致。

验签逻辑则和普通签名相同,通过r,s,v验证公钥是否一致即可。

具体实现

链下签名

一个标准的签名结构如下所示

{
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" },
{ "name": "chainId", "type": "uint256" },
{ "name": "verifyingContract", "type": "address" }
],
"Person": [
{ "name": "name", "type": "string" },
{ "name": "wallet", "type": "address" }
],
"Mail": [
{ "name": "from", "type": "Person" },
{ "name": "to", "type": "Person" },
{ "name": "contents", "type": "string" }
]
},
"primaryType": "Mail",
"domain": {
"name": "MyDapp",
"version": "1",
"chainId": 1,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Alice",
"wallet": "0x1234567890abcdef1234567890abcdef12345678"
},
"to": {
"name": "Bob",
"wallet": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
},
"contents": "Hello, Bob!"
}
}

其中types是签名信息中出现的数据结构的类型定义,包括固定的EIP712Domain以及自定义的结构体,在这个示例中是PersonMail。因为EIP712支持嵌套结构,可以看到Mail中出现了Person的成员变量。

domainmessage就是types中定义的结构的具体实现,domain就是固定的指向EIP712Domain,其中nameversion需要与验签合约中定义的一致,chainIdverifyContract就是合约部署的链与地址。

message在这个示例中就是一个Mail对象,可以看到primaryTypeMail,这代表了message的对象类型,因为支持嵌套,所以在解析时会从primaryType开始解析,然后逐步解析内置的其他结构体。在ether.js中会自动分析primaryType,所以无需指定。

签名时会按照结构体来向用户展示message信息。

链上合约

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract EIP712Storage {
using ECDSA for bytes32; bytes32 private constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
bytes32 private constant STORAGE_TYPEHASH = keccak256("Person(string name,address wallet)");
bytes32 private DOMAIN_SEPARATOR;
}

在变量中定义了两个TYPEHASH常量,分别是domainmessage的type,用于后面生成签名摘要。

    constructor(){
DOMAIN_SEPARATOR = keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH, // type hash
keccak256(bytes("EIP712Storage")), // name
keccak256(bytes("1")), // version
block.chainid, // chain id
address(this) // contract address
));
owner = msg.sender;
}

在构造函数中定义了domainnameversion,因此链下签名中的domain也要保持一致。

    function permitStore(string memory name, bytes memory _signature) public {
// 检查签名长度,65是标准r,s,v签名的长度
require(_signature.length == 65, "invalid signature length");
bytes32 r;
bytes32 s;
uint8 v;
// 目前只能用assembly (内联汇编)来从签名中获得r,s,v的值
assembly {
/*
前32 bytes存储签名的长度 (动态数组存储规则)
add(sig, 32) = sig的指针 + 32
等效为略过signature的前32 bytes
mload(p) 载入从内存地址p起始的接下来32 bytes数据
*/
// 读取长度数据后的32 bytes
r := mload(add(_signature, 0x20))
// 读取之后的32 bytes
s := mload(add(_signature, 0x40))
// 读取最后一个byte
v := byte(0, mload(add(_signature, 0x60)))
} // 获取签名消息hash
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(STORAGE_TYPEHASH, name, msg.sender))
)); address signer = digest.recover(v, r, s); // 恢复签名者
require(signer == msg.sender, "EIP712Storage: Invalid signature"); // 检查签名
}

可以看到在方法中重新生成了一个签名摘要digest,也就是message hash,其中 "\x19\x01"是签名哈希的固定前缀,然后拼接上domainmessage内容的哈希,再调用recover恢复出signer

此处的recover方法是openzeppelin库的语法糖,因为前面通过 using ECDSA for bytes32;引入了ECDSA,用recover替代了底层的ecrecover实现,使校验更方便。

ERC20 Permit

基于EIP712,可以在链下实现对ERC20token的授权,称为ERC20Permit。

即链下实现签名,链上合约验证,验证成功后调用approve方法。在这种情况下token的owner无需持有gas,只需要在链下签名后将签名给到有gas的B,由B去执行,就可以实现将token授权给B甚至第三方的操作。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; /**
* @dev ERC20 Permit 扩展的接口,允许通过签名进行批准,如 https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]中定义。
*
* 添加了 {permit} 方法,可以通过帐户签名的消息更改帐户的 ERC20 余额(参见 {IERC20-allowance})。通过不依赖 {IERC20-approve},代币持有者的帐户无需发送交易,因此完全不需要持有 Ether。
*/
contract ERC20Permit is ERC20, IERC20Permit, EIP712 {
mapping(address => uint) private _nonces; bytes32 private constant _PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); /**
* @dev 初始化 EIP712 的 name 以及 ERC20 的 name 和 symbol
*/
constructor(string memory name, string memory symbol) EIP712(name, "1") ERC20(name, symbol){} /**
* @dev See {IERC20Permit-permit}.
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual override {
// 检查 deadline
require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); // 拼接 Hash
bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));
bytes32 hash = _hashTypedDataV4(structHash); // 从签名和消息计算 signer,并验证签名
address signer = ECDSA.recover(hash, v, r, s);
require(signer == owner, "ERC20Permit: invalid signature"); // 授权
_approve(owner, spender, value);
} /**
* @dev See {IERC20Permit-nonces}.
*/
function nonces(address owner) public view virtual override returns (uint256) {
return _nonces[owner];
} /**
* @dev See {IERC20Permit-DOMAIN_SEPARATOR}.
*/
function DOMAIN_SEPARATOR() external view override returns (bytes32) {
return _domainSeparatorV4();
} /**
* @dev "消费nonce": 返回 `owner` 当前的 `nonce`,并增加 1。
*/
function _useNonce(address owner) internal virtual returns (uint256 current) {
current = _nonces[owner];
_nonces[owner] += 1;
}
}

solidity学习之EIP712的更多相关文章

  1. solidity学习-cryptoPunks为实例

    在这里使用cryptoPunks为实例来进行solidity的介绍,一般这些内容理解了就能够进行相对简单的智能合约的编写了,同时会添加一些我认为也十分重要的内容学习文档为http://solidity ...

  2. solidity 学习笔记(7)内联汇编

    为什么要有内联汇编? //普通循环和内敛汇编循环比较 pragma solidity ^0.4.25; contract Assembly{ function nativeLoop() public ...

  3. solidity 学习笔记(6)call 函数

    call() 方法 call()是一个底层的接口,用来向一个合约发送消息,也就是说如果你想实现自己的消息传递,可以使用这个函数.函数支持传入任意类型的任意参数,并将参数打包成32字节,相互拼接后向合约 ...

  4. solidity 学习笔记(5)接口

    接口:不用实现方法,仅仅定义方法. pragma solidity ^; contract cat{ //cat实际上实现了接口animalEat,因为他们有相同的方法. string name; f ...

  5. solidity 学习笔记(3) 函数修饰符/继承

    修饰符: 函数修饰符有 constant  view pure 其中 constant和view的功能是一样的  不会消耗gas 也就是说不会做任何存储   constant在5.0以后的版本中被废弃 ...

  6. solidity 学习笔记 2 (二维数组)

    solidity 二维数组: pragma solidity ^0.4.23; contract twoArray{ uint[2][3] grade =[[20,30],[40,50],[45,60 ...

  7. solidity学习笔记

    一 pragam solidity ^0.4.23; contract helloword{ string public  name ="hello"; function getN ...

  8. 以太坊智能合约开发 Solidity学习

    1. pragma solidity >=0.4.22 <0.6.0;//版本号,头文件 contract BooleanTest { bool _a;//默认返回false int nu ...

  9. solidity 学习笔记(4)library库

    library库的申明: library SafeMath{ functrion mul(uint a,uint b) public returns (uint){ uint c= a*b; asse ...

  10. cryptopunks的代码解释

    1.imageHash就是将punk所有图像合在一起的那张图punks.png进行hash得到一个值,并将该值存储到链上,用处就是你可以通过将图像hash然后跟该值对比看图像对不对.这就是它的用处,在 ...

随机推荐

  1. Chester1011的疑问

    题目背景 一天,\(\texttt{Chester}\)和\(\texttt{hsh}\)在写数据结构题. 他们开始刷起了羊毛地毯.在羊毛地毯的落地点,有一个漏斗.漏斗下面会经过漏斗矿车,每次只能吸走 ...

  2. 一文速通 Python 并行计算:12 Python 多进程编程-进程池 Pool

    一文速通 Python 并行计算:12 Python 多进程编程-进程池 Pool 摘要: 在Python多进程编程中,Pool类用于创建进程池,可并行执行多个任务.通过map.apply等方法,将函 ...

  3. xshell里面实现kafka一对一发送和接收消息测试

    1.连接相关xshell,打开两个窗口一个给生产者用一个给消费者用 在生产者里输入:./kafka-console-producer.sh --broker-list localhost:9092 - ...

  4. MySQL事务:工作原理与实用指南

    MySQL事务:工作原理与实用指南 在数据库操作中,事务是保证数据一致性的重要机制.本文将深入探讨 MySQL 事务的特性.隔离级别以及实际应用场景,帮助你更好地理解和使用事务. 一.什么是事务? 事 ...

  5. Vertx 实现webapi实战项目(二)

      消息解析:消息序列化和反序列化---上传json解析和返回json编码. 整理下工程项目 一:实现消息接口,在imp文件夹下新建接口MessageFactory 1 /****** 2 * 消息编 ...

  6. webFlux入门

    今天发现一个特别好的文章,是关于springBoot框架中响应式编程的,那下面就是这位博主所整理的一些干货 ---------------------------------------------- ...

  7. 探索 Vue.js 组件的最新特性

    引言: Vue.js 作为一款流行的前端框架,始终在不断发展和演进,为开发者带来新的特性和功能,以提升开发效率和用户体验.Vue.js 组件是构建 Vue 应用的基础,其最新特性为开发者提供了更强大的 ...

  8. C# 动态类型 模型转XML

    <?xml version="1.0" encoding="utf-8"?> <qrylist><order orderid=&q ...

  9. github action 与自动化部署

    前言 github action 一直都是自动化部署的引领者,今天就介绍一下如何它部署咱们的网站和服务 服务器生成ssh密钥 通过终端(finalshell.xshell)登录到您的 linux 服务 ...

  10. Java中==与equals()函数的区别

    前段时间写网站,在servlet中要对用户输入的密码做判断,就出现一个很奇怪的现象:if条件句中如果用"=="作判断条件,就没法通过验证,而一换成equlas()函数,就完美解决了 ...