Those pesky signatures. Hard to debug, hard to get right. Therefore, I have created a template repository for the next project needing
TL;DR
- Clone the repo from GitHub
npm install
to install dependenciesnpx hardhat test
to compile the contract and runs tests- 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)