Merge pull request #66 from gnosis/development

PoC: Use EIP-712 signedTypedData in Web3's HttpProvideras signer with PersonalEdition contracts
This commit is contained in:
Adolfo Panizo 2018-09-06 16:50:10 +02:00 committed by GitHub
commit f355ae1b26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 286 additions and 6 deletions

View File

@ -1,10 +1,16 @@
// @flow // @flow
import { TX_SERVICE_HOST, ENABLED_TX_SERVICE_MODULES, ENABLED_TX_SERVICE_REMOVAL_SENDER } from '~/config/names' import {
TX_SERVICE_HOST,
ENABLED_TX_SERVICE_MODULES,
ENABLED_TX_SERVICE_REMOVAL_SENDER,
SIGNATURES_VIA_METAMASK,
} from '~/config/names'
const devConfig = { const devConfig = {
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/', [TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
[ENABLED_TX_SERVICE_MODULES]: false, [ENABLED_TX_SERVICE_MODULES]: false,
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false, [ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
[SIGNATURES_VIA_METAMASK]: false,
} }
export default devConfig export default devConfig

View File

@ -1,6 +1,11 @@
// @flow // @flow
import { ensureOnce } from '~/utils/singleton' import { ensureOnce } from '~/utils/singleton'
import { TX_SERVICE_HOST, ENABLED_TX_SERVICE_MODULES, ENABLED_TX_SERVICE_REMOVAL_SENDER } from '~/config/names' import {
TX_SERVICE_HOST,
ENABLED_TX_SERVICE_MODULES,
ENABLED_TX_SERVICE_REMOVAL_SENDER,
SIGNATURES_VIA_METAMASK,
} from '~/config/names'
import devConfig from './development' import devConfig from './development'
import testConfig from './testing' import testConfig from './testing'
import prodConfig from './production' import prodConfig from './production'
@ -38,3 +43,9 @@ export const allowedRemoveSenderInTxHistoryService = () => {
return config[ENABLED_TX_SERVICE_REMOVAL_SENDER] return config[ENABLED_TX_SERVICE_REMOVAL_SENDER]
} }
export const signaturesViaMetamask = () => {
const config = getConfig()
return config[SIGNATURES_VIA_METAMASK]
}

View File

@ -3,3 +3,4 @@
export const TX_SERVICE_HOST = 'tsh' export const TX_SERVICE_HOST = 'tsh'
export const ENABLED_TX_SERVICE_MODULES = 'tsm' export const ENABLED_TX_SERVICE_MODULES = 'tsm'
export const ENABLED_TX_SERVICE_REMOVAL_SENDER = 'trs' export const ENABLED_TX_SERVICE_REMOVAL_SENDER = 'trs'
export const SIGNATURES_VIA_METAMASK = 'svm'

View File

