solidity学习之EIP712
什么是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以及自定义的结构体,在这个示例中是Person和Mail。因为EIP712支持嵌套结构,可以看到Mail中出现了Person的成员变量。
domain和message就是types中定义的结构的具体实现,domain就是固定的指向EIP712Domain,其中name和version需要与验签合约中定义的一致,chainId和verifyContract就是合约部署的链与地址。
而message在这个示例中就是一个Mail对象,可以看到primaryType为Mail,这代表了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常量,分别是domain和message的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;
}
在构造函数中定义了domain的name和version,因此链下签名中的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"是签名哈希的固定前缀,然后拼接上domain和message内容的哈希,再调用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的更多相关文章
- solidity学习-cryptoPunks为实例
在这里使用cryptoPunks为实例来进行solidity的介绍,一般这些内容理解了就能够进行相对简单的智能合约的编写了,同时会添加一些我认为也十分重要的内容学习文档为http://solidity ...
- solidity 学习笔记(7)内联汇编
为什么要有内联汇编? //普通循环和内敛汇编循环比较 pragma solidity ^0.4.25; contract Assembly{ function nativeLoop() public ...
- solidity 学习笔记(6)call 函数
call() 方法 call()是一个底层的接口,用来向一个合约发送消息,也就是说如果你想实现自己的消息传递,可以使用这个函数.函数支持传入任意类型的任意参数,并将参数打包成32字节,相互拼接后向合约 ...
- solidity 学习笔记(5)接口
接口:不用实现方法,仅仅定义方法. pragma solidity ^; contract cat{ //cat实际上实现了接口animalEat,因为他们有相同的方法. string name; f ...
- solidity 学习笔记(3) 函数修饰符/继承
修饰符: 函数修饰符有 constant view pure 其中 constant和view的功能是一样的 不会消耗gas 也就是说不会做任何存储 constant在5.0以后的版本中被废弃 ...
- solidity 学习笔记 2 (二维数组)
solidity 二维数组: pragma solidity ^0.4.23; contract twoArray{ uint[2][3] grade =[[20,30],[40,50],[45,60 ...
- solidity学习笔记
一 pragam solidity ^0.4.23; contract helloword{ string public name ="hello"; function getN ...
- 以太坊智能合约开发 Solidity学习
1. pragma solidity >=0.4.22 <0.6.0;//版本号,头文件 contract BooleanTest { bool _a;//默认返回false int nu ...
- solidity 学习笔记(4)library库
library库的申明: library SafeMath{ functrion mul(uint a,uint b) public returns (uint){ uint c= a*b; asse ...
- cryptopunks的代码解释
1.imageHash就是将punk所有图像合在一起的那张图punks.png进行hash得到一个值,并将该值存储到链上,用处就是你可以通过将图像hash然后跟该值对比看图像对不对.这就是它的用处,在 ...
随机推荐
- Java遍历Map对象的常见方法
在Java中遍历Map有不少的方法,我们看一下最常用的方法及其优缺点. 既然Java中的所有map都实现了Map接口,以下方法适用于任何map实现(HashMap, TreeMap, LinkedHa ...
- protobuf 'NoneType' object has no attribute 'message_types_by_name'
最近爬一个网站,用的protobuf协议,报错查了半天.报错'NoneType' object has no attribute 'message_types_by_name',最后是因为protob ...
- GC-QA-RAG 智能问答系统的文档切片
本章节介绍 GC-QA-RAG 智能问答系统的文档切片原理,即如何将原始文档的知识点切片后存入向量数据库. 1. 原始思路 将整个文档作为输入,交由大语言模型自动生成问答对(QA Pairs),以支持 ...
- 全球首个无限执行的 AI 出现!给我玩爽了
给 AI 一句话,它就能自主执行任务,生成复杂的大型网站.几十页的 PPT.万字爆款图文.千万字长篇小说? 据说这是世界上第一款无限的 AI 智能体 -- Flowith. 无限这个词听起来有点东西啊 ...
- CommonsBeanutils链与无commons collections的shiro反序列化利用
CommonsBeanutils链与无commons collections的shiro反序列化利用 在cc2中,我们知道可以在commons-collections4通过java.util.Comp ...
- MongoDB可视化工具
简单说明 这里使用mongodb的过程中,可以通过mongo shell或者mongo的可视化工具进行连接. mongo shell连接 # 使用root用户登录mongo mongodb@p8lnp ...
- HTTP POST方式调用SOAP OPERATION类的接口
wsdl地址或者接口地址中有多个方法(operation) 如下是soapui测试的例子,wsdl地址下包含多个operation,但是现在我想用http的方式,只做getKnowledgeParts ...
- HyperWorks的四面体网格剖分
HyperMesh 向用户提供了若干种生成四面体网格的方法.标准四面体网格剖分(Standard Tetramesh)基于一个已有的封闭壳单元包络而成的空间,在合理设置参数的基础上生成四面体网格.标准 ...
- openwrt 烧录rootfs的大小
make menuconfig 里面有一个 target Image的选项,然后进去可以在rootfs修改烧录sd卡的大小
- lingo 练习 二
简介 练习测试 KeyPoints: 条件过滤的应用,循环乘法的应用 EX1.求sets中前几个数的和 model: data: N=6; enddata sets: number/1..N/:x; ...