Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-6860: Web3 URL 到 EVM 调用消息的转换

一个 HTTP 样式的 Web3 URL 到 EVM 调用消息的转换

Authors Qi Zhou (@qizhou), Chao Pi (@pichaoqkc), Sam Wilson (@SamWilsn), Nicolas Deschildre (@nand2)
Created 2023-09-29
Discussion Link https://ethereum-magicians.org/t/eip-4804-web3-url-to-evm-call-message-translation/8300
Requires EIP-137

摘要

本标准将 RFC 3986 URI(如 web3://uniswap.eth/)转换为 EVM 消息,例如:

EVMMessage {
   To: 0xaabbccddee.... // 其中 uniswap.eth 的地址在 ENS 注册
   Calldata: 0x
   ...
}

⚠️ 本提案通过小的更正、澄清和修改来更新 ERC-4804

动机

目前,从 Web3 读取数据通常依赖于 Web2 代理到 Web3 区块链的转换。这种转换主要由 dApp 网站/节点服务提供商/etherscan 等代理完成,这些代理不受用户控制。这里的标准旨在为 Web2 用户提供一种直接访问 Web3 内容的简单方法,特别是链上 Web 内容,如 SVG/HTML。此外,该标准使得能够与已经兼容 URI 的其他标准(如 SVG/HTML)互操作。

规范

本规范仅定义只读(即 Solidity 的 view 函数)语义。状态修改函数可能会在未来的扩展中定义。

本规范使用 RFC 2234 的增强巴科斯-诺尔范式 (ABNF) 符号。完整的 URI 语法列于附录 A。

Web3 URL 是以下形式的 ASCII 字符串:

web3URL         = schema "://" [ userinfo "@" ] contractName [ ":" chainid ] pathQuery [ "#" fragment ]
schema          = "w3" / "web3"
userinfo        = address

userinfo 表示哪个用户正在调用 EVM,即 EVM 调用消息中的“From”字段。如果未指定,协议将使用 0x0 作为发送者地址。

contractName    = address 
                / domainName
address         = "0x" 20( HEXDIG HEXDIG )
domainName      = *( unreserved / pct-encoded / sub-delims ) ; 如 RFC 3986 中所述

contractName 表示要调用的合约,即 EVM 调用消息中的“To”字段。如果 contractName 是一个地址,那么它将被用于“To”字段。否则,contractName 是来自域名服务的域名,并且必须解析为一个地址才能用于“To”字段。

将域名从域名服务解析为地址的方法在 ERC-6821 中针对以太坊名称服务进行了指定,并且将在后续 ERC 中针对其他名称服务进行讨论。

chainid         = %x31-39 *DIGIT

chainid 表示要解析 contractName 并调用消息的链。如果未指定,协议将使用所使用的名称服务提供商的主链,例如,eth 为 1。如果没有使用名称服务提供商,则默认的 chainid 为 1。

pathQuery       = mPathQuery ; 手动模式的 path+query
                / aPathQuery ; 自动模式的 path+query

pathQuery,由路径和可选查询组成,根据解析模式是“手动”还是“自动”,将具有不同的结构。

fragment        = *VCHAR

fragment,像 HTTP URL 中一样,是一个字符字符串,用于引用资源,不会传输到智能合约。

web3UrlRef      = web3URL 
                / relativeWeb3URL
relativeWeb3URL = relPathQuery
relPathQuery    = relMPathQuery ; 手动模式的相对 URL path+query
                / relAPathQuery ; 自动模式的相对 URL path+query

支持相对 URL,但支持程度取决于解析模式。

解析模式

一旦确定了“To”地址和 chainid,协议将通过调用“To”地址的 resolveMode 方法来检查合约的解析模式。resolveMode 的 Solidity 签名是:

function resolveMode() external returns (bytes32);

该协议当前支持两种解析模式:自动和手动。

  • 如果 resolveMode 返回值是 0x6d616e75616c0000000000000000000000000000000000000000000000000000,即 bytes32 中的“manual”,将使用手动模式。
  • 如果:
    • resolveMode 返回值是 0x6175746f00000000000000000000000000000000000000000000000000000000,即 bytes32 中的“auto”,或者
    • resolveMode 返回值是 0x0000000000000000000000000000000000000000000000000000000000000000,或者
    • resolveMode 的调用抛出错误(方法未实现或方法中抛出错误) 将使用自动模式。
  • 否则,协议将使请求失败,并显示错误“不支持的解析模式”。

手动模式

mPathQuery      = mPath [ "?" mQuery ]

mPath           = mPathAbempty ; 以 "/" 开头或为空
mPathAbempty    = [ *( "/" segment ) "/" segment [ "." fileExtension ] ]
segment         = *pchar ; 如 RFC 3986 中所述
fileExtension   = 1*( ALPHA / DIGIT )

mQuery = *( pchar / "/" / "?" ) ; 如 RFC 3986 中所述

手动模式将直接使用原始的 mPathQuery 作为消息的 calldata(不会进行百分号编码解码)。如果 mPathQuery 为空,则发送的 calldata 将为 / (0x2f)。

返回的消息数据将被视为 ABI 编码的字节,解码后的字节将返回到前端。

默认情况下,返回到前端的 MIME 类型是 text/html,但如果存在 fileExtension,则会被覆盖。在这种情况下,MIME 类型将从文件名扩展名中推断出来。

relMPathQuery   = relMPath [ "?" mQuery ]
relMPath        = mPathAbsolute ; 以 "/" 开头,但不以 "//" 开头
                / mPathNoscheme ; 以非冒号段开头
                / mPathEmpty    ; 零个字符

mPathAbsolute   = "/" [ segmentNz *( "/" segment ) ] [ "." fileExtension ]
mPathNoscheme   = segmentNzNc *( "/" segment ) [ "." fileExtension ]
mPathEmpty      = 0<pchar>

segmentNz       = 1*pchar ; 如 RFC 3986 中所述
segmentNzNc     = 1*( unreserved / pct-encoded / sub-delims / "@" )
                ; 如 RFC 3986 中所述:不包含任何冒号 ":" 的非零长度段

对手动模式相对 URL 的支持与 HTTP URL 类似:允许相对于当前合约的 URL,包括绝对路径和相对路径。

自动模式

aPathQuery      = aPath [ "?" aQuery ]
aPath           = [ "/" [ method *( "/" argument ) ] ]

在自动模式下,如果 aPath 为空或为 “/”,则协议将使用空 calldata 调用目标合约。否则,EVM 消息的 calldata 将使用标准的 Solidity 合约 ABI。

method          = ( ALPHA / "$" / "_" ) *( ALPHA / DIGIT / "$" / "_" )

method 是要调用的函数方法字符串。

argument        = boolArg
                / uintArg
                / intArg
                / addressArg
                / bytesArg
                / stringArg
boolArg         = [ "bool!" ] ( "true" / "false" )
uintArg         = [ "uint" [ intSizes ] "!" ] 1*DIGIT
intArg          = "int" [ intSizes ] "!" 1*DIGIT
intSizes        = "8" / "16" / "24" / "32" / "40" / "48" / "56" / "64" / "72" / "80" / "88" / "96" / "104" / "112" / "120" / "128" / "136" / "144" / "152" / "160" / "168" / "176" / "184" / "192" / "200" / "208" / "216" / "224" / "232" / "240" / "248" / "256"
addressArg      = [ "address!" ] ( address / domainName )
bytesArg        = [ "bytes!" ] bytes
                / "bytes1!0x" 1( HEXDIG HEXDIG )
                / "bytes2!0x" 2( HEXDIG HEXDIG )
                ...
                / "bytes32!0x" 32( HEXDIG HEXDIG )
stringArg       = "string!" *pchar [ "." fileExtension ]

argument 是方法的一个参数,具有类型无关的 [ type "!" ] value 语法。如果指定了 type,则该值将被转换为相应的类型。该协议目前支持这些基本类型:bool、int、uint、int<X>、uint<X>(X 的范围从 8 到 256,步长为 8)、address、bytes<X>(X 的范围从 1 到 32)、bytes 和 string。如果未指定 type,则将按照以下顺序规则自动检测类型:

  1. type=”uint256”,如果 value 是数字;或者
  2. type=”bytes32”,如果 value 的形式为 0x+32 字节数据十六进制;或者
  3. type=”address”,如果 value 的形式为 0x+20 字节数据十六进制;或者
  4. type=”bytes”,如果 value 的形式为 0x 后跟除 20 或 32 之外的任意数量的字节;或者
  5. type=”bool”,如果 valuetruefalse;或者
  6. 否则 type=”address”,并将参数解析为域名。如果无法解析域名,将返回不支持的名称服务提供商错误。
aQuery          = attribute *( "&" attribute )
attribute       = attrName "=" attrValue
attrName        = "returns"
                / "returnTypes"
attrValue       = [ "(" [ retTypes ] ")" ]
retTypes        = retType *( "," retType )
retType         = retRawType *( "[" [ %x31-39 *DIGIT ] "]" )
retRawType      = "(" retTypes ")"
                / retBaseType
retBaseType      = "bool" / "uint" [ intSizes ] / "int" [ intSize ] / "address" / "bytes" [ bytesSizes ] / "string"
bytesSizes      = %x31-39              ; 1-9
                / ( "1" / "2" ) DIGIT  ; 10-29
                / "31" / "32"          ; 31-32

aQuery 中的 “returns” 属性指示返回数据的格式。它遵循以太坊 ABI 函数签名的参数部分的语法(授权使用 uintint 别名)。

  • 如果 “returns” 属性值未定义或为空,则返回的消息数据将被视为 ABI 编码的字节,解码后的字节将被返回到前端。默认情况下,返回到前端的 MIME 类型将是未定义的,但如果最后一个参数是字符串类型并且具有 fileExtension,则会被覆盖,在这种情况下,MIME 类型将从文件名扩展名中推断出来。(请注意,fileExtension 不会从给智能合约的字符串参数中排除)
  • 如果 “returns” 属性值等于 “()”,则返回的消息数据的原始字节将以 JSON 格式的数组中以 “0x” 开头的十六进制字符串的形式返回:["0xXXXXX"]
  • 否则,返回的消息数据将按照 returns 值中指定的数据类型进行 ABI 解码,并以 JSON 格式进行编码。数据的编码将遵循以太坊 JSON-RPC 格式:
    • 未格式化的数据(字节、地址)将编码为十六进制,以 “0x” 为前缀,每个字节两个十六进制数字
    • 数量(整数)将编码为十六进制,以 “0x” 为前缀,最紧凑的表示形式(稍微例外:零应表示为 “0x0”)
    • 布尔值和字符串将是原生 JSON 布尔值和字符串

如果存在多个 “returns” 属性,则将应用最后一个 “returns” 属性的值。请注意,”returnTypes” 是 “returns” 的别名,但不建议使用,主要用于 ERC-4804 向后兼容目的。

relAPathQuery   = aPath [ "?" aQuery ]

对自动模式相对 URL 的支持是有限的:允许相对于当前合约的 URL,并将引用自身(空)、/ 路径或完整的方法及其参数。

示例

示例 1a

web3://w3url.eth/

其中 w3url.eth 的合约处于手动模式。

协议将从链 ID 1(主网)上的 ENS 中找到 w3url.eth 的地址。然后,协议将使用 “Calldata” = keccak("resolveMode()")[0:4] = “0xDD473FAE” 调用该地址,这将以 ABI 类型 “(bytes32)” 返回 “manual”。在确定合约的手动模式后,协议将使用 “To” = contractAddress 和 “Calldata” = “0x2F” 调用该地址。返回的数据将被视为 ABI 类型 “(bytes)”,并且解码后的字节将返回到前端,并提供 MIME 类型为 text/html 的信息。

示例 1b

web3://w3url.eth/

其中 w3url.eth 的合约处于自动模式。

协议将从链 ID 1(主网)上的 ENS 中找到 w3url.eth 的地址。然后,协议将使用 “Calldata” = keccak("resolveMode()")[0:4] = “0xDD473FAE” 调用该地址,这将返回 ““,即合约处于自动模式。在确定合约的自动模式后,协议将使用 “To” = contractAddress 和 “Calldata” = “” 调用该地址。返回的数据将被视为 ABI 类型 “(bytes)”,并且解码后的字节将返回到前端,并提供 MIME 类型为未定义的信息。

示例 2

web3://cyberbrokers-meta.eth/renderBroker/9999

其中 cyberbrokers-meta.eth 的合约处于自动模式。

协议将从链 ID 1(主网)上的 ENS 中找到 cyberbrokers-meta.eth 的地址。然后,协议将使用 “Calldata” = keccak("resolveMode()")[0:4] = “0xDD473FAE” 调用该地址,这将返回 ““,即合约处于自动模式。在确定合约的自动模式后,协议将使用 “To” = contractAddress 和 “Calldata” = “0x” + keccak("renderBroker(uint256)")[0:4] + abi.encode(uint256(9999)) 调用该地址。返回的数据将被视为 ABI 类型 “(bytes)”,并且解码后的字节将返回到前端,并提供 MIME 类型为未定义的信息。

示例 3

web3://vitalikblog.eth:5/

其中 vitalikblog.eth:5 的合约处于手动模式。

协议将从链 ID 5(Goerli)上的 ENS 中找到 vitalikblog.eth 的地址。然后在确定合约处于手动模式后,协议将使用 “To” = contractAddress 和 “Calldata” = “0x2F”,且链 ID = 5 调用该地址。返回的数据将被视为 ABI 类型 “(bytes)”,并且解码后的字节将返回到前端,并提供 MIME 类型为 text/html 的信息。

示例 4

web3://0xe4ba0e245436b737468c206ab5c8f4950597ab7f:42170/

其中合约 “0xe4ba0e245436b737468c206ab5c8f4950597ab7f:42170” 处于手动模式。

在确定合约处于手动模式后,协议将使用 “To” = “0xe4ba0e245436b737468c206ab5c8f4950597ab7f” 和 “Calldata” = “0x2F”,且链 ID = 42170 (Arbitrum Nova) 调用该地址。返回的数据将被视为 ABI 类型 “(bytes)”,并且解码后的字节将返回到前端,并提供 MIME 类型为 text/html 的信息。

示例 5

web3://0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/balanceOf/vitalik.eth?returns=(uint256)

其中合约 “0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48” 处于自动模式。

协议将从链 ID 1(主网)上的 ENS 中找到 vitalik.eth 的地址,然后使用 vitalik.eth 的地址调用合约的 “balanceOf(address)” 方法。来自合约调用的返回数据将被视为 ABI 类型 “(uint256)”,并且解码后的数据将以 JSON 格式(如 [ "0x9184e72a000" ])返回到前端,并提供 MIME 类型为 application/json 的信息。

示例 6

web3://0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/balanceOf/vitalik.eth?returns=()

其中合约 “0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48” 处于自动模式。

协议将从链 ID 1(主网)上的 ENS 中找到 vitalik.eth 的地址,然后调用该地址的 “balanceOf(address)” 方法。来自合约调用的返回数据将被视为原始字节,并将以 JSON 格式(如 ["0x000000000000000000000000000000000000000000000000000009184e72a000"])进行编码,并返回到前端,并提供 MIME 类型为 application/json 的信息。

附录 A:Web3 URL 的完整 ABNF

web3URL         = schema "://" [ userinfo "@" ] contractName [ ":" chainid ] pathQuery [ "#" fragment ]
schema          = "w3" / "web3"
userinfo        = address
contractName    = address 
                / domainName
chainid         = %x31-39 *DIGIT

pathQuery       = mPathQuery ; 手动模式的 path+query
                / aPathQuery ; 自动模式的 path+query
fragment        = *VCHAR

web3UrlRef      = web3URL 
                / relativeWeb3URL
relativeWeb3URL = relPathQuery
relPathQuery    = relMPathQuery ; 手动模式的相对 URL path+query
                / relAPathQuery ; 自动模式的相对 URL path+query

mPathQuery      = mPath [ "?" mQuery ]
mPath           = mPathAbempty ; 以 "/" 开头或为空

relMPathQuery   = relMPath [ "?" mQuery ]
relMPath        = mPathAbsolute ; 以 "/" 开头,但不以 "//" 开头
                / mPathNoscheme ; 以非冒号段开头
                / mPathEmpty    ; 零个字符

mPathAbempty    = [ *( "/" segment ) "/" segment [ "." fileExtension ] ]
mPathAbsolute   = "/" [ segmentNz *( "/" segment ) ] [ "." fileExtension ]
mPathNoscheme   = segmentNzNc *( "/" segment ) [ "." fileExtension ]
mPathEmpty      = 0<pchar>

segment         = *pchar ; 如 RFC 3986 中所述
segmentNz       = 1*pchar ; 如 RFC 3986 中所述
segmentNzNc     = 1*( unreserved / pct-encoded / sub-delims / "@" )
                ; 如 RFC 3986 中所述:不包含任何冒号 ":" 的非零长度段

mQuery          = *( pchar / "/" / "?" ) ; 如 RFC 3986 中所述

aPathQuery      = aPath [ "?" aQuery ]
aPath           = [ "/" [ method *( "/" argument ) ] ]
relAPathQuery   = aPath [ "?" aQuery ]
method          = ( ALPHA / "$" / "_" ) *( ALPHA / DIGIT / "$" / "_" )
argument        = boolArg
                / uintArg
                / intArg
                / addressArg
                / bytesArg
                / stringArg
boolArg         = [ "bool!" ] ( "true" / "false" )
uintArg         = [ "uint" [ intSizes ] "!" ] 1*DIGIT
intArg          = "int" [ intSizes ] "!" 1*DIGIT
intSizes        = "8" / "16" / "24" / "32" / "40" / "48" / "56" / "64" / "72" / "80" / "88" / "96" / "104" / "112" / "120" / "128" / "136" / "144" / "152" / "160" / "168" / "176" / "184" / "192" / "200" / "208" / "216" / "224" / "232" / "240" / "248" / "256"
addressArg      = [ "address!" ] ( address / domainName )
bytesArg        = [ "bytes!" ] bytes
                / "bytes1!0x" 1( HEXDIG HEXDIG )
                / "bytes2!0x" 2( HEXDIG HEXDIG )
                / "bytes3!0x" 3( HEXDIG HEXDIG )
                / "bytes4!0x" 4( HEXDIG HEXDIG )
                / "bytes5!0x" 5( HEXDIG HEXDIG )
                / "bytes6!0x" 6( HEXDIG HEXDIG )
                / "bytes7!0x" 7( HEXDIG HEXDIG )
                / "bytes8!0x" 8( HEXDIG HEXDIG )
                / "bytes9!0x" 9( HEXDIG HEXDIG )
                / "bytes10!0x" 10( HEXDIG HEXDIG )
                / "bytes11!0x" 11( HEXDIG HEXDIG )
                / "bytes12!0x" 12( HEXDIG HEXDIG )
                / "bytes13!0x" 13( HEXDIG HEXDIG )
                / "bytes14!0x" 14( HEXDIG HEXDIG )
                / "bytes15!0x" 15( HEXDIG HEXDIG )
                / "bytes16!0x" 16( HEXDIG HEXDIG )
                / "bytes17!0x" 17( HEXDIG HEXDIG )
                / "bytes18!0x" 18( HEXDIG HEXDIG )
                / "bytes19!0x" 19( HEXDIG HEXDIG )
                / "bytes20!0x" 20( HEXDIG HEXDIG )
                / "bytes21!0x" 21( HEXDIG HEXDIG )
                / "bytes22!0x" 22( HEXDIG HEXDIG )
                / "bytes23!0x" 23( HEXDIG HEXDIG )
                / "bytes24!0x" 24( HEXDIG HEXDIG )
                / "bytes25!0x" 25( HEXDIG HEXDIG )
                / "bytes26!0x" 26( HEXDIG HEXDIG )
                / "bytes27!0x" 27( HEXDIG HEXDIG )
                / "bytes28!0x" 28( HEXDIG HEXDIG )
                / "bytes29!0x" 29( HEXDIG HEXDIG )
                / "bytes30!0x" 30( HEXDIG HEXDIG )
                / "bytes31!0x" 31( HEXDIG HEXDIG )
                / "bytes32!0x" 32( HEXDIG HEXDIG )
stringArg       = "string!" *pchar [ "." fileExtension ]

aQuery          = attribute *( "&" attribute )
attribute       = attrName "=" attrValue
attrName        = "returns"
                / "returnTypes"
attrValue       = [ "(" [ retTypes ] ")" ]
retTypes        = retType *( "," retType )
retType         = retRawType *( "[" [ %x31-39 *DIGIT ] "]" )
retRawType      = "(" retTypes ")"
                / retBaseType
retBaseType      = "bool" / "uint" [ intSizes ] / "int" [ intSize ] / "address" / "bytes" [ bytesSizes ] / "string"
bytesSizes      = %x31-39              ; 1-9
                / ( "1" / "2" ) DIGIT  ; 10-29
                / "31" / "32"          ; 31-32

domainName      = *( unreserved / pct-encoded / sub-delims ) ; 如 RFC 3986 中所述

fileExtension   = 1*( ALPHA / DIGIT )

address         = "0x" 20( HEXDIG HEXDIG )
bytes           = "0x" *( HEXDIG HEXDIG )

pchar           = unreserved / pct-encoded / sub-delims / ":" / "@" ; 如 RFC 3986 中所述

pct-encoded     = "%" HEXDIG HEXDIG ; 如 RFC 3986 中所述

unreserved      = ALPHA / DIGIT / "-" / "." / "_" / "~" ; 如 RFC 3986 中所述
sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
                / "*" / "+" / "," / ";" / "=" ; 如 RFC 3986 中所述

附录 B:与 ERC-4804 的不同之处

修正

  • 手动模式:ERC-4804 规定不对路径 [ “?” query ] 进行解释。此 ERC 指出,实际上会对路径进行解释,以确定 MIME 类型。
  • 自动模式:如果 query 中没有 returns 属性,ERC-4804 规定返回的数据被视为 ABI 编码的 bytes32。此 ERC 指出,实际上返回的数据被视为 ABI 编码的字节。

澄清

  • 正式规范:此 ERC 添加了 URL 格式的 ABNF 定义。
  • 解析模式:此 ERC 指出了有关如何确定解析模式的更多详细信息。
  • 手动模式:此 ERC 指出了如何处理 URI 百分号编码、返回数据以及如何确定 MIME 类型。
  • 自动模式:此 ERC 更详细地指出了参数值的编码,以及 returns 值的格式和处理。
  • 示例:此 ERC 为示例添加了更多详细信息。

修改

  • 协议名称:ERC-4804 提到了 ethereum-web3://eth-web3://,这些已被删除。
  • 自动模式:支持的类型:ERC-4804 仅支持 uint256、bytes32、address、bytes 和 string。此 ERC 添加了更多类型。
  • 自动模式:指定 returns 属性时,返回整数的编码:ERC-4804 在示例 5 中建议将整数编码为字符串。此 ERC 指出要遵循以太坊 JSON RPC 规范,并将整数编码为十六进制字符串,并以 “0x” 为前缀。

理由

该提案的目的是为以太坊添加一个去中心化的表示层。有了这一层,我们就能够使用人类可读的 URL 在链上渲染任何 Web 内容(包括 HTML/CSS/JPG/PNG/SVG 等),从而使 EVM 可以作为去中心化的后端。该标准的设计基于以下原则:

  • 人类可读。Web3 URL 应该像 Web2 URL (http://) 一样,易于人类识别。因此,我们支持来自名称服务的名称来代替地址,以提高可读性。此外,我们使用人类可读的方法 + 参数,而不是使用十六进制的 calldata,并将它们转换为 calldata,以提高可读性。

  • 与 HTTP-URL 标准最大程度地兼容。Web3 URL 应该与 HTTP-URL 标准兼容,包括相对路径、查询、片段、百分号编码等,以便可以轻松地将对现有 HTTP-URL 的支持(例如,通过浏览器)扩展到 Web3 URL,而只需进行最少的修改。这也意味着现有的 Web2 用户可以轻松地迁移到 Web3,而只需掌握最少的该标准知识。

  • 简单。我们没有在参数中提供显式类型,而是使用“最大似然”原则自动检测参数的类型,例如地址、bytes32 和 uint256。这可以大大缩短 URL 的长度,同时避免混淆。此外,也支持显式类型,以便在必要时消除混淆。

  • 灵活。合约可以覆盖编码规则,以便合约可以精细地控制理解用户想要定位的实际 Web 资源。

安全注意事项

未发现任何安全注意事项。

版权

CC0 下放弃版权和相关权利。

Citation

Please cite this document as:

Qi Zhou (@qizhou), Chao Pi (@pichaoqkc), Sam Wilson (@SamWilsn), Nicolas Deschildre (@nand2), "ERC-6860: Web3 URL 到 EVM 调用消息的转换 [DRAFT]," Ethereum Improvement Proposals, no. 6860, September 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6860.