@ -1,10 +1,16 @@
// @flow // @flow
import { TX_SERVICE_HOST, ENABLED_TX_SERVICE_MODULES, ENABLED_TX_SERVICE_REMOVAL_SENDER } from '~/config/names' import {
TX_SERVICE_HOST,
ENABLED_TX_SERVICE_MODULES,
ENABLED_TX_SERVICE_REMOVAL_SENDER,
SIGNATURES_VIA_METAMASK,
} from '~/config/names'
const prodConfig = { const prodConfig = {
[TX_SERVICE_HOST]: 'https://safe-transaction-history.dev.gnosisdev.com/api/v1/', [TX_SERVICE_HOST]: 'https://safe-transaction-history.dev.gnosisdev.com/api/v1/',
[ENABLED_TX_SERVICE_MODULES]: false, [ENABLED_TX_SERVICE_MODULES]: false,
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false, [ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
[SIGNATURES_VIA_METAMASK]: false,
} }
export default prodConfig export default prodConfig

View File

@ -1,10 +1,16 @@
// @flow // @flow
import { TX_SERVICE_HOST, ENABLED_TX_SERVICE_MODULES, ENABLED_TX_SERVICE_REMOVAL_SENDER } from '~/config/names' import {
TX_SERVICE_HOST,
ENABLED_TX_SERVICE_MODULES,
ENABLED_TX_SERVICE_REMOVAL_SENDER,
SIGNATURES_VIA_METAMASK,
} from '~/config/names'
const testConfig = { const testConfig = {
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/', [TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
[ENABLED_TX_SERVICE_MODULES]: false, [ENABLED_TX_SERVICE_MODULES]: false,
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false, [ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
[SIGNATURES_VIA_METAMASK]: false,
} }
export default testConfig export default testConfig

View File

@ -4,10 +4,12 @@ import { ensureOnce } from '~/utils/singleton'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { promisify } from '~/utils/promisify' import { promisify } from '~/utils/promisify'
import GnosisSafeSol from '#/GnosisSafeTeamEdition.json' import GnosisSafeSol from '#/GnosisSafeTeamEdition.json'
import GnosisPersonalSafeSol from '#/GnosisSafePersonalEdition.json'
import ProxyFactorySol from '#/ProxyFactory.json' import ProxyFactorySol from '#/ProxyFactory.json'
import CreateAndAddModules from '#/CreateAndAddModules.json' import CreateAndAddModules from '#/CreateAndAddModules.json'
import DailyLimitModule from '#/DailyLimitModule.json' import DailyLimitModule from '#/DailyLimitModule.json'
import { calculateGasOf, calculateGasPrice, EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { calculateGasOf, calculateGasPrice, EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { signaturesViaMetamask } from '~/config'
let proxyFactoryMaster let proxyFactoryMaster
let createAndAddModuleMaster let createAndAddModuleMaster
@ -30,8 +32,14 @@ function createAndAddModulesData(dataArray) {
return dataArray.reduce((acc, data) => acc + mw.setup.getData(data).substr(74), EMPTY_DATA) return dataArray.reduce((acc, data) => acc + mw.setup.getData(data).substr(74), EMPTY_DATA)
} }
const createGnosisSafeContract = (web3: any) => { const createGnosisSafeContract = (web3: any) => {
if (signaturesViaMetamask()) {
const gnosisSafe = contract(GnosisPersonalSafeSol)
gnosisSafe.setProvider(web3.currentProvider)
return gnosisSafe
}
const gnosisSafe = contract(GnosisSafeSol) const gnosisSafe = contract(GnosisSafeSol)
gnosisSafe.setProvider(web3.currentProvider) gnosisSafe.setProvider(web3.currentProvider)

View File

@ -3,6 +3,9 @@ import { calculateGasOf, checkReceiptStatus, calculateGasPrice } from '~/logic/w
import { type Operation, submitOperation } from '~/logic/safe/safeTxHistory' import { type Operation, submitOperation } from '~/logic/safe/safeTxHistory'
import { getDailyLimitModuleFrom } from '~/logic/contracts/dailyLimitContracts' import { getDailyLimitModuleFrom } from '~/logic/contracts/dailyLimitContracts'
import { getSafeEthereumInstance } from '~/logic/safe/safeFrontendOperations' import { getSafeEthereumInstance } from '~/logic/safe/safeFrontendOperations'
import { generateMetamaskSignature, generateTxGasEstimateFrom, estimateDataGas } from '~/logic/safe/safeTxSigner'
import { storeSignature, getSignaturesFrom } from '~/utils/localStorage/signatures'
import { signaturesViaMetamask } from '~/config'
export const approveTransaction = async ( export const approveTransaction = async (
safeAddress: string, safeAddress: string,
@ -15,6 +18,14 @@ export const approveTransaction = async (
) => { ) => {
const gasPrice = await calculateGasPrice() const gasPrice = await calculateGasPrice()
if (signaturesViaMetamask()) {
const safe = await getSafeEthereumInstance(safeAddress)
const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
const signature =
await generateMetamaskSignature(safe, safeAddress, sender, to, valueInWei, nonce, data, operation, txGasEstimate)
storeSignature(safeAddress, nonce, signature)
}
const gnosisSafe = await getSafeEthereumInstance(safeAddress) const gnosisSafe = await getSafeEthereumInstance(safeAddress)
const txData = gnosisSafe.contract.approveTransactionWithParameters.getData(to, valueInWei, data, operation, nonce) const txData = gnosisSafe.contract.approveTransactionWithParameters.getData(to, valueInWei, data, operation, nonce)
const gas = await calculateGasOf(txData, sender, safeAddress) const gas = await calculateGasOf(txData, sender, safeAddress)
@ -39,6 +50,40 @@ export const executeTransaction = async (
) => { ) => {
const gasPrice = await calculateGasPrice() const gasPrice = await calculateGasPrice()
if (signaturesViaMetamask()) {
const safe = await getSafeEthereumInstance(safeAddress)
const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
const signature =
await generateMetamaskSignature(safe, safeAddress, sender, to, valueInWei, nonce, data, operation, txGasEstimate)
storeSignature(safeAddress, nonce, signature)
const sigs = getSignaturesFrom(safeAddress, nonce)
const threshold = await safe.getThreshold()
const gas = await estimateDataGas(safe, to, valueInWei, data, operation, txGasEstimate, 0, nonce, Number(threshold))
const numOwners = await safe.getOwners()
const gasIncludingRemovingStoreUpfront = gas + txGasEstimate + (numOwners.length * 15000)
const txReceipt = await safe.execTransactionAndPaySubmitter(
to,
valueInWei,
data,
operation,
txGasEstimate,
0,
0,
0,
sigs,
{ from: sender, gas: gasIncludingRemovingStoreUpfront, gasPrice },
)
const txHash = txReceipt.tx
await checkReceiptStatus(txHash)
// await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'execution')
return txHash
}
const gnosisSafe = await getSafeEthereumInstance(safeAddress) const gnosisSafe = await getSafeEthereumInstance(safeAddress)
const txConfirmationData = const txConfirmationData =
gnosisSafe.contract.execTransactionIfApproved.getData(to, valueInWei, data, operation, nonce) gnosisSafe.contract.execTransactionIfApproved.getData(to, valueInWei, data, operation, nonce)

View File

@ -0,0 +1,161 @@
// @flow
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { promisify } from '~/utils/promisify'
import { BigNumber } from 'bignumber.js'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getSignaturesFrom } from '~/utils/localStorage/signatures'
const estimateDataGasCosts = (data) => {
const reducer = (accumulator, currentValue) => {
if (currentValue === EMPTY_DATA) {
return accumulator + 0
}
if (currentValue === '00') {
return accumulator + 4
}
return accumulator + 68
}
return data.match(/.{2}/g).reduce(reducer, 0)
}
export const estimateDataGas = (
safe: any,
to: string,
valueInWei: number,
data: string,
operation: number,
txGasEstimate: number,
gasToken: number,
nonce: number,
signatureCount: number,
) => {
// numbers < 256 are 192 -> 31 * 4 + 68
// numbers < 65k are 256 -> 30 * 4 + 2 * 68
// For signature array length and dataGasEstimate we already calculated
// the 0 bytes so we just add 64 for each non-zero byte
const gasPrice = 0 // no need to get refund when we submit txs to metamask
const signatureCost = signatureCount * (68 + 2176 + 2176) // array count (3 -> r, s, v) * signature count
const sigs = getSignaturesFrom(safe.address, nonce)
const payload = safe.contract.execTransactionAndPaySubmitter
.getData(to, valueInWei, data, operation, txGasEstimate, 0, gasPrice, gasToken, sigs)
let dataGasEstimate = estimateDataGasCosts(payload) + signatureCost
if (dataGasEstimate > 65536) {
dataGasEstimate += 64
} else {
dataGasEstimate += 128
}
return dataGasEstimate + 34000 // Add aditional gas costs (e.g. base tx costs, transfer costs)
}
// eslint-disable-next-line
export const generateTxGasEstimateFrom = async (
safe: any,
safeAddress: string,
data: string,
to: string,
valueInWei: number,
operation: number,
) => {
try {
const estimateData = safe.contract.requiredTxGas.getData(to, valueInWei, data, operation)
const estimateResponse = await promisify(cb => getWeb3().eth.call({
to: safeAddress,
from: safeAddress,
data: estimateData,
}, cb))
const txGasEstimate = new BigNumber(estimateResponse.substring(138), 16)
// Add 10k else we will fail in case of nested calls
return Promise.resolve(txGasEstimate.toNumber() + 10000)
} catch (error) {
// eslint-disable-next-line
console.log("Error calculating tx gas estimation " + error)
return Promise.resolve(0)
}
}
const generateTypedDataFrom = async (
safe: any,
safeAddress: string,
to: string,
valueInWei: number,
nonce: number,
data: string,
operation: number,
txGasEstimate: number,
) => {
const txGasToken = 0
// const threshold = await safe.getThreshold()
// estimateDataGas(safe, to, valueInWei, data, operation, txGasEstimate, txGasToken, nonce, threshold)
const dataGasEstimate = 0
const gasPrice = 0
const typedData = {
types: {
EIP712Domain: [
{
type: 'address',
name: 'verifyingContract',
},
],
PersonalSafeTx: [
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'value' },
{ type: 'bytes', name: 'data' },
{ type: 'uint8', name: 'operation' },
{ type: 'uint256', name: 'safeTxGas' },
{ type: 'uint256', name: 'dataGas' },
{ type: 'uint256', name: 'gasPrice' },
{ type: 'address', name: 'gasToken' },
{ type: 'uint256', name: 'nonce' },
],
},
domain: {
verifyingContract: safeAddress,
},
primaryType: 'PersonalSafeTx',
message: {
to,
value: valueInWei,
data,
operation,
safeTxGas: txGasEstimate,
dataGas: dataGasEstimate,
gasPrice,
gasToken: txGasToken,
nonce,
},
}
return typedData
}
export const generateMetamaskSignature = async (
safe: any,
safeAddress: string,
sender: string,
to: string,
valueInWei: number,
nonce: number,
data: string,
operation: number,
txGasEstimate: number,
) => {
const web3 = getWeb3()
const typedData =
await generateTypedDataFrom(safe, safeAddress, to, valueInWei, nonce, data, operation, txGasEstimate)
const signedTypedData = {
jsonrpc: '2.0',
method: 'eth_signTypedData',
params: [sender, typedData],
id: Date.now(),
}
const txSignedResponse = await promisify(cb => web3.currentProvider.sendAsync(signedTypedData, cb))
return txSignedResponse.result.replace(EMPTY_DATA, '')
}

View File

@ -12,6 +12,7 @@ import { promisify } from '~/utils/promisify'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { safeTransactionsSelector } from '~/routes/safe/store/selectors' import { safeTransactionsSelector } from '~/routes/safe/store/selectors'
import fetchSafe from '~/routes/safe/store/actions/fetchSafe' import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
import { signaturesViaMetamask } from '~/config'
import { testTransactionFrom, testSizeOfTransactions } from './utils/historyServiceHelper' import { testTransactionFrom, testSizeOfTransactions } from './utils/historyServiceHelper'
@ -34,7 +35,7 @@ describe('Transactions Suite', () => {
const gnosisSafe = await getSafeEthereumInstance(safeAddress) const gnosisSafe = await getSafeEthereumInstance(safeAddress)
const firstTxData = gnosisSafe.contract.addOwnerWithThreshold.getData(accounts[1], 2) const firstTxData = gnosisSafe.contract.addOwnerWithThreshold.getData(accounts[1], 2)
const executor = accounts[0] const executor = accounts[0]
const nonce = Date.now() const nonce = signaturesViaMetamask() ? await gnosisSafe.nonce() : Date.now()
const firstTxHash = await createTransaction(safe, 'Add Owner Second account', safeAddress, 0, nonce, executor, firstTxData) const firstTxHash = await createTransaction(safe, 'Add Owner Second account', safeAddress, 0, nonce, executor, firstTxData)
await store.dispatch(fetchSafe(safe)) await store.dispatch(fetchSafe(safe))
safe = getSafeFrom(store.getState(), safeAddress) safe = getSafeFrom(store.getState(), safeAddress)

View File

@ -4,6 +4,9 @@ import abi from 'ethereumjs-abi'
import { promisify } from '~/utils/promisify' import { promisify } from '~/utils/promisify'
/* /*
console.log(`to[${to}] \n\n valieInWei[${valueInWei}] \n\n
data[${data}] \n\n operation[${operation}] \n\n sigs[${sigs}]`)
const gnosisSafe = await getSafeEthereumInstance(address) const gnosisSafe = await getSafeEthereumInstance(address)
await printOutApprove("Remove owner 3", address, await gnosisSafe.getOwners(), tx.get('data'), tx.get('nonce')) await printOutApprove("Remove owner 3", address, await gnosisSafe.getOwners(), tx.get('data'), tx.get('nonce'))
const txData = const txData =

View File

@ -0,0 +1,32 @@
// @flow
import { Map } from 'immutable'
import { load } from '~/utils/localStorage'
const getSignaturesKeyFrom = (safeAddress: string) => `TXS-SIGNATURES-${safeAddress}`
export const storeSignature = (safeAddress: string, nonce: number, signature: string) => {
const signaturesKey = getSignaturesKeyFrom(safeAddress)
const subjects = Map(load(signaturesKey)) || Map()
try {
const key = `${nonce}`
const existingSignatures = subjects.get(key)
const signatures = existingSignatures ? existingSignatures + signature : signature
const updatedSubjects = subjects.set(key, signatures)
const serializedState = JSON.stringify(updatedSubjects)
localStorage.setItem(signaturesKey, serializedState)
} catch (err) {
// eslint-disable-next-line
console.log('Error storing signatures in localstorage')
}
}
export const getSignaturesFrom = (safeAddress: string, nonce: number) => {
const key = getSignaturesKeyFrom(safeAddress)
const data: any = load(key)
const signatures = data ? Map(data) : Map()
const txSigs = signatures.get(String(nonce)) || ''
return `0x${txSigs}`
}