1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Fix default gas race condition (#8490)

A race condition exists where after adding an unapproved transaction,
it could be mutated and then replaced when the default gas parameters
are set. This happens because the transaction is added to state and
broadcast before the default gas parameters are set, because
calculating the default gas parameters to use takes some time.
Once they've been calculated, the false assumption was made that the
transaction hadn't changed.

The method responsible for setting the default gas now retrieves an
up-to-date copy of `txMeta`, and conditionally sets the defaults only
if they haven't yet been set.

This race condition was introduced in #2962, though that PR also added
a loading screen that avoided this issue by preventing the user from
interacting with the transaction until after the gas had been
estimated. Unfortunately this loading screen was not carried forward to
the new UI.
This commit is contained in:
Mark Stacey 2020-05-01 12:25:45 -03:00 committed by GitHub
parent 165666b315
commit 5b5b67a985
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 64 additions and 51 deletions

View File

@ -247,13 +247,13 @@ export default class TransactionController extends EventEmitter {
txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse) txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse)
} catch (error) { } catch (error) {
log.warn(error) log.warn(error)
txMeta = this.txStateManager.getTx(txMeta.id)
txMeta.loadingDefaults = false txMeta.loadingDefaults = false
this.txStateManager.updateTx(txMeta, 'Failed to calculate gas defaults.') this.txStateManager.updateTx(txMeta, 'Failed to calculate gas defaults.')
throw error throw error
} }
txMeta.loadingDefaults = false txMeta.loadingDefaults = false
// save txMeta // save txMeta
this.txStateManager.updateTx(txMeta, 'Added new unapproved transaction.') this.txStateManager.updateTx(txMeta, 'Added new unapproved transaction.')
@ -266,24 +266,53 @@ export default class TransactionController extends EventEmitter {
* @returns {Promise<object>} - resolves with txMeta * @returns {Promise<object>} - resolves with txMeta
*/ */
async addTxGasDefaults (txMeta, getCodeResponse) { async addTxGasDefaults (txMeta, getCodeResponse) {
const txParams = txMeta.txParams const defaultGasPrice = await this._getDefaultGasPrice(txMeta)
const { gasLimit: defaultGasLimit, simulationFails } = await this._getDefaultGasLimit(txMeta, getCodeResponse)
let gasPrice = txParams.gasPrice txMeta = this.txStateManager.getTx(txMeta.id)
if (!gasPrice) { if (simulationFails) {
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice() txMeta.simulationFails = simulationFails
} }
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) if (defaultGasPrice && !txMeta.txParams.gasPrice) {
txMeta.txParams.gasPrice = defaultGasPrice
}
if (defaultGasLimit && !txMeta.txParams.gas) {
txMeta.txParams.gas = defaultGasLimit
}
return txMeta
}
// set gasLimit /**
* Gets default gas price, or returns `undefined` if gas price is already set
* @param {Object} txMeta - The txMeta object
* @returns {Promise<string>} The default gas price
*/
async _getDefaultGasPrice (txMeta) {
if (txMeta.txParams.gasPrice) {
return
}
const gasPrice = this.getGasPrice
? this.getGasPrice()
: await this.query.gasPrice()
if (txParams.gas) { return ethUtil.addHexPrefix(gasPrice.toString(16))
return txMeta }
/**
* Gets default gas limit, or debug information about why gas estimate failed.
* @param {Object} txMeta - The txMeta object
* @param {string} getCodeResponse - The transaction category code response, used for debugging purposes
* @returns {Promise<Object>} Object containing the default gas limit, or the simulation failure object
*/
async _getDefaultGasLimit (txMeta, getCodeResponse) {
if (txMeta.txParams.gas) {
return {}
} else if ( } else if (
txParams.to && txMeta.txParams.to &&
txMeta.transactionCategory === SEND_ETHER_ACTION_KEY txMeta.transactionCategory === SEND_ETHER_ACTION_KEY
) { ) {
// if there's data in the params, but there's no contract code, it's not a valid transaction // if there's data in the params, but there's no contract code, it's not a valid transaction
if (txParams.data) { if (txMeta.txParams.data) {
const err = new Error('TxGasUtil - Trying to call a function on a non-contract address') const err = new Error('TxGasUtil - Trying to call a function on a non-contract address')
// set error key so ui can display localized error message // set error key so ui can display localized error message
err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY
@ -294,17 +323,14 @@ export default class TransactionController extends EventEmitter {
} }
// This is a standard ether simple send, gas requirement is exactly 21k // This is a standard ether simple send, gas requirement is exactly 21k
txParams.gas = SIMPLE_GAS_COST return { gasLimit: SIMPLE_GAS_COST }
return txMeta
} }
const { blockGasLimit, estimatedGasHex, simulationFails } = await this.txGasUtil.analyzeGasUsage(txMeta) const { blockGasLimit, estimatedGasHex, simulationFails } = await this.txGasUtil.analyzeGasUsage(txMeta)
if (simulationFails) {
txMeta.simulationFails = simulationFails // add additional gas buffer to our estimation for safety
} else { const gasLimit = this.txGasUtil.addGasBuffer(ethUtil.addHexPrefix(estimatedGasHex), blockGasLimit)
this.txGasUtil.setTxGas(txMeta, blockGasLimit, estimatedGasHex) return { gasLimit, simulationFails }
}
return txMeta
} }
/** /**
@ -647,14 +673,16 @@ export default class TransactionController extends EventEmitter {
status: 'unapproved', status: 'unapproved',
loadingDefaults: true, loadingDefaults: true,
}).forEach((tx) => { }).forEach((tx) => {
this.addTxGasDefaults(tx) this.addTxGasDefaults(tx)
.then((txMeta) => { .then((txMeta) => {
txMeta.loadingDefaults = false txMeta.loadingDefaults = false
this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot')
}).catch((error) => { }).catch((error) => {
tx.loadingDefaults = false const txMeta = this.txStateManager.getTx(tx.id)
this.txStateManager.updateTx(tx, 'failed to estimate gas during boot cleanup.') txMeta.loadingDefaults = false
this.txStateManager.setTxStatusFailed(tx.id, error) this.txStateManager.updateTx(txMeta, 'failed to estimate gas during boot cleanup.')
this.txStateManager.setTxStatusFailed(txMeta.id, error)
}) })
}) })

View File

@ -1,7 +1,6 @@
import EthQuery from 'ethjs-query' import EthQuery from 'ethjs-query'
import { hexToBn, BnMultiplyByFraction, bnToHex } from '../../lib/util' import { hexToBn, BnMultiplyByFraction, bnToHex } from '../../lib/util'
import log from 'loglevel' import log from 'loglevel'
import { addHexPrefix } from 'ethereumjs-util'
/** /**
* Result of gas analysis, including either a gas estimate for a successful analysis, or * Result of gas analysis, including either a gas estimate for a successful analysis, or
@ -31,15 +30,19 @@ export default class TxGasUtil {
*/ */
async analyzeGasUsage (txMeta) { async analyzeGasUsage (txMeta) {
const block = await this.query.getBlockByNumber('latest', false) const block = await this.query.getBlockByNumber('latest', false)
let estimatedGasHex
// fallback to block gasLimit
const blockGasLimitBN = hexToBn(block.gasLimit)
const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
let estimatedGasHex = bnToHex(saferGasLimitBN)
let simulationFails let simulationFails
try { try {
estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
} catch (err) { } catch (error) {
log.warn(err) log.warn(error)
simulationFails = { simulationFails = {
reason: err.message, reason: error.message,
errorKey: err.errorKey, errorKey: error.errorKey,
debug: { blockNumber: block.number, blockGasLimit: block.gasLimit }, debug: { blockNumber: block.number, blockGasLimit: block.gasLimit },
} }
} }
@ -50,37 +53,15 @@ export default class TxGasUtil {
/** /**
Estimates the tx's gas usage Estimates the tx's gas usage
@param {Object} txMeta - the txMeta object @param {Object} txMeta - the txMeta object
@param {string} blockGasLimitHex - hex string of the block's gas limit
@returns {string} - the estimated gas limit as a hex string @returns {string} - the estimated gas limit as a hex string
*/ */
async estimateTxGas (txMeta, blockGasLimitHex) { async estimateTxGas (txMeta) {
const txParams = txMeta.txParams const txParams = txMeta.txParams
// fallback to block gasLimit
const blockGasLimitBN = hexToBn(blockGasLimitHex)
const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
txParams.gas = bnToHex(saferGasLimitBN)
// estimate tx gas requirements // estimate tx gas requirements
return await this.query.estimateGas(txParams) return await this.query.estimateGas(txParams)
} }
/**
Writes the gas on the txParams in the txMeta
@param {Object} txMeta - the txMeta object to write to
@param {string} blockGasLimitHex - the block gas limit hex
@param {string} estimatedGasHex - the estimated gas hex
*/
setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) {
const txParams = txMeta.txParams
// if gasLimit not originally specified,
// try adding an additional gas buffer to our estimation for safety
const recommendedGasHex = this.addGasBuffer(addHexPrefix(estimatedGasHex), blockGasLimitHex)
txParams.gas = recommendedGasHex
return
}
/** /**
Adds a gas buffer with out exceeding the block gas limit Adds a gas buffer with out exceeding the block gas limit

View File

@ -230,7 +230,11 @@ describe('Transaction Controller', function () {
describe('#addTxGasDefaults', function () { describe('#addTxGasDefaults', function () {
it('should add the tx defaults if their are none', async function () { it('should add the tx defaults if their are none', async function () {
txController.txStateManager._saveTxList([
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {}, history: [{}] },
])
const txMeta = { const txMeta = {
id: 1,
txParams: { txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d', from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d', to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',