Skip to main content

Using abi.encode() & abi.encodePacked() & keccak256()

  • abi.encode(): This is a built-in Solidity function that allows developers to pack together one or more Solidity values into a tightly packed byte array. abi.encode() automatically includes padding and metadata in the byte array to ensure that it conforms to the Solidity ABI. The packed byte array can be useful for various purposes, such as generating unique identifiers for specific objects or creating hashes of multiple variables. Developers can then use abi.decode() in external applications, such as Python scripts, to decode the packed byte array and recover the original Solidity values.

  • abi.encodePacked(): This is a built-in Solidity function that allows developers to tightly pack one or more Solidity values into a byte array without any additional padding or metadata. abi.encodePacked() leaves the values as they are without any 32-byte alignment. This is useful when the packed data needs to be hashed or sent over the network in a compact format. However, developers should be careful when using abi.encodePacked() to ensure that the resulting byte array conforms to the expected data structure.

  • keccak256(): This is a built-in Solidity function that computes the Keccak-256 hash of a given input. The Keccak-256 hash is a one-way cryptographic hash function that generates a fixed-size output (32 bytes) for any input of arbitrary size. The output is generally used as a unique identifier or fingerprint for the input data. In Ethereum, keccak256() is often used to generate unique contract addresses, as well as to verify digital signatures.

Example Usage in Solidity

EIP712.sol has usage all of them.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
* @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data.
*
* The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible,
* thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding
* they need in their contracts using a combination of `abi.encode` and `keccak256`.
*
* This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding
* scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA
* ({_hashTypedDataV4}).
*
* The implementation of the domain separator was designed to be as efficient as possible while still properly updating
* the chain id to protect against replay attacks on an eventual fork of the chain.
*
* NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method
* https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].
*
* _Available since v3.4._
*/
abstract contract EIP712 {
/* solhint-disable var-name-mixedcase */
// Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
// invalidate the cached domain separator if the chain id changes.
bytes32 public immutable _CACHED_DOMAIN_SEPARATOR;
uint256 public immutable _CACHED_CHAIN_ID;

bytes32 public immutable _HASHED_NAME;
bytes32 public immutable _HASHED_VERSION;
bytes32 public immutable _TYPE_HASH;
/* solhint-enable var-name-mixedcase */

/**
* @dev Initializes the domain separator and parameter caches.
*
* The meaning of `name` and `version` is specified in
* https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]:
*
* - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol.
* - `version`: the current major version of the signing domain.
*
* NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart
* contract upgrade].
*/
constructor(string memory name, string memory version) {
bytes32 hashedName = keccak256(bytes(name));
bytes32 hashedVersion = keccak256(bytes(version));
bytes32 typeHash = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
_HASHED_NAME = hashedName;
_HASHED_VERSION = hashedVersion;
_CACHED_CHAIN_ID = block.chainid;
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion);
_TYPE_HASH = typeHash;
}

/**
* @dev Returns the domain separator for the current chain.
*/
function _domainSeparatorV4() internal view returns (bytes32) {
if (block.chainid == _CACHED_CHAIN_ID) {
return _CACHED_DOMAIN_SEPARATOR;
} else {
return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION);
}
}

function _buildDomainSeparator(bytes32 typeHash, bytes32 name, bytes32 version) private view returns (bytes32) {
return keccak256(
abi.encode(
typeHash,
name,
version,
block.chainid,
address(this)
)
);
}

/**
* @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this
* function returns the hash of the fully encoded EIP712 message for this domain.
*
* This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:
*
* ```solidity
* bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
* keccak256("Mail(address to,string contents)"),
* mailTo,
* keccak256(bytes(mailContents))
* )));
* address signer = ECDSA.recover(digest, signature);
* ```
*/
function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), structHash));
}
}

contract TEST is EIP712 {
constructor(
string memory name,
string memory version
) EIP712(name, version) {}

function getHashTypedDataV4(bytes32 structHash) external view returns(bytes32){
return _hashTypedDataV4(structHash);
}
}

In the given contract, the _hashTypedDataV4 function calculates a hash value based on the provided parameters. The function takes two parameters, domainSeparator and structuredData, and returns a bytes32 value.

The domainSeparator parameter is a unique identifier for the domain of the contract, and is calculated by calling the _domainSeparatorV4 function. The structuredData parameter is a struct that contains several fields, each representing a specific piece of data.

To calculate the hash value, the _hashTypedDataV4 function concatenates the domainSeparator with the structuredData using the abi.encodePacked() function. The resulting byte array is then hashed using the keccak256() function, which returns a 32-byte hash value.

This process of concatenating and hashing is designed to provide a secure and tamper-resistant way to validate the authenticity of the data being passed to the contract. By combining the domainSeparator with the structured data, the contract can ensure that the data was signed by a specific entity and has not been modified since it was signed.

To calculate the _hashTypedDataV4 value in python using Brownie, you can follow these steps:

from brownie import convert
from brownie import web3


class EIP712:
_CACHED_DOMAIN_SEPARATOR = bytearray(32)
_CACHED_CHAIN_ID = 0
_HASHED_NAME = 0
_HASHED_VERSION = 0
_TYPE_HASH = 0

def __init__(self, name: bytes, version: bytes, address_of_contract: str):
self.address_of_contract = address_of_contract
hashedName = web3.sha3(name)
hashedVersion = web3.sha3(version)
typeHash = web3.sha3(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
self._HASHED_NAME = hashedName
self._HASHED_VERSION = hashedVersion
self._CACHED_CHAIN_ID = web3.chain_id
self._CACHED_DOMAIN_SEPARATOR = self._buildDomainSeparator(typeHash, hashedName, hashedVersion)
self._TYPE_HASH = typeHash

def _buildDomainSeparator(self, typeHash: bytes, name: bytes, version: bytes) -> bytes:
return web3.sha3(
convert.to_bytes(typeHash) + convert.to_bytes(name) + \
convert.to_bytes(version) + convert.to_bytes(web3.chain_id) +
convert.to_bytes(self.address_of_contract)
)

def _domainSeparatorV4(self) -> bytes:
if web3.chain_id == self._CACHED_CHAIN_ID:
return self. _CACHED_DOMAIN_SEPARATOR
return self._buildDomainSeparator(self._TYPE_HASH, self._HASHED_NAME, self._HASHED_VERSION)


def _hashTypedDataV4(self,structHash: bytes) -> bytes:
return web3.sha3(
convert.to_bytes(b'\x19\x01', type_str="bytes2") + convert.to_bytes(self._domainSeparatorV4()) + \
convert.to_bytes(structHash, type_str='bytes32')
)

If we check both variables it can be seen that result of _hashTypedDataV4 is same.

>>> testContract = TEST.deploy("some_name", "some_version", {'from': accounts[0]})
Transaction sent: 0x6195046ef40818de5f402ab049c9e6bf366402bdf4a3d1ffab2e4281d63706e8
Gas price: 0.0 gwei Gas limit: 2000000001 Nonce: 1
TEST.constructor confirmed Block: 2 Gas used: 221401 (0.01%)
TEST deployed at: 0x602C71e4DAC47a042Ee7f46E0aee17F94A3bA0B6

>>> testContract.getHashTypedDataV4(b'this_is_my_struct_hash')
0xf888f903dd71488c4ce5607b41ad4a2b523dfa29aa76c6679308dd5bc3968797
>>> eip = EIP712(b"some_name", b"some_version", testContract.address)
>>> eip._hashTypedDataV4(b'this_is_my_struct_hash')
HexBytes('0xf888f903dd71488c4ce5607b41ad4a2b523dfa29aa76c6679308dd5bc3968797')