Utilities
Multiple libraries and general purpose utilities included in the community version of OpenZeppelin Contracts. These are only a set of utility contracts. For the full list, check out the API Reference.
Cryptography
Validating Typed Data Signatures
For prior knowledge on how to validate signatures on-chain, check out the OpenZeppelin Contracts documentation
As opposed to validating plain-text messages, it is possible to let your users sign structured data (i.e. typed values) in a way that is still readable on their wallets. This is possible by implementing EIP712
, a standard way to encode structured data into a typed data hash.
To start validating signed typed structures, just validate the typed data hash:
// contracts/MyContractDomain.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
/// @dev Unsafe contract to demonstrate the use of EIP712 and ECDSA.
abstract contract MyContractDomain is EIP712 {
function validateSignature(
address mailTo,
string memory mailContents,
bytes memory signature
) internal view returns (address) {
bytes32 digest = _hashTypedDataV4(
keccak256(abi.encode(keccak256("Mail(address to,string contents)"), mailTo, keccak256(bytes(mailContents))))
);
return ECDSA.recover(digest, signature);
}
}
As part of the message, EIP-712 requires implementers to include a domain separator, which is a hash that includes the current smart contract address and the chain id where it’s deployed. This way, the smart contract can be sure that the structured message was signed for its specific domain, avoiding replayability of signatures in smart contracts.
Validating Nested EIP-712 Signatures
Accounts (i.e. Smart Contract Wallets or Smart Accounts) are particularly likely to be controlled by multiple signers. As such, it’s important to make sure that signatures are:
-
Only valid for the intended domain and account.
-
Validated in a way that’s readable for the end signer.
On one hand, making sure that the Account signature is only valid for an specific smart contract (i.e. an application) is difficult since it requires to validate a signature whose domain is the application but also the Account itself. For these reason, the community developed ERC-7739; a defensive rehashing mechanism that binds a signature to a single domain using a nested EIP-712 approach (i.e. an EIP-712 typed structure wrapping another).
In case your smart contract validates signatures, using ERC7739Signer
will implement the IERC1271
interface for validating smart contract signatures following the approach suggested by ERC-7739:
// contracts/ERC7739ECDSA.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC7739} from "@openzeppelin/community-contracts/utils/cryptography/ERC7739.sol";
contract ERC7739ECDSA is ERC7739 {
address private immutable _signer;
constructor(address signerAddr) EIP712("ERC7739ECDSA", "1") {
_signer = signerAddr;
}
function _rawSignatureValidation(
bytes32 hash,
bytes calldata signature
) internal view virtual override returns (bool) {
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
return _signer == recovered && err == ECDSA.RecoverError.NoError;
}
}
ERC-7913 Signature Verifiers
ERC-7913 extends the concept of signature verification to support keys that don’t have their own Ethereum address. This is particularly useful for integrating non-Ethereum cryptographic curves, hardware devices, or other identity systems into smart accounts.
The standard defines a verifier interface that can be implemented to support different types of keys. A signer is represented as a bytes
object that concatenates a verifier address and a key: verifier || key
.
ERC7913Utils
provides functions for verifying signatures using ERC-7913 compatible verifiers:
using ERC7913Utils for bytes;
function _verify(bytes memory signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
return signer.isValidSignatureNow(hash, signature);
}
The verification process works as follows:
-
If
signer.length < 20
: verification fails -
If
signer.length == 20
: verification is done using SignatureChecker -
Otherwise: verification is done using an ERC-7913 verifier.
This allows for backward compatibility with EOAs and ERC-1271 contracts while supporting new types of keys.