Wallet information standardization
- Wallet information standardization
- Abstract
- Motivation
- Specification
- Rationale
- Backwards Compatibility
- Security Considerations
- Acknowledgements
- Futher Considerations
- Copyright
Abstract
This ERC aims to do two things.
- Set out a standard of information that wallets offer
For EIP-712 signed data, wallets should always offer to show the EIP-712 Digest
.
For transactions that include calldata, wallets should always offer to show the Calldata Digest
.
- Define resulting digests laid out in EIP-712
Namely:
- Domain Hash:
domainSeparator = hashStruct(eip712Domain)
- Message Hash:
hashStruct(message)
- EIP-712 Digest:
encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)
Motivation
Verifying data on hardware devices is challenging. With the recent hacks of Bybit ($1.4B), WazirX ($200M), and Radiant Capital ($50M), we saw how important it is to verify what you’re signing on your device, because websites can be hacked and are often hacked, so we should not trust them to send the correct data to our wallets. We must rely on our wallets exclusively to show the correct data.
For EIP-712 data, if the device shows the entire struct, you have to either rely on a device to extract the struct to verify it on another device, or you have to review each character with your eyes. For small structs, this is ok, and even good, as it was the direct motivation behind EIP-712, however, for large amounts of data, this task is incredibly difficult.
For “normal” transaction calldata, we can decode the calldata to make it more human readable; however, developers often “pack” data so that it cannot be decoded, but it will save gas.
And finally, when it comes to signing smart contract wallet transactions, the terminology can get quite convoluted. A user is expected to sign a safeMessage
which is different from the result of the encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)
computation set out in EIP-712. However, they also have a SafeMessage hash
which is different from the SafeMessage
, even though the SafeMessage
is also a hash, but very different from the SafeMessage Hash
. So if the result of the encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)
computation set out in EIP-712 had a name, we could talk about it easier, and have something less confusing than SafeMessage
which is a hash, and SafeMessage hash
, which is also a hash, just not the SafeMessage hash
.
Examples of different terminology:
- Openzeppelin calls it the “typed data hash” implicitly
- Safe{Wallet} calls it either the
safeTxHash
,SafeMessageHash, safeMessageMessage
or other depending on context
Specification
Definitions
EIP-712 Digest
The result of the encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)
computation set out in EIP-712 will be henceforth referred to as the “EIP-712 digest”.
Domain Hash
EIP-712 outlines the domainSeparator
as domainSeparator = hashStruct(eip712Domain)
. Many wallets and UIs have started calling this the domain hash
. This ERC outlines to formalize this name.
Message Hash
EIP-712 does not formalize a name for the resulting digest from the hashStruct(message)
calculation. Many wallets and UIs have started calling this the mesage hash
. This ERC outlines to formalize this name.
Calldata Digest
This is a new term for a hash of the calldata sent in a transaction, defined as such:
calldataDigest = keccak256(len(calldata) ‖ calldata)
Python implementation
from eth_hash.auto import keccak
import binascii
def compute_calldata_digest(calldata):
if isinstance(calldata, str) and calldata.startswith("0x"):
calldata = binascii.unhexlify(calldata[2:])
length = len(calldata)
length_bytes = length.to_bytes(32, byteorder="big")
combined = length_bytes + calldata
return "0x" + keccak(combined).hex()
Rationale
Let’s say I want to do the following:
- Approve my ERC20 token to be deposited into Aave with the
approve
function - Deposit my ERC20 into Aave using the
supply
function - Using a batch transaction with my Safe{Wallet} smart contract wallet
When I go to sign my EIP-712 typed data, my Metamask (or other software wallet) looks like so:
The data
section is populated with the calldata associated with the batch transactions. As I am on a computer, it is not hard to copy paste the calldata (and the entire SafeTx
message data) and verify its correctness.
However, let’s look at what this looks like on several different hardware wallets. What you are about to see is a single screen from each hardware wallet that is showing the EIP-712 struct.
Gridplus, with 3 pages of data similar to this:
Trezor, with 8 pages of data similar to this:
Ledger, with too many pages of data that Ledger just “stops” (this is mitigated by the fact that they show the domain & message hash, though. More on that soon)
Users are then expected to do one of the following:
- Eyeball the data in the EIP-712 struct and calldata
- Use another device to pull the calldata off these devices
Doing number 1 is a recipe for disaster, as a single digit of calldata can easily be missed, and could be the difference between success and disaster. Number 2 seems like the wrong answer, since we are now forcing wallets to be more directly connected to external sources.
What we propose instead: EIP-712 signatures
Instead, for this EIP-712 data, we could show the digest at the bottom (as of today, Ledger almost does this, it shows the Message Hash & Domain Hash, which can be combined to show the EIP-712 digest).
Ledger (as of today) shows the Domain & Message hash, instead (or in addition) to these two, we would show the EIP-712 Digest.
A user can then calculate the EIP-712 hash themselves and compare it to what they see on their wallet.
What we propose instead: Transactions
For transactions, we propose the calldata digest is placed on the bottom of the wallet.
What we propose instead: Smart Contract Wallets
Finally, smart contract wallets like Safe{Wallet} could use consistent terminology. Instead of safeTxHash
they can use EIP-712 digest
. Instead of SafeMessage
and SafeMessage Hash
, they can use SafeMessage
(unchanged) and EIP-712 digest
, which seems much less confusing.
Example workflow
- User initializes transaction or EIP-712 signature
- User can either:
- Walk through entire struct/calldata to make sure it is correct
- Use tools such as safe-hash, safeutils, safe-tx-hashes-utils, swiss-knife, or other to calculate
calldata digest
oreip-712 digest
, and compare digest on wallet - Use a software wallet combined with their hardware wallet to tell them the resulting digests
- User can feel confident they are signing what they want to sign
Backwards Compatibility
None of this EIP/ERC has any effect on the core of Ethereum. This is also merely additive, so I do not suspect it should have an impact on any backward compatibility.
Security Considerations
Hackers will know that for large calldatas and EIP-712 structs, users will rely on the calldata and EIP-712 digests for signing their transacitons, and may wish to mine a digest with different calldata that matches what a user may expect. At this time, we consider this computationally infeasible.
We intentionally didn’t include a chainId
, deadline
, etc in the calldata digest, because we think users should be able to use the same calldata digest for the same calldata, no matter the chain, time, etc. But I’d love others to weigh in on if they think a different hash would be more appropriate.
For users who do choose to “hook up” their software wallets (ie, Metamask, Rabby) to their hardware wallets, they will likely start to rely on the software wallet to correctly show this new digest. This would put increased pressure on software wallets to be secure. Instead of trusting the website, a user is trusting their software wallet or EIP-712 digest/calldata digest calculation tool.
Acknowledgements
The argent team in the past did something similar, where they would hash a new account address, convert that into emojis, and show that to the user. This way, non-technical users could have some assurance they were looking at the correct new address, as the emojis could/should match.
Futher Considerations
There are some additional improvements we could make to this.
- Add emoji digests instead of hex digests
This is an ERC aimed at making verifying calldata and EIP-712 structs easier. It might be worth considering showing emojis instead of hex data. Users less familiar with hex data may have an easier time thinking “I’m looking for duck, flag, tree, frog” than “I’m looking for 0xe1e5c20c6a7a236391fa479108b2d621fc216932efd6945d57b963ff218f0ef0”.
- Add a new EIP-191 type
For further gas savings, we could turn the EIP-712 digest and calldata digest into new EIP-191 types, this way, smart contract developers could have users sign off on calldata without having to store the entire calldata on-chain or in a transaction. At the moment, this seems a bit unimportant, and potentially even more confusing, because an EIP-712 signature could easily implement this without creating a new type.
- MCP AI with calldata decoding
It could be interesting to give an AI model the ability to decode calldata, and explain a transaction in English, but today, there seem to be a lot of issues with that. Like privacy, what if the AI messes up, etc. But, there is precedent and even MVP’s of what this could like, for example Pranesh’s MCP with foundry example.
Copyright
Copyright and related rights waived via CC0.