Compare commits

...

17 Commits

Author SHA1 Message Date
Alexander Kolotov
af6848f39c Merge branch 'mev-executor' into fake-mev-executor 2022-02-07 23:21:53 +03:00
Kirill Fedoseev
72e1242544 Refine configuration 2022-02-07 20:22:20 +04:00
Kirill Fedoseev
13be1a4b6a Update log level 2022-02-07 18:51:51 +04:00
Kirill Fedoseev
9586b78e10 Fake MEV execution for BSC 2022-02-02 22:46:51 +03:00
Kirill Fedoseev
3e84b01f3b Helper watcher process for MEV bundling 2022-01-31 23:13:36 +03:00
Kirill Fedoseev
8ec11d0476 Fix eip1559 transaction sending problems (#632) 2022-01-17 16:51:29 +03:00
Kirill Fedoseev
8d732adba1 Add support for EIP1559 gas price oracle (#631) 2022-01-03 17:58:36 +03:00
Kirill Fedoseev
296e5c5a22 Refetch old logs ranges to see if there are missed events (#627) 2022-01-03 15:32:38 +03:00
Alexander Kolotov
b17fff2b56 Merge the develop branch to the master branch, preparation to v3.3.0
This merge contains the following set of changes:
  * [Oracle, Improvement] Add oracle helper script for fetching interest amounts via async calls (#615)
  * [Monitor, Fix] Include cDAI balance in balanceDiff (#613)
  * [Monitor, Fix] Added logging prior the investedAmount call (#614)
  * [ALM, Fix] Fetch AMB signatures a bit earlier (#620)
2021-11-05 20:22:46 +03:00
Kirill Fedoseev
4eba91ef7e Fetch AMB signatures a bit earlier (#620) 2021-11-03 19:21:35 +03:00
Kirill Fedoseev
1e3aa53ab3 Add oracle helper script for fetching interest amounts via async calls (#615) 2021-10-21 13:33:12 +03:00
Alexander Kolotov
a05ff51555 Added logging prior the investedAmount call (#614) 2021-10-12 18:06:50 +03:00
Kirill Fedoseev
52e1c88b58 Include cDAI balance in balanceDiff (#613) 2021-10-11 12:14:55 +03:00
Alexander Kolotov
d36dcadd34 Merge the develop branch to the master branch, preparation to v3.2.0
This merge contains the following set of changes:
  * [Oracle, Fix] Use more accurate gas estimates for very high message gas limits (#611)
  * [Common, Other] Final version of AMB and OB contracts security audit report by ChainSecurity (#608)
2021-10-07 21:45:51 +03:00
Kirill Fedoseev
d543dbb339 Use more accurate gas estimates for very high message gas limits (#611) 2021-10-07 20:44:14 +03:00
Kirill Fedoseev
bd21cc163e Merge pull request #608 from poanetwork/reports/chainsecurity-amb-6.0.0-ob-1.1.0-contracts
AMB and OB contracts security audit report by ChainSecurity
2021-09-28 21:12:37 +03:00
Alexander Kolotov
4a0fc936a1 Final version of AMB and OB contracts security audit report by ChainSecurity 2021-09-28 20:39:04 +03:00
43 changed files with 1974 additions and 133 deletions

View File

@@ -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
@@ -53,6 +53,12 @@ ORACLE_SHUTDOWN_CONTRACT_METHOD | Method signature to be used in the side chain
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Foreign chain. Infinite, if not provided. | `integer`
ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Home chain. Infinite, if not provided. | `integer`
ORACLE_JSONRPC_ERROR_CODES | Override default JSON rpc error codes that can trigger RPC fallback to the next URL from the list (or a retry in case of a single RPC URL). Default is `-32603,-32002,-32005`. Should be a comma-separated list of negative integers. | `string`
ORACLE_HOME_EVENTS_REPROCESSING | If set to `true`, home events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the home chain. Defaults to `1000` | `integer`
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the home chain. Defaults to `500` | `integer`
ORACLE_FOREIGN_EVENTS_REPROCESSING | If set to `true`, foreign events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the foreign chain. Defaults to `500` | `integer`
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the foreign chain. Defaults to `250` | `integer`
## Monitor configuration

View File

@@ -45,6 +45,7 @@ export const ManualExecutionButton = ({
const { library, activate, account, active } = useWeb3React()
const [manualExecution, setManualExecution] = useState(false)
const [allowFailures, setAllowFailures] = useState(false)
const notReady = !foreign.bridgeContract || !signatureCollected || !signatureCollected.length
useEffect(
() => {
@@ -150,7 +151,7 @@ export const ManualExecutionButton = ({
return (
<div>
<div className="is-center">
<ActionButton className="button outline" onClick={() => setManualExecution(true)}>
<ActionButton disabled={notReady} className="button outline" onClick={() => setManualExecution(true)}>
Execute
</ActionButton>
</div>

View File

@@ -86,6 +86,14 @@ export const getConfirmationsForTx = async (
setPendingConfirmations(validatorPendingConfirmations.length > 0)
} else {
setPendingConfirmations(false)
if (fromHome) {
// fetch collected signatures for possible manual processing
setSignatureCollected(
await Promise.all(
Array.from(Array(requiredSignatures).keys()).map(i => bridgeContract.methods.signature(hashMsg, i).call())
)
)
}
}
const undefinedConfirmations = validatorConfirmations.filter(
@@ -115,15 +123,6 @@ export const getConfirmationsForTx = async (
status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED
}))
updateConfirmations(notRequiredConfirmations)
if (fromHome) {
// fetch collected signatures for possible manual processing
setSignatureCollected(
await Promise.all(
Array.from(Array(requiredSignatures).keys()).map(i => bridgeContract.methods.signature(hashMsg, i).call())
)
)
}
}
// get transactions from success signatures

View File

@@ -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"

View File

@@ -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')
@@ -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) {
@@ -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}`)
}
@@ -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 &&

View File

@@ -23,3 +23,9 @@ ORACLE_HOME_START_BLOCK=1
ORACLE_FOREIGN_START_BLOCK=1
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt
ORACLE_FOREIGN_ARCHIVE_RPC_URL=http://parity2:8545
ORACLE_HOME_EVENTS_REPROCESSING=false
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE=10
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY=10
ORACLE_FOREIGN_EVENTS_REPROCESSING=true
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE=10
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY=10

View File

@@ -22,3 +22,9 @@ ORACLE_ALLOW_HTTP_FOR_RPC=yes
ORACLE_HOME_START_BLOCK=1
ORACLE_FOREIGN_START_BLOCK=1
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt
ORACLE_HOME_EVENTS_REPROCESSING=true
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE=10
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY=10
ORACLE_FOREIGN_EVENTS_REPROCESSING=true
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE=10
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY=10

View File

@@ -51,6 +51,14 @@ async function main(bridgeMode, eventsInfo) {
const foreignErc20Balance = await erc20Contract.methods
.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS)
.call({}, foreignDelayedBlockNumber)
let foreignErc20BalanceBN = new BN(foreignErc20Balance).plus(lateForeignConfirmationsTotalValue)
try {
logger.debug('calling foreignBridge.methods.investedAmount')
const invested = await foreignBridge.methods.investedAmount(erc20Address).call({}, foreignDelayedBlockNumber)
foreignErc20BalanceBN = foreignErc20BalanceBN.plus(invested)
} catch (_) {
logger.debug('compounding related methods are not present in the foreign bridge')
}
const homeBridge = new web3Home.eth.Contract(HOME_ERC_TO_NATIVE_ABI, COMMON_HOME_BRIDGE_ADDRESS)
logger.debug('calling homeBridge.methods.blockRewardContract')
@@ -66,7 +74,6 @@ async function main(bridgeMode, eventsInfo) {
const mintedCoinsBN = new BN(mintedCoins)
const burntCoinsBN = new BN(burntCoins)
const totalSupplyBN = mintedCoinsBN.minus(burntCoinsBN)
const foreignErc20BalanceBN = new BN(foreignErc20Balance).plus(lateForeignConfirmationsTotalValue)
const diff = foreignErc20BalanceBN.minus(totalSupplyBN).toFixed()
logger.debug('Done')

View File

@@ -23,7 +23,13 @@ const {
ORACLE_HOME_START_BLOCK,
ORACLE_FOREIGN_START_BLOCK,
ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT,
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT,
ORACLE_HOME_EVENTS_REPROCESSING,
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE,
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY,
ORACLE_FOREIGN_EVENTS_REPROCESSING,
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE,
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY
} = process.env
let homeAbi
@@ -61,7 +67,12 @@ const homeConfig = {
blockPollingLimit: parseInt(ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT, 10),
web3: web3Home,
bridgeContract: homeContract,
eventContract: homeContract
eventContract: homeContract,
reprocessingOptions: {
enabled: ORACLE_HOME_EVENTS_REPROCESSING === 'true',
batchSize: parseInt(ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE, 10) || 1000,
blockDelay: parseInt(ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY, 10) || 500
}
}
const foreignContract = new web3Foreign.eth.Contract(foreignAbi, COMMON_FOREIGN_BRIDGE_ADDRESS)
@@ -74,7 +85,12 @@ const foreignConfig = {
blockPollingLimit: parseInt(ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT, 10),
web3: web3Foreign,
bridgeContract: foreignContract,
eventContract: foreignContract
eventContract: foreignContract,
reprocessingOptions: {
enabled: ORACLE_FOREIGN_EVENTS_REPROCESSING === 'true',
batchSize: parseInt(ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE, 10) || 500,
blockDelay: parseInt(ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY, 10) || 250
}
}
const maxProcessingTime =

View File

@@ -0,0 +1,37 @@
const baseConfig = require('./base.config')
const { DEFAULT_TRANSACTION_RESEND_INTERVAL } = require('../src/utils/constants')
const { MEV_HELPER_ABI } = require('../src/utils/mev')
const { web3Foreign, getFlashbotsProvider } = require('../src/services/web3')
const {
ORACLE_FOREIGN_TX_RESEND_INTERVAL,
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS,
ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE
} = process.env
const contract = new baseConfig.foreign.web3.eth.Contract(MEV_HELPER_ABI, ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS)
module.exports = {
...baseConfig,
pollingInterval: baseConfig.foreign.pollingInterval,
mevForeign: {
contractAddress: ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
contract,
minGasPrice: ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
flatMinerFee: ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
maxPriorityFeePerGas: ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
maxFeePerGas: ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS,
bundlesPerIteration: Math.max(parseInt(ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE, 10) || 5, 1),
getFlashbotsProvider
},
mevJobsRedisKey: `${baseConfig.id}-collected-signatures-mev:mevJobs`,
id: 'mev-sender-foreign',
name: 'mev-sender-foreign',
web3: web3Foreign,
resendInterval: parseInt(ORACLE_FOREIGN_TX_RESEND_INTERVAL, 10) || DEFAULT_TRANSACTION_RESEND_INTERVAL
}

View File

@@ -8,7 +8,6 @@ const { ORACLE_FOREIGN_TX_RESEND_INTERVAL } = process.env
module.exports = {
...baseConfig,
queue: 'foreign-prioritized',
oldQueue: 'foreign',
id: 'foreign',
name: 'sender-foreign',
web3: web3Foreign,

View File

@@ -8,7 +8,6 @@ const { ORACLE_HOME_TX_RESEND_INTERVAL } = process.env
module.exports = {
...baseConfig,
queue: 'home-prioritized',
oldQueue: 'home',
id: 'home',
name: 'sender-home',
web3: web3Home,

View File

@@ -0,0 +1,30 @@
const baseConfig = require('./base.config')
const { MEV_HELPER_ABI } = require('../src/utils/mev')
const {
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS
} = process.env
const id = `${baseConfig.id}-collected-signatures-mev`
const contract = new baseConfig.foreign.web3.eth.Contract(MEV_HELPER_ABI, ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS)
module.exports = {
...baseConfig,
mevForeign: {
contractAddress: ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
contract,
minGasPrice: ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
flatMinerFee: ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
maxPriorityFeePerGas: ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
maxFeePerGas: ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS
},
main: baseConfig.home,
event: 'CollectedSignatures',
name: `watcher-${id}`,
id
}

View File

@@ -0,0 +1,64 @@
---
version: '2.4'
services:
redis:
cpus: 0.1
mem_limit: 500m
command: [ redis-server, --appendonly, 'yes' ]
hostname: redis
image: redis:4
restart: unless-stopped
volumes: [ '~/bridge_data/helpers/redis:/data' ]
interestFetcher:
cpus: 0.1
mem_limit: 500m
image: poanetwork/tokenbridge-oracle:latest
env_file: ./.env
environment:
NODE_ENV: production
INTEREST_FETCHER_PRIVATE_KEY: ${INTEREST_FETCHER_PRIVATE_KEY}
INTEREST_FETCH_CONTRACT_ADDRESS: '0xCd152c7Bd4189Ddee97EaBb783FC5cD93CF2D230'
INTERVAL: 300000
restart: unless-stopped
entrypoint: yarn helper:interestFether
mevWatcher:
cpus: 0.1
mem_limit: 500m
image: poanetwork/tokenbridge-oracle:latest
env_file: ./.env
environment:
NODE_ENV: production
ORACLE_VALIDATOR_ADDRESS: ${ORACLE_VALIDATOR_ADDRESS}
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS: 'TBD'
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE: '50000000000' # 50 gwei
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE: '1500000000000000' # 0.0015 eth = 300k gas * 5 gwei
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS: '0' # 0 gwei
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS: '1000000000000' # 1000 gwei
ORACLE_FOREIGN_RPC_POLLING_INTERVAL: '15000' # CollectedSignatures event polling interval
ORACLE_HOME_START_BLOCK: 'TBD'
ORACLE_HOME_SKIP_MANUAL_LANE: 'true'
restart: unless-stopped
entrypoint: yarn mev:watcher:collected-signatures
mevSender:
cpus: 0.1
mem_limit: 500m
image: poanetwork/tokenbridge-oracle:latest
env_file: ./.env
environment:
NODE_ENV: production
ORACLE_VALIDATOR_ADDRESS: ${ORACLE_VALIDATOR_ADDRESS}
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY: ${ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY}
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS: 'TBD'
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE: '50000000000' # 50 gwei
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE: '1500000000000000' # 0.0015 eth = 300k gas * 5 gwei
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS: '0' # 0 gwei
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS: '1000000000000' # 1000 gwei
ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL: 'https://relay-goerli.flashbots.net'
ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY: 82db7175932f4e6c8e45283b78b54fd5f195149378ec90d95b8fd0ec8bdadf1d
ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE: '5'
ORACLE_FOREIGN_RPC_POLLING_INTERVAL: '70000' # time between sending different batches of MEV bundles (~= 5 blocks * 14 seconds)
FAKE_MEV_RELAY_TX_SENDING: 'true'
FAKE_MEV_RELAY_NO_EIP1559: 'true'
COMMON_FOREIGN_GAS_PRICE_FALLBACK: 100000000000 # will essentially replace block base fee
restart: unless-stopped
entrypoint: yarn mev:sender:foreign

View File

@@ -18,6 +18,9 @@
"confirm:collected-signatures": "./scripts/start-worker.sh confirmRelay collected-signatures-watcher",
"confirm:information-request": "./scripts/start-worker.sh confirmRelay information-request-watcher",
"manager:shutdown": "./scripts/start-worker.sh shutdownManager shutdown-manager",
"helper:interestFether": "node ./scripts/interestFetcher.js",
"mev:watcher:collected-signatures": "./scripts/start-worker.sh mevWatcher mev-collected-signatures-watcher",
"mev:sender:foreign": "./scripts/start-worker.sh mevSender foreign-mev-sender",
"dev": "concurrently -n 'watcher:signature-request,watcher:collected-signatures,watcher:affirmation-request,watcher:transfer, sender:home,sender:foreign' -c 'red,green,yellow,blue,magenta,cyan' 'yarn watcher:signature-request' 'yarn watcher:collected-signatures' 'yarn watcher:affirmation-request' 'yarn watcher:transfer' 'yarn sender:home' 'yarn sender:foreign'",
"test": "NODE_ENV=test mocha",
"test:watch": "NODE_ENV=test mocha --watch --reporter=min",
@@ -27,17 +30,19 @@
"author": "",
"license": "ISC",
"dependencies": {
"@flashbots/ethers-provider-bundle": "^0.4.3",
"amqp-connection-manager": "^2.0.0",
"amqplib": "^0.5.2",
"bignumber.js": "^7.2.1",
"dotenv": "^5.0.1",
"ethers": "^5.5.3",
"ioredis": "^3.2.2",
"node-fetch": "^2.1.2",
"pino": "^4.17.3",
"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",

View File

@@ -36,7 +36,7 @@ async function main() {
data,
nonce,
gasPrice: FOREIGN_TEST_TX_GAS_PRICE,
amount: '0',
value: '0',
gasLimit,
to: bridgeableTokenAddress,
web3: web3Foreign,

View File

@@ -29,7 +29,7 @@ async function main() {
data: '0x',
nonce,
gasPrice: HOME_TEST_TX_GAS_PRICE,
amount: HOME_MIN_AMOUNT_PER_TX,
value: web3Home.utils.toWei(HOME_MIN_AMOUNT_PER_TX),
gasLimit: 100000,
to: COMMON_HOME_BRIDGE_ADDRESS,
web3: web3Home,

View File

@@ -0,0 +1,65 @@
require('../env')
const { isAddress } = require('web3').utils
const { privateKeyToAddress, setIntervalAndRun } = require('../src/utils/utils')
const { EXIT_CODES } = require('../src/utils/constants')
const { web3Home } = require('../src/services/web3')
const { sendTx } = require('../src/tx/sendTx')
const privateKey = process.env.INTEREST_FETCHER_PRIVATE_KEY
const interval = process.env.INTERVAL ? parseInt(process.env.INTERVAL, 10) : 3600 * 1000
const contractAddress = process.env.INTEREST_FETCH_CONTRACT_ADDRESS
if (!privateKey) {
console.error('Environment variable INTEREST_FETCHER_PRIVATE_KEY is not set')
process.exit(EXIT_CODES.GENERAL_ERROR)
}
if (interval < 300 * 1000) {
console.error('Interval is to small, should be at least 5 minutes')
process.exit(EXIT_CODES.GENERAL_ERROR)
}
if (!isAddress(contractAddress)) {
console.error('Invalid contract address provided', contractAddress)
process.exit(EXIT_CODES.GENERAL_ERROR)
}
const gasPrice = process.env.COMMON_HOME_GAS_PRICE_FALLBACK || '1000000000'
async function main() {
// assuming that we are using this contract - https://github.com/omni/interest-fetcher-contract
const contract = new web3Home.eth.Contract([{ name: 'fetchInterest', type: 'function', inputs: [] }], contractAddress)
const chainId = await web3Home.eth.getChainId()
const data = contract.methods.fetchInterest().encodeABI()
const senderAddress = privateKeyToAddress(privateKey)
console.log(
`Initialized, chainId=${chainId}, data=${data}, contract=${contractAddress}, interval=${interval / 1000}s`
)
await setIntervalAndRun(async () => {
let gasLimit
try {
gasLimit = await contract.methods.fetchInterest().estimateGas()
} catch (e) {
console.log('Gas limit estimation failed, will retry later', new Date())
return
}
const nonce = await web3Home.eth.getTransactionCount(senderAddress)
const txHash = await sendTx({
privateKey,
to: contractAddress,
data,
nonce,
gasPrice,
gasLimit: Math.round(gasLimit * 1.5),
value: '0',
chainId,
web3: web3Home
})
console.log('Sent transaction with fetch interest', txHash, new Date())
}, interval)
}
main()

View File

@@ -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)
@@ -174,13 +174,13 @@ async function sendJobTx(jobs) {
const txHash = await sendTx({
data: job.data,
nonce,
gasPrice,
amount: '0',
value: '0',
gasLimit,
privateKey: config.validatorPrivateKey,
to: job.to,
chainId,
web3
web3,
gasPriceOptions
})
nonce++
@@ -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}.`
)

View File

@@ -15,7 +15,7 @@ async function estimateGas({ web3, homeBridge, validatorContract, message, addre
const gasEstimate = await homeBridge.methods.executeAffirmation(message).estimateGas({
from: address
})
const msgGasLimit = parseAMBHeader(message).gasLimit
const msgGasLimit = Math.ceil((parseAMBHeader(message).gasLimit * 64) / 63)
// message length in bytes
const len = strip0x(message).length / 2 - MIN_AMB_HEADER_LENGTH

View File

@@ -4,7 +4,6 @@ const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError
const logger = require('../../services/logger').child({
module: 'processCollectedSignatures:estimateGas'
})
const { parseAMBHeader } = require('../../utils/message')
const web3 = new Web3()
const { toBN } = Web3.utils
@@ -22,15 +21,9 @@ async function estimateGas({
address
}) {
try {
const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({
return await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({
from: address
})
const msgGasLimit = parseAMBHeader(message).gasLimit
// + estimateExtraGas(len)
// is not needed here, since estimateGas will already take into account gas
// needed for memory expansion, message processing, etc.
return gasEstimate + msgGasLimit
} catch (e) {
if (e instanceof HttpListProviderError) {
throw e

View File

@@ -0,0 +1,183 @@
require('dotenv').config()
const promiseLimit = require('promise-limit')
const { HttpListProviderError } = require('../../services/HttpListProvider')
const { getValidatorContract } = require('../../tx/web3')
const rootLogger = require('../../services/logger')
const { signatureToVRS, packSignatures } = require('../../utils/message')
const { readAccessListFile, isRevertError } = require('../../utils/utils')
const { parseAMBMessage } = require('../../../../commons')
const estimateGas = require('../processAMBCollectedSignatures/estimateGas')
const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors')
const { MAX_CONCURRENT_EVENTS, EXTRA_GAS_ABSOLUTE } = require('../../utils/constants')
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
const { ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST, ORACLE_HOME_TO_FOREIGN_BLOCK_LIST } = process.env
const ORACLE_HOME_SKIP_MANUAL_LANE = process.env.ORACLE_HOME_SKIP_MANUAL_LANE === 'true'
function processCollectedSignaturesBuilder(config) {
const { home, foreign, mevForeign } = config
let validatorContract = null
return async function processCollectedSignatures(signatures) {
const txToSend = []
if (validatorContract === null) {
validatorContract = await getValidatorContract(foreign.bridgeContract, foreign.web3)
}
rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`)
const callbacks = signatures
.map(colSignature => async () => {
const { messageHash, NumberOfCollectedSignatures } = colSignature.returnValues
const logger = rootLogger.child({
eventTransactionHash: colSignature.transactionHash
})
logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`)
const message = await home.bridgeContract.methods.message(messageHash).call()
const parsedMessage = parseAMBMessage(message)
if (ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST || ORACLE_HOME_TO_FOREIGN_BLOCK_LIST) {
const sender = parsedMessage.sender.toLowerCase()
const executor = parsedMessage.executor.toLowerCase()
if (ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST) {
const allowanceList = await readAccessListFile(ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST, logger)
if (!allowanceList.includes(executor) && !allowanceList.includes(sender)) {
logger.info(
{ sender, executor },
'Validator skips a message. Neither sender nor executor addresses are in the allowance list.'
)
return
}
} else if (ORACLE_HOME_TO_FOREIGN_BLOCK_LIST) {
const blockList = await readAccessListFile(ORACLE_HOME_TO_FOREIGN_BLOCK_LIST, logger)
if (blockList.includes(executor)) {
logger.info({ executor }, 'Validator skips a message. Executor address is in the block list.')
return
}
if (blockList.includes(sender)) {
logger.info({ sender }, 'Validator skips a message. Sender address is in the block list.')
return
}
}
}
if (ORACLE_HOME_SKIP_MANUAL_LANE && parsedMessage.decodedDataType.manualLane) {
logger.info(
{ dataType: parsedMessage.dataType },
'Validator skips a message. Message was forwarded to the manual lane by the extension'
)
return
}
logger.debug({ NumberOfCollectedSignatures }, 'Number of signatures to get')
const requiredSignatures = []
requiredSignatures.length = NumberOfCollectedSignatures
requiredSignatures.fill(0)
const signaturesArray = []
const [v, r, s] = [[], [], []]
logger.debug('Getting message signatures')
const signaturePromises = requiredSignatures.map(async (el, index) => {
logger.debug({ index }, 'Getting message signature')
const signature = await home.bridgeContract.methods.signature(messageHash, index).call()
const vrs = signatureToVRS(signature)
v.push(vrs.v)
r.push(vrs.r)
s.push(vrs.s)
signaturesArray.push(vrs)
})
await Promise.all(signaturePromises)
const signatures = packSignatures(signaturesArray)
logger.info(`Processing messageId: ${parsedMessage.messageId}`)
let gasEstimate
try {
logger.debug('Estimate gas')
gasEstimate = await estimateGas({
foreignBridge: foreign.bridgeContract,
validatorContract,
v,
r,
s,
signatures,
message,
numberOfCollectedSignatures: NumberOfCollectedSignatures,
messageId: parsedMessage.messageId,
address: config.validatorAddress
})
logger.debug({ gasEstimate }, 'Gas estimated')
} catch (e) {
if (e instanceof HttpListProviderError) {
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
} else if (e instanceof AlreadyProcessedError) {
logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`)
return
} else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) {
logger.error(`The message couldn't be processed; skipping: ${e.message}`)
return
} else {
logger.error(e, 'Unknown error while processing transaction')
throw e
}
}
const executeData = foreign.bridgeContract.methods.executeSignatures(message, signatures).encodeABI()
const profit = await estimateProfit(
mevForeign.contract,
mevForeign.minGasPrice,
executeData,
mevForeign.flatMinerFee
)
if (profit === '0') {
logger.error('No MEV opportunity found when testing with min gas price, skipping job')
return
}
logger.info(`Estimated profit of ${profit} when simulating with ${mevForeign.minGasPrice} gas price`)
txToSend.push({
profit,
executeData,
data: mevForeign.contract.methods.execute(executeData).encodeABI(),
gasEstimate,
extraGas: EXTRA_GAS_ABSOLUTE,
maxFeePerGas: mevForeign.maxFeePerGas,
maxPriorityFeePerGas: mevForeign.maxPriorityFeePerGas,
transactionReference: colSignature.transactionHash,
to: mevForeign.contractAddress,
value: mevForeign.flatMinerFee
})
})
.map(promise => limit(promise))
await Promise.all(callbacks)
return txToSend
}
}
async function estimateProfit(contract, gasPrice, data, minerFee) {
return contract.methods
.estimateProfit(gasPrice, data)
.call({ value: minerFee })
.then(
res => res.toString(),
e => {
if (isRevertError(e)) {
return '0'
}
throw e
}
)
}
module.exports = {
processCollectedSignaturesBuilder,
estimateProfit
}

View File

@@ -24,7 +24,8 @@ async function estimateGas({
// message length in bytes
const len = strip0x(result).length / 2
const callbackGasLimit = parseInt(await homeBridge.methods.maxGasPerTx().call(), 10)
let callbackGasLimit = parseInt(await homeBridge.methods.maxGasPerTx().call(), 10)
callbackGasLimit = Math.ceil((callbackGasLimit * 64) / 63)
return gasEstimate + callbackGasLimit + estimateExtraGas(len)
} catch (e) {

View File

@@ -6,11 +6,9 @@ const logger = require('../../services/logger').child({
async function estimateGas({ web3, homeBridge, validatorContract, recipient, value, txHash, address }) {
try {
const gasEstimate = await homeBridge.methods.executeAffirmation(recipient, value, txHash).estimateGas({
return await homeBridge.methods.executeAffirmation(recipient, value, txHash).estimateGas({
from: address
})
return gasEstimate
} catch (e) {
if (e instanceof HttpListProviderError) {
throw e

View File

@@ -20,8 +20,7 @@ async function estimateGas({
signatures
}) {
try {
const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas()
return gasEstimate
return await foreignBridge.methods.executeSignatures(message, signatures).estimateGas()
} catch (e) {
if (e instanceof HttpListProviderError) {
throw e

View File

@@ -6,10 +6,9 @@ const logger = require('../../services/logger').child({
async function estimateGas({ web3, homeBridge, validatorContract, signature, message, address }) {
try {
const gasEstimate = await homeBridge.methods.submitSignature(signature, message).estimateGas({
return await homeBridge.methods.submitSignature(signature, message).estimateGas({
from: address
})
return gasEstimate
} catch (e) {
if (e instanceof HttpListProviderError) {
throw e

178
oracle/src/mevSender.js Normal file
View File

@@ -0,0 +1,178 @@
require('../env')
const path = require('path')
const BigNumber = require('bignumber.js')
const { redis } = require('./services/redisClient')
const logger = require('./services/logger')
const { sendTx } = require('./tx/sendTx')
const { getNonce, getChainId, getBlock } = require('./tx/web3')
const { addExtraGas, checkHTTPS, watchdog } = require('./utils/utils')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
const { estimateProfit } = require('./events/processAMBCollectedSignaturesMEV')
if (process.argv.length < 3) {
logger.error('Please check the number of arguments, config file was not provided')
process.exit(EXIT_CODES.GENERAL_ERROR)
}
const config = require(path.join('../config/', process.argv[2]))
const GasPrice = require('./services/gasPrice')
const { web3, mevForeign, validatorAddress } = config
const FAKE_MEV_RELAY_TX_SENDING = process.env.FAKE_MEV_RELAY_TX_SENDING === 'true'
const FAKE_MEV_RELAY_NO_EIP1559 = process.env.FAKE_MEV_RELAY_NO_EIP1559 === 'true'
let chainId = 0
let flashbotsProvider
async function initialize() {
try {
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
web3.currentProvider.urls.forEach(checkHttps(config.id))
chainId = await getChainId(web3)
if (!FAKE_MEV_RELAY_TX_SENDING) {
flashbotsProvider = await mevForeign.getFlashbotsProvider(chainId)
}
return runMain()
} catch (e) {
logger.error(e.message)
process.exit(EXIT_CODES.GENERAL_ERROR)
}
}
async function runMain() {
try {
if (redis.status === 'ready') {
if (config.maxProcessingTime) {
await watchdog(main, config.maxProcessingTime, () => {
logger.fatal('Max processing time reached')
process.exit(EXIT_CODES.MAX_TIME_REACHED)
})
} else {
await main()
}
}
} catch (e) {
logger.error(e)
}
setTimeout(runMain, config.pollingInterval)
}
async function main() {
try {
const jobs = Object.values(await redis.hgetall(config.mevJobsRedisKey)).map(JSON.parse)
const totalJobs = jobs.length
if (totalJobs === 0) {
logger.debug('Nothing to process')
return
}
const { baseFeePerGas, number: pendingBlockNumber } = await getBlock(web3, 'pending')
let pendingBaseFee = baseFeePerGas
if (FAKE_MEV_RELAY_TX_SENDING && FAKE_MEV_RELAY_NO_EIP1559) {
// emulate baseFee with the current gasPrice
await GasPrice.start('foreign', web3, true)
pendingBaseFee = GasPrice.gasPriceOptions().gasPrice
}
const bestJob = pickBestJob(jobs, pendingBaseFee)
if (!bestJob) {
logger.info({ totalJobs, pendingBaseFee }, 'No suitable job was found, waiting for a lower gas price')
return
}
const jobLogger = logger.child({ eventTransactionHash: bestJob.transactionReference })
const maxProfit = await estimateProfit(
mevForeign.contract,
mevForeign.minGasPrice,
bestJob.executeData,
bestJob.value
)
if (maxProfit === '0') {
jobLogger.info(`No MEV opportunity found when testing with min gas price ${mevForeign.minGasPrice}, removing job`)
await redis.hdel(config.mevJobsRedisKey, bestJob.transactionReference)
return
}
jobLogger.info(`Estimated profit of ${maxProfit} when simulating with ${mevForeign.minGasPrice} gas price`)
bestJob.profit = maxProfit
if (new BigNumber(pendingBaseFee).gt(mevForeign.minGasPrice)) {
const profit = await estimateProfit(mevForeign.contract, pendingBaseFee, bestJob.executeData, bestJob.value)
if (profit === '0') {
jobLogger.info(
`No MEV opportunity found when testing with current gas price ${pendingBaseFee}, waiting for lower gas price`
)
bestJob.maxFeePerGas = pendingBaseFee
await redis.hset(config.mevJobsRedisKey, bestJob.transactionReference, JSON.stringify(bestJob))
return
}
jobLogger.info(`Estimated profit of ${profit} when simulating with ${pendingBaseFee} gas price`)
}
let gasLimit
if (typeof bestJob.extraGas === 'number') {
gasLimit = addExtraGas(bestJob.gasEstimate + bestJob.extraGas, 0, MAX_GAS_LIMIT)
} else {
gasLimit = addExtraGas(bestJob.gasEstimate, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT)
}
const nonce = await getNonce(web3, validatorAddress)
jobLogger.info(
{ nonce, fromBlock: pendingBlockNumber, toBlock: pendingBlockNumber + mevForeign.bundlesPerIteration - 1 },
'Sending MEV bundles'
)
const opts = {
data: bestJob.data,
nonce,
value: bestJob.value,
gasLimit,
privateKey: config.validatorPrivateKey,
to: bestJob.to,
chainId,
web3,
gasPriceOptions: {
maxFeePerGas: bestJob.maxFeePerGas,
maxPriorityFeePerGas: bestJob.maxPriorityFeePerGas
},
mevOptions: {
provider: flashbotsProvider,
fromBlock: pendingBlockNumber,
toBlock: pendingBlockNumber + mevForeign.bundlesPerIteration - 1,
logger
}
}
if (FAKE_MEV_RELAY_TX_SENDING) {
if (FAKE_MEV_RELAY_NO_EIP1559) {
opts.gasPriceOptions = { gasPrice: pendingBaseFee }
}
delete opts.mevOptions
}
const txHash = await sendTx(opts)
jobLogger.info({ txHash }, `Tx generated ${txHash} for event Tx ${bestJob.transactionReference}`)
await redis.hset(config.mevJobsRedisKey, bestJob.transactionReference, JSON.stringify(bestJob))
jobLogger.debug(`Finished processing msg`)
} catch (e) {
logger.error(e)
}
}
function pickBestJob(jobs, feePerGas) {
const feePerGasBN = new BigNumber(feePerGas)
let best = null
jobs.forEach(job => {
if (feePerGasBN.lt(job.maxFeePerGas) && (!best || new BigNumber(best.profit).lt(job.profit))) {
best = job
}
})
return best
}
initialize()

251
oracle/src/mevWatcher.js Normal file
View File

@@ -0,0 +1,251 @@
require('../env')
const path = require('path')
const { redis } = require('./services/redisClient')
const logger = require('./services/logger')
const { getBlockNumber, getRequiredBlockConfirmations, getEvents } = require('./tx/web3')
const { checkHTTPS, watchdog, syncForEach } = require('./utils/utils')
const { processCollectedSignaturesBuilder } = require('./events/processAMBCollectedSignaturesMEV')
const {
EXIT_CODES,
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT,
MAX_HISTORY_BLOCK_TO_REPROCESS
} = require('./utils/constants')
if (process.argv.length < 3) {
logger.error('Please check the number of arguments, config file was not provided')
process.exit(EXIT_CODES.GENERAL_ERROR)
}
const config = require(path.join('../config/', process.argv[2]))
const processAMBCollectedSignaturesMEV = processCollectedSignaturesBuilder(config)
const {
web3,
bridgeContract,
eventContract,
startBlock,
pollingInterval,
chain,
reprocessingOptions,
blockPollingLimit
} = config.main
const lastBlockRedisKey = `${config.id}:lastProcessedBlock`
const lastReprocessedBlockRedisKey = `${config.id}:lastReprocessedBlock`
const seenEventsRedisKey = `${config.id}:seenEvents`
const mevJobsRedisKey = `${config.id}:mevJobs`
let lastProcessedBlock = Math.max(startBlock - 1, 0)
let lastReprocessedBlock
let lastSeenBlockNumber = 0
let sameBlockNumberCounter = 0
async function initialize() {
try {
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
web3.currentProvider.urls.forEach(checkHttps(chain))
await getLastProcessedBlock()
await getLastReprocessedBlock()
runMain({ sendToQueue: saveJobsToRedis })
} catch (e) {
logger.error(e)
process.exit(EXIT_CODES.GENERAL_ERROR)
}
}
async function runMain({ sendToQueue }) {
try {
if (redis.status === 'ready') {
if (config.maxProcessingTime) {
await watchdog(() => main({ sendToQueue }), config.maxProcessingTime, () => {
logger.fatal('Max processing time reached')
process.exit(EXIT_CODES.MAX_TIME_REACHED)
})
} else {
await main({ sendToQueue })
}
}
} catch (e) {
logger.error(e)
}
setTimeout(() => {
runMain({ sendToQueue })
}, pollingInterval)
}
async function saveJobsToRedis(jobs) {
return syncForEach(jobs, job => redis.hset(mevJobsRedisKey, job.transactionReference, JSON.stringify(job)))
}
async function getLastProcessedBlock() {
const result = await redis.get(lastBlockRedisKey)
logger.debug({ fromRedis: result, fromConfig: lastProcessedBlock }, 'Last Processed block obtained')
lastProcessedBlock = result ? parseInt(result, 10) : lastProcessedBlock
}
async function getLastReprocessedBlock() {
if (reprocessingOptions.enabled) {
const result = await redis.get(lastReprocessedBlockRedisKey)
if (result) {
lastReprocessedBlock = Math.max(parseInt(result, 10), lastProcessedBlock - MAX_HISTORY_BLOCK_TO_REPROCESS)
} else {
lastReprocessedBlock = lastProcessedBlock
}
logger.debug({ block: lastReprocessedBlock }, 'Last reprocessed block obtained')
} else {
// when reprocessing is being enabled not for the first time,
// we do not want to process blocks for which we didn't recorded seen events,
// instead, we want to start from the current block.
// Thus we should delete this reprocessing pointer once it is disabled.
await redis.del(lastReprocessedBlockRedisKey)
}
}
function updateLastProcessedBlock(lastBlockNumber) {
lastProcessedBlock = lastBlockNumber
return redis.set(lastBlockRedisKey, lastProcessedBlock)
}
function updateLastReprocessedBlock(lastBlockNumber) {
lastReprocessedBlock = lastBlockNumber
return redis.set(lastReprocessedBlockRedisKey, lastReprocessedBlock)
}
function processEvents(events) {
switch (config.id) {
case 'amb-collected-signatures-mev':
return processAMBCollectedSignaturesMEV(events)
default:
return []
}
}
const eventKey = e => `${e.transactionHash}-${e.logIndex}`
async function reprocessOldLogs(sendToQueue) {
const fromBlock = lastReprocessedBlock + 1
const toBlock = lastReprocessedBlock + reprocessingOptions.batchSize
const events = await getEvents({
contract: eventContract,
event: config.event,
fromBlock,
toBlock,
filter: config.eventFilter
})
const alreadySeenEvents = await getSeenEvents(fromBlock, toBlock)
const missingEvents = events.filter(e => !alreadySeenEvents[eventKey(e)])
if (missingEvents.length === 0) {
logger.debug('No missed events were found')
} else {
logger.info(`Found ${missingEvents.length} ${config.event} missed events`)
const job = await processEvents(missingEvents)
logger.info('Missed events transactions to send:', job.length)
if (job.length) {
await sendToQueue(job)
}
}
await updateLastReprocessedBlock(toBlock)
await deleteSeenEvents(0, toBlock)
}
async function getSeenEvents(fromBlock, toBlock) {
const keys = await redis.zrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
const res = {}
keys.forEach(k => {
res[k] = true
})
return res
}
function deleteSeenEvents(fromBlock, toBlock) {
return redis.zremrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
}
function addSeenEvents(events) {
return redis.zadd(seenEventsRedisKey, ...events.flatMap(e => [e.blockNumber, eventKey(e)]))
}
async function getLastBlockToProcess(web3, bridgeContract) {
const [lastBlockNumber, requiredBlockConfirmations] = await Promise.all([
getBlockNumber(web3),
getRequiredBlockConfirmations(bridgeContract)
])
if (lastBlockNumber < lastSeenBlockNumber) {
sameBlockNumberCounter = 0
logger.warn({ lastBlockNumber, lastSeenBlockNumber }, 'Received block number less than already seen block')
web3.currentProvider.switchToFallbackRPC()
} else if (lastBlockNumber === lastSeenBlockNumber) {
sameBlockNumberCounter++
if (sameBlockNumberCounter > 1) {
logger.info({ lastBlockNumber, sameBlockNumberCounter }, 'Received the same block number more than twice')
if (sameBlockNumberCounter >= BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT) {
sameBlockNumberCounter = 0
logger.warn(
{ lastBlockNumber, n: BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT },
'Received the same block number for too many times. Probably node is not synced anymore'
)
web3.currentProvider.switchToFallbackRPC()
}
}
} else {
sameBlockNumberCounter = 0
lastSeenBlockNumber = lastBlockNumber
}
return lastBlockNumber - requiredBlockConfirmations
}
async function main({ sendToQueue }) {
try {
const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract)
if (reprocessingOptions.enabled) {
if (lastReprocessedBlock + reprocessingOptions.batchSize + reprocessingOptions.blockDelay < lastBlockToProcess) {
await reprocessOldLogs(sendToQueue)
return
}
}
if (lastBlockToProcess <= lastProcessedBlock) {
logger.debug('All blocks already processed')
return
}
const fromBlock = lastProcessedBlock + 1
const rangeEndBlock = blockPollingLimit ? fromBlock + blockPollingLimit : lastBlockToProcess
const toBlock = Math.min(lastBlockToProcess, rangeEndBlock)
const events = await getEvents({
contract: eventContract,
event: config.event,
fromBlock,
toBlock,
filter: config.eventFilter
})
logger.info(`Found ${events.length} ${config.event} events`)
if (events.length) {
const job = await processEvents(events)
logger.info('Transactions to send:', job.length)
if (job.length) {
await sendToQueue(job)
}
if (reprocessingOptions.enabled) {
await addSeenEvents(events)
}
}
logger.debug({ lastProcessedBlock: toBlock.toString() }, 'Updating last processed block')
await updateLastProcessedBlock(toBlock)
} catch (e) {
logger.error(e)
}
logger.debug('Finished')
}
initialize()

View File

@@ -9,6 +9,8 @@ const { sendTx } = require('./tx/sendTx')
const { getNonce, getChainId } = require('./tx/web3')
const {
addExtraGas,
applyMinGasFeeBump,
chooseGasPriceOptions,
checkHTTPS,
syncForEach,
waitForFunds,
@@ -19,7 +21,7 @@ const {
isInsufficientBalanceError,
isNonceError
} = require('./utils/utils')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT, MIN_GAS_PRICE_BUMP_FACTOR } = require('./utils/constants')
const { ORACLE_TX_REDUNDANCY } = process.env
@@ -42,7 +44,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()
@@ -55,7 +57,6 @@ async function initialize() {
function connectQueue() {
connectSenderToQueue({
queueName: config.queue,
oldQueueName: config.oldQueue,
resendInterval: config.resendInterval,
cb: options => {
if (config.maxProcessingTime) {
@@ -121,7 +122,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
@@ -147,6 +148,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
}
try {
const newGasPriceOptions = chooseGasPriceOptions(gasPriceOptions, job.gasPriceOptions)
if (isResend) {
const tx = await web3Fallback.eth.getTransaction(job.txHash)
@@ -159,24 +161,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(newGasPriceOptions)
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',
value: '0',
gasLimit,
privateKey: config.validatorPrivateKey,
to: job.to,
chainId,
web3: web3Redundant
web3: web3Redundant,
gasPriceOptions: newGasPriceOptions
})
const resendJob = {
...job,
txHash,
gasPrice
gasPriceOptions: newGasPriceOptions
}
resendJobs.push(resendJob)
@@ -194,8 +198,8 @@ 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)
failedTx.push(job)
GasPrice.start(config.id, web3)
failedTx.push(applyMinGasFeeBump(job, MIN_GAS_PRICE_BUMP_FACTOR))
} else if (isResend || isSameTransactionError(e)) {
resendJobs.push(job)
} else {
@@ -208,7 +212,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}.`
)

View File

@@ -40,23 +40,9 @@ function connectWatcherToQueue({ queueName, cb }) {
cb({ sendToQueue, channel: channelWrapper })
}
function connectSenderToQueue({ queueName, oldQueueName, cb, resendInterval }) {
function connectSenderToQueue({ queueName, cb, resendInterval }) {
const deadLetterExchange = `${queueName}-retry`
async function resendMessagesToNewQueue(channel) {
logger.info(`Trying to check messages in the old non-priority queue ${queueName}`)
while (true) {
const msg = await channel.get(oldQueueName)
if (msg === false) {
logger.info(`No messages in the old queue ${oldQueueName} left`)
break
}
logger.debug(`Message in the old queue ${oldQueueName} was found, redirecting it to the new queue ${queueName}`)
await channel.sendToQueue(queueName, msg.content, { persistent: true, priority: SENDER_QUEUE_SEND_PRIORITY })
await channel.ack(msg)
}
}
const channelWrapper = connection.createChannel({
json: true
})
@@ -64,7 +50,6 @@ function connectSenderToQueue({ queueName, oldQueueName, cb, resendInterval }) {
channelWrapper.addSetup(async channel => {
await channel.assertExchange(deadLetterExchange, 'fanout', { durable: true })
await channel.assertQueue(queueName, { durable: true, maxPriority: SENDER_QUEUE_MAX_PRIORITY })
await channel.assertQueue(oldQueueName, { durable: true }).then(() => resendMessagesToNewQueue(channel))
await channel.bindQueue(queueName, deadLetterExchange)
await channel.prefetch(1)
await channel.consume(queueName, msg =>

View File

@@ -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
@@ -49,7 +49,7 @@ 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
@@ -57,7 +57,7 @@ async function start(chainId, fetchOnce) {
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}'`)
}
@@ -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
}

View File

@@ -1,4 +1,6 @@
const Web3 = require('web3')
const ethers = require('ethers')
const flashbots = require('@flashbots/ethers-provider-bundle')
const { HttpListProvider } = require('./HttpListProvider')
const { SafeEthLogsProvider } = require('./SafeEthLogsProvider')
const { RedundantHttpListProvider } = require('./RedundantHttpListProvider')
@@ -9,6 +11,8 @@ const {
COMMON_FOREIGN_RPC_URL,
ORACLE_SIDE_RPC_URL,
ORACLE_FOREIGN_ARCHIVE_RPC_URL,
ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL,
ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY,
ORACLE_RPC_REQUEST_TIMEOUT,
ORACLE_HOME_RPC_POLLING_INTERVAL,
ORACLE_FOREIGN_RPC_POLLING_INTERVAL
@@ -94,6 +98,15 @@ if (foreignUrls.length > 1) {
web3ForeignRedundant = new Web3(redundantProvider)
}
let getFlashbotsProvider
if (ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL) {
const provider = new ethers.providers.JsonRpcProvider(foreignUrls[0])
const authSigner = new ethers.Wallet(ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY, provider)
getFlashbotsProvider = chainId =>
flashbots.FlashbotsBundleProvider.create(provider, authSigner, ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL, chainId)
}
module.exports = {
web3Home,
web3Foreign,
@@ -102,5 +115,6 @@ module.exports = {
web3HomeRedundant,
web3ForeignRedundant,
web3HomeFallback,
web3ForeignFallback
web3ForeignFallback,
getFlashbotsProvider
}

View File

@@ -1,25 +1,38 @@
const { toWei } = require('web3').utils
async function sendTx({ privateKey, data, nonce, gasPrice, amount, gasLimit, to, chainId, web3 }) {
async function sendTx(opts) {
const { privateKey, data, nonce, gasPrice, gasPriceOptions, value, gasLimit, to, chainId, web3, mevOptions } = opts
const gasOpts = gasPriceOptions || { gasPrice }
const serializedTx = await web3.eth.accounts.signTransaction(
{
nonce: Number(nonce),
chainId,
to,
data,
value: toWei(amount),
gasPrice,
gas: gasLimit
value,
gas: gasLimit,
...gasOpts
},
privateKey
)
return new Promise((res, rej) =>
web3.eth
.sendSignedTransaction(serializedTx.rawTransaction)
.once('transactionHash', res)
.once('error', rej)
if (!mevOptions) {
return new Promise((res, rej) =>
web3.eth
.sendSignedTransaction(serializedTx.rawTransaction)
.once('transactionHash', res)
.once('error', rej)
)
}
mevOptions.logger.debug(
{ rawTx: serializedTx.rawTransaction, txHash: serializedTx.transactionHash },
'Signed MEV helper transaction'
)
for (let blockNumber = mevOptions.fromBlock; blockNumber <= mevOptions.toBlock; blockNumber++) {
mevOptions.logger.debug({ txHash: serializedTx.transactionHash, blockNumber }, 'Sending MEV bundle transaction')
await mevOptions.provider.sendRawBundle([serializedTx.rawTransaction], blockNumber)
}
return Promise.resolve(serializedTx.transactionHash)
}
module.exports = {

View File

@@ -87,7 +87,7 @@ async function getEvents({ contract, event, fromBlock, toBlock, filter }) {
)
const pastEvents = await contract.getPastEvents(event, { fromBlock, toBlock, filter })
logger.debug({ contractAddress, event, count: pastEvents.length }, 'Past events obtained')
return pastEvents
return pastEvents.sort((a, b) => a.blockNumber - b.blockNumber || a.transactionIndex - b.transactionIndex)
} catch (e) {
logger.error(e.message)
throw new Error(`${event} events cannot be obtained`)

View File

@@ -1,10 +1,11 @@
module.exports = {
EXTRA_GAS_PERCENTAGE: 4,
EXTRA_GAS_ABSOLUTE: 200000,
EXTRA_GAS_ABSOLUTE: 250000,
AMB_AFFIRMATION_REQUEST_EXTRA_GAS_ESTIMATOR: len => Math.floor(0.0035 * len ** 2 + 40 * len),
MIN_AMB_HEADER_LENGTH: 32 + 20 + 20 + 4 + 2 + 1 + 2,
MAX_GAS_LIMIT: 10000000,
MAX_CONCURRENT_EVENTS: 50,
MAX_HISTORY_BLOCK_TO_REPROCESS: 10000,
RETRY_CONFIG: {
retries: 20,
factor: 1.4,
@@ -23,6 +24,7 @@ module.exports = {
MIN: 1,
MAX: 1000
},
MIN_GAS_PRICE_BUMP_FACTOR: 0.1,
DEFAULT_TRANSACTION_RESEND_INTERVAL: 20 * 60 * 1000,
FALLBACK_RPC_URL_SWITCH_TIMEOUT: 60 * 60 * 1000,
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT: 10,

43
oracle/src/utils/mev.js Normal file
View File

@@ -0,0 +1,43 @@
const MEV_HELPER_ABI = [
{
constant: false,
inputs: [
{
name: '_data',
type: 'bytes'
}
],
name: 'execute',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_gasPrice',
type: 'uint256'
},
{
name: '_data',
type: 'bytes'
}
],
name: 'estimateProfit',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: true,
stateMutability: 'nonpayable',
type: 'function'
}
]
module.exports = {
MEV_HELPER_ABI
}

View File

@@ -2,6 +2,9 @@ const fs = require('fs')
const BigNumber = require('bignumber.js')
const promiseRetry = require('promise-retry')
const Web3 = require('web3')
const { GAS_PRICE_BOUNDARIES } = require('./constants')
const { toBN, toWei } = Web3.utils
const retrySequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 60]
@@ -34,8 +37,8 @@ const promiseRetryForever = f => promiseRetry(f, { forever: true, factor: 1 })
async function waitForFunds(web3, address, minimumBalance, cb, logger) {
promiseRetryForever(async retry => {
logger.debug('Getting balance of validator account')
const newBalance = web3.utils.toBN(await web3.eth.getBalance(address))
if (newBalance.gte(web3.utils.toBN(minimumBalance.toString(10)))) {
const newBalance = toBN(await web3.eth.getBalance(address))
if (newBalance.gte(toBN(minimumBalance.toString(10)))) {
logger.debug({ balance: newBalance, minimumBalance }, 'Validator has minimum necessary balance')
cb(newBalance)
} else {
@@ -64,6 +67,48 @@ function addExtraGas(gas, extraPercentage, maxGasLimit = Infinity) {
return BigNumber.min(maxGasLimit, gasWithExtra)
}
function applyMinGasFeeBump(job, bumpFactor = 0.1) {
if (!job.gasPriceOptions) {
return job
}
const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = job.gasPriceOptions
const maxGasPrice = toWei(GAS_PRICE_BOUNDARIES.MAX.toString(), 'gwei')
if (gasPrice) {
return {
...job,
gasPriceOptions: {
gasPrice: addExtraGas(gasPrice, bumpFactor, maxGasPrice).toString()
}
}
}
if (maxFeePerGas && maxPriorityFeePerGas) {
return {
...job,
gasPriceOptions: {
maxFeePerGas: addExtraGas(maxFeePerGas, bumpFactor, maxGasPrice).toString(),
maxPriorityFeePerGas: addExtraGas(maxPriorityFeePerGas, bumpFactor, maxGasPrice).toString()
}
}
}
return job
}
function chooseGasPriceOptions(a, b) {
if (!a) {
return b
}
if (a && b && a.gasPrice && b.gasPrice) {
return { gasPrice: BigNumber.max(a.gasPrice, b.gasPrice).toString() }
}
if (a && b && a.maxFeePerGas && b.maxFeePerGas && a.maxPriorityFeePerGas && b.maxPriorityFeePerGas) {
return {
maxFeePerGas: BigNumber.max(a.maxFeePerGas, b.maxFeePerGas).toString(),
maxPriorityFeePerGas: BigNumber.max(a.maxPriorityFeePerGas, b.maxPriorityFeePerGas).toString()
}
}
return a
}
async function setIntervalAndRun(f, interval) {
const handler = setInterval(f, interval)
await f()
@@ -183,6 +228,8 @@ module.exports = {
waitForFunds,
waitForUnsuspend,
addExtraGas,
chooseGasPriceOptions,
applyMinGasFeeBump,
setIntervalAndRun,
watchdog,
add0xPrefix,

View File

@@ -6,7 +6,11 @@ const logger = require('./services/logger')
const { getShutdownFlag } = require('./services/shutdownState')
const { getBlockNumber, getRequiredBlockConfirmations, getEvents } = require('./tx/web3')
const { checkHTTPS, watchdog } = require('./utils/utils')
const { EXIT_CODES, BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT } = require('./utils/constants')
const {
EXIT_CODES,
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT,
MAX_HISTORY_BLOCK_TO_REPROCESS
} = require('./utils/constants')
if (process.argv.length < 3) {
logger.error('Please check the number of arguments, config file was not provided')
@@ -26,9 +30,21 @@ const processAMBInformationRequests = require('./events/processAMBInformationReq
const { getTokensState } = require('./utils/tokenState')
const { web3, bridgeContract, eventContract, startBlock, pollingInterval, chain } = config.main
const {
web3,
bridgeContract,
eventContract,
startBlock,
pollingInterval,
chain,
reprocessingOptions,
blockPollingLimit
} = config.main
const lastBlockRedisKey = `${config.id}:lastProcessedBlock`
const lastReprocessedBlockRedisKey = `${config.id}:lastReprocessedBlock`
const seenEventsRedisKey = `${config.id}:seenEvents`
let lastProcessedBlock = Math.max(startBlock - 1, 0)
let lastReprocessedBlock
let lastSeenBlockNumber = 0
let sameBlockNumberCounter = 0
@@ -39,6 +55,8 @@ async function initialize() {
web3.currentProvider.urls.forEach(checkHttps(chain))
await getLastProcessedBlock()
await getLastReprocessedBlock()
await checkConditions()
connectWatcherToQueue({
queueName: config.queue,
cb: runMain
@@ -76,11 +94,34 @@ async function getLastProcessedBlock() {
lastProcessedBlock = result ? parseInt(result, 10) : lastProcessedBlock
}
async function getLastReprocessedBlock() {
if (reprocessingOptions.enabled) {
const result = await redis.get(lastReprocessedBlockRedisKey)
if (result) {
lastReprocessedBlock = Math.max(parseInt(result, 10), lastProcessedBlock - MAX_HISTORY_BLOCK_TO_REPROCESS)
} else {
lastReprocessedBlock = lastProcessedBlock
}
logger.debug({ block: lastReprocessedBlock }, 'Last reprocessed block obtained')
} else {
// when reprocessing is being enabled not for the first time,
// we do not want to process blocks for which we didn't recorded seen events,
// instead, we want to start from the current block.
// Thus we should delete this reprocessing pointer once it is disabled.
await redis.del(lastReprocessedBlockRedisKey)
}
}
function updateLastProcessedBlock(lastBlockNumber) {
lastProcessedBlock = lastBlockNumber
return redis.set(lastBlockRedisKey, lastProcessedBlock)
}
function updateLastReprocessedBlock(lastBlockNumber) {
lastReprocessedBlock = lastBlockNumber
return redis.set(lastReprocessedBlockRedisKey, lastReprocessedBlock)
}
function processEvents(events) {
switch (config.id) {
case 'erc-native-signature-request':
@@ -114,6 +155,71 @@ async function checkConditions() {
}
}
const eventKey = e => `${e.transactionHash}-${e.logIndex}`
async function reprocessOldLogs(sendToQueue) {
const fromBlock = lastReprocessedBlock + 1
let toBlock = lastReprocessedBlock + reprocessingOptions.batchSize
const events = await getEvents({
contract: eventContract,
event: config.event,
fromBlock,
toBlock,
filter: config.eventFilter
})
const alreadySeenEvents = await getSeenEvents(fromBlock, toBlock)
const missingEvents = events.filter(e => !alreadySeenEvents[eventKey(e)])
if (missingEvents.length === 0) {
logger.debug('No missed events were found')
} else {
logger.info(`Found ${missingEvents.length} ${config.event} missed events`)
let job
if (config.id === 'amb-information-request') {
// obtain block number and events from the earliest block
const batchBlockNumber = missingEvents[0].blockNumber
const batchEvents = missingEvents.filter(event => event.blockNumber === batchBlockNumber)
// if there are some other events in the later blocks,
// adjust lastReprocessedBlock so that these events will be processed again on the next iteration
if (batchEvents.length < missingEvents.length) {
// pick event outside from the batch
toBlock = missingEvents[batchEvents.length].blockNumber - 1
}
job = await processAMBInformationRequests(batchEvents)
if (job === null) {
return
}
} else {
job = await processEvents(missingEvents)
}
logger.info('Missed events transactions to send:', job.length)
if (job.length) {
await sendToQueue(job)
}
}
await updateLastReprocessedBlock(toBlock)
await deleteSeenEvents(0, toBlock)
}
async function getSeenEvents(fromBlock, toBlock) {
const keys = await redis.zrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
const res = {}
keys.forEach(k => {
res[k] = true
})
return res
}
function deleteSeenEvents(fromBlock, toBlock) {
return redis.zremrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
}
function addSeenEvents(events) {
return redis.zadd(seenEventsRedisKey, ...events.flatMap(e => [e.blockNumber, eventKey(e)]))
}
async function getLastBlockToProcess(web3, bridgeContract) {
const [lastBlockNumber, requiredBlockConfirmations] = await Promise.all([
getBlockNumber(web3),
@@ -158,24 +264,29 @@ async function main({ sendToQueue }) {
const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract)
if (reprocessingOptions.enabled) {
if (lastReprocessedBlock + reprocessingOptions.batchSize + reprocessingOptions.blockDelay < lastBlockToProcess) {
await reprocessOldLogs(sendToQueue)
return
}
}
if (lastBlockToProcess <= lastProcessedBlock) {
logger.debug('All blocks already processed')
return
}
await checkConditions()
const fromBlock = lastProcessedBlock + 1
const rangeEndBlock = config.blockPollingLimit ? fromBlock + config.blockPollingLimit : lastBlockToProcess
const rangeEndBlock = blockPollingLimit ? fromBlock + blockPollingLimit : lastBlockToProcess
let toBlock = Math.min(lastBlockToProcess, rangeEndBlock)
const events = (await getEvents({
let events = await getEvents({
contract: eventContract,
event: config.event,
fromBlock,
toBlock,
filter: config.eventFilter
})).sort((a, b) => a.blockNumber - b.blockNumber)
})
logger.info(`Found ${events.length} ${config.event} events`)
if (events.length) {
@@ -192,9 +303,10 @@ async function main({ sendToQueue }) {
if (batchEvents.length < events.length) {
// pick event outside from the batch
toBlock = events[batchEvents.length].blockNumber - 1
events = batchEvents
}
job = await processAMBInformationRequests(batchEvents)
job = await processAMBInformationRequests(events)
if (job === null) {
return
}
@@ -206,6 +318,9 @@ async function main({ sendToQueue }) {
if (job.length) {
await sendToQueue(job)
}
if (reprocessingOptions.enabled) {
await addSeenEvents(events)
}
}
logger.debug({ lastProcessedBlock: toBlock.toString() }, 'Updating last processed block')

View File

@@ -71,10 +71,10 @@ 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 () => {
@@ -82,10 +82,10 @@ describe('gasPrice', () => {
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 () => {
@@ -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 () => {
@@ -120,10 +120,10 @@ 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 () => {
@@ -131,7 +131,7 @@ describe('gasPrice', () => {
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

View File

@@ -3,7 +3,13 @@ const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
const BigNumber = require('bignumber.js')
const proxyquire = require('proxyquire')
const { addExtraGas, syncForEach, promiseAny } = require('../src/utils/utils')
const {
addExtraGas,
applyMinGasFeeBump,
chooseGasPriceOptions,
syncForEach,
promiseAny
} = require('../src/utils/utils')
chai.use(chaiAsPromised)
chai.should()
@@ -173,4 +179,43 @@ describe('utils', () => {
await promiseAny(array.map(f)).should.be.rejected
})
})
describe('applyMinGasFeeBump', () => {
it('should bump pre-eip1559 fee', () => {
const job = { gasPriceOptions: { gasPrice: '100000000000' } }
const newJob = applyMinGasFeeBump(job)
expect(newJob.gasPriceOptions.gasPrice).to.be.equal('110000000000')
})
it('should bump eip1559 fee', () => {
const job = { gasPriceOptions: { maxFeePerGas: '100000000000', maxPriorityFeePerGas: '20000000000' } }
const newJob = applyMinGasFeeBump(job)
expect(newJob.gasPriceOptions.maxFeePerGas).to.be.equal('110000000000')
expect(newJob.gasPriceOptions.maxPriorityFeePerGas).to.be.equal('22000000000')
})
})
describe('chooseGasPriceOptions', () => {
it('should choose max pre-eip1559 fee', () => {
const opts1 = { gasPrice: '100000000000' }
const opts2 = { gasPrice: '101000000000' }
expect(chooseGasPriceOptions(opts1, opts2).gasPrice).to.be.equal('101000000000')
expect(chooseGasPriceOptions(opts2, opts1).gasPrice).to.be.equal('101000000000')
expect(chooseGasPriceOptions(opts2, undefined).gasPrice).to.be.equal('101000000000')
expect(chooseGasPriceOptions(undefined, opts2).gasPrice).to.be.equal('101000000000')
})
it('should choose max eip1559 fee', () => {
const opts1 = { maxFeePerGas: '100000000000', maxPriorityFeePerGas: '21000000000' }
const opts2 = { maxFeePerGas: '101000000000', maxPriorityFeePerGas: '20000000000' }
expect(chooseGasPriceOptions(opts1, opts2).maxFeePerGas).to.be.equal('101000000000')
expect(chooseGasPriceOptions(opts1, opts2).maxPriorityFeePerGas).to.be.equal('21000000000')
expect(chooseGasPriceOptions(opts2, opts1).maxFeePerGas).to.be.equal('101000000000')
expect(chooseGasPriceOptions(opts2, opts1).maxPriorityFeePerGas).to.be.equal('21000000000')
expect(chooseGasPriceOptions(opts2, undefined).maxFeePerGas).to.be.equal('101000000000')
expect(chooseGasPriceOptions(opts2, undefined).maxPriorityFeePerGas).to.be.equal('20000000000')
expect(chooseGasPriceOptions(undefined, opts2).maxFeePerGas).to.be.equal('101000000000')
expect(chooseGasPriceOptions(undefined, opts2).maxPriorityFeePerGas).to.be.equal('20000000000')
})
})
})

View File

@@ -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"
}
}

732
yarn.lock

File diff suppressed because it is too large Load Diff