Compare commits

...

15 Commits

Author SHA1 Message Date
Alexander Kolotov
f2a6a64637 Merge the develop branch to the master branch, preparation to v1.1.0 (#238) 2019-11-15 22:13:03 +01:00
Gerardo Nardelli
8d4eb86a19 Fix logs and start block parameters in oracle deployment (#235)
* Update start block env vars in oracle deployment
* Set logger to remote server for docker-compose-transfer.yml
* avoid calculating start block parameter if provided in oracle deployment config
* set home and foreign start block in erc20-native e2e
* set home and foreign start block in erc20-native ultimate test
2019-11-15 21:31:13 +01:00
phahulin
cc6afb3736 Fix monitor path in crontab example (#236)
This is a minor PR which fixes path in crontab example for monorepo
2019-11-15 06:16:21 +01:00
phahulin
84ecfc30d9 Optional ability to point ORACLE_LOG_LEVEL in the deployment configuration (#234) 2019-11-13 20:18:46 +01:00
Gerardo Nardelli
5d770e8607 Add AMB monitor e2e tests (#231)
* Add amb monitor e2e tests
* Fix eventsStats endpoint monitor
2019-11-13 07:51:10 +01:00
Alexander Kolotov
db89d1c12e Merge the develop branch to the master branch, preparation to v1.0.0 #230
* Support alternative receiver in Oracle (#221)
* Support alternative receiver feature in Monitor (#223)
* Support token migration (#224)
* Fix suggested gas price in transaction for ui production build (#222)
* Updated links to new repo with tokenbridge contracts (#226)
* Update the contract's submodule to the release 3.2.0 (#228)
2019-11-12 06:37:23 +01:00
Gerardo Nardelli
4fd4ac3d73 Merge branch 'master' into develop
# Conflicts:
#	commons/constants.js
#	e2e-commons/up.sh
#	monitor/alerts.js
#	monitor/eventsStats.js
#	monitor/utils/events.js
#	monitor/utils/message.js
2019-11-08 09:53:06 -03:00
Alexander Kolotov
cbd9d607ce Update the contract's submodule to the release 3.2.0 (#228) 2019-11-07 22:50:37 +01:00
Gerardo Nardelli
346fa1e732 Support token migration (#224)
* Filter transfer event in token swap in Oracle
* Support token swap in monitor
2019-11-06 22:49:01 +03:00
Alexander Kolotov
f6fa83d7ea Updated links to new repo with tokenbridge contracts (#226) 2019-11-05 15:53:47 +03:00
Gerardo Nardelli
1564ccc580 Support alternative receiver feature in Monitor (#223)
* Update monitor to support changes from alternative receiver
* Add monitor event processing unit tests
* update chrome version used en e2e tests
* update chromedriver version
2019-10-29 22:22:02 +03:00
Gerardo Nardelli
7a54e584d5 Support alternative receiver in Oracle (#221)
* Add transfer watcher
* Filter userRequestForAffirmation events in Transfer events
* Add extended oracle composer file
2019-10-29 17:55:47 +03:00
Gerardo Nardelli
1d79cf82f3 Fix suggested gas price in trasaction for ui production build (#222)
* use string version of bignumber when converting gasPrice to hex
* update chromedriver version
2019-10-25 15:38:30 +03:00
Alexander Kolotov
9884b4b424 Add support for AMB contracts (#199) 2019-10-21 15:57:28 +03:00
Gerardo Nardelli
d577a71096 Add support for AMB contracts (#199) 2019-09-18 22:45:13 +03:00
67 changed files with 2375 additions and 1752 deletions

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "contracts"]
path = contracts
url = https://github.com/poanetwork/poa-bridge-contracts.git
url = https://github.com/poanetwork/tokenbridge-contracts.git

View File

@@ -30,7 +30,7 @@ Sub-repositories maintained within this monorepo are listed below.
| [Commons](commons/README.md) | Interfaces, constants and utilities shared between the sub-repositories |
| [E2E-Commons](e2e-commons/README.md) | Common utilities and configuration used in end to end tests |
Additionally there are [Smart Contracts](https://github.com/poanetwork/poa-bridge-contracts) used to manage bridge validators, collect signatures, and confirm asset relay and disposal.
Additionally there are [Smart Contracts](https://github.com/poanetwork/tokenbridge-contracts) used to manage bridge validators, collect signatures, and confirm asset relay and disposal.
## Available deployments
@@ -52,11 +52,12 @@ Additionally there are [Smart Contracts](https://github.com/poanetwork/poa-bridg
## Operational Modes
The POA TokenBridge provides three operational modes:
The POA TokenBridge provides four operational modes:
- [x] `Native-to-ERC20` **Coins** on a Home network can be converted to ERC20-compatible **tokens** on a Foreign network. Coins are locked on the Home side and the corresponding amount of ERC20 tokens are minted on the Foreign side. When the operation is reversed, tokens are burnt on the Foreign side and unlocked in the Home network. **More Information: [POA-to-POA20 Bridge](https://medium.com/poa-network/introducing-poa-bridge-and-poa20-55d8b78058ac)**
- [x] `ERC20-to-ERC20` ERC20-compatible tokens on the Foreign network are locked and minted as ERC20-compatible tokens (ERC677 tokens) on the Home network. When transferred from Home to Foreign, they are burnt on the Home side and unlocked in the Foreign network. This can be considered a form of atomic swap when a user swaps the token "X" in network "A" to the token "Y" in network "B". **More Information: [ERC20-to-ERC20](https://medium.com/poa-network/introducing-the-erc20-to-erc20-tokenbridge-ce266cc1a2d0)**
- [x] `ERC20-to-Native`: Pre-existing **tokens** in the Foreign network are locked and **coins** are minted in the `Home` network. In this mode, the Home network consensus engine invokes [Parity's Block Reward contract](https://wiki.parity.io/Block-Reward-Contract.html) to mint coins per the bridge contract request. **More Information: [xDai Chain](https://medium.com/poa-network/poa-network-partners-with-makerdao-on-xdai-chain-the-first-ever-usd-stable-blockchain-65a078c41e6a)**
- [x] `Arbitrary-Message`: Transfer arbitrary data between two networks as so the data could be interpreted as an arbitrary contract method invocation.
## Initializing the monorepository

View File

@@ -10,6 +10,9 @@ const ERC677_BRIDGE_TOKEN_ABI = require('../contracts/build/contracts/ERC677Brid
const BLOCK_REWARD_ABI = require('../contracts/build/contracts/IBlockReward').abi
const BRIDGE_VALIDATORS_ABI = require('../contracts/build/contracts/BridgeValidators').abi
const REWARDABLE_VALIDATORS_ABI = require('../contracts/build/contracts/RewardableValidators').abi
const HOME_AMB_ABI = require('../contracts/build/contracts/HomeAMB').abi
const FOREIGN_AMB_ABI = require('../contracts/build/contracts/ForeignAMB').abi
const BOX_ABI = require('../contracts/build/contracts/Box').abi
const { HOME_V1_ABI, FOREIGN_V1_ABI } = require('./v1Abis')
const { BRIDGE_MODES } = require('./constants')
@@ -60,6 +63,9 @@ function getBridgeABIs(bridgeMode) {
} else if (bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1) {
HOME_ABI = HOME_V1_ABI
FOREIGN_ABI = FOREIGN_V1_ABI
} else if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
HOME_ABI = HOME_AMB_ABI
FOREIGN_ABI = FOREIGN_AMB_ABI
} else {
throw new Error(`Unrecognized bridge mode: ${bridgeMode}`)
}
@@ -83,5 +89,8 @@ module.exports = {
REWARDABLE_VALIDATORS_ABI,
HOME_V1_ABI,
FOREIGN_V1_ABI,
ERC20_BYTES32_ABI
ERC20_BYTES32_ABI,
HOME_AMB_ABI,
FOREIGN_AMB_ABI,
BOX_ABI
}

View File

@@ -2,7 +2,8 @@ const BRIDGE_MODES = {
NATIVE_TO_ERC: 'NATIVE_TO_ERC',
ERC_TO_ERC: 'ERC_TO_ERC',
ERC_TO_NATIVE: 'ERC_TO_NATIVE',
NATIVE_TO_ERC_V1: 'NATIVE_TO_ERC_V1'
NATIVE_TO_ERC_V1: 'NATIVE_TO_ERC_V1',
ARBITRARY_MESSAGE: 'ARBITRARY_MESSAGE'
}
const ERC_TYPES = {
@@ -16,4 +17,11 @@ const FEE_MANAGER_MODE = {
UNDEFINED: 'UNDEFINED'
}
module.exports = { BRIDGE_MODES, ERC_TYPES, FEE_MANAGER_MODE }
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
module.exports = {
BRIDGE_MODES,
ERC_TYPES,
FEE_MANAGER_MODE,
ZERO_ADDRESS
}

View File

@@ -1,9 +1,11 @@
const constants = require('./constants')
const abis = require('./abis')
const utils = require('./utils')
const message = require('./message')
module.exports = {
...constants,
...abis,
...utils
...utils,
...message
}

27
commons/message.js Normal file
View File

@@ -0,0 +1,27 @@
function strip0x(input) {
return input.replace(/^0x/, '')
}
function addTxHashToData({ encodedData, transactionHash }) {
return encodedData.slice(0, 2) + strip0x(transactionHash) + encodedData.slice(2)
}
function parseAMBMessage(message) {
message = strip0x(message)
const txHash = `0x${message.slice(0, 64)}`
const sender = `0x${message.slice(64, 104)}`
const executor = `0x${message.slice(104, 144)}`
return {
sender,
executor,
txHash
}
}
module.exports = {
addTxHashToData,
parseAMBMessage,
strip0x
}

View File

@@ -8,6 +8,10 @@
"test": "NODE_ENV=test mocha"
},
"dependencies": {
"web3-utils": "1.0.0-beta.30"
"web3-utils": "1.0.0-beta.34"
},
"devDependencies": {
"bn-chai": "^1.0.1",
"chai": "^4.2.0"
}
}

View File

@@ -3,7 +3,7 @@ const { BRIDGE_MODES, ERC_TYPES } = require('../constants')
describe('constants', () => {
it('should contain correct number of bridge types', () => {
expect(Object.keys(BRIDGE_MODES).length).to.be.equal(4)
expect(Object.keys(BRIDGE_MODES).length).to.be.equal(5)
})
it('should contain correct number of erc types', () => {

View File

@@ -0,0 +1,69 @@
const { BN } = require('web3-utils')
const { expect } = require('chai').use(require('bn-chai')(BN))
const { parseAMBMessage, strip0x, addTxHashToData } = require('../message')
describe('strip0x', () => {
it('should remove 0x from input', () => {
// Given
const input = '0x12345'
// When
const result = strip0x(input)
// Then
expect(result).to.be.equal('12345')
})
it('should not modify input if 0x is not present', () => {
// Given
const input = '12345'
// When
const result = strip0x(input)
// Then
expect(result).to.be.equal(input)
})
})
describe('addTxHashToData', () => {
it('should add txHash to encoded data at position 2', () => {
// Given
const msgSender = '0x003667154bb32e42bb9e1e6532f19d187fa0082e'
const msgExecutor = '0xf4bef13f9f4f2b203faf0c3cbbaabe1afe056955'
const msgGasLimit = '000000000000000000000000000000000000000000000000000000005b877705'
const msgDataType = '00'
const msgData = '0xb1591967aed668a4b27645ff40c444892d91bf5951b382995d4d4f6ee3a2ce03'
const encodedData = `0x${strip0x(msgSender)}${strip0x(msgExecutor)}${msgGasLimit}${msgDataType}${strip0x(msgData)}`
const transactionHash = '0xbdceda9d8c94838aca10c687da1411a07b1390e88239c0638cb9cc264219cc10'
const message = `0x${strip0x(transactionHash)}${strip0x(msgSender)}${strip0x(
msgExecutor
)}${msgGasLimit}${msgDataType}${strip0x(msgData)}`
// When
const result = addTxHashToData({ encodedData, transactionHash })
// Then
expect(result).to.be.equal(message)
})
})
describe('parseAMBMessage', () => {
it('should parse data type 00', () => {
const msgSender = '0x003667154bb32e42bb9e1e6532f19d187fa0082e'
const msgExecutor = '0xf4bef13f9f4f2b203faf0c3cbbaabe1afe056955'
const msgTxHash = '0xbdceda9d8c94838aca10c687da1411a07b1390e88239c0638cb9cc264219cc10'
const msgGasLimit = '000000000000000000000000000000000000000000000000000000005b877705'
const msgDataType = '00'
const msgData = '0xb1591967aed668a4b27645ff40c444892d91bf5951b382995d4d4f6ee3a2ce03'
const message = `0x${strip0x(msgTxHash)}${strip0x(msgSender)}${strip0x(
msgExecutor
)}${msgGasLimit}${msgDataType}${strip0x(msgData)}`
// when
const { sender, executor, txHash } = parseAMBMessage(message)
// then
expect(sender).to.be.equal(msgSender)
expect(executor).to.be.equal(msgExecutor)
expect(txHash).to.be.equal(msgTxHash)
})
})

View File

@@ -10,6 +10,8 @@ function decodeBridgeMode(bridgeModeHash) {
return BRIDGE_MODES.ERC_TO_ERC
case '0x18762d46':
return BRIDGE_MODES.ERC_TO_NATIVE
case '0x2544fbb9':
return BRIDGE_MODES.ARBITRARY_MESSAGE
default:
throw new Error(`Unrecognized bridge mode hash: '${bridgeModeHash}'`)
}

View File

@@ -32,5 +32,31 @@
content: "{{ docker_compose_parsed | to_yaml }}"
dest: "/home/poadocker/bridge/oracle/docker-compose.yml"
- name: Slurp docker compose extended file
slurp:
src: "/home/poadocker/bridge/oracle/docker-compose-transfer.yml"
register: docker_compose_extended_slurp
- name: Parse docker compose file
set_fact:
docker_compose_extended_parsed: "{{ docker_compose_extended_slurp['content'] | b64decode | from_yaml }}"
- name: Add the external network used to connect to Parity nodes in compose extended file
set_fact:
docker_compose_extended_parsed: "{{ docker_compose_extended_parsed |combine({'networks': {'ultimate': {'external': 'true'}}}, recursive=True) }}"
- name: Add all Oracle containers to the network in compose extended file
set_fact:
docker_compose_extended_parsed: "{{ docker_compose_extended_parsed | combine({'services': {item: {'networks': docker_compose_extended_parsed.services[item].networks | union(['ultimate'])}}}, recursive=True) }}"
with_items: "{{ docker_compose_extended_parsed.services }}"
- name: Expose Redis port to allow connecting from redis-cli in compose extended file
set_fact:
docker_compose_extended_parsed: "{{ docker_compose_extended_parsed | combine({'services': {'redis': {'ports': ['6379:6379']}}}, recursive=True) }}"
- name: Write new docker-compose extended file
copy:
content: "{{ docker_compose_extended_parsed | to_yaml }}"
dest: "/home/poadocker/bridge/oracle/docker-compose-transfer.yml"
- name: start the service
shell: service poabridge start

View File

@@ -36,6 +36,8 @@ provisioner:
COMMON_FOREIGN_RPC_URL: "http://parity2:8545"
ORACLE_VALIDATOR_ADDRESS: "0xaaB52d66283F7A1D5978bcFcB55721ACB467384b"
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY: "8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
ORACLE_HOME_START_BLOCK: 1
ORACLE_FOREIGN_START_BLOCK: 1
ui-erc-to-native-host:
COMMON_HOME_RPC_URL: "http://localhost:8541"
COMMON_FOREIGN_RPC_URL: "http://localhost:8542"

View File

@@ -18,6 +18,25 @@
content: "{{ docker_compose_parsed | to_yaml }}"
dest: "{{ bridge_path }}/oracle/docker-compose.yml"
- name: Slurp docker compose extended file
slurp:
src: "{{ bridge_path }}/oracle/docker-compose-transfer.yml"
register: docker_compose_extended_slurp
- name: Parse docker compose extended file
set_fact:
docker_compose_extended_parsed: "{{ docker_compose_extended_slurp['content'] | b64decode | from_yaml }}"
- name: Set logger to remote server for extended file
set_fact:
docker_compose_extended_parsed: "{{ docker_compose_extended_parsed |combine({'services': {item: {'logging': {'driver': 'syslog','options': {'tag': '{{.Name}}/{{.ID}}'}}}}}, recursive=True) }}"
with_items: "{{ docker_compose_extended_parsed.services }}"
- name: Write new docker-compose-extended file
copy:
content: "{{ docker_compose_extended_parsed | to_yaml }}"
dest: "{{ bridge_path }}/oracle/docker-compose-transfer.yml"
- name: Set the local container logs configuration file
template:
src: 31-oracle-docker.conf.j2

View File

@@ -5,14 +5,16 @@
args:
chdir: "{{ bridge_path }}/oracle"
register: BLOCKS
when: (ORACLE_HOME_START_BLOCK is not defined) or (ORACLE_FOREIGN_START_BLOCK is not defined)
- name: Write blocks
blockinfile:
path: "{{ bridge_path }}/oracle/.env"
marker: "## {mark} Calculated by scripts/getValidatorStartBlocks.js"
block: |
HOME_START_BLOCK={{ (BLOCKS.stdout | from_json).homeStartBlock }}
FOREIGN_START_BLOCK={{ (BLOCKS.stdout | from_json).foreignStartBlock }}
ORACLE_HOME_START_BLOCK={{ (BLOCKS.stdout | from_json).homeStartBlock }}
ORACLE_FOREIGN_START_BLOCK={{ (BLOCKS.stdout | from_json).foreignStartBlock }}
when: (ORACLE_HOME_START_BLOCK is not defined) or (ORACLE_FOREIGN_START_BLOCK is not defined)
- name: Get validator address
become_user: "{{ compose_service_user }}"
@@ -25,6 +27,21 @@
set_fact:
ORACLE_VALIDATOR_ADDRESS: "{{ VADDRESS.stdout }}"
- name: Get foreign erc type
become_user: "{{ compose_service_user }}"
shell: docker-compose run --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
set_fact: composefileoverride="-f docker-compose-transfer.yml"
when: ORACLE_BRIDGE_MODE == "ERC_TO_NATIVE" or ( ORACLE_BRIDGE_MODE == "ERC_TO_ERC" and FOREIGN_ERC_TYPE != "ERC677")
- name: Install .key config
template:
src: key.j2

View File

@@ -1,6 +1,8 @@
## General settings
ORACLE_BRIDGE_MODE={{ ORACLE_BRIDGE_MODE }}
{% if ORACLE_LOG_LEVEL | default('') != '' %}
ORACLE_LOG_LEVEL={{ ORACLE_LOG_LEVEL }}
{% endif %}
## Home contract
COMMON_HOME_RPC_URL={{ COMMON_HOME_RPC_URL }}
@@ -45,3 +47,10 @@ 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_HOME_START_BLOCK | default('') != '' %}
ORACLE_HOME_START_BLOCK={{ ORACLE_HOME_START_BLOCK }}
{% endif %}
{% if ORACLE_FOREIGN_START_BLOCK | default('') != '' %}
ORACLE_FOREIGN_START_BLOCK={{ ORACLE_FOREIGN_START_BLOCK }}
{% endif %}

View File

@@ -16,6 +16,7 @@ WORKDIR="{{ '/home/' + compose_service_user | default('poadocker') + '/' + bridg
keyfile="{{ keyfile_path }}"
vaddr="ORACLE_VALIDATOR_ADDRESS="
vkey="ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY="
composefileoverride="{{ composefileoverride | default('') }}"
#Parsing file content and add key to variable
while read -r line
@@ -33,30 +34,30 @@ done < $keyfile
start(){
echo "Starting bridge.."
cd $WORKDIR
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose down -v
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose rm -fv
sudo -u "{{ compose_service_user }}" ORACLE_VALIDATOR_ADDRESS=$vaddr ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=$vkey /usr/local/bin/docker-compose up --detach
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose $composefileoverride down -v
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose $composefileoverride rm -fv
sudo -u "{{ compose_service_user }}" ORACLE_VALIDATOR_ADDRESS=$vaddr ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=$vkey /usr/local/bin/docker-compose $composefileoverride up --detach
}
stop(){
echo "Stopping bridge.."
cd $WORKDIR
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose down -v
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose $composefileoverride down -v
sleep 2
}
status(){
echo "Bridge status:"
cd $WORKDIR
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose ps
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose $composefileoverride ps
}
rebuild(){
echo "Rebuild bridge.."
cd $WORKDIR
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose down -v
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose rm -fv
sudo -u "{{ compose_service_user }}" ORACLE_VALIDATOR_ADDRESS=$vaddr ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=$vkey /usr/local/bin/docker-compose up --detach --force-recreate --no-deps --build
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose $composefileoverride down -v
sudo -u "{{ compose_service_user }}" /usr/local/bin/docker-compose $composefileoverride rm -fv
sudo -u "{{ compose_service_user }}" ORACLE_VALIDATOR_ADDRESS=$vaddr ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=$vkey /usr/local/bin/docker-compose $composefileoverride up --detach --force-recreate --no-deps --build
}

View File

@@ -0,0 +1,18 @@
COMMON_HOME_RPC_URL=http://parity1:8545
COMMON_FOREIGN_RPC_URL=http://parity2:8545
COMMON_HOME_BRIDGE_ADDRESS=0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0
COMMON_FOREIGN_BRIDGE_ADDRESS=0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0
MONITOR_HOME_START_BLOCK=0
MONITOR_FOREIGN_START_BLOCK=0
MONITOR_VALIDATOR_HOME_TX_LIMIT=300000
COMMON_HOME_GAS_PRICE_SUPPLIER_URL=https://gasprice.poa.network/
COMMON_HOME_GAS_PRICE_SPEED_TYPE=standard
COMMON_HOME_GAS_PRICE_FALLBACK=1000000000
COMMON_HOME_GAS_PRICE_FACTOR=1
MONITOR_VALIDATOR_FOREIGN_TX_LIMIT=300000
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL=https://gasprice.poa.network/
COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE=standard
COMMON_FOREIGN_GAS_PRICE_FALLBACK=1000000000
COMMON_FOREIGN_GAS_PRICE_FACTOR=1
MONITOR_TX_NUMBER_THRESHOLD=100
MONITOR_PORT=3013

View File

@@ -0,0 +1,23 @@
ORACLE_BRIDGE_MODE=ARBITRARY_MESSAGE
ORACLE_QUEUE_URL=amqp://rabbit
ORACLE_REDIS_URL=redis://redis
COMMON_HOME_RPC_URL=http://parity1:8545
COMMON_FOREIGN_RPC_URL=http://parity2:8545
COMMON_HOME_BRIDGE_ADDRESS=0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0
COMMON_FOREIGN_BRIDGE_ADDRESS=0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0
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=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_SPEED_TYPE=standard
COMMON_FOREIGN_GAS_PRICE_FALLBACK=10000000000
ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL=600000
COMMON_FOREIGN_GAS_PRICE_FACTOR=1
ORACLE_HOME_RPC_POLLING_INTERVAL=500
ORACLE_FOREIGN_RPC_POLLING_INTERVAL=500
ORACLE_ALLOW_HTTP_FOR_RPC=yes

View File

@@ -21,3 +21,5 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR=0.1
ORACLE_HOME_RPC_POLLING_INTERVAL=500
ORACLE_FOREIGN_RPC_POLLING_INTERVAL=500
ORACLE_ALLOW_HTTP_FOR_RPC=yes
ORACLE_HOME_START_BLOCK=1
ORACLE_FOREIGN_START_BLOCK=1

View File

@@ -37,6 +37,13 @@
"ui": "http://localhost:3002",
"monitor": "http://monitor-erc20-native:3012"
},
"amb": {
"home": "0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0",
"foreign": "0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0",
"homeBox": "0x6C4EaAb8756d53Bf599FFe2347FAFF1123D6C8A1",
"foreignBox": "0x6C4EaAb8756d53Bf599FFe2347FAFF1123D6C8A1",
"monitor": "http://monitor-amb:3013"
},
"homeRPC": {
"URL": "http://parity1:8545",
"ID": "77"

View File

@@ -0,0 +1,25 @@
BRIDGE_MODE=ARBITRARY_MESSAGE
DEPLOYMENT_ACCOUNT_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9
HOME_DEPLOYMENT_GAS_PRICE=10000000000
FOREIGN_DEPLOYMENT_GAS_PRICE=10000000000
GET_RECEIPT_INTERVAL_IN_MILLISECONDS=50
DEPLOYMENT_GAS_LIMIT_EXTRA=0.2
HOME_RPC_URL=http://parity1:8545
HOME_BRIDGE_OWNER=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_VALIDATORS_OWNER=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_UPGRADEABLE_ADMIN=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_MAX_AMOUNT_PER_TX=8000000
HOME_REQUIRED_BLOCK_CONFIRMATIONS=1
HOME_GAS_PRICE=1000000000
FOREIGN_RPC_URL=http://parity2:8545
FOREIGN_BRIDGE_OWNER=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_VALIDATORS_OWNER=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_UPGRADEABLE_ADMIN=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_MAX_AMOUNT_PER_TX=8000000
FOREIGN_REQUIRED_BLOCK_CONFIRMATIONS=1
FOREIGN_GAS_PRICE=10000000000
REQUIRED_NUMBER_OF_VALIDATORS=1
VALIDATORS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b

View File

@@ -61,6 +61,17 @@ services:
command: "true"
networks:
- ultimate
oracle-amb:
build:
context: ..
dockerfile: oracle/Dockerfile
args:
DOT_ENV_PATH: e2e-commons/components-envs/oracle-amb.env
environment:
- NODE_ENV=production
command: "true"
networks:
- ultimate
ui:
build:
context: ..
@@ -121,6 +132,17 @@ services:
- "3012:3012"
networks:
- ultimate
monitor-amb:
build:
context: ..
dockerfile: monitor/Dockerfile
args:
DOT_ENV_PATH: e2e-commons/components-envs/monitor-amb.env
entrypoint: yarn check-and-start
ports:
- "3013:3013"
networks:
- ultimate
e2e:
build:
context: ..

View File

@@ -29,3 +29,14 @@ cp "$ENVS_PATH/erc-to-native.env" "$DEPLOY_PATH/.env"
cd "$DEPLOY_PATH"
node deploy.js
cd - > /dev/null
echo -e "\n\n############ Deploying amb ############\n"
cp "$ENVS_PATH/amb.env" "$DEPLOY_PATH/.env"
cd "$DEPLOY_PATH"
node deploy.js
cd - > /dev/null
echo -e "\n\n############ Deploying test contract for amb ############\n"
cd "$DEPLOY_PATH"
node src/utils/deployTestBox.js
cd - > /dev/null

View File

@@ -9,7 +9,7 @@ docker-compose up -d parity1 parity2 e2e
while [ "$1" != "" ]; do
if [ "$1" == "oracle" ]; then
docker-compose up -d redis rabbit oracle oracle-erc20 oracle-erc20-native
docker-compose up -d redis rabbit oracle oracle-erc20 oracle-erc20-native oracle-amb
docker-compose run -d oracle yarn watcher:signature-request
docker-compose run -d oracle yarn watcher:collected-signatures
@@ -17,9 +17,14 @@ while [ "$1" != "" ]; do
docker-compose run -d oracle-erc20 yarn watcher:signature-request
docker-compose run -d oracle-erc20 yarn watcher:collected-signatures
docker-compose run -d oracle-erc20 yarn watcher:affirmation-request
docker-compose run -d oracle-erc20 yarn watcher:transfer
docker-compose run -d oracle-erc20-native yarn watcher:signature-request
docker-compose run -d oracle-erc20-native yarn watcher:collected-signatures
docker-compose run -d oracle-erc20-native yarn watcher:affirmation-request
docker-compose run -d oracle-erc20-native yarn watcher:transfer
docker-compose run -d oracle-amb yarn watcher:signature-request
docker-compose run -d oracle-amb yarn watcher:collected-signatures
docker-compose run -d oracle-amb yarn watcher:affirmation-request
docker-compose run -d oracle yarn sender:home
docker-compose run -d oracle yarn sender:foreign
fi
@@ -41,7 +46,7 @@ while [ "$1" != "" ]; do
fi
if [ "$1" == "monitor" ]; then
docker-compose up -d monitor monitor-erc20 monitor-erc20-native
docker-compose up -d monitor monitor-erc20 monitor-erc20-native monitor-amb
fi
if [ "$1" == "native-to-erc" ]; then

View File

@@ -3,4 +3,5 @@ while true; do
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor yarn check-all
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20 yarn check-all
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20-native yarn check-all
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-amb yarn check-all
done

128
monitor-e2e/test/amb.js Normal file
View File

@@ -0,0 +1,128 @@
const assert = require('assert')
const axios = require('axios')
const { amb, user, foreignRPC, homeRPC, validator } = require('../../e2e-commons/constants.json')
const { waitUntil, sendAMBMessage, addValidator } = require('../utils')
const baseUrl = amb.monitor
describe('AMB', () => {
describe('balances', async () => {
let data
before(async () => {
;({ data } = await axios.get(`${baseUrl}`))
})
describe('home', async () => {
it('should contain toForeign:', () => assert(data.home.toForeign === 0))
it('should contain fromForeign', () => assert(data.home.fromForeign === 0))
})
describe('foreign', async () => {
it('should contain fromHome:', () => assert(data.foreign.fromHome === 0))
it('should contain toHome', () => assert(data.foreign.toHome === 0))
})
describe('general', async () => {
it('should contain fromHomeToForeignDiff', () => assert(data.fromHomeToForeignDiff === 0))
it('should contain fromForeignToHomeDiff', () => assert(data.fromForeignToHomeDiff === 0))
it('should contain lastChecked', () => assert(data.lastChecked >= 0))
it('should contain timeDiff', () => assert(data.timeDiff >= 0))
it('should contain lastChecked', () => assert(data.lastChecked >= 0))
})
})
describe('validators', async () => {
let data
before(async () => {
;({ data } = await axios.get(`${baseUrl}/validators`))
})
it('home', () => {
assert(typeof data.home.validators === 'object')
assert(data.home.validators[validator.address].balance > 0)
assert(data.home.validators[validator.address].leftTx > 0)
assert(data.home.validators[validator.address].gasPrice > 0)
})
it('foreign', () => {
assert(typeof data.foreign.validators === 'object')
assert(data.foreign.validators[validator.address].balance > 0)
assert(data.foreign.validators[validator.address].leftTx > 0)
assert(data.foreign.validators[validator.address].gasPrice > 0)
})
it('requiredSignaturesMatch', () => assert(data.requiredSignaturesMatch, 1))
it('validatorsMatch', () => assert(data.validatorsMatch))
it('lastChecked', () => assert(data.lastChecked >= 0))
it('timeDiff', () => assert(data.timeDiff >= 0))
it('homeOk', () => assert(data.homeOk))
it('foreignOk', () => assert(data.foreignOk))
it('ok', () => assert(data.ok))
})
describe('eventsStats', async () => {
let data
before(async () => {
;({ data } = await axios.get(`${baseUrl}/eventsStats`))
})
it('ok', () => assert(data.ok))
it('lastChecked', () => assert(data.lastChecked >= 0))
it('timeDiff', () => assert(data.timeDiff >= 0))
it('home-deliveredMsgNotProcessedInForeign', () =>
assert(typeof data.home.deliveredMsgNotProcessedInForeign === 'object'))
it('home-processedMsgNotDeliveredInForeign', () =>
assert(typeof data.home.processedMsgNotDeliveredInForeign === 'object'))
it('foreign-deliveredMsgNotProcessedInHome', () =>
assert(typeof data.foreign.deliveredMsgNotProcessedInHome === 'object'))
it('foreign-processedMsgNotDeliveredInHome', () =>
assert(typeof data.foreign.processedMsgNotDeliveredInHome === 'object'))
})
describe('alerts', async () => {
let data
before(async () => {
;({ data } = await axios.get(`${baseUrl}/alerts`))
})
it('ok', () => assert(data.ok))
it('lastChecked', () => assert(data.lastChecked >= 0))
it('timeDiff', () => assert(data.timeDiff >= 0))
it('executeSignatures', () => assert(typeof data.executeSignatures === 'object'))
it('executeAffirmations', () => assert(typeof data.executeAffirmations === 'object'))
})
describe('changing state of contracts', () => {
let data
before(async () => {
assert((await axios.get(`${baseUrl}/validators`)).data.validatorsMatch === true)
})
it('should change fromForeignToHomeDiff', async () => {
// send message
await sendAMBMessage(foreignRPC.URL, user, amb.foreignBox, amb.foreign, amb.homeBox)
await waitUntil(async () => {
;({ data } = await axios.get(`${baseUrl}`))
return data.fromForeignToHomeDiff !== 0
})
})
it('should change fromHomeToForeignDiff', async () => {
// send message
await sendAMBMessage(homeRPC.URL, user, amb.homeBox, amb.home, amb.foreignBox)
await waitUntil(async () => {
;({ data } = await axios.get(`${baseUrl}`))
return data.fromHomeToForeignDiff !== 0
})
})
it('should change validatorsMatch', async () => {
await addValidator(foreignRPC.URL, validator, amb.foreign)
await waitUntil(async () => {
;({ data } = await axios.get(`${baseUrl}/validators`))
return data.validatorsMatch === false
})
})
})
})

View File

@@ -1,5 +1,5 @@
const Web3 = require('web3')
const { ERC677_BRIDGE_TOKEN_ABI, BRIDGE_VALIDATORS_ABI, FOREIGN_NATIVE_TO_ERC_ABI } = require('../commons')
const { ERC677_BRIDGE_TOKEN_ABI, BRIDGE_VALIDATORS_ABI, FOREIGN_NATIVE_TO_ERC_ABI, BOX_ABI } = require('../commons')
const waitUntil = async (predicate, step = 100, timeout = 10000) => {
const stopTime = Date.now() + timeout
@@ -37,6 +37,17 @@ const sendTokens = async (rpcUrl, account, tokenAddress, recipientAddress) => {
})
}
const sendAMBMessage = async (rpcUrl, account, boxAddress, bridgeAddress, boxOtherSideAddress) => {
const web3 = new Web3(new Web3.providers.HttpProvider(rpcUrl))
web3.eth.accounts.wallet.add(account.privateKey)
const homeBox = new web3.eth.Contract(BOX_ABI, boxAddress)
await homeBox.methods.setValueOnOtherNetwork(3, bridgeAddress, boxOtherSideAddress).send({
from: account.address,
gas: '400000'
})
}
const addValidator = async (rpcUrl, account, bridgeAddress) => {
const web3 = new Web3(new Web3.providers.HttpProvider(rpcUrl))
web3.eth.accounts.wallet.add(account.privateKey)
@@ -53,5 +64,6 @@ module.exports = {
waitUntil,
sendEther,
sendTokens,
addValidator
addValidator,
sendAMBMessage
}

View File

@@ -7,6 +7,7 @@ check_files_exist() {
(docker-compose -f ../e2e-commons/docker-compose.yml exec monitor /bin/bash -c "$command") || rc=1
(docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20 /bin/bash -c "$command") || rc=1
(docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20-native /bin/bash -c "$command") || rc=1
(docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-amb /bin/bash -c "$command") || rc=1
done
return $rc
}

View File

@@ -8,5 +8,8 @@
"rules": {
"no-use-before-define": "off",
"node/no-unpublished-require": "off"
},
"env": {
"mocha": true
}
}

View File

@@ -3,6 +3,8 @@ const Web3 = require('web3')
const logger = require('./logger')('alerts')
const eventsInfo = require('./utils/events')
const { getBlockNumber } = require('./utils/contract')
const { processedMsgNotDelivered, eventWithoutReference } = require('./utils/message')
const { BRIDGE_MODES } = require('../commons')
const { COMMON_HOME_RPC_URL, COMMON_FOREIGN_RPC_URL } = process.env
@@ -13,11 +15,23 @@ const foreignProvider = new Web3.providers.HttpProvider(COMMON_FOREIGN_RPC_URL)
const web3Foreign = new Web3(foreignProvider)
async function main() {
const { foreignDeposits, homeDeposits, homeWithdrawals, foreignWithdrawals } = await eventsInfo()
const xSignatures = foreignDeposits.filter(findDifferences(homeDeposits))
const xAffirmations = homeWithdrawals.filter(findDifferences(foreignWithdrawals))
const {
homeToForeignRequests,
homeToForeignConfirmations,
foreignToHomeConfirmations,
foreignToHomeRequests,
bridgeMode
} = await eventsInfo()
let xSignatures
let xAffirmations
if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
xSignatures = homeToForeignConfirmations.filter(processedMsgNotDelivered(homeToForeignRequests))
xAffirmations = foreignToHomeConfirmations.filter(processedMsgNotDelivered(foreignToHomeRequests))
} else {
xSignatures = homeToForeignConfirmations.filter(eventWithoutReference(homeToForeignRequests))
xAffirmations = foreignToHomeConfirmations.filter(eventWithoutReference(foreignToHomeRequests))
}
logger.debug('building misbehavior blocks')
const [homeBlockNumber, foreignBlockNumber] = await getBlockNumber(web3Home, web3Foreign)
@@ -37,8 +51,8 @@ async function main() {
const foreignValidators = await Promise.all(xSignatures.map(event => findTxSender(web3Foreign)(event)))
const homeValidators = await Promise.all(xAffirmations.map(event => findTxSender(web3Home)(event)))
const xSignaturesTxs = xSignatures.map(normalizeEventInformation).reduce(buildTxList(foreignValidators), {})
const xAffirmationsTxs = xAffirmations.map(normalizeEventInformation).reduce(buildTxList(homeValidators), {})
const xSignaturesTxs = xSignatures.reduce(buildTxList(foreignValidators), {})
const xAffirmationsTxs = xAffirmations.reduce(buildTxList(homeValidators), {})
logger.debug('Done')
@@ -135,7 +149,7 @@ const findTxSender = web3 => async ({ transactionHash }) => {
* }}}
*/
const buildTxList = validatorsList => (acc, event, index) => {
acc[event.txHash] = {
acc[event.transactionHash] = {
value: event.value,
block: event.blockNumber,
referenceTx: event.referenceTx,
@@ -145,38 +159,4 @@ const buildTxList = validatorsList => (acc, event, index) => {
return acc
}
/**
* Finds a missing destDeposit in src list if there's any
* @param {Array} src
* @returns {function(*=): boolean}
*/
const findDifferences = src => dest => {
const b = normalizeEventInformation(dest)
return (
src
.map(normalizeEventInformation)
.filter(a => a.referenceTx === b.referenceTx && a.recipient === b.recipient && a.value === b.value).length === 0
)
}
/**
* Normalizes the different event objects to facilitate data processing
* @param {Object} event
* @returns {{
* txHash: string,
* blockNumber: number,
* referenceTx: string,
* recipient: string | *,
* value: *
* }}
*/
const normalizeEventInformation = event => ({
txHash: event.transactionHash,
blockNumber: event.blockNumber,
referenceTx: event.returnValues.transactionHash || event.transactionHash,
recipient: event.returnValues.recipient || event.returnValues.from,
value: event.returnValues.value
})
module.exports = main

View File

@@ -1,5 +1,5 @@
# Yarn:
*/4 * * * * cd $HOME/bridge-monitor; yarn check-all >>cronWorker.out 2>>cronWorker.err
*/4 * * * * cd $HOME/tokenbridge/monitor; yarn check-all >>cronWorker.out 2>>cronWorker.err
# Docker:
*/4 * * * * cd $HOME/bridge-monitor; docker-compose exec monitor yarn check-all >>cronWorker.out 2>>cronWorker.err
*/4 * * * * cd $HOME/tokenbridge/monitor; docker-compose exec monitor yarn check-all >>cronWorker.out 2>>cronWorker.err

View File

@@ -1,81 +1,51 @@
require('dotenv').config()
const logger = require('./logger')('eventsStats')
const eventsInfo = require('./utils/events')
function compareDepositsHome(foreign) {
return homeDeposit => {
return (
foreign.filter(foreignDeposit => {
return (
foreignDeposit.returnValues.transactionHash === homeDeposit.transactionHash &&
foreignDeposit.returnValues.recipient === homeDeposit.returnValues.recipient &&
foreignDeposit.returnValues.value === homeDeposit.returnValues.value
)
}).length === 0
)
}
}
function compareDepositsForeign(home) {
return foreignDeposit => {
return (
home.filter(homeDeposit => {
return (
homeDeposit.transactionHash === foreignDeposit.returnValues.transactionHash &&
homeDeposit.returnValues.recipient === foreignDeposit.returnValues.recipient &&
homeDeposit.returnValues.value === foreignDeposit.returnValues.value
)
}).length === 0
)
}
}
function compareTransferHome(foreign) {
return homeDeposit => {
return (
foreign.filter(foreignDeposit => {
return (
homeDeposit.returnValues.transactionHash === foreignDeposit.transactionHash &&
homeDeposit.returnValues.recipient === foreignDeposit.returnValues.from &&
homeDeposit.returnValues.value === foreignDeposit.returnValues.value
)
}).length === 0
)
}
}
function compareTransferForeign(home) {
return foreignDeposit => {
return (
home.filter(homeDeposit => {
return (
foreignDeposit.transactionHash === homeDeposit.returnValues.transactionHash &&
foreignDeposit.returnValues.from === homeDeposit.returnValues.recipient &&
foreignDeposit.returnValues.value === homeDeposit.returnValues.value
)
}).length === 0
)
}
}
const { processedMsgNotDelivered, deliveredMsgNotProcessed, eventWithoutReference } = require('./utils/message')
const { BRIDGE_MODES } = require('../commons')
async function main() {
const { foreignDeposits, homeDeposits, homeWithdrawals, foreignWithdrawals, isExternalErc20 } = await eventsInfo()
const {
homeToForeignRequests,
homeToForeignConfirmations,
foreignToHomeConfirmations,
foreignToHomeRequests,
bridgeMode
} = await eventsInfo()
const onlyInHomeDeposits = homeDeposits.filter(compareDepositsHome(foreignDeposits))
const onlyInForeignDeposits = foreignDeposits.concat([]).filter(compareDepositsForeign(homeDeposits))
if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
return {
home: {
deliveredMsgNotProcessedInForeign: homeToForeignRequests.filter(
deliveredMsgNotProcessed(homeToForeignConfirmations)
),
processedMsgNotDeliveredInForeign: foreignToHomeConfirmations.filter(
processedMsgNotDelivered(foreignToHomeRequests)
)
},
foreign: {
deliveredMsgNotProcessedInHome: foreignToHomeRequests.filter(
deliveredMsgNotProcessed(foreignToHomeConfirmations)
),
processedMsgNotDeliveredInHome: homeToForeignConfirmations.filter(
processedMsgNotDelivered(homeToForeignRequests)
)
},
lastChecked: Math.floor(Date.now() / 1000)
}
} else {
const onlyInHomeDeposits = homeToForeignRequests.filter(eventWithoutReference(homeToForeignConfirmations))
const onlyInForeignDeposits = homeToForeignConfirmations.filter(eventWithoutReference(homeToForeignRequests))
const onlyInHomeWithdrawals = isExternalErc20
? homeWithdrawals.filter(compareTransferHome(foreignWithdrawals))
: homeWithdrawals.filter(compareDepositsForeign(foreignWithdrawals))
const onlyInForeignWithdrawals = isExternalErc20
? foreignWithdrawals.filter(compareTransferForeign(homeWithdrawals))
: foreignWithdrawals.filter(compareDepositsHome(homeWithdrawals))
const onlyInHomeWithdrawals = foreignToHomeConfirmations.filter(eventWithoutReference(foreignToHomeRequests))
const onlyInForeignWithdrawals = foreignToHomeRequests.filter(eventWithoutReference(foreignToHomeConfirmations))
logger.debug('Done')
return {
onlyInHomeDeposits,
onlyInForeignDeposits,
onlyInHomeWithdrawals,
onlyInForeignWithdrawals,
lastChecked: Math.floor(Date.now() / 1000)
return {
onlyInHomeDeposits,
onlyInForeignDeposits,
onlyInHomeWithdrawals,
onlyInForeignWithdrawals,
lastChecked: Math.floor(Date.now() / 1000)
}
}
}

View File

@@ -112,6 +112,12 @@ async function main(bridgeMode) {
balanceDiff: Number(Web3Utils.fromWei(diff)),
lastChecked: Math.floor(Date.now() / 1000)
}
} else if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
return {
home: {},
foreign: {},
lastChecked: Math.floor(Date.now() / 1000)
}
} else {
throw new Error(`Unrecognized bridge mode: '${bridgeMode}'`)
}

View File

@@ -1,19 +1,40 @@
require('dotenv').config()
const eventsInfo = require('./utils/events')
const { BRIDGE_MODES } = require('../commons')
async function main(bridgeMode) {
const { foreignDeposits, homeDeposits, homeWithdrawals, foreignWithdrawals } = await eventsInfo(bridgeMode)
const {
homeToForeignConfirmations,
homeToForeignRequests,
foreignToHomeConfirmations,
foreignToHomeRequests
} = await eventsInfo(bridgeMode)
return {
depositsDiff: homeDeposits.length - foreignDeposits.length,
withdrawalDiff: homeWithdrawals.length - foreignWithdrawals.length,
home: {
deposits: homeDeposits.length,
withdrawals: homeWithdrawals.length
},
foreign: {
deposits: foreignDeposits.length,
withdrawals: foreignWithdrawals.length
if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
return {
fromHomeToForeignDiff: homeToForeignRequests.length - homeToForeignConfirmations.length,
fromForeignToHomeDiff: foreignToHomeConfirmations.length - foreignToHomeRequests.length,
home: {
toForeign: homeToForeignRequests.length,
fromForeign: foreignToHomeConfirmations.length
},
foreign: {
fromHome: homeToForeignConfirmations.length,
toHome: foreignToHomeRequests.length
}
}
} else {
return {
depositsDiff: homeToForeignRequests.length - homeToForeignConfirmations.length,
withdrawalDiff: foreignToHomeConfirmations.length - foreignToHomeRequests.length,
home: {
deposits: homeToForeignRequests.length,
withdrawals: foreignToHomeConfirmations.length
},
foreign: {
deposits: homeToForeignConfirmations.length,
withdrawals: foreignToHomeRequests.length
}
}
}
}

View File

@@ -77,10 +77,10 @@ app.get('/eventsStats', async (req, res, next) => {
try {
const results = await readFile('./responses/eventsStats.json')
results.ok =
results.onlyInHomeDeposits.length === 0 &&
results.onlyInForeignDeposits.length === 0 &&
results.onlyInHomeWithdrawals.length === 0 &&
results.onlyInForeignWithdrawals.length === 0
(results.onlyInHomeDeposits || results.home.deliveredMsgNotProcessedInForeign).length === 0 &&
(results.onlyInForeignDeposits || results.home.processedMsgNotDeliveredInForeign).length === 0 &&
(results.onlyInHomeWithdrawals || results.foreign.deliveredMsgNotProcessedInHome).length === 0 &&
(results.onlyInForeignWithdrawals || results.foreign.processedMsgNotDeliveredInHome).length === 0
res.json(results)
} catch (e) {
// this will eventually be handled by your error handling middleware

View File

@@ -8,7 +8,8 @@
"start": "node index.js",
"check-and-start": "yarn check-all && yarn start",
"lint": "eslint . --ignore-path ../.eslintignore",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"test": "NODE_ENV=test mocha"
},
"author": "",
"license": "ISC",
@@ -23,5 +24,6 @@
"node": ">=8.9"
},
"devDependencies": {
"chai": "^4.2.0"
}
}

View File

@@ -0,0 +1,206 @@
const { expect } = require('chai')
const { normalizeEventInformation, eventWithoutReference } = require('../utils/message')
describe('normalizeEventInformation', () => {
it('should return normalized object for UserRequestForSignature event', () => {
// Given
const event = {
address: '0x7301CFA0e1756B71869E93d4e4Dca5c7d0eb0AA6',
blockNumber: 324231,
transactionHash: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
returnValues: {
recipient: '0xA84944735B66e957Fe385567dcc85975022Fe68A',
value: '100000000000000000000'
},
event: 'UserRequestForSignature'
}
// When
const result = normalizeEventInformation(event)
// Then
expect(result.transactionHash).to.equal(event.transactionHash)
expect(result.blockNumber).to.equal(event.blockNumber)
expect(result.referenceTx).to.equal(event.transactionHash)
expect(result.recipient).to.equal(event.returnValues.recipient)
expect(result.value).to.equal(event.returnValues.value)
})
it('should return normalized object for UserRequestForAffirmation event', () => {
// Given
const event = {
address: '0x7301CFA0e1756B71869E93d4e4Dca5c7d0eb0AA6',
blockNumber: 324231,
transactionHash: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
returnValues: {
recipient: '0xA84944735B66e957Fe385567dcc85975022Fe68A',
value: '100000000000000000000'
},
event: 'UserRequestForAffirmation'
}
// When
const result = normalizeEventInformation(event)
// Then
expect(result.transactionHash).to.equal(event.transactionHash)
expect(result.blockNumber).to.equal(event.blockNumber)
expect(result.referenceTx).to.equal(event.transactionHash)
expect(result.recipient).to.equal(event.returnValues.recipient)
expect(result.value).to.equal(event.returnValues.value)
})
it('should return normalized object for transfer event', () => {
// Given
const event = {
address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359',
blockNumber: 6593953,
transactionHash: '0x05afb402e27946d3600b100020dc23419ffd10cb61d3b241cee7b4a84909b48a',
returnValues: {
from: '0x13C0a8009A578837fB7A80Aa252F6A3ba4aD6B79',
to: '0x4aa42145Aa6Ebf72e164C9bBC74fbD3788045016',
value: '4000000000000000000'
},
event: 'Transfer'
}
// When
const result = normalizeEventInformation(event)
// Then
expect(result.transactionHash).to.equal(event.transactionHash)
expect(result.blockNumber).to.equal(event.blockNumber)
expect(result.referenceTx).to.equal(event.transactionHash)
expect(result.recipient).to.equal(event.returnValues.from)
expect(result.value).to.equal(event.returnValues.value)
})
it('should return normalized object for RelayedMessage event', () => {
// Given
const event = {
address: '0x4aa42145Aa6Ebf72e164C9bBC74fbD3788045016',
blockNumber: 7025826,
transactionHash: '0x6ee5969973da763d6d9f162d2dd1b1ec34c2dd977dc39e6b25030b4f04471567',
returnValues: {
recipient: '0x38BC00Ea43EbB5ef5150593A0BA6C381803717e2',
value: '4900000000000000000',
transactionHash: '0x5c5c2ab5e333bda4acd035a6a30ea29c7370351891d85373b2d06c7cc6cbb210'
},
event: 'RelayedMessage'
}
// When
const result = normalizeEventInformation(event)
// Then
expect(result.transactionHash).to.equal(event.transactionHash)
expect(result.blockNumber).to.equal(event.blockNumber)
expect(result.referenceTx).to.equal(event.returnValues.transactionHash)
expect(result.recipient).to.equal(event.returnValues.recipient)
expect(result.value).to.equal(event.returnValues.value)
})
it('should return normalized object for AffirmationCompleted event', () => {
// Given
const event = {
address: '0x7301CFA0e1756B71869E93d4e4Dca5c7d0eb0AA6',
blockNumber: 474439,
transactionHash: '0x654004b372ba32754cef34f403153bbdf43f0fbb3191d5e4683bba7f32e0dc4a',
returnValues: {
recipient: '0x9b7b2B4f7a391b6F14A81221AE0920A9735B67Fb',
value: '5000000000000000000',
transactionHash: '0xe96da94bbda2cfc865acd3f98040f5c79a627ee9de839d86885d34acd8ecd10d'
},
event: 'AffirmationCompleted'
}
// When
const result = normalizeEventInformation(event)
// Then
expect(result.transactionHash).to.equal(event.transactionHash)
expect(result.blockNumber).to.equal(event.blockNumber)
expect(result.referenceTx).to.equal(event.returnValues.transactionHash)
expect(result.recipient).to.equal(event.returnValues.recipient)
expect(result.value).to.equal(event.returnValues.value)
})
})
describe('eventWithoutReference', () => {
it('should return false if event is present', () => {
// Given
const event = {
txHash: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
blockNumber: 474439,
referenceTx: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
recipient: '0x9b7b2B4f7a391b6F14A81221AE0920A9735B67Fb',
value: '5000000000000000000'
}
const otherSideEvents = [
{
txHash: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
blockNumber: 474439,
referenceTx: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
recipient: '0x9b7b2B4f7a391b6F14A81221AE0920A9735B67Fb',
value: '5000000000000000000'
},
{
txHash: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
blockNumber: 474439,
referenceTx: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
recipient: '0x38BC00Ea43EbB5ef5150593A0BA6C381803717e2',
value: '6000000000000000000'
},
{
txHash: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
blockNumber: 474439,
referenceTx: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
recipient: '0x38BC00Ea43EbB5ef5150593A0BA6C381803717e2',
value: '8000000000000000000'
}
]
// When
const result = eventWithoutReference(otherSideEvents)(event)
// Then
expect(result).to.equal(false)
})
it('should return true if event is not present', () => {
// Given
const event = {
txHash: '0xe96da94bbda2cfc865acd3f98040f5c79a627ee9de839d86885d34acd8ecd10d',
blockNumber: 474439,
referenceTx: '0xe96da94bbda2cfc865acd3f98040f5c79a627ee9de839d86885d34acd8ecd10d',
recipient: '0x9b7b2B4f7a391b6F14A81221AE0920A9735B67Fb',
value: '2000000000000000000'
}
const otherSideEvents = [
{
txHash: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
blockNumber: 474439,
referenceTx: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
recipient: '0x9b7b2B4f7a391b6F14A81221AE0920A9735B67Fb',
value: '5000000000000000000'
},
{
txHash: '0x05afb402e27946d3600b100020dc23419ffd10cb61d3b241cee7b4a84909b48a',
blockNumber: 474439,
referenceTx: '0x05afb402e27946d3600b100020dc23419ffd10cb61d3b241cee7b4a84909b48a',
recipient: '0x38BC00Ea43EbB5ef5150593A0BA6C381803717e2',
value: '6000000000000000000'
},
{
txHash: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
blockNumber: 474439,
referenceTx: '0x17be1e0745136b9e2857124542f8218812db8fe4458236d5ae045c1ceeb79978',
recipient: '0x38BC00Ea43EbB5ef5150593A0BA6C381803717e2',
value: '8000000000000000000'
}
]
// When
const result = eventWithoutReference(otherSideEvents)(event)
// Then
expect(result).to.equal(true)
})
})

View File

@@ -11,8 +11,10 @@ const {
ERC20_ABI,
ERC677_BRIDGE_TOKEN_ABI,
getTokenType,
getPastEvents
getPastEvents,
ZERO_ADDRESS
} = require('../../commons')
const { normalizeEventInformation } = require('./message')
const {
COMMON_HOME_RPC_URL,
@@ -38,61 +40,118 @@ async function main(mode) {
const homeBridge = new web3Home.eth.Contract(HOME_ABI, COMMON_HOME_BRIDGE_ADDRESS)
const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS)
const v1Bridge = bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1
const erc20MethodName = bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC || v1Bridge ? 'erc677token' : 'erc20token'
const erc20Address = await foreignBridge.methods[erc20MethodName]().call()
const tokenType = await getTokenType(
new web3Foreign.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, erc20Address),
COMMON_FOREIGN_BRIDGE_ADDRESS
)
const isExternalErc20 = tokenType === ERC_TYPES.ERC20
const erc20Contract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address)
let isExternalErc20
let erc20Contract
let normalizeEvent = normalizeEventInformation
if (bridgeMode !== BRIDGE_MODES.ARBITRARY_MESSAGE) {
const erc20MethodName = bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC || v1Bridge ? 'erc677token' : 'erc20token'
const erc20Address = await foreignBridge.methods[erc20MethodName]().call()
const tokenType = await getTokenType(
new web3Foreign.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, erc20Address),
COMMON_FOREIGN_BRIDGE_ADDRESS
)
isExternalErc20 = tokenType === ERC_TYPES.ERC20
erc20Contract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address)
} else {
normalizeEvent = e => e
}
logger.debug('getting last block numbers')
const [homeBlockNumber, foreignBlockNumber] = await getBlockNumber(web3Home, web3Foreign)
logger.debug("calling homeBridge.getPastEvents('UserRequestForSignature')")
const homeDeposits = await getPastEvents(homeBridge, {
const homeToForeignRequests = (await getPastEvents(homeBridge, {
event: v1Bridge ? 'Deposit' : 'UserRequestForSignature',
fromBlock: MONITOR_HOME_START_BLOCK,
toBlock: homeBlockNumber
})
})).map(normalizeEvent)
logger.debug("calling foreignBridge.getPastEvents('RelayedMessage')")
const foreignDeposits = await getPastEvents(foreignBridge, {
const homeToForeignConfirmations = (await getPastEvents(foreignBridge, {
event: v1Bridge ? 'Deposit' : 'RelayedMessage',
fromBlock: MONITOR_FOREIGN_START_BLOCK,
toBlock: foreignBlockNumber
})
})).map(normalizeEvent)
logger.debug("calling homeBridge.getPastEvents('AffirmationCompleted')")
const homeWithdrawals = await getPastEvents(homeBridge, {
const foreignToHomeConfirmations = (await getPastEvents(homeBridge, {
event: v1Bridge ? 'Withdraw' : 'AffirmationCompleted',
fromBlock: MONITOR_HOME_START_BLOCK,
toBlock: homeBlockNumber
})
})).map(normalizeEvent)
logger.debug("calling foreignBridge.getPastEvents('UserRequestForAffirmation')")
const foreignWithdrawals = isExternalErc20
? await getPastEvents(erc20Contract, {
event: 'Transfer',
fromBlock: MONITOR_FOREIGN_START_BLOCK,
toBlock: foreignBlockNumber,
options: {
filter: { to: COMMON_FOREIGN_BRIDGE_ADDRESS }
}
})
: await getPastEvents(foreignBridge, {
event: v1Bridge ? 'Withdraw' : 'UserRequestForAffirmation',
let foreignToHomeRequests = (await getPastEvents(foreignBridge, {
event: v1Bridge ? 'Withdraw' : 'UserRequestForAffirmation',
fromBlock: MONITOR_FOREIGN_START_BLOCK,
toBlock: foreignBlockNumber
})).map(normalizeEvent)
if (isExternalErc20) {
logger.debug("calling erc20Contract.getPastEvents('Transfer')")
let transferEvents = (await getPastEvents(erc20Contract, {
event: 'Transfer',
fromBlock: MONITOR_FOREIGN_START_BLOCK,
toBlock: foreignBlockNumber,
options: {
filter: { to: COMMON_FOREIGN_BRIDGE_ADDRESS }
}
})).map(normalizeEvent)
let directTransfers = transferEvents
const tokensSwappedAbiExists = FOREIGN_ABI.filter(e => e.type === 'event' && e.name === 'TokensSwapped')[0]
if (tokensSwappedAbiExists) {
const tokensSwappedEvents = await getPastEvents(foreignBridge, {
event: 'TokensSwapped',
fromBlock: MONITOR_FOREIGN_START_BLOCK,
toBlock: foreignBlockNumber
})
// 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 uniqueTokenAddresses = [...new Set(bridgeTokensSwappedEvents.map(e => e.returnValues.from))]
await Promise.all(
uniqueTokenAddresses.map(async tokenAddress => {
const previousERC20 = new web3Foreign.eth.Contract(ERC20_ABI, tokenAddress)
const previousTransferEvents = (await getPastEvents(previousERC20, {
event: 'Transfer',
fromBlock: MONITOR_FOREIGN_START_BLOCK,
toBlock: foreignBlockNumber,
options: {
filter: { to: COMMON_FOREIGN_BRIDGE_ADDRESS }
}
})).map(normalizeEvent)
transferEvents = [...previousTransferEvents, ...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
)
}
// Get transfer events that didn't have a UserRequestForAffirmation event in the same transaction
directTransfers = directTransfers.filter(
e => foreignToHomeRequests.findIndex(t => t.referenceTx === e.referenceTx) === -1
)
foreignToHomeRequests = [...foreignToHomeRequests, ...directTransfers]
}
logger.debug('Done')
return {
homeDeposits,
foreignDeposits,
homeWithdrawals,
foreignWithdrawals,
isExternalErc20
homeToForeignRequests,
homeToForeignConfirmations,
foreignToHomeConfirmations,
foreignToHomeRequests,
isExternalErc20,
bridgeMode
}
}

72
monitor/utils/message.js Normal file
View File

@@ -0,0 +1,72 @@
const web3Utils = require('web3').utils
const { addTxHashToData, parseAMBMessage } = require('../../commons')
function deliveredMsgNotProcessed(processedList) {
return deliveredMsg => {
const msg = parseAMBMessage(
addTxHashToData({
encodedData: deliveredMsg.returnValues.encodedData,
transactionHash: deliveredMsg.transactionHash
})
)
return (
processedList.filter(processedMsg => {
return messageEqualsEvent(msg, processedMsg.returnValues)
}).length === 0
)
}
}
function processedMsgNotDelivered(deliveredList) {
return processedMsg => {
return (
deliveredList.filter(deliveredMsg => {
const msg = parseAMBMessage(
addTxHashToData({
encodedData: deliveredMsg.returnValues.encodedData,
transactionHash: deliveredMsg.transactionHash
})
)
return messageEqualsEvent(msg, processedMsg.returnValues)
}).length === 0
)
}
}
function messageEqualsEvent(parsedMsg, event) {
return (
web3Utils.toChecksumAddress(parsedMsg.sender) === event.sender &&
web3Utils.toChecksumAddress(parsedMsg.executor) === event.executor &&
parsedMsg.txHash === event.transactionHash
)
}
/**
* Normalizes the different event objects to facilitate data processing
* @param {Object} event
* @returns {{
* transactionHash: string,
* blockNumber: number,
* referenceTx: string,
* recipient: string | *,
* value: *
* }}
*/
const normalizeEventInformation = event => ({
transactionHash: event.transactionHash,
blockNumber: event.blockNumber,
referenceTx: event.returnValues.transactionHash || event.transactionHash,
recipient: event.returnValues.recipient || event.returnValues.from,
value: event.returnValues.value
})
const eventWithoutReference = otherSideEvents => e =>
otherSideEvents.filter(a => a.referenceTx === e.referenceTx && a.recipient === e.recipient && a.value === e.value)
.length === 0
module.exports = {
deliveredMsgNotProcessed,
processedMsgNotDelivered,
normalizeEventInformation,
eventWithoutReference
}

102
oracle-e2e/test/amb.js Normal file
View File

@@ -0,0 +1,102 @@
const Web3 = require('web3')
const assert = require('assert')
const promiseRetry = require('promise-retry')
const { user, homeRPC, foreignRPC, amb } = require('../../e2e-commons/constants.json')
const { generateNewBlock } = require('../../e2e-commons/utils')
const { BOX_ABI } = require('../../commons')
const { toBN } = Web3.utils
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 homeBox = new homeWeb3.eth.Contract(BOX_ABI, amb.homeBox)
const foreignBox = new foreignWeb3.eth.Contract(BOX_ABI, amb.foreignBox)
describe('arbitrary message bridging', () => {
describe('Home to Foreign', () => {
describe('Subsidized Mode', () => {
it('should bridge message', async () => {
const newValue = 3
const initialValue = await foreignBox.methods.value().call()
assert(!toBN(initialValue).eq(toBN(newValue)), 'initial value should be different from new value')
const setValueTx = await homeBox.methods
.setValueOnOtherNetwork(newValue, amb.home, amb.foreignBox)
.send({
from: user.address,
gas: '400000'
})
.catch(e => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(homeWeb3, user.address)
// The bridge should create a new transaction with a CollectedSignatures
// event so we generate another trivial transaction
await promiseRetry(
async retry => {
const lastBlockNumber = await homeWeb3.eth.getBlockNumber()
if (lastBlockNumber >= setValueTx.blockNumber + 2) {
await generateNewBlock(homeWeb3, user.address)
} else {
retry()
}
},
{
forever: true,
factor: 1,
minTimeout: 500
}
)
// check that value changed and balance decreased
await promiseRetry(async retry => {
const value = await foreignBox.methods.value().call()
if (!toBN(value).eq(toBN(newValue))) {
retry()
}
})
})
})
})
describe('Foreign to Home', () => {
describe('Subsidized Mode', () => {
it('should bridge message', async () => {
const newValue = 7
const initialValue = await homeBox.methods.value().call()
assert(!toBN(initialValue).eq(toBN(newValue)), 'initial value should be different from new value')
await foreignBox.methods
.setValueOnOtherNetwork(newValue, amb.foreign, amb.homeBox)
.send({
from: user.address,
gas: '400000'
})
.catch(e => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that value changed and balance decreased
await promiseRetry(async retry => {
const value = await homeBox.methods.value().call()
if (!toBN(value).eq(toBN(newValue))) {
retry()
}
})
})
})
})
})

View File

@@ -1,8 +1,8 @@
const Web3 = require('web3')
const assert = require('assert')
const promiseRetry = require('promise-retry')
const { user, ercToErcBridge, homeRPC, foreignRPC } = require('../../e2e-commons/constants.json')
const { ERC677_BRIDGE_TOKEN_ABI } = require('../../commons')
const { user, secondUser, ercToErcBridge, homeRPC, foreignRPC } = require('../../e2e-commons/constants.json')
const { ERC677_BRIDGE_TOKEN_ABI, FOREIGN_ERC_TO_NATIVE_ABI } = require('../../commons')
const { generateNewBlock } = require('../../e2e-commons/utils')
const homeWeb3 = new Web3(new Web3.providers.HttpProvider(homeRPC.URL))
@@ -17,6 +17,7 @@ homeWeb3.eth.accounts.wallet.add(user.privateKey)
foreignWeb3.eth.accounts.wallet.add(user.privateKey)
const erc20Token = new foreignWeb3.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, ercToErcBridge.foreignToken)
const foreignBridge = new foreignWeb3.eth.Contract(FOREIGN_ERC_TO_NATIVE_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS)
const erc677Token = new homeWeb3.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, ercToErcBridge.homeToken)
describe('erc to erc', () => {
@@ -24,9 +25,49 @@ describe('erc to erc', () => {
const balance = await erc20Token.methods.balanceOf(user.address).call()
assert(!toBN(balance).isZero(), 'Account should have tokens')
const firstTransferValue = homeWeb3.utils.toWei('0.01')
// approve tokens to foreign bridge
await erc20Token.methods
.approve(COMMON_FOREIGN_BRIDGE_ADDRESS, firstTransferValue)
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
// call bridge method to transfer tokens to a different recipient
await foreignBridge.methods
.relayTokens(secondUser.address, firstTransferValue)
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async retry => {
const balance = await erc677Token.methods.balanceOf(user.address).call()
const recipientBalance = await erc677Token.methods.balanceOf(secondUser.address).call()
assert(toBN(balance).isZero(), 'User balance should be the same')
if (toBN(recipientBalance).isZero()) {
retry()
}
})
const secondTransferValue = homeWeb3.utils.toWei('0.05')
// send tokens to foreign bridge
await erc20Token.methods
.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, homeWeb3.utils.toWei('0.01'))
.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, secondTransferValue)
.send({
from: user.address,
gas: '1000000'
@@ -44,6 +85,8 @@ describe('erc to erc', () => {
const balance = await erc677Token.methods.balanceOf(user.address).call()
if (toBN(balance).isZero()) {
retry()
} else {
assert(toBN(balance).eq(toBN(secondTransferValue)), 'User balance should be increased only by second transfer')
}
})
})

View File

@@ -1,8 +1,8 @@
const Web3 = require('web3')
const assert = require('assert')
const promiseRetry = require('promise-retry')
const { user, ercToNativeBridge, homeRPC, foreignRPC } = require('../../e2e-commons/constants.json')
const { ERC677_BRIDGE_TOKEN_ABI } = require('../../commons')
const { user, secondUser, ercToNativeBridge, homeRPC, foreignRPC } = require('../../e2e-commons/constants.json')
const { ERC677_BRIDGE_TOKEN_ABI, FOREIGN_ERC_TO_NATIVE_ABI } = require('../../commons')
const { generateNewBlock } = require('../../e2e-commons/utils')
const homeWeb3 = new Web3(new Web3.providers.HttpProvider(homeRPC.URL))
@@ -17,16 +17,56 @@ homeWeb3.eth.accounts.wallet.add(user.privateKey)
foreignWeb3.eth.accounts.wallet.add(user.privateKey)
const erc20Token = new foreignWeb3.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, ercToNativeBridge.foreignToken)
const foreignBridge = new foreignWeb3.eth.Contract(FOREIGN_ERC_TO_NATIVE_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS)
describe('erc to native', () => {
it('should convert tokens in foreign to coins in home', async () => {
const balance = await erc20Token.methods.balanceOf(user.address).call()
const originalBalanceOnHome = await homeWeb3.eth.getBalance(user.address)
const initialBalanceSecondUser = await homeWeb3.eth.getBalance(secondUser.address)
assert(!toBN(balance).isZero(), 'Account should have tokens')
// approve tokens to foreign bridge
await erc20Token.methods
.approve(COMMON_FOREIGN_BRIDGE_ADDRESS, homeWeb3.utils.toWei('0.01'))
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
// call bridge method to transfer tokens to a different recipient
await foreignBridge.methods
.relayTokens(secondUser.address, homeWeb3.utils.toWei('0.01'))
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async retry => {
const balance = await homeWeb3.eth.getBalance(user.address)
const secondUserbalance = await homeWeb3.eth.getBalance(secondUser.address)
assert(toBN(balance).lte(toBN(originalBalanceOnHome)), 'User balance should be the same')
if (toBN(secondUserbalance).lte(toBN(initialBalanceSecondUser))) {
retry()
}
})
const transferValue = homeWeb3.utils.toWei('0.05')
// send tokens to foreign bridge
await erc20Token.methods
.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, homeWeb3.utils.toWei('0.01'))
.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, transferValue)
.send({
from: user.address,
gas: '1000000'
@@ -44,6 +84,11 @@ describe('erc to native', () => {
const balance = await homeWeb3.eth.getBalance(user.address)
if (toBN(balance).lte(toBN(originalBalanceOnHome))) {
retry()
} else {
assert(
toBN(balance).eq(toBN(originalBalanceOnHome).add(toBN(transferValue))),
'User balance should be increased only by second transfer'
)
}
})
})

View File

@@ -10,7 +10,7 @@ The Oracle is deployed on specified validator nodes (only nodes whose private ke
## Architecture
### Native-to-ERC20
### Native-to-ERC20 and Arbitrary-Message
![Native-to-ERC](Native-to-ERC.png)
@@ -27,7 +27,7 @@ There are three Watchers:
- **Signature Request Watcher**: Listens to `UserRequestForSignature` events on the Home network.
- **Collected Signatures Watcher**: Listens to `CollectedSignatures` events on the Home network.
- **Affirmation Request Watcher**: Depends on the bridge mode.
- `Native-to-ERC20`: Listens to `UserRequestForAffirmation` raised by the bridge contract.
- `Native-to-ERC20` and `Arbitrary-Message`: Listens to `UserRequestForAffirmation` raised by the bridge contract.
- `ERC20-to-ERC20` and `ERC20-to-Native`: Listens to `Transfer` events raised by the token contract.
### Sender
@@ -53,7 +53,7 @@ For more information on the Redis/RabbitMQ requirements, see [#90](/../../issues
**Note:** The following steps detail the bridge deployment process for development and testing. For deployment in a production environment we recommend using the [Bridge Deployment Playbooks](../deployment/README.md).
1. [Deploy the bridge contracts](https://github.com/poanetwork/poa-bridge-contracts/blob/master/deploy/README.md)
1. [Deploy the bridge contracts](https://github.com/poanetwork/tokenbridge-contracts/blob/master/deploy/README.md)
2. Open `bridgeDeploymentResults.json` or copy the JSON output generated by the bridge contract deployment process.
@@ -232,10 +232,10 @@ When running the processes, the following commands can be used to test functiona
| `USER_ADDRESS` | An account - the current owner of coins/tokens. |
| `USER_ADDRESS_PRIVATE_KEY` | A private key belonging to the account. |
| `COMMON_HOME_BRIDGE_ADDRESS` | Address of the bridge in the Home network to send transactions. |
| `HOME_MIN_AMOUNT_PER_TX` | Value (in _eth_ or tokens) to be sent in one transaction for the Home network. This should be greater than or equal to the value specified in the `poa-bridge-contracts/deploy/.env` file. The default value in that file is 500000000000000000, which is equivalent to 0.5. |
| `HOME_MIN_AMOUNT_PER_TX` | Value (in _eth_ or tokens) to be sent in one transaction for the Home network. This should be greater than or equal to the value specified in the `tokenbridge-contracts/deploy/.env` file. The default value in that file is 500000000000000000, which is equivalent to 0.5. |
| `HOME_TEST_TX_GAS_PRICE` | The gas price (in Wei) that is used to send transactions in the Home network . |
| `COMMON_FOREIGN_BRIDGE_ADDRESS` | Address of the bridge in the Foreign network to send transactions. |
| `FOREIGN_MIN_AMOUNT_PER_TX` | Value (in _eth_ or tokens) to be sent in one transaction for the Foreign network. This should be greater than or equal to the value specified in the `poa-bridge-contracts/deploy/.env` file. The default value in that file is 500000000000000000, which is equivalent to 0.5. |
| `FOREIGN_MIN_AMOUNT_PER_TX` | Value (in _eth_ or tokens) to be sent in one transaction for the Foreign network. This should be greater than or equal to the value specified in the `tokenbridge-contracts/deploy/.env` file. The default value in that file is 500000000000000000, which is equivalent to 0.5. |
| `FOREIGN_TEST_TX_GAS_PRICE` | The gas price (in Wei) that is used to send transactions in the Foreign network . |
## Contributing

View File

@@ -1,5 +1,5 @@
const baseConfig = require('./base.config')
const { ERC20_ABI, ERC_TYPES } = require('../../commons')
const { ERC_TYPES } = require('../../commons')
const initialChecksJson = process.argv[3]
@@ -20,24 +20,11 @@ if (baseConfig.id === 'erc-erc' && initialChecks.foreignERC === ERC_TYPES.ERC677
const id = `${baseConfig.id}-affirmation-request`
module.exports =
(baseConfig.id === 'erc-erc' && initialChecks.foreignERC === ERC_TYPES.ERC20) || baseConfig.id === 'erc-native'
? {
...baseConfig.bridgeConfig,
...baseConfig.foreignConfig,
event: 'Transfer',
eventContractAddress: initialChecks.bridgeableTokenAddress,
eventAbi: ERC20_ABI,
eventFilter: { to: process.env.COMMON_FOREIGN_BRIDGE_ADDRESS },
queue: 'home',
name: `watcher-${id}`,
id
}
: {
...baseConfig.bridgeConfig,
...baseConfig.foreignConfig,
event: 'UserRequestForAffirmation',
queue: 'home',
name: `watcher-${id}`,
id
}
module.exports = {
...baseConfig.bridgeConfig,
...baseConfig.foreignConfig,
event: 'UserRequestForAffirmation',
queue: 'home',
name: `watcher-${id}`,
id
}

View File

@@ -8,7 +8,9 @@ const {
HOME_ERC_TO_ERC_ABI,
FOREIGN_ERC_TO_ERC_ABI,
HOME_ERC_TO_NATIVE_ABI,
FOREIGN_ERC_TO_NATIVE_ABI
FOREIGN_ERC_TO_NATIVE_ABI,
HOME_AMB_ABI,
FOREIGN_AMB_ABI
} = require('../../commons')
const { web3Home, web3Foreign } = require('../src/services/web3')
const { privateKeyToAddress } = require('../src/utils/utils')
@@ -35,6 +37,11 @@ switch (process.env.ORACLE_BRIDGE_MODE) {
foreignAbi = FOREIGN_ERC_TO_NATIVE_ABI
id = 'erc-native'
break
case BRIDGE_MODES.ARBITRARY_MESSAGE:
homeAbi = HOME_AMB_ABI
foreignAbi = FOREIGN_AMB_ABI
id = 'amb'
break
default:
if (process.env.NODE_ENV !== 'test') {
throw new Error(`Bridge Mode: ${process.env.ORACLE_BRIDGE_MODE} not supported.`)

View File

@@ -0,0 +1,42 @@
const baseConfig = require('./base.config')
const { ERC20_ABI, ERC_TYPES } = 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.')
}
if (baseConfig.id === 'erc-erc' && initialChecks.foreignERC === ERC_TYPES.ERC677) {
baseConfig.id = 'erc677-erc677'
}
const id = `${baseConfig.id}-transfer`
const transferWatcherRequired =
(baseConfig.id === 'erc-erc' && initialChecks.foreignERC === ERC_TYPES.ERC20) || baseConfig.id === 'erc-native'
if (!transferWatcherRequired) {
console.error(`Transfer watcher not required for bridge mode ${process.env.ORACLE_BRIDGE_MODE}`)
process.exit(EXIT_CODES.WATCHER_NOT_REQUIRED)
}
module.exports = {
...baseConfig.bridgeConfig,
...baseConfig.foreignConfig,
event: 'Transfer',
eventContractAddress: initialChecks.bridgeableTokenAddress,
eventAbi: ERC20_ABI,
eventFilter: { to: process.env.COMMON_FOREIGN_BRIDGE_ADDRESS },
queue: 'home',
name: `watcher-${id}`,
id
}

View File

@@ -0,0 +1,91 @@
---
version: '2.4'
services:
rabbit:
extends:
file: docker-compose.yml
service: rabbit
networks:
- net_rabbit_bridge_transfer
redis:
extends:
file: docker-compose.yml
service: redis
networks:
- net_db_bridge_transfer
bridge_request:
extends:
file: docker-compose.yml
service: bridge_request
networks:
- net_db_bridge_request
- net_rabbit_bridge_request
bridge_collected:
extends:
file: docker-compose.yml
service: bridge_collected
networks:
- net_db_bridge_request
- net_rabbit_bridge_request
bridge_affirmation:
extends:
file: docker-compose.yml
service: bridge_affirmation
networks:
- net_db_bridge_request
- net_rabbit_bridge_request
bridge_transfer:
cpus: 0.1
mem_limit: 500m
build:
context: ..
dockerfile: oracle/Dockerfile
env_file: ./.env
environment:
- NODE_ENV=production
- ORACLE_VALIDATOR_ADDRESS=${ORACLE_VALIDATOR_ADDRESS}
restart: unless-stopped
entrypoint: yarn watcher:transfer
networks:
- net_db_bridge_transfer
- net_rabbit_bridge_transfer
bridge_senderhome:
extends:
file: docker-compose.yml
service: bridge_senderhome
networks:
- net_db_bridge_request
- net_rabbit_bridge_request
bridge_senderforeign:
extends:
file: docker-compose.yml
service: bridge_senderforeign
networks:
- net_db_bridge_request
- net_rabbit_bridge_request
networks:
net_db_bridge_request:
driver: bridge
net_db_bridge_collected:
driver: bridge
net_db_bridge_affirmation:
driver: bridge
net_db_bridge_transfer:
driver: bridge
net_db_bridge_senderhome:
driver: bridge
net_db_bridge_senderforeign:
driver: bridge
net_rabbit_bridge_request:
driver: bridge
net_rabbit_bridge_collected:
driver: bridge
net_rabbit_bridge_affirmation:
driver: bridge
net_rabbit_bridge_transfer:
driver: bridge
net_rabbit_bridge_senderhome:
driver: bridge
net_rabbit_bridge_senderforeign:
driver: bridge

View File

@@ -8,9 +8,10 @@
"watcher:signature-request": "./scripts/start-worker.sh watcher signature-request-watcher",
"watcher:collected-signatures": "./scripts/start-worker.sh watcher collected-signatures-watcher",
"watcher:affirmation-request": "./scripts/start-worker.sh watcher affirmation-request-watcher",
"watcher:transfer": "./scripts/start-worker.sh watcher transfer-watcher",
"sender:home": "./scripts/start-worker.sh sender home-sender",
"sender:foreign": "./scripts/start-worker.sh sender foreign-sender",
"dev": "concurrently -n 'watcher:signature-request,watcher:collected-signatures,watcher:affirmation-request,sender:home,sender:foreign' -c 'red,green,yellow,blue,magenta' 'yarn watcher:signature-request' 'yarn watcher:collected-signatures' 'yarn watcher:affirmation-request' 'yarn sender:home' 'yarn sender:foreign'",
"dev": "concurrently -n 'watcher:signature-request,watcher:collected-signatures,watcher:affirmation-request,watcher:transfer,sender:home,sender:foreign' -c 'red,green,yellow,blue,magenta,cyan' 'yarn watcher:signature-request' 'yarn watcher:collected-signatures' 'yarn watcher:affirmation-request' 'yarn watcher:transfer' 'yarn sender:home' 'yarn sender:foreign'",
"test": "NODE_ENV=test mocha",
"test:watch": "NODE_ENV=test mocha --watch --reporter=min",
"coverage": "NODE_ENV=test nyc --reporter=text --reporter=html mocha",

View File

@@ -0,0 +1,51 @@
const { HttpListProviderError } = require('http-list-provider')
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
const logger = require('../../services/logger').child({
module: 'processAffirmationRequests:estimateGas'
})
async function estimateGas({ web3, homeBridge, validatorContract, message, address }) {
try {
const gasEstimate = await homeBridge.methods.executeAffirmation(message).estimateGas({
from: address
})
return gasEstimate
} catch (e) {
if (e instanceof HttpListProviderError) {
throw e
}
const messageHash = web3.utils.soliditySha3(message)
const senderHash = web3.utils.soliditySha3(address, messageHash)
// Check if minimum number of validations was already reached
logger.debug('Check if minimum number of validations was already reached')
const numAffirmationsSigned = await homeBridge.methods.numAffirmationsSigned(messageHash).call()
const alreadyProcessed = await homeBridge.methods.isAlreadyProcessed(numAffirmationsSigned).call()
if (alreadyProcessed) {
throw new AlreadyProcessedError(e.message)
}
// Check if the message was already signed by this validator
logger.debug('Check if the message was already signed')
const alreadySigned = await homeBridge.methods.affirmationsSigned(senderHash).call()
if (alreadySigned) {
throw new AlreadySignedError(e.message)
}
// Check if address is validator
logger.debug('Check if address is a validator')
const isValidator = await validatorContract.methods.isValidator(address).call()
if (!isValidator) {
throw new InvalidValidatorError(`${address} is not a validator`)
}
throw new Error('Unknown error while processing message')
}
}
module.exports = estimateGas

View File

@@ -0,0 +1,95 @@
require('dotenv').config()
const { HttpListProviderError } = require('http-list-provider')
const promiseLimit = require('promise-limit')
const rootLogger = require('../../services/logger')
const { web3Home } = require('../../services/web3')
const bridgeValidatorsABI = require('../../../../contracts/build/contracts/BridgeValidators').abi
const { EXIT_CODES, MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
const estimateGas = require('./estimateGas')
const { addTxHashToData, parseAMBMessage } = require('../../../../commons')
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
let validatorContract = null
function processAffirmationRequestsBuilder(config) {
const homeBridge = new web3Home.eth.Contract(config.homeBridgeAbi, config.homeBridgeAddress)
return async function processAffirmationRequests(affirmationRequests) {
const txToSend = []
if (validatorContract === null) {
rootLogger.debug('Getting validator contract address')
const validatorContractAddress = await homeBridge.methods.validatorContract().call()
rootLogger.debug({ validatorContractAddress }, 'Validator contract address obtained')
validatorContract = new web3Home.eth.Contract(bridgeValidatorsABI, validatorContractAddress)
}
rootLogger.debug(`Processing ${affirmationRequests.length} AffirmationRequest events`)
const callbacks = affirmationRequests
.map(affirmationRequest => async () => {
const { encodedData } = affirmationRequest.returnValues
const logger = rootLogger.child({
eventTransactionHash: affirmationRequest.transactionHash
})
const message = addTxHashToData({
encodedData,
transactionHash: affirmationRequest.transactionHash
})
const { sender, executor } = parseAMBMessage(message)
logger.info({ sender, executor }, `Processing affirmationRequest ${affirmationRequest.transactionHash}`)
let gasEstimate
try {
logger.debug('Estimate gas')
gasEstimate = await estimateGas({
web3: web3Home,
homeBridge,
validatorContract,
message,
address: config.validatorAddress
})
logger.debug({ gasEstimate }, 'Gas estimated')
} catch (e) {
if (e instanceof HttpListProviderError) {
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
} else if (e instanceof InvalidValidatorError) {
logger.fatal({ address: config.validatorAddress }, 'Invalid validator')
process.exit(EXIT_CODES.INCOMPATIBILITY)
} else if (e instanceof AlreadySignedError) {
logger.info(`Already signed affirmationRequest ${affirmationRequest.transactionHash}`)
return
} else if (e instanceof AlreadyProcessedError) {
logger.info(
`affirmationRequest ${affirmationRequest.transactionHash} was already processed by other validators`
)
return
} else {
logger.error(e, 'Unknown error while processing transaction')
throw e
}
}
const data = await homeBridge.methods.executeAffirmation(message).encodeABI()
txToSend.push({
data,
gasEstimate,
transactionReference: affirmationRequest.transactionHash,
to: config.homeBridgeAddress
})
})
.map(promise => limit(promise))
await Promise.all(callbacks)
return txToSend
}
}
module.exports = processAffirmationRequestsBuilder

View File

@@ -0,0 +1,62 @@
const Web3 = require('web3')
const { HttpListProviderError } = require('http-list-provider')
const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors')
const logger = require('../../services/logger').child({
module: 'processCollectedSignatures:estimateGas'
})
const web3 = new Web3()
const { toBN } = Web3.utils
async function estimateGas({
foreignBridge,
validatorContract,
message,
numberOfCollectedSignatures,
v,
r,
s,
signatures,
txHash,
address
}) {
try {
const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({
from: address
})
return gasEstimate
} catch (e) {
if (e instanceof HttpListProviderError) {
throw e
}
// check if the message was already processed
logger.debug('Check if the message was already processed')
const alreadyProcessed = await foreignBridge.methods.relayedMessages(txHash).call()
if (alreadyProcessed) {
throw new AlreadyProcessedError()
}
// check if the number of signatures is enough
logger.debug('Check if number of signatures is enough')
const requiredSignatures = await validatorContract.methods.requiredSignatures().call()
if (toBN(requiredSignatures).gt(toBN(numberOfCollectedSignatures))) {
throw new IncompatibleContractError('The number of collected signatures does not match')
}
// check if all the signatures were made by validators
for (let i = 0; i < v.length; i++) {
const address = web3.eth.accounts.recover(message, web3.utils.toHex(v[i]), r[i], s[i])
logger.debug({ address }, 'Check that signature is from a validator')
const isValidator = await validatorContract.methods.isValidator(address).call()
if (!isValidator) {
throw new InvalidValidatorError(`Message signed by ${address} that is not a validator`)
}
}
throw new Error('Unknown error while processing message')
}
}
module.exports = estimateGas

View File

@@ -0,0 +1,118 @@
require('dotenv').config()
const promiseLimit = require('promise-limit')
const { HttpListProviderError } = require('http-list-provider')
const bridgeValidatorsABI = require('../../../../contracts/build/contracts/BridgeValidators').abi
const rootLogger = require('../../services/logger')
const { web3Home, web3Foreign } = require('../../services/web3')
const { signatureToVRS, signatureToVRSAMB, packSignatures } = require('../../utils/message')
const { parseAMBMessage } = require('../../../../commons')
const estimateGas = require('./estimateGas')
const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors')
const { MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
let validatorContract = null
function processCollectedSignaturesBuilder(config) {
const homeBridge = new web3Home.eth.Contract(config.homeBridgeAbi, config.homeBridgeAddress)
const foreignBridge = new web3Foreign.eth.Contract(config.foreignBridgeAbi, config.foreignBridgeAddress)
return async function processCollectedSignatures(signatures) {
const txToSend = []
if (validatorContract === null) {
rootLogger.debug('Getting validator contract address')
const validatorContractAddress = await foreignBridge.methods.validatorContract().call()
rootLogger.debug({ validatorContractAddress }, 'Validator contract address obtained')
validatorContract = new web3Foreign.eth.Contract(bridgeValidatorsABI, validatorContractAddress)
}
rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`)
const callbacks = signatures
.map(colSignature => async () => {
const { authorityResponsibleForRelay, messageHash, NumberOfCollectedSignatures } = colSignature.returnValues
const logger = rootLogger.child({
eventTransactionHash: colSignature.transactionHash
})
if (authorityResponsibleForRelay !== web3Home.utils.toChecksumAddress(config.validatorAddress)) {
logger.info(`Validator not responsible for relaying CollectedSignatures ${colSignature.transactionHash}`)
return
}
logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`)
const message = await homeBridge.methods.message(messageHash).call()
const requiredSignatures = new Array(NumberOfCollectedSignatures).fill(0)
const signaturesArray = []
const [v, r, s] = [[], [], []]
logger.debug('Getting message signatures')
const signaturePromises = requiredSignatures.map(async (el, index) => {
logger.debug({ index }, 'Getting message signature')
const signature = await homeBridge.methods.signature(messageHash, index).call()
const vrs = signatureToVRS(signature)
v.push(vrs.v)
r.push(vrs.r)
s.push(vrs.s)
const recover = signatureToVRSAMB(signature)
signaturesArray.push(recover)
})
await Promise.all(signaturePromises)
const signatures = packSignatures(signaturesArray)
const { txHash } = parseAMBMessage(message)
let gasEstimate
try {
logger.debug('Estimate gas')
gasEstimate = await estimateGas({
foreignBridge,
validatorContract,
v,
r,
s,
signatures,
message,
numberOfCollectedSignatures: NumberOfCollectedSignatures,
txHash,
address: config.validatorAddress
})
logger.debug({ gasEstimate }, 'Gas estimated')
} catch (e) {
if (e instanceof HttpListProviderError) {
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
} else if (e instanceof AlreadyProcessedError) {
logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`)
return
} else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) {
logger.error(`The message couldn't be processed; skipping: ${e.message}`)
return
} else {
logger.error(e, 'Unknown error while processing transaction')
throw e
}
}
const data = await foreignBridge.methods.executeSignatures(message, signatures).encodeABI()
txToSend.push({
data,
gasEstimate,
transactionReference: colSignature.transactionHash,
to: config.foreignBridgeAddress
})
})
.map(promise => limit(promise))
await Promise.all(callbacks)
return txToSend
}
}
module.exports = processCollectedSignaturesBuilder

View File

@@ -0,0 +1,99 @@
require('dotenv').config()
const promiseLimit = require('promise-limit')
const { HttpListProviderError } = require('http-list-provider')
const bridgeValidatorsABI = require('../../../../contracts/build/contracts/BridgeValidators').abi
const rootLogger = require('../../services/logger')
const { web3Home } = require('../../services/web3')
const { addTxHashToData, parseAMBMessage } = require('../../../../commons')
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)
let validatorContract = null
function processSignatureRequestsBuilder(config) {
const homeBridge = new web3Home.eth.Contract(config.homeBridgeAbi, config.homeBridgeAddress)
return async function processSignatureRequests(signatureRequests) {
const txToSend = []
if (validatorContract === null) {
rootLogger.debug('Getting validator contract address')
const validatorContractAddress = await homeBridge.methods.validatorContract().call()
rootLogger.debug({ validatorContractAddress }, 'Validator contract address obtained')
validatorContract = new web3Home.eth.Contract(bridgeValidatorsABI, validatorContractAddress)
}
rootLogger.debug(`Processing ${signatureRequests.length} SignatureRequest events`)
const callbacks = signatureRequests
.map(signatureRequest => async () => {
const { encodedData } = signatureRequest.returnValues
const logger = rootLogger.child({
eventTransactionHash: signatureRequest.transactionHash
})
const message = addTxHashToData({
encodedData,
transactionHash: signatureRequest.transactionHash
})
const { sender, executor } = parseAMBMessage(message)
logger.info({ sender, executor }, `Processing signatureRequest ${signatureRequest.transactionHash}`)
const signature = web3Home.eth.accounts.sign(message, `0x${ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY}`)
let gasEstimate
try {
logger.debug('Estimate gas')
gasEstimate = await estimateGas({
web3: web3Home,
homeBridge,
validatorContract,
signature: signature.signature,
message,
address: config.validatorAddress
})
logger.debug({ gasEstimate }, 'Gas estimated')
} catch (e) {
if (e instanceof HttpListProviderError) {
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
} else if (e instanceof InvalidValidatorError) {
logger.fatal({ address: config.validatorAddress }, 'Invalid validator')
process.exit(EXIT_CODES.INCOMPATIBILITY)
} else if (e instanceof AlreadySignedError) {
logger.info(`Already signed signatureRequest ${signatureRequest.transactionHash}`)
return
} else if (e instanceof AlreadyProcessedError) {
logger.info(
`signatureRequest ${signatureRequest.transactionHash} was already processed by other validators`
)
return
} else {
logger.error(e, 'Unknown error while processing transaction')
throw e
}
}
const data = await homeBridge.methods.submitSignature(signature.signature, message).encodeABI()
txToSend.push({
data,
gasEstimate,
transactionReference: signatureRequest.transactionHash,
to: config.homeBridgeAddress
})
})
.map(promise => limit(promise))
await Promise.all(callbacks)
return txToSend
}
}
module.exports = processSignatureRequestsBuilder

View File

@@ -28,8 +28,8 @@ function processAffirmationRequestsBuilder(config) {
}
rootLogger.debug(`Processing ${affirmationRequests.length} AffirmationRequest events`)
const callbacks = affirmationRequests.map(affirmationRequest =>
limit(async () => {
const callbacks = affirmationRequests
.map(affirmationRequest => async () => {
const { recipient, value } = affirmationRequest.returnValues
const logger = rootLogger.child({
@@ -82,7 +82,7 @@ function processAffirmationRequestsBuilder(config) {
to: config.homeBridgeAddress
})
})
)
.map(promise => limit(promise))
await Promise.all(callbacks)
return txToSend

View File

@@ -30,74 +30,73 @@ function processCollectedSignaturesBuilder(config) {
}
rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`)
const callbacks = signatures.map(colSignature =>
limit(async () => {
const callbacks = signatures
.map(colSignature => async () => {
const { authorityResponsibleForRelay, messageHash, NumberOfCollectedSignatures } = colSignature.returnValues
const logger = rootLogger.child({
eventTransactionHash: colSignature.transactionHash
})
if (authorityResponsibleForRelay === web3Home.utils.toChecksumAddress(config.validatorAddress)) {
logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`)
const message = await homeBridge.methods.message(messageHash).call()
const requiredSignatures = []
requiredSignatures.length = NumberOfCollectedSignatures
requiredSignatures.fill(0)
const [v, r, s] = [[], [], []]
logger.debug('Getting message signatures')
const signaturePromises = requiredSignatures.map(async (el, index) => {
logger.debug({ index }, 'Getting message signature')
const signature = await homeBridge.methods.signature(messageHash, index).call()
const recover = signatureToVRS(signature)
v.push(recover.v)
r.push(recover.r)
s.push(recover.s)
})
await Promise.all(signaturePromises)
let gasEstimate
try {
logger.debug('Estimate gas')
gasEstimate = await estimateGas({
foreignBridge,
validatorContract,
v,
r,
s,
message,
numberOfCollectedSignatures: NumberOfCollectedSignatures
})
logger.debug({ gasEstimate }, 'Gas estimated')
} catch (e) {
if (e instanceof HttpListProviderError) {
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
} else if (e instanceof AlreadyProcessedError) {
logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`)
return
} else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) {
logger.error(`The message couldn't be processed; skipping: ${e.message}`)
return
} else {
logger.error(e, 'Unknown error while processing transaction')
throw e
}
}
const data = await foreignBridge.methods.executeSignatures(v, r, s, message).encodeABI()
txToSend.push({
data,
gasEstimate,
transactionReference: colSignature.transactionHash,
to: config.foreignBridgeAddress
})
} else {
if (authorityResponsibleForRelay !== web3Home.utils.toChecksumAddress(config.validatorAddress)) {
logger.info(`Validator not responsible for relaying CollectedSignatures ${colSignature.transactionHash}`)
return
}
logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`)
const message = await homeBridge.methods.message(messageHash).call()
const requiredSignatures = new Array(NumberOfCollectedSignatures).fill(0)
const [v, r, s] = [[], [], []]
logger.debug('Getting message signatures')
const signaturePromises = requiredSignatures.map(async (el, index) => {
logger.debug({ index }, 'Getting message signature')
const signature = await homeBridge.methods.signature(messageHash, index).call()
const recover = signatureToVRS(signature)
v.push(recover.v)
r.push(recover.r)
s.push(recover.s)
})
await Promise.all(signaturePromises)
let gasEstimate
try {
logger.debug('Estimate gas')
gasEstimate = await estimateGas({
foreignBridge,
validatorContract,
v,
r,
s,
message,
numberOfCollectedSignatures: NumberOfCollectedSignatures
})
logger.debug({ gasEstimate }, 'Gas estimated')
} catch (e) {
if (e instanceof HttpListProviderError) {
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
} else if (e instanceof AlreadyProcessedError) {
logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`)
return
} else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) {
logger.error(`The message couldn't be processed; skipping: ${e.message}`)
return
} else {
logger.error(e, 'Unknown error while processing transaction')
throw e
}
}
const data = await foreignBridge.methods.executeSignatures(v, r, s, message).encodeABI()
txToSend.push({
data,
gasEstimate,
transactionReference: colSignature.transactionHash,
to: config.foreignBridgeAddress
})
})
)
.map(promise => limit(promise))
await Promise.all(callbacks)

View File

@@ -35,8 +35,8 @@ function processSignatureRequestsBuilder(config) {
}
rootLogger.debug(`Processing ${signatureRequests.length} SignatureRequest events`)
const callbacks = signatureRequests.map(signatureRequest =>
limit(async () => {
const callbacks = signatureRequests
.map(signatureRequest => async () => {
const { recipient, value } = signatureRequest.returnValues
const logger = rootLogger.child({
@@ -98,7 +98,7 @@ function processSignatureRequestsBuilder(config) {
to: config.homeBridgeAddress
})
})
)
.map(promise => limit(promise))
await Promise.all(callbacks)
return txToSend

View File

@@ -1,9 +1,9 @@
require('../../../env')
const promiseLimit = require('promise-limit')
const { HttpListProviderError } = require('http-list-provider')
const { BRIDGE_VALIDATORS_ABI } = require('../../../../commons')
const { BRIDGE_VALIDATORS_ABI, ZERO_ADDRESS } = require('../../../../commons')
const rootLogger = require('../../services/logger')
const { web3Home } = require('../../services/web3')
const { web3Home, web3Foreign } = require('../../services/web3')
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
const { EXIT_CODES, MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
const estimateGas = require('../processAffirmationRequests/estimateGas')
@@ -14,6 +14,12 @@ let validatorContract = null
function processTransfersBuilder(config) {
const homeBridge = new web3Home.eth.Contract(config.homeBridgeAbi, config.homeBridgeAddress)
const userRequestForAffirmationAbi = config.foreignBridgeAbi.filter(
e => e.type === 'event' && e.name === 'UserRequestForAffirmation'
)[0]
const tokensSwappedAbi = config.foreignBridgeAbi.filter(e => e.type === 'event' && e.name === 'TokensSwapped')[0]
const userRequestForAffirmationHash = web3Home.eth.abi.encodeEventSignature(userRequestForAffirmationAbi)
const tokensSwappedHash = tokensSwappedAbi ? web3Home.eth.abi.encodeEventSignature(tokensSwappedAbi) : '0x'
return async function processTransfers(transfers) {
const txToSend = []
@@ -27,8 +33,8 @@ function processTransfersBuilder(config) {
}
rootLogger.debug(`Processing ${transfers.length} Transfer events`)
const callbacks = transfers.map(transfer =>
limit(async () => {
const callbacks = transfers
.map(transfer => async () => {
const { from, value } = transfer.returnValues
const logger = rootLogger.child({
@@ -37,6 +43,32 @@ function processTransfersBuilder(config) {
logger.info({ from, value }, `Processing transfer ${transfer.transactionHash}`)
const receipt = await web3Foreign.eth.getTransactionReceipt(transfer.transactionHash)
const existsAffirmationEvent = receipt.logs.some(
e => e.address === config.foreignBridgeAddress && e.topics[0] === userRequestForAffirmationHash
)
if (existsAffirmationEvent) {
logger.info(
`Transfer event discarded because a transaction with alternative receiver detected in transaction ${
transfer.transactionHash
}`
)
return
}
const existsTokensSwappedEvent = tokensSwappedAbi
? receipt.logs.some(e => e.address === config.foreignBridgeAddress && e.topics[0] === tokensSwappedHash)
: false
if (from === ZERO_ADDRESS && existsTokensSwappedEvent) {
logger.info(
`Transfer event discarded because token swap is detected in transaction ${transfer.transactionHash}`
)
return
}
let gasEstimate
try {
logger.debug('Estimate gas')
@@ -79,7 +111,7 @@ function processTransfersBuilder(config) {
to: config.homeBridgeAddress
})
})
)
.map(promise => limit(promise))
await Promise.all(callbacks)
return txToSend

View File

@@ -11,6 +11,7 @@ module.exports = {
DEFAULT_GAS_PRICE_FACTOR: 1,
EXIT_CODES: {
GENERAL_ERROR: 1,
WATCHER_NOT_REQUIRED: 0,
INCOMPATIBILITY: 10,
MAX_TIME_REACHED: 11
},

View File

@@ -1,10 +1,6 @@
const assert = require('assert')
const Web3Utils = require('web3-utils')
// strips leading "0x" if present
function strip0x(input) {
return input.replace(/^0x/, '')
}
const { strip0x } = require('../../../commons')
function createMessage({ recipient, value, transactionHash, bridgeAddress, expectedMessageLength }) {
recipient = strip0x(recipient)
@@ -63,8 +59,32 @@ function signatureToVRS(signature) {
return { v, r, s }
}
function signatureToVRSAMB(rawSignature) {
const signature = strip0x(rawSignature)
const v = signature.substr(64 * 2)
const r = signature.substr(0, 32 * 2)
const s = signature.substr(32 * 2, 32 * 2)
return { v, r, s }
}
function packSignatures(array) {
const length = strip0x(Web3Utils.toHex(array.length))
const msgLength = length.length === 1 ? `0${length}` : length
let v = ''
let r = ''
let s = ''
array.forEach(e => {
v = v.concat(e.v)
r = r.concat(e.r)
s = s.concat(e.s)
})
return `0x${msgLength}${v}${r}${s}`
}
module.exports = {
createMessage,
parseMessage,
signatureToVRS
signatureToVRS,
signatureToVRSAMB,
packSignatures
}

View File

@@ -21,6 +21,9 @@ const processSignatureRequests = require('./events/processSignatureRequests')(co
const processCollectedSignatures = require('./events/processCollectedSignatures')(config)
const processAffirmationRequests = require('./events/processAffirmationRequests')(config)
const processTransfers = require('./events/processTransfers')(config)
const processAMBSignatureRequests = require('./events/processAMBSignatureRequests')(config)
const processAMBCollectedSignatures = require('./events/processAMBCollectedSignatures')(config)
const processAMBAffirmationRequests = require('./events/processAMBAffirmationRequests')(config)
const ZERO = toBN(0)
const ONE = toBN(1)
@@ -93,10 +96,18 @@ function processEvents(events) {
return processCollectedSignatures(events)
case 'native-erc-affirmation-request':
case 'erc677-erc677-affirmation-request':
return processAffirmationRequests(events)
case 'erc-erc-affirmation-request':
case 'erc-native-affirmation-request':
case 'erc-erc-affirmation-request':
return processAffirmationRequests(events)
case 'erc-erc-transfer':
case 'erc-native-transfer':
return processTransfers(events)
case 'amb-signature-request':
return processAMBSignatureRequests(events)
case 'amb-collected-signatures':
return processAMBCollectedSignatures(events)
case 'amb-affirmation-request':
return processAMBAffirmationRequests(events)
default:
return []
}

View File

@@ -36,7 +36,7 @@
"initialize": "yarn clean && git submodule update --init && yarn install --unsafe-perm --frozen-lockfile && yarn install:deploy && yarn compile:contracts",
"build": "yarn workspace ui run build",
"lint": "yarn wsrun --exclude token-bridge-contracts lint",
"test": "yarn wsrun --exclude monitor --exclude oracle-e2e --exclude ui-e2e --exclude monitor-e2e test",
"test": "yarn wsrun --exclude oracle-e2e --exclude ui-e2e --exclude monitor-e2e test",
"oracle-e2e": "./oracle-e2e/run-tests.sh",
"ui-e2e": "./ui-e2e/run-tests.sh",
"clean": "rm -rf ./node_modules ./**/node_modules ./**/**/node_modules ./**/build",

View File

@@ -4,9 +4,9 @@
"license": "MIT",
"private": true,
"devDependencies": {
"selenium-webdriver": "3.6.0",
"chromedriver": "^77.0.0",
"mocha": "^5.2.0",
"chromedriver": "76.0.0"
"selenium-webdriver": "3.6.0"
},
"scripts": {
"lint": "eslint . --ignore-path ../.eslintignore"

View File

@@ -52,7 +52,7 @@ The following is an example setup using the POA Sokol testnet as the Home networ
### Dependencies
- [Smart Contracts](https://github.com/poanetwork/poa-bridge-contracts)
- [Smart Contracts](https://github.com/poanetwork/tokenbridge-contracts)
- [Oracle](../oracle/README.md)
- [Node.js](https://nodejs.org/en/download/)
- [AlphaWallet](https://alphawallet.com/) or [Nifty Wallet](https://github.com/poanetwork/nifty-wallet) or [MetaMask](https://metamask.io/)
@@ -72,8 +72,8 @@ The following is an example setup using the POA Sokol testnet as the Home networ
* Get free Kovan Coins from the [gitter channel](https://gitter.im/kovan-testnet/faucet) or [Iracus faucet](https://github.com/kovan-testnet/faucet) for Foreign account(s). Get 5 Keth to 1 acc, and transfer from there to all other wallets if more than one account is used.
4. Deploy the Sokol <-> Kovan Bridge contracts.
* Go to the the `sokol-kovan-bridge` folder created in step 1 and `git clone https://github.com/poanetwork/poa-bridge-contracts`
* Follow instructions in the [POA Bridge contracts repo](https://github.com/poanetwork/poa-bridge-contracts).
* Go to the the `sokol-kovan-bridge` folder created in step 1 and `git clone https://github.com/poanetwork/tokenbridge-contracts`
* Follow instructions in the [POA Bridge contracts repo](https://github.com/poanetwork/tokenbridge-contracts).
* Set the parameters in the .env file.
* `DEPLOYMENT_ACCOUNT_PRIVATE_KEY`: Export the private key from step 2
* `HOME_RPC_URL`=https://sokol.poa.network
@@ -86,7 +86,7 @@ The following is an example setup using the POA Sokol testnet as the Home networ
* `FOREIGN_UPGRADEABLE_ADMIN_BRIDGE`
* `VALIDATORS` _Note: Wallet address(es) for validator(s) are separated by a space. For testing, you can use the same address that was used as the bridge contracts management account._
* `FOREIGN_RPC_URL`=https://kovan.infura.io/mew
* When deployment is finished, check that the `bridgeDeploymentResults.json` file exists in the `poa-bridge-contracts/deploy` directory and includes the bridge contract addresses.
* When deployment is finished, check that the `bridgeDeploymentResults.json` file exists in the `tokenbridge-contracts/deploy` directory and includes the bridge contract addresses.
5. Install and run the TokenBridge Oracle.
* Go to the `sokol-kovan-bridge` folder
@@ -125,7 +125,7 @@ cp .env.example .env
````
* Insert the addresses from the bridgeDeploymentResults.json file (from step 4) into the .env file. No other changes are needed, see [Env Parameter Details](#env-parameter-details) for information about each parameter.
```
cat ../poa-bridge-contracts/deploy/bridgeDeploymentResults.json
cat ../tokenbridge-contracts/deploy/bridgeDeploymentResults.json
```
```bash

View File

@@ -60,7 +60,7 @@ class GasPriceStore {
@computed
get gasPriceInHex() {
return toHex(this.gasPrice)
return toHex(this.gasPrice.toString())
}
}

View File

@@ -64,7 +64,7 @@ class TxStore {
try {
return this.web3Store.getWeb3Promise.then(async () => {
if (this.web3Store.defaultAccount.address) {
const data = await contract.methods.transferAndCall(to, value, '0x00').encodeABI()
const data = await contract.methods.transferAndCall(to, value, '0x').encodeABI()
return this.doSend({ to: tokenAddress, from, value: '0x00', data, sentValue: value })
} else {
this.alertStore.pushError('Please unlock wallet')

1935
yarn.lock

File diff suppressed because it is too large Load Diff