176 lines
9.0 KiB
Markdown
176 lines
9.0 KiB
Markdown
---
|
|
eip: 5267
|
|
title: Retrieval of EIP-712 domain
|
|
description: A way to describe and retrieve an EIP-712 domain to securely integrate EIP-712 signatures.
|
|
author: Francisco Giordano (@frangio)
|
|
discussions-to: https://ethereum-magicians.org/t/eip-5267-retrieval-of-eip-712-domain/9951
|
|
status: Final
|
|
type: Standards Track
|
|
category: ERC
|
|
created: 2022-07-14
|
|
requires: 155, 712, 2612
|
|
---
|
|
|
|
## Abstract
|
|
|
|
This EIP complements [EIP-712](./eip-712.md) 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](./eip-2612.md) (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.
|
|
|
|
```solidity
|
|
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 bit `i` is set to 1 if and only if domain field `i` 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 in `EIP712Domain`, if present according to `fields`. 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 of `fields` 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](./eip-155.md) id of the underlying chain. Contracts MAY emit the event `EIP712DomainChanged` defined below to signal that the domain could have changed.
|
|
|
|
```solidity
|
|
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
|
|
|
|
```solidity
|
|
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:
|
|
|
|
```json5
|
|
{
|
|
name: "Example",
|
|
chainId: 1,
|
|
verifyingContract: "0x0000000000000000000000000000000000000001"
|
|
}
|
|
```
|
|
|
|
### JavaScript
|
|
|
|
A domain object can be constructed based on the return values of an `eip712Domain()` invocation.
|
|
|
|
```javascript
|
|
/** 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.
|
|
|
|
```javascript
|
|
/** 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](../LICENSE.md).
|