EC signatures and recovery in Ethereum Smart Contracts

Example use case: A multisig wallet where only recipients pay gas

A multisig wallet is a wallet contract that requires multiple signatures before funds can be sent out from it. Parity has a multisig wallet implementation that can do N-out-of-M signatures.

Now imagine this scenario: There is a multisig wallet owned by 3 people, Alice, Bob, and Carol. All three signatures are required to send money. But here’s the twist: they want only the recipient to have to pay for gas when he/she withdraws money from the wallet, and the owners pay absolutely nothing in the process.

To achieve this, we can simply utilize ECDSA signatures. The general idea is, we require all 3 owners to sign a message saying they allow some amount of money to be sent to some address. Then the recipient of the money can submit a transaction with all 3 owners signatures to get the money.

Disclaimer: Please do not copy paste this code in production as it has never been audited, and probably has more vulnerabilities that I have identified.

The approval process

For the owners to approve a spending request, all 3 of them must sign the following message:

keccak256(addressOfTheWallet, recipientAddress, value, nonce)

Wallet address and nonce are there to prevent replaying attacks. Nonce increments every time a request has been approved or denied.

The following Javascript code (using web3 and ethereumjs-util) demonstrates the signing and approval process.

const eutil = require('ethereumjs-util')
//...
// async () => {
const message = await instance.hashPermissionMessage(recipient, web3.toWei(0.01))
const rsvAlice = eutil.fromRpcSig(web3.eth.sign(alice, message))
const rsvBob = eutil.fromRpcSig(web3.eth.sign(bob, message))
const rsvCarol = eutil.fromRpcSig(web3.eth.sign(carol, message))
const tx = await instance.sendMoney(recipient,
web3.toWei(0.01),
eutil.bufferToHex(rsvAlice.r),
eutil.bufferToHex(rsvAlice.s),
rsvAlice.v,
eutil.bufferToHex(rsvBob.r),
eutil.bufferToHex(rsvBob.s),
rsvBob.v,
eutil.bufferToHex(rsvCarol.r),
eutil.bufferToHex(rsvCarol.s),
rsvCarol.v)

The owners would send the signatures over an off-chain communication channel to the spender. The spender would provide all 3 signatures to the wallet contract to spend the money.

Once the money is spent, the count increments in the wallet, which will make the signatures no longer valid.

Defending against replay attacks

The signatures include the wallet address and the nonce. The idea of nonce is similar to what the txCount of an externally owned account is. Once the approval message has been signed, it can only be used to release the funds once.

Vetoing against the transaction

In case Alice or Bob or Carol disapproves the transaction by not signing the approval message, one of them must call skip() to increment the nonce. This is to invalidate the messages already signed by one or two of the owners.

Other risks and vulnerabilities

One drawback of the system is that the recipients must spend the money immediately, otherwise the owners of the wallet can skip() after all of them have signed the approval message, or attempt to double spend by signing an approval to send someone else money with the same nonce.


The (not very) technical details on signing using different libraries

There’s a few ways you can sign a message. Here I’m going to demonstrate how you can sign a message with

Depending on your situation, you may want to use one lib over another. Either way, the signature should work with the recover function we wrote in the contract above.

Know that anything you sign can be replayed and potentially used against you!

Web3 RPC sign

At least in geth, the signature comes prefixed with "\x19Ethereum Signed Message:\n32" when you call web3.eth.sign.

Before signing a message you should first hash it.

const eutil = require('ethereumjs-util')
const hashedMsg = web3.sha3('foobar')
const signedData = web3.eth.sign(web3.eth.accounts[0], hashedMsg)
const rsv = eutil.fromRpcSig(signedData)

signedData signed data in a hex-prefixed string. We use ethereumjs-util to easily convert it to a more usable format. The signature RSV can then be recovered in the contract.

// should return the signer address
myContract.recover(hashedMsg,
eutil.bufferToHex(rsv.r),
eutil.bufferToHex(rsv.s),
rsv.v)

ethereumjs-util ecsign

Signing can be done entirely in ethereumjs-util without web3, if the private key is known.

const eutil = require('ethereumjs-util')
const hashedMsg = eutil.sha3('foobar')
const personalMsg = eutil.hashPersonalMessage(hashedMsg)
const rsv = eutil.ecsign(personalMsg, privKey)

As you can see, we sign the original message, then hash it with the personal message prefix, before calling ecsign.

eth-lightwallet sign message

We don’t know the private key, but luckily eth-lightwallet provides a signMsgHash function we can use. Hash the message, the hash it again with the prefix, before calling it.

const wallet = require('eth-lightwallet')
const eutil = require('ethereumjs-util')
const hashedMsg = eutil.sha3('foobar')
const personalMsg = eutil.hashPersonalMessage(hashedMsg)
const rsv = wallet.signing.signMsgHash(keystore, pwDerivedKey, personalMsg, address)