Skip to content

Commit

Permalink
Add support for EIP1559 gas price oracle (#631)
Browse files Browse the repository at this point in the history
  • Loading branch information
k1rill-fedoseev authored Jan 3, 2022
1 parent 296e5c5 commit 8d732ad
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 49 deletions.
4 changes: 2 additions & 2 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ COMMON_HOME_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in t
COMMON_FOREIGN_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in the Foreign network. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
COMMON_HOME_BRIDGE_ADDRESS | The address of the bridge contract address in the Home network. It is used to listen to events from and send validators' transactions to the Home network. | hexidecimal beginning with "0x"
COMMON_FOREIGN_BRIDGE_ADDRESS | The address of the bridge contract address in the Foreign network. It is used to listen to events from and send validators' transactions to the Foreign network. | hexidecimal beginning with "0x"
COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. | URL
COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
COMMON_HOME_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_HOME_GAS_PRICE_SUPPLIER_URL` is not used. | `instant` / `fast` / `standard` / `slow`
COMMON_HOME_GAS_PRICE_FALLBACK | The gas price (in Wei) that is used if both the oracle and the fall back gas price specified in the Home Bridge contract are not available. | integer
COMMON_HOME_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. Set to `gas-price-oracle` if you want to use npm `gas-price-oracle` package for retrieving gas price from multiple sources. | URL
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. Set to `gas-price-oracle` if you want to use npm `gas-price-oracle` package for retrieving gas price from multiple sources. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL`is not used. | `instant` / `fast` / `standard` / `slow`
COMMON_FOREIGN_GAS_PRICE_FALLBACK | The gas price (in Wei) used if both the oracle and fall back gas price specified in the Foreign Bridge contract are not available. | integer
COMMON_FOREIGN_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
Expand Down
1 change: 1 addition & 0 deletions commons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"test": "NODE_ENV=test mocha"
},
"dependencies": {
"@mycrypto/gas-estimation": "^1.1.0",
"gas-price-oracle": "^0.1.5",
"web3-utils": "^1.3.0",
"node-fetch": "^2.1.2"
Expand Down
19 changes: 14 additions & 5 deletions commons/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { toWei, toBN, BN } = require('web3-utils')
const { GasPriceOracle } = require('gas-price-oracle')
const { estimateFees } = require('@mycrypto/gas-estimation')
const fetch = require('node-fetch')
const { BRIDGE_MODES } = require('./constants')
const { REWARDABLE_VALIDATORS_ABI } = require('./abis')
Expand Down Expand Up @@ -176,12 +177,20 @@ const gasPriceWithinLimits = (gasPrice, limits) => {
const normalizeGasPrice = (oracleGasPrice, factor, limits = null) => {
let gasPrice = oracleGasPrice * factor
gasPrice = gasPriceWithinLimits(gasPrice, limits)
return toBN(toWei(gasPrice.toFixed(2).toString(), 'gwei'))
return toWei(gasPrice.toFixed(2).toString(), 'gwei')
}

const gasPriceFromSupplier = async (url, options = {}) => {
const gasPriceFromSupplier = async (web3, url, options = {}) => {
try {
let json
if (url === 'eip1559-gas-estimation') {
const { maxFeePerGas, maxPriorityFeePerGas } = await estimateFees(web3)
const res = { maxFeePerGas: maxFeePerGas.toString(10), maxPriorityFeePerGas: maxPriorityFeePerGas.toString(10) }
options.logger &&
options.logger.debug &&
options.logger.debug(res, 'Gas price updated using eip1559-gas-estimation')
return res
}
if (url === 'gas-price-oracle') {
json = await gasPriceOracle.fetchGasPricesOffChain()
} else if (url) {
Expand All @@ -205,7 +214,7 @@ const gasPriceFromSupplier = async (url, options = {}) => {
options.logger.debug &&
options.logger.debug({ oracleGasPrice, normalizedGasPrice }, 'Gas price updated using the API')

return normalizedGasPrice
return { gasPrice: normalizedGasPrice }
} catch (e) {
options.logger && options.logger.error && options.logger.error(`Gas Price API is not available. ${e.message}`)
}
Expand All @@ -214,11 +223,11 @@ const gasPriceFromSupplier = async (url, options = {}) => {

const gasPriceFromContract = async (bridgeContract, options = {}) => {
try {
const gasPrice = await bridgeContract.methods.gasPrice().call()
const gasPrice = (await bridgeContract.methods.gasPrice().call()).toString()
options.logger &&
options.logger.debug &&
options.logger.debug({ gasPrice }, 'Gas price updated using the contracts')
return gasPrice
return { gasPrice }
} catch (e) {
options.logger &&
options.logger.error &&
Expand Down
2 changes: 1 addition & 1 deletion oracle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"pino-pretty": "^2.0.1",
"promise-limit": "^2.7.0",
"promise-retry": "^1.1.1",
"web3": "^1.3.0"
"web3": "^1.6.0"
},
"devDependencies": {
"bn-chai": "^1.0.1",
Expand Down
12 changes: 6 additions & 6 deletions oracle/src/confirmRelay.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,11 @@ async function main({ sendJob, txHashes }) {
}

async function sendJobTx(jobs) {
await GasPrice.start(chain, true)
const gasPrice = GasPrice.getPrice().toString(10)

const { web3 } = config.sender === 'foreign' ? config.foreign : config.home

await GasPrice.start(chain, web3, true)
const gasPriceOptions = GasPrice.gasPriceOptions()

const chainId = await getChainId(web3)
let nonce = await getNonce(web3, config.validatorAddress)

Expand All @@ -174,13 +174,13 @@ async function sendJobTx(jobs) {
const txHash = await sendTx({
data: job.data,
nonce,
gasPrice,
amount: '0',
gasLimit,
privateKey: config.validatorPrivateKey,
to: job.to,
chainId,
web3
web3,
gasPriceOptions
})

nonce++
Expand All @@ -197,7 +197,7 @@ async function sendJobTx(jobs) {

if (e.message.toLowerCase().includes('insufficient funds')) {
const currentBalance = await web3.eth.getBalance(config.validatorAddress)
const minimumBalance = gasLimit.multipliedBy(gasPrice)
const minimumBalance = gasLimit.multipliedBy(gasPriceOptions.gasPrice || gasPriceOptions.maxFeePerGas)
logger.error(
`Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.`
)
Expand Down
20 changes: 11 additions & 9 deletions oracle/src/sender.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async function initialize() {

web3.currentProvider.urls.forEach(checkHttps(config.id))

GasPrice.start(config.id)
GasPrice.start(config.id, web3)

chainId = await getChainId(web3)
connectQueue()
Expand Down Expand Up @@ -120,7 +120,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT

const txArray = JSON.parse(msg.content)
logger.debug(`Msg received with ${txArray.length} Tx to send`)
const gasPrice = GasPrice.getPrice().toString(10)
const gasPriceOptions = GasPrice.gasPriceOptions()

let nonce
let insufficientFunds = false
Expand Down Expand Up @@ -158,24 +158,26 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
nonce = await readNonce(true)
}

logger.info(`Transaction ${job.txHash} was not mined, updating gasPrice: ${job.gasPrice} -> ${gasPrice}`)
const oldGasPrice = JSON.stringify(job.gasPriceOptions)
const newGasPrice = JSON.stringify(gasPriceOptions)
logger.info(`Transaction ${job.txHash} was not mined, updating gasPrice: ${oldGasPrice} -> ${newGasPrice}`)
}
logger.info(`Sending transaction with nonce ${nonce}`)
const txHash = await sendTx({
data: job.data,
nonce,
gasPrice,
amount: '0',
gasLimit,
privateKey: config.validatorPrivateKey,
to: job.to,
chainId,
web3: web3Redundant
web3: web3Redundant,
gasPriceOptions
})
const resendJob = {
...job,
txHash,
gasPrice
gasPriceOptions,
...job
}
resendJobs.push(resendJob)

Expand All @@ -193,7 +195,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT

if (isGasPriceError(e)) {
logger.info('Replacement transaction underpriced, forcing gas price update')
GasPrice.start(config.id)
GasPrice.start(config.id, web3)
failedTx.push(job)
} else if (isResend || isSameTransactionError(e)) {
resendJobs.push(job)
Expand All @@ -207,7 +209,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
if (isInsufficientBalanceError(e)) {
insufficientFunds = true
const currentBalance = await web3.eth.getBalance(config.validatorAddress)
minimumBalance = gasLimit.multipliedBy(gasPrice)
minimumBalance = gasLimit.multipliedBy(gasPriceOptions.gasPrice || gasPriceOptions.maxFeePerGas)
logger.error(
`Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.`
)
Expand Down
28 changes: 14 additions & 14 deletions oracle/src/services/gasPrice.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,21 @@ const {
COMMON_HOME_GAS_PRICE_FACTOR
} = process.env

let cachedGasPrice = null
let cachedGasPriceOptions = null

let fetchGasPriceInterval = null

const fetchGasPrice = async (speedType, factor, bridgeContract, gasPriceSupplierUrl) => {
const fetchGasPrice = async (speedType, factor, web3, bridgeContract, gasPriceSupplierUrl) => {
const contractOptions = { logger }
const supplierOptions = { speedType, factor, limits: GAS_PRICE_BOUNDARIES, logger }
cachedGasPrice =
(await gasPriceFromSupplier(gasPriceSupplierUrl, supplierOptions)) ||
cachedGasPriceOptions =
(await gasPriceFromSupplier(web3, gasPriceSupplierUrl, supplierOptions)) ||
(await gasPriceFromContract(bridgeContract, contractOptions)) ||
cachedGasPrice
return cachedGasPrice
cachedGasPriceOptions
return cachedGasPriceOptions
}

async function start(chainId, fetchOnce) {
async function start(chainId, web3, fetchOnce) {
clearInterval(fetchGasPriceInterval)

let contract = null
Expand All @@ -49,15 +49,15 @@ async function start(chainId, fetchOnce) {
updateInterval = ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL || DEFAULT_UPDATE_INTERVAL
factor = Number(COMMON_HOME_GAS_PRICE_FACTOR) || DEFAULT_GAS_PRICE_FACTOR

cachedGasPrice = COMMON_HOME_GAS_PRICE_FALLBACK
cachedGasPriceOptions = { gasPrice: COMMON_HOME_GAS_PRICE_FALLBACK }
} else if (chainId === 'foreign') {
contract = foreign.bridgeContract
gasPriceSupplierUrl = COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL
speedType = COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE
updateInterval = ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL || DEFAULT_UPDATE_INTERVAL
factor = Number(COMMON_FOREIGN_GAS_PRICE_FACTOR) || DEFAULT_GAS_PRICE_FACTOR

cachedGasPrice = COMMON_FOREIGN_GAS_PRICE_FALLBACK
cachedGasPriceOptions = { gasPrice: COMMON_FOREIGN_GAS_PRICE_FALLBACK }
} else {
throw new Error(`Unrecognized chainId '${chainId}'`)
}
Expand All @@ -67,21 +67,21 @@ async function start(chainId, fetchOnce) {
}

if (fetchOnce) {
await fetchGasPrice(speedType, factor, contract, gasPriceSupplierUrl)
await fetchGasPrice(speedType, factor, web3, contract, gasPriceSupplierUrl)
} else {
fetchGasPriceInterval = await setIntervalAndRun(
() => fetchGasPrice(speedType, factor, contract, gasPriceSupplierUrl),
() => fetchGasPrice(speedType, factor, web3, contract, gasPriceSupplierUrl),
updateInterval
)
}
}

function getPrice() {
return cachedGasPrice
function gasPriceOptions() {
return cachedGasPriceOptions
}

module.exports = {
start,
getPrice,
gasPriceOptions,
fetchGasPrice
}
7 changes: 4 additions & 3 deletions oracle/src/tx/sendTx.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
const { toWei } = require('web3').utils

async function sendTx({ privateKey, data, nonce, gasPrice, amount, gasLimit, to, chainId, web3 }) {
async function sendTx({ privateKey, data, nonce, gasPrice, gasPriceOptions, amount, gasLimit, to, chainId, web3 }) {
const gasOpts = gasPriceOptions || { gasPrice }
const serializedTx = await web3.eth.accounts.signTransaction(
{
nonce: Number(nonce),
chainId,
to,
data,
value: toWei(amount),
gasPrice,
gas: gasLimit
gas: gasLimit,
...gasOpts
},
privateKey
)
Expand Down
18 changes: 9 additions & 9 deletions oracle/test/gasPrice.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,21 @@ describe('gasPrice', () => {
await gasPrice.start('home')

// when
await gasPrice.fetchGasPrice('standard', 1, null, null)
await gasPrice.fetchGasPrice('standard', 1, null, null, null)

// then
expect(gasPrice.getPrice()).to.equal('101000000000')
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '101000000000' })
})

it('should fetch gas from supplier', async () => {
// given
await gasPrice.start('home')

// when
await gasPrice.fetchGasPrice('standard', 1, null, 'url')
await gasPrice.fetchGasPrice('standard', 1, null, null, 'url')

// then
expect(gasPrice.getPrice().toString()).to.equal('103000000000')
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '103000000000' })
})

it('should fetch gas from contract', async () => {
Expand All @@ -101,10 +101,10 @@ describe('gasPrice', () => {
}

// when
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, null)
await gasPrice.fetchGasPrice('standard', 1, null, bridgeContractMock, null)

// then
expect(gasPrice.getPrice().toString()).to.equal('102000000000')
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '102000000000' })
})

it('should fetch the gas price from the oracle first', async () => {
Expand All @@ -120,18 +120,18 @@ describe('gasPrice', () => {
}

// when
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, 'url')
await gasPrice.fetchGasPrice('standard', 1, null, bridgeContractMock, 'url')

// then
expect(gasPrice.getPrice().toString()).to.equal('103000000000')
expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '103000000000' })
})

it('log error using the logger', async () => {
// given
await gasPrice.start('home')

// when
await gasPrice.fetchGasPrice('standard', 1, null, null)
await gasPrice.fetchGasPrice('standard', 1, null, null, null)

// then
expect(fakeLogger.warn.calledOnce).to.equal(true) // one warning
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@
"compile:contracts": "yarn workspace tokenbridge-contracts run compile",
"install:deploy": "cd contracts/deploy && npm install --unsafe-perm --silent",
"postinstall": "test -n \"$NOYARNPOSTINSTALL\" || ln -sf $(pwd)/node_modules/openzeppelin-solidity/ contracts/node_modules/openzeppelin-solidity"
},
"resolutions": {
"**/@mycrypto/eth-scan": "3.5.3"
}
}
Loading

0 comments on commit 8d732ad

Please sign in to comment.