diff --git a/src/logic/safe/safeBlockchainOperations.js b/src/logic/safe/safeBlockchainOperations.js index 9f65f44c..1be91b66 100644 --- a/src/logic/safe/safeBlockchainOperations.js +++ b/src/logic/safe/safeBlockchainOperations.js @@ -3,7 +3,8 @@ import { calculateGasOf, checkReceiptStatus, calculateGasPrice } from '~/logic/w import { type Operation, submitOperation } from '~/logic/safe/safeTxHistory' import { getDailyLimitModuleFrom } from '~/logic/contracts/dailyLimitContracts' import { getSafeEthereumInstance } from '~/logic/safe/safeFrontendOperations' -import { generateMetamaskSignature, generateTxGasEstimateFrom, estimateDataGas } from '~/logic/safe/safeTxSigner' +import { buildSignaturesFrom } from '~/logic/safe/safeTxSigner' +import { generateMetamaskSignature, generateTxGasEstimateFrom, estimateDataGas } from '~/logic/safe/safeTxSignerEIP712' import { storeSignature, getSignaturesFrom } from '~/utils/localStorage/signatures' import { signaturesViaMetamask } from '~/config' @@ -30,10 +31,12 @@ export const approveTransaction = async ( } const gnosisSafe = await getSafeEthereumInstance(safeAddress) - const txData = gnosisSafe.contract.approveTransactionWithParameters.getData(to, valueInWei, data, operation, nonce) - const gas = await calculateGasOf(txData, sender, safeAddress) - const txReceipt = await gnosisSafe - .approveTransactionWithParameters(to, valueInWei, data, operation, nonce, { from: sender, gas, gasPrice }) + const contractTxHash = await gnosisSafe.getTransactionHash(to, valueInWei, data, operation, 0, 0, 0, 0, 0, nonce) + + const approveData = gnosisSafe.contract.approveHash.getData(contractTxHash) + const gas = await calculateGasOf(approveData, sender, safeAddress) + const txReceipt = await gnosisSafe.approveHash(contractTxHash, { from: sender, gas, gasPrice }) + const txHash = txReceipt.tx await checkReceiptStatus(txHash) @@ -89,17 +92,24 @@ export const executeTransaction = async ( } const gnosisSafe = await getSafeEthereumInstance(safeAddress) - const txConfirmationData = - gnosisSafe.contract.execTransactionIfApproved.getData(to, valueInWei, data, operation, nonce) + const ownersWhoHasSigned = [] // to obtain from tx-history-service + const signatures = buildSignaturesFrom(ownersWhoHasSigned, sender) + const txExecutionData = + gnosisSafe.contract.execTransaction.getData(to, valueInWei, data, operation, 0, 0, 0, 0, 0, signatures) + const gas = await calculateGasOf(txExecutionData, sender, safeAddress) const numOwners = await gnosisSafe.getOwners() - const gas = await calculateGasOf(txConfirmationData, sender, safeAddress) const gasIncludingRemovingStoreUpfront = gas + (numOwners.length * 15000) - const txReceipt = await gnosisSafe.execTransactionIfApproved( + const txReceipt = await gnosisSafe.execTransaction( to, valueInWei, data, operation, - nonce, + 0, + 0, + 0, + 0, + 0, + signatures, { from: sender, gas: gasIncludingRemovingStoreUpfront, gasPrice }, ) const txHash = txReceipt.tx diff --git a/src/logic/safe/safeTxHistory.js b/src/logic/safe/safeTxHistory.js index 86ebc355..4d14d286 100644 --- a/src/logic/safe/safeTxHistory.js +++ b/src/logic/safe/safeTxHistory.js @@ -18,7 +18,8 @@ const calculateBodyFrom = async ( type: TxServiceType, ) => { const gnosisSafe = await getSafeEthereumInstance(safeAddress) - const contractTransactionHash = await gnosisSafe.getTransactionHash(to, valueInWei, data, operation, nonce) + const contractTransactionHash = + await gnosisSafe.getTransactionHash(to, valueInWei, data, operation, 0, 0, 0, 0, 0, nonce) return JSON.stringify({ to: getWeb3().toChecksumAddress(to), diff --git a/src/logic/safe/safeTxSigner.js b/src/logic/safe/safeTxSigner.js index 7d99efde..f1a4f507 100644 --- a/src/logic/safe/safeTxSigner.js +++ b/src/logic/safe/safeTxSigner.js @@ -1,166 +1,13 @@ // @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 generateSignatureFrom = (account: string) => + `000000000000000000000000${account.replace('0x', '')}000000000000000000000000000000000000000000000000000000000000000001` -const estimateDataGasCosts = (data) => { - const reducer = (accumulator, currentValue) => { - if (currentValue === EMPTY_DATA) { - return accumulator + 0 - } +export const buildSignaturesFrom = (ownersWhoHasSigned: string[], sender: string) => { + let sigs = '0x' + ownersWhoHasSigned.forEach((owner: string) => { + sigs += generateSignatureFrom(owner) + }) + sigs += generateSignatureFrom(sender) - 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, - refundReceiver: 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.execTransaction - .getData(to, valueInWei, data, operation, txGasEstimate, 0, gasPrice, gasToken, refundReceiver, 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 refundReceiver = 0 - const typedData = { - types: { - EIP712Domain: [ - { - type: 'address', - name: 'verifyingContract', - }, - ], - SafeTx: [ - { 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: 'address', name: 'refundReceiver' }, - { type: 'uint256', name: 'nonce' }, - ], - }, - domain: { - verifyingContract: safeAddress, - }, - primaryType: 'SafeTx', - message: { - to, - value: Number(valueInWei), - data, - operation, - safeTxGas: txGasEstimate, - dataGas: dataGasEstimate, - gasPrice, - gasToken: txGasToken, - refundReceiver, - nonce: Number(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 jsonTypedData = JSON.stringify(typedData) - const signedTypedData = { - method: 'eth_signTypedData_v3', - params: [jsonTypedData, sender], - from: sender, - } - const txSignedResponse = await promisify(cb => web3.currentProvider.sendAsync(signedTypedData, cb)) - - return txSignedResponse.result.replace(EMPTY_DATA, '') + return sigs } diff --git a/src/logic/safe/safeTxSignerEIP712.js b/src/logic/safe/safeTxSignerEIP712.js new file mode 100644 index 00000000..1bd1c23b --- /dev/null +++ b/src/logic/safe/safeTxSignerEIP712.js @@ -0,0 +1,169 @@ +// @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, + refundReceiver: 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.execTransaction + .getData(to, valueInWei, data, operation, txGasEstimate, 0, gasPrice, gasToken, refundReceiver, 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 refundReceiver = 0 + const typedData = { + types: { + EIP712Domain: [ + { + type: 'address', + name: 'verifyingContract', + }, + ], + SafeTx: [ + { 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: 'address', name: 'refundReceiver' }, + { type: 'uint256', name: 'nonce' }, + ], + }, + domain: { + verifyingContract: safeAddress, + }, + primaryType: 'SafeTx', + message: { + to, + value: Number(valueInWei), + data, + operation, + safeTxGas: txGasEstimate, + dataGas: dataGasEstimate, + gasPrice, + gasToken: txGasToken, + refundReceiver, + nonce: Number(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 jsonTypedData = JSON.stringify(typedData) + const signedTypedData = { + method: 'eth_signTypedData_v3', + // To change once Metamask fixes their status + // https://github.com/MetaMask/metamask-extension/pull/5368 + // https://github.com/MetaMask/metamask-extension/issues/5366 + params: [jsonTypedData, sender], + from: sender, + } + const txSignedResponse = await promisify(cb => web3.currentProvider.sendAsync(signedTypedData, cb)) + + return txSignedResponse.result.replace(EMPTY_DATA, '') +} diff --git a/src/test/utils/logTransactions.js b/src/test/utils/logTransactions.js index d668c861..2b0e2d6b 100644 --- a/src/test/utils/logTransactions.js +++ b/src/test/utils/logTransactions.js @@ -15,7 +15,7 @@ export const printOutApprove = async ( console.log(subject) const gnosisSafe = await getGnosisSafeInstanceAt(address) - const transactionHash = await gnosisSafe.getTransactionHash(address, 0, data, 0, nonce) + const transactionHash = await gnosisSafe.getTransactionHash(address, 0, data, 0, 0, 0, 0, 0, 0, nonce) // eslint-disable-next-line console.log(`EO transaction hash ${transactionHash}`)