Skip to content

Friendly Metatransaction Signing #1300

@area

Description

@area

It is a truth universally acknowledged, that a single user in possession of a good fortune, must be in want of a human-readable message to sign when making metatransactions.

Currently, when signing metatransactions, we show the user an unintelligble blob:

Screenshot 2024-10-03 at 16 26 35

A sufficiently savvy user could build the transaction they're expecting to encode, hash it, and compare it to this one, but practically no-one is going to do that. It's also scary for non-technical users to sign something they don't understand. To solve both of these, we want intelligble signing messages that will look something like this:

Screenshot 2024-10-03 at 13 21 04

The way this example was achieved was with EIP712. Realistically, I don't believe there is another way - certainly not as widely supported as EIP712 which, in a regime where we are attempting to support as many different wallets as possible, seems like an essential property. Alternatives might be our own Metamask Snap in a similar vein to this one, or the Sourcify Metamask Snap in conjunction with validating our contracts - but these require the user installing extra components, and don't work outside Metamask.

I also believe that it is essential that the human-readable elements of this are directly used to execute the transaction. Including a hex blob that is used to execute the transaction in addition to human readable elements achieves nothing, in my mind. The human readable elements must be validated on chain, and either proved they are consistent with the transaction to be sent, or be used to call the function in question directly.

Per-function implementation

What is the cost of implementing this? Unfortunately, I believe quite high. Our current metatransaction implementation adds a single function callMetaTransaction which can be used to call any function on the contracts via metatransactions. It is my current belief that to support EIP712, we would need to implement a separate function for each function we want to call via metatransactions - so the interface would become something like:


  struct EIP712Signature {
    bytes32 r;
    bytes32 s;
    uint8 v;
    address signer;
    uint256 deadline;
  }


  function someFunction(uint256 arg1, address arg2) external;
  function someFunctionWithSignature(uint256 arg1, address arg2, EIP712Signature signature) external;

where here, I've used a custom data type for the signature to reduce the number of arguments that need to be added to the function call (which potentially represents a real issue for functions where we are close to the stack limit). The latter function, after confirming the signature was valid according to EIP712, would call someFunction with the arguments provided and indicating it is a metatransaction in the same way the current metatransaction implementation does, with msgSender() returning the address of the signer.

There remains a small chance that requiring an extra function is not true for the contracts - there is a possibility that our function arguments are simple enough that a functional-enough equivalent of abi.encode could be written to handle all of them. I intend to explore this possibility, but I think this is unlikely. If it were true now, it might also not be true in the future as functions are added. It will also surely be gassier than a per-function implementation.

Implementing functions on a case-by-case basis would mostly be rote work, once the first few implementations were done, but we have a lot of functions. It would also mean that new versions of the contracts, with new functions, would require more work than they do currently (as every function would need to be implemented twice).

ChainId

With the multi-chain functionality, there is a desire for users to not have to switch chains in order to sign metatransactions. On the face of it, EIP712 represents an issue on that front. From the specification of the DomainSeparator:

uint256 chainId the EIP-155 chain id. The user-agent should refuse signing if it does not match the currently active chain.

(emphasis not mine). This is the case, at least, for Metamask (I have checked). However, the specification for the domain separator also says:

... the type of eip712Domain is a struct named EIP712Domain with one or more of the below fields.

(emphasis mine). So we can, in fact, leave out the chainId field from the domain separator. This is not ideal, but it is a workaround that would allow us to use EIP712 without requiring users to switch chains. The intention behind the chainId field is to prevent replay attacks, but I believe we can instead use the salt field, set to the chainId, to achieve the same effect. This would mean Metamask would not refuse to sign the message (I have tested this to be true), but it would still (I believe) not allow cross-chain replays. This is a non-standard use of the field (and would mean that, for example, we could not use the OpenZeppelin implementation of EIP712), but I can't see a (security-related) reason why we can't do this.

Conclusion

None of the above is set in stone, it merely represents my current understanding of the situation but I thought it would be useful to get this in place. I am open to suggestions on directions to explore, but given the amount of work that I now believe this will require (including on the frontend), I thought it best to get visibility on it as early as possible.

All that being said, it is my current belief that EIP712 support represents the best way to achieve these friendly metatransactions, despite the amount of work that it would appear to represent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions