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:
parent
165666b315
commit
5b5b67a985
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user