How to enable Gnosis multi-sig sign in

This is a technical blog post by the Gallery Engineering team.

Got questions? Reach out to Kaito on twitter.

A massive breakthrough for Web3 is enabling users to have self-sovereign ownership over their logins and keys. As a result, like most Dapps, here at Gallery we use an Ethereum wallet sign in instead of the dated email+password. Naturally, we prioritized Metamask and WalletConnect support to account for the majority of users before building out support for additional wallets.

Over the last half year, as the number of DAOs have steadily increased and the influence they hold over the broader NFT ecosystem continues to rise, we understood it was critical to support DAOs as a first-class citizen, and the very first step to doing so was enabling them to natively sign in and create an account.

To do so, we started with supporting sign in with Gnosis Multi-sig. Over the past year, Gnosis has emerged as the vault of choice for the majority of DAOs due to frictionless start up, intuitive transaction handling across multiple wallets signers, and reliable security. By supporting Gnosis sign in, the majority of DAOs will be able to sign in as a user and create their gallery.

While Gnosis utilizes WalletConnect to sign in, we learned that there were nuances to the signing flow that we had to account for. During the process of building Gnosis sign-in support, we were surprised to find that there was little to no documentation around an integration like this. So we decided to write it. This is the step-by-step guide we wish we had. If you’re building an app and looking to support Gnosis Safes, buckle up and read on!

a cry for help (tweet image by poet.so)
a cry for help (tweet image by poet.so)

Outline

This post explains how to enable an app to prompt users to sign a message with a Gnosis Safe multi-sig, wait until the transaction is confirmed + executed by the required multi-sig owners, and then validate the signature.

We'll be going over:

  1. Why Gnosis Safe needs a unique solution
  2. What our signing flow looks like
  3. How to implement the flow

Note: We assume you are familiar with enabling web3 connections in your app, and that your app already supports message signing with WalletConnect. Here we are specifically integrating WalletConnect with Gnosis (our open source implementation). If you’d like more detail on other wallet connections, let us know.

Why Gnosis Safe needs a unique solution

The authentication mechanism that we originally built allowed users to connect and sign a message via WalletConnect using a wallet like Rainbow, but it didn't work when we tried it with a Gnosis Safe. Why?

Because Gnosis Safes are Contract Accounts (aka. smart contracts), which cannot sign messages in the same way as Externally Owned Accounts, or EOAs (aka. normal wallets). Wait, what are EOAs and Contract Accounts?

There are two types of Ethereum accounts: Externally Owned Accounts (EOA) and Contract Accounts.

  • EOAs have a private key used to sign messages or send transactions.
  • Contract Accounts don't have a private key and have executable code instead.

You can learn more about Ethereum accounts in their official docs.

This affected us in two key ways:

1. How the message is signed

Today, with EOAs, signing a message is off-chain and direct. The frontend prompts the user to sign a message through a provider, and the user signs with their wallet, which passes the signature back to the frontend. Easy.

However, because Gnosis Safe doesn't have a private key to sign a message with, the multi-sig owners must instead execute a transaction on-chain that marks the message as "signed". This means that the frontend must listen on-chain for this transaction to be executed by the Gnosis Safe contract to know it is ready to be validated.

2. How we validate the signed message

Because Contract Accounts cannot sign a message the way an EOA does, EIP-1271 introduced a mechanism that allows smart contracts to sign a message and have it validated. Indeed, Gnosis supports EIP-1271. In order to validate a signature from them, we must instantiate the Gnosis Safe contract and use their implementation of the isValidSignature method, which returns true if that particular Gnosis Safe has signed the provided message on-chain.

This contrasts from EOAs, where we immediately receive the signature when the user signs the message off-chain, and we validate it by deriving the public address from the signature.

What our signing flow looks like

Here is what our signing flow looks like for Gnosis Safe users:

  1. App connects to Gnosis Safe with WalletConnect and prompts the user to queue a Sign Message transaction.
  2. Gnosis Safe owners confirm and execute the transaction on-chain, which will mark our nonce message as signed. This step costs gas.
  3. App waits for the transaction to be executed by listening for a SignMsg event that the Gnosis Safe contract emits upon execution.
  4. App validates that the Safe signed the message by calling the validation method isSignatureValid on the contract.

Compared to the signing flow for EOAs (using Metamask or WalletConnect):

  1. App connects to wallet with Metamask or WalletConnect.
  2. User receives a prompt in their wallet app or extension to sign our nonce message. User accepts, and this step does not cost gas.
  3. The frontend receives the signature from the wallet app. This signature is validated.

How to implement the flow

1. Prompting a Gnosis user to sign a message with WalletConnect

First, your app must be able to allow a user to connect their wallet (or Gnosis Safe) via WalletConnect, and then prompt them to sign a message. This is the start of the signing flow and works the same for both EOAs and contract accounts.

If your app already supports message signing for EOAs via WalletConnect, you can move to the next step, because this means you can already connect to a Gnosis Safe.

If your app doesn't, there are docs online that can help you get there, or you can also look at our implementation as an example. (Happy to dive deeper here in a future post — let us know.)

2. Detecting that the Sign Message transaction was executed by Gnosis Safe

The flows for Gnosis signing and EOA signing are basically the same until the user actually reacts to the sign message prompt in their wallet.

With EOAs, the signature will immediately be available for validation once the user signs with their wallet.

However, with Gnosis Safe, since signing a message requires an on-chain transaction, we must wait until the transaction is executed before we are able to validate it.

How do we know when the transaction has been executed?

