本篇文章說明 EIP-712 的標準,以及如何使用 Solidity 實作 EIP-712 的智能合約簽章驗證。
簡介
在之前的文章介紹過一些簽章和檢驗的方式,而實際運用的時候可能會簽署許多資料來使用,例如這個例子:
1 2 3 4 5 6 7 8 9
| function permit(address owner, address spender, uint value, uint nonce, uint deadline, uint8 v, bytes32 r, bytes32 s) external { require(deadline >= block.timestamp, "signature expired"); require(!usedNonces[nonce], "used nonce"); bytes32 messageHash = abi.encodePacked(block.chainid, address(this), spender, value, nonce, deadline); bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); require(owner == ecrecover(hash , v, r, s), "invalid signature"); usedNonces[nonce] = true; }
|
這裡使用多種資料,最後簽署的是 messageHash
,如果使用 Metamask 之類的錢包,會看到下面的提示:
對於使用者來說,並不知道簽署的實際內容是什麼。為了解決這個問題,提出 EIP-712 的標準,能夠簽署和預覽結構的資料。例如看到簽署的內容如下:
規格和定義
這邊稍微做一些修改,以比較簡單的方式來說明定義。我們知道最後要被驗證的內容會是一個 hash
如下:
1
| ecrecover(hash , v, r, s);
|
而 EIP-712 就是定義這個 hash
怎麼來的。
hash
為當初簽署的資料,定義如下:
1
| hash = keccak256("\x19\x01" + domainSeparator + hashStruct(message))
|
domainSeparator
定義如下:
1
| domainSeparator = hashStruct(eip712Domain)
|
hashStruct
為把結構化資料進行雜湊,定義如下:
1
| hashStruct(s) = keccak256(typeHash of s + encodeData(data of s))
|
進行雜湊的內容包含型別描述 typeHash
和所有資料內容。
typeHash
針對不同的結構化資料,會有不同的 typeHash
1
| typeHash = keccak256(encodeType(type of s))
|
encodeType
描述結構的字串,定義比較複雜,格式如下:
1
| 結構名稱(型別 變數名稱,型別 變數名稱2,...)
|
例如
1 2 3 4 5
| struct Mail { address from; address to; string contents; }
|
encodeType
結果為
1
| Mail(address from,address to,string contents)
|
注意
uint
和 int
必須寫成 uin256
和 int256
。- 陣列寫作
Type[n]
或 Type[]
,例如 uint256[2]
或 uint256[]
。
巢狀結構時,把其他結構名稱照字母順序排列接在後面,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct Transaction { Person from; Person to; Asset asset; }
struct Person { address wallet; string name; }
struct Asset { address token; uint256 amount; }
|
encodeType
結果為
1
| Transaction(Person from,Person to,Asset asset)Asset(address token,uint256 amount)Person(address wallet,string name)
|
Asset
排在 Person
前面。
encodeData
定義結構化資料如何編碼,整理為以下幾點:
- 每個變數都轉換為 32 bytes 長度的資料,依照後面幾點進行。
- 變數為
struct
時,遞迴呼叫 hashStruct()
轉為 bytes32
,同理 struct
陣列時,轉為 bytes32[]
。 - 變數為動態長度
string
、bytes
和陣列時,使用 abi.encodePacked
接合並進行 keccak256
雜湊。 - 其他一般型別前面補零成為 32 bytes 長度,在 Solidity 中使用
abi.encode
可達成。
上面 Mail
例子的 encodeData
:
1 2 3 4 5 6 7 8
| bytes32 internal constant TYPE_HASH = keccak256("Mail(address from,address to,string contents)");
abi.encode( TYPE_HASH, mail.from, mail.to, keccak256(abi.encodePacked(mail.content)) )
|
後面章節有更多範例。
eip712Domain
最後回到 domainSeparator
中的 eip712Domain
定義,他是另一組預定義結構化的資料,用來區分不同使用範圍的簽章,可防止重放攻擊。包含以下欄位:
1 2 3 4 5 6 7
| struct EIP712Domain { string name; string version; uint256 chainId; address verifyingContract; }
|
- name:自定的名稱。
- version:自訂的版號。
- chainId:鏈 ID,智能合約一般使用
block.chainid
取得。 - verifyingContract:驗證簽章的合約地址,智能合約中一般使用
address(this)
取得。 - salt:標準中定義了這個欄位,當如果還有碰撞的可能,可以再用這個欄位區別。不過一般沒有使用,目前實務上沒有包含這欄位。
encodeType
為
1
| EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
|
設定好 name
和 version
等資料後,將這組資料一樣丟到 hashStruct
就可以得到 domainSeparator
。
範例
完整範例
一個完整的合約範例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| contract Example { struct Mail { address from; address to; string contents; }
bytes32 public immutable domainSeparator;
bytes32 private constant EIP712DOMAIN_TYPE_HASH = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" );
bytes32 private constant MAIL_TYPE_HASH = keccak256( "Mail(address from,address to,string contents)" );
constructor() { domainSeparator = keccak256( abi.encode( EIP712DOMAIN_TYPE_HASH, keccak256("Example"), keccak256("1"), block.chainid, address(this) ) ); }
function hashStruct(Mail memory mail) public pure returns (bytes32) { return keccak256( abi.encode( MAIL_TYPE_HASH, mail.from, mail.to, keccak256(abi.encodePacked(mail.contents)) ) ); }
function toTypedDataHash(bytes32 structHash) public view returns (bytes32) { return keccak256(abi.encodePacked( "\x19\x01", domainSeparator, structHash )); }
function recover(Mail memory mail, uint8 v, bytes32 r, bytes32 s) public view returns (address) { bytes32 structHash = hashStruct(mail); bytes32 hash = toTypedDataHash(structHash); return ecrecover(hash , v, r, s); } }
|
不定義 struct
合約內不一定要真的定義 struct
,也可以直接把變數帶入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function hashStruct(address from, address to, string memory contents) public pure returns (bytes32) { return keccak256( abi.encode( MAIL_TYPEHASH, from, to, keccak256(abi.encodePacked(contents)) ) ); }
function recover(address from, address to, string memory contents, uint8 v, bytes32 r, bytes32 s) public view returns (address) { bytes32 structHash = hashStruct(from, to, contents); bytes32 hash = toTypedDataHash(structHash); return ecrecover(hash , v, r, s); }
|
巢狀結構
以上面的 Transaction
為例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| bytes32 internal constant TRANSACTION_TYPE_HASH = keccak256("Transaction(Person from,Person to,Asset asset)Asset(address token,uint256 amount)Person(address wallet,string name)"); bytes32 internal constant PERSON_TYPE_HASH = keccak256("Person(address wallet,string name)"); bytes32 internal constant ASSET_TYPE_HASH = keccak256("Asset(address token,uint256 amount)");
function hashStructOfTransaction(Transaction memory transaction) internal pure returns(bytes32) { return keccak256( abi.encode( TRANSACTION_TYPE_HASH, hashStructOfPerson(transaction.from), hashStructOfPerson(transaction.to), hashStructOfAsset(transaction.asset) ) ); }
function hashStructOfPerson(Person memory person) internal pure returns(bytes32) { return keccak256( abi.encode( PERSON_TYPE_HASH, person.wallet, keccak256(abi.encodePacked(person.name)) ) ); }
function hashStructOfAsset(Asset memory asset) internal pure returns(bytes32) { return keccak256( abi.encode( ASSET_TYPE_HASH, asset.token, asset.amount ) ); }
|
陣列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| struct Mail { address from; address[] to; string contents; }
bytes32 internal constant TYPE_HASH = keccak256("Mail(address from,address[] to,string contents)");
function hashStruct(Mail memory mail) internal pure returns(bytes32) { return keccak256( abi.encode( TYPE_HASH, mail.from, keccak256(abi.encodePacked(mail.to)), keccak256(abi.encodePacked(mail.contents)) ) ); }
|
結構陣列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| struct Mail { Person from; Person[] to; string contents; }
struct Person { address wallet; string name; }
bytes32 internal constant MAIL_TYPE_HASH = keccak256("Mail(Person from,Person[] to,string contents)Person(address wallet,string name)"); bytes32 internal constant PERSON_TYPE_HASH = keccak256("Person(address wallet,string name)");
function hashStructOfMail(Mail memory mail) internal pure returns(bytes32) { bytes32[] memory encodeData = new bytes32[](mail.to.length); for (uint i; i < mail.to.length; ++i) { encodeData[i] = hashStructOfPerson(mail.to[i]); }
return keccak256( abi.encode( MAIL_TYPE_HASH, hashStructOfPerson(mail.from), keccak256(abi.encodePacked(encodeData)), keccak256(abi.encodePacked(mail.contents)) ) ); }
function hashStructOfPerson(Person memory person) internal pure returns(bytes32) { return keccak256( abi.encode( PERSON_TYPE_HASH, person.wallet, keccak256(abi.encodePacked(person.name)) ) ); }
|
使用套件
我們也可以使用 OpenZeppelin EIP712 套件 來處理此類型驗證簽章。首先引用並繼承 EIP712,在建構子的時候定義 name
和 version
:
1 2 3 4 5 6
| import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
contract Example is EIP712 { constructor() EIP712("Example", "1") { } }
|
套件會產生 domainSeparator
存起來。自己只需要處理 hashStruct
,最後使用下面的函式生成要被檢驗的 hash
:
1
| function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32);
|
上面的範例使用套件後改寫如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
contract Example is EIP712 { struct Mail { address from; address to; string contents; }
bytes32 private constant MAIL_TYPEHASH = keccak256( "Mail(address from,address to,string contents)" );
constructor() EIP712("Example", "1") { }
function hashStruct(Mail memory mail) public pure returns (bytes32) { return keccak256( abi.encode( MAIL_TYPEHASH, mail.from, mail.to, keccak256(abi.encodePacked(mail.contents)) ) ); }
function recover(Mail memory mail, uint8 v, bytes32 r, bytes32 s) public view returns (address) { bytes32 structHash = hashStruct(mail); bytes32 hash = _hashTypedDataV4(structHash); return ecrecover(hash , v, r, s); } }
|
延伸閱讀
Solidity 智能合約 - ecrecover 簽章檢驗
使用 Web3.js 進行 EIP-712 類型結構化資料簽名
使用 Ethers.js 進行 EIP-712 類型結構化資料簽名