52. EIP712 类型化数据签名
WTF Solidity极简入门: 52. EIP712 类型化数据签名
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
这一讲,我们介绍一种更先进、安全的签名方法,EIP712 类型化数据签名。
EIP712
之前我们介绍了 EIP191 签名标准(personal sign) ,它可以给一段消息签名。但是它过于简单,当签名数据比较复杂时,用户只能看到一串十六进制字符串(数据的哈希),无法核实签名内容是否与预期相符。
EIP712类型化数据签名是一种更高级、更安全的签名方法。当支持 EIP712 的 Dapp 请求签名时,钱包会展示签名消息的原始数据,用户可以在验证数据符合预期之后签名。
EIP712 使用方法
EIP712 的应用一般包含链下签名(前端或脚本)和链上验证(合约)两部分,下面我们用一个简单的例子 EIP712Storage
来介绍 EIP712 的使用方法。EIP712Storage
合约有一个状态变量 number
,需要验证 EIP712 签名才可以更改。
链下签名
-
EIP712 签名必须包含一个
EIP712Domain
部分,它包含了合约的 name,version(一般约定为 “1”),chainId 和 verifyingContract(验证签名的合约地址)。1
2
3
4
5
6EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
]这些信息会在用户签名时显示,并确保只有特定链的特定合约才能验证签名。你需要在脚本中传入相应参数。
1
2
3
4
5
6const domain = {
name: "EIP712Storage",
version: "1",
chainId: "1",
verifyingContract: "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
}; -
你需要根据使用场景自定义一个签名的数据类型,他要与合约匹配。在
EIP712Storage
例子中,我们定义了一个Storage
类型,它有两个成员:address
类型的spender
,指定了可以修改变量的调用者;uint256
类型的number
,指定了变量修改后的值。1
2
3
4
5
6const types = {
Storage: [
{ name: "spender", type: "address" },
{ name: "number", type: "uint256" },
],
}; -
创建一个
message
变量,传入要被签名的类型化数据。1
2
3
4const message = {
spender: "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
number: "100",
}; -
调用钱包对象的
signTypedData()
方法,传入前面步骤中的domain
,types
,和message
变量进行签名(这里使用ethersjs v6
)。1
2
3
4
5// 获得provider
const provider = new ethers.BrowserProvider(window.ethereum)
// 获得signer后调用signTypedData方法进行eip712签名
const signature = await signer.signTypedData(domain, types, message);
console.log("Signature:", signature);
链上验证
接下来就是 EIP712Storage
合约部分,它需要验证签名,如果通过,则修改 number
状态变量。它有 5
个状态变量。
EIP712DOMAIN_TYPEHASH
:EIP712Domain
的类型哈希,为常量。STORAGE_TYPEHASH
:Storage
的类型哈希,为常量。DOMAIN_SEPARATOR
: 这是混合在签名中的每个域 (Dapp) 的唯一值,由EIP712DOMAIN_TYPEHASH
以及EIP712Domain
(name, version, chainId, verifyingContract)组成,在constructor()
中初始化。number
: 合约中存储值的状态变量,可以被permitStore()
方法修改。owner
: 合约所有者,在constructor()
中初始化,在permitStore()
方法中验证签名的有效性。
另外,EIP712Storage
合约有 3
个函数。
- 构造函数: 初始化
DOMAIN_SEPARATOR
和owner
。 retrieve()
: 读取number
的值。permitStore
: 验证 EIP712 签名,并修改number
的值。首先,它先将签名拆解为r
,s
,v
。然后用DOMAIN_SEPARATOR
,STORAGE_TYPEHASH
, 调用者地址,和输入的_num
参数拼出签名的消息文本digest
。最后利用ECDSA
的recover()
方法恢复出签名者地址,如果签名有效,则更新number
的值。
1 | // SPDX-License-Identifier: MIT |
部署复现
-
在
Remix
部署EIP712Storage
合约。 -
运行
eip712storage.html
,根据浏览器的内容安全策略(Content Security Policy)的要求,MetaMask 不能通过打开的本地文件(file:// 协议)与 DApp 通信。 可以使用 Node 静态文件服务器http-server
启动本地服务,在包含eip712storage.html
文件的目录下执行以下命令:1
2npm install -g http-server
http-server在浏览器中打开
http://127.0.0.1:8080
就可以访问了。 然后将Contract Address
改为部署的EIP712Storage
合约地址,然后依次点击Connect Metamask
和Sign Permit
按钮签名。签名要使用部署合约的钱包,比如 Remix 测试钱包:1
2public_key: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
private_key: 503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb -
调用合约的
permitStore()
方法,输入相应的_num
和签名,修改number
的值。 -
调用合约的
retrieve()
方法,看到number
的值已经改变。
总结
这一讲,我们介绍了 EIP712 类型化数据签名,一种更先进、安全的签名标准。在请求签名时,钱包会展示签名消息的原始数据,用户可以在验证数据后签名。该标准应用广泛,在 Metamask,Uniswap 代币对,DAI 稳定币等场景均有使用,希望大家好好掌握。