I want to be able to call execTransaction without gnosis infra (as a recovery measure), so I made a little signer web3.js app that takes all the parameters, hashes them exactly the way the safe contract does it in encodeTransactionData, this produces safeTxHash. I checked it against previous transactions I made through the Safe web UI, it matches.

I then use personal_sign to prompt the user to sign the hash.

                    const sign = await ethereum.request({
                        method: 'personal_sign',
                        params: [safeTxHash, from, 'Random text'],

We can then send around the signatures with my colleagues in email and once we have enough signatures, we can call execTransaction in eg. etherscan to interact with the safe without Gnosis servers.

However, calling execTransaction fails with GS026 (which means some of the signatures are bad). OK maybe I got the nonce wrong, or something, so let's try calling checkNSignatures directly to see which signature is bad. It turns out all of them are. Here's an example.

address: 0x360048390d7D967Bc634e1253fB9632290130ED7
data: 0x1901f575497ea4aae42517ffea2ac3cee51f6af01be4288b09746385e7ca8edb21900e9a43707f7f38353cc9a99de2dd46c6b41d91d425c2727e90d914785190cb7b
data hash: 0x3db1b18173909221b28b2a497995a26baef0712558a83f2755fda3799fbe93b0
signature: 0x1f48e4661e3adbb06c60c31291aa38265565ad72d87079bc07ce96cf9cbffabb7c5edefa071c108f9737930dad8085a33d0d5127c3fb8ddebc0be44c057c23711b

What drives me insane is that this is a transaction we've already signed and executed through the Safe Desktop app, and the signature matches the one that we see on the blockchain in the successful execTransaction call!!

etherscan screenshot showing signature matching the one above

Sooo... what's wrong with my signature?

  • BTW you don't need to port the safe tx hashing code from solidity to javascript: you can just call getTransactionHash on the contract. Just sign that and you're fine
    – Tech Capo
    Commented Jan 24, 2023 at 11:01

1 Answer 1


Well, the last byte is wrong. The signature consists of three parts, r, s and v, where v is a single byte signifying the version. In the above example in the one generated by the web3.js app, it's v == 0x1b which is 27. On the blockchain (in the transaction generated by Gnosis infra), the version is 0x1f, which is 31.

So clearly, you need to add four (4) to the signature. Five shalt thou not count, neither count thou three, excepting that thou then proceed to four. Six is right out.

I've realised this while simulating and debugging the checkNSignatures call in Tenderly (simu link). There's a special case if v > 30 where the contract subtracts 4 from the version and adds some boilerplate to the hash.

            } else if (v > 30) {
                // If v > 30 then default va (27,28) has been adjusted for eth_sign flow
                // To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before applying ecrecover
                currentOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s);
            } else {
                // Default is the ecrecover flow with the provided data hash
                // Use ecrecover with the messageHash for EOA signatures
                currentOwner = ecrecover(dataHash, v, r, s);
            require(currentOwner > lastOwner && owners[currentOwner] != address(0) && currentOwner != SENTINEL_OWNERS, "GS026");

If you are using personal_sign, instead eth_sign, it still seems like it appends the boilerplate under the hood. If you increment the signatures by 4 (0x1b -> 0x1f), checkNSignatures and also execTransaction works like a charm (simu link).

  • If the site allowed it, I'd give you 100 upvotes for the Python quote. Commented Dec 20, 2023 at 20:56

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.