Those pesky signatures. Hard to debug, hard to get right. Therefore, I have created a template repository for the next project needing

TL;DR

  1. Clone the repo from GitHub
  2. npm install to install dependencies
  3. npx hardhat test to compile the contract and runs tests
  4. Read through the tests to udnerstand how to fit this to your use case

EIP-712 Signature and Solidity

Full source available on GitHub

As a demo we EIP-712 wrap a function with the following signature:

someFunc(address sender, address receiver, uint256 amount)

The core function is implemented as the _someFunc which is private to ensure that is is not being called directly.

The wrapper is a function with the same initial signature, and the a whole lot of fields more:

function someFuncGasless(  address sender,  address recevier,  uint256 amount,  uint256 deadline,  uint8 v,  bytes32 r,  bytes32 s) external { ... }

Note the deadline, and v, r, s. These are the protocol specific values needed to securely accept the function invokation from a third party signer. The nonce is verified implicitly via the signature.

// Check deadlinerequire(deadline >= block.timestamp, "EIP712: Expired");

We implicitly use the sender and the one we expect signed the message.

Next happens that packing, hashing and signing. This is the tricky part of the protocol.

bytes32 digest = keccak256(    abi.encodePacked(        "\x19\x01",        DOMAIN_SEPARATOR,        keccak256(            abi.encode(                SOME_FUNC_TYPEHASH,                sender,                keccak256(abi.encodePacked(receivers)),                amount,                deadline,                nonces[sender]++            )        )    ));

This is an example of a non-nested structure. It is a good starting point to incremenatally add more complext datatypes to it. For full reference see the EIP-712 specification.

Also note the nonces[sender]++. This is the nonce check that inline increments the nonce to ensure replay attacks are not possible.

Last thing to do is to check the signature and call the function body.

address recoveredAddress = ecrecover(digest, v, r, s);require(    recoveredAddress != address(0) && recoveredAddress == sender,    "EIP712: Invalid signature");_someFunc(sender, receivers, amount);

And that's it!

Crafting an EIP-712 Signature Client Side

A number of implementations of this is available in the tests that are available on GitHub.

The core is to generate a signature either using a direct ETH RPC call to the walllet

const signature = await ethers.provider.send("eth_signTypedData_v4", [message.sender, data])

or by calling the experimental _signTypedData

const signature = await otherAccount._signTypedData(data.domain, data.types, data.message)

Note: These functinos have different signatures. In particular, _signTypedData does not expect to have the tyoe of the domain passed.

The signature is split into it's parts for cheaper processing on the EVM side.

const r = signature.substring(0, 66);const s = "0x" + signature.substring(66, 130);const v = parseInt(signature.substring(130, 132), 16);

And lastly the contract is being called by

gaslessContract.someFuncGasless(message.sender, message.receivers, message.amount, message.deadline, v, r, s)