This is the second article illustrating hashing and signing typed structured data, the first article discussed key generation and message hashing and signing..
It is the procedure for hashing and signing typed structured data as opposed to strings, it encodes function data similar to & compatible with solidity structs, and it depends on two other EIPs:
0x01
which was briefly mentioned in the previous article. Unlike the two other versions, this structured data version doesn't start with 0x19
because it is indeed a RLP-structure, but it is still in-compliant with EIP-191 by including the ‘version byte` fixed to 0x01
If block.number ≥ FORK_BLKNUM & Chain Id is available, then computing the transaction hash should hash nine RLP encoded parameters. If you do, then the v of the signature MUST include chain Id as shown in table.
This proposal aims to improve the usability of off-chain message signing for use on-chain. We are seeing growing adoption of off-chain message signing as it saves gas and reduces the number of transactions on the blockchain.
Q: What is the acceptable typed structured data?
It accepts data like it does with struct in solidity where the struct has a name as an identifier that contains zero or several member variables; each member variable has a type and a name. The type can be either an atomic type, a dynamic type, or a reference type as shown in table below:
Basically we're defining the struct that will be used in any transaction & specify their types, let's take ERC20Permit as an example & walk through how the data is structured:
/**
* `Permit` is the struct identifier name.
* - `address` is the atomic type of named `owner`.
* - `uint256` is the atomic type of named `spender`.
* - `uint256` is the atomic type of named `value`.
* - `uint256` is the atomic type of the randomly
* generated value during the signing process named `nonce`.
* - `uint256` is the atomic type of named `deadline`
*/
struct Permit {
address owner;
address spender;
uint256 value;
uint256 nonce;
uint256 deadline;
}
So the identifier name of the struct, type of each element and its value are what the proposal use to build its components which are:
1- Hash Struct
— from it's name; it is hashing the struct.
-hashing the struct consists of hashing two components:
hashStruct = keccak256(typeHash ‖ encodeData(s))
typeHash
:
- is the process of hashing the encoded type of struct where each member is written as type & name.
- is a constant for a given struct type & doesn't need to be runtime computed.
typeHash = keccak256(encodeType(typeOf(s)))
/**
* Hashing the struct name, where each memeber has a type & name in
* a constant variable `_PERMIT_TYPEHASH` which is fixed at compile-time
* - `Permit` struct identifier name.
* - `address` is the type of `owner` name.
* - `address` is the type of `spender` name.
* - `uint256` is the type of `value` name.
* - `uint256` is the type of `nonce` name.
* - `uint256` is the type of `deadline` name.
*/
bytes32 private constant _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
encodeData
:
- is the process of encoding the struct instance.
- it concatenates all encoded member values in the order they appear in the type, where each member value is 32 bytes long.// `permit` function which instantiate `Permit` struct.
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual override {
require(block.timestamp <= deadline, "ERC20Permit: expired deadline");
/**
* encode the type of struct Hash `_PERMIT_TYPEHASH'
* enode the struct instance in the order they appear as follows:
* - `owner`, `spender`, `value`, `nonce`, & `deadline`
* finally hash the set of structurced typed data containing all
* the instances of all the strcut types
*/
bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, v, r, s);
require(signer == owner, "ERC20Permit: invalid signature");
_approve(owner, spender, value);
}
Q: How different types are encoded in the EVM?
2- Domain Separator
domainSperator = hashStruct(eip712Domain)
eip712Domain
is a struct named EIP712Domain.
struct EIP712Domain {
string name; // name of the DApp
string version; // current version of the signing domain
uint256 chainId; // EIP-155 chain id
address verifyingContract; // address of contract verifying signature
bytes32 salt; // domain separtor
}
This is already build and coded in EIP712.sol from OZ so if you are inheriting the contract you don't need to do anything, but to follow the example:
First:
we will hash all type values in struct and make it constant to be computed at compile-time as follows:
bytes32 private constant _TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
Note: be careful with the order of the types to be exactly the same as struct & no spaces after each comma.
Second:
In your contract constructor you will input your DApp or Protocol name and version, but under the hood the following is happening:
keccak256(bytes(name))
keccak256(bytes(version))
function domainSeparator() public view returns(bytes32) {
return keccak256(abi.encode(_TYPE_HASH, hashedName, hashedVersion, block.chainid, address(this)))
Customize Signing Domain Example:
In case you want to customize the EIP-712 and code it yourself, as an example we will have the name of the protocol as a constant since it will not change during it's life time:
// optional
string public constant NAME = 'dapp name';
bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = keccak256(EIP712Domain(string version,uint256 chainId,address verifyingContract));
/**
* domainSeparator = hashStruct(eip712Domain).
* - `DOMAIN_SEPARATOR_TYPEHASH`: keccack256 hashing `typeHash`
* - `keccak256(bytes(version))`: Dynamic data type hashing of string
* - `block.chainid` & `address(this)`: Atomic data type hashing of uint256 & address.
*/
function domainSeparator() public view returns(bytes32) {
return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, keccak256(bytes(version)), block.chainid, address(this)))
}
Signing Typed Structured Data
eth_signTypedData
is similar to eth_sign
method with 2 parameters:
Typed data is a JSON object that contains type information, domain separator, and the message object which will be displayed to signer as:
which will returns a 65 bytes signature as in eth_sign
with the same parameters of r & s each equals 32 bytes and v of 1 byte that includes the chain id.
If the signing address is an Externally Owned Account (EOA) then ecrecover
is used to verify that the public key matches the key that generates the signature. Further restrictions can be specified like the signer is the owner of the contract as in ERC20Permit.
That will lead us to the next article that will answer the question, what if the signing account isn't an EOA but another smart contract.