ERC-6860 - Web3 URL to EVM Call Message Translation

Created 2023-09-29
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

This standard translates an RFC 3986 URI like web3://uniswap.eth/ to an EVM message such as:

EVMMessage {
   To: 0xaabbccddee.... // where uniswap.eth's address registered at ENS
   Calldata: 0x
   ...
}

⚠️ This proposal updates ERC-4804 with minor corrections, clarifications and modifications.

Motivation

Currently, reading data from Web3 generally relies on a translation done by a Web2 proxy to Web3 blockchain. The translation is mostly done by the proxies such as dApp websites/node service provider/etherscan, which are out of the control of users. The standard here aims to provide a simple way for Web2 users to directly access the content of Web3, especially on-chain Web contents such as SVG/HTML. Moreover, this standard enables interoperability with other standards already compatible with URIs, like SVG/HTML.

Specification

This specification only defines read-only (i.e. Solidity's view functions) semantics. State modifying functions may be defined as a future extension.

This specification uses the Augmented Backus-Naur Form (ABNF) notation of RFC 2234. The complete URI syntax is listed in Appendix A.

A Web3 URL is an ASCII string in the following form :

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

userinfo indicates which user is calling the EVM, i.e., "From" field in EVM call message. If not specified, the protocol will use 0x0 as the sender address.

contractName    = address 
                / domainName
address         = "0x" 20( HEXDIG HEXDIG )
domainName      = *( unreserved / pct-encoded / sub-delims ) ; As in RFC 3986

contractName indicates the contract to be called, i.e., the "To" field in the EVM call message. If the contractName is an address then it will be used for the "To" field. Otherwise, contractName is a domain name from a domain name service, and it must be resolved to an address to use for the "To" field.

The way to resolve the domain name from a domain name service to an address is specified in ERC-6821 for the Ethereum Name service, and will be discussed in later ERCs for other name services.

chainid         = %x31-39 *DIGIT

chainid indicates which chain to resolve contractName and call the message. If not specified, the protocol will use the primary chain of the name service provider used, e.g., 1 for eth. If no name service provider was used, the default chainid is 1.

pathQuery       = mPathQuery ; path+query for manual mode
                / aPathQuery ; path+query for auto mode

pathQuery, made of the path and optional query, will have a different structure whether the resolve mode is "manual" or "auto".

fragment        = *VCHAR

fragment, like in HTTP URLs, is a string of characters meant to refer to a resource, and is not transmitted to the smart contract.

web3UrlRef      = web3URL 
                / relativeWeb3URL
relativeWeb3URL = relPathQuery
relPathQuery    = relMPathQuery ; Relative URL path+query for manual mode
                / relAPathQuery ; Relative URL path+query for auto mode

Relative URLs are supported, but the support differs based on the resolve mode.

Resolve Mode

Once the "To" address and chainid are determined, the protocol will check the resolver mode of contract by calling the resolveMode method of the "To" address. The Solidity signature of resolveMode is:

function resolveMode() external returns (bytes32);

The protocol currently supports two resolve modes: auto and manual.

Manual Mode

mPathQuery      = mPath [ "?" mQuery ]

mPath           = mPathAbempty ; begins with "/" or is empty
mPathAbempty    = [ *( "/" segment ) "/" segment [ "." fileExtension ] ]
segment         = *pchar ; as in RFC 3986
fileExtension   = 1*( ALPHA / DIGIT )

mQuery = *( pchar / "/" / "?" ) ; as in RFC 3986

The manual mode will use the raw mPathQuery as calldata of the message directly (no percent-encoding decoding will be done). If mPathQuery is empty, the sent calldata will be / (0x2f).

The returned message data will be treated as ABI-encoded bytes and the decoded bytes will be returned to the frontend.

The MIME type returned to the frontend is text/html by default, but will be overriden if a fileExtension is present. In this case, the MIME type will be deduced from the filename extension.

relMPathQuery   = relMPath [ "?" mQuery ]
relMPath        = mPathAbsolute ; begins with "/" but not "//"
                / mPathNoscheme ; begins with a non-colon segment
                / mPathEmpty    ; zero characters

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

segmentNz       = 1*pchar ; as in RFC 3986
segmentNzNc     = 1*( unreserved / pct-encoded / sub-delims / "@" )
                ; as in RFC 3986: non-zero-length segment without any colon ":"

Support for manual mode relative URLs is similar to HTTP URLs : URLs relative to the current contract are allowed, both with an absolute path and a relative path.

Auto Mode

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

In the auto mode, if aPath is empty or "/", then the protocol will call the target contract with empty calldata. Otherwise, the calldata of the EVM message will use standard Solidity contract ABI.

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

method is a string of the function method to be called

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 is an argument of the method with a type-agnostic syntax of [ type "!" ] value. If type is specified, the value will be translated to the corresponding type. The protocol currently supports these basic types: bool, int, uint, int<X>, uint<X> (with X ranging from 8 to 256 in steps of 8), address, bytes<X> (with X ranging from 1 to 32), bytes, and string. If type is not specified, then the type will be automatically detected using the following rule in a sequential way:

  1. type="uint256", if value is digits; or
  2. type="bytes32", if value is in the form of 0x+32-byte-data hex; or
  3. type="address", if value is in the form of 0x+20-byte-data hex; or
  4. type="bytes", if value is in the form of 0x followed by any number of bytes besides 20 or 32; or
  5. type="bool", if value is either true or false; or
  6. else type="address" and parse the argument as a domain name. If unable to resolve the domain name, an unsupported name service provider error will be returned.
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

The "returns" attribute in aQuery tells the format of the returned data. It follows the syntax of the arguments part of the ethereum ABI function signature (uint and int aliases are authorized).

If multiple "returns" attributes are present, the value of the last "returns" attribute will be applied. Note that "returnTypes" is the alias of "returns", but it is not recommended to use and is mainly for ERC-4804 backward-compatible purpose.

relAPathQuery   = aPath [ "?" aQuery ]

Support for auto mode relative URLs is limited : URLs relative to the current contract are allowed and will either reference itself (empty), the / path or a full method and its arguments.

Examples

Example 1a

web3://w3url.eth/

where the contract of w3url.eth is in manual mode.

The protocol will find the address of w3url.eth from ENS in chainid 1 (Mainnet). Then the protocol will call the address with "Calldata" = keccak("resolveMode()")[0:4] = "0xDD473FAE", which returns "manual" in ABI-type "(bytes32)". After determining the manual mode of the contract, the protocol will call the address with "To" = contractAddress and "Calldata" = "0x2F". The returned data will be treated as ABI-type "(bytes)", and the decoded bytes will be returned to the frontend, with the information that the MIME type is text/html.

Example 1b

web3://w3url.eth/

where the contract of w3url.eth is in auto mode.

The protocol will find the address of w3url.eth from ENS in chainid 1 (Mainnet). Then the protocol will call the address with "Calldata" = keccak("resolveMode()")[0:4] = "0xDD473FAE", which returns "", i.e., the contract is in auto mode. After determining the auto mode of the contract, the protocol will call the address with "To" = contractAddress and "Calldata" = "". The returned data will be treated as ABI-type "(bytes)", and the decoded bytes will be returned to the frontend, with the information that the MIME type is undefined.

Example 2

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

where the contract of cyberbrokers-meta.eth is in auto mode.

The protocol will find the address of cyberbrokers-meta.eth from ENS on chainid 1 (Mainnet). Then the protocol will call the address with "Calldata" = keccak("resolveMode()")[0:4] = "0xDD473FAE", which returns "", i.e., the contract is in auto mode. After determining the auto mode of the contract, the protocol will call the address with "To" = contractAddress and "Calldata" = "0x" + keccak("renderBroker(uint256)")[0:4] + abi.encode(uint256(9999)). The returned data will be treated as ABI-type "(bytes)", and the decoded bytes will be returned to the frontend, with the information that the MIME type is undefined.

Example 3

web3://vitalikblog.eth:5/

where the contract of vitalikblog.eth:5 is in manual mode.

The protocol will find the address of vitalikblog.eth from ENS on chainid 5 (Goerli). Then after determing the contract is in manual mode, the protocol will call the address with "To" = contractAddress and "Calldata" = "0x2F" with chainid = 5. The returned data will be treated as ABI-type "(bytes)", and the decoded bytes will be returned to the frontend, with the information that the MIME type is text/html.

Example 4

web3://0xe4ba0e245436b737468c206ab5c8f4950597ab7f:42170/

where the contract "0xe4ba0e245436b737468c206ab5c8f4950597ab7f:42170" is in manual mode.

After determing the contract is in manual mode, the protocol will call the address with "To" = "0xe4ba0e245436b737468c206ab5c8f4950597ab7f" and "Calldata" = "0x2F" with chainid = 42170 (Arbitrum Nova). The returned data will be treated as ABI-type "(bytes)", and the decoded bytes will be returned to the frontend, with the information that the MIME type is text/html.

Example 5

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

where the contract "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" is in auto mode.

The protocol will find the addresses of vitalik.eth from ENS on chainid 1 (Mainnet) and then call the method "balanceOf(address)" of the contract with the vitalik.eth's address. The returned data from the call of the contract will be treated as ABI-type "(uint256)", and the decoded data will be returned to the frontend in JSON format like [ "0x9184e72a000" ], with the information that the MIME type is application/json.

Example 6

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

where the contract ”0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48“ is in auto mode.

The protocol will find the address of vitalik.eth from ENS on chainid 1 (Mainnet) and then call the method "balanceOf(address)" of the address. The returned data from the call of the contract will be treated as raw bytes and will be encoded in JSON format like ["0x000000000000000000000000000000000000000000000000000009184e72a000"] and returned to the frontend, with the information that the MIME type is application/json.

Appendix A: Complete ABNF for Web3 URLs

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

pathQuery       = mPathQuery ; path+query for manual mode
                / aPathQuery ; path+query for auto mode
fragment        = *VCHAR

web3UrlRef      = web3URL 
                / relativeWeb3URL
relativeWeb3URL = relPathQuery
relPathQuery    = relMPathQuery ; Relative URL path+query for manual mode
                / relAPathQuery ; Relative URL path+query for auto mode

mPathQuery      = mPath [ "?" mQuery ]
mPath           = mPathAbempty ; begins with "/" or is empty

relMPathQuery   = relMPath [ "?" mQuery ]
relMPath        = mPathAbsolute ; begins with "/" but not "//"
                / mPathNoscheme ; begins with a non-colon segment
                / mPathEmpty    ; zero characters

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

segment         = *pchar ; as in RFC 3986
segmentNz       = 1*pchar ; as in RFC 3986
segmentNzNc     = 1*( unreserved / pct-encoded / sub-delims / "@" )
                ; as in RFC 3986: non-zero-length segment without any colon ":"

mQuery          = *( pchar / "/" / "?" ) ; as in 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 ) ; As in RFC 3986

fileExtension   = 1*( ALPHA / DIGIT )

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

pchar           = unreserved / pct-encoded / sub-delims / ":" / "@" ; As in RFC 3986

pct-encoded     = "%" HEXDIG HEXDIG ; As in RFC 3986

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

Appendix B: Changes versus ERC-4804

Corrections

Clarifications

Modifications

Rationale

The purpose of the proposal is to add a decentralized presentation layer for Ethereum. With the layer, we are able to render any web content (including HTML/CSS/JPG/PNG/SVG, etc) on-chain using human-readable URLs, and thus EVM can be served as a decentralized backend. The design of the standard is based on the following principles:

Security Considerations

No security considerations were found.

Copyright

Copyright and related rights waived via CC0.