Human-readable descriptions for machine executable operations,
described in higher level machine readable data, so that wallets
can provide meaningful feedback to the user describing the
action the user is about to perform.
Motivation
When using an Ethereum Wallet (e.g. MetaMask, Clef, Hardware
Wallets) users must accept and authorize signing messages or
sending transactions.
Due to the complexity of Ethereum transactions, wallets are very
limitd in their ability to provide insight into the contents of
transactions user are approving; outside special-cased support
for common transactions such as ERC20 transfers, this often amounts
to asking the user to sign an opaque blob of binary data.
This EIP presents a method for dapp developers to enable a more
comfortable user experience by providing wallets with a means
to generate a better description about what the contract claims
will happen.
It does not address malicious contracts which wish to lie, it
only addresses honest contracts that want to make their user’s
life better. We believe that this is a reasonable security model,
as transaction descriptions can be audited at the same time as
contract code, allowing auditors and code reviewers to check that
transaction descriptions are accurate as part of their review.
Specification
The description string and described data are generated
simultaneously by evaluating the contract
(i.e. the describer), passing the describer inputs to the
method:
The method must be executable in a static context, (i.e. any
side effects, such as logX, sstore, etc.), including through
indirect calls may be ignored.
During evaluation, the ADDRESS (i.e. to), CALLER
(i.e. from), VALUE, and GASPRICE must be the same as the
values for the transaction being described, so that the
code generating the description can rely on them. For signing
described messages, VALUE should always be 0.
When executing the bytecode, best efforts should be made to
ensure BLOCKHASH, NUMBER, TIMESTAMP and DIFFICULTY
match the "latest" block. The COINBASE should be the zero
address.
The method may revert, in which case the signing must be aborted.
New JSON-RPC Methods
Clients which manage private keys should expose additional
methods for interacting with the related accounts.
If an user interface is not present or expected for any other
account-based operations, the description strings should be
ignored and the described data used directly.
These JSON-RPC methods will also be implemented in standard
Ethereum libraries, so the JSON-RPC description is meant more
of a canonical way to describe them.
Signing Described Messages
eth_signDescribedMessage(address,describer,describerInput)// Result: {
// description: "text/plain;Hello World",
// data: "0x...", // described data
// signature: "0x..."
// }
Compute the description string and described data by
evaluating the call to describer, with the
describerInput passed to the ABI encoded call to
eipXXXDescription(bytes). The VALUE during execution must
be 0.
If the wallet contains a user interface for accepting or
denying signing a message, it should present the description
string to the user. Optionally, a wallet may wish to
additionally provide a way to examine the described data.
If accepted, the computed described data is signed
according to EIP-191, with the version
byte of 0x00 and the version specific data of describer
address.
That is:
0x19 0x00 DESCRIBER_ADDRESS 0xDESCRIBED_DATA
The returned result includes the described data, allowing
dapps that use parameters computed in the contract to be
available.
Compute the description string and described data by
evaluating the call to the describerto, with the
describerInput passed to the ABI encoded call to
eipXXXDescription(bytes).
If the wallet contains a user interface for accepting or
denying a transaction, it should present the description string
along with fee and value information. Optionally, a wallet may
wish to additionally provide a way to further examine the
transaction.
If accepted, the transaction data is set to the computed
described data, the derived transaction is signed and sent,
and the description string and serialized signed
transaction is returned.
Compute the description string and described data by
evaluating the call to the describerto, with the
describerInput passed to the ABI encoded call to
eipXXXDescription(bytes).
If the wallet contains a user interface for accepting or
denying a transaction, it should present the description string
along with fee and value information. Optionally, a wallet may
wish to additionally provide a way to further examine the
transaction.
If accepted, the transaction data is set to the computed
described data, the derived transaction is signed (and not
sent) and the description string and serialized signed
transaction is returned.
Description Strings
A description string must begin with a mime-type followed
by a semi-colon (;). This EIP specifies only the text/plain
mime-type, but future EIPs may specify additional types to
enable more rich processing, such as text/markdown so that
addresses can be linkable within clients or to enable
multi-locale options, similar to multipart/form-data.
Rationale
Meta Description
There have been many attempts to solve this problem, many of
which attempt to examine the encoded transaction data or
message data directly.
In many cases, the information that would be necessary for a
meaningful description is not present in the final encoded
transaction data or message data.
Instead this EIP uses an indirect description of the data.
For example, the commit(bytes32) method of ENS places a
commitement hash on-chain. The hash contains the
blinded name and address; since the name is blinded, the
encoded data (i.e. the hash) no longer contains the original
values and is insufficient to access the necessary values to
be included in a description.
By instead describing the commitment indirectly (with the
original information intact: NAME, ADDRESS and SECRET) a
meaningful description can be computed (e.g. “commit to NAME for ADDRESS (with SECRET)”)
and the matching data can be computed (i.e. commit(hash(name, owner, secret))).
Entangling the Contract Address
To prevent data being signed from one contract being used
against another, the contract address is entanlged into
both the transaction (implicitly via the to field) and
in messages by the EIP-191 versions specific data.
The use of the zero address is reserved.
Alternatives
NatSpec and company are a class of more complex languages that attempt to describe the encoded data directly. Because of the language complexity they often end up being quite large requiring entire runtime environments with ample processing power and memory, as well as additional sandboxing to reduce security concerns. One goal of this is to reduce the complexity to something that could execute on hardware wallets and other simple wallets. These also describe the data directly, which in many cases (such as blinded data), cannot adequately describe the data at all
Custom Languages; due to the complexity of Ethereum transactions, any language used would require a lot of expressiveness and re-inventing the wheel. The EVM already exists (it may not be ideal), but it is there and can handle everything necessary.
Format Strings (e.g. Trustless Signing UI Protocol; format strings can only operate on the class of regular languages, which in many cases is insufficient to describe an Ethereum transaction. This was an issue quite often during early attempts at solving this problem.
The signTypedData EIP-712 has many parallels to what this EIP aims to solve
@TODO: More
Backwards Compatibility
All signatures for messages are generated using EIP-191
which had a previously compatible version byte of 0x00, so
there should be no concerns with backwards compatibility.
Test Cases
All test cases operate against the published and verified contracts:
All transaction test cases use the ropsten network (chainId: 3)
and for all unspecified properties use 0.
Example: ERC-20 transfer
Input:
Address: 0xab3045AA85cBCaBb06eD3F3FE968fA5457727270
Describer Input: 0xa9059cbb000000000000000000000000000000000000000000000000000000000000000000000000000000008ba1f109551bd432803012645ac136ddd64dba720000000000000000000000000000000000000000000000002b992b75cbeb6000
i.e. encode(
[ "bytes4", "address", "uint"],
[ SEL("transfer(address,uint256)"), "0x8ba1f109551bD432803012645Ac136ddd64DBA72", 3.14159e18 ]
)
Output:
Description: text/plain;Send 3.14159 TOKN to "ricmoose.eth" (0x8ba1f109551bD432803012645Ac136ddd64DBA72)?
Described Data: 0xa9059cbb0000000000000000000000000000000000000000000000002b992b75cbeb60000000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72
i.e. encodeWithSelector("transfer(address,uint256)", "0x8ba1f109551bD432803012645Ac136ddd64DBA72", 3.14159e18)
Signing:
Signed Transaction: 0xf8a280808094ab3045aa85cbcabb06ed3f3fe968fa545772727080b844a9059cbb0000000000000000000000000000000000000000000000002b992b75cbeb60000000000000000000000000008ba1f109551bd432803012645ac136ddd64dba7229a0f33ea492d326ac32d9b7ae203c61bf7cf0ac576fb0cf8be8e4c63dc89c90de12a06c8efb28aaf3b70c032b3bd1edfc664578c49f040cf749bb19b000da56507fb2
Example: ERC-20 approve
Input:
Address: 0xab3045AA85cBCaBb06eD3F3FE968fA5457727270
Describer Input: 0x095ea7b3000000000000000000000000000000000000000000000000000000000000000000000000000000008ba1f109551bd432803012645ac136ddd64dba720000000000000000000000000000000000000000000000002b992b75cbeb6000
i.e. encode(
[ "bytes4", "address", "uint"],
[ SEL("approve(address,uint256)"), "0x8ba1f109551bD432803012645Ac136ddd64DBA72", 3.14159e18 ]
)
Output:
Description: text/plain;Approve "ricmoose.eth" (0x8ba1f109551bD432803012645Ac136ddd64DBA72) to manage 3.14159 TOKN tokens?
Described Data: 0xa9059cbb0000000000000000000000000000000000000000000000002b992b75cbeb60000000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72
i.e. encodeWithSelector("approve(address,uint256)", "0x8ba1f109551bD432803012645Ac136ddd64DBA72", 3.14159e18)
Signing:
Signed Transaction: 0xf8a280808094ab3045aa85cbcabb06ed3f3fe968fa545772727080b844a9059cbb0000000000000000000000000000000000000000000000002b992b75cbeb60000000000000000000000000008ba1f109551bd432803012645ac136ddd64dba7229a0f33ea492d326ac32d9b7ae203c61bf7cf0ac576fb0cf8be8e4c63dc89c90de12a06c8efb28aaf3b70c032b3bd1edfc664578c49f040cf749bb19b000da56507fb2
Example: ENS commit
Input:
Address: 0xab3045AA85cBCaBb06eD3F3FE968fA5457727270
Describer Input: 0x0f0e373f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000e31f43c1d823afaa67a8c5fbb8348176d225a79e65462b0520ef7d3df61b9992ed3bea0c56ead753be7c8b3614e0ce01e4cac41b00000000000000000000000000000000000000000000000000000000000000087269636d6f6f7365000000000000000000000000000000000000000000000000
i.e. encode(
[ "bytes4", "string", "address", "bytes32"],
[ SEL("commit(string,address,bytes32)"), "ricmoose", "0xE31f43C1d823AfAA67A8C5fbB8348176d225A79e", "0x65462b0520ef7d3df61b9992ed3bea0c56ead753be7c8b3614e0ce01e4cac41b" ]
)
Output:
Description: text/plain;Commit to the ENS name "ricmoose.eth" for 0xE31f43C1d823AfAA67A8C5fbB8348176d225A79e?
Described Data: 0xf14fcbc8e4a4f2bb818545497be34c7ab30e6e87e0001df4ba82e7c8b3f224fbaf255b91
i.e. encodeWithSelector("commit(bytes32)", makeCommitment("ricmoose", "0xE31f43C1d823AfAA67A8C5fbB8348176d225A79e", "0x65462b0520ef7d3df61b9992ed3bea0c56ead753be7c8b3614e0ce01e4cac41b"))
Signing:
Signed Transaction: 0xf88180808094ab3045aa85cbcabb06ed3f3fe968fa545772727080a4f14fcbc8e4a4f2bb818545497be34c7ab30e6e87e0001df4ba82e7c8b3f224fbaf255b912aa0a62b41d1ebda584fe84cf8a05f61b429fe4ec361e13c17f30a23281106b38a8da00bcdd896fe758d8f0cfac46445a48f76f5e9fe27790d67c51412cb98a12a0844
Example: WETH mint()
Input:
Address: 0xab3045AA85cBCaBb06eD3F3FE968fA5457727270
Describer Input: 0x1249c58b00000000000000000000000000000000000000000000000000000000
i.e. encode(
[ "bytes4" ],
[ SEL("mint()") ]
)
Value: 1.23 ether
Output:
Description: text/plain;Mint 1.23 WETH (spending 1.23 ether)?
Described Data: 0x1249c58b
i.e. encodeWithSelector("mint()")
Signing:
Signed Transaction: 0xf86980808094ab3045aa85cbcabb06ed3f3fe968fa5457727270881111d67bb1bb0000841249c58b29a012df802e1394a97caab23c15c3a8c931668df4b2d6d604ca23f3f6b836d0aafca0071a2aebef6a9848616b4d618912f2003fb4babde3dba451b5246f866281a654
Reference Implementation
@TODO (consider adding it as one or more files in ../assets/eip-####/)
I will add examples in Solidity and JavaScript.
Security Considerations
Escaping Text
Wallets must be careful when displaying text provided by
contracts and proper efforts must be taken to sanitize
it, for example, be sure to consider:
HTML could be embedded to attempt to trick web-based wallets into executing code using the script tag (possibly uploading any private keys to a server)
In general, extreme care must be used when rendering HTML; consider the ENS names <span style="display:none">not-</span>ricmoo.eth or  ricmoo.eth, which if rendered without care would appear as ricmoo.eth, which it is not
Other marks which require escaping could be included, such as quotes ("), formatting (\n (new line), \f (form feed), \t (tab), any of many non-standard whitespaces), back-slassh (\)
UTF-8 has had bugs in the past which could allow arbitrary code execution and crashing renderers; consider using the UTF-8 replacement character (or something) for code-points outside common planes or common sub-sets within planes
Homoglyphs attacks
Right-to-left marks may affect rendering
Many other things, deplnding on your environment
Distinguished Signed Data
Applications implementing this EIP to sign message data should
ensure there are no collisions within the data which could
result in ambiguously signed data.
@TODO: Expand on this; compare packed data to ABI encoded data?
Enumeration
If an abort occurs during signing, the response from this call
should match the response from a declined signing request;
otherwise this could be used for enumeration attacks, etc. A
random interactive-scale delay should also be added, otherwise
a < 10ms response could be interpreted as an error.
Replayablility
Transactions contain an explicit nonce, but signed messages do
not.
For many purposes, such as signing in, a nonce could be
injected (using block.timestamp) into the data. The log in
service can verify this is a recent timestamp. The timestamp
may or may not be omitted from the description string in this
case, as it it largely useful internally only.
In general, when signing messages a nonce often makes sense to
include to prevent the same signed data from being used in the
future.
Richard Moore (@ricmoo), Nick Johnson (@arachnid), "ERC-3224: Described Data [DRAFT]," Ethereum Improvement Proposals, no. 3224, January 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3224.