Retrieval of EIP-712 domain
Abstract
This EIP complements EIP-712 by standardizing how contracts should publish the fields and values that describe their domain. This enables applications to retrieve this description and generate appropriate domain separators in a general way, and thus integrate EIP-712 signatures securely and scalably.
Motivation
EIP-712 is a signature scheme for complex structured messages. In order to avoid replay attacks and mitigate phishing, the scheme includes a “domain separator” that makes the resulting signature unique to a specific domain (e.g., a specific contract) and allows user-agents to inform end users the details of what is being signed and how it may be used. A domain is defined by a data structure with fields from a predefined set, all of which are optional, or from extensions. Notably, EIP-712 does not specify any way for contracts to publish which of these fields they use or with what values. This has likely limited adoption of EIP-712, as it is not possible to develop general integrations, and instead applications find that they need to build custom support for each EIP-712 domain. A prime example of this is EIP-2612 (permit), which has not been widely adopted by applications even though it is understood to be a valuable improvement to the user experience. The present EIP defines an interface that can be used by applications to retrieve a definition of the domain that a contract uses to verify EIP-712 signatures.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Compliant contracts MUST define eip712Domain
exactly as declared below. All specified values MUST be returned even if they are not used, to ensure proper decoding on the client side.
function eip712Domain() external view returns (
bytes1 fields,
string name,
string version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] extensions
);
The return values of this function MUST describe the domain separator that is used for verification of EIP-712 signatures in the contract. They describe both the form of the EIP712Domain
struct (i.e., which of the optional fields and extensions are present) and the value of each field, as follows.
fields
: A bit map where biti
is set to 1 if and only if domain fieldi
is present (0 ≤ i ≤ 4
). Bits are read from least significant to most significant, and fields are indexed in the order that is specified by EIP-712, identical to the order in which they are listed in the function type.name
,version
,chainId
,verifyingContract
,salt
: The value of the corresponding field inEIP712Domain
, if present according tofields
. If the field is not present, the value is unspecified. The semantics of each field is defined in EIP-712.extensions
: A list of EIP numbers, each of which MUST refer to an EIP that extends EIP-712 with new domain fields, along with a method to obtain the value for those fields, and potentially conditions for inclusion. The value offields
does not affect their inclusion.
The return values of this function (equivalently, its EIP-712 domain) MAY change throughout the lifetime of a contract, but changes SHOULD NOT be frequent. The chainId
field, if used, SHOULD change to mirror the EIP-155 id of the underlying chain. Contracts MAY emit the event EIP712DomainChanged
defined below to signal that the domain could have changed.
event EIP712DomainChanged();
Rationale
A notable application of EIP-712 signatures is found in EIP-2612 (permit), which specifies a DOMAIN_SEPARATOR
function that returns a bytes32
value (the actual domain separator, i.e., the result of hashStruct(eip712Domain)
). This value does not suffice for the purposes of integrating with EIP-712, as the RPC methods defined there receive an object describing the domain and not just the separator in hash form. Note that this is not a flaw of the RPC methods, it is indeed part of the security proposition that the domain should be validated and informed to the user as part of the signing process. On its own, a hash does not allow this to be implemented, given it is opaque. The present EIP fills this gap in both EIP-712 and EIP-2612.
Extensions are described by their EIP numbers because EIP-712 states: “Future extensions to this standard can add new fields […] new fields should be proposed through the EIP process.”
Backwards Compatibility
This is an optional extension to EIP-712 that does not introduce backwards compatibility issues.
Upgradeable contracts that make use of EIP-712 signatures MAY be upgraded to implement this EIP.
User-agents or applications that use this EIP SHOULD additionally support those contracts that due to their immutability cannot be upgraded to implement it. The simplest way to achieve this is to hardcode common domains based on contract address and chain id. However, it is also possible to implement a more general solution by guessing possible domains based on a few common patterns using the available information, and selecting the one whose hash matches a DOMAIN_SEPARATOR
or domainSeparator
function in the contract.
Reference Implementation
Solidity Example
pragma solidity 0.8.0;
contract EIP712VerifyingContract {
function eip712Domain() external view returns (
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
) {
return (
hex"0d", // 01101
"Example",
"",
block.chainid,
address(this),
bytes32(0),
new uint256[](0)
);
}
}
This contract’s domain only uses the fields name
, chainId
, and verifyingContract
, therefore the fields
value is 01101
, or 0d
in hexadecimal.
Assuming this contract is on Ethereum mainnet and its address is 0x0000000000000000000000000000000000000001, the domain it describes is:
{
name: "Example",
chainId: 1,
verifyingContract: "0x0000000000000000000000000000000000000001"
}
JavaScript
A domain object can be constructed based on the return values of an eip712Domain()
invocation.
/** Retrieves the EIP-712 domain of a contract using EIP-5267 without extensions. */
async function getDomain(contract) {
const { fields, name, version, chainId, verifyingContract, salt, extensions } =
await contract.eip712Domain();
if (extensions.length > 0) {
throw Error("Extensions not implemented");
}
return buildBasicDomain(fields, name, version, chainId, verifyingContract, salt);
}
const fieldNames = ['name', 'version', 'chainId', 'verifyingContract', 'salt'];
/** Builds a domain object without extensions based on the return values of `eip712Domain()`. */
function buildBasicDomain(fields, name, version, chainId, verifyingContract, salt) {
const domain = { name, version, chainId, verifyingContract, salt };
for (const [i, field] of fieldNames.entries()) {
if (!(fields & (1 << i))) {
delete domain[field];
}
}
return domain;
}
Extensions
Suppose EIP-XYZ defines a new field subdomain
of type bytes32
and a function getSubdomain()
to retrieve its value.
The function getDomain
from above would be extended as follows.
/** Retrieves the EIP-712 domain of a contract using EIP-5267 with support for EIP-XYZ. */
async function getDomain(contract) {
const { fields, name, version, chainId, verifyingContract, salt, extensions } =
await contract.eip712Domain();
const domain = buildBasicDomain(fields, name, version, chainId, verifyingContract, salt);
for (const n of extensions) {
if (n === XYZ) {
domain.subdomain = await contract.getSubdomain();
} else {
throw Error(`EIP-${n} extension not implemented`);
}
}
return domain;
}
Additionally, the type of the EIP712Domain
struct needs to be extended with the subdomain
field. This is left out of scope of this reference implementation.
Security Considerations
While this EIP allows a contract to specify a verifyingContract
other than itself, as well as a chainId
other than that of the current chain, user-agents and applications should in general validate that these do match the contract and chain before requesting any user signatures for the domain. This may not always be a valid assumption.
Copyright
Copyright and related rights waived via CC0.