5afe / safe-react

Deprecated! New repo – https://github.com/safe-global/web-core

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to generate offline signature? (cold storage, bank vault, hardware wallet, machine never connected to the internet)

marsrobertson opened this issue · comments

Pretty sure doable.

Pretty sure someone has done it.

Pretty sure someone wants / needs it.

It may make sense to a hardware wallet in the bank vault to never touch machine connected to the internet...

Are there any simplified instructions available?


Surely need to take care of the nonce and some other complexities on top of that...

I'm also interested in that. Is it possible to use Gnosis Safe completely offline?
It seems that connecting to the Trezor, at least, requires contacting a Trezor URL, so is the answer no?

It's currently not possible to use the Safe web/react interface completely offline due to the dependency on our backend. I agree that it would be nice to have some kind of offline signer functionality. However closing this for now since it's not on our react roadmap for now

I managed to get it working and this is how I'm currently doing it.

First you need to generate the safe transaction so you can get the hash of it, this step requires a connection

// This runs on the ONLINE machine

const web3 = require('web3');
const ethers = require('ethers').ethers
const sdk = require('@gnosis.pm/safe-core-sdk')
const Safe = sdk.default
const SENDER_PRIVATE_KEY = 'XXX'
const TO_ADDRESS = 'YYY'
const Value = web3.utils.toWei('0.01', 'ether')
const GAS_PRICE = web3.utils.toWei('20', 'Gwei')
const GAS_LIMIT = 100000
const NETWORK = 'rinkeby'

const provider = ethers.getDefaultProvider(NETWORK);        
const adapter = new sdk.EthersAdapter({
    ethers,
    signer: new ethers.Wallet(SENDER_PRIVATE_KEY, provider)
})


const safe = await Safe.create({ ethAdapter: adapter, safeAddress: SAFE_ADDRESS })

// Prepare the Tx data
const tx = {
    to: TO_ADDRESS,
    value: web3.utils.toHex(Value),
    data: '0x00',
    // baseGas: web3.utils.toHex(GAS_LIMIT),
    // gasPrice: web3.utils.toHex(GAS_PRICE),
}

// Create the safe Tx
const safeTx = await safe.createTransaction(tx)
const safeTxHash = await safe.getTransactionHash(safeTx)

safeTxHash is a string that needs to be signed and it should be transferred to the offline machine in a way however you see fit (USB key, QR code, etc).

And on the offline machine you can sign that hash in several ways, for example using a raw private key:

// This runs on the OFFLINE machine

function fixSignature(signature) {
    // https://docs.gnosis-safe.io/contracts/signatures#eth_sign-signature
    // Need to add 4 to the last byte of the signature, which is v
    let bytes = web3.utils.hexToBytes(signature);
    bytes[64] += 4
    return web3.utils.bytesToHex(bytes)
}

async function signPrivateKey(safeTxHash) {
    const OWNER_PRIVATE_KEY = "XXX"
    const signer = new ethers.Wallet(OWNER_PRIVATE_KEY)
    const address = await signer.getAddress()
    const signature = await signer.signMessage(web3.utils.hexToBytes(safeTxHash))
    return {
        signer: address,
        data: fixSignature(signature),
    }
}

The returned data from the sign function needs to be transferred back to the online machine so that it can be included in the tx as the owner signatures (repeat this for all the N required signatures)

// This runs on the ONLINE machine

const EthSignSignature = require('@gnosis.pm/safe-core-sdk/dist/src/utils/signatures/SafeSignature').EthSignSignature
const newSignature = new EthSignSignature(signature.signer, signature.data)
safeTx.addSignature(newSignature)

And once all the signatures are added the tx can be executed (on the online machine)

// This runs on the ONLINE machine

const options = {
    gasPrice: web3.utils.toHex(GAS_PRICE),
    gasLimit: web3.utils.toHex(GAS_LIMIT),         
}
const result = await safe.executeTransaction(safeTx, options)

How to sign with trezor and ledger :)

const HID_PATH = "m/44'/60'/0'/0/0"

async function signTxTrezor(safeTxHash) {
    console.log('Signing with Trezor...')
    // https://github.com/trezor/connect/blob/develop/docs/methods.md
    var TrezorConnect = require('trezor-connect').default
    
    TrezorConnect.manifest({
        email: '',
        appUrl: ''
    })
    
    console.log('Please confirm on your Trezor device...')
    result = await TrezorConnect.ethereumSignMessage({
        useEmptyPassphrase: true,
        path: HID_PATH,
        message: safeTxHash,
        hex: true
    })

    if (result.success) {        
        return {
            signer: result.payload.address,
            data: fixSignature('0x' + result.payload.signature),
        }
    } else {
        throw "TrezorConnect.ethereumSignMessage failed: " + result
    }        
}

async function signLedger(safeTxHash) {
    /*
    https://github.com/LedgerHQ/ledgerjs
    https://www.npmjs.com/package/@ledgerhq/hw-transport-node-hid
    https://www.npmjs.com/package/@ledgerhq/hw-app-eth
    */
    console.log('Signing with Ledger...')
    const Transport = require('@ledgerhq/hw-transport-node-hid').default
    const Eth  = require("@ledgerhq/hw-app-eth").default

    let transport = null
    try {
        transport = await Transport.open('')   
    } catch (e) {
        console.log('Failed to open ledger transport, is your device connected and unlocked?')
        throw e
    }

    try {
        const eth = new Eth(transport)
        let result = await eth.getAddress(HID_PATH)
        const address = result.address        
        console.log('Please confirm on your Ledger device...')
        // The '0x' prefix needs to be removed for Ledger...
        const hexString = safeTxHash.substring(2)
        result = await eth.signPersonalMessage(HID_PATH, hexString)
        
        const v = web3.utils.toHex(result.v).substring(2)
        const signature = '0x' + result.r + result.s + v
        return {
            signer: address,
            data: fixSignature(signature),
        }
    } catch (e) {
        console.log('Failed to sign message, have you switched to the Ethereum app on the device?')
        throw e
    }
}

I made a little python script for this: https://github.com/bingen/gnosis-offline-signer

I use Gnosis Safe UI to get the safe tx hash, and then I use the API, POST method multisig-transactions_confirmations_create, to publish the signature (still gasless).

Then again using Gnosis Safe UI I just execute the transaction.

@bingen How do you get the safe tx hash without being an owner?

@717a56e1 I get the safe tx from the online device, connected as one of the owners, then move it to the offline device using a QR code (you could use an USB stick, or even manually copy it, whatever feels safe for you), and then sign it there with another owner private key.