EIP712类型化数据签名是一种更高级、更安全的签名方法。当支持 EIP712 的 Dapp 请求签名时, 钱包会展示签名消息的原始数据,用户可以在验证数据符合预期之后签名。
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 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);
// SPDX-License-Identifier: MIT
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 hash
keccak256(bytes("EIP712Storage")), // name
keccak256(bytes("1")), // version
block.chainid, // chain id
address(this) // contract address
));
owner = msg.sender;
}
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 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, msg.sender, _num))));
address signer = digest.recover(v, r, s); // 恢复签名者
require(signer == owner, "EIP712Storage: Invalid signature"); // 检查签名
// 修改状态变量
number = _num;
}
function retrieve() public view returns (uint256){
return number;
}
}