Compare commits

..

29 Commits

Author SHA1 Message Date
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
Alexander Kolotov
78564afabd Merge the develop branch to the master branch, preparation to v3.1.0
This merge contains the following set of changes:
  * [Oracle, Improvement] Try to detect unsynced node state (#592)
  * [Oracle, Improvement] Allow to override JSON RPC error codes (#603)
  * [Oracle, Fix] Fix handling of Compound related Transfer events (#595)
  * [Oracle, Fix] Add new nonce-related error messages (#599)
  * [Deployment, Fix] .env template includes latest changes related to the oracle configuration (#601)
  * [Deployment, Fix] Improvements for the local logs configuration (#602)
  * [Common, Other] Update the contract's submodule to the release 6.0.0 (#600)
2021-09-20 23:56:37 +03:00
Kirill Fedoseev
70a2c30b4c Fix imported ABI 2021-09-20 13:31:37 +03:00
Kirill Fedoseev
06a9586148 Update yarn.lock 2021-09-20 12:50:07 +03:00
Kirill Fedoseev
7379fe4190 Allow to override JSON RPC error codes (#603) 2021-09-20 12:07:49 +03:00
Alexander Kolotov
e59766c5df Update the contract's submodule to the release 6.0.0 (#600) 2021-09-18 13:44:48 +03:00
Alexander Kolotov
fdb18a1a17 Improvements for the local logs configuration (#602) 2021-09-18 13:42:29 +03:00
Alexander Kolotov
4412046f66 .env template includes latest changes related to the oracle configuration (#601) 2021-09-18 13:41:56 +03:00
Kirill Fedoseev
0ff224ccd3 Add new nonce-related error messages (#599) 2021-09-15 23:42:32 +03:00
Kirill Fedoseev
5cedacafe5 Fix handling of Compound related Transfer events (#595) 2021-08-24 11:31:15 +03:00
Kirill Fedoseev
2e6179f974 Try to detect unsynced node state (#592) 2021-08-03 23:36:50 +03:00
Alexander Kolotov
4f5e3c47be Merge the develop branch to the master branch, preparation to v3.0.0
This merge contains the following set of changes:
  * [Oracle, Improvement] Async calls error codes (#587)
  * [Oracle, Improvement] Refactor/async call serializers (#588)
2021-07-16 17:58:21 +03:00
Kirill Fedoseev
4c06329153 Refactor/async call serializers (#588) 2021-07-13 12:25:05 +03:00
Kirill Fedoseev
d53452675e Async calls error codes (#587) 2021-07-13 11:57:08 +03:00
Alexander Kolotov
c92f80c484 Merge the develop branch to the master branch, preparation to v3.0.0-rc1
This merge contains the following set of changes:
  * [Monitor, Improvement] Add statistics about used AMB information requests (#577)
  * [ALM, Improvement] Add safe-execute button in ALM (#580), closes #573, closes #551
  * [Oracle, Improvement] Improve confirm-relay feature (#582), closes #569
  * [Monitor, Fix] Prune print of long error messages about missing file (#579), closes #578
  * [Oracle, Fix] Use safe approach for eth_getLogs requests (#581)
  * [Oracle, Fix] Fix logging in gas price service (#583), closes #552
  * [Oracle, Fix] Fix oracle error patterns and oracle e2e tests (#585)
  * [Common, Other] Fix dependencies versions and update example.yml (#566)
  * [Common, Other] Upload services logs in e2e and ultimate tests (#568)
  * [Oracle, Other] added example of emergency shutdown controller contract (#572)
  * [Oracle, Other] Refactor oracle configuration (#584)
2021-07-10 10:10:22 +03:00
Kirill Fedoseev
5e95b5d8c5 Fix oracle error patterns and oracle e2e tests (#585) 2021-07-08 16:42:56 +03:00
Kirill Fedoseev
6e1f57493a Refactor oracle configuration (#584) 2021-07-08 09:38:55 +03:00
Kirill Fedoseev
8ed6550635 Improve confirm-relay feature (#582) 2021-07-07 16:21:01 +03:00
Kirill Fedoseev
c8eb0f1ed8 Fix logging in gas price service (#583) 2021-07-05 13:50:18 +03:00
Kirill Fedoseev
3e5e50c06e Use safe approach for eth_getLogs requests (#581) 2021-06-30 12:28:03 +03:00
Kirill Fedoseev
92e1b597c4 Add safe-execute button in ALM (#580) 2021-06-18 09:43:18 +03:00
Kirill Fedoseev
8b1a97e673 Prune print of long error messages about missing file (#579) 2021-06-12 00:24:20 +03:00
Kirill Fedoseev
3cf184c391 Add statistics about used AMB information requests (#577) 2021-05-26 08:58:20 -06:00
Alexander Kolotov
8f72516374 added example of emergency shutdown controller contract (#572) 2021-05-16 15:42:36 -06:00
Kirill Fedoseev
98155e3075 Upload services logs in e2e and ultimate tests (#568) 2021-05-15 10:29:20 -06:00
Kirill Fedoseev
0d724147bd Fix dependencies versions and update example.yml (#566) 2021-05-10 14:36:57 -06:00
88 changed files with 2095 additions and 4058 deletions

View File

@@ -77,7 +77,7 @@ jobs:
- name: Rebuild and push updated images
run: |
function check_if_image_exists() {
curl -fsSlL -H 'Authorization: bearer ${{ github.token }}' "https://${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
curl -fsSlL "https://${{ github.actor }}:${{ github.token }}@${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
}
updated=()
if ! check_if_image_exists e2e ${E2E_TAG}; then updated+=("e2e"); fi
@@ -104,7 +104,7 @@ jobs:
- name: Rebuild and push molecule runner e2e image
run: |
function check_if_image_exists() {
curl -fsSlL -H 'Authorization: bearer ${{ github.token }}' "https://${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
curl -fsSlL "https://${{ github.actor }}:${{ github.token }}@${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
}
if check_if_image_exists molecule_runner ${MOLECULE_RUNNER_TAG}; then
echo "Image already exists"
@@ -149,6 +149,12 @@ jobs:
run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
- name: yarn run ${{ matrix.task }}
run: ${{ !matrix.use-cache || steps.cache-repo.outputs.cache-hit }} && yarn run ${{ matrix.task }}
- name: Upload logs
if: always()
uses: actions/upload-artifact@v2
with:
name: logs-${{ matrix.task }}
path: e2e-commons/logs
deployment:
runs-on: ubuntu-latest
needs:
@@ -199,7 +205,7 @@ jobs:
- name: Login to docker registry
run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
- name: Deploy contracts
run: ${{ steps.cache-repo.outputs.cache-hit }} && e2e-commons/up.sh deploy blocks
run: ${{ steps.cache-repo.outputs.cache-hit }} && e2e-commons/up.sh deploy generate-amb-tx blocks
- name: Pull e2e oracle image
run: |
docker-compose -f ./e2e-commons/docker-compose.yml pull oracle-amb
@@ -210,3 +216,12 @@ jobs:
run: sudo chown -R $USER:docker /var/run/docker.sock
- name: Run oracle e2e tests
run: docker-compose -f ./e2e-commons/docker-compose.yml run -e ULTIMATE=true e2e yarn workspace oracle-e2e run ${{ matrix.task }}
- name: Save logs
if: always()
run: e2e-commons/down.sh
- name: Upload logs
if: always()
uses: actions/upload-artifact@v2
with:
name: logs-ultimate-${{ matrix.task }}
path: e2e-commons/logs

View File

@@ -52,6 +52,7 @@ ORACLE_SHUTDOWN_CONTRACT_ADDRESS | Optional contract address in the side chain a
ORACLE_SHUTDOWN_CONTRACT_METHOD | Method signature to be used in the side chain to identify the current shutdown status. Method should return boolean. Default value is `isShutdown()`. | `function signature`
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`
## Monitor configuration

View File

@@ -19,6 +19,7 @@ COPY --from=contracts /mono/contracts/build ./contracts/build
COPY commons/package.json ./commons/
COPY oracle-e2e/package.json ./oracle-e2e/
COPY monitor-e2e/package.json ./monitor-e2e/
COPY oracle/src/utils/constants.js ./oracle/src/utils/constants.js
COPY yarn.lock .
RUN NOYARNPOSTINSTALL=1 yarn install --frozen-lockfile --production

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
cd $(dirname $0)
../e2e-commons/up.sh deploy blocks alm alm-e2e
../e2e-commons/up.sh deploy generate-amb-tx blocks alm alm-e2e
# run oracle amb e2e tests to generate transactions for alm
docker-compose -f ../e2e-commons/docker-compose.yml run e2e yarn workspace oracle-e2e run alm

View File

@@ -6,8 +6,8 @@ jest.setTimeout(60000)
const statusText = 'Success'
const statusSelector = 'label[data-id="status"]'
const homeToForeignTxURL = 'http://localhost:3004/77/0xbc83d43bdc675a615a2b820e43e52d25857aa5fdd77acf2dd92cd247af2c693c'
const foreignToHomeTxURL = 'http://localhost:3004/42/0x09dfb947dbd17e27bcc117773b6e133829f7cef9646199a93ef019c4f7c0fec6'
const homeToForeignTxURL = 'http://localhost:3004/77/0x295efbe6ae98937ef35d939376c9bd752b4dc6f6899a9d5ddd6a57cea3d76c89'
const foreignToHomeTxURL = 'http://localhost:3004/42/0x7262f7dbe6c30599edded2137fbbe93c271b37f5c54dd27f713f0cf510e3b4dd'
describe('ALM', () => {
let browser

View File

@@ -123,6 +123,24 @@ const abi: AbiItem[] = [
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_data',
type: 'bytes'
},
{
name: '_signatures',
type: 'bytes'
}
],
name: 'safeExecuteSignaturesWithAutoGasLimit',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
import { useWindowWidth } from '@react-hook/window-size'
import { SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS, ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION } from '../config/constants'
@@ -9,6 +9,7 @@ import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
import { Thead, AgeTd, StatusTd } from './commons/Table'
import { ManualExecutionButton } from './ManualExecutionButton'
import { useStateProvider } from '../state/StateProvider'
const StyledExecutionConfirmation = styled.div`
margin-top: 30px;
@@ -33,6 +34,8 @@ export const ExecutionConfirmation = ({
executionEventsFetched,
setPendingExecution
}: ExecutionConfirmationParams) => {
const { foreign } = useStateProvider()
const [safeExecutionAvailable, setSafeExecutionAvailable] = useState(false)
const availableManualExecution =
!isHome &&
(executionData.status === VALIDATOR_CONFIRMATION_STATUS.WAITING ||
@@ -48,6 +51,22 @@ export const ExecutionConfirmation = ({
const formattedValidator =
windowWidth < 850 && executionData.validator ? formatTxHash(executionData.validator) : executionData.validator
useEffect(
() => {
if (!availableManualExecution || !foreign.bridgeContract) return
const p = foreign.bridgeContract.methods.getBridgeInterfacesVersion().call()
p.then(({ major, minor }: any) => {
major = parseInt(major, 10)
minor = parseInt(minor, 10)
if (major < 5 || (major === 5 && minor < 7)) return
setSafeExecutionAvailable(true)
})
},
[availableManualExecution, foreign.bridgeContract]
)
const getExecutionStatusElement = (validatorStatus = '') => {
switch (validatorStatus) {
case VALIDATOR_CONFIRMATION_STATUS.SUCCESS:
@@ -105,6 +124,7 @@ export const ExecutionConfirmation = ({
{availableManualExecution && (
<td>
<ManualExecutionButton
safeExecutionAvailable={safeExecutionAvailable}
messageData={messageData}
setExecutionData={setExecutionData}
signatureCollected={signatureCollected as string[]}

View File

@@ -15,16 +15,19 @@ import { signatureToVRS, packSignatures } from '../utils/signatures'
import { getSuccessExecutionData } from '../utils/getFinalizationEvent'
import { TransactionReceipt } from 'web3-eth'
const StyledButton = styled.button`
const ActionButton = styled.button`
color: var(--button-color);
border-color: var(--font-color);
margin-top: 10px;
min-width: 120px;
padding: 1rem;
&:focus {
outline: var(--button-color);
}
`
interface ManualExecutionButtonParams {
safeExecutionAvailable: boolean
messageData: string
setExecutionData: Function
signatureCollected: string[]
@@ -32,6 +35,7 @@ interface ManualExecutionButtonParams {
}
export const ManualExecutionButton = ({
safeExecutionAvailable,
messageData,
setExecutionData,
signatureCollected,
@@ -40,6 +44,7 @@ export const ManualExecutionButton = ({
const { foreign, setError } = useStateProvider()
const { library, activate, account, active } = useWeb3React()
const [manualExecution, setManualExecution] = useState(false)
const [allowFailures, setAllowFailures] = useState(false)
useEffect(
() => {
@@ -72,7 +77,11 @@ export const ManualExecutionButton = ({
const signatures = packSignatures(signatureCollected.map(signatureToVRS))
const messageId = messageData.slice(0, 66)
const bridge = foreign.bridgeContract
const data = bridge.methods.executeSignatures(messageData, signatures).encodeABI()
const executeMethod =
safeExecutionAvailable && !allowFailures
? bridge.methods.safeExecuteSignaturesWithAutoGasLimit
: bridge.methods.executeSignatures
const data = executeMethod(messageData, signatures).encodeABI()
setManualExecution(false)
library.eth
@@ -132,15 +141,35 @@ export const ManualExecutionButton = ({
messageData,
signatureCollected,
setExecutionData,
setPendingExecution
setPendingExecution,
safeExecutionAvailable,
allowFailures
]
)
return (
<div className="is-center">
<StyledButton className="button outline" onClick={() => setManualExecution(true)}>
Execute
</StyledButton>
<div>
<div className="is-center">
<ActionButton className="button outline" onClick={() => setManualExecution(true)}>
Execute
</ActionButton>
</div>
{safeExecutionAvailable && (
<div
title="Allow executed message to fail and record its failure on-chain without reverting the whole transaction.
Use fixed gas limit for execution."
className="is-center"
style={{ paddingTop: 10 }}
>
<input
type="checkbox"
id="allow-failures"
checked={allowFailures}
onChange={e => setAllowFailures(e.target.checked)}
/>
<label htmlFor="allow-failures">Unsafe mode</label>
</div>
)}
</div>
)
}

View File

@@ -1,5 +1,5 @@
const HOME_ERC_TO_NATIVE_ABI = require('../contracts/build/contracts/HomeBridgeErcToNative').abi
const FOREIGN_ERC_TO_NATIVE_ABI = require('../contracts/build/contracts/ForeignBridgeErcToNative').abi
const FOREIGN_ERC_TO_NATIVE_ABI = require('../contracts/build/contracts/XDaiForeignBridge.json').abi
const ERC20_ABI = require('../contracts/build/contracts/ERC20').abi
const BLOCK_REWARD_ABI = require('../contracts/build/contracts/BlockRewardMock').abi
const BRIDGE_VALIDATORS_ABI = require('../contracts/build/contracts/BridgeValidators').abi

View File

@@ -1,3 +1,5 @@
const { soliditySha3 } = require('web3-utils')
function strip0x(input) {
return input.replace(/^0x/, '')
}
@@ -39,8 +41,35 @@ const normalizeAMBMessageEvent = e => {
return parseAMBMessage(msgData)
}
const ambInformationSignatures = [
'eth_call(address,bytes)',
'eth_call(address,bytes,uint256)',
'eth_call(address,address,uint256,bytes)',
'eth_blockNumber()',
'eth_getBlockByNumber()',
'eth_getBlockByNumber(uint256)',
'eth_getBlockByHash(bytes32)',
'eth_getBalance(address)',
'eth_getBalance(address,uint256)',
'eth_getTransactionCount(address)',
'eth_getTransactionCount(address,uint256)',
'eth_getTransactionByHash(bytes32)',
'eth_getTransactionReceipt(bytes32)',
'eth_getStorageAt(address,bytes32)',
'eth_getStorageAt(address,bytes32,uint256)'
]
const ambInformationSelectors = Object.fromEntries(ambInformationSignatures.map(sig => [soliditySha3(sig), sig]))
const normalizeAMBInfoRequest = e => ({
messageId: e.returnValues.messageId,
sender: e.returnValues.sender,
requestSelector: ambInformationSelectors[e.returnValues.requestSelector] || 'unknown',
data: e.returnValues.data
})
module.exports = {
strip0x,
parseAMBMessage,
normalizeAMBMessageEvent
normalizeAMBMessageEvent,
ambInformationSignatures,
normalizeAMBInfoRequest
}

View File

@@ -9,7 +9,8 @@
},
"dependencies": {
"gas-price-oracle": "^0.1.5",
"web3-utils": "^1.3.0"
"web3-utils": "^1.3.0",
"node-fetch": "^2.1.2"
},
"devDependencies": {
"bn-chai": "^1.0.1",

View File

@@ -1,5 +1,6 @@
const { toWei, toBN, BN } = require('web3-utils')
const { GasPriceOracle } = require('gas-price-oracle')
const fetch = require('node-fetch')
const { BRIDGE_MODES } = require('./constants')
const { REWARDABLE_VALIDATORS_ABI } = require('./abis')
@@ -178,17 +179,16 @@ const normalizeGasPrice = (oracleGasPrice, factor, limits = null) => {
return toBN(toWei(gasPrice.toFixed(2).toString(), 'gwei'))
}
// fetchFn has to be supplied (instead of just url to oracle),
// because this utility function is shared between Browser and Node,
// we use built-in 'fetch' on browser side, and `node-fetch` package in Node.
const gasPriceFromSupplier = async (fetchFn, options = {}) => {
const gasPriceFromSupplier = async (url, options = {}) => {
try {
let json
if (fetchFn) {
const response = await fetchFn()
if (url === 'gas-price-oracle') {
json = await gasPriceOracle.fetchGasPricesOffChain()
} else if (url) {
const response = await fetch(url, { timeout: 2000 })
json = await response.json()
} else {
json = await gasPriceOracle.fetchGasPricesOffChain()
return null
}
const oracleGasPrice = json[options.speedType]

View File

@@ -34,7 +34,7 @@ provisioner:
inventory:
host_vars:
multiple-host:
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY: "8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY: "6c48435bd464a53ed66ed62127c4dba8af75cf1a99a8ebe2680599948fbfbc6d"
MONITOR_PORT: 3003
syslog_server_port: "udp://127.0.0.1:514"
verifier:

View File

@@ -33,7 +33,7 @@ provisioner:
inventory:
host_vars:
oracle-host:
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY: "8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY: "6c48435bd464a53ed66ed62127c4dba8af75cf1a99a8ebe2680599948fbfbc6d"
syslog_server_port: "udp://127.0.0.1:514"
verifier:
name: testinfra

View File

@@ -5,13 +5,13 @@ ORACLE_LOG_LEVEL: debug
## Home contract
COMMON_HOME_RPC_URL: "https://sokol.poa.network"
COMMON_HOME_BRIDGE_ADDRESS: "0x98aFdE294f1C46aA0a27Cc4049ED337F879d8976"
COMMON_HOME_BRIDGE_ADDRESS: "0x59ba90A588ce732AB33FD32Aab1b58c21400A0f6"
ORACLE_HOME_RPC_POLLING_INTERVAL: 5000
## Foreign contract
COMMON_FOREIGN_RPC_URL: "https://sokol.poa.network"
COMMON_FOREIGN_BRIDGE_ADDRESS: "0x5a584f4C30B36f282848dAc9a2b20E7BEF481981"
ORACLE_FOREIGN_RPC_POLLING_INTERVAL: 1000
COMMON_FOREIGN_RPC_URL: "https://kovan.infura.io/v3/5d7bd94c50ed43fab1cb8e74f58678b0"
COMMON_FOREIGN_BRIDGE_ADDRESS: "0xdA4a49a00F4fF4A5988b9AceE95f99e3b2c208b6"
ORACLE_FOREIGN_RPC_POLLING_INTERVAL: 5000
## Home Gasprice
COMMON_HOME_GAS_PRICE_SUPPLIER_URL: "https://gasprice.poa.network/"
@@ -31,8 +31,8 @@ ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL: 600000
MONITOR_BRIDGE_NAME: "bridge"
MONITOR_PORT: 3003
MONITOR_CACHE_EVENTS: "false"
MONITOR_HOME_START_BLOCK: 0
MONITOR_FOREIGN_START_BLOCK: 0
MONITOR_HOME_START_BLOCK: 20821049
MONITOR_FOREIGN_START_BLOCK: 24773297
MONITOR_VALIDATOR_HOME_TX_LIMIT: 300000
MONITOR_VALIDATOR_FOREIGN_TX_LIMIT: 300000
MONITOR_TX_NUMBER_THRESHOLD: 100

View File

@@ -1,6 +1,6 @@
/var/log/docker/*/docker.log {
rotate 5
size 1G
size 100M
compress
missingok
delaycompress
@@ -8,7 +8,7 @@
}
/var/log/docker/*.log {
rotate 5
size 1G
size 100M
compress
missingok
delaycompress

View File

@@ -7,7 +7,7 @@
loop_control:
loop_var: file
- name: Set the local container logs configuration file
- name: Set the oracle's containers local logs configuration file
template:
src: 31-oracle-docker.conf.j2
dest: /etc/rsyslog.d/31-oracle-docker.conf
@@ -15,6 +15,22 @@
group: root
mode: 0644
- name: Set the redis container local logs configuration file
template:
src: 32-redis-docker.conf.j2
dest: /etc/rsyslog.d/32-redis-docker.conf
owner: root
group: root
mode: 0644
- name: Set the rabbit MQ container local logs configuration file
template:
src: 33-rabbit-docker.conf.j2
dest: /etc/rsyslog.d/33-rabbit-docker.conf
owner: root
group: root
mode: 0644
- name: Set the log configuration file to send container logs to remote server
template:
src: 36-oracle-remote-logging.conf.j2

View File

@@ -27,17 +27,6 @@
set_fact:
ORACLE_VALIDATOR_ADDRESS: "{{ VADDRESS.stdout }}"
- name: Get foreign erc type
become_user: "{{ compose_service_user }}"
shell: docker-compose run --rm --entrypoint "node scripts/initialChecks.js" bridge_affirmation
args:
chdir: "{{ bridge_path }}/oracle"
register: ERCTYPE
- name: Set FOREIGN_ERC_TYPE variable
set_fact:
FOREIGN_ERC_TYPE: "{{ (ERCTYPE.stdout).foreignERC | default('') }}"
- name: Extend docker compose file for erc to native
set_fact: composefileoverride="-f docker-compose-transfer.yml"
when: ORACLE_BRIDGE_MODE == "ERC_TO_NATIVE"

View File

@@ -11,9 +11,16 @@ ORACLE_HOME_RPC_POLLING_INTERVAL={{ ORACLE_HOME_RPC_POLLING_INTERVAL }}
## Foreign contract
COMMON_FOREIGN_RPC_URL={{ COMMON_FOREIGN_RPC_URL }}
{% if ORACLE_FOREIGN_ARCHIVE_RPC_URL | default('') != '' %}
ORACLE_FOREIGN_ARCHIVE_RPC_URL={{ ORACLE_FOREIGN_ARCHIVE_RPC_URL }}
{% endif %}
COMMON_FOREIGN_BRIDGE_ADDRESS={{ COMMON_FOREIGN_BRIDGE_ADDRESS }}
ORACLE_FOREIGN_RPC_POLLING_INTERVAL={{ ORACLE_FOREIGN_RPC_POLLING_INTERVAL }}
{% if ORACLE_TX_REDUNDANCY | default('') != '' %}
ORACLE_TX_REDUNDANCY={{ ORACLE_TX_REDUNDANCY }}
{% endif %}
## Gasprice
{% if COMMON_HOME_GAS_PRICE_SUPPLIER_URL | default('') != '' %}
COMMON_HOME_GAS_PRICE_SUPPLIER_URL={{ COMMON_HOME_GAS_PRICE_SUPPLIER_URL }}
@@ -47,8 +54,28 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR={{ COMMON_FOREIGN_GAS_PRICE_FACTOR }}
ORACLE_ALLOW_HTTP_FOR_RPC={{ "yes" if ORACLE_ALLOW_HTTP_FOR_RPC else "no" }}
ORACLE_QUEUE_URL={{ ORACLE_QUEUE_URL }}
ORACLE_REDIS_URL={{ ORACLE_REDIS_URL }}
{% if ORACLE_TX_REDUNDANCY | default('') != '' %}
ORACLE_TX_REDUNDANCY={{ ORACLE_TX_REDUNDANCY }}
{% if ORACLE_FOREIGN_TX_RESEND_INTERVAL | default('') != '' %}
ORACLE_FOREIGN_TX_RESEND_INTERVAL={{ ORACLE_FOREIGN_TX_RESEND_INTERVAL }}
{% endif %}
{% if ORACLE_HOME_TX_RESEND_INTERVAL | default('') != '' %}
ORACLE_HOME_TX_RESEND_INTERVAL={{ ORACLE_HOME_TX_RESEND_INTERVAL }}
{% endif %}
## Emergency shutdown configuration
{% if ORACLE_SHUTDOWN_SERVICE_URL | default('') != '' %}
ORACLE_SHUTDOWN_SERVICE_URL={{ ORACLE_SHUTDOWN_SERVICE_URL }}
{% endif %}
{% if ORACLE_SHUTDOWN_SERVICE_POLLING_INTERVAL | default('') != '' %}
ORACLE_SHUTDOWN_SERVICE_POLLING_INTERVAL={{ ORACLE_SHUTDOWN_SERVICE_POLLING_INTERVAL }}
{% endif %}
{% if ORACLE_SIDE_RPC_URL | default('') != '' %}
ORACLE_SIDE_RPC_URL={{ ORACLE_SIDE_RPC_URL }}
{% endif %}
{% if ORACLE_SHUTDOWN_CONTRACT_ADDRESS | default('') != '' %}
ORACLE_SHUTDOWN_CONTRACT_ADDRESS={{ ORACLE_SHUTDOWN_CONTRACT_ADDRESS }}
{% endif %}
{% if ORACLE_SHUTDOWN_CONTRACT_METHOD | default('') != '' %}
ORACLE_SHUTDOWN_CONTRACT_METHOD={{ ORACLE_SHUTDOWN_CONTRACT_METHOD }}
{% endif %}
{% if ORACLE_HOME_START_BLOCK | default('') != '' %}

View File

@@ -0,0 +1,11 @@
$FileCreateMode 0644
template(name="DockerLogFileName_Redis" type="list") {
constant(value="/var/log/docker/")
property(name="syslogtag" securepath="replace" regex.type="ERE" regex.submatch="1" regex.expression="oracle_(.*redis.*)\\/[a-zA-Z0-9]+\\[")
constant(value="/docker.log")
}
if $programname contains 'oracle' and $programname contains 'redis' then \
?DockerLogFileName_Redis
$FileCreateMode 0600

View File

@@ -0,0 +1,11 @@
$FileCreateMode 0644
template(name="DockerLogFileName_Rabbit" type="list") {
constant(value="/var/log/docker/")
property(name="syslogtag" securepath="replace" regex.type="ERE" regex.submatch="1" regex.expression="oracle_(.*rabbit.*)\\/[a-zA-Z0-9]+\\[")
constant(value="/docker.log")
}
if $programname contains 'oracle' and $programname contains 'rabbit' then \
?DockerLogFileName_Rabbit
$FileCreateMode 0600

View File

@@ -6,14 +6,12 @@ COMMON_HOME_RPC_URL=http://parity1:8545
COMMON_FOREIGN_RPC_URL=http://parity2:8545
COMMON_HOME_BRIDGE_ADDRESS=0x8397be90BCF57b0B71219f555Fe121b22e5a994C
COMMON_FOREIGN_BRIDGE_ADDRESS=0x1feB40aD9420b186F019A717c37f5546165d411E
ORACLE_VALIDATOR_ADDRESS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9
COMMON_HOME_GAS_PRICE_SUPPLIER_URL=https://gasprice.poa.network/
COMMON_HOME_GAS_PRICE_SUPPLIER_URL=
COMMON_HOME_GAS_PRICE_SPEED_TYPE=standard
COMMON_HOME_GAS_PRICE_FALLBACK=1000000000
ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL=600000
COMMON_HOME_GAS_PRICE_FACTOR=1
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL=https://gasprice.poa.network/
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL=
COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE=standard
COMMON_FOREIGN_GAS_PRICE_FALLBACK=10000000000
ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL=600000

View File

@@ -6,8 +6,6 @@ COMMON_HOME_RPC_URL=http://parity1:8545
COMMON_FOREIGN_RPC_URL=http://parity2:8545
COMMON_HOME_BRIDGE_ADDRESS=0x5118AC62AE912Dd5B51EEfF7338c4fcb0248Ba8c
COMMON_FOREIGN_BRIDGE_ADDRESS=0x32198D570fffC7033641F8A9094FFDCaAEF42624
ORACLE_VALIDATOR_ADDRESS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9
COMMON_HOME_GAS_PRICE_SUPPLIER_URL=https://gasprice.poa.network/
COMMON_HOME_GAS_PRICE_SPEED_TYPE=standard
COMMON_HOME_GAS_PRICE_FALLBACK=1

View File

@@ -38,7 +38,7 @@
"ercToNativeBridge": {
"home": "0x5118AC62AE912Dd5B51EEfF7338c4fcb0248Ba8c",
"foreign": "0x32198D570fffC7033641F8A9094FFDCaAEF42624",
"foreignToken": "0x7cc4b1851c35959d34e635a470f6b5c43ba3c9c9",
"foreignToken": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"monitor": "http://monitor-erc20-native:3012/bridge"
},
"amb": {
@@ -49,6 +49,12 @@
"blockedHomeBox": "0xF9698Eb93702dfdd0e2d802088d4c21822a8A977",
"monitor": "http://monitor-amb:3013/bridge"
},
"amb2": {
"home": "0x5A42E119990c3F3A80Fea20aAF4c3Ff4DB240Cc9",
"foreign": "0x897527391ad3837604973d78D3514f44c36AB9FC",
"homeBox": "0xb008E9076fCbDB2C3AF84225Bc07Eb35B2bE5ECD",
"foreignBox": "0x4a58D6d8D416a5fBCAcf3dC52eb8bE8948E25127"
},
"homeRPC": {
"URL": "http://parity1:8545",
"ID": "77"

View File

@@ -32,7 +32,7 @@ FOREIGN_GAS_PRICE=10000000000
FOREIGN_REWARDABLE=false
BLOCK_REWARD_ADDRESS=0xdbeE25CbE97e4A5CC6c499875774dc7067E9426B
ERC20_TOKEN_ADDRESS=0x7cc4b1851c35959d34e635a470f6b5c43ba3c9c9
ERC20_TOKEN_ADDRESS=0x6B175474E89094C44Da98b954EedeAC495271d0F
REQUIRED_NUMBER_OF_VALIDATORS=1
VALIDATORS="0xaaB52d66283F7A1D5978bcFcB55721ACB467384b 0xdCC784657C78054aa61FbcFFd2605F32374816A4 0xDcef88209a20D52165230104B245803C3269454d"

View File

@@ -1,7 +1,33 @@
#!/usr/bin/env bash
cd $(dirname $0)
if [ $CI ]; then exit $rc; fi
if [ $CI ]; then
rm -rf logs || true
mkdir ./logs
for project in "" validator{1,2,3}; do
for container in $(docker-compose -p "$project" ps | tail -n +3 | awk '{print $1}') ; do
if [[ -z "$project" ]]; then
path="./logs/$container.log"
else
mkdir -p "./logs/$project"
path="./logs/$project/$container.log"
fi
docker logs "$container" > "$path" 2>&1
done
done
touch ../oracle/.env
for file in ../oracle/docker-compose-{amb,transfer}.yml; do
for container in $(docker-compose -f "$file" ps | tail -n +3 | awk '{print $1}') ; do
mkdir -p "./logs/oracle"
docker logs "$container" > "./logs/oracle/$container.log" 2>&1
done
done
exit $rc;
fi
ps | grep node | grep -v grep | grep -v yarn | awk '{print "kill " $1}' | /bin/bash
docker-compose down

View File

@@ -35,3 +35,9 @@ echo -e "\n\n############ Deploying one more test contract for amb ############\
cd "$DEPLOY_PATH"
node src/utils/deployTestBox.js
cd - > /dev/null
echo -e "\n\n############ Deploying one more amb without oracle for confirm relay tests ############\n"
cp "$ENVS_PATH/amb.env" "$DEPLOY_PATH/.env"
cd "$DEPLOY_PATH"
node deploy.js
cd - > /dev/null

View File

@@ -15,42 +15,46 @@ docker network create --driver bridge ultimate || true
docker-compose up -d parity1 parity2 e2e
startValidator () {
db_env="-e ORACLE_QUEUE_URL=amqp://$4 -e ORACLE_REDIS_URL=redis://$3"
db_env="-e ORACLE_QUEUE_URL=amqp://$3 -e ORACLE_REDIS_URL=redis://$2"
docker-compose $1 run -d --name $3 redis
docker-compose $1 run -d --name $4 rabbit
docker-compose $1 run -d --name $2 redis
docker-compose $1 run -d --name $3 rabbit
if [[ -z "$MODE" || "$MODE" == erc-to-native ]]; then
docker-compose $1 run $2 $db_env -d oracle-erc20-native yarn watcher:signature-request
docker-compose $1 run $2 $db_env -d oracle-erc20-native yarn watcher:collected-signatures
docker-compose $1 run $2 $db_env -d oracle-erc20-native yarn watcher:affirmation-request
docker-compose $1 run $2 $db_env -d oracle-erc20-native yarn watcher:transfer
docker-compose $1 run $oraclePK $db_env -d oracle-erc20-native yarn watcher:signature-request
docker-compose $1 run $oracleAddr $db_env -d oracle-erc20-native yarn watcher:collected-signatures
docker-compose $1 run $oracleAddr $db_env -d oracle-erc20-native yarn watcher:affirmation-request
docker-compose $1 run $oracleAddr $db_env -d oracle-erc20-native yarn watcher:transfer
fi
if [[ -z "$MODE" || "$MODE" == amb ]]; then
docker-compose $1 run $2 $db_env -d oracle-amb yarn watcher:signature-request
docker-compose $1 run $2 $db_env -d oracle-amb yarn watcher:collected-signatures
docker-compose $1 run $2 $db_env -d oracle-amb yarn watcher:affirmation-request
docker-compose $1 run $2 $db_env -d oracle-amb yarn watcher:information-request
docker-compose $1 run $oraclePK $db_env -d oracle-amb yarn watcher:signature-request
docker-compose $1 run $oracleAddr $db_env -d oracle-amb yarn watcher:collected-signatures
docker-compose $1 run $oracleAddr $db_env -d oracle-amb yarn watcher:affirmation-request
docker-compose $1 run $oracleAddr $db_env -d oracle-amb yarn watcher:information-request
fi
docker-compose $1 run $2 $db_env -d oracle-amb yarn sender:home
docker-compose $1 run $2 $db_env -d oracle-amb yarn sender:foreign
docker-compose $1 run $2 $db_env -d oracle-amb yarn manager:shutdown
docker-compose $1 run $oraclePK $db_env -d oracle-amb yarn sender:home
docker-compose $1 run $oraclePK $db_env -d oracle-amb yarn sender:foreign
docker-compose $1 run $oracleAddr $db_env -d oracle-amb yarn manager:shutdown
}
while [ "$1" != "" ]; do
if [ "$1" == "oracle" ]; then
startValidator "-p validator1" "" redis rabbit
oracleAddr="-e ORACLE_VALIDATOR_ADDRESS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b"
oraclePK="-e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
startValidator "-p validator1" redis rabbit
fi
if [ "$1" == "oracle-validator-2" ]; then
oracle2Values="-e ORACLE_VALIDATOR_ADDRESS=0xdCC784657C78054aa61FbcFFd2605F32374816A4 -e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=5a5c3645d0f04e9eb4f27f94ed4c244a225587405b8838e7456f7781ce3a9513"
startValidator "-p validator2" "$oracle2Values" redis2 rabbit2
oracleAddr="-e ORACLE_VALIDATOR_ADDRESS=0xdCC784657C78054aa61FbcFFd2605F32374816A4"
oraclePK="-e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=5a5c3645d0f04e9eb4f27f94ed4c244a225587405b8838e7456f7781ce3a9513"
startValidator "-p validator2" redis2 rabbit2
fi
if [ "$1" == "oracle-validator-3" ]; then
oracle3Values="-e ORACLE_VALIDATOR_ADDRESS=0xDcef88209a20D52165230104B245803C3269454d -e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=f877f62a1c19f852cff1d29f0fb1ecac18821c0080d4cc0520c60c098293dca1"
startValidator "-p validator3" "$oracle3Values" redis3 rabbit3
oracleAddr="-e ORACLE_VALIDATOR_ADDRESS=0xDcef88209a20D52165230104B245803C3269454d"
oraclePK="-e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=f877f62a1c19f852cff1d29f0fb1ecac18821c0080d4cc0520c60c098293dca1"
startValidator "-p validator3" redis3 rabbit3
fi
if [ "$1" == "alm" ]; then
@@ -84,13 +88,38 @@ while [ "$1" != "" ]; do
if [ "$1" == "alm-e2e" ]; then
MODE=amb
startValidator "-p validator1" "" redis rabbit
oracleAddr="-e ORACLE_VALIDATOR_ADDRESS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b"
oraclePK="-e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
startValidator "-p validator1" redis rabbit
oracle2Values="-e ORACLE_VALIDATOR_ADDRESS=0xdCC784657C78054aa61FbcFFd2605F32374816A4 -e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=5a5c3645d0f04e9eb4f27f94ed4c244a225587405b8838e7456f7781ce3a9513"
startValidator "-p validator2" "$oracle2Values" redis2 rabbit2
oracleAddr="-e ORACLE_VALIDATOR_ADDRESS=0xdCC784657C78054aa61FbcFFd2605F32374816A4"
oraclePK="-e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=5a5c3645d0f04e9eb4f27f94ed4c244a225587405b8838e7456f7781ce3a9513"
startValidator "-p validator2" redis2 rabbit2
oracle3Values="-e ORACLE_VALIDATOR_ADDRESS=0xDcef88209a20D52165230104B245803C3269454d -e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=f877f62a1c19f852cff1d29f0fb1ecac18821c0080d4cc0520c60c098293dca1"
startValidator "-p validator3" "$oracle3Values" redis3 rabbit3
oracleAddr="-e ORACLE_VALIDATOR_ADDRESS=0xDcef88209a20D52165230104B245803C3269454d"
oraclePK="-e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=f877f62a1c19f852cff1d29f0fb1ecac18821c0080d4cc0520c60c098293dca1"
startValidator "-p validator3" redis3 rabbit3
fi
if [ "$1" == "generate-amb-tx" ]; then
docker-compose run e2e yarn workspace oracle-e2e run generate-amb-tx
fi
if [ "$1" == "manual-amb-relay" ]; then
oraclePK="-e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
env="-e COMMON_HOME_BRIDGE_ADDRESS=0x5A42E119990c3F3A80Fea20aAF4c3Ff4DB240Cc9 -e COMMON_FOREIGN_BRIDGE_ADDRESS=0x897527391ad3837604973d78D3514f44c36AB9FC"
# these tx hash are hardcoded and need to be updated manually
# once e2e environment setup process is changed
echo '0xea625a823bc5018dc3a4efe349f623e5ebb8c987b55f44d50d6556f42af9a400' > txHashes.txt
docker-compose -p validator1 run -v $(pwd)/txHashes.txt:/tmp/txHashes.txt $oraclePK $env oracle-amb yarn confirm:affirmation-request \
/tmp/txHashes.txt \
0x031c42e44485002c9215a5b1b75e9516131485ce29884a58765bf7a0038538f9
docker-compose -p validator1 run $oraclePK $env oracle-amb yarn confirm:signature-request \
0x1506a18af91afe732167ccbc178b55fc2547da4a814d13c015b6f496cf171754 | tee .tmp.log
tx_hash=$(cat .tmp.log | grep generatedTransactionHash | jq -r .generatedTransactionHash)
rm .tmp.log
rm txHashes.txt
docker-compose -p validator1 run $oraclePK $env oracle-amb yarn confirm:collected-signatures $tx_hash
fi
shift # Shift all the parameters down by one

View File

@@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"start": "mocha --timeout 120000",
"start": "mocha --timeout 120000 --exit",
"lint": "eslint . --ignore-path ../.eslintignore"
},
"author": "",

View File

@@ -1,10 +1,9 @@
require('dotenv').config()
const logger = require('./logger')('detectMediators.js')
const { isHomeContract, isForeignContract } = require('./utils/web3Cache')
const eventsInfo = require('./utils/events')
const { getHomeTxSender, getForeignTxSender } = require('./utils/web3Cache')
const { addExecutionStatus } = require('./utils/message')
const { normalizeAMBMessageEvent } = require('../commons')
const { getHomeTxSender, getForeignTxSender, isHomeContract, isForeignContract } = require('./utils/web3Cache')
const { addExecutionStatus, addRetrievalStatus } = require('./utils/message')
const { normalizeAMBMessageEvent, normalizeAMBInfoRequest } = require('../commons')
function countInteractions(requests) {
const stats = {}
@@ -30,6 +29,41 @@ function countInteractions(requests) {
return stats
}
function countInfoRequests(requests) {
const stats = {}
requests.forEach(msg => {
if (!stats[msg.sender]) {
stats[msg.sender] = {}
}
if (!stats[msg.sender][msg.requestSelector]) {
stats[msg.sender][msg.requestSelector] = {
callSucceeded: {
callbackSucceeded: 0,
callbackFailed: 0
},
callFailed: {
callbackSucceeded: 0,
callbackFailed: 0
},
pending: 0
}
}
const stat = stats[msg.sender][msg.requestSelector]
if (msg.callStatus === true && msg.callbackStatus === true) {
stat.callSucceeded.callbackSucceeded += 1
} else if (msg.callStatus === true && msg.callbackStatus === false) {
stat.callSucceeded.callbackFailed += 1
} else if (msg.callStatus === false && msg.callbackStatus === true) {
stat.callFailed.callbackSucceeded += 1
} else if (msg.callStatus === false && msg.callbackStatus === false) {
stat.callFailed.callbackFailed += 1
} else {
stat.pending += 1
}
})
return stats
}
const normalize = event => ({
...normalizeAMBMessageEvent(event),
txHash: event.transactionHash,
@@ -88,10 +122,13 @@ async function main(mode) {
homeToForeignRequests,
foreignToHomeRequests,
homeToForeignConfirmations,
foreignToHomeConfirmations
foreignToHomeConfirmations,
informationRequests,
informationResponses
} = await eventsInfo(mode)
const homeToForeign = homeToForeignRequests.map(normalize).map(addExecutionStatus(homeToForeignConfirmations))
const foreignToHome = foreignToHomeRequests.map(normalize).map(addExecutionStatus(foreignToHomeConfirmations))
const infoRequests = informationRequests.map(normalizeAMBInfoRequest).map(addRetrievalStatus(informationResponses))
for (const event of homeToForeign) {
// AMB contract emits a single UserRequestForSignature event for every home->foreign request.
@@ -146,6 +183,7 @@ async function main(mode) {
floatingMediators,
remotelyControlledMediators,
unknown,
informationReceivers: countInfoRequests(infoRequests),
lastChecked: Math.floor(Date.now() / 1000)
}
}

View File

@@ -46,24 +46,6 @@ async function main(bridgeMode, eventsInfo) {
const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_ERC_TO_NATIVE_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS)
const erc20Address = await foreignBridge.methods.erc20token().call()
const erc20Contract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address)
let investedAmountInDai = 0
let bridgeDsrBalance = 0
let displayChaiToken = false
try {
logger.debug('calling foreignBridge.methods.isChaiTokenEnabled')
if (await foreignBridge.methods.isChaiTokenEnabled().call()) {
displayChaiToken = true
logger.debug('calling foreignBridge.methods.investedAmountInDai')
investedAmountInDai = await foreignBridge.methods.investedAmountInDai().call()
logger.debug('calling foreignBridge.methods.dsrBalance')
bridgeDsrBalance = await foreignBridge.methods.dsrBalance().call()
} else {
logger.debug('Chai token is currently disabled')
}
} catch (e) {
logger.debug('Methods for chai token are not present')
}
logger.debug('calling erc20Contract.methods.balanceOf')
const foreignErc20Balance = await erc20Contract.methods
@@ -85,29 +67,16 @@ async function main(bridgeMode, eventsInfo) {
const burntCoinsBN = new BN(burntCoins)
const totalSupplyBN = mintedCoinsBN.minus(burntCoinsBN)
const foreignErc20BalanceBN = new BN(foreignErc20Balance).plus(lateForeignConfirmationsTotalValue)
const investedAmountInDaiBN = new BN(investedAmountInDai)
const bridgeDsrBalanceBN = new BN(bridgeDsrBalance)
const diff = foreignErc20BalanceBN
.plus(investedAmountInDaiBN)
.minus(totalSupplyBN)
.toFixed()
const foreign = {
erc20Balance: Web3Utils.fromWei(foreignErc20Balance)
}
if (displayChaiToken) {
foreign.investedErc20Balance = Web3Utils.fromWei(investedAmountInDai)
foreign.accumulatedInterest = Web3Utils.fromWei(bridgeDsrBalanceBN.minus(investedAmountInDaiBN).toString(10))
}
const diff = foreignErc20BalanceBN.minus(totalSupplyBN).toFixed()
logger.debug('Done')
return {
home: {
totalSupply: Web3Utils.fromWei(totalSupplyBN.toFixed())
},
foreign,
foreign: {
erc20Balance: Web3Utils.fromWei(foreignErc20Balance)
},
balanceDiff: Number(Web3Utils.fromWei(diff)),
...blockRanges,
lastChecked: Math.floor(Date.now() / 1000)

View File

@@ -21,7 +21,9 @@ async function main(bridgeMode, eventsInfo) {
homeToForeignConfirmations,
homeToForeignRequests,
foreignToHomeConfirmations,
foreignToHomeRequests
foreignToHomeRequests,
informationRequests,
informationResponses
} = eventsInfo
if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
@@ -34,7 +36,9 @@ async function main(bridgeMode, eventsInfo) {
fromForeignToHomeDiff: foreignToHomeConfirmations.length - foreignToHomeRequests.length,
home: {
toForeign: homeToForeignRequests.length,
fromForeign: foreignToHomeConfirmations.length
fromForeign: foreignToHomeConfirmations.length,
informationRequests: informationRequests.length,
informationResponses: informationResponses.length
},
foreign: {
fromHome: homeToForeignConfirmations.length,

View File

@@ -11,7 +11,6 @@ const {
OLD_AMB_USER_REQUEST_FOR_AFFIRMATION_ABI
} = require('../../commons')
const { normalizeEventInformation } = require('./message')
const { filterTransferBeforeES } = require('./tokenUtils')
const { writeFile, readCacheFile } = require('./file')
const { web3Home, web3Foreign, getHomeBlockNumber, getForeignBlockNumber } = require('./web3')
const { getPastEvents } = require('./web3Cache')
@@ -129,6 +128,27 @@ async function main(mode) {
})).map(normalizeEvent)
foreignToHomeRequests = [...foreignToHomeRequests, ...foreignToHomeRequestsNew]
let informationRequests
let informationResponses
if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
logger.debug("calling homeBridge.getPastEvents('UserRequestForInformation')")
informationRequests = (await getPastEvents(homeBridge, {
event: 'UserRequestForInformation',
fromBlock: MONITOR_HOME_START_BLOCK,
toBlock: homeDelayedBlockNumber,
chain: 'home'
})).map(normalizeEvent)
logger.debug("calling foreignBridge.getPastEvents('InformationRetrieved')")
informationResponses = (await getPastEvents(homeBridge, {
event: 'InformationRetrieved',
fromBlock: MONITOR_HOME_START_BLOCK,
toBlock: homeBlockNumber,
safeToBlock: homeDelayedBlockNumber,
chain: 'home'
})).map(normalizeEvent)
}
if (isExternalErc20) {
logger.debug("calling erc20Contract.getPastEvents('Transfer')")
let transferEvents = (await getPastEvents(erc20Contract, {
@@ -139,80 +159,32 @@ async function main(mode) {
filter: { to: COMMON_FOREIGN_BRIDGE_ADDRESS }
},
chain: 'foreign'
}))
.map(normalizeEvent)
.filter(e => e.recipient !== ZERO_ADDRESS) // filter mint operation during SCD-to-MCD swaps
.filter(e => e.recipient.toLowerCase() !== '0x5d3a536e4d6dbd6114cc1ead35777bab948e3643') // filter cDai withdraws during compounding
// Get transfer events for each previously used Sai token
const saiTokenAddress = '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'
const halfDuplexTokenContract = new web3Foreign.eth.Contract(ERC20_ABI, saiTokenAddress)
logger.debug('Half duplex token:', saiTokenAddress)
logger.debug("calling halfDuplexTokenContract.getPastEvents('Transfer')")
// https://etherscan.io/tx/0xd0c3c92c94e05bc71256055ce8c4c993e047f04e04f3283a04e4cb077b71f6c6
const blockNumberHalfDuplexDisabled = 9884448
const halfDuplexTransferEvents = (await getPastEvents(halfDuplexTokenContract, {
event: 'Transfer',
fromBlock: MONITOR_FOREIGN_START_BLOCK,
toBlock: Math.min(blockNumberHalfDuplexDisabled, foreignDelayedBlockNumber),
options: {
filter: { to: COMMON_FOREIGN_BRIDGE_ADDRESS }
},
chain: 'foreign'
})).map(normalizeEvent)
let directTransfers = transferEvents
const tokensSwappedAbiExists = FOREIGN_ABI.filter(e => e.type === 'event' && e.name === 'TokensSwapped')[0]
if (tokensSwappedAbiExists) {
logger.debug('collecting half duplex tokens participated in the bridge balance')
logger.debug("calling foreignBridge.getPastEvents('TokensSwapped')")
const tokensSwappedEvents = await getPastEvents(foreignBridge, {
event: 'TokensSwapped',
fromBlock: MONITOR_FOREIGN_START_BLOCK,
toBlock: foreignBlockNumber,
chain: 'foreign',
safeToBlock: foreignDelayedBlockNumber
})
// Get token swap events emitted by foreign bridge
const bridgeTokensSwappedEvents = tokensSwappedEvents.filter(e => e.address === COMMON_FOREIGN_BRIDGE_ADDRESS)
// Get transfer events for each previous erc20
const uniqueTokenAddressesSet = new Set(bridgeTokensSwappedEvents.map(e => e.returnValues.from))
// Exclude chai token from previous erc20
try {
logger.debug('calling foreignBridge.chaiToken() to remove it from half duplex tokens list')
const chaiToken = await foreignBridge.methods.chaiToken().call()
uniqueTokenAddressesSet.delete(chaiToken)
} catch (e) {
logger.debug('call to foreignBridge.chaiToken() failed')
}
// Exclude dai token from previous erc20
try {
logger.debug('calling foreignBridge.erc20token() to remove it from half duplex tokens list')
const daiToken = await foreignBridge.methods.erc20token().call()
uniqueTokenAddressesSet.delete(daiToken)
} catch (e) {
logger.debug('call to foreignBridge.erc20token() failed')
}
const uniqueTokenAddresses = [...uniqueTokenAddressesSet]
await Promise.all(
uniqueTokenAddresses.map(async tokenAddress => {
const halfDuplexTokenContract = new web3Foreign.eth.Contract(ERC20_ABI, tokenAddress)
logger.debug('Half duplex token:', tokenAddress)
logger.debug("calling halfDuplexTokenContract.getPastEvents('Transfer')")
const halfDuplexTransferEvents = (await getPastEvents(halfDuplexTokenContract, {
event: 'Transfer',
fromBlock: MONITOR_FOREIGN_START_BLOCK,
toBlock: foreignDelayedBlockNumber,
options: {
filter: { to: COMMON_FOREIGN_BRIDGE_ADDRESS }
},
chain: 'foreign'
})).map(normalizeEvent)
// Remove events after the ES
logger.debug('filtering half duplex transfers happened before ES')
const validHalfDuplexTransfers = await filterTransferBeforeES(halfDuplexTransferEvents)
transferEvents = [...validHalfDuplexTransfers, ...transferEvents]
})
)
// filter transfer that is part of a token swap
directTransfers = transferEvents.filter(
e =>
bridgeTokensSwappedEvents.findIndex(
t => t.transactionHash === e.referenceTx && e.recipient === ZERO_ADDRESS
) === -1
)
}
transferEvents = [...halfDuplexTransferEvents, ...transferEvents]
// Get transfer events that didn't have a UserRequestForAffirmation event in the same transaction
directTransfers = directTransfers.filter(
const directTransfers = transferEvents.filter(
e => foreignToHomeRequests.findIndex(t => t.referenceTx === e.referenceTx) === -1
)
@@ -225,6 +197,8 @@ async function main(mode) {
homeToForeignConfirmations,
foreignToHomeConfirmations,
foreignToHomeRequests,
informationRequests,
informationResponses,
isExternalErc20,
bridgeMode,
homeBlockNumber,

View File

@@ -8,8 +8,8 @@ function readFile(filePath, parseJson = true) {
const json = JSON.parse(content)
const timeDiff = Math.floor(Date.now() / 1000) - json.lastChecked
return Object.assign({}, json, { timeDiff })
} catch (e) {
console.error('readFlle', e)
} catch (_) {
console.error(`File ${filePath} does not exist`)
return {
error: 'the bridge statistics are not available'
}

View File

@@ -28,6 +28,21 @@ function addExecutionStatus(processedList) {
}
}
function addRetrievalStatus(retrievedInfoList) {
const statuses = {}
retrievedInfoList.forEach(e => {
statuses[e.returnValues.messageId] = {
callStatus: e.returnValues.status,
callbackStatus: e.returnValues.callbackStatus
}
})
return deliveredMsg => {
deliveredMsg.callStatus = statuses[deliveredMsg.messageId].callStatus
deliveredMsg.callbackStatus = statuses[deliveredMsg.messageId].callbackStatus
return deliveredMsg
}
}
/**
* Normalizes the different event objects to facilitate data processing
* @param {Object} event
@@ -89,6 +104,7 @@ module.exports = {
deliveredMsgNotProcessed,
processedMsgNotDelivered,
addExecutionStatus,
addRetrievalStatus,
normalizeEventInformation,
eventWithoutReference,
unclaimedHomeToForeignRequests,

View File

@@ -1,27 +0,0 @@
// https://etherscan.io/tx/0xd0c3c92c94e05bc71256055ce8c4c993e047f04e04f3283a04e4cb077b71f6c6
const blockNumberHalfDuplexDisabled = 9884448
/**
* Returns true if the event was before the bridge stopped supporting half duplex transfers.
*/
async function transferBeforeES(event) {
return event.blockNumber < blockNumberHalfDuplexDisabled
}
async function filterTransferBeforeES(array) {
const newArray = []
// Iterate events from newer to older
for (let i = array.length - 1; i >= 0; i--) {
const beforeES = await transferBeforeES(array[i])
if (beforeES) {
// add element to first position so the new array will have the same order
newArray.unshift(array[i])
}
}
return newArray
}
module.exports = {
filterTransferBeforeES,
blockNumberHalfDuplexDisabled
}

View File

@@ -1,6 +1,5 @@
require('dotenv').config()
const Web3Utils = require('web3').utils
const fetch = require('node-fetch')
const logger = require('./logger')('validators')
const { getBridgeABIs, BRIDGE_VALIDATORS_ABI, gasPriceFromSupplier } = require('../commons')
const { web3Home, web3Foreign, getHomeBlockNumber, getForeignBlockNumber } = require('./utils/web3')
@@ -81,7 +80,7 @@ async function main(bridgeMode) {
if (MONITOR_VALIDATOR_HOME_TX_LIMIT) {
logger.debug('calling home getGasPrices')
homeGasPrice =
(await gasPriceFromSupplier(() => fetch(COMMON_HOME_GAS_PRICE_SUPPLIER_URL), homeGasPriceSupplierOpts)) ||
(await gasPriceFromSupplier(COMMON_HOME_GAS_PRICE_SUPPLIER_URL, homeGasPriceSupplierOpts)) ||
Web3Utils.toBN(COMMON_HOME_GAS_PRICE_FALLBACK)
homeGasPriceGwei = Web3Utils.fromWei(homeGasPrice.toString(), 'gwei')
homeTxCost = homeGasPrice.mul(Web3Utils.toBN(MONITOR_VALIDATOR_HOME_TX_LIMIT))
@@ -93,13 +92,9 @@ async function main(bridgeMode) {
if (MONITOR_VALIDATOR_FOREIGN_TX_LIMIT) {
logger.debug('calling foreign getGasPrices')
const fetchFn =
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL === 'gas-price-oracle'
? null
: () => fetch(COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL)
foreignGasPrice =
(await gasPriceFromSupplier(fetchFn, foreignGasPriceSupplierOpts)) ||
(await gasPriceFromSupplier(COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL, foreignGasPriceSupplierOpts)) ||
Web3Utils.toBN(COMMON_FOREIGN_GAS_PRICE_FALLBACK)
foreignGasPriceGwei = Web3Utils.fromWei(foreignGasPrice.toString(), 'gwei')
foreignTxCost = foreignGasPrice.mul(Web3Utils.toBN(MONITOR_VALIDATOR_FOREIGN_TX_LIMIT))

View File

@@ -4,7 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"start": "mocha --exit",
"start": "mocha",
"generate-amb-tx": "node ./scripts/generate-amb-tx.js",
"lint": "eslint . --ignore-path ../.eslintignore",
"amb": "mocha test/amb.js",
"erc-to-native": "mocha test/ercToNative.js",

View File

@@ -10,7 +10,7 @@ case "$mode" in
;;
esac
MODE="$mode" ../e2e-commons/up.sh deploy blocks oracle oracle-validator-2 oracle-validator-3
MODE="$mode" ../e2e-commons/up.sh deploy generate-amb-tx manual-amb-relay blocks oracle oracle-validator-2 oracle-validator-3
docker-compose -f ../e2e-commons/docker-compose.yml run e2e yarn workspace oracle-e2e run start $script
rc=$?

View File

@@ -0,0 +1,29 @@
const Web3 = require('web3')
const { user, homeRPC, foreignRPC, amb2: amb } = require('../../e2e-commons/constants.json')
const { BOX_ABI } = require('../../commons')
const homeWeb3 = new Web3(new Web3.providers.HttpProvider(homeRPC.URL))
const foreignWeb3 = new Web3(new Web3.providers.HttpProvider(foreignRPC.URL))
homeWeb3.eth.accounts.wallet.add(user.privateKey)
foreignWeb3.eth.accounts.wallet.add(user.privateKey)
const opts = {
from: user.address,
gas: 400000,
gasPrice: '1'
}
const homeBox = new homeWeb3.eth.Contract(BOX_ABI, amb.homeBox, opts)
const foreignBox = new foreignWeb3.eth.Contract(BOX_ABI, amb.foreignBox, opts)
async function main() {
const res1 = await homeBox.methods.setValueOnOtherNetwork(123, amb.home, amb.foreignBox).send()
const res2 = await foreignBox.methods.setValueOnOtherNetwork(456, amb.foreign, amb.homeBox).send()
const res3 = await foreignBox.methods.setValueOnOtherNetwork(789, amb.foreign, amb.homeBox).send()
console.log(res1.transactionHash)
console.log(res2.transactionHash)
console.log(res3.transactionHash)
}
main()

View File

@@ -1,8 +1,9 @@
const Web3 = require('web3')
const assert = require('assert')
const { ASYNC_CALL_ERRORS } = require('../../oracle/src/utils/constants')
const { user, homeRPC, foreignRPC, amb, validator } = require('../../e2e-commons/constants.json')
const { uniformRetry } = require('../../e2e-commons/utils')
const { BOX_ABI, HOME_AMB_ABI, FOREIGN_AMB_ABI } = require('../../commons')
const { BOX_ABI, HOME_AMB_ABI, FOREIGN_AMB_ABI, ambInformationSignatures } = require('../../commons')
const { delay, setRequiredSignatures } = require('./utils')
const { toBN } = Web3.utils
@@ -26,27 +27,49 @@ const foreignBox = new foreignWeb3.eth.Contract(BOX_ABI, amb.foreignBox, opts)
const homeBridge = new homeWeb3.eth.Contract(HOME_AMB_ABI, amb.home, opts)
const foreignBridge = new foreignWeb3.eth.Contract(FOREIGN_AMB_ABI, amb.foreign, opts)
function validateBlock(web3, serialized, block) {
assert.strictEqual(serialized.length, 2 + 64 * 12)
const values = web3.eth.abi.decodeParameter(
'(uint256,bytes32,address,uint256,uint256,bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256)',
serialized
)
assert.strictEqual(values[0], block.number.toString(), 'wrong block number returned')
assert.strictEqual(values[1], block.hash, 'wrong block hash returned')
assert.strictEqual(values[2], block.miner, 'wrong block miner returned')
assert.strictEqual(values[3], block.gasUsed.toString(), 'wrong block gasUsed returned')
assert.strictEqual(values[4], block.gasLimit.toString(), 'wrong block gasLimit returned')
assert.strictEqual(values[5], block.parentHash, 'wrong block parentHash returned')
assert.strictEqual(values[6], block.receiptsRoot, 'wrong block receiptsRoot returned')
assert.strictEqual(values[7], block.stateRoot, 'wrong block stateRoot returned')
assert.strictEqual(values[8], block.transactionsRoot, 'wrong block transactionsRoot returned')
assert.strictEqual(values[9], block.timestamp.toString(), 'wrong block timestamp returned')
assert.strictEqual(values[10], block.difficulty, 'wrong block difficulty returned')
assert.strictEqual(values[11], block.totalDifficulty, 'wrong block totalDifficulty returned')
}
function validateTransaction(web3, serialized, tx) {
assert.strictEqual(serialized.length, 64 * 13 + tx.input.length + 56)
const values = web3.eth.abi.decodeParameter(
'(bytes32,uint256,bytes32,uint256,address,address,uint256,uint256,uint256,uint256,bytes)',
serialized
)
assert.strictEqual(values[0], tx.hash, 'wrong txHash returned')
assert.strictEqual(values[1], tx.blockNumber.toString(), 'wrong tx blockNumber returned')
assert.strictEqual(values[2], tx.blockHash.toString(), 'wrong tx blockHash returned')
assert.strictEqual(values[3], tx.transactionIndex.toString(), 'wrong tx transactionIndex returned')
assert.strictEqual(values[4], tx.from, 'wrong tx from returned')
assert.strictEqual(values[5], tx.to, 'wrong tx to returned')
assert.strictEqual(values[6], tx.value, 'wrong tx value returned')
assert.strictEqual(values[7], tx.nonce.toString(), 'wrong tx nonce returned')
assert.strictEqual(values[8], tx.gas.toString(), 'wrong tx gas returned')
assert.strictEqual(values[9], tx.gasPrice, 'wrong tx gasPrice returned')
assert.strictEqual(values[10], tx.input, 'wrong tx data returned')
}
describe('arbitrary message bridging', () => {
let requiredSignatures = 1
before(async () => {
const allowedMethods = [
'eth_call(address,bytes)',
'eth_call(address,bytes,uint256)',
'eth_call(address,address,uint256,bytes)',
'eth_blockNumber()',
'eth_getBlockByNumber()',
'eth_getBlockByNumber(uint256)',
'eth_getBlockByHash(bytes32)',
'eth_getBalance(address)',
'eth_getBalance(address,uint256)',
'eth_getTransactionCount(address)',
'eth_getTransactionCount(address,uint256)',
'eth_getTransactionByHash(bytes32)',
'eth_getTransactionReceipt(bytes32)',
'eth_getStorageAt(address,bytes32)',
'eth_getStorageAt(address,bytes32,uint256)'
]
for (const method of allowedMethods) {
for (const method of ambInformationSignatures) {
const selector = homeWeb3.utils.soliditySha3(method)
await homeBridge.methods.enableAsyncRequestSelector(selector, true).send({ from: validator.address })
}
@@ -80,6 +103,19 @@ describe('arbitrary message bridging', () => {
}
})
})
if (process.env.ULTIMATE !== 'true') {
describe('Confirm Relay', () => {
it('should process lost affirmation-request via confirm relay', async () => {
const value = await homeBox.methods.value().call()
assert(value === '789', 'incorrect value')
})
it('should process lost signature-request & collected-signatures via confirm relay', async () => {
const value = await foreignBox.methods.value().call()
assert(value === '123', 'incorrect value')
})
})
}
describe('Home to Foreign', () => {
describe('Subsidized Mode', () => {
it('should bridge message', async () => {
@@ -88,12 +124,13 @@ describe('arbitrary message bridging', () => {
const initialValue = await foreignBox.methods.value().call()
assert(!toBN(initialValue).eq(toBN(newValue)), 'initial value should be different from new value')
await homeBox.methods
const res = await homeBox.methods
.setValueOnOtherNetwork(newValue, amb.home, amb.foreignBox)
.send()
.catch(e => {
console.error(e)
})
console.log(res.transactionHash)
// check that value changed and balance decreased
await uniformRetry(async retry => {
@@ -186,12 +223,13 @@ describe('arbitrary message bridging', () => {
const initialValue = await homeBox.methods.value().call()
assert(!toBN(initialValue).eq(toBN(newValue)), 'initial value should be different from new value')
await foreignBox.methods
const res = await foreignBox.methods
.setValueOnOtherNetwork(newValue, amb.foreign, amb.homeBox)
.send()
.catch(e => {
console.error(e)
})
console.log(res.transactionHash)
// check that value changed and balance decreased
await uniformRetry(async retry => {
@@ -266,7 +304,7 @@ describe('arbitrary message bridging', () => {
await makeAsyncCall(selector, data2)
assert(!(await homeBox.methods.status().call()), 'status is true')
assert.strictEqual(await homeBox.methods.data().call(), null, 'returned data is incorrect')
assert.strictEqual(await homeBox.methods.data().call(), ASYNC_CALL_ERRORS.REVERT, 'returned data is incorrect')
const data3 = homeWeb3.eth.abi.encodeParameters(
['address', 'address', 'uint256', 'bytes'],
@@ -276,7 +314,7 @@ describe('arbitrary message bridging', () => {
await makeAsyncCall(selector, data3)
assert(!(await homeBox.methods.status().call()), 'status is true')
assert.strictEqual(await homeBox.methods.data().call(), null, 'returned data is incorrect')
assert.strictEqual(await homeBox.methods.data().call(), ASYNC_CALL_ERRORS.REVERT, 'returned data is incorrect')
})
it('should make async eth_call for specific block', async () => {
@@ -285,7 +323,7 @@ describe('arbitrary message bridging', () => {
const selector = homeWeb3.utils.soliditySha3('eth_call(address,bytes,uint256)')
const data1 = homeWeb3.eth.abi.encodeParameters(
['address', 'bytes', 'uint256'],
[amb.foreignBox, foreignBox.methods.value().encodeABI(), 60]
[amb.foreignBox, foreignBox.methods.value().encodeABI(), 25]
)
const data2 = homeWeb3.eth.abi.encodeParameters(
['address', 'bytes', 'uint256'],
@@ -317,6 +355,11 @@ describe('arbitrary message bridging', () => {
await makeAsyncCall(selector, data3)
assert(!(await homeBox.methods.status().call()), 'status is true')
assert.strictEqual(
await homeBox.methods.data().call(),
ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE,
'returned data is incorrect'
)
})
it('should make async eth_blockNumber', async () => {
@@ -336,15 +379,8 @@ describe('arbitrary message bridging', () => {
assert(await homeBox.methods.status().call(), 'status is false')
const data = await homeBox.methods.data().call()
assert.strictEqual(data.length, 2 + 64 * 3)
const { 0: number, 1: hash, 2: miner } = homeWeb3.eth.abi.decodeParameters(
['uint256', 'bytes32', 'address'],
data
)
const block = await foreignWeb3.eth.getBlock(blockNumber)
assert.strictEqual(number, blockNumber, 'wrong block number returned')
assert.strictEqual(hash, block.hash, 'wrong block hash returned')
assert.strictEqual(miner, block.miner, 'wrong block miner returned')
validateBlock(homeWeb3, data, block)
})
it('should make async eth_getBlockByNumber and return latest block', async () => {
@@ -354,7 +390,7 @@ describe('arbitrary message bridging', () => {
assert(await homeBox.methods.status().call(), 'status is false')
const data = await homeBox.methods.data().call()
assert.strictEqual(data.length, 2 + 64 * 3)
assert.strictEqual(data.length, 2 + 64 * 12)
})
it('should make async eth_getBlockByHash', async () => {
@@ -366,16 +402,7 @@ describe('arbitrary message bridging', () => {
assert(await homeBox.methods.status().call(), 'status is false')
const data = await homeBox.methods.data().call()
assert.strictEqual(data.length, 2 + 64 * 3)
const { 0: number, 1: hash, 2: miner } = homeWeb3.eth.abi.decodeParameters(
['uint256', 'bytes32', 'address'],
data
)
assert.strictEqual(number, blockNumber, 'wrong block number returned')
assert.strictEqual(hash, block.hash, 'wrong block hash returned')
assert.strictEqual(miner, block.miner, 'wrong block miner returned')
validateBlock(homeWeb3, data, block)
})
it('should make async eth_getBalance', async () => {
@@ -463,7 +490,7 @@ describe('arbitrary message bridging', () => {
})
it('should make async eth_getTransactionByHash', async () => {
const txHash = '0x09dfb947dbd17e27bcc117773b6e133829f7cef9646199a93ef019c4f7c0fec6'
const txHash = '0x7262f7dbe6c30599edded2137fbbe93c271b37f5c54dd27f713f0cf510e3b4dd'
const tx = await foreignWeb3.eth.getTransaction(txHash)
const selector = homeWeb3.utils.soliditySha3('eth_getTransactionByHash(bytes32)')
@@ -471,32 +498,11 @@ describe('arbitrary message bridging', () => {
assert(await homeBox.methods.status().call(), 'status is false')
const data = await homeBox.methods.data().call()
const dataTypes = [
'bytes32',
'uint256',
'address',
'address',
'uint256',
'uint256',
'uint256',
'uint256',
'bytes'
]
const values = homeWeb3.eth.abi.decodeParameters(dataTypes, data)
assert.strictEqual(values[0], txHash, 'wrong txHash returned')
assert.strictEqual(values[1], tx.blockNumber.toString(), 'wrong tx blockNumber returned')
assert.strictEqual(values[2], tx.from, 'wrong tx from returned')
assert.strictEqual(values[3], tx.to, 'wrong tx to returned')
assert.strictEqual(values[4], tx.value, 'wrong tx value returned')
assert.strictEqual(values[5], tx.nonce.toString(), 'wrong tx nonce returned')
assert.strictEqual(values[6], tx.gas.toString(), 'wrong tx gas returned')
assert.strictEqual(values[7], tx.gasPrice, 'wrong tx gasPrice returned')
assert.strictEqual(values[8], tx.input, 'wrong tx data returned')
validateTransaction(homeWeb3, data, tx)
})
it('should make async eth_getTransactionReceipt', async () => {
const txHash = '0x09dfb947dbd17e27bcc117773b6e133829f7cef9646199a93ef019c4f7c0fec6'
const txHash = '0x7262f7dbe6c30599edded2137fbbe93c271b37f5c54dd27f713f0cf510e3b4dd'
const receipt = await foreignWeb3.eth.getTransactionReceipt(txHash)
const selector = homeWeb3.utils.soliditySha3('eth_getTransactionReceipt(bytes32)')
@@ -504,18 +510,25 @@ describe('arbitrary message bridging', () => {
assert(await homeBox.methods.status().call(), 'status is false')
const data = await homeBox.methods.data().call()
const dataTypes = ['bytes32', 'uint256', 'bool', '(address,bytes32[],bytes)[]']
const values = homeWeb3.eth.abi.decodeParameters(dataTypes, data)
const values = homeWeb3.eth.abi.decodeParameter(
'(bytes32,uint256,bytes32,uint256,address,address,uint256,bool,(address,bytes32[],bytes)[])',
data
)
assert.strictEqual(values[0], txHash, 'wrong txHash returned')
assert.strictEqual(values[1], receipt.blockNumber.toString(), 'wrong tx blockNumber returned')
assert.strictEqual(values[2], receipt.status, 'wrong tx status returned')
assert.strictEqual(values[3].length, 1, 'wrong logs length returned')
assert.strictEqual(values[3][0][0], receipt.logs[0].address, 'wrong log address returned')
assert.strictEqual(values[3][0][1].length, 2, 'wrong log topics length returned')
assert.strictEqual(values[3][0][1][0], receipt.logs[0].topics[0], 'wrong event signature returned')
assert.strictEqual(values[3][0][1][1], receipt.logs[0].topics[1], 'wrong message id returned')
assert.strictEqual(values[3][0][2], receipt.logs[0].data, 'wrong log data returned')
assert.strictEqual(values[2], receipt.blockHash, 'wrong tx blockHash returned')
assert.strictEqual(values[3], receipt.transactionIndex.toString(), 'wrong tx transactionIndex returned')
assert.strictEqual(values[4].toLowerCase(), receipt.from, 'wrong tx from returned')
assert.strictEqual(values[5].toLowerCase(), receipt.to, 'wrong tx to returned')
assert.strictEqual(values[6], receipt.gasUsed.toString(), 'wrong gasUsed to returned')
assert.strictEqual(values[7], receipt.status, 'wrong tx status returned')
assert.strictEqual(values[8].length, 1, 'wrong logs length returned')
assert.strictEqual(values[8][0][0], receipt.logs[0].address, 'wrong log address returned')
assert.strictEqual(values[8][0][1].length, 2, 'wrong log topics length returned')
assert.strictEqual(values[8][0][1][0], receipt.logs[0].topics[0], 'wrong event signature returned')
assert.strictEqual(values[8][0][1][1], receipt.logs[0].topics[1], 'wrong message id returned')
assert.strictEqual(values[8][0][2], receipt.logs[0].data, 'wrong log data returned')
})
it('should make async eth_getStorageAt', async () => {

View File

@@ -33,6 +33,10 @@ const homeBridge = new homeWeb3.eth.Contract(HOME_ERC_TO_NATIVE_ABI, COMMON_HOME
describe('erc to native', () => {
before(async () => {
console.log('Initializing interest')
await foreignBridge.methods
.initializeInterest(ercToNativeBridge.foreignToken, 1, 1, validator.address)
.send({ from: validator.address, gas: '4000000' })
if (process.env.ULTIMATE === 'true') {
return
}
@@ -100,6 +104,8 @@ describe('erc to native', () => {
const transferValue = homeWeb3.utils.toWei('0.05')
// transfer that should not be processed by the filter
await erc20Token.methods.transfer(secondUser.address, transferValue).send({ from: user.address, gas: 100000 })
// send tokens to foreign bridge
await erc20Token.methods
.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, transferValue)
@@ -110,6 +116,7 @@ describe('erc to native', () => {
.catch(e => {
console.error(e)
})
await foreignBridge.methods.investDai().send({ from: validator.address, gas: '4000000' })
// check that balance increases
await uniformRetry(async retry => {

View File

@@ -1 +1 @@
--timeout 120000
--timeout 120000 --exit

View File

@@ -6,6 +6,7 @@ module.exports = {
...baseConfig,
main: baseConfig.foreign,
event: 'UserRequestForAffirmation',
sender: 'home',
queue: 'home-prioritized',
name: `watcher-${id}`,
id

View File

@@ -8,7 +8,8 @@ const {
FOREIGN_AMB_ABI
} = require('../../commons')
const { web3Home, web3Foreign } = require('../src/services/web3')
const { privateKeyToAddress } = require('../src/utils/utils')
const { add0xPrefix, privateKeyToAddress } = require('../src/utils/utils')
const { EXIT_CODES } = require('../src/utils/constants')
const {
ORACLE_BRIDGE_MODE,
@@ -79,9 +80,22 @@ const foreignConfig = {
const maxProcessingTime =
parseInt(ORACLE_MAX_PROCESSING_TIME, 10) || 4 * Math.max(homeConfig.pollingInterval, foreignConfig.pollingInterval)
let validatorPrivateKey
if (ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY) {
validatorPrivateKey = add0xPrefix(ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY)
const derived = privateKeyToAddress(validatorPrivateKey)
if (ORACLE_VALIDATOR_ADDRESS && derived.toLowerCase() !== ORACLE_VALIDATOR_ADDRESS.toLowerCase()) {
console.error(
`Derived address from private key - ${derived} is different from ORACLE_VALIDATOR_ADDRESS=${ORACLE_VALIDATOR_ADDRESS}`
)
process.exit(EXIT_CODES.INCOMPATIBILITY)
}
}
module.exports = {
eventFilter: {},
validatorAddress: ORACLE_VALIDATOR_ADDRESS || privateKeyToAddress(ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY),
validatorPrivateKey,
validatorAddress: ORACLE_VALIDATOR_ADDRESS || privateKeyToAddress(validatorPrivateKey),
maxProcessingTime,
shutdownKey: 'oracle-shutdown',
home: homeConfig,

View File

@@ -6,6 +6,7 @@ module.exports = {
...baseConfig,
main: baseConfig.home,
event: 'CollectedSignatures',
sender: 'foreign',
queue: 'foreign-prioritized',
name: `watcher-${id}`,
id

View File

@@ -8,6 +8,7 @@ module.exports = {
web3ForeignArchive: web3ForeignArchive || baseConfig.foreign.web3,
main: baseConfig.home,
event: 'UserRequestForInformation',
sender: 'home',
queue: 'home-prioritized',
name: `watcher-${id}`,
id

View File

@@ -6,6 +6,7 @@ module.exports = {
...baseConfig,
main: baseConfig.home,
event: 'UserRequestForSignature',
sender: 'home',
queue: 'home-prioritized',
name: `watcher-${id}`,
id

View File

@@ -1,20 +1,7 @@
const baseConfig = require('./base.config')
const { ERC20_ABI } = require('../../commons')
const { ERC20_ABI, ZERO_ADDRESS } = require('../../commons')
const { EXIT_CODES } = require('../src/utils/constants')
const initialChecksJson = process.argv[3]
if (!initialChecksJson) {
throw new Error('initial check parameter was not provided.')
}
let initialChecks
try {
initialChecks = JSON.parse(initialChecksJson)
} catch (e) {
throw new Error('Error on decoding values from initial checks.')
}
const id = `${baseConfig.id}-transfer`
if (baseConfig.id !== 'erc-native') {
@@ -22,14 +9,15 @@ if (baseConfig.id !== 'erc-native') {
process.exit(EXIT_CODES.WATCHER_NOT_REQUIRED)
}
// exact address of the token contract is set in the watcher.js checkConditions() function
baseConfig.foreign.eventContract = new baseConfig.foreign.web3.eth.Contract(ERC20_ABI, ZERO_ADDRESS)
module.exports = {
...baseConfig,
main: {
...baseConfig.foreign,
eventContract: new baseConfig.foreign.web3.eth.Contract(ERC20_ABI, initialChecks.bridgeableTokenAddress)
},
main: baseConfig.foreign,
event: 'Transfer',
eventFilter: { to: process.env.COMMON_FOREIGN_BRIDGE_ADDRESS },
eventFilter: { to: baseConfig.foreign.bridgeAddress },
sender: 'home',
queue: 'home-prioritized',
name: `watcher-${id}`,
id

52
oracle/esController.sol Normal file
View File

@@ -0,0 +1,52 @@
//SPDX-License-Identifier: GPL-3.0-only
/*
This contract can be used together with the emergency shutdown
functionality of the TokenBridge oracles.
*/
pragma solidity 0.7.6;
contract PauseController {
address public manager;
bool internal paused;
bytes32 private immutable ID;
constructor (string memory _id, address _manager) {
require(bytes(_id).length <= 32);
bytes32 id;
assembly {
id := mload(add(_id, 32))
}
ID = id;
manager = _manager;
}
modifier onlyManager() {
require(msg.sender == manager);
_;
}
function changeManager(address _newmanager) external onlyManager {
require(_newmanager != address(0));
manager = _newmanager;
}
function pause() external onlyManager {
paused = true;
}
function play() external onlyManager {
paused = false;
}
function isPaused() external view returns(bool) {
return paused;
}
function id() external view returns(string memory) {
return string(abi.encodePacked(ID));
}
}

View File

@@ -13,6 +13,10 @@
"sender:home": "./scripts/start-worker.sh sender home-sender",
"sender:foreign": "./scripts/start-worker.sh sender foreign-sender",
"confirm:transfer": "./scripts/start-worker.sh confirmRelay transfer-watcher",
"confirm:affirmation-request": "./scripts/start-worker.sh confirmRelay affirmation-request-watcher",
"confirm:signature-request": "./scripts/start-worker.sh confirmRelay signature-request-watcher",
"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",
"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",

View File

@@ -1,26 +0,0 @@
require('../env')
const { getTokensState } = require('../src/utils/tokenState')
const { FOREIGN_ERC_TO_NATIVE_ABI } = require('../../commons')
const { web3Foreign } = require('../src/services/web3')
const emptyLogger = {
debug: () => {},
info: () => {}
}
async function initialChecks() {
const { ORACLE_BRIDGE_MODE, COMMON_FOREIGN_BRIDGE_ADDRESS } = process.env
let result = {}
if (ORACLE_BRIDGE_MODE === 'ERC_TO_NATIVE') {
const bridge = new web3Foreign.eth.Contract(FOREIGN_ERC_TO_NATIVE_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS)
result = await getTokensState(bridge, emptyLogger)
}
console.log(JSON.stringify(result))
return result
}
const result = initialChecks()
module.exports = result

View File

@@ -8,12 +8,10 @@ LOGS_DIR="logs/"
WORKER="${WORKERS_DIR}${1}.js"
CONFIG="${2}.config.js"
LOG="${LOGS_DIR}${2}.txt"
TX_HASH="${3}"
CHECKS=$(node scripts/initialChecks.js)
TX_HASH=${@:3}
if [ "${NODE_ENV}" = "production" ]; then
exec node "${WORKER}" "${CONFIG}" "$CHECKS" "$TX_HASH"
exec node "${WORKER}" "${CONFIG}" $TX_HASH
else
node "${WORKER}" "${CONFIG}" "$CHECKS" "$TX_HASH" | tee -a "${LOG}" | pino-pretty
node "${WORKER}" "${CONFIG}" $TX_HASH | tee -a "${LOG}" | pino-pretty
fi

View File

@@ -1,22 +1,39 @@
require('../env')
const path = require('path')
const fs = require('fs')
const { isAttached, connectWatcherToQueue, connection } = require('./services/amqpClient')
const logger = require('./services/logger')
const GasPrice = require('./services/gasPrice')
const { getNonce, getChainId, getEventsFromTx } = require('./tx/web3')
const { sendTx } = require('./tx/sendTx')
const { getTokensState } = require('./utils/tokenState')
const { checkHTTPS, watchdog, syncForEach, addExtraGas } = require('./utils/utils')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
const { ORACLE_VALIDATOR_ADDRESS, ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY, ORACLE_ALLOW_HTTP_FOR_RPC } = process.env
const { ORACLE_ALLOW_HTTP_FOR_RPC } = process.env
if (process.argv.length < 5) {
if (process.argv.length < 4) {
logger.error('Please check the number of arguments, transaction hash is not present')
process.exit(EXIT_CODES.GENERAL_ERROR)
}
const config = require(path.join('../config/', process.argv[2]))
const txHash = process.argv[4]
const { web3, eventContract, chain, bridgeContract } = config.main
const isTxHash = txHash => txHash.length === 66 && web3.utils.isHexStrict(txHash)
function readTxHashes(filePath) {
return fs
.readFileSync(filePath)
.toString()
.split('\n')
.map(v => v.trim())
.filter(isTxHash)
}
const txHashesArgs = process.argv.slice(3)
const rawTxHashes = txHashesArgs.filter(isTxHash)
const txHashesFiles = txHashesArgs.filter(path => fs.existsSync(path)).flatMap(readTxHashes)
const txHashes = [...rawTxHashes, ...txHashesFiles]
const processSignatureRequests = require('./events/processSignatureRequests')(config)
const processCollectedSignatures = require('./events/processCollectedSignatures')(config)
@@ -27,15 +44,13 @@ const processAMBCollectedSignatures = require('./events/processAMBCollectedSigna
const processAMBAffirmationRequests = require('./events/processAMBAffirmationRequests')(config)
const processAMBInformationRequests = require('./events/processAMBInformationRequests')(config)
const { web3, eventContract } = config.main
let attached
async function initialize() {
try {
const checkHttps = checkHTTPS(ORACLE_ALLOW_HTTP_FOR_RPC, logger)
web3.currentProvider.urls.forEach(checkHttps(config.chain))
web3.currentProvider.urls.forEach(checkHttps(chain))
attached = await isAttached()
if (attached) {
@@ -59,12 +74,12 @@ async function runMain({ sendToQueue }) {
const sendJob = attached ? sendToQueue : sendJobTx
if (!attached || connection.isConnected()) {
if (config.maxProcessingTime) {
await watchdog(() => main({ sendJob, txHash }), config.maxProcessingTime, () => {
await watchdog(() => main({ sendJob, txHashes }), config.maxProcessingTime, () => {
logger.fatal('Max processing time reached')
process.exit(EXIT_CODES.MAX_TIME_REACHED)
})
} else {
await main({ sendJob, txHash })
await main({ sendJob, txHashes })
}
} else {
setTimeout(() => {
@@ -99,27 +114,37 @@ function processEvents(events) {
}
}
async function main({ sendJob, txHash }) {
try {
const events = await getEventsFromTx({
web3,
contract: eventContract,
event: config.event,
txHash,
filter: config.eventFilter
})
logger.info(`Found ${events.length} ${config.event} events`)
async function main({ sendJob, txHashes }) {
if (config.id === 'erc-native-transfer') {
logger.debug('Getting token address to listen Transfer events')
const state = await getTokensState(bridgeContract, logger)
eventContract.options.address = state.bridgeableTokenAddress
}
if (events.length) {
const job = await processEvents(events)
logger.info('Transactions to send:', job.length)
logger.info(`Processing ${txHashes.length} input transactions`)
for (const txHash of txHashes) {
try {
logger.info({ txHash }, `Processing transaction`)
const events = await getEventsFromTx({
web3,
contract: eventContract,
event: config.event,
txHash,
filter: config.eventFilter
})
logger.info({ txHash }, `Found ${events.length} ${config.event} events`)
if (job.length) {
await sendJob(job)
if (events.length) {
const job = await processEvents(events)
logger.info({ txHash }, 'Transactions to send:', job.length)
if (job.length) {
await sendJob(job)
}
}
} catch (e) {
logger.error(e)
}
} catch (e) {
logger.error(e)
}
await connection.close()
@@ -128,9 +153,13 @@ async function main({ sendJob, txHash }) {
}
async function sendJobTx(jobs) {
const gasPrice = await GasPrice.start(config.chain, true)
await GasPrice.start(chain, true)
const gasPrice = GasPrice.getPrice().toString(10)
const { web3 } = config.sender === 'foreign' ? config.foreign : config.home
const chainId = await getChainId(web3)
let nonce = await getNonce(web3, ORACLE_VALIDATOR_ADDRESS)
let nonce = await getNonce(web3, config.validatorAddress)
await syncForEach(jobs, async job => {
let gasLimit
@@ -145,10 +174,10 @@ async function sendJobTx(jobs) {
const txHash = await sendTx({
data: job.data,
nonce,
gasPrice: gasPrice.toString(10),
gasPrice,
amount: '0',
gasLimit,
privateKey: ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY,
privateKey: config.validatorPrivateKey,
to: job.to,
chainId,
web3
@@ -167,7 +196,7 @@ async function sendJobTx(jobs) {
)
if (e.message.toLowerCase().includes('insufficient funds')) {
const currentBalance = await web3.eth.getBalance(ORACLE_VALIDATOR_ADDRESS)
const currentBalance = await web3.eth.getBalance(config.validatorAddress)
const minimumBalance = gasLimit.multipliedBy(gasPrice)
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

@@ -1,6 +1,7 @@
const { toBN } = require('web3').utils
const { zipToObject } = require('../../../utils/utils')
const { ASYNC_CALL_ERRORS, ASYNC_ETH_CALL_MAX_GAS_LIMIT } = require('../../../utils/constants')
const { zipToObject, isRevertError } = require('../../../utils/utils')
const argTypes = {
to: 'address',
@@ -17,14 +18,23 @@ function makeCall(argNames) {
const { blockNumber, ...opts } = zipToObject(argNames, args)
if (blockNumber && toBN(blockNumber).gt(toBN(foreignBlock.number))) {
return [false, '0x']
return [false, ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE]
}
const [status, result] = await web3.eth
.call(opts, blockNumber || foreignBlock.number)
.then(result => [true, result], err => [false, err.data])
// different clients might use different default gas limits, so it makes sense to limit it by some large number
if (!opts.gas || toBN(opts.gas).gt(toBN(ASYNC_ETH_CALL_MAX_GAS_LIMIT))) {
opts.gas = ASYNC_ETH_CALL_MAX_GAS_LIMIT
}
return [status, web3.eth.abi.encodeParameter('bytes', result)]
return web3.eth
.call(opts, blockNumber || foreignBlock.number)
.then(result => [true, web3.eth.abi.encodeParameter('bytes', result)])
.catch(e => {
if (isRevertError(e)) {
return [false, ASYNC_CALL_ERRORS.REVERT]
}
throw e
})
}
}

View File

@@ -1,5 +1,7 @@
const { toBN } = require('web3').utils
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
async function call(web3, data, foreignBlock) {
const address = web3.eth.abi.decodeParameter('address', data)
@@ -12,7 +14,7 @@ async function callArchive(web3, data, foreignBlock) {
const { 0: address, 1: blockNumber } = web3.eth.abi.decodeParameters(['address', 'uint256'], data)
if (toBN(blockNumber).gt(toBN(foreignBlock.number))) {
return [false, '0x']
return [false, ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE]
}
const balance = await web3.eth.getBalance(address, blockNumber)

View File

@@ -1,3 +1,4 @@
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
const { serializeBlock } = require('./serializers')
async function call(web3, data, foreignBlock) {
@@ -6,7 +7,7 @@ async function call(web3, data, foreignBlock) {
const block = await web3.eth.getBlock(blockHash)
if (block === null || block.number > foreignBlock.number) {
return [false, '0x']
return [false, ASYNC_CALL_ERRORS.NOT_FOUND]
}
return [true, serializeBlock(web3, block)]

View File

@@ -1,12 +1,13 @@
const { toBN } = require('web3').utils
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
const { serializeBlock } = require('./serializers')
async function call(web3, data, foreignBlock) {
const blockNumber = web3.eth.abi.decodeParameter('uint256', data)
if (toBN(blockNumber).gt(toBN(foreignBlock.number))) {
return [false, '0x']
return [false, ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE]
}
const block = await web3.eth.getBlock(blockNumber)

View File

@@ -1,5 +1,7 @@
const { toBN } = require('web3').utils
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
async function call(web3, data, foreignBlock) {
const { 0: address, 1: slot } = web3.eth.abi.decodeParameters(['address', 'bytes32'], data)
@@ -12,7 +14,7 @@ async function callArchive(web3, data, foreignBlock) {
const { 0: address, 1: slot, 2: blockNumber } = web3.eth.abi.decodeParameters(['address', 'bytes32', 'uint256'], data)
if (toBN(blockNumber).gt(toBN(foreignBlock.number))) {
return [false, '0x']
return [false, ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE]
}
const value = await web3.eth.getStorageAt(address, slot, blockNumber)

View File

@@ -1,3 +1,4 @@
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
const { serializeTx } = require('./serializers')
async function call(web3, data, foreignBlock) {
@@ -6,7 +7,7 @@ async function call(web3, data, foreignBlock) {
const tx = await web3.eth.getTransaction(hash)
if (tx === null || tx.blockNumber > foreignBlock.number) {
return [false, '0x']
return [false, ASYNC_CALL_ERRORS.NOT_FOUND]
}
return [true, serializeTx(web3, tx)]

View File

@@ -1,5 +1,7 @@
const { toBN } = require('web3').utils
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
async function call(web3, data, foreignBlock) {
const address = web3.eth.abi.decodeParameter('address', data)
@@ -12,7 +14,7 @@ async function callArchive(web3, data, foreignBlock) {
const { 0: address, 1: blockNumber } = web3.eth.abi.decodeParameters(['address', 'uint256'], data)
if (toBN(blockNumber).gt(toBN(foreignBlock.number))) {
return [false, '0x']
return [false, ASYNC_CALL_ERRORS.BLOCK_IS_IN_THE_FUTURE]
}
const nonce = await web3.eth.getTransactionCount(address, blockNumber)

View File

@@ -1,3 +1,4 @@
const { ASYNC_CALL_ERRORS } = require('../../../utils/constants')
const { serializeReceipt } = require('./serializers')
async function call(web3, data, foreignBlock) {
@@ -6,7 +7,7 @@ async function call(web3, data, foreignBlock) {
const receipt = await web3.eth.getTransactionReceipt(hash)
if (receipt === null || receipt.blockNumber > foreignBlock.number) {
return [false, '0x']
return [false, ASYNC_CALL_ERRORS.NOT_FOUND]
}
return [true, serializeReceipt(web3, receipt)]

View File

@@ -1,15 +1,30 @@
const { ZERO_ADDRESS } = require('../../../../../commons')
const serializeBlock = (web3, block) => {
const args = [block.number, block.hash, block.miner]
const types = ['uint256', 'bytes32', 'address']
return web3.eth.abi.encodeParameters(types, args)
const args = [
block.number,
block.hash,
block.miner,
block.gasUsed,
block.gasLimit,
block.parentHash,
block.receiptsRoot,
block.stateRoot,
block.transactionsRoot,
block.timestamp,
block.difficulty,
block.totalDifficulty
]
const type = '(uint256,bytes32,address,uint256,uint256,bytes32,bytes32,bytes32,bytes32,uint256,uint256,uint256)'
return web3.eth.abi.encodeParameter(type, args)
}
const serializeTx = (web3, tx) => {
const args = [
tx.hash,
tx.blockNumber,
tx.blockHash,
tx.transactionIndex,
tx.from,
tx.to || ZERO_ADDRESS,
tx.value,
@@ -18,16 +33,26 @@ const serializeTx = (web3, tx) => {
tx.gasPrice,
tx.input
]
const types = ['bytes32', 'uint256', 'address', 'address', 'uint256', 'uint256', 'uint256', 'uint256', 'bytes']
return web3.eth.abi.encodeParameters(types, args)
const type = '(bytes32,uint256,bytes32,uint256,address,address,uint256,uint256,uint256,uint256,bytes)'
return web3.eth.abi.encodeParameter(type, args)
}
const normalizeLog = log => [log.address, log.topics, log.data]
const serializeReceipt = (web3, receipt) => {
const args = [receipt.transactionHash, receipt.blockNumber, receipt.status, receipt.logs.map(normalizeLog)]
const types = ['bytes32', 'uint256', 'bool', '(address,bytes32[],bytes)[]']
return web3.eth.abi.encodeParameters(types, args)
const args = [
receipt.transactionHash,
receipt.blockNumber,
receipt.blockHash,
receipt.transactionIndex,
receipt.from,
receipt.to || ZERO_ADDRESS,
receipt.gasUsed,
receipt.status,
receipt.logs.map(normalizeLog)
]
const type = '(bytes32,uint256,bytes32,uint256,address,address,uint256,bool,(address,bytes32[],bytes)[])'
return web3.eth.abi.encodeParameter(type, args)
}
module.exports = {

View File

@@ -6,7 +6,16 @@ const logger = require('../../services/logger').child({
const { strip0x } = require('../../../../commons')
const { AMB_AFFIRMATION_REQUEST_EXTRA_GAS_ESTIMATOR: estimateExtraGas } = require('../../utils/constants')
async function estimateGas({ web3, homeBridge, validatorContract, messageId, status, result, address }) {
async function estimateGas({
web3,
homeBridge,
validatorContract,
messageId,
status,
result,
address,
homeBlockNumber
}) {
try {
const gasEstimate = await homeBridge.methods.confirmInformation(messageId, status, result).estimateGas({
from: address
@@ -15,7 +24,8 @@ async function estimateGas({ web3, homeBridge, validatorContract, messageId, sta
// 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) {
@@ -51,6 +61,20 @@ async function estimateGas({ web3, homeBridge, validatorContract, messageId, sta
throw new InvalidValidatorError(`${address} is not a validator`)
}
logger.debug('Check if InformationRetrieved event for this message already exists')
const logs = await homeBridge.getPastEvents('InformationRetrieved', {
fromBlock: homeBlockNumber,
toBlock: 'latest',
filter: { messageId }
})
if (logs.length > 0) {
logger.warn(
'This particular message was already signed and processed by other validators.' +
'However, evaluated async call result is different from the one recorded on-chain.'
)
throw new AlreadyProcessedError(e.message)
}
throw new Error('Unknown error while processing message')
}
}

View File

@@ -4,7 +4,13 @@ const { soliditySha3 } = require('web3').utils
const { HttpListProviderError } = require('../../services/HttpListProvider')
const rootLogger = require('../../services/logger')
const makeBlockFinder = require('../../services/blockFinder')
const { EXIT_CODES, MAX_CONCURRENT_EVENTS, EXTRA_GAS_ABSOLUTE } = require('../../utils/constants')
const {
EXIT_CODES,
MAX_CONCURRENT_EVENTS,
EXTRA_GAS_ABSOLUTE,
ASYNC_CALL_ERRORS,
MAX_ASYNC_CALL_RESULT_LENGTH
} = require('../../utils/constants')
const estimateGas = require('./estimateGas')
const { getValidatorContract, getBlock, getBlockNumber, getRequiredBlockConfirmations } = require('../../tx/web3')
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
@@ -79,12 +85,17 @@ function processInformationRequestsBuilder(config) {
logger.info({ requestSelector, method: asyncCallMethod, data }, 'Processing async request')
const call = asyncCalls[asyncCallMethod]
const [status, result] = await call(web3ForeignArchive, data, foreignClosestBlock).catch(e => {
let [status, result] = await call(web3ForeignArchive, data, foreignClosestBlock).catch(e => {
if (e instanceof HttpListProviderError) {
throw e
}
return [false, '0x']
logger.error({ error: e.message }, 'Unknown error during async call execution')
throw e
})
if (result.length > 2 + MAX_ASYNC_CALL_RESULT_LENGTH * 2) {
status = false
result = ASYNC_CALL_ERRORS.RESULT_IS_TOO_LONG
}
logger.info({ requestSelector, method: asyncCallMethod, status, result }, 'Request result obtained')
let gasEstimate
@@ -97,7 +108,8 @@ function processInformationRequestsBuilder(config) {
messageId,
status,
result,
address: config.validatorAddress
address: config.validatorAddress,
homeBlockNumber: homeBlock.number
})
logger.debug({ gasEstimate }, 'Gas estimated')
} catch (e) {

View File

@@ -8,8 +8,6 @@ const estimateGas = require('../processSignatureRequests/estimateGas')
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
const { EXIT_CODES, MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
const { ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY } = process.env
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
function processSignatureRequestsBuilder(config) {
@@ -37,7 +35,7 @@ function processSignatureRequestsBuilder(config) {
const { sender, executor } = parseAMBMessage(message)
logger.info({ sender, executor }, `Processing signatureRequest ${messageId}`)
const signature = web3.eth.accounts.sign(message, `0x${ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY}`)
const signature = web3.eth.accounts.sign(message, config.validatorPrivateKey)
let gasEstimate
try {

View File

@@ -8,8 +8,6 @@ const estimateGas = require('./estimateGas')
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
const { EXIT_CODES, MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
const { ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY } = process.env
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
function processSignatureRequestsBuilder(config) {
@@ -48,7 +46,7 @@ function processSignatureRequestsBuilder(config) {
expectedMessageLength
})
const signature = web3.eth.accounts.sign(message, `0x${ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY}`)
const signature = web3.eth.accounts.sign(message, config.validatorPrivateKey)
let gasEstimate
try {

View File

@@ -13,12 +13,25 @@ const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
function processTransfersBuilder(config) {
const { bridgeContract, web3 } = config.home
const userRequestForAffirmationAbi = config.foreign.bridgeABI.find(
e => e.type === 'event' && e.name === 'UserRequestForAffirmation'
)
const tokensSwappedAbi = config.foreign.bridgeABI.find(e => e.type === 'event' && e.name === 'TokensSwapped')
const userRequestForAffirmationHash = web3.eth.abi.encodeEventSignature(userRequestForAffirmationAbi)
const tokensSwappedHash = tokensSwappedAbi ? web3.eth.abi.encodeEventSignature(tokensSwappedAbi) : '0x'
const userRequestForAffirmationHash = web3.eth.abi.encodeEventSignature('UserRequestForAffirmation(address,uint256)')
const redeemHash = web3.eth.abi.encodeEventSignature('Redeem(address,uint256,uint256)')
const transferHash = web3.eth.abi.encodeEventSignature('Transfer(address,address,uint256)')
const foreignBridgeAddress = config.foreign.bridgeAddress
const decodeAddress = data => web3.eth.abi.decodeParameter('address', data)
const isUserRequestForAffirmation = e =>
e.address.toLowerCase() === foreignBridgeAddress.toLowerCase() && e.topics[0] === userRequestForAffirmationHash
const isRedeem = cTokenAddress => e =>
e.address.toLowerCase() === cTokenAddress.toLowerCase() &&
e.topics[0] === redeemHash &&
decodeAddress(e.data.slice(0, 66)).toLowerCase() === foreignBridgeAddress.toLowerCase()
const isCTokenTransfer = cTokenAddress => e =>
e.address.toLowerCase() === cTokenAddress.toLowerCase() &&
e.topics[0] === transferHash &&
decodeAddress(e.topics[1]).toLowerCase() === foreignBridgeAddress.toLowerCase() &&
decodeAddress(e.topics[2]).toLowerCase() === cTokenAddress.toLowerCase()
let validatorContract = null
@@ -32,37 +45,35 @@ function processTransfersBuilder(config) {
rootLogger.debug(`Processing ${transfers.length} Transfer events`)
const callbacks = transfers
.map(transfer => async () => {
const { from, value } = transfer.returnValues
const { from, to, value } = transfer.returnValues
const logger = rootLogger.child({
eventTransactionHash: transfer.transactionHash
eventTransactionHash: transfer.transactionHash,
from,
to,
value
})
logger.info({ from, value }, `Processing transfer ${transfer.transactionHash}`)
logger.info('Processing transfer')
const receipt = await config.foreign.web3.eth.getTransactionReceipt(transfer.transactionHash)
const existsAffirmationEvent = receipt.logs.some(
e => e.address === config.foreign.bridgeAddress && e.topics[0] === userRequestForAffirmationHash
)
if (existsAffirmationEvent) {
logger.info(
`Transfer event discarded because a transaction with alternative receiver detected in transaction ${
transfer.transactionHash
}`
)
if (receipt.logs.some(isUserRequestForAffirmation)) {
logger.info('Transfer event discarded because affirmation is detected in the same transaction')
return
}
const existsTokensSwappedEvent = tokensSwappedAbi
? receipt.logs.some(e => e.address === config.foreign.bridgeAddress && e.topics[0] === tokensSwappedHash)
: false
if (from === ZERO_ADDRESS) {
logger.info('Mint-like transfers from zero address are not processed')
return
}
if (from === ZERO_ADDRESS && existsTokensSwappedEvent) {
logger.info(
`Transfer event discarded because token swap is detected in transaction ${transfer.transactionHash}`
)
// when bridge performs a withdrawal from Compound, the following three events occur
// * token.Transfer(from=cToken, to=bridge, amount=X)
// * cToken.Redeem(redeemer=bridge, amount=X, tokens=Y)
// * cToken.Transfer(from=bridge, to=cToken, amount=Y)
if (receipt.logs.some(isRedeem(from)) && receipt.logs.some(isCTokenTransfer(from))) {
logger.info('Transfer event discarded because cToken redeem is detected in the same transaction')
return
}

View File

@@ -10,18 +10,18 @@ const { getNonce, getChainId } = require('./tx/web3')
const {
addExtraGas,
checkHTTPS,
privateKeyToAddress,
syncForEach,
waitForFunds,
waitForUnsuspend,
watchdog,
nonceError
isGasPriceError,
isSameTransactionError,
isInsufficientBalanceError,
isNonceError
} = require('./utils/utils')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
const { ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY, ORACLE_TX_REDUNDANCY } = process.env
const ORACLE_VALIDATOR_ADDRESS = privateKeyToAddress(ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY)
const { ORACLE_TX_REDUNDANCY } = process.env
if (process.argv.length < 3) {
logger.error('Please check the number of arguments, config file was not provided')
@@ -40,7 +40,7 @@ async function initialize() {
try {
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
web3.currentProvider.urls.forEach(checkHttps(config.chain))
web3.currentProvider.urls.forEach(checkHttps(config.id))
GasPrice.start(config.id)
@@ -84,7 +84,7 @@ async function readNonce(forceUpdate) {
logger.debug('Reading nonce')
if (forceUpdate) {
logger.debug('Forcing update of nonce')
return getNonce(web3, ORACLE_VALIDATOR_ADDRESS)
return getNonce(web3, config.validatorAddress)
}
const nonce = await redis.get(nonceKey)
@@ -93,7 +93,7 @@ async function readNonce(forceUpdate) {
return Number(nonce)
} else {
logger.warn("Nonce wasn't found in the DB")
return getNonce(web3, ORACLE_VALIDATOR_ADDRESS)
return getNonce(web3, config.validatorAddress)
}
}
@@ -168,7 +168,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
gasPrice,
amount: '0',
gasLimit,
privateKey: ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY,
privateKey: config.validatorPrivateKey,
to: job.to,
chainId,
web3: web3Redundant
@@ -192,12 +192,11 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
e.message
)
const message = e.message.toLowerCase()
if (message.includes('replacement transaction underpriced')) {
if (isGasPriceError(e)) {
logger.info('Replacement transaction underpriced, forcing gas price update')
GasPrice.start(config.id)
failedTx.push(job)
} else if (isResend || message.includes('transaction with the same hash was already imported')) {
} else if (isResend || isSameTransactionError(e)) {
resendJobs.push(job)
} else {
// if initial transaction sending has failed not due to the same hash error
@@ -206,14 +205,14 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
failedTx.push(job)
}
if (message.includes('insufficient funds')) {
if (isInsufficientBalanceError(e)) {
insufficientFunds = true
const currentBalance = await web3.eth.getBalance(ORACLE_VALIDATOR_ADDRESS)
const currentBalance = await web3.eth.getBalance(config.validatorAddress)
minimumBalance = gasLimit.multipliedBy(gasPrice)
logger.error(
`Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.`
)
} else if (nonceError(e)) {
} else if (isNonceError(e)) {
nonce = await readNonce(true)
}
}
@@ -238,7 +237,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
if (insufficientFunds) {
logger.warn('Insufficient funds. Stop sending transactions until the account has the minimum balance')
channel.close()
waitForFunds(web3, ORACLE_VALIDATOR_ADDRESS, minimumBalance, resume, logger)
waitForFunds(web3, config.validatorAddress, minimumBalance, resume, logger)
}
} catch (e) {
logger.error(e)

View File

@@ -2,8 +2,14 @@ const fetch = require('node-fetch')
const promiseRetry = require('promise-retry')
const { FALLBACK_RPC_URL_SWITCH_TIMEOUT } = require('../utils/constants')
const { onInjected } = require('./injectedLogger')
const { ORACLE_JSONRPC_ERROR_CODES } = process.env
// From EIP-1474 and Infura documentation
const JSONRPC_ERROR_CODES = [-32603, -32002, -32005]
const JSONRPC_ERROR_CODES = ORACLE_JSONRPC_ERROR_CODES
? ORACLE_JSONRPC_ERROR_CODES.split(',').map(s => parseInt(s, 10))
: [-32603, -32002, -32005]
const defaultOptions = {
name: 'main',
@@ -33,14 +39,24 @@ function HttpListProvider(urls, options = {}) {
this.options = { ...defaultOptions, ...options }
this.currentIndex = 0
this.lastTimeUsedPrimary = 0
this.logger = {
debug: () => {},
info: () => {}
}
onInjected(logger => {
this.logger = logger.child({ module: `HttpListProvider:${this.options.name}` })
})
}
HttpListProvider.prototype.setLogger = function(logger) {
this.logger = logger.child({ module: `HttpListProvider:${this.options.name}` })
HttpListProvider.prototype.switchToFallbackRPC = function() {
if (this.urls.length < 2) {
return
}
const prevIndex = this.currentIndex
const newIndex = (prevIndex + 1) % this.urls.length
this.logger.info(
{ index: newIndex, oldURL: this.urls[prevIndex], newURL: this.urls[newIndex] },
'Switching to fallback JSON-RPC URL'
)
this.currentIndex = newIndex
}
HttpListProvider.prototype.send = async function send(payload, callback) {

View File

@@ -1,6 +1,7 @@
const promiseRetry = require('promise-retry')
const { promiseAny } = require('../utils/utils')
const { defaultOptions, HttpListProviderError, send } = require('./HttpListProvider')
const { onInjected } = require('./injectedLogger')
function RedundantHttpListProvider(urls, options = {}) {
if (!(this instanceof RedundantHttpListProvider)) {
@@ -13,10 +14,9 @@ function RedundantHttpListProvider(urls, options = {}) {
this.urls = urls
this.options = { ...defaultOptions, ...options }
}
RedundantHttpListProvider.prototype.setLogger = function(logger) {
this.logger = logger.child({ module: `RedundantHttpListProvider:${this.options.name}` })
onInjected(logger => {
this.logger = logger.child({ module: `RedundantHttpListProvider:${this.options.name}` })
})
}
RedundantHttpListProvider.prototype.send = async function send(payload, callback) {

View File

@@ -0,0 +1,37 @@
const { hexToNumber, isHexStrict } = require('web3').utils
function SafeEthLogsProvider(provider) {
const oldSend = provider.send.bind(provider)
const newSend = function(payload, callback) {
if (payload.method === 'eth_getLogs' && isHexStrict(payload.params[0].toBlock)) {
this.logger.debug('Modifying eth_getLogs request to include batch eth_blockNumber request')
const newPayload = [payload, { jsonrpc: '2.0', id: payload.id + 1, method: 'eth_blockNumber', params: [] }]
oldSend(newPayload, (err, res) => {
if (err) {
callback(err, null)
} else {
const rawLogs = res.find(({ id }) => id === payload.id)
const rawBlockNumber = res.find(({ id }) => id === payload.id + 1)
const blockNumber = hexToNumber(rawBlockNumber.result)
const toBlock = hexToNumber(payload.params[0].toBlock)
if (blockNumber < toBlock) {
this.logger.warn({ toBlock, blockNumber }, 'Returned block number is less than the specified toBlock')
callback(new Error('block number too low'), null)
} else {
callback(null, rawLogs)
}
}
})
} else {
oldSend(payload, callback)
}
}
provider.send = newSend.bind(provider)
return provider
}
module.exports = {
SafeEthLogsProvider
}

View File

@@ -1,5 +1,4 @@
require('../../env')
const fetch = require('node-fetch')
const { home, foreign } = require('../../config/base.config')
const logger = require('../services/logger').child({
module: 'gasPrice'
@@ -25,11 +24,11 @@ let cachedGasPrice = null
let fetchGasPriceInterval = null
const fetchGasPrice = async (speedType, factor, bridgeContract, gasPriceSupplierFetchFn) => {
const fetchGasPrice = async (speedType, factor, bridgeContract, gasPriceSupplierUrl) => {
const contractOptions = { logger }
const supplierOptions = { speedType, factor, limits: GAS_PRICE_BOUNDARIES, logger }
cachedGasPrice =
(await gasPriceFromSupplier(gasPriceSupplierFetchFn, supplierOptions)) ||
(await gasPriceFromSupplier(gasPriceSupplierUrl, supplierOptions)) ||
(await gasPriceFromContract(bridgeContract, contractOptions)) ||
cachedGasPrice
return cachedGasPrice
@@ -63,16 +62,15 @@ async function start(chainId, fetchOnce) {
throw new Error(`Unrecognized chainId '${chainId}'`)
}
let fetchFn = null
if (gasPriceSupplierUrl !== 'gas-price-oracle') {
fetchFn = () => fetch(gasPriceSupplierUrl, { timeout: 2000 })
if (!gasPriceSupplierUrl) {
logger.warn({ chainId }, 'Gas price API is not configured, will fallback to the contract-supplied gas price')
}
if (fetchOnce) {
await fetchGasPrice(speedType, factor, contract, fetchFn)
await fetchGasPrice(speedType, factor, contract, gasPriceSupplierUrl)
} else {
fetchGasPriceInterval = await setIntervalAndRun(
() => fetchGasPrice(speedType, factor, contract, fetchFn),
() => fetchGasPrice(speedType, factor, contract, gasPriceSupplierUrl),
updateInterval
)
}

View File

@@ -0,0 +1,27 @@
// workaround to avoid circular dependencies in module imports
// e.g. logger -> config -> web3 -> provider -> logger
// transforms to the following import chain
// logger -> config -> web3 -> provider -> injectedLogger
// logger -> injectedLogger
let logger
const callbacks = []
function onInjected(cb) {
if (logger) {
cb(logger)
} else {
callbacks.push(cb)
}
}
function setLogger(newLogger) {
logger = newLogger
callbacks.forEach(cb => cb(logger))
}
module.exports = {
onInjected,
setLogger
}

View File

@@ -1,15 +1,7 @@
const pino = require('pino')
const path = require('path')
const {
web3Home,
web3Foreign,
web3ForeignArchive,
web3Side,
web3HomeFallback,
web3ForeignFallback,
web3HomeRedundant,
web3ForeignRedundant
} = require('./web3')
const { setLogger } = require('./injectedLogger')
const config = process.env.NODE_ENV !== 'test' ? require(path.join('../../config/', process.argv[2])) : {}
@@ -17,27 +9,11 @@ const logger = pino({
enabled: process.env.NODE_ENV !== 'test',
name: config.name,
level: process.env.ORACLE_LOG_LEVEL || 'debug',
base:
process.env.NODE_ENV === 'production'
? {
validator: process.env.ORACLE_VALIDATOR_ADDRESS
}
: {}
base: {
validator: config.validatorAddress
}
})
web3Home.currentProvider.setLogger(logger)
web3Foreign.currentProvider.setLogger(logger)
web3HomeFallback.currentProvider.setLogger(logger)
web3ForeignFallback.currentProvider.setLogger(logger)
web3HomeRedundant.currentProvider.setLogger(logger)
web3ForeignRedundant.currentProvider.setLogger(logger)
if (web3ForeignArchive) {
web3ForeignArchive.currentProvider.setLogger(logger)
}
if (web3Side) {
web3Side.currentProvider.setLogger(logger)
}
setLogger(logger)
module.exports = logger

View File

@@ -1,5 +1,6 @@
const Web3 = require('web3')
const { HttpListProvider } = require('./HttpListProvider')
const { SafeEthLogsProvider } = require('./SafeEthLogsProvider')
const { RedundantHttpListProvider } = require('./RedundantHttpListProvider')
const { RETRY_CONFIG } = require('../utils/constants')
@@ -37,10 +38,10 @@ const foreignOptions = {
retry: RETRY_CONFIG
}
const homeProvider = new HttpListProvider(homeUrls, homeOptions)
const homeProvider = SafeEthLogsProvider(new HttpListProvider(homeUrls, homeOptions))
const web3Home = new Web3(homeProvider)
const foreignProvider = new HttpListProvider(foreignUrls, foreignOptions)
const foreignProvider = SafeEthLogsProvider(new HttpListProvider(foreignUrls, foreignOptions))
const web3Foreign = new Web3(foreignProvider)
let web3ForeignArchive = null

View File

@@ -11,7 +11,7 @@ async function sendTx({ privateKey, data, nonce, gasPrice, amount, gasLimit, to,
gasPrice,
gas: gasLimit
},
`0x${privateKey}`
privateKey
)
return new Promise((res, rej) =>

View File

@@ -102,6 +102,7 @@ async function getEventsFromTx({ web3, contract, event, txHash, filter }) {
const eventAbi = contract.options.jsonInterface.find(abi => abi.name === event)
const decodeAbi = contract._decodeEventABI.bind(eventAbi)
const pastEvents = logs
.filter(event => event.address.toLowerCase() === contractAddress.toLowerCase())
.filter(event => event.topics[0] === eventAbi.signature)
.map(decodeAbi)
.filter(event =>

View File

@@ -1,6 +1,6 @@
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,
@@ -25,7 +25,20 @@ module.exports = {
},
DEFAULT_TRANSACTION_RESEND_INTERVAL: 20 * 60 * 1000,
FALLBACK_RPC_URL_SWITCH_TIMEOUT: 60 * 60 * 1000,
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT: 10,
SENDER_QUEUE_MAX_PRIORITY: 10,
SENDER_QUEUE_SEND_PRIORITY: 5,
SENDER_QUEUE_CHECK_STATUS_PRIORITY: 1
SENDER_QUEUE_CHECK_STATUS_PRIORITY: 1,
ASYNC_CALL_ERRORS: {
// requested transaction/block/receipt does not exist
NOT_FOUND: '0x0000000000000000000000000000000000000000000000000000000000000000',
// requested custom block does not exist yet or its timestamp is greater than the home block timestamp
BLOCK_IS_IN_THE_FUTURE: '0x0000000000000000000000000000000000000000000000000000000000000001',
// eth_call has reverted or finished with OOG error
REVERT: '0x0000000000000000000000000000000000000000000000000000000000000002',
// evaluated output length exceeds allowed length of 64 KB
RESULT_IS_TOO_LONG: '0x0000000000000000000000000000000000000000000000000000000000000003'
},
MAX_ASYNC_CALL_RESULT_LENGTH: 64 * 1024,
ASYNC_ETH_CALL_MAX_GAS_LIMIT: 100000000
}

View File

@@ -99,12 +99,44 @@ function privateKeyToAddress(privateKey) {
return privateKey ? new Web3().eth.accounts.privateKeyToAccount(add0xPrefix(privateKey)).address : null
}
function nonceError(e) {
function isGasPriceError(e) {
const message = e.message.toLowerCase()
return message.includes('replacement transaction underpriced')
}
function isSameTransactionError(e) {
const message = e.message.toLowerCase()
return (
message.includes('transaction with the same hash was already imported') ||
message.includes('already known') ||
message.includes('alreadyknown')
)
}
function isInsufficientBalanceError(e) {
const message = e.message.toLowerCase()
return message.includes('insufficient funds')
}
function isNonceError(e) {
const message = e.message.toLowerCase()
return (
message.includes('transaction nonce is too low') ||
message.includes('nonce too low') ||
message.includes('transaction with same nonce in the queue')
message.includes('transaction with same nonce in the queue') ||
message.includes('oldnonce')
)
}
function isRevertError(e) {
const message = e.message.toLowerCase()
// OE and NE returns "VM execution error"/"Transaction execution error"
// Geth returns "out of gas"/"intrinsic gas too low"/"execution reverted"
return (
message.includes('execution error') ||
message.includes('intrinsic gas too low') ||
message.includes('out of gas') ||
message.includes('execution reverted')
)
}
@@ -153,8 +185,13 @@ module.exports = {
addExtraGas,
setIntervalAndRun,
watchdog,
add0xPrefix,
privateKeyToAddress,
nonceError,
isGasPriceError,
isSameTransactionError,
isInsufficientBalanceError,
isNonceError,
isRevertError,
getRetrySequence,
promiseAny,
readAccessListFile,

View File

@@ -6,7 +6,7 @@ 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 } = require('./utils/constants')
const { EXIT_CODES, BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT } = require('./utils/constants')
if (process.argv.length < 3) {
logger.error('Please check the number of arguments, config file was not provided')
@@ -29,6 +29,8 @@ const { getTokensState } = require('./utils/tokenState')
const { web3, bridgeContract, eventContract, startBlock, pollingInterval, chain } = config.main
const lastBlockRedisKey = `${config.id}:lastProcessedBlock`
let lastProcessedBlock = Math.max(startBlock - 1, 0)
let lastSeenBlockNumber = 0
let sameBlockNumberCounter = 0
async function initialize() {
try {
@@ -117,6 +119,28 @@ async function getLastBlockToProcess(web3, bridgeContract) {
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
}
@@ -132,8 +156,6 @@ async function main({ sendToQueue }) {
logger.info(`Oracle watcher was unsuspended.`)
}
await checkConditions()
const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract)
if (lastBlockToProcess <= lastProcessedBlock) {
@@ -141,6 +163,8 @@ async function main({ sendToQueue }) {
return
}
await checkConditions()
const fromBlock = lastProcessedBlock + 1
const rangeEndBlock = config.blockPollingLimit ? fromBlock + config.blockPollingLimit : lastBlockToProcess
let toBlock = Math.min(lastBlockToProcess, rangeEndBlock)

View File

@@ -3,77 +3,75 @@ const { expect } = require('chai')
const proxyquire = require('proxyquire').noPreserveCache()
const { DEFAULT_UPDATE_INTERVAL } = require('../src/utils/constants')
describe('gasPrice', () => {
describe('start', () => {
const utils = { setIntervalAndRun: sinon.spy() }
beforeEach(() => {
utils.setIntervalAndRun.resetHistory()
})
it('should call setIntervalAndRun with ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL interval value on Home', async () => {
// given
process.env.ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL = 15000
const gasPrice = proxyquire('../src/services/gasPrice', { '../utils/utils': utils })
const utils = { setIntervalAndRun: sinon.spy() }
const fetchStub = () => ({
json: () => ({
standard: '103'
})
})
const fakeLogger = { error: sinon.spy(), warn: sinon.spy(), child: () => fakeLogger }
fetchStub['@global'] = true
const gasPriceDefault = proxyquire('../src/services/gasPrice', {
'../utils/utils': utils,
'node-fetch': fetchStub,
'../services/logger': { child: () => fakeLogger }
})
process.env.ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL = 15000
process.env.ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL = 30000
process.env.COMMON_HOME_GAS_PRICE_FALLBACK = '101000000000'
const gasPrice = proxyquire('../src/services/gasPrice', {
'../utils/utils': utils,
'node-fetch': fetchStub,
'../services/logger': { child: () => fakeLogger }
})
describe('gasPrice', () => {
beforeEach(() => {
utils.setIntervalAndRun.resetHistory()
fakeLogger.error.resetHistory()
fakeLogger.warn.resetHistory()
})
describe('start', () => {
it('should call setIntervalAndRun with ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL interval value on Home', async () => {
// when
await gasPrice.start('home')
// then
expect(process.env.ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL).to.equal('15000')
expect(process.env.ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL).to.not.equal(DEFAULT_UPDATE_INTERVAL.toString())
expect(utils.setIntervalAndRun.args[0][1]).to.equal(process.env.ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL.toString())
})
it('should call setIntervalAndRun with ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL interval value on Foreign', async () => {
// given
process.env.ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL = 15000
const gasPrice = proxyquire('../src/services/gasPrice', { '../utils/utils': utils })
// when
await gasPrice.start('foreign')
// then
expect(process.env.ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL).to.equal('15000')
expect(process.env.ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL).to.not.equal(DEFAULT_UPDATE_INTERVAL.toString())
expect(utils.setIntervalAndRun.args[0][1]).to.equal(
process.env.ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL.toString()
)
})
it('should call setIntervalAndRun with default interval value on Home', async () => {
// given
delete process.env.ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL
const gasPrice = proxyquire('../src/services/gasPrice', { '../utils/utils': utils })
// when
await gasPrice.start('home')
await gasPriceDefault.start('home')
// then
expect(process.env.ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL).to.equal(undefined)
expect(utils.setIntervalAndRun.args[0][1]).to.equal(DEFAULT_UPDATE_INTERVAL)
})
it('should call setIntervalAndRun with default interval value on Foreign', async () => {
// given
delete process.env.ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL
const gasPrice = proxyquire('../src/services/gasPrice', { '../utils/utils': utils })
// when
await gasPrice.start('foreign')
await gasPriceDefault.start('foreign')
// then
expect(process.env.ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL).to.equal(undefined)
expect(utils.setIntervalAndRun.args[0][1]).to.equal(DEFAULT_UPDATE_INTERVAL)
})
})
describe('fetching gas price', () => {
const utils = { setIntervalAndRun: () => {} }
it('should fall back to default if contract and supplier are not working', async () => {
// given
process.env.COMMON_HOME_GAS_PRICE_FALLBACK = '101000000000'
const gasPrice = proxyquire('../src/services/gasPrice', { '../utils/utils': utils })
await gasPrice.start('home')
// when
await gasPrice.fetchGasPrice('standard', 1, null, () => null)
await gasPrice.fetchGasPrice('standard', 1, null, null)
// then
expect(gasPrice.getPrice()).to.equal('101000000000')
@@ -81,18 +79,10 @@ describe('gasPrice', () => {
it('should fetch gas from supplier', async () => {
// given
process.env.COMMON_HOME_GAS_PRICE_FALLBACK = '101000000000'
const gasPrice = proxyquire('../src/services/gasPrice', { '../utils/utils': utils })
await gasPrice.start('home')
const gasPriceSupplierFetchFn = () => ({
json: () => ({
standard: '103'
})
})
// when
await gasPrice.fetchGasPrice('standard', 1, null, gasPriceSupplierFetchFn)
await gasPrice.fetchGasPrice('standard', 1, null, 'url')
// then
expect(gasPrice.getPrice().toString()).to.equal('103000000000')
@@ -100,8 +90,6 @@ describe('gasPrice', () => {
it('should fetch gas from contract', async () => {
// given
process.env.COMMON_HOME_GAS_PRICE_FALLBACK = '101000000000'
const gasPrice = proxyquire('../src/services/gasPrice', { '../utils/utils': utils })
await gasPrice.start('home')
const bridgeContractMock = {
@@ -113,7 +101,7 @@ describe('gasPrice', () => {
}
// when
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, () => {})
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, null)
// then
expect(gasPrice.getPrice().toString()).to.equal('102000000000')
@@ -121,8 +109,6 @@ describe('gasPrice', () => {
it('should fetch the gas price from the oracle first', async () => {
// given
process.env.COMMON_HOME_GAS_PRICE_FALLBACK = '101000000000'
const gasPrice = proxyquire('../src/services/gasPrice', { '../utils/utils': utils })
await gasPrice.start('home')
const bridgeContractMock = {
@@ -133,33 +119,23 @@ describe('gasPrice', () => {
}
}
const gasPriceSupplierFetchFn = () => ({
json: () => ({
standard: '103'
})
})
// when
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, gasPriceSupplierFetchFn)
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, 'url')
// then
expect(gasPrice.getPrice().toString()).to.equal('103000000000')
})
it('log errors using the logger', async () => {
it('log error using the logger', async () => {
// given
const fakeLogger = { error: sinon.spy() }
const gasPrice = proxyquire('../src/services/gasPrice', {
'../utils/utils': utils,
'../services/logger': { child: () => fakeLogger }
})
await gasPrice.start('home')
// when
await gasPrice.fetchGasPrice('standard', 1, null, () => {})
await gasPrice.fetchGasPrice('standard', 1, null, null)
// then
expect(fakeLogger.error.calledTwice).to.equal(true) // two errors
expect(fakeLogger.warn.calledOnce).to.equal(true) // one warning
expect(fakeLogger.error.calledOnce).to.equal(true) // one error
})
})
})

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
pragma solidity 0.4.24;
interface IERC20 {
function transferFrom(address from,address to,uint256 value) external;
function transfer(address to,uint256 value) external;
}
contract cDaiMock {
IERC20 daiToken;
event Transfer(address indexed from, address indexed to, uint amount);
event Mint(address minter, uint mintAmount, uint mintTokens);
event Redeem(address redeemer, uint redeemAmount, uint redeemTokens);
function mint(uint256 mintAmount) external returns (uint256) {
daiToken.transferFrom(msg.sender, address(this), mintAmount);
emit Mint(msg.sender, mintAmount, mintAmount);
emit Transfer(address(this), msg.sender, mintAmount);
return 0;
}
function redeemUnderlying(uint256 redeemAmount) external returns (uint256) {
daiToken.transfer(msg.sender, redeemAmount);
emit Transfer(msg.sender, address(this), redeemAmount);
emit Redeem(msg.sender, redeemAmount, redeemAmount);
return 0;
}
}

4419
yarn.lock

File diff suppressed because it is too large Load Diff