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;
}
}

浙公網安備 33010602011771號