Alert Source Discuss
Standards Track: ERC

ERC-5267: EIP-712 域的检索

一种描述和检索 EIP-712 域以安全集成 EIP-712 签名的方法。

Authors Francisco Giordano (@frangio)
Created 2022-07-14
Requires EIP-155, EIP-712, EIP-2612

摘要

此 EIP 通过标准化合约应如何发布描述其域的字段和值来补充 EIP-712。这使应用程序能够以通用方式检索此描述并生成适当的域分隔符,从而安全且可扩展地集成 EIP-712 签名。

动机

EIP-712 是用于复杂结构化消息的签名方案。为了避免重放攻击并减轻网络钓鱼,该方案包括一个“域分隔符”,该分隔符使生成的签名对于特定域(例如,特定合约)是唯一的,并允许用户代理告知最终用户正在签名的内容以及其用法的详细信息。域由具有来自预定义集合的字段的数据结构定义,所有字段都是可选的,或者来自扩展。值得注意的是,EIP-712 没有指定任何合约发布它们使用哪些字段或使用哪些值的方式。这可能限制了 EIP-712 的采用,因为无法开发通用集成,相反,应用程序发现他们需要为每个 EIP-712 域构建自定义支持。一个典型的例子是 EIP-2612 (permit),即使它被认为是用户体验的一个有价值的改进,但尚未被应用程序广泛采用。本 EIP 定义了一个接口,应用程序可以使用该接口来检索合约用于验证 EIP-712 签名的域的定义。

规范

本文档中的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY”和“OPTIONAL”应按照 RFC 2119 中的描述进行解释。

兼容的合约必须完全按照如下声明定义 eip712Domain。即使未使用所有指定值也必须返回,以确保客户端的正确解码。

function eip712Domain() external view returns (
    bytes1 fields,
    string name,
    string version,
    uint256 chainId,
    address verifyingContract,
    bytes32 salt,
    uint256[] extensions
);

此函数的返回值必须描述用于验证合约中 EIP-712 签名的域分隔符。它们描述了 EIP712Domain 结构的形式(即,存在哪些可选字段和扩展)以及每个字段的值,如下所示。

  • fields:一个位图,当且仅当域字段 i 存在时,位 i 设置为 1 (0 ≤ i ≤ 4)。位从最低有效位到最高有效位读取,字段按照 EIP-712 指定的顺序索引,与它们在函数类型中列出的顺序相同。
  • nameversionchainIdverifyingContractsaltEIP712Domain 中相应字段的值(如果根据 fields 存在)。如果该字段不存在,则该值未指定。每个字段的语义在 EIP-712 中定义。
  • extensions:EIP 编号列表,每个编号必须引用一个使用新域字段扩展 EIP-712 的 EIP,以及获取这些字段值的方法,以及潜在的包含条件。fields 的值不影响它们的包含。

此函数的返回值(等效地,其 EIP-712 域)可能会在合约的整个生命周期中发生变化,但变化不应频繁。chainId 字段(如果使用)应更改以反映底层链的 EIP-155 id。合约可以发出下面定义的事件 EIP712DomainChanged,以表明域可能已更改。

event EIP712DomainChanged();

理由

EIP-712 签名的一个值得注意的应用是在 EIP-2612 (permit) 中找到的,它指定了一个返回 bytes32 值的 DOMAIN_SEPARATOR 函数(实际的域分隔符,即 hashStruct(eip712Domain) 的结果)。此值不足以与 EIP-712 集成,因为那里定义的 RPC 方法接收描述域的对象,而不仅仅是哈希形式的分隔符。请注意,这不是 RPC 方法的缺陷,它实际上是安全命题的一部分,即域应该作为签名过程的一部分进行验证并告知用户。就其本身而言,哈希不允许实现这一点,因为它是不透明的。本 EIP 填补了 EIP-712 和 EIP-2612 中的这一空白。

扩展由其 EIP 编号描述,因为 EIP-712 声明:“对此标准的未来扩展可以添加新字段 […] 应通过 EIP 流程提出新字段。”

向后兼容性

这是 EIP-712 的一个可选扩展,不会引入向后兼容性问题。

使用 EIP-712 签名的可升级合约可以升级以实现此 EIP。

使用此 EIP 的用户代理或应用程序还应支持那些由于其不变性而无法升级以实现它的合约。实现这一点的最简单方法是根据合约地址和链 id 硬编码公共域。但是,也可以通过使用可用信息猜测基于一些常见模式的可能域,并 selecting 合约中哈希与 DOMAIN_SEPARATORdomainSeparator 函数匹配的域来实现更通用的解决方案。

参考实现

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)
      );
  }
}

此合约的域仅使用字段 namechainIdverifyingContract,因此 fields 值为 01101,或十六进制的 0d

假设此合约位于以太坊主网上,并且其地址为 0x0000000000000000000000000000000000000001,它描述的域是:

{
  name: "Example",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000001"
}

JavaScript

可以根据 eip712Domain() 调用的返回值构造域对象。

/** 使用 EIP-5267 检索不带扩展的合约的 EIP-712 域。 */
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'];

/** 基于 `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;
}

扩展

假设 EIP-XYZ 定义了一个类型为 bytes32 的新字段 subdomain 和一个函数 getSubdomain() 来检索其值。

上面的函数 getDomain 将按如下方式扩展。

/** 检索使用 EIP-5267 的合约的 EIP-712 域,并支持 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;
}

此外,EIP712Domain 结构的类型需要使用 subdomain 字段进行扩展。这不在本参考实现的范围内。

安全考虑

虽然此 EIP 允许合约指定一个不是它自身的 verifyingContract,以及一个不是当前链的 chainId,但用户代理和应用程序通常应验证这些是否与在请求任何用户签名域之前,与合约和链匹配。这可能并不总是一个有效的假设。

版权

版权及相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Francisco Giordano (@frangio), "ERC-5267: EIP-712 域的检索," Ethereum Improvement Proposals, no. 5267, July 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5267.