We need to listen to the Gnosis Safe contract on-chain. When the transaction is approved by the multi-sig owners and executed, the Gnosis Safe contract emits a "SignMsg" event on-chain. A listener in our app will detect this event and invoke a callback that will let us proceed once the transaction is executed, such as validating the signature.

Here is how we set up a listener for the SignMsg event. First we need to instantiate the Gnosis Safe contract in our app:

import { Contract } from '@ethersproject/contracts';
import GNOSIS_SAFE_CONTRACT_ABI from 'abis/gnosis-safe-contract.json';

// Instantiate the Gnosis Safe contract so that we can call the contract's methods, or set up listeners for it

// note on inputs:
// `address` is the contract address. ie the address of the Gnosis Safe that is trying to connect.
// `GNOSIS_SAFE_CONTRACT_ABI` is a json file that contains the contract's ABI
// `library` is a Web3Provider that is available via Web3React, like so:
// const { library } = useWeb3React();
const gnosisSafeContract = new Contract(address, GNOSIS_SAFE_CONTRACT_ABI, library);

An ABI is an interface definition that tells your app what methods and events are available on a contract. When using an ABI in a React app, you can place the definition in a .json file somewhere in your app directory, and import it. Here's an example of an ABI for Gnosis Safe that defines the SignMsg event. Often for published smart contracts, you can get the ABI via Etherscan. We grabbed the SignMsg event definition from the Gnosis Safe SignMessageLib contract on Etherscan here. (SignMessageLib is what emits the event if you look at the source code)

// partial example of ABI for Gnosis Safe contract (this is just for the SignMsg event)
[
	{
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "msgHash",
        "type": "bytes32"
      }
    ],
    "name": "SignMsg",
    "type": "event"
  }
]

Once the Gnosis Safe contract is instantiated, we can set up our listener.

We do this by using the contract.on() function (docs)


// Set up a listener for the SignMsg event
gnosisSafeContract.on('SignMsg', async (msgHash) => {
	// Stuff to do when the event is detected, like validate the signature
	console.log("SignMsg event detected")
};

3. Validating that the Gnosis Safe contract signed the message as expected

Once we've detected the SignMsg event on-chain, we must validate that our message has been signed as expected. It's possible that the detected SignMsg event was emitted while signing a different message, so we want to check that our intended message has been signed.

We validate the signature by calling the isValidSignature method on the Gnosis Safe contract, which is defined here.

It takes two arguments:

  1. The message hash
  2. The "signature" returned by the personal_sign method. This is always 0x, which denotes an empty string or an empty array of bytes.
confirmation about validation input from Mikhail, dev @gnosis via Gnosis discord
confirmation about validation input from Mikhail, dev @gnosis via Gnosis discord

The message hash is a keccak256 hash of the message, formatted as an Ethereum signed message. The example below shows how you can generate the message hash yourself using the keccak256 and toUtf8Bytes util functions from the ethersproject library.

import { keccak256 } from '@ethersproject/keccak256';
import { toUtf8Bytes } from '@ethersproject/strings';

// To turn the original message string into the message hash we need,
// format the message as a Ethereum signed message and then keccak256 hash it
const message = "This is the message that we prompted the user to sign.";
const prependedMessage = `\x19Ethereum Signed Message:\n${message.length}${message}`;
const messageHash = keccak256(toUtf8Bytes(prependedMessage));

Once you have the message hash, you can validate it on the contract:

// this magic value is the constant that is returned by the isValidSignature method if the signature has been signed.
// defined here: https://github.com/gnosis/safe-contracts/blob/2620a21c0844f23df39ea98438b82e378bb334f0/contracts/handler/CompatibilityFallbackHandler.sol
const GNOSIS_VALID_SIGNATURE_MAGIC_VALUE =  '0x1626ba7e';

// call isValidSignature to verify that the message was signed by the Gnosis Safe
const magicValue = await gnosisSafeContract.isValidSignature(messageHash, '0x');
const messageWasSigned = magicValue === GNOSIS_VALID_SIGNATURE_MAGIC_VALUE;

Putting it all together:

// prompt user to sign message via WalletConnect
const signature = await connector.walletConnectProvider.connector.signPersonalMessage([
	message,
  address,
]);

// instantiate Gnosis Safe contract
const gnosisSafeContract = new Contract(address, GNOSIS_SAFE_CONTRACT_ABI, library);

// generate message hash
const prependedMessage = `\x19Ethereum Signed Message:\n${message.length}${message}`;
const messageHash = keccak256(toUtf8Bytes(prependedMessage));

// create listener that will listen for the SignMsg event on the Gnosis contract
const listenToGnosisSafeContract = new Promise((resolve) => {
  gnosisSafeContract.on('SignMsg', async (msgHash) => {
    // Upon detecting the SignMsg event, validate that the contract signed the message
    const magicValue = await gnosisSafeContract.isValidSignature(messageHash, '0x');
    const messageWasSigned = magicValue === GNOSIS_VALID_SIGNATURE_MAGIC_VALUE;

    if (messageWasSigned) {
      resolve(msgHash);
    }
  });
});

// start listening
await listenToGnosisSafeContract;

Conclusion

This should give you all the pieces you need to prompt a user to sign a message with a Gnosis Safe, wait until the transaction has been executed, and validate that the correct message was signed as expected. This will cost the user some gas, but Gnosis may support a gasless solution in the future.

Hopefully this helps more more apps support DAOs natively! If you have questions, feel free to reach out to us on Twitter or Discord.

If you're interested in solving problems like these, come join us at Gallery – we're hiring! Check out our open roles here.

Special thanks to the folks at Gnosis, especially Mikhail Mikheev on the engineering team, for being super helpful and answering all my questions on their discord.

Subscribe to GALLERY
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.