【从0学习Solidity】 52. EIP712 类型化数据签名
- 博主简介:不写代码没饭吃,一名全栈领域的创作者,专注于研究互联网产品的解决方案和技术。熟悉云原生、微服务架构,分享一些项目实战经验以及前沿技术的见解。
- 关注我们的主页,探索全栈开发,期待与您一起在移动开发的世界中,不断进步和创造!
- 本文收录于 不写代码没饭吃 的学习汇报系列,大家有兴趣的可以看一看。
- 欢迎访问我们的微信公众号:不写代码没饭吃,获取更多精彩内容、实用技巧、行业资讯等。您关注的是我们前进的动力!
这一讲,我们介绍一种更先进、安全的签名方法,EIP712 类型化数据签名。
EIP712
之前我们介绍了 EIP191 签名标准(personal sign) ,它可以给一段消息签名。但是它过于简单,当签名数据比较复杂时,用户只能看到一串十六进制字符串(数据的哈希),无法核实签名内容是否与预期相符。
EIP712类型化数据签名是一种更高级、更安全的签名方法。当支持 EIP712 的 Dapp 请求签名时,钱包会展示签名消息的原始数据,用户可以在验证数据符合预期之后签名。
EIP712 使用方法
EIP712 的应用一般包含链下签名(前端或脚本)和链上验证(合约)两部分,下面我们用一个简单的例子 EIP712Storage
来介绍 EIP712 的使用方法。EIP712Storage
合约有一个状态变量 number
,需要验证 EIP712 签名才可以更改。
链下签名
-
EIP712 签名必须包含一个
EIP712Domain
部分,它包含了合约的 name,version(一般约定为 “1”),chainId,和 verifyingContract(验证签名的合约地址)。EIP712Domain: [{ name: "name", type: "string" },{ name: "version", type: "string" },{ name: "chainId", type: "uint256" },{ name: "verifyingContract", type: "address" }, ]
这些信息会在用户签名时显示,并确保只有特定链的特定合约才能验证签名。你需要在脚本中传入相应参数。
const domain = {name: "EIP712Storage",version: "1",chainId: "1",verifyingContract: "0xf8e81D47203A594245E36C48e151709F0C19fBe8", };
-
你需要根据使用场景自定义一个签名的数据类型,他要与合约匹配。在
EIP712Storage
例子中,我们定义了一个Storage
类型,它有两个成员:address
类型的spender
,指定了可以修改变量的调用者;uint256
类型的number
,指定了变量修改后的值。const types = {Storage: [{ name: "spender", type: "address" },{ name: "number", type: "uint256" },], };
-
创建一个
message
变量,传入要被签名的类型化数据。const message = {spender: "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",number: "100", };
-
调用钱包对象的
signTypedData()
方法,传入前面步骤中的domain
,types
,和message
变量进行签名(这里使用ethersjs v6
)。// 获得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
的值。
// SPDX-License-Identifier: MIT
// By 0xAA
pragma solidity ^0.8.0;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("Storage(address spender,uint256 number)");bytes32 private DOMAIN_SEPARATOR;uint256 number;address owner;constructor(){DOMAIN_SEPARATOR = keccak256(abi.encode(EIP712DOMAIN_TYPEHASH, // type hashkeccak256(bytes("EIP712Storage")), // namekeccak256(bytes("1")), // versionblock.chainid, // chain idaddress(this) // contract address));owner = msg.sender;}/*** @dev Store value in variable*/function permitStore(uint256 _num, 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 bytesmload(p) 载入从内存地址p起始的接下来32 bytes数据*/// 读取长度数据后的32 bytesr := mload(add(_signature, 0x20))// 读取之后的32 bytess := mload(add(_signature, 0x40))// 读取最后一个bytev := byte(0, mload(add(_signature, 0x60)))}// 获取签名消息hashbytes32 digest = keccak256(abi.encodePacked("\x19\x01",DOMAIN_SEPARATOR,keccak256(abi.encode(STORAGE_TYPEHASH, msg.sender, _num)))); address signer = digest.recover(v, r, s); // 恢复签名者require(signer == owner, "EIP712Storage: Invalid signature"); // 检查签名// 修改状态变量number = _num;}/*** @dev Return value * @return value of 'number'*/function retrieve() public view returns (uint256){return number;}
}
Remix 复现
-
部署
EIP712Storage
合约。 -
运行
eip712storage.html
,将Contract Address
改为部署的EIP712Storage
合约地址,然后依次点击Connect Metamask
和Sign Permit
按钮签名。签名要使用部署合约的钱包,比如 Remix 测试钱包:public_key: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 private_key: 503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb
-
调用合约的
permitStore()
方法,输入相应的_num
和签名,修改number
的值。 -
调用合约的
retrieve()
方法,看到number
的值已经改变。
总结
这一讲,我们介绍了 EIP712 类型化数据签名,一种更先进、安全的签名标准。在请求签名时,钱包会展示签名消息的原始数据,用户可以在验证数据后签名。该标准应用广泛,在 Metamask,Uniswap 代币对,DAI 稳定币等场景均有使用,希望大家好好掌握。
如果这份博客对大家有帮助,希望各位给作者一个免费的点赞👍作为鼓励,并评论收藏一下⭐,谢谢大家!!!
制作不易,如果大家有什么疑问或给作者的意见,欢迎评论区留言。