3

I am not entirely sure how signing and signature verification is done with Gnosis. Scouring stackoverflow, Gnosis documentation and discord has left me more confused than anything. I am trying to make my login support Gnosis Safe via Walletconnect

1. Signing:

I've done the following:

// Front-end Signing method:

  let signature
  try {
    signature = await provider.request({ method: 'personal_sign', params: [message, wallet], jsonrpc: '2.0' })
  } catch (e) {
    // User refused to sign.
    console.error(e)
    signature = null
  }

On login, the user copies the QRcode and pastes it into WalletConnect app on Gnosis' website.

Now at this point we note that Gnosis Safe asks the user to sign our message and that upon signing the message personal_sign will return 0x.

In any case this is normal.

Now we want to verify the signature and this is were the trouble begins.

2. Verifying the signature at the backend:

// Function in backend checking if the signature is valid:

const { message, signature, wallet } = req.body

// Signature is `0x` therefore we likely are trying to login using a multiSig.
  if (signature == '0x' ) {
    let msgHash = ethers.utils.hashMessage(message)

    const magicValue = '0x1626ba7e'

    const contract = gnosisSafeContract(wallet, provider) // function that returns a ethers.Contract instance

    const _signature = ?????????

    const value = await contract.isValidSignature(msgHash,_signature)
   
    return value == magicValue
  }

At this point I realise my question is almost a duplicate of this stackoverflow post but the "solution" isn't really a solution.

Three questions:

  • How does one obtain the signature to then use isValidSignature?
  • Some people say to use contract.signMessage to generate a transaction rather than personal_sign but signMessage will just revert given we don't have a signer yet. How does the front end know to call metamask or x provider since the user isn't logged in yet?
  • Even if one were to use signMessage, the signMessage function doesn't return a signature.

I deployed a safe on rinkeby and it is the one I've been using to figure this out. https://rinkeby.etherscan.io/address/0x8390Ac0155FFC8a373F0Fd725cc15CD0841ED91B#code

I definitely need help.

2 Answers 2

4

tl;dr

Just use 0x as signature. The signature is valid if Gnosis Safe returns the EIP-1271 magic value (0x1626ba7e).

if (signature == '0x') {
   let msgHash = ethers.utils.hashMessage(message);
   const magicValue = '0x1626ba7e';
   const contract = gnosisSafeContract(wallet, provider);
   const _signature = '0x';
   const response = await contract.isValidSignature(msgHash, _signature);

   // check if response equals EIP-1271 magic value
   if(response == magicValue) {
      ...
   }
}

Details

When signing a message, Gnosis Safe only returns 0x as signature. As explained here, the reason is that Gnosis is a smart contract wallet that cannot generate ECDSA signatures.

Internally, Gnosis Safe stores the message hash in a map that contains all signed messages.

When isValidSignature(msgHash,_signature) is called, Gnosis Safe checks if the map contains the msgHash. If true, it returns the magic value.

Regarding your second question, Gnosis Safe emits the following event after signing:

event SignMsg(bytes32 indexed msgHash);

You may listen to that event to know when the message has been signed.

1

Ok sweet, thanks for the above. We decided to go with the event listening option.

(using a nodejs express backend)

export default async function (req: any, res: any) {
  let { message, signature, wallet } = req.body

if (signature === '0x') {
// generate gnosis contract instance (using new ethers.Contract(...) )
    const wsSafeProxyContract = gnosisProxyContract(wallet)
    if (!wsSafeProxyContract) {
      res.json({ success: false })
      return
    }
    const msgHash = ethers.utils.hashMessage(message)
    const getMessageHash = await wsSafeProxyContract.getMessageHash(msgHash)

    const waitForSignedEvent = new Promise<boolean>((myResolve, myReject) => {
      const onMultiSigSigned = () => {
        myResolve(true)
      }
      setTimeout(() => { wsSafeProxyContract.removeListener(wsSafeProxyContract.filters.SignMsg(getMessageHash), onMultiSigSigned)
        myReject(false)
      }, 5 * 60 * 60 * 1000)
      // login() only after the _signMessage() txn is mined and SignMsg(msgHash) event emitted
      wsSafeProxyContract.once(wsSafeProxyContract.filters.SignMsg(getMessageHash), onMultiSigSigned)
    })

    waitForSignedEvent
      .then(async (value) => {
        if (value) {
          const { token, name, isNewUser } = await getUserInfo(res, wallet, options)
          res.json({ success: true, token, name, isNewUser })
        }
      })
      .catch((err) => {
        console.error(err)
        res.json({ success: false })
      })
} else {
// signature either undefined or the usual
